WooCommerce: Additional Stock Inventory Location

The WooCommerce plugin allows you to manage stock for each product, but you only have a single stock quantity field!

What if you have two warehouses and, as a store admin, need to manage the inventory for each location? Besides, what if an item is out of stock at location 1, but it’s in stock at location 2, and therefore the customer needs to be able to purchase it?

This amazing workaround will add a second input number in the product settings, redefine stock quantity and status on the frontend by summing up stock 1 + stock 2, and finally decrease stock 1 until it goes to 0, after which it will decrease stock 2.

This default behavior can be changed of course e.g. it’s possible to define from where the stock is reduced (by distance?) via additional code. Also, additional code can be written to make it compatible with variable products or custom product types, as well as make it work with refunds. Either way, enjoy!

Here’s the second stock field. In this case, stock quantity is 0 in the default warehouse, so technically the product is out of stock. With the snippet below, and the fact that there is 1 item in stock in Location 2, the product is actually “in stock” with a quantity of “1”. Upon order, this is the quantity that gets decreased.

PHP Snippet: Second Stock Location Management

/**
 * @snippet       Second Stock Location @ WooCommerce Edit Product
 * @how-to        businessbloomer.com/woocommerce-customization
 * @author        Rodolfo Melogli, Business Bloomer
 * @compatible    WooCommerce 8
 * @community     https://businessbloomer.com/club/
 */

add_action( 'woocommerce_product_options_stock', 'bbloomer_additional_stock_location' );

function bbloomer_additional_stock_location() {
	global $product_object;
	echo '<div class="show_if_simple show_if_variable">';
	woocommerce_wp_text_input(
		array(
			'id' => '_stock2',
			'value' => get_post_meta( $product_object->get_id(), '_stock2', true ),
			'label' => '2nd Stock Location',
			'data_type' => 'stock',
		)
	);
	echo '</div>';
}

add_action( 'save_post_product', 'bbloomer_save_additional_stock' );
  
function bbloomer_save_additional_stock( $product_id ) {
    global $typenow;
    if ( 'product' === $typenow ) {
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
		if ( isset( $_POST['_stock2'] ) ) {
			update_post_meta( $product_id, '_stock2', $_POST['_stock2'] );
		}
	}
}

add_filter( 'woocommerce_product_get_stock_quantity' , 'bbloomer_get_overall_stock_quantity', 9999, 2 );

function bbloomer_get_overall_stock_quantity( $value, $product ) {
	$value = (int) $value + (int) get_post_meta( $product->get_id(), '_stock2', true );
    return $value;
}

add_filter( 'woocommerce_product_get_stock_status' , 'bbloomer_get_overall_stock_status', 9999, 2 );

function bbloomer_get_overall_stock_status( $status, $product ) {
	if ( ! $product->managing_stock() ) return $status;
	$stock = (int) $product->get_stock_quantity() + (int) get_post_meta( $product->get_id(), '_stock2', true );
	$status = $stock && ( $stock > 0 ) ? 'instock' : 'outofstock';
    return $status;
}

add_filter( 'woocommerce_payment_complete_reduce_order_stock', 'bbloomer_maybe_reduce_second_stock', 9999, 2 );

function bbloomer_maybe_reduce_second_stock( $reduce, $order_id ) {
	$order = wc_get_order( $order_id );
	$atleastastock2change = false;
	foreach ( $order->get_items() as $item ) {	
		if ( ! $item->is_type( 'line_item' ) ) {
			continue;
		}
		$product = $item->get_product();
		$item_stock_reduced = $item->get_meta( '_reduced_stock', true );
		if ( $item_stock_reduced || ! $product || ! $product->managing_stock() ) {
			continue;
		}
		$qty = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item );
		$stock1 = (int) get_post_meta( $product->get_id(), '_stock', true );
		if ( $qty <= $stock1 ) continue;
		$atleastastock2change = true;
	}
	if ( ! $atleastastock2change ) return $reduce;	
	foreach ( $order->get_items() as $item ) {	
		if ( ! $item->is_type( 'line_item' ) ) {
			continue;
		}
		$product = $item->get_product();
		$item_stock_reduced = $item->get_meta( '_reduced_stock', true );
		if ( $item_stock_reduced || ! $product || ! $product->managing_stock() ) {
			continue;
		}	
		$item_name = $product->get_formatted_name();
		$qty = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item );
		$stock1 = (int) get_post_meta( $product->get_id(), '_stock', true );
		$stock2 = (int) get_post_meta( $product->get_id(), '_stock2', true );
		if ( $qty <= $stock1 ) {
			wc_update_product_stock( $product, $qty, 'decrease' );
			$order->add_order_note( sprintf( 'Reduced stock for item "%s"; Stock 1: "%s" to "%s".', $item_name, $stock1, $stock1 - $qty ) );
		} else {		
			$newstock2 = $stock2 - ( $qty - $stock1 );
			wc_update_product_stock( $product, $stock1, 'decrease' );
			update_post_meta( $product->get_id(), '_stock2', $newstock2 );
			$item->add_meta_data( '_reduced_stock', $qty, true );
			$item->save();				
			$order->add_order_note( sprintf( 'Reduced stock for item "%s"; Stock 1: "%s" to "0" and Stock 2: "%s" to "%s".', $item_name, $stock1, $stock2, $newstock2 ) );
		}
	}
	$order->get_data_store()->set_stock_reduced( $order_id, true );
	return false;
}

In plain English:

  • bbloomer_additional_stock_location shows the second stock quantity field
  • bbloomer_save_additional_stock saves the custom stock amount
  • bbloomer_get_overall_stock_quantity sets the product stock inventory to stock 1 + stock 2
  • bbloomer_get_overall_stock_status sets the product stock status based on stock 1 + stock 2
  • bbloomer_maybe_reduce_second_stock decreases the stock from stock 1 and then from stock 2 in case the ordered quantity is greater than stock 1, otherwise it lets WooCommerce do the default stock decrease

Advanced Plugin (Sync Stock Between Stores): WooMultistore

In case you need to sync products and stock levels across multiple WooCommerce stores, WooMultistore may be a good fit.

WooMultistore is a plugin specifically designed to manage multiple WooCommerce stores from a single interface. It allows for the synchronization of product data, stock levels, and other product-related details across multiple stores.

Where to add custom code?

You should place custom PHP in functions.php and custom CSS in style.css of your child theme: where to place WooCommerce customization?

This code still works, unless you report otherwise. To exclude conflicts, temporarily switch to the Storefront theme, disable all plugins except WooCommerce, and test the snippet again: WooCommerce troubleshooting 101

Related content

Rodolfo Melogli

Business Bloomer Founder

Author, WooCommerce expert and WordCamp speaker, Rodolfo has worked as an independent WooCommerce freelancer since 2011. His goal is to help entrepreneurs and developers overcome their WooCommerce nightmares. Rodolfo loves travelling, chasing tennis & soccer balls and, of course, wood fired oven pizza. Follow @rmelogli

14 thoughts on “WooCommerce: Additional Stock Inventory Location

  1. Great snippet , Thanks Rodolfo !

    It inspired me to create a country based stock snippet ,
    I have a question regarding the data type parameter in the code below

    woocommerce_wp_text_input(
          array(
             'id' => '_stock2',
             'value' => get_post_meta( $product_object->get_id(), '_stock2', true ),
             'label' => '2nd Stock Location',
             'data_type' => 'stock',
          )
       );

    I could not find an online documentation regarding this parameter , even though it is used around 34 times in woocommerce files with multiple values (price, stock , percent, decimal) !
    I wonder what is the benefit of this parameter ? and what is the difference between type and data_type parameters ?

    I would really appreciate it if you could explain !

    1. You’re welcome! That parameter just adds some conditional classes and formats the values according to the type – see function woocommerce_wp_text_input inside the plugin

  2. How to make the snippet work for variable products

    Thanks and regards,

    Mike van Dijk

    1. Hello Mike, the reason I haven’t done that is because I’m still validating if the system works and is compatible with any possible theme/plugin/whatever integration.

      Once I’m happy with that, I will create a mini-plugin about this so that I can implement it for all product types.

      Thank you for your patience

  3. It’s not clear how to assign a location for processing online orders and how to assign a second location, for example for a YITH POS plugin that already uses the base inventory location Woocommerce And how can he be taught to use the second supply location?

    1. Hello Volodymyr, thanks so much for your comment! Yes, this is definitely possible, but I’m afraid it’s custom work. If you’d like to get a quote, feel free to contact me here. Thanks a lot for your understanding!

  4. Hi I used this code but when I am in checkout page and click submit to register order this error is displayed:
    ‘Not enough units of pen code 123 are available in stock to fulfil this order’ and in product status column is written outstock . how can improve it ?

    1. Thanks for your feedback Mina. Does that happen only for that product?

      1. hi,

        The same thing happens on my site, when clicking to make the payment, in all the products that have stock 0 in the main stock, having stock in stock2.

        And orders are generated in the backend, but remain in “pending payment” status, but customers cannot follow their payment method.

        1. I encountered this issue and managed to find a workaround, although it’s worth noting that altering the core functionality of WooCommerce appears to be the only viable solution. The key file that requires modification is located at:

          /public_html/your_theme/wp-content/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php

          To implement this change, we need to apply a filter hook to ‘get_query_for_stock’:

          public function get_query_for_stock( $product_id ) {
              global $wpdb;
              $query = $wpdb->prepare(
                  "
                  SELECT COALESCE ( MAX( meta_value ), 0 ) FROM $wpdb->postmeta as meta_table
                  WHERE meta_table.meta_key = '_stock'
                  AND meta_table.post_id = %d
                  ",
                  $product_id
              );
          
              return apply_filters( 'woocommerce_query_for_stock', $query, $product_id );
          }
          

          In this instance, I’ve named the hook ‘woocommerce_query_for_stock.’ Following this, we must add a filter function to the mentioned hook to modify the $query, like so:

          function alter_query_for_stock($query, $product_id){
             global $wpdb;
             $query = $wpdb->prepare(
                "
                SELECT  SUM(meta_value) as meta_value
                FROM
                (
                   SELECT COALESCE ( MAX( meta_value ), 0 ) as meta_value FROM $wpdb->postmeta as meta_table1 WHERE meta_table1.meta_key = '_stock' AND meta_table1.post_id = %d
                   UNION ALL
                   SELECT COALESCE ( MAX( meta_value ), 0 ) as meta_value FROM $wpdb->postmeta as meta_table2 WHERE meta_table2.meta_key = '_stock2' AND meta_table2.post_id = %d
                ) subquery
                ",
                $product_id,
                $product_id
             );
             return $query;
          }
          

          And that’s it!

          1. Haven’t tested it and can’t recreate the issue on my end, but thanks for sharing!

      2. This error appear when the standard stock quantity is 0, at checkout woocommerce make a check to the stock status and return out of stock, how can we fix it?

        1. I experienced the same issue as above. In case anyone else comes across this post I wanted to share that I was able to resolve it by leaving the Hold stock (minutes) option blank under WooCommerce > Settings > Products > Inventory.

Questions? Feedback? Customization? Leave your comment now!
_____

If you are writing code, please wrap it like so: [php]code_here[/php]. Failure to complying with this, as well as going off topic or not using the English language will result in comment disapproval. You should expect a reply in about 2 weeks - this is a popular blog but I need to get paid work done first. Please consider joining the Business Bloomer Club to get quick WooCommerce support. Thank you!

Your email address will not be published. Required fields are marked *