Is there a solution for Role Based inventory for WooCommerce?

341 Views Asked by At

I'm trying to find a solution to use a double inventory where a different stock is being used based based on the user role. I know there are many easy solutions to limit the amount of products they can order at once, but this doesn't prevent them from placing another order. Ideally I would like to have a second inventory for each product which is used for a specific user role.

I've already been experimenting with a workaround where I add each product twice with a different inventory and show/hide the product based on user role. But this requires a lot of changes throughout the entire website to show/hide products from every page and element, not to mention all the links to product pages.

3

There are 3 best solutions below

1
businessbloomer On

This is a very complex task. First, you need to display an additional input field in the Edit Product page:

add_action( 'woocommerce_product_options_stock', 'bbloomer_role_based_stock' );

function bbloomer_role_based_stock() {
    global $product_object;
    echo '<div class="stock_fields show_if_simple show_if_variable">';
    woocommerce_wp_text_input(
        array(
            'id' => '_stock_role',
            'value' => get_post_meta( $product_object->get_id(), '_stock_role', true ),
            'label' => 'Stock by role',
            'data_type' => 'stock',
    ));
    echo '</div>';      
}

Then, you need to save it:

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

Then, you need to filter stock quantity and status if the customer is logged in:

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

function bbloomer_get_stock_quantity_curr_role( $value, $product ) {
    if ( wc_current_user_has_role( 'custom_role' ) ) {
        $value = get_post_meta( $product_id, '_stock_role', true ) ? get_post_meta( $product_id, '_stock_role', true ) : 0;
    }
    return $value;
}


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

function bbloomer_get_stock_status_curr_role( $status, $product ) {
    if ( wc_current_user_has_role( 'custom_role' ) ) {
        $value = get_post_meta( $product_id, '_stock_role', true );
        $status = $value && ( $value > 0 ) ? 'instock' : 'outofstock';
    }
    return $status;
}

Finally, you need to reduce the correct stock on payment complete:

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

function bbloomer_maybe_reduce_role_stock( $reduce, $order_id ) {
    $order = wc_get_order( $order_id );
    if ( wc_user_has_role( $order->get_customer_id(), 'custom_role' ) { 
        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 );
            $item_name = $product->get_formatted_name();
            $old_stock = get_post_meta( $product_id, '_stock_role', true ) ? (int) get_post_meta( $product_id, '_stock_role', true ) : 0;
            update_post_meta( $product_id, '_stock_role', $old_stock - $qty );
            $item->add_meta_data( '_reduced_stock', $qty, true );
            $item->save();
            $order->add_order_note( sprintf( 'Reduced role stock for item "%s" from %s to %s.', $item_name, $old_stock, $old_stock - $qty ) );
            $reduce = false;
        }   
    }   
    return $reduce;
}

Totally untested and works with simple products only.

0
Uncarved On

Thanks for getting me started, I already figured it would be a complex task :) With some minor adjustments in your code I got it working for simple products (just haven't tested the last step after purchase yet).

Being motivated by your approach, I pushed forward and created a solution for variable products by adding a custom field for each variation:

add_action( 'woocommerce_variation_options_inventory', 'sf_role_based_stock', 10, 3 );

function sf_role_based_stock( $loop, $variation_data, $variation ) {

    woocommerce_wp_text_input(
        array(
            'id'            => '_stock_role_variation[' . $loop . ']',
            'label'         => 'Role based stock quantity',
            'wrapper_class' => 'form-row',
            'placeholder'   => 'Type here...',
            'desc_tip'      => true,
            'description'   => 'Enter a number to set custom role stock quantity on variation level.',
            'value'         => get_post_meta( $variation->ID, '_stock_role_variation', true )
        )
    );

}

And saving the data from these custom fields again:

add_action( 'woocommerce_save_product_variation', 'sf_save_role_based_stock', 10, 2 );

function sf_save_role_based_stock( $variation_id, $loop ) {
    $stockrolevariation = ! empty( $_POST[ '_stock_role_variation' ][ $loop ] ) ? $_POST[ '_stock_role_variation' ][ $loop ] : '';
    update_post_meta( $variation_id, '_stock_role_variation', sanitize_text_field( $stockrolevariation ) );
}

Everything above works fine :) but now I have been struggling for a few hours to get it working on the product page. I understand we need jQuery to get the variation id (unless I'm wrong?). Showing the variation id in HTML is easy, but how do we get the custom role stock value in php? Is it only possible with AJAX? Based on one of your examples I created this:

add_filter( 'woocommerce_product_get_stock_quantity' , 'sf_get_stock_quantity_custom_role', 9999, 2 );
function sf_get_stock_quantity_custom_role( $value, $product ) {
    if ( wc_current_user_has_role( 'Custom_Role' ) ) {
        global $custom_variation_stock;
        if ($product->is_type( 'variable' ))
        {
            // Get custom role stock for variable products
            
            echo '<div class="variation_info"></div>';
            wc_enqueue_js( "
                 $( 'input.variation_id' ).change( function(){
                    if( '' != $(this).val() ) {
                       var var_id = $(this).val();
                       $('.variation_info').html(var_id);
                    }
                 });
              " );
            
            $variation_id = ''; // Need to get variation id here
            $value = get_post_meta( '$variation_id', '_stock_role_variation', true );
            
        } else {
            // Get custom role stock for simple products
            $value = get_post_meta( $product->get_id(), '_stock_role', true );
        }
    }
    return $value;
}

In another attempt I managed to get ALL role based stock quantities for every variation of the product without jQuery, but then I don't know how to determine which one is currently selected:

add_filter( 'woocommerce_product_get_stock_quantity' , 'sf_get_stock_quantity_custom_role', 9999, 2 );
function sf_get_stock_quantity_custom_role( $value, $product ) {
    if ( wc_current_user_has_role( 'Custom_Role' ) ) {
        global $custom_variation_stock;
        if ($product->is_type( 'variable' ))
        {
            foreach($product->get_available_variations() as $variation){
                $variation_id = $variation['variation_id'];
                $variation_obj = new WC_Product_variation($variation_id);
                $value = get_post_meta( $variation_id, '_stock_role_variation', true );
                echo "Custom role variation stock: " . $value . " ";
            }
        } else {
            // Get custom role stock for simple products
            $value = get_post_meta( $product->get_id(), '_stock_role', true );
        }
    }
    return $value;
}
0
davvcarpenter On

I was able to get this figured out using businessbloomer's answer and your answer as a starting point. I changed some of the phrasing because I'm using this to create a B2B-specific stock quantity field.

First, add a field for B2B stock on simple products

add_action( 'woocommerce_product_options_stock', 'b2b_role_based_stock' );

function b2b_role_based_stock() {
    global $product_object;
    echo '<div class="stock_fields show_if_simple show_if_variable">';
    woocommerce_wp_text_input(
        array(
            'id' => '_b2b_stock',
            'value' => get_post_meta( $product_object->get_id(), '_b2b_stock', true ),
            'label' => 'B2B Stock',
            'data_type' => 'stock',
        ));
    echo '</div>';
}

Then, add a field for B2B stock on variations

add_action( 'woocommerce_variation_options_inventory', 'b2b_role_based_stock', 10, 3 );

function b2b_role_based_stock( $loop, $variation_data, $variation ) {

    woocommerce_wp_text_input(
        array(
            'id'            => '_b2b_stock_variation[' . $loop . ']',
            'label'         => 'B2B Stock',
            'wrapper_class' => 'form-row',
            'placeholder'   => '',
            'desc_tip'      => true,
            'description'   => 'This stock will be used for B2B customers',
            'value'         => get_post_meta( $variation->ID, '_b2b_stock_variation', true )
        )
    );
}

Then, save the simple product B2B stock value

add_action( 'save_post_product', 'b2b_save_additional_stocks' );

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

Then, save the variation B2B stock value

add_action( 'woocommerce_save_product_variation', 'b2b_save_role_based_stock', 10, 2 );

function b2b_save_role_based_stock( $variation_id, $loop ) {
    $variation_b2b_stock = ! empty( $_POST[ '_b2b_stock_variation' ][ $loop ] ) ? $_POST[ '_b2b_stock_variation' ][ $loop ] : '';
    update_post_meta( $variation_id, '_b2b_stock_variation', sanitize_text_field( $variation_b2b_stock ) );
}

Now, we will use both woocommerce_product_get_stock_quantity and woocommerce_product_variation_get_stock_quantity hooks to display the correct stock quantity on the frontend

add_filter( 'woocommerce_product_get_stock_quantity' ,          'b2b_get_stock_quantity_curr_role', 9999, 2 );
add_filter( 'woocommerce_product_variation_get_stock_quantity', 'b2b_get_stock_quantity_curr_role', 10, 2 );

function b2b_get_stock_quantity_curr_role( $value, $product ) {

    // Simple product post meta key
    $key = '_b2b_stock';

    // Variable product post meta key
    if ($product->get_type() == 'variation') {
        $key = '_b2b_stock_variation';
    }

    // Get B2B quantity if user has certain role
    if ( wc_current_user_has_role( 'custom_role' ) ) {
        $value = get_post_meta( $product->get_id(), $key, true ) ? get_post_meta( $product->get_id(), $key, true ) : 0;
    }
    return $value;
}

Lastly, we will decrement the B2B stock quantity values after checkout

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

function b2b_maybe_reduce_role_stock( $reduce, $order_id ) {
    $order = wc_get_order( $order_id );
    if ( wc_user_has_role( $order->get_customer_id(), 'custom_role' ) ) {
        foreach ( $order->get_items() as $item ) {

            if ( ! $item->is_type( 'line_item' ) ) {
                continue;
            }

            $product = $item->get_product();

            $key = '_b2b_stock';
            $product_id = $item->get_product_id();

            // If item is variation, get variation stock meta key and variation ID instead of product ID
            if ($product->get_type() == 'variation') {
                $key = '_b2b_stock_variation';
                $product_id = $product->get_id();
            }

            $item_stock_reduced = $item->get_meta( '_reduced_stock', true );
            if ( $item_stock_reduced || ! $product || ! $product->managing_stock() ) {
                continue;
            }

            // Reduce stock
            $qty = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item );
            $item_name = $product->get_formatted_name();
            $old_stock = get_post_meta( $product_id, $key, true ) ? (int) get_post_meta( $product_id, $key, true ) : 0;
            update_post_meta( $product_id, $key, $old_stock - $qty );
            $item->add_meta_data( '_reduced_stock', $qty, true );
            $item->save();
            $order->add_order_note( sprintf( 'Reduced role stock for item "%s" from %s to %s.', $item_name, $old_stock, $old_stock - $qty ) );
            $reduce = false;
        }
    }
    return $reduce;
}

I ran into an issue with reserved stock that I solved by just disabling reserved stock in the WooCommerce settings but I'm sure there is a clever way to get past it.

Thanks businessbloomer and Uncarved for the springboard here.