inventory
inventory copied to clipboard
MSI does not respect the "Display out of stock products" setting
MSI does not respect the "Display out of stock products" setting
Preconditions (*)
- Magento 2.3.5 with MSI activated
Steps to reproduce (*)
- Create a configurable with 2 simples
- Set
qty > 0
for one simple, the other hasqty = 0
andis_salable = 0
- Set "Display out of stock products" = 1 in Store -> Configuration -> Catalog -> Inventory
Expected result (*)
- The swatches belonging to the simple with
is_salable = 0
should be displayed and striked through, indicating that the simple is out of stock
Actual result (*)
- The swatches belonging to the simple with
is_salable = 0
is NOT displayed at all
The technical background:
\Magento\InventoryConfigurableProduct\Plugin\Model\ResourceModel\Attribute\IsSalableOptionSelectBuilder::afterGetSelect()
does not respect the store configuration setting for "Display out of stock products", it always adds is_salable = 1
. It should be implemented similar to \Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\InStockOptionSelectBuilder::afterGetSelect()
where it queries the store configuration setting before joining.
There is even more inconsistency which leads to confusion: When you have 2 dimensions (size and color for example) you make up an array. Let's say the size attribute has a range from 1 to 12. And there are three colors: red, green, blue. Then it looks like this:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
red | x | x | x | x | x | x | x | x | ||||
blue | x | x | x | x | x | x | x | |||||
green | x | x | x | x | x | x | x | x |
x
means is_salable = 1
. Currently MSI returns all products with an x. On the product detail page the blue/3 is displayed but striked through. MSI didn't return the product to the frontend, but as there are other simples in blue, the frontend renders it and strikes it.
BUT: As no simple is given in any color for the sizes 9 to 12, their swatches are NOT displayed. This is inconsistent behavior!
Here are two examples without and with bugfix for IsSalableOptionSelectBuilder::afterGetSelect()
:
Without bugfix
With bugfix
Here is the fix for the file IsSalableOptionSelectBuilder
:
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\InventoryConfigurableProduct\Plugin\Model\ResourceModel\Attribute;
use Magento\CatalogInventory\Api\StockConfigurationInterface;
use Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\DB\Select;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\InventoryIndexer\Model\StockIndexTableNameResolverInterface;
use Magento\InventoryCatalogApi\Api\DefaultStockProviderInterface;
use Magento\InventorySalesApi\Api\Data\SalesChannelInterface;
use Magento\InventorySalesApi\Api\StockResolverInterface;
use Magento\Store\Model\StoreManagerInterface;
/**
* Plugin for OptionSelectBuilderInterface to add "is_salable" filter.
*/
class IsSalableOptionSelectBuilder
{
/**
* @var StoreManagerInterface
*/
private $storeManager;
/**
* @var StockResolverInterface
*/
private $stockResolver;
/**
* @var StockIndexTableNameResolverInterface
*/
private $stockIndexTableNameResolver;
/**
* @var StockConfigurationInterface
*/
private $stockConfig;
/**
* @var DefaultStockProviderInterface
*/
private $defaultStockProvider;
/**
* @param StoreManagerInterface $storeManager
* @param StockResolverInterface $stockResolver
* @param StockIndexTableNameResolverInterface $stockIndexTableNameResolver
* @param StockConfigurationInterface $stockConfig
* @param DefaultStockProviderInterface $defaultStockProvider
*/
public function __construct(
StoreManagerInterface $storeManager,
StockResolverInterface $stockResolver,
StockIndexTableNameResolverInterface $stockIndexTableNameResolver,
StockConfigurationInterface $stockConfig,
DefaultStockProviderInterface $defaultStockProvider = null
) {
$this->storeManager = $storeManager;
$this->stockResolver = $stockResolver;
$this->stockIndexTableNameResolver = $stockIndexTableNameResolver;
$this->stockConfig = $stockConfig;
$this->defaultStockProvider = $defaultStockProvider ?: ObjectManager::getInstance()
->get(DefaultStockProviderInterface::class);
}
/**
* Add "is_salable" filter to select.
*
* @param OptionSelectBuilderInterface $subject
* @param Select $select
* @return Select
*
* @throws LocalizedException
* @throws NoSuchEntityException
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function afterGetSelect(
OptionSelectBuilderInterface $subject,
Select $select
) {
if (!$this->stockConfig->isShowOutOfStock()) {
$websiteCode = $this->storeManager->getWebsite()->getCode();
$stock = $this->stockResolver->execute(SalesChannelInterface::TYPE_WEBSITE, $websiteCode);
$stockId = (int)$stock->getStockId();
if ($stockId === $this->defaultStockProvider->getId()) {
return $select;
}
$stockTable = $this->stockIndexTableNameResolver->execute($stockId);
$select->joinInner(
['stock' => $stockTable],
'stock.sku = entity.sku',
[]
)->where(
'stock.is_salable = ?',
1
);
}
return $select;
}
}
@sydekumf I've made a diff patch based on your changes, thanks again
diff --git a/vendor/magento/module-inventory-configurable-product/Plugin/Model/ResourceModel/Attribute/IsSalableOptionSelectBuilder.php b/vendor/magento/module-inventory-configurable-product/Plugin/Model/ResourceModel/Attribute/IsSalableOptionSelectBuilder.php
index fa845d6..4469a8d 100644
--- a/vendor/magento/module-inventory-configurable-product/Plugin/Model/ResourceModel/Attribute/IsSalableOptionSelectBuilder.php
+++ b/vendor/magento/module-inventory-configurable-product/Plugin/Model/ResourceModel/Attribute/IsSalableOptionSelectBuilder.php
@@ -7,6 +7,7 @@ declare(strict_types=1);
namespace Magento\InventoryConfigurableProduct\Plugin\Model\ResourceModel\Attribute;
+use Magento\CatalogInventory\Api\StockConfigurationInterface;
use Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\DB\Select;
@@ -38,6 +39,11 @@ class IsSalableOptionSelectBuilder
*/
private $stockIndexTableNameResolver;
+ /**
+ * @var StockConfigurationInterface
+ */
+ private $stockConfig;
+
/**
* @var DefaultStockProviderInterface
*/
@@ -47,17 +53,20 @@ class IsSalableOptionSelectBuilder
* @param StoreManagerInterface $storeManager
* @param StockResolverInterface $stockResolver
* @param StockIndexTableNameResolverInterface $stockIndexTableNameResolver
+ * @param StockConfigurationInterface $stockConfig
* @param DefaultStockProviderInterface $defaultStockProvider
*/
public function __construct(
StoreManagerInterface $storeManager,
StockResolverInterface $stockResolver,
StockIndexTableNameResolverInterface $stockIndexTableNameResolver,
+ StockConfigurationInterface $stockConfig,
DefaultStockProviderInterface $defaultStockProvider = null
) {
$this->storeManager = $storeManager;
$this->stockResolver = $stockResolver;
$this->stockIndexTableNameResolver = $stockIndexTableNameResolver;
+ $this->stockConfig = $stockConfig;
$this->defaultStockProvider = $defaultStockProvider ?: ObjectManager::getInstance()
->get(DefaultStockProviderInterface::class);
}
@@ -77,22 +86,24 @@ class IsSalableOptionSelectBuilder
OptionSelectBuilderInterface $subject,
Select $select
) {
- $websiteCode = $this->storeManager->getWebsite()->getCode();
- $stock = $this->stockResolver->execute(SalesChannelInterface::TYPE_WEBSITE, $websiteCode);
- $stockId = (int)$stock->getStockId();
- if ($stockId === $this->defaultStockProvider->getId()) {
- return $select;
- }
- $stockTable = $this->stockIndexTableNameResolver->execute($stockId);
+ if (!$this->stockConfig->isShowOutOfStock()) {
+ $websiteCode = $this->storeManager->getWebsite()->getCode();
+ $stock = $this->stockResolver->execute(SalesChannelInterface::TYPE_WEBSITE, $websiteCode);
+ $stockId = (int)$stock->getStockId();
+ if ($stockId === $this->defaultStockProvider->getId()) {
+ return $select;
+ }
+ $stockTable = $this->stockIndexTableNameResolver->execute($stockId);
- $select->joinInner(
- ['stock' => $stockTable],
- 'stock.sku = entity.sku',
- []
- )->where(
- 'stock.is_salable = ?',
- 1
- );
+ $select->joinInner(
+ ['stock' => $stockTable],
+ 'stock.sku = entity.sku',
+ []
+ )->where(
+ 'stock.is_salable = ?',
+ 1
+ );
+ }
return $select;
}
is this still reproducible in 2.4.6 GA community ver