We’re all aware of the fact that Magento handles inventory of products in a (fairly) straightforward fashion. There is only one “warehouse”, one inventory, one “number” in the database that is responsible for a final say – how much of it is in stock. A lot of other functionality is dependent on the fact that stock is global. If we check “Advanced inventory” configuration from the administration of a product, we notice that all of the options are global: Out of stock threshold, Minimum and maximum qty allowed in Shopping Cart, backorders, notifications etc.
While this is perfectly normal – having a global quantity (stock), requires most of the functionality related to it, to have global setting as well, a situation may arise, in which we need some of the functionality to work on a different scope (e.g. website or store view scope). Example of this would be backordering. A simple functionality – allow customer to purchase something even if it’s not in stock. This decreases quantity further into negative values, e.g. -5.
Can we extend this functionality beyond just global scope, and have it work on, say, website level? It turns out we can, and it’s not that big of a change.
To investigate this first, we’d like to add to cart more than we have. By doing this, we end up in (omitting a few steps for brevity) vendor/magento/module-catalog-inventory/Model/StockStateProvider.php
/**
* Check quantity
*
* @param StockItemInterface $stockItem
* @param int|float $qty
* @exception MagentoFrameworkExceptionLocalizedException
* @return bool
*/
public function checkQty(StockItemInterface $stockItem, $qty)
{
if (!$this->qtyCheckApplicable) {
return true;
}
if (!$stockItem->getManageStock()) {
return true;
}
if ($stockItem->getQty() - $stockItem->getMinQty() - $qty < 0) {
switch ($stockItem->getBackorders()) {
case MagentoCatalogInventoryModelStock::BACKORDERS_YES_NONOTIFY:
case MagentoCatalogInventoryModelStock::BACKORDERS_YES_NOTIFY:
break;
default:
return false;
}
}
return true;
}
What this does is check if qty
we want to purchase is available. If it’s not, it performs a checks for the backorder functionality, which leads us further, to vendor/magento/module-catalog-inventory/Model/Configuration.php
for retrieving the backorder information from database (core_config_data
).
/**
* Retrieve backorders status
*
* @param null|string|bool|int|MagentoStoreModelStore $store
* @return int
*/
public function getBackorders($store = null)
{
return (int) $this->scopeConfig->getValue(
self::XML_PATH_BACKORDERS,
MagentoStoreModelScopeInterface::SCOPE_STORE,
$store
);
}
Interesting thing to notice is the $store
variable which is passed when fetching the configuration details. So, Magento is looking for a scoped value of backorder setting in database, but since there is none, a global is returned. What if we change scope for this configuration from global to website? Let’s try.
- Make a simple Magento 2 module (like this one, for example).
- Create a file at
appcodeYour_VendorYour_Moduleetcadminhtmlsystem.xml
with the following content:
<?xml version="1.0"?>
<!--
/**
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance" xsi_noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="cataloginventory" translate="label" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1">
<group id="item_options" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<field id="backorders" translate="label" type="select" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1">
<label>Backorders</label>
<source_model>MagentoCatalogInventoryModelSourceBackorders</source_model>
<backend_model>MagentoCatalogInventoryModelConfigBackendBackorders</backend_model>
<comment>Changing can take some time due to processing whole catalog.</comment>
</field>
</group>
</section>
</system>
</config>
We’ve just copied over core configuration for backorders located at vendor/magento/module-catalog-inventory/etc/adminhtml/system.xml
and updated showInWebsite
from 0 to 1. Now we have configuration for backorders on a website level.
Couple of implications arise from this change:
- obviously, we can set backorder functionality on a website level
- since there is still only 1 inventory, we need to make sure that default scope for backorders is turned on, since cataloginventory_stockstatus indexer uses this to properly set status
- if default config is enabled (as point 2 suggests), then all products would be “Available” on frontend, which is not something we want for <b>all</b> websites (“Add to cart” button would be visible everywhere)
Thankfully, Magento offers a handy events which we can observe in order to “fix” stock status for website that does not have bacorders available.
vendor/magento/module-catalog/Model/Product.php
has isSalable()
method, called from frontend (when deciding whether add-to-cart button should be displayed). It is dispatching catalog_product_is_salable_before
and catalog_product_is_salable_after
which we can use, and display products as “not salable” if they are in the store that does not provide backorder functionality.
/**
* Check is product available for sale
*
* @return bool
*/
public function isSalable()
{
// ...
<strong>$this->_eventManager->dispatch('catalog_product_is_salable_before', ['product' => $this]);</strong>
$salable = $this->isAvailable();
$object = new MagentoFrameworkDataObject(['product' => $this, 'is_salable' => $salable]);
<strong>$this->_eventManager->dispatch(
'catalog_product_is_salable_after',
['product' => $this, 'salable' => $object]
);</strong>
// ...
}
What we can do here is create a custom observer that would listen to one of these events, and set product as not salable ($product->setIsSalable(false)
) when a qty is <= 0 and store is the one where we don’t have backorders.
That’s it – a small change in the configuration, and a custom observer to make sure all data is properly displayed, gives us the ability to have backorder functionality on a website scope.