Magento Catalog Core – Source of is_salable

catalogcore

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.

Best Answer

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.

Related Topic