where does is_salable come from?

  • Note: If you've been editing products by PHP code, then re-index them in the admin after, save your self hours trying to work out why they not showing like me below ...

    I am going round in circles trying to work out how is_salable is set for a product, and thus work out why my products are now showing.

    There's only one place in the code I can find that sets it:

    $salable = $this->isAvailable();
    

    but I can't work out how or where in gets this from, as when I follow isAvailable it just seems to loop back around ....

    /app/code/core/Mage/Catalog/Model/Product.php

        public function isSalable()
        {
            Mage::dispatchEvent('catalog_product_is_salable_before', array(
                'product'   => $this
            ));
    
            $salable = $this->isAvailable();
    
        $object = new Varien_Object(array(
            'product'    => $this,
            'is_salable' => $salable
        ));
        Mage::dispatchEvent('catalog_product_is_salable_after', array(
            'product'   => $this,
            'salable'   => $object
        ));
        return $object->getIsSalable();
    }
    

    following $this->isAvailable() from here it goes a few lines:

    **public function isAvailable()
    { 
        return $this->getTypeInstance(true)->isSalable($this);
    }**
    

    this then calls app/code/core/Mage/Catalog/Model/Product/Type/Configurable.php's isSalable

    public function isSalable($product = null)
    {
        $salable = parent::isSalable($product);
    
        if ($salable !== false) {
            $salable = false;
            if (!is_null($product)) {
                $this->setStoreFilter($product->getStoreId(), $product);
            }
            foreach ($this->getUsedProducts(null, $product) as $child) {
                if ($child->isSalable()) {
                    $salable = true;
                    break;
                }
            }
        }
    
        return $salable;
    }
    

    which calls the parent: /app/code/core/Mage/Catalog/Model/Product/Type/Abstract.php's isSalable :

    public function isSalable($product = null)
    {
        $salable = $this->getProduct($product)->getStatus() == Mage_Catalog_Model_Product_Status::STATUS_ENABLED;
        if ($salable && $this->getProduct($product)->hasData('is_salable')) {
            $salable = $this->getProduct($product)->getData('is_salable');
        }
        elseif ($salable && $this->isComposite()) {
            $salable = null;
        }
    
        return (boolean) (int) $salable;
    }
    

    which just does a has/get data call on the is_saleable value?!? Did I track that right? Where is this value coming from?

    I issued a recursive grep on my installation for is_salable, surely this should show any lines where it is set but I don't see any right away:

    grep -r is_salable *
    app/code/core/Mage/CatalogInventory/Model/Stock/Status.php:            $object = new Varien_Object(array('is_in_stock' => $product->getData('is_salable')));
    app/code/core/Mage/XmlConnect/Block/Wishlist.php:                $itemXmlObj->addChild('is_salable', (int)$item->getProduct()->isSalable());
    app/code/core/Mage/XmlConnect/Block/Catalog/Product.php:            $item->addChild('is_salable', (int)$product->isSalable());
    app/code/core/Mage/XmlConnect/Block/Cart/Crosssell.php:                $itemXmlObj->addChild('is_salable', 0);
    app/code/core/Mage/XmlConnect/Block/Cart/Crosssell.php:                $itemXmlObj->addChild('is_salable', (int)$product->isSalable());
    app/code/core/Mage/Catalog/Model/Product.php:        Mage::dispatchEvent('catalog_product_is_salable_before', array(
    app/code/core/Mage/Catalog/Model/Product.php:            'is_salable' => $salable
    app/code/core/Mage/Catalog/Model/Product.php:        Mage::dispatchEvent('catalog_product_is_salable_after', array(
    app/code/core/Mage/Catalog/Model/Product.php:        if ($this->hasData('is_salable')) {
    app/code/core/Mage/Catalog/Model/Product.php:            return $this->getData('is_salable');
    app/code/core/Mage/Catalog/Model/Product/Type/Abstract.php:        if ($salable && $this->getProduct($product)->hasData('is_salable')) {
    app/code/core/Mage/Catalog/Model/Product/Type/Abstract.php:            $salable = $this->getProduct($product)->getData('is_salable');
    

    FOUND:

    grep -r setIsSalable *
    app/code/core/Mage/CatalogInventory/Model/Stock/Status.php:        $product->setIsSalable($stockStatus);
    app/code/core/Mage/CatalogInventory/Model/Stock/Status.php:                    $product->setIsSalable($status);
    

    It was setIsSalable that I did not think/know to look for rather than just setIsSalable.

  • Marius

    Marius Correct answer

    8 years ago

    isAvailable() looks like this:

    public function isAvailable()
    {
        return $this->getTypeInstance(true)->isSalable($this)
            || Mage::helper('catalog/product')->getSkipSaleableCheck();
    }
    

    This means that the result of the method depends on the product type.
    Each product type has a isSalable() method:

    • Mage_Catalog_Model_Product_Type_Grouped::isSalable() - for grouped products
    • Mage_Catalog_Model_Product_Type_Configurable::isSalable() - for configurable products
    • Mage_Catalog_Model_Product_Type_Abstract::isSalable() - for the rest of product types since all the product types extend Mage_Catalog_Model_Product_Type_Abstract.
      I think that the call $this->getTypeInstance(true) confuses you. The method getTypeInstance() does not return an instance of the product model, but an instance of a product type.

    [EDIT]
    For a simple product this is called Mage_Catalog_Model_Product_Type_Grouped::isSalable(). This method checks if the product is enabled. If not then false is returned. If it's enabled, then it checks if it has a property is_salable that can be set by one of your observers.
    If it does not have such a property then it checks if the product type instance $this->isComposite(). If it is then it's not salable.
    For configurable products it checks if the conditions above are met and if there is a simple product associated to the configurable one that is salable (Again with the conditions above)
    The same is done for the grouped product but in a different way.

    In conclusion is_salable doesn't have to exist. But just in case you have an observer that sets that property it's taken into consideration when checking if the product can be sold.
    Here is an example from the core: The method Mage_CatalogInventory_Model_Stock_Status::assignProduct() cals $product->setIsSalable()
    Same goes for Mage_CatalogInventory_Model_Stock_Status::addStockStatusToProducts.
    The last one is called by the event catalog_product_collection_load_after.

    thankyou very much for your help again, i had traced isAvailable to isSalable and to Configurable.php's isSalable which just calls Abstract.php's isSalable but all it does is check 'is_salable' but i find no setting of it ?? i will add my code trail above.

    added to my question

    added to my answer :)

    i have configurable with simple, and im getting true for: $this->getProduct($product)->hasData('is_salable') so your saying there is an observer somewhere setting this ? (it is a heavily modified store ive taken over and trying to fix)

    Yes there is. I mentioned it in my answer `catalog_product_collection_load_after`. But this is just an example. There is also `cataloginventory/observer::addInventoryData` that calls `assignProduct` that sets `is_salable`. There could be others but I didn't look for all.

    how would i find any active observers ?

    I have no reliable method for doing this. Maybe get all events that start with `catalog_product_` and debug the methods called by the observers on those events. and see if one calls `setIsSalable` or `setData('is_salable')`

    ive added the results of a grep to the first post, surely it would show it being set ? what is the xmlconnect bits ? can you have a look at my addition

    XmlConnect allows you to connect your mobile application to your website. Ignore it.

    your mention of setIsSalable was the missing piece of the puzzle, i found it being used, forced it to 1, and my products show up, now i can trace that part of the code to find out why. thankyou ill post my grep to find it above

    For those who visit this page trying to track down why a product is returning false or null for `is_salable` then I suggest starting with reindexing `cataloginventory_stock` from the shell indexer.

License under CC-BY-SA with attribution


Content dated before 7/24/2021 11:53 AM

Tags used