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()
                 );
             }
         }

Product Recommendation module can not get custom attribute on JSON response

SOLUTION:

The current implementation does not support special characters in the website codes. Values like en_US, fr_FR, etc. are not currently supported. 

We are looking into updating our public-facing documentation. In the meantime, as a workaround, they have suggested not to include special characters in the website code. 

Our recommendation is to check the release notes to see what new features are added when Product Recommendations is updated.