Skip to content

Instantly share code, notes, and snippets.

@mikejolley
Last active May 17, 2021 13:51
Show Gist options
  • Save mikejolley/347b8f162257f6736c4d to your computer and use it in GitHub Desktop.
Save mikejolley/347b8f162257f6736c4d to your computer and use it in GitHub Desktop.
WooCommerce - Split shipping class items into a new package and limit shipping methods
/**
* This function loops over cart items, and moves any item with shipping class 'special-class' into a new package.
* The new package in this example only takes flat rate shipping.
*/
function split_special_shipping_class_items( $packages ) {
$found_item = false;
$special_class = 'special-class'; // edit this with the slug of your shippig class
$new_package = current( $packages );
$new_package['contents'] = array();
$new_package['contents_cost'] = 0;
$new_package['applied_coupons'] = array();
$new_package['ship_via'] = array( 'flat_rate' ); // Only allow flat rate for items in special class
foreach ( WC()->cart->get_cart() as $item_key => $item ) {
// Is the product in the special class?
if ( $item['data']->needs_shipping() && $special_class === $item['data']->get_shipping_class() ) {
$found_item = true;
$new_package['contents'][ $item_key ] = $item;
$new_package['contents_cost'] += $item['line_total'];
// Remove from original package
$packages[0]['contents_cost'] = $packages[0]['contents_cost'] - $item['line_total'];
unset( $packages[0]['contents'][ $item_key ] );
// If there are no items left in the previous package, remove it completely.
if ( empty( $packages[0]['contents'] ) ) {
unset( $packages[0] );
}
}
}
if ( $found_item ) {
$packages[] = $new_package;
}
return $packages;
}
// Hook into shipping packages filter
add_filter( 'woocommerce_cart_shipping_packages', 'split_special_shipping_class_items' );
@nishitlangaliya
Copy link

@mikejolley,

Thanks for reply :)

Next time I will post it into plugin support.

Thanks again....

@dvelopit
Copy link

Thanks for this code - works great, just wondering if there is a way to rename the new package. The default package is called 'shipping' and the extra one created gets called 'shipping 2'. Would be great if I could rename the new package created to be called 'Shipping is unavailable for the following products'.

Cheers,

@prunly
Copy link

prunly commented Nov 23, 2016

@dvelopit - It so happens I'm currently working on something very similar. The following worked for me...

  1. Add your label to the $new_package array
  2. Use the woocommerce_shipping_package_name filter to change the default label

Putting it all together... Add this code to Mike's snippet on line 13 (above the foreach loop):

$new_package['name'] = 'Shipping is unavailable for the following products'; // your custom label for this package

Then add this function to your functions.php file:

// output the new package name
add_filter( 'woocommerce_shipping_package_name', 'rename_custom_package', 10, 3 );
 
function rename_custom_package( $package_name, $i, $package ) {
 
    if ( ! empty( $package['name'] ) ) {
        $package_name = $package['name'];
    }
 
    return $package_name;
}

@dvelopit
Copy link

@prunly - You are a champion! Worked perfectly - thanks for your help :)

@JimmyAppelt
Copy link

@mikejolley @prunly

Any idea how I would just hide the shipping, instead of returning new packages ?

Lets say I have a product that only is allowed to pickup. if it's in the cart, I would like to hide all shipping methods and only show Local Pickup.

Kind regards

@progbdb
Copy link

progbdb commented Feb 16, 2017

@mikejolley @prunly

please tell me how to solve this proble?
I have some products that only is allowed to pickup. if i select it, then I would like to hide all other shipping methods and only show Local Pickup. And when i choose another shipping method then it should show the all shipping methods what i have selected.

thanx

@ianulpindo
Copy link

@mikejolley @prunly

Thank you very much for the codes. Worked perfectly!

However, just as @JimmyAppelt and @progbdb are requesting, would it be possible to hide all the other shipping methods and only show Local Pickup with a custom message like 'Shipping is unavailable for the following products. Store pickup only.'?

Thank you very much!

@littlelinners
Copy link

littlelinners commented Mar 16, 2017

@ianulpindo @progbdb @JimmeyAppelt

I had the same problem where I wanted to force a flat rate shipping on all products if any one of the products met a certain criteria.
I took the above code and replaced lines 14-30 with the following (and it seems to work ok for me):

foreach ( WC()->cart->get_cart() as $item_key => $item ) {
	// Are any products in the special class?
	if ( $item['data']->needs_shipping() && $special_class === $item['data']->get_shipping_class() ) {
		$found_item = true;
	}
}
if($found_item == true){
	foreach ( WC()->cart->get_cart() as $item_key => $item ) {
		$new_package['contents'][ $item_key ]  = $item;
		$new_package['contents_cost']         += $item['line_total'];

		// Remove from original package
		$packages[0]['contents_cost'] = $packages[0]['contents_cost'] - $item['line_total'];
		unset( $packages[0]['contents'][ $item_key ] );

		// If there are no items left in the previous package, remove it completely.
		if ( empty( $packages[0]['contents'] ) ) {
		unset( $packages[0] );
		}
	}
}

@bengtch
Copy link

bengtch commented Apr 16, 2017

The code works beautifully, thank you! Is it possible to limit the splitting of packages by country? We ship to 4 countries. We want to split items into multiple packages for only one of these countries. Not all 4. Is there any way to have this code work for the one country and not the other 3?

Thanks again.

@androos
Copy link

androos commented May 7, 2017

I wan't items that match to be free shipping. But regardless what slug i'm using it always say, no shipping method available. Except 'flat_rate'.

Edit: ok it's not free shipping to all countries only to one. So I wanted to use advanced_free_shipping. I need this because the weight gets counted on the whole order if the items are not excluded from package.

@TomLuijts
Copy link

TomLuijts commented Aug 15, 2019

Hi! Works great in isolation but when I combine it with another snippet, it doesn’t work and I have no clue why (I’m a php rookie). The code I’m talking about can be found here: https://www.xadapter.com/hide-checkout-fields-based-selected-shipping-method/
I use the code on this github page disable the ‘local pickup’ shipping method from my product. What I end up with are 2 different flat rate shipping methods called ‘flat_rate:9’ end ‘flat_rate:10’. I then use the other code (see link above) to remove the checkout fields for only the ‘flat_rate:10’ shipping method. When I insert this shipping method in this other code (as $shipping_method), the checkout fields are removed regardless of the shipping method selected. When I insert ‘flat_rate:9’, no checkout fields get removed regardless of the shipping method selected. I don’t know why this happens but when I remove the code from this github page, everything works fine. Could anyone help me to find the problem and make these two snippets compatible? I'm guessing it has something to do with how shipping class items are split into a new package that doesn't play nice with the other code...

Copy link

ghost commented Aug 15, 2019

Hi works great when you are just trying to split the packages up. Im running into issues when im trying to get the sorted products back into the regular items.

I need help with writing code to offer partial free shipping. These are my requirements and product A and product B are the products in the cart

Product A = FREE SHIPPING WHEN 25.00 or more of the product is in the cart or when product B is in the cart

Product B = FLAT RATE SHIPPING ONLY

@Crytix
Copy link

Crytix commented Sep 4, 2019

@progbdb and @JimmyAppelt even if the answer comes quite late. Maybe some people are still interested if you stumble across this article. I use the function of @mikejolley and have added some more functions and overrides.

Here I create the new package for "local_pickup".

/**
 * This function loops over cart items, and moves any item with shipping class 'special-class' into a new package. 
 * The new package only takes local pickup.
 */
function split_special_shipping_class_items( $packages ) {
	$found_item                     = false;
	$special_class                  = 'special-class'; // edit this with the slug of your local pickup shippig class
	$new_package                    = current( $packages );
	$new_package['contents']        = array();
	$new_package['contents_cost']   = 0;
	$new_package['applied_coupons'] = array();
	$new_package['ship_via']        = array( 'local_pickup' ); // Only allow local pickup for items in special class
	
	foreach ( WC()->cart->get_cart() as $item_key => $item ) {
		// Is the product in the special class?
		if ( $item['data']->needs_shipping() && $special_class === $item['data']->get_shipping_class() ) {
			$new_package['name'] = 'PickUp'; // your custom label for this package
			$found_item                            = true;
			$new_package['contents'][ $item_key ]  = $item;
			$new_package['contents_cost']         += $item['line_total'];
			// Remove from original package
			$packages[0]['contents_cost'] = $packages[0]['contents_cost'] - $item['line_total'];
			unset( $packages[0]['contents'][ $item_key ] );
			// If there are no items left in the previous package, remove it completely.
			if ( empty( $packages[0]['contents'] ) ) {
				unset( $packages[0] );
			}
		}
	}
	if ( $found_item ) {
	   $packages[] = $new_package;
	}
	return $packages;
}

function rename_custom_package( $package_name, $i, $package ) {
 
    if ( ! empty( $package['name'] ) ) {
        $package_name = $package['name'];
    }
 
    return $package_name;
}

@TomLuijts
Hide the Local PickUp option for the package being shipped.

function hide_shipping_method_based_on_shipping_class( $rates, $package )
{
    if ( is_admin() && ! defined( 'DOING_AJAX' ) )
        return;
    // HERE define your shipping class to find
    $class = 77;
    // HERE define the shipping methods you want to hide
    $method_key_ids = array('local_pickup:8', 'local_pickup:9');
    // Checking in cart items
    foreach( $package['contents'] as $item ) {
        // If we find the shipping class
        if( $item['data']->get_shipping_class_id() == $class ){
            foreach( $method_key_ids as $method_key_id ){
                unset($rates[$method_key_id]); // Remove the targeted methods
            }
            break; // Stop the loop
        }
    }
    return $rates;
}

The following lines must be added so that the changes can also be displayed.

// hook into shipping packages filter
add_filter( 'woocommerce_cart_shipping_packages', 'split_special_shipping_class_items' );
// output the new package name
add_filter( 'woocommerce_shipping_package_name', 'rename_custom_package', 10, 3 );
// hide local pickup on dropshipping items
add_filter( 'woocommerce_package_rates', 'hide_shipping_method_based_on_shipping_class', 10, 2 );

I myself have specified the delivery time and have a message displayed during the checkout, which refers to the Local PickUp articles.

Thanks again to all who contributed directly and indirectly to the code like @dvelopit and @mikejolley .

A picture as it looks like and lines of code you find in my git https://github.com/Crytix/Woocommerce-Local-PickUp-and-Shipping-Split

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment