27

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.

18

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. – Hayden Thring Jun 28 '13 at 11:09
  • added to my question – Hayden Thring Jun 28 '13 at 11:19
  • 2
    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. – Marius Jun 28 '13 at 11:35
  • 1
    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') – Marius Jun 28 '13 at 13:06
  • 1
    XmlConnect allows you to connect your mobile application to your website. Ignore it. – Marius Jun 28 '13 at 13:51
2

if is Saleable returns false, Reindexing can also have issue reindex the data

0

If after reindexing and debugging is_salable to no end and your configurable product is still showing as out of stock, make sure all the simple's have status set to Enabled, in ALL the store views. I just wasted two hours wondering why a configurable was out of stock no matter what I did, until I checked all the store views and found out some body had disabled the status.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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