Search

Mar 28, 2023

Product with "Salable Qty of 0" and "Out of stock" status shows "In Stock" on product detail page (magento 2.4.3 2.4.4)

Steps by steps:

1. Went to admin page

2. Set some SKUs to `out of stock` status

3. Went to detail page

4. Selected color: Mellow Mauve

5. Expected result: Sizes S, M, L are disabled

Actual result: Sizes S, M, L still enable

6. Tried to add to cart product Mellow Mauve (S, M, L)

7. Got the message: There are no source items with the in stock status


SOLUTION 1:

The issue is resolved by disabling Manage Stock for parent product. Edit product -> Advanced Inventory -> Manage Stock -> set No


SOLUTION 2:

Front end Product shows OUT OF STOCK after we changed product to IN STOCK in admin page

Follow the patch

diff --git a/vendor/magento/module-catalog/Model/ResourceModel/GetProductTypeById.php b/vendor/magento/module-catalog/Model/ResourceModel/GetProductTypeById.php
new file mode 100644
index 000000000000..bd62fedb457a
--- /dev/null
+++ b/vendor/magento/module-catalog/Model/ResourceModel/GetProductTypeById.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+declare (strict_types=1);
+namespace Magento\Catalog\Model\ResourceModel;
+
+use Magento\Catalog\Api\Data\ProductInterface;
+use Magento\Framework\App\ResourceConnection;
+
+/**
+ * Get product type ID by product ID.
+ */
+class GetProductTypeById
+{
+    /**
+     * @var ResourceConnection
+     */
+    private $resource;
+
+    /**
+     * @param ResourceConnection $resource
+     */
+    public function __construct(
+        ResourceConnection $resource
+    ) {
+        $this->resource = $resource;
+    }
+
+    /**
+     * Retrieve product type by its product ID
+     *
+     * @param int $productId
+     * @return string
+     */
+    public function execute(int $productId): string
+    {
+        $connection = $this->resource->getConnection();
+        $productTable = $this->resource->getTableName('catalog_product_entity');
+
+        $select = $connection->select()
+            ->from(
+                $productTable,
+                ProductInterface::TYPE_ID
+            )->where('entity_id = ?', $productId);
+
+        $result = $connection->fetchOne($select);
+        return $result ?: '';
+    }
+}
diff --git a/vendor/magento/module-configurable-product/Model/Inventory/ChangeParentStockStatus.php b/vendor/magento/module-configurable-product/Model/Inventory/ChangeParentStockStatus.php
index 9bb4659b31db..4ad15ea905f0 100644
--- a/vendor/magento/module-configurable-product/Model/Inventory/ChangeParentStockStatus.php
+++ b/vendor/magento/module-configurable-product/Model/Inventory/ChangeParentStockStatus.php
@@ -106,6 +106,7 @@ private function processStockForParent(int $productId): void
         if ($this->isNeedToUpdateParent($parentStockItem, $childrenIsInStock)) {
             $parentStockItem->setIsInStock($childrenIsInStock);
             $parentStockItem->setStockStatusChangedAuto(1);
+            $parentStockItem->setStockStatusChangedAutomaticallyFlag(true);
             $this->stockItemRepository->save($parentStockItem);
         }
     }
diff --git a/vendor/magento/module-configurable-product/Model/Plugin/UpdateStockChangedAuto.php b/vendor/magento/module-configurable-product/Model/Plugin/UpdateStockChangedAuto.php
new file mode 100644
index 000000000000..c5a0cd5eae7f
--- /dev/null
+++ b/vendor/magento/module-configurable-product/Model/Plugin/UpdateStockChangedAuto.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+declare (strict_types=1);
+namespace Magento\ConfigurableProduct\Model\Plugin;
+
+use Magento\Catalog\Model\ResourceModel\GetProductTypeById;
+use Magento\CatalogInventory\Model\ResourceModel\Stock\Item as ItemResourceModel;
+use Magento\Framework\Model\AbstractModel as StockItem;
+use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
+
+/**
+ * Updates stock_status_changed_auto setting for configurable product when it was saved manually
+ */
+class UpdateStockChangedAuto
+{
+    /**
+     * @var GetProductTypeById
+     */
+    private $getProductTypeById;
+
+    /**
+     * @param GetProductTypeById $getProductTypeById
+     */
+    public function __construct(GetProductTypeById $getProductTypeById)
+    {
+        $this->getProductTypeById = $getProductTypeById;
+    }
+
+    /**
+     * Updates stock_status_changed_auto for configurable product
+     *
+     * @param ItemResourceModel $subject
+     * @param StockItem $stockItem
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function beforeSave(ItemResourceModel $subject, StockItem $stockItem): void
+    {
+        if (!$stockItem->getIsInStock() &&
+            !$stockItem->hasStockStatusChangedAutomaticallyFlag() &&
+            $this->getProductTypeById->execute($stockItem->getProductId()) == Configurable::TYPE_CODE
+        ) {
+            $stockItem->setStockStatusChangedAuto(0);
+        }
+    }
+}
diff --git a/vendor/magento/module-configurable-product/etc/di.xml b/vendor/magento/module-configurable-product/etc/di.xml
index 270e8ec74609..16e3aaafc9cb 100644
--- a/vendor/magento/module-configurable-product/etc/di.xml
+++ b/vendor/magento/module-configurable-product/etc/di.xml
@@ -280,4 +280,7 @@
             <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Json</argument>
         </arguments>
     </type>
+    <type name="Magento\CatalogInventory\Model\ResourceModel\Stock\Item">
+        <plugin name="updateStockChangedAuto" type="Magento\ConfigurableProduct\Model\Plugin\UpdateStockChangedAuto" />
+    </type>
 </config>
diff --git a/vendor/magento/module-inventory-configurable-product/Plugin/CatalogInventory/UpdateSourceItemAtLegacyStockItemSavePlugin.php b/vendor/magento/module-inventory-configurable-product/Plugin/CatalogInventory/UpdateSourceItemAtLegacyStockItemSavePlugin.php
new file mode 100644
index 00000000000..6674af98b6c
--- /dev/null
+++ b/vendor/magento/module-inventory-configurable-product/Plugin/CatalogInventory/UpdateSourceItemAtLegacyStockItemSavePlugin.php
@@ -0,0 +1,144 @@
+<?php
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+declare(strict_types=1);
+
+namespace Magento\InventoryConfigurableProduct\Plugin\CatalogInventory;
+
+use Magento\Catalog\Model\ResourceModel\GetProductTypeById;
+use Magento\CatalogInventory\Model\ResourceModel\Stock\Item as ItemResourceModel;
+use Magento\Framework\Model\AbstractModel as StockItem;
+use Magento\InventoryCatalog\Model\ResourceModel\SetDataToLegacyStockStatus;
+use Magento\InventoryCatalogApi\Model\GetSkusByProductIdsInterface;
+use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
+use Magento\CatalogInventory\Model\Stock;
+use Magento\InventorySalesApi\Api\AreProductsSalableInterface;
+use Magento\InventoryConfiguration\Model\GetLegacyStockItem;
+
+/**
+ * Class provides after Plugin on Magento\CatalogInventory\Model\ResourceModel\Stock\Item::save
+ * to update legacy stock status for configurable product
+ */
+class UpdateSourceItemAtLegacyStockItemSavePlugin
+{
+    /**
+     * @var GetProductTypeById
+     */
+    private $getProductTypeById;
+
+    /**
+     * @var SetDataToLegacyStockStatus
+     */
+    private $setDataToLegacyStockStatus;
+
+    /**
+     * @var GetSkusByProductIdsInterface
+     */
+    private $getSkusByProductIds;
+
+    /**
+     * @var Configurable
+     */
+    private $configurableType;
+
+    /**
+     * @var AreProductsSalableInterface
+     */
+    private $areProductsSalable;
+
+    /**
+     * @var GetLegacyStockItem
+     */
+    private $getLegacyStockItem;
+
+    /**
+     * @param GetProductTypeById $getProductTypeById
+     * @param SetDataToLegacyStockStatus $setDataToLegacyStockStatus
+     * @param GetSkusByProductIdsInterface $getSkusByProductIds
+     * @param Configurable $configurableType
+     * @param AreProductsSalableInterface $areProductsSalable
+     * @param GetLegacyStockItem $getLegacyStockItem
+     */
+    public function __construct(
+        GetProductTypeById $getProductTypeById,
+        SetDataToLegacyStockStatus $setDataToLegacyStockStatus,
+        GetSkusByProductIdsInterface $getSkusByProductIds,
+        Configurable $configurableType,
+        AreProductsSalableInterface $areProductsSalable,
+        GetLegacyStockItem $getLegacyStockItem
+    ) {
+        $this->getProductTypeById = $getProductTypeById;
+        $this->setDataToLegacyStockStatus = $setDataToLegacyStockStatus;
+        $this->getSkusByProductIds = $getSkusByProductIds;
+        $this->configurableType = $configurableType;
+        $this->areProductsSalable = $areProductsSalable;
+        $this->getLegacyStockItem = $getLegacyStockItem;
+    }
+
+    /**
+     * Update source item for legacy stock of a configurable product
+     *
+     * @param ItemResourceModel $subject
+     * @param ItemResourceModel $result
+     * @param StockItem $stockItem
+     * @return void
+     * @throws Exception
+     *
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function afterSave(ItemResourceModel $subject, ItemResourceModel $result, StockItem $stockItem): void
+    {
+        if ($stockItem->getIsInStock() &&
+            $this->getProductTypeById->execute($stockItem->getProductId()) === Configurable::TYPE_CODE
+        ) {
+            $productSku = $this->getSkusByProductIds
+                ->execute([$stockItem->getProductId()])[$stockItem->getProductId()];
+
+            if ($stockItem->getStockStatusChangedAuto() ||
+                ($this->stockStatusChange($productSku) && $this->hasChildrenInStock($stockItem->getProductId()))
+            ) {
+                $this->setDataToLegacyStockStatus->execute(
+                    $productSku,
+                    (float) $stockItem->getQty(),
+                    Stock::STOCK_IN_STOCK
+                );
+            }
+        }
+    }
+
+    /**
+     * Checks if configurable product stock item status was changed
+     *
+     * @param string $sku
+     * @return bool
+     */
+    private function stockStatusChange(string $sku): bool
+    {
+        return $this->getLegacyStockItem->execute($sku)->getIsInStock() == Stock::STOCK_OUT_OF_STOCK;
+    }
+
+    /**
+     * Checks if configurable has salable options
+     *
+     * @param int $productId
+     * @return bool
+     */
+    private function hasChildrenInStock(int $productId): bool
+    {
+        $childrenIds = $this->configurableType->getChildrenIds($productId);
+        if (empty($childrenIds)) {
+            return false;
+        }
+        $skus = $this->getSkusByProductIds->execute(array_shift($childrenIds));
+        $areSalableResults = $this->areProductsSalable->execute($skus, Stock::DEFAULT_STOCK_ID);
+        foreach ($areSalableResults as $productSalable) {
+            if ($productSalable->isSalable() === true) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/vendor/magento/module-inventory-configurable-product/composer.json b/vendor/magento/module-inventory-configurable-product/composer.json
index 3855b9d6d11..adca1ede518 100644
--- a/vendor/magento/module-inventory-configurable-product/composer.json
+++ b/vendor/magento/module-inventory-configurable-product/composer.json
@@ -11,6 +11,8 @@
         "php": "~7.4.0||~8.1.0",
         "magento/framework": "*",
         "magento/module-catalog": "*",
+        "magento/module-inventory-catalog": "*",
+        "magento/module-inventory-configuration": "*",
         "magento/module-inventory-sales-api": "1.2.*",
         "magento/module-inventory-catalog-api": "1.3.*",
         "magento/module-inventory-indexer": "2.1.*",
diff --git a/vendor/magento/module-inventory-configurable-product/etc/di.xml b/vendor/magento/module-inventory-configurable-product/etc/di.xml
index 7b32be4f885..a3fc8ce2038 100644
--- a/vendor/magento/module-inventory-configurable-product/etc/di.xml
+++ b/vendor/magento/module-inventory-configurable-product/etc/di.xml
@@ -40,4 +40,8 @@
         <plugin name="update_parent_configurable_product_stock_status_in_legacy_stock"
                 type="Magento\InventoryConfigurableProduct\Plugin\InventoryApi\UpdateParentStockStatusInLegacyStockPlugin"/>
     </type>
+    <type name="Magento\CatalogInventory\Model\ResourceModel\Stock\Item">
+        <plugin name="update_source_stock_for_configurable_product"
+                type="Magento\InventoryConfigurableProduct\Plugin\CatalogInventory\UpdateSourceItemAtLegacyStockItemSavePlugin" />
+    </type>
 </config>


SOLUTION 3:

Follow the patch

diff --git a/vendor/magento/module-swatches/view/base/web/js/swatch-renderer.js b/vendor/magento/module-swatches/view/base/web/js/swatch-renderer.js
index 6782a87b02a9..06897c449734 100644
--- a/vendor/magento/module-swatches/view/base/web/js/swatch-renderer.js
+++ b/vendor/magento/module-swatches/view/base/web/js/swatch-renderer.js
@@ -496,6 +496,27 @@ define([
             $widget._EmulateSelected($widget._getSelectedAttributes());
         },

+        disableSwatchForOutOfStockProducts: function () {
+            let $widget = this, container = this.element;
+
+            $.each(this.options.jsonConfig.attributes, function () {
+                let item = this;
+
+                if ($widget.options.jsonConfig.canDisplayShowOutOfStockStatus) {
+                    let salableProducts = $widget.options.jsonConfig.salable[item.id],
+                        swatchOptions = container.find('.swatch-option');
+
+                    swatchOptions.each(function (key, value) {
+                        let optionId = $(value).data('option-id');
+
+                        if (!salableProducts.hasOwnProperty(optionId)) {
+                            $(value).attr('disabled', true).addClass('disabled');
+                        }
+                    });
+                }
+            });
+        },
+
         /**
          * Render swatch options by part of config
          *
@@ -886,6 +907,7 @@ define([
                 .attr('disabled', true)
                 .addClass('disabled')
                 .attr('tabindex', '-1');
+            this.disableSwatchForOutOfStockProducts();
         },

         /**
diff --git a/vendor/magento/module-inventory-catalog/Plugin/CatalogInventory/Api/StockRegistry/AdaptUpdateStockStatusBySkuPlugin.php b/vendor/magento/module-inventory-catalog/Plugin/CatalogInventory/Api/StockRegistry/AdaptUpdateStockStatusBySkuPlugin.php
index afe66fea814..b2ab3d2b5f8 100644
--- a/vendor/magento/module-inventory-catalog/Plugin/CatalogInventory/Api/StockRegistry/AdaptUpdateStockStatusBySkuPlugin.php
+++ b/vendor/magento/module-inventory-catalog/Plugin/CatalogInventory/Api/StockRegistry/AdaptUpdateStockStatusBySkuPlugin.php
@@ -11,6 +11,7 @@
 use Magento\CatalogInventory\Model\Stock;
 use Magento\InventoryCatalog\Model\ResourceModel\SetDataToLegacyStockStatus;
 use Magento\InventoryCatalogApi\Model\GetProductTypesBySkusInterface;
+use Magento\InventoryConfiguration\Model\GetLegacyStockItem;
 use Magento\InventoryConfiguration\Model\LegacyStockItem\CacheStorage;
 use Magento\InventoryConfigurationApi\Api\GetStockItemConfigurationInterface;
 use Magento\InventoryConfigurationApi\Model\IsSourceItemManagementAllowedForProductTypeInterface;
@@ -42,25 +43,33 @@ class AdaptUpdateStockStatusBySkuPlugin
      */
     private $legacyStockItemCacheStorage;

+    /**
+     * @var GetLegacyStockItem
+     */
+    private $getLegacyStockItem;
+
     /**
      * @param SetDataToLegacyStockStatus $setDataToLegacyStockStatus
      * @param GetProductTypesBySkusInterface $getProductTypesBySkus
      * @param IsSourceItemManagementAllowedForProductTypeInterface $isSourceItemManagementAllowedForProductType
      * @param GetStockItemConfigurationInterface $getStockItemConfiguration
      * @param CacheStorage $legacyStockItemCacheStorage
+     * @param GetLegacyStockItem $getLegacyStockItem
      */
     public function __construct(
         SetDataToLegacyStockStatus $setDataToLegacyStockStatus,
         GetProductTypesBySkusInterface $getProductTypesBySkus,
         IsSourceItemManagementAllowedForProductTypeInterface $isSourceItemManagementAllowedForProductType,
         GetStockItemConfigurationInterface $getStockItemConfiguration,
-        CacheStorage $legacyStockItemCacheStorage
+        CacheStorage $legacyStockItemCacheStorage,
+        GetLegacyStockItem $getLegacyStockItem
     ) {
         $this->setDataToLegacyStockStatus = $setDataToLegacyStockStatus;
         $this->getProductTypesBySkus = $getProductTypesBySkus;
         $this->isSourceItemManagementAllowedForProductType = $isSourceItemManagementAllowedForProductType;
         $this->getStockItemConfiguration = $getStockItemConfiguration;
         $this->legacyStockItemCacheStorage = $legacyStockItemCacheStorage;
+        $this->getLegacyStockItem = $getLegacyStockItem;
     }

     /**
@@ -82,21 +91,23 @@ public function afterUpdateStockItemBySku(
         // Remove cache to get updated legacy stock item on the next request.
         $this->legacyStockItemCacheStorage->delete($productSku);

+        $updatedStockItem = $this->getLegacyStockItem->execute($productSku);
+
         $productType = $this->getProductTypesBySkus->execute([$productSku])[$productSku];

         $stockItemConfiguration = $this->getStockItemConfiguration->execute($productSku, Stock::DEFAULT_STOCK_ID);
         if ($stockItemConfiguration->isManageStock() === false
             || $stockItemConfiguration->isUseConfigManageStock() === false
         ) {
-            $this->setDataToLegacyStockStatus->execute($productSku, (float)$stockItem->getQty(), 1);
+            $this->setDataToLegacyStockStatus->execute($productSku, (float)$updatedStockItem->getQty(), 1);
         } else {
             if ($this->isSourceItemManagementAllowedForProductType->execute($productType)
-                && $stockItem->getQty() !== null
+                && $updatedStockItem->getQty() !== null
             ) {
                 $this->setDataToLegacyStockStatus->execute(
                     $productSku,
-                    (float)$stockItem->getQty(),
-                    $stockItem->getIsInStock()
+                    (float)$updatedStockItem->getQty(),
+                    $updatedStockItem->getIsInStock()
                 );
             }
         }

No comments:

Post a Comment