Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configurable product is out of stock after place order #79

Open
szymonnosal opened this issue Aug 30, 2022 · 8 comments
Open

Configurable product is out of stock after place order #79

szymonnosal opened this issue Aug 30, 2022 · 8 comments
Labels
help wanted Extra attention is needed to triage

Comments

@szymonnosal
Copy link

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.

@convenient
Copy link
Contributor

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 convenient added the help wanted Extra attention is needed label Sep 1, 2022
@szymonnosal
Copy link
Author

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:
magento/magento2#35724
magento/magento2#35494
magento/inventory#3350

@mattyl
Copy link

mattyl commented Sep 14, 2022

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

@szymonnosal
Copy link
Author

@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 :)

@mattyl
Copy link

mattyl commented Oct 11, 2022 via email

@RonanCapitaine
Copy link

Hello,

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

@satinderjot-tech
Copy link

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

any help??

@msyhr
Copy link

msyhr commented Feb 17, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed to triage
Projects
None yet
Development

No branches or pull requests

7 participants