1

On our Shop page, we have various badges (things like "Series 1") that, when clicked, link to their respective product tag pages. However, we want a "low stock" badge as well. The problem is that we need to dynamically figure out whether something is low stock. We can't just tag it.

I'm making the low stock badge link to its own page with Woocommerce [products] shortcode, but now I need to only show ones with stocks within that range. So far the solutions I've found all mention removing products by doing a taxonomy query, but I can't do that since I can't manually assign these to a low stock tag/category.

For reference, here the taxonomy query I've been using as a basis:

function custom_pre_get_posts_query( $q ) {
    if( is_shop() ) {
        $tag_query = (array) $q->get( 'tax_query' );

        $tag_query[] = array(
               'taxonomy' => 'product_tag',
               'field' => 'slug',
               'terms' => array( 'retired' ), // Don't display products with the tag "retired"
               'operator' => 'NOT IN'
        );

        $q->set( 'tax_query', $tag_query );
    }
}
add_action( 'woocommerce_product_query', 'custom_pre_get_posts_query' );
1

There's 2 things that I think you want addressed if I understand your question, example, and use case:

  1. Create a low stock status for products to use

    Following these steps helps give users the ability to set the low stock status on their products, and provides a way that is logical to query and for these types of products. There's basically two scenarios we need to make sure are accounted for when creating the low stock status:

    a. User manually sets the stock status on a product

    In this scenario the user has unchecked the option to manage stock under Inventory > Manage Stock. Since this is the case, the user can set the stock status here to specify if a product is out of stock, in stock, on backorder, etc. We will want to add the low stock option there so it can easily be manually set per product as needed. To do this we will filter the options populated in these dropdowns using the woocommerce_product_stock_status_options filter:

    /**
     * Add low stock to WooCommerce stock status options.
     */
    function add_low_stock_option( $options ) {
        $options['lowstock'] = __( 'Low Stock', 'textdomain' );
        return $options;
    }
    
    add_filter( 'woocommerce_product_stock_status_options', 'add_low_stock_option' );
    

    b. User has stock management enabled for a product

    Now we need to account for the user managing their stock by selecting the Inventory > Manage Stock option on a product. With this option selected they can set the quantity of items they have in stock along with the low stock threshold. I will use this field as our threshold for the low stock status to be set, so there's an easy way for users to change this per product as needed. We want to make sure that we update the low stock status when the product meta is processed, so we will hook into WooCommerce's woocommerce_process_product_meta hook to make sure our custom status is added:

    /**
     * Save the low stock status.
     */
    function save_low_stock_status( $product_id ) {
        $product = wc_get_product( $product_id );
    
        // Stock managed for product individually and between "Out of Stock" and "Low Stock Amount".
        if ( $product->get_manage_stock() &&
            ( $product->get_stock_quantity() <= $product->get_low_stock_amount() )
        ) {
            // Update to our low stock status.
            update_post_meta( $product_id, '_stock_status', 'lowstock' );
        }
    }
    
    add_action( 'woocommerce_process_product_meta', 'save_low_stock_status', 20 );
    
  2. Use the low stock status on the site

    You mentioned wanting to create badges based on this custom status, and had given a code sample of filtering a query. Depending on exactly what your end goal is from here, there's many ways of accomplishing these things.

    Here's some code examples based on the stuff above that you might find useful when implementing the custom status:

    Example 1: Update availablity message on product pages (ie show "Low stock" instead of "1 item available"[or whatever your threshold is]):

    /**
     * Get availablity status.
     */
    function get_custom_availability( $data, $product ) {
        $stock_status = get_post_meta( $product->id , '_stock_status' , true );
        if ( 'lowstock' === $stock_status ) {
            $data = [
                'availability' => __( 'Low stock', 'textdomain' ),
                'class' => 'low-stock'
            ];
        }
    
        return $data;
    }
    
    add_action( 'woocommerce_get_availability', 'get_custom_availability', 20, 2 );
    

    Example 2: Add low stock badges to products on your shop pages

    /**
     * Add a low stock badge on shop pages.
     */
    function add_low_stock_badge() {
        global $product;
    
        $stock_status = $product->get_stock_status();
    
        if ( 'lowstock' === $stock_status ) {
            echo '<span class="low-stock-badge">' . __( 'Low stock', 'textdomain' ) . '</span>';
        }
    }
    
    add_action( 'woocommerce_before_shop_loop_item', 'add_low_stock_badge' );
    

    Example 3: Show only products with the lowstock status

    In your code example where the query is being filtered by a taxonomy term and removed from the results, that works - if that's the behavior you need. For our lowstock status we created, we don't need that exactly, but you were on the right path for your basis. We will still want to hook into the woocommerce_product_query like your example, but instead filter the results by the metadata attached to the product. This is done in meta_query, so this code matches any products that have lowstock as the value for the _stock_status metadata on a product:

    /**
     * Show only lowstock stock status products in queries.
     */
    function show_low_stock_products( $q ) {
        $meta_query = $q->get( 'meta_query' );
    
        $meta_query[] = [
            'key' => '_stock_status',
            'value' => 'lowstock',
            'compare' => '=',
        ];
    
        $q->set( 'meta_query', $meta_query );
    }
    
    add_action( 'woocommerce_product_query', 'show_low_stock_products' );
    

    Example 4: Hide products with the lowstock status

    Of course, the title of this question asks how to hide products that have a stock lower than the threshold from the shop loop. Using the code in section c above, you can simply inverse the logic of compare in the query to hide the lowstock status products and show all others. I'd also recommend updating the function name to hide_low_stock_products for better legibility:

    /**
     * Hide the lowstock stock status products in queries.
     */
    function hide_low_stock_products( $q ) {
        $meta_query = $q->get( 'meta_query' );
    
        $meta_query[] = [
            'key' => '_stock_status',
            'value' => 'lowstock',
            'compare' => '!=',
        ];
    
        $q->set( 'meta_query', $meta_query );
    }
    
    add_action( 'woocommerce_product_query', 'hide_low_stock_products' );
    

I've also gone ahead and created this gist to include hiding the items from the query, the badge stuff, and availability message examples altogether to make it easier for you to copy and manipulate.

Edit:

Updating Existing Products

For a freshsite, and for our existing products the solution that will work best can vary from site to site depending on various factors. Basically, in the method above save_low_stock_status, we hook into woocommerce_process_product_meta. This is ran when a product's metadata is being processed and saved, so existing products aren't updated with our new status information.

This works great for a new site, but in most cases you'll need to run some sort of migration script. To keep it relevant, I won't include upgrade hook information and the steps involved since it sounds like this is for just a single site run to get you in the right direction. We will want to update the _stock_status metadata key entry in the database based on the criteria of any products _stock quantity being at or below the threshold you decide on, but not completely out of stock. We can achieve that through a SQL query. The code using $wpdb would look like this:

/**
 * Update all product stock statuses that meet the criteria.
 */
function update_lowstock_products_stock_status(){
    global $wpdb;

    // Low Quantity threshold.
    $threshold = '2';

    // SQL statement to update _stock_status to lowstock on all products where _stock >= 1 AND _stock <= $threshold set for lowstock status.
    $sql .= "UPDATE $wpdb->postmeta stock, (SELECT DISTINCT post_id FROM $wpdb->postmeta WHERE meta_key = '_stock' AND meta_value BETWEEN 1 AND $threshold ) id SET stock.meta_value = 'lowstock' WHERE stock.post_id = id.post_id AND stock.meta_key = '_stock_status';";

    // Execute query.
    $wpdb->query( $sql );
}

This would update every product that matches your criteria. You may need to rewrite the query to address your specific site and needs. For example: if your products do actively have the option for manage stock set manually by someone, then you may want to look at _low_stock_amount meta value to get the low stock amounts for those products instead of the hard threshold that is being set for handling the rest automatically.

Just keep in mind we only need to run this update once, so if it's a single site, you can execute the code once in your theme's functions.php file:

// Only needs to update once, so just add to theme's functions.php or if in a plugin you can have migration happen in update hook.
update_lowstock_products_stock_status();
2
  • First of all, I really appreciate the thorough answer! I should have mentioned that we are managing stock status via enabling it on a product. However, I'm having an issue when it comes to the "save the low stock status" part of the process. Products that I know meet the conditions of having managed stock and stocks lower than the threshold still only have the default options of instock and onbackorder. Any suggestions on why this status isn't getting changed?
    – Saxt0n
    Sep 23 '19 at 14:26
  • Not a problem, glad to help! It is likely because they are existing products, so you will need to update the database to match your new schema and constraints you desire. I edited my answer to include an example using $wpdb query, which should get you going in the right direction. Any other meta can be used in your conditions, but just make sure you test your query thoroughly before updating the db on a live site!
    – Tim Elsass
    Sep 23 '19 at 19:32

Not the answer you're looking for? Browse other questions tagged or ask your own question.