-
Notifications
You must be signed in to change notification settings - Fork 63
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
Comments
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 |
Hi @convenient It looks like the problem is not with the module (at least not directly). The problem is with the class executed by Plugin: And problematic class: 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: |
@szymonnosal whats the fix? we are experiencing the same on 2.4.5 |
@mattyl <?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 <?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 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 :) |
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.
<?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 :)
—
Reply to this email directly, view it on GitHub <#79 (comment)>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AADMWA5GRLRU2GD5U43ML43V6MRAHANCNFSM6AAAAAAQAPDH2Q>.
You are receiving this because you were mentioned.
|
Hello, I got the same issue on my side. I temporary created this patch : |
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 patch for grouped products |
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.
The text was updated successfully, but these errors were encountered: