Skip to content

Commit 1c9a4dd

Browse files
committed
ACP2E-262: Bundle product status in backend does not change automatically
1 parent 26edafb commit 1c9a4dd

File tree

10 files changed

+411
-85
lines changed

10 files changed

+411
-85
lines changed
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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\Bundle\Model\Inventory;
9+
10+
use Magento\Bundle\Model\Product\Type;
11+
use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink;
12+
use Magento\CatalogInventory\Api\Data\StockItemInterface;
13+
use Magento\CatalogInventory\Api\StockConfigurationInterface;
14+
use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory;
15+
use Magento\CatalogInventory\Api\StockItemRepositoryInterface;
16+
use Magento\CatalogInventory\Api\StockRegistryInterface;
17+
18+
/***
19+
* Update stock status of bundle products based on children products stock status
20+
*/
21+
class ChangeParentStockStatus
22+
{
23+
/**
24+
* @var StockRegistryInterface
25+
*/
26+
private $stockRegistry;
27+
28+
/**
29+
* @var Type
30+
*/
31+
private $bundleType;
32+
33+
/**
34+
* @var StockItemCriteriaInterfaceFactory
35+
*/
36+
private $criteriaInterfaceFactory;
37+
38+
/**
39+
* @var StockItemRepositoryInterface
40+
*/
41+
private $stockItemRepository;
42+
43+
/**
44+
* @var StockConfigurationInterface
45+
*/
46+
private $stockConfiguration;
47+
48+
/**
49+
* @var ProductWebsiteLink
50+
*/
51+
private $productWebsiteLink;
52+
53+
/**
54+
* @param StockRegistryInterface $stockRegistry
55+
* @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory
56+
* @param StockItemRepositoryInterface $stockItemRepository
57+
* @param StockConfigurationInterface $stockConfiguration
58+
* @param Type $bundleType
59+
* @param ProductWebsiteLink $productWebsiteLink
60+
*/
61+
public function __construct(
62+
StockRegistryInterface $stockRegistry,
63+
StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory,
64+
StockItemRepositoryInterface $stockItemRepository,
65+
StockConfigurationInterface $stockConfiguration,
66+
Type $bundleType,
67+
ProductWebsiteLink $productWebsiteLink
68+
) {
69+
$this->stockRegistry = $stockRegistry;
70+
$this->bundleType = $bundleType;
71+
$this->criteriaInterfaceFactory = $criteriaInterfaceFactory;
72+
$this->stockItemRepository = $stockItemRepository;
73+
$this->stockConfiguration = $stockConfiguration;
74+
$this->productWebsiteLink = $productWebsiteLink;
75+
}
76+
77+
/**
78+
* Update stock status of bundle products based on children products stock status
79+
*
80+
* @param array $childrenIds
81+
* @return void
82+
*/
83+
public function execute(array $childrenIds): void
84+
{
85+
$parentIds = $this->bundleType->getParentIdsByChild($childrenIds);
86+
foreach (array_unique($parentIds) as $productId) {
87+
$this->processStockForParent((int)$productId);
88+
}
89+
}
90+
91+
/**
92+
* Update stock status of bundle product based on children products stock status
93+
*
94+
* @param int $productId
95+
* @return void
96+
*/
97+
private function processStockForParent(int $productId): void
98+
{
99+
$criteria = $this->criteriaInterfaceFactory->create();
100+
$criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId());
101+
102+
$criteria->setProductsFilter($productId);
103+
$stockItemCollection = $this->stockItemRepository->getList($criteria);
104+
$allItems = $stockItemCollection->getItems();
105+
if (empty($allItems)) {
106+
return;
107+
}
108+
$parentStockItem = array_shift($allItems);
109+
$childrenIsInStock = $this->isChildrenInStock($productId);
110+
111+
if ($this->isNeedToUpdateParent($parentStockItem, $childrenIsInStock)) {
112+
$parentStockItem->setIsInStock($childrenIsInStock);
113+
$parentStockItem->setStockStatusChangedAuto(1);
114+
$this->stockItemRepository->save($parentStockItem);
115+
}
116+
}
117+
118+
/**
119+
* Check if any of bundle children products is in stock
120+
*
121+
* @param int $productId
122+
* @return bool
123+
*/
124+
private function isChildrenInStock(int $productId) : bool
125+
{
126+
$childrenIsInStock = false;
127+
$childrenIds = $this->bundleType->getChildrenIds($productId, true);
128+
$websiteIds = $this->productWebsiteLink->getWebsiteIdsByProductId($productId);
129+
//prepend global scope
130+
array_unshift($websiteIds, null);
131+
foreach ($childrenIds as $childrenIdsPerOption) {
132+
$childrenIsInStock = false;
133+
foreach ($childrenIdsPerOption as $id) {
134+
foreach ($websiteIds as $scopeId) {
135+
if ((int)$this->stockRegistry->getProductStockStatus($id, $scopeId) === 1) {
136+
$childrenIsInStock = true;
137+
break 2;
138+
}
139+
}
140+
}
141+
if (!$childrenIsInStock) {
142+
break;
143+
}
144+
}
145+
146+
return $childrenIsInStock;
147+
}
148+
149+
/**
150+
* Check if parent item should be updated
151+
*
152+
* @param StockItemInterface $parentStockItem
153+
* @param bool $childrenIsInStock
154+
* @return bool
155+
*/
156+
private function isNeedToUpdateParent(
157+
StockItemInterface $parentStockItem,
158+
bool $childrenIsInStock
159+
): bool {
160+
return $parentStockItem->getIsInStock() !== $childrenIsInStock &&
161+
($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto());
162+
}
163+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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\Bundle\Model\Inventory;
9+
10+
use Magento\Catalog\Api\Data\ProductInterface as Product;
11+
use Magento\CatalogInventory\Observer\ParentItemProcessorInterface;
12+
13+
/**
14+
* Bundle product stock item processor
15+
*/
16+
class ParentItemProcessor implements ParentItemProcessorInterface
17+
{
18+
/**
19+
* @var ChangeParentStockStatus
20+
*/
21+
private $changeParentStockStatus;
22+
23+
/**
24+
* @param ChangeParentStockStatus $changeParentStockStatus
25+
*/
26+
public function __construct(
27+
ChangeParentStockStatus $changeParentStockStatus
28+
) {
29+
$this->changeParentStockStatus = $changeParentStockStatus;
30+
}
31+
32+
/**
33+
* @inheritdoc
34+
*/
35+
public function process(Product $product)
36+
{
37+
$this->changeParentStockStatus->execute([$product->getId()]);
38+
}
39+
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,4 +278,11 @@
278278
</argument>
279279
</arguments>
280280
</type>
281+
<type name="Magento\CatalogInventory\Observer\SaveInventoryDataObserver">
282+
<arguments>
283+
<argument name="parentItemProcessorPool" xsi:type="array">
284+
<item name="bundle" xsi:type="object">Magento\Bundle\Model\Inventory\ParentItemProcessor</item>
285+
</argument>
286+
</arguments>
287+
</type>
281288
</config>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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\BundleImportExport\Plugin\Import\Product;
9+
10+
use Magento\CatalogImportExport\Model\StockItemImporterInterface;
11+
use Magento\Bundle\Model\Inventory\ChangeParentStockStatus;
12+
13+
/**
14+
* Update bundle products stock item status based on children products stock status after import
15+
*/
16+
class UpdateBundleProductsStockItemStatusPlugin
17+
{
18+
/**
19+
* @var ChangeParentStockStatus
20+
*/
21+
private $changeParentStockStatus;
22+
23+
/**
24+
* @param ChangeParentStockStatus $changeParentStockStatus
25+
*/
26+
public function __construct(
27+
ChangeParentStockStatus $changeParentStockStatus
28+
) {
29+
$this->changeParentStockStatus = $changeParentStockStatus;
30+
}
31+
32+
/**
33+
* Update bundle products stock item status based on children products stock status after import
34+
*
35+
* @param StockItemImporterInterface $subject
36+
* @param mixed $result
37+
* @param array $stockData
38+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
39+
*/
40+
public function afterImport(
41+
StockItemImporterInterface $subject,
42+
$result,
43+
array $stockData
44+
): void {
45+
if ($stockData) {
46+
$this->changeParentStockStatus->execute(array_column($stockData, 'product_id'));
47+
}
48+
}
49+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,9 @@
1313
</argument>
1414
</arguments>
1515
</type>
16+
<type name="Magento\CatalogImportExport\Model\StockItemImporterInterface">
17+
<plugin name="update_bundle_products_stock_item_status"
18+
type="Magento\BundleImportExport\Plugin\Import\Product\UpdateBundleProductsStockItemStatusPlugin"
19+
sortOrder="200"/>
20+
</type>
1621
</config>

dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@
99
Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products.php');
1010

1111
$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
12+
$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);
1213

1314
$productIds = range(10, 12, 1);
1415
foreach ($productIds as $productId) {
16+
$product = $productRepository->getById($productId, true, null, true);
17+
if ((int) $product->getStatus() === \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED) {
18+
$product->unlockAttribute('status')
19+
->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED);
20+
$product->save();
21+
}
1522
/** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */
1623
$stockItem = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Item::class);
1724
$stockItem->load($productId, 'product_id');
@@ -167,7 +174,6 @@
167174
]
168175
]
169176
);
170-
$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);
171177

172178
if ($product->getBundleOptionsData()) {
173179
$options = [];

0 commit comments

Comments
 (0)