magento2-disable-stock-reservation icon indicating copy to clipboard operation
magento2-disable-stock-reservation copied to clipboard

Configurable product is out of stock after place order

Open szymonnosal opened this issue 3 years ago • 8 comments

After upgrading Magento to 2.4.4 (Commerce Edition), we started to have a problem with products. After placing the order, the configurable product is out of stock in the cataloginventory_stock_item table.

On version 2.4.3, everything was fine. However, I tested that with the newest version, too (1.1.4), and it still has the same problem. I noticed the Is_in_stock value would come to zero directly after placing the order from the site.

Did you face that problem?

It is pretty annoying when many products go out of stock after orders are placed.

szymonnosal avatar Aug 30 '22 14:08 szymonnosal

Hello, I have not tested this on 2.4.4 yet so cannot comment.

This would need to be run through in a debugger to see what is occurring.

If anyone has any solutions PRs are welcome

convenient avatar Sep 01 '22 11:09 convenient

Hi @convenient I made a further investigation. That previous message was also based on Magento/Adobe Support.

It looks like the problem is not with the module (at least not directly).

The problem is with the class executed by Plugin: Magento\InventoryConfigurableProduct\Plugin\InventoryApi\UpdateParentStockStatusInLegacyStockPlugin

And problematic class: Magento\ConfigurableProduct\Model\Inventory\ChangeParentStockStatus

That class only checks default stock when we keep it in different store stocks and nothing in the default one since we use MSI.

In our case, when someone bought a product, the configurable one was immediately marked as an out-of-stock by the mentioned Plugin. Variations have stock in non-default default, but that Plugin checks only the default one.

Our implemented fix checks all stocks instead of the default ones, and that solves our issue.

In some issues reported already to the Magento, there is information about the Magento 2.4.4 vanilla installation: https://github.com/magento/magento2/issues/35724 https://github.com/magento/magento2/issues/35494 https://github.com/magento/inventory/issues/3350

szymonnosal avatar Sep 07 '22 05:09 szymonnosal

@szymonnosal whats the fix? we are experiencing the same on 2.4.5

mattyl avatar Sep 14 '22 08:09 mattyl

@mattyl Our solution is not the best one but fits our requirements. I created a new module, where I disabled original plugin, and added the custom one. I know that could be done by patch or preference, but I think that solution is a bit more clear.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Inventory\Model\SourceItem\Command\DecrementSourceItemQty">
        <plugin name="update_parent_configurable_product_stock_status_in_legacy_stock" disabled="true"/>
        <plugin name="custom_update_parent_configurable_product_stock_status_in_legacy_stock"
                type="Project\Inventory\Plugin\InventoryApi\UpdateParentStockStatusInLegacyStockPlugin"/>
    </type>
</config>

Then, a new plugin, which calls the custom ChangeParentStockStatus class

<?php

declare(strict_types=1);

/**
 *  NOTICE OF LICENSE
 *
 *  This source file is released under a commercial license by Lamia Oy.
 *
 * @copyright Copyright (c) Lamia Oy (https://lamia.fi)
 */

namespace Project\Inventory\Plugin\InventoryApi;

use Project\Inventory\Model\Inventory\ChangeParentStockStatus;
use Magento\Inventory\Model\SourceItem\Command\DecrementSourceItemQty;
use Magento\InventoryApi\Api\Data\SourceItemInterface;
use Magento\InventoryCatalogApi\Model\GetProductIdsBySkusInterface;

class UpdateParentStockStatusInLegacyStockPlugin
{
    /**
     * @var ChangeParentStockStatus
     */
    private $changeParentStockStatus;

    /**
     * @var GetProductIdsBySkusInterface
     */
    private $getProductIdsBySkus;

    /**
     * @param GetProductIdsBySkusInterface $getProductIdsBySkus
     * @param ChangeParentStockStatus $changeParentStockStatus
     */
    public function __construct(
        GetProductIdsBySkusInterface $getProductIdsBySkus,
        ChangeParentStockStatus $changeParentStockStatus
    ) {
        $this->getProductIdsBySkus = $getProductIdsBySkus;
        $this->changeParentStockStatus = $changeParentStockStatus;
    }

    /**
     *  Make configurable product out of stock if all its children are out of stock
     *
     * @param DecrementSourceItemQty $subject
     * @param void $result
     * @param SourceItemInterface[] $sourceItemDecrementData
     * @return void
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function afterExecute(DecrementSourceItemQty $subject, $result, array $sourceItemDecrementData): void
    {
        $productIds = [];
        $sourceItems = array_column($sourceItemDecrementData, 'source_item');
        foreach ($sourceItems as $sourceItem) {
            $sku = $sourceItem->getSku();
            $productIds[] = (int)$this->getProductIdsBySkus->execute([$sku])[$sku];
        }
        if ($productIds) {
            $this->changeParentStockStatus->execute($productIds);
        }
    }
}

And finally the problematic class. We are checking all stocks, instead of the default one.

<?php

declare(strict_types=1);

/**
 *  NOTICE OF LICENSE
 *
 *  This source file is released under a commercial license by Lamia Oy.
 *
 * @copyright Copyright (c) Lamia Oy (https://lamia.fi)
 */

namespace Project\Inventory\Model\Inventory;

use Magento\CatalogInventory\Api\Data\StockItemInterface;
use Magento\CatalogInventory\Api\StockConfigurationInterface;
use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory;
use Magento\CatalogInventory\Api\StockItemRepositoryInterface;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\InventoryCatalogApi\Model\GetSkusByProductIdsInterface;
use Magento\InventorySalesApi\Api\AreProductsSalableInterface;

/**
 * Original class @see: \Magento\ConfigurableProduct\Model\Inventory\ChangeParentStockStatus
 * Original functionality check only default stock. The project uses multiple warehouses, and the default always has qty = 0.
 *
 */
class ChangeParentStockStatus
{

    /**
     * @var Configurable
     */
    private $configurableType;

    /**
     * @var StockItemCriteriaInterfaceFactory
     */
    private $criteriaInterfaceFactory;

    /**
     * @var StockItemRepositoryInterface
     */
    private $stockItemRepository;

    /**
     * @var StockConfigurationInterface
     */
    private $stockConfiguration;

    /**
     * Scope config.
     *
     * @var ScopeConfigInterface
     */
    private $scopeConfig;

    /**
     * @var GetSkusByProductIdsInterface
     */
    private $getSkusByProductIds;

    /**
     * @var AreProductsSalableInterface
     */
    private $areProductsSalable;

    /**
     * @param Configurable $configurableType
     * @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory
     * @param StockItemRepositoryInterface $stockItemRepository
     * @param StockConfigurationInterface $stockConfiguration
     * @param ScopeConfigInterface $scopeConfig
     * @param GetSkusByProductIdsInterface $getSkusByProductIds
     * @param AreProductsSalableInterface $areProductsSalable
     */
    public function __construct(
        Configurable $configurableType,
        StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory,
        StockItemRepositoryInterface $stockItemRepository,
        StockConfigurationInterface $stockConfiguration,
        ScopeConfigInterface $scopeConfig,
        GetSkusByProductIdsInterface $getSkusByProductIds,
        AreProductsSalableInterface $areProductsSalable
    ) {
        $this->configurableType = $configurableType;
        $this->criteriaInterfaceFactory = $criteriaInterfaceFactory;
        $this->stockItemRepository = $stockItemRepository;
        $this->stockConfiguration = $stockConfiguration;
        $this->scopeConfig = $scopeConfig;
        $this->getSkusByProductIds = $getSkusByProductIds;
        $this->areProductsSalable = $areProductsSalable;
    }

    /**
     * Update stock status of configurable products based on children's products stock status
     *
     * @param array $childrenIds
     * @return void
     */
    public function execute(array $childrenIds): void
    {
        $parentIds = $this->configurableType->getParentIdsByChild($childrenIds);
        foreach (array_unique($parentIds) as $productId) {
            $this->processStockForParent((int)$productId);
        }
    }

    /**
     * Update stock status of configurable product based on children's products stock status
     *
     * @param int $productId
     * @return void
     */
    private function processStockForParent(int $productId): void
    {
        $criteria = $this->criteriaInterfaceFactory->create();
        $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId());

        $criteria->setProductsFilter($productId);
        $stockItemCollection = $this->stockItemRepository->getList($criteria);
        $allItems = $stockItemCollection->getItems();
        if (empty($allItems)) {
            return;
        }
        $parentStockItem = array_shift($allItems);

        $childrenIsInStock = $this->childrenIsInStock($productId);

        if ($this->isNeedToUpdateParent($parentStockItem, $childrenIsInStock)) {
            $parentStockItem->setIsInStock($childrenIsInStock);
            $parentStockItem->setStockStatusChangedAuto(1);
            $parentStockItem->setStockStatusChangedAutomaticallyFlag(true);
            $this->stockItemRepository->save($parentStockItem);
        }
    }

    /**
     * Check if the parent item should be updated
     *
     * @param StockItemInterface $parentStockItem
     * @param bool $childrenIsInStock
     * @return bool
     */
    private function isNeedToUpdateParent(
        StockItemInterface $parentStockItem,
        bool $childrenIsInStock
    ): bool {
        return $parentStockItem->getIsInStock() !== $childrenIsInStock &&
            ($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto());
    }

    private function childrenIsInStock($productId)
    {
        $childrenIds = $this->configurableType->getChildrenIds($productId);

        $childrenIds = array_shift($childrenIds);

        if (empty($childrenIds)) {
            return false;
        }

        $skus = $this->getSkusByProductIds->execute($childrenIds);

        $stocks = $this->getStocksToCheck();
        foreach ($stocks as $stock) {
            $areSalableResults = $this->areProductsSalable->execute($skus, (int) $stock);
            foreach ($areSalableResults as $productSalable) {
                if ($productSalable->isSalable() === true) {
                    return true;
                }
            }
        }

        return false;

    }

    /**
     * @return string[]
     */
    private function getStocksToCheck()
    {
        $stocks = $this->scopeConfig->getValue('project_catalog/inventory/stocks');

        if(empty($stocks)) {
            return [$this->stockConfiguration->getDefaultScopeId()];
        }

        return explode(',', $stocks);
    }

}

As you can see, the function loads stocks to check from the configuration. The list contains comma-separated ids. That list is sorted by stock, which has the highest probability to contains product :)

That solution is based on some other Magento solutions provided for us by Magento/Adobe support but extended to multi-sources.

If you have any idea how to improve it, please tell :)

szymonnosal avatar Sep 15 '22 13:09 szymonnosal

Thank you! We are experiencing issues with this plugin and after pay - so we may see which is the least worst option - disabling this plugin and living with the annoyances caused by the stock reservation and our setup or having to fix the plugin

On 15 Sep 2022, at 15:37, Szymon @.***> wrote:

@mattyl https://github.com/mattyl Our solution is not the best one but fits our requirements. I created a new module, where I disabled original plugin, and added the custom one. I know that could be done by patch or preference, but I think that solution is a bit more clear.

Then, a new plugin, which calls the custom ChangeParentStockStatus class getProductIdsBySkus = $getProductIdsBySkus; $this->changeParentStockStatus = $changeParentStockStatus; } /** * Make configurable product out of stock if all its children are out of stock * * @param DecrementSourceItemQty $subject * @param void $result * @param SourceItemInterface[] $sourceItemDecrementData * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterExecute(DecrementSourceItemQty $subject, $result, array $sourceItemDecrementData): void { $productIds = []; $sourceItems = array_column($sourceItemDecrementData, 'source_item'); foreach ($sourceItems as $sourceItem) { $sku = $sourceItem->getSku(); $productIds[] = (int)$this->getProductIdsBySkus->execute([$sku])[$sku]; } if ($productIds) { $this->changeParentStockStatus->execute($productIds); } } } And finally the problematic class. We are checking all stocks, instead of the default one. configurableType = $configurableType; $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; $this->stockItemRepository = $stockItemRepository; $this->stockConfiguration = $stockConfiguration; $this->scopeConfig = $scopeConfig; $this->getSkusByProductIds = $getSkusByProductIds; $this->areProductsSalable = $areProductsSalable; } /** * Update stock status of configurable products based on children's products stock status * * @param array $childrenIds * @return void */ public function execute(array $childrenIds): void { $parentIds = $this->configurableType->getParentIdsByChild($childrenIds); foreach (array_unique($parentIds) as $productId) { $this->processStockForParent((int)$productId); } } /** * Update stock status of configurable product based on children's products stock status * * @param int $productId * @return void */ private function processStockForParent(int $productId): void { $criteria = $this->criteriaInterfaceFactory->create(); $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); $criteria->setProductsFilter($productId); $stockItemCollection = $this->stockItemRepository->getList($criteria); $allItems = $stockItemCollection->getItems(); if (empty($allItems)) { return; } $parentStockItem = array_shift($allItems); $childrenIsInStock = $this->childrenIsInStock($productId); if ($this->isNeedToUpdateParent($parentStockItem, $childrenIsInStock)) { $parentStockItem->setIsInStock($childrenIsInStock); $parentStockItem->setStockStatusChangedAuto(1); $parentStockItem->setStockStatusChangedAutomaticallyFlag(true); $this->stockItemRepository->save($parentStockItem); } } /** * Check if the parent item should be updated * * @param StockItemInterface $parentStockItem * @param bool $childrenIsInStock * @return bool */ private function isNeedToUpdateParent( StockItemInterface $parentStockItem, bool $childrenIsInStock ): bool { return $parentStockItem->getIsInStock() !== $childrenIsInStock && ($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto()); } private function childrenIsInStock($productId) { $childrenIds = $this->configurableType->getChildrenIds($productId); $childrenIds = array_shift($childrenIds); if (empty($childrenIds)) { return false; } $skus = $this->getSkusByProductIds->execute($childrenIds); $stocks = $this->getStocksToCheck(); foreach ($stocks as $stock) { $areSalableResults = $this->areProductsSalable->execute($skus, (int) $stock); foreach ($areSalableResults as $productSalable) { if ($productSalable->isSalable() === true) { return true; } } } return false; } /** * @return string[] */ private function getStocksToCheck() { $stocks = $this->scopeConfig->getValue('project_catalog/inventory/stocks'); if(empty($stocks)) { return [$this->stockConfiguration->getDefaultScopeId()]; } return explode(',', $stocks); } } As you can see, the function loads stocks to check from the configuration. The list contains comma-separated ids. That list is sorted by stock, which has the highest probability to contains product :) That solution is based on some other Magento solutions provided for us by Magento/Adobe support but extended to multi-sources. If you have any idea how to improve it, please tell :) — Reply to this email directly, view it on GitHub , or unsubscribe . You are receiving this because you were mentioned.

mattyl avatar Oct 11 '22 08:10 mattyl

Hello,

I got the same issue on my side. I temporary created this patch : AC_FIX_INVENTORY_CONFIGURABLE_STOCK_2.4.5.patch.txt

RonanCapitaine avatar Dec 08 '22 15:12 RonanCapitaine

We have the same issue with group products. After place order group product is OOS even child products are in stock.

any help??

satinderjot-tech avatar Feb 02 '23 12:02 satinderjot-tech

@satinderjot-tech patch for grouped products grouped_stock_patch_245.patch.txt using @RonanCapitaine's method.

msyhr avatar Feb 17 '23 18:02 msyhr