inventory icon indicating copy to clipboard operation
inventory copied to clipboard

MSI does not respect the "Display out of stock products" setting

Open sydekumf opened this issue 4 years ago • 2 comments

MSI does not respect the "Display out of stock products" setting

Preconditions (*)

  1. Magento 2.3.5 with MSI activated

Steps to reproduce (*)

  1. Create a configurable with 2 simples
  2. Set qty > 0 for one simple, the other has qty = 0 and is_salable = 0
  3. Set "Display out of stock products" = 1 in Store -> Configuration -> Catalog -> Inventory

Expected result (*)

  1. 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 (*)

  1. 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 Bildschirmfoto 2020-05-14 um 17 18 58

With bugfix Bildschirmfoto 2020-05-14 um 17 19 21

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 avatar May 14 '20 17:05 sydekumf

@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;
     }

ThisIsRuddy avatar Feb 19 '21 17:02 ThisIsRuddy

is this still reproducible in 2.4.6 GA community ver

mrtuvn avatar Apr 11 '23 00:04 mrtuvn