Skip to content

Commit a52d069

Browse files
author
Oleksandr Iegorov
committed
MAGETWO-99941: Configuarable product stock status stays 'In Stock'
1 parent 2dd90d0 commit a52d069

File tree

6 files changed

+306
-1
lines changed

6 files changed

+306
-1
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\CatalogInventory\Observer;
9+
10+
use Magento\Catalog\Api\Data\ProductInterface as Product;
11+
12+
/**
13+
* Interface for processing parent items of complex product types
14+
*/
15+
interface ParentItemProcessorInterface
16+
{
17+
/**
18+
* Process stock for parent items
19+
*
20+
* @param Product $product
21+
* @return void
22+
*/
23+
public function process(Product $product);
24+
}

app/code/Magento/CatalogInventory/Observer/SaveInventoryDataObserver.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
use Magento\CatalogInventory\Api\StockRegistryInterface;
1414
use Magento\CatalogInventory\Model\StockItemValidator;
1515
use Magento\Framework\Event\Observer as EventObserver;
16+
use Magento\Framework\Exception\LocalizedException;
17+
use Magento\Framework\Exception\NoSuchEntityException;
1618

1719
/**
1820
* Saves stock data from a product to the Stock Item
@@ -39,6 +41,11 @@ class SaveInventoryDataObserver implements ObserverInterface
3941
*/
4042
private $stockItemValidator;
4143

44+
/**
45+
* @var ParentItemProcessorInterface[]
46+
*/
47+
private $parentItemProcessorPool;
48+
4249
/**
4350
* @var array
4451
*/
@@ -77,15 +84,18 @@ class SaveInventoryDataObserver implements ObserverInterface
7784
* @param StockConfigurationInterface $stockConfiguration
7885
* @param StockRegistryInterface $stockRegistry
7986
* @param StockItemValidator $stockItemValidator
87+
* @param ParentItemProcessorInterface[] $parentItemProcessorPool
8088
*/
8189
public function __construct(
8290
StockConfigurationInterface $stockConfiguration,
8391
StockRegistryInterface $stockRegistry,
84-
StockItemValidator $stockItemValidator = null
92+
StockItemValidator $stockItemValidator = null,
93+
array $parentItemProcessorPool = []
8594
) {
8695
$this->stockConfiguration = $stockConfiguration;
8796
$this->stockRegistry = $stockRegistry;
8897
$this->stockItemValidator = $stockItemValidator ?: ObjectManager::getInstance()->get(StockItemValidator::class);
98+
$this->parentItemProcessorPool = $parentItemProcessorPool;
8999
}
90100

91101
/**
@@ -96,10 +106,15 @@ public function __construct(
96106
*
97107
* @param EventObserver $observer
98108
* @return void
109+
* @throws LocalizedException
110+
* @throws NoSuchEntityException
99111
*/
100112
public function execute(EventObserver $observer)
101113
{
114+
/** @var Product $product */
102115
$product = $observer->getEvent()->getProduct();
116+
117+
/** @var Item $stockItem */
103118
$stockItem = $this->getStockItemToBeUpdated($product);
104119

105120
if ($product->getStockData() !== null) {
@@ -108,6 +123,7 @@ public function execute(EventObserver $observer)
108123
}
109124
$this->stockItemValidator->validate($product, $stockItem);
110125
$this->stockRegistry->updateStockItemBySku($product->getSku(), $stockItem);
126+
$this->processParents($product);
111127
}
112128

113129
/**
@@ -156,4 +172,17 @@ private function getStockData(Product $product)
156172
}
157173
return $stockData;
158174
}
175+
176+
/**
177+
* Process stock data for parent products
178+
*
179+
* @param Product $product
180+
* @return void
181+
*/
182+
private function processParents(Product $product)
183+
{
184+
foreach ($this->parentItemProcessorPool as $processor) {
185+
$processor->process($product);
186+
}
187+
}
159188
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\ConfigurableProduct\Model\Inventory;
9+
10+
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
11+
use Magento\Catalog\Api\Data\ProductInterface as Product;
12+
use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory;
13+
use Magento\CatalogInventory\Api\StockItemRepositoryInterface;
14+
use Magento\CatalogInventory\Api\StockConfigurationInterface;
15+
use Magento\CatalogInventory\Api\Data\StockItemInterface;
16+
use Magento\CatalogInventory\Observer\ParentItemProcessorInterface;
17+
18+
/**
19+
* Process parent stock item
20+
*/
21+
class ParentItemProcessor implements ParentItemProcessorInterface
22+
{
23+
/**
24+
* @var Configurable
25+
*/
26+
private $configurableType;
27+
28+
/**
29+
* @var StockItemCriteriaInterfaceFactory
30+
*/
31+
private $criteriaInterfaceFactory;
32+
33+
/**
34+
* @var StockItemRepositoryInterface
35+
*/
36+
private $stockItemRepository;
37+
38+
/**
39+
* @var StockConfigurationInterface
40+
*/
41+
private $stockConfiguration;
42+
43+
public function __construct(
44+
Configurable $configurableType,
45+
StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory,
46+
StockItemRepositoryInterface $stockItemRepository,
47+
StockConfigurationInterface $stockConfiguration
48+
) {
49+
$this->configurableType = $configurableType;
50+
$this->criteriaInterfaceFactory = $criteriaInterfaceFactory;
51+
$this->stockItemRepository = $stockItemRepository;
52+
$this->stockConfiguration = $stockConfiguration;
53+
}
54+
55+
/**
56+
* Process parent products
57+
*
58+
* @param Product $product
59+
* @return void
60+
*/
61+
public function process(Product $product)
62+
{
63+
$parentIds = $this->configurableType->getParentIdsByChild($product->getId());
64+
foreach ($parentIds as $productId) {
65+
$this->processStockForParent((int)$productId);
66+
}
67+
}
68+
69+
/**
70+
* Change stock item for parent product depending on children stock items
71+
*
72+
* @param int $productId
73+
* @return void
74+
*/
75+
private function processStockForParent(int $productId)
76+
{
77+
$criteria = $this->criteriaInterfaceFactory->create();
78+
$criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId());
79+
80+
$criteria->setProductsFilter($productId);
81+
$stockItemCollection = $this->stockItemRepository->getList($criteria);
82+
$allItems = $stockItemCollection->getItems();
83+
if (empty($allItems)) {
84+
return;
85+
}
86+
$parentStockItem = array_shift($allItems);
87+
88+
$childrenIds = $this->configurableType->getChildrenIds($productId);
89+
$criteria->setProductsFilter($childrenIds);
90+
$stockItemCollection = $this->stockItemRepository->getList($criteria);
91+
$allItems = $stockItemCollection->getItems();
92+
93+
$childrenIsInStock = false;
94+
95+
foreach ($allItems as $childItem) {
96+
if ($childItem->getIsInStock() === true) {
97+
$childrenIsInStock = true;
98+
break;
99+
}
100+
}
101+
102+
if ($this->isNeedToUpdateParent($parentStockItem, $childrenIsInStock)) {
103+
$parentStockItem->setIsInStock($childrenIsInStock);
104+
$parentStockItem->setStockStatusChangedAuto(1);
105+
$this->stockItemRepository->save($parentStockItem);
106+
}
107+
}
108+
109+
/**
110+
* Check is parent item should be updated
111+
*
112+
* @param StockItemInterface $parentStockItem
113+
* @param bool $childrenIsInStock
114+
* @return bool
115+
*/
116+
private function isNeedToUpdateParent(
117+
StockItemInterface $parentStockItem,
118+
bool $childrenIsInStock
119+
): bool {
120+
return $parentStockItem->getIsInStock() !== $childrenIsInStock &&
121+
($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto());
122+
}
123+
}

app/code/Magento/ConfigurableProduct/etc/di.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,11 @@
255255
</argument>
256256
</arguments>
257257
</type>
258+
<type name="Magento\CatalogInventory\Observer\SaveInventoryDataObserver">
259+
<arguments>
260+
<argument name="parentItemProcessorPool" xsi:type="array">
261+
<item name="configurable" xsi:type="object"> Magento\ConfigurableProduct\Model\Inventory\ParentItemProcessor</item>
262+
</argument>
263+
</arguments>
264+
</type>
258265
</config>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\CatalogInventory\Observer;
10+
11+
use Magento\TestFramework\Helper\Bootstrap;
12+
use PHPUnit\Framework\TestCase;
13+
use Magento\Catalog\Api\ProductRepositoryInterface;
14+
use Magento\Catalog\Api\Data\ProductExtensionInterface;
15+
use Magento\Catalog\Api\Data\ProductInterface;
16+
use Magento\CatalogInventory\Api\Data\StockItemInterface;
17+
use Magento\CatalogInventory\Api\StockItemRepositoryInterface;
18+
use Magento\Framework\Exception\NoSuchEntityException;
19+
use Magento\Framework\Exception\InputException;
20+
use Magento\Framework\Exception\StateException;
21+
use Magento\Framework\Exception\CouldNotSaveException;
22+
23+
/**
24+
* Test for SaveInventoryDataObserver
25+
*/
26+
class SaveInventoryDataObserverTest extends TestCase
27+
{
28+
/**
29+
* @var ProductRepositoryInterface
30+
*/
31+
private $productRepository;
32+
33+
/**
34+
* @var StockItemRepositoryInterface
35+
*/
36+
private $stockItemRepository;
37+
38+
/**
39+
* @inheritDoc
40+
*/
41+
protected function setUp()
42+
{
43+
$this->productRepository = Bootstrap::getObjectManager()
44+
->get(ProductRepositoryInterface::class);
45+
$this->stockItemRepository = Bootstrap::getObjectManager()
46+
->get(StockItemRepositoryInterface::class);
47+
}
48+
49+
/**
50+
* Check that parent product will be out of stock
51+
*
52+
* @magentoAppArea adminhtml
53+
* @magentoAppIsolation enabled
54+
* @magentoDataFixture Magento/CatalogInventory/_files/configurable_options_with_low_stock.php
55+
* @throws NoSuchEntityException
56+
* @throws InputException
57+
* @throws StateException
58+
* @throws CouldNotSaveException
59+
* @return void
60+
*/
61+
public function testAutoChangingIsInStockForParent()
62+
{
63+
/** @var ProductInterface $product */
64+
$product = $this->productRepository->get('simple_10');
65+
66+
/** @var ProductExtensionInterface $attributes*/
67+
$attributes = $product->getExtensionAttributes();
68+
69+
/** @var StockItemInterface $stockItem */
70+
$stockItem = $attributes->getStockItem();
71+
$stockItem->setQty(0);
72+
$stockItem->setIsInStock(false);
73+
$attributes->setStockItem($stockItem);
74+
$product->setExtensionAttributes($attributes);
75+
$this->productRepository->save($product);
76+
77+
/** @var ProductInterface $product */
78+
$parentProduct = $this->productRepository->get('configurable');
79+
80+
$parentProductStockItem = $this->stockItemRepository->get(
81+
$parentProduct->getExtensionAttributes()->getStockItem()->getItemId()
82+
);
83+
$this->assertSame(false, $parentProductStockItem->getIsInStock());
84+
}
85+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
use Magento\Catalog\Api\Data\ProductInterface;
9+
use Magento\CatalogInventory\Api\Data\StockItemInterface;
10+
use Magento\CatalogInventory\Api\StockItemRepositoryInterface;
11+
use Magento\TestFramework\Helper\Bootstrap;
12+
13+
require __DIR__ . '/../../../Magento/ConfigurableProduct/_files/product_configurable.php';
14+
15+
$objectManager = Bootstrap::getObjectManager();
16+
17+
/** @var StockItemRepositoryInterface $stockItemRepository */
18+
$stockItemRepository = $objectManager->get(StockItemRepositoryInterface::class);
19+
20+
/** @var ProductInterface $product */
21+
$product = $productRepository->get('simple_10');
22+
23+
/** @var StockItemInterface $stockItem */
24+
$stockItem = $product->getExtensionAttributes()->getStockItem();
25+
$stockItem->setIsInStock(true)
26+
->setQty(1);
27+
$stockItemRepository->save($stockItem);
28+
29+
/** @var ProductInterface $product */
30+
$product = $productRepository->get('simple_20');
31+
32+
/** @var StockItemInterface $stockItem */
33+
$stockItem = $product->getExtensionAttributes()->getStockItem();
34+
$stockItem->setIsInStock(false)
35+
->setQty(0);
36+
$stockItemRepository->save($stockItem);
37+

0 commit comments

Comments
 (0)