Skip to content

Commit 8c02c91

Browse files
authored
Merge pull request #4380 from magento-tango/PR_2019_06_18
[tango] MAGETWO-99930: [Magento Cloud] Configuarable product stock status stays 'In Stock'
2 parents 01a441d + 73ebe79 commit 8c02c91

File tree

12 files changed

+628
-86
lines changed

12 files changed

+628
-86
lines changed

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ class SaveInventoryDataObserver implements ObserverInterface
3939
*/
4040
private $stockItemValidator;
4141

42+
/**
43+
* @var array
44+
*/
45+
private $parentItemProcessorPool;
46+
4247
/**
4348
* @var array
4449
*/
@@ -77,15 +82,18 @@ class SaveInventoryDataObserver implements ObserverInterface
7782
* @param StockConfigurationInterface $stockConfiguration
7883
* @param StockRegistryInterface $stockRegistry
7984
* @param StockItemValidator $stockItemValidator
85+
* @param array $parentItemProcessorPool
8086
*/
8187
public function __construct(
8288
StockConfigurationInterface $stockConfiguration,
8389
StockRegistryInterface $stockRegistry,
84-
StockItemValidator $stockItemValidator = null
90+
StockItemValidator $stockItemValidator = null,
91+
array $parentItemProcessorPool = []
8592
) {
8693
$this->stockConfiguration = $stockConfiguration;
8794
$this->stockRegistry = $stockRegistry;
8895
$this->stockItemValidator = $stockItemValidator ?: ObjectManager::getInstance()->get(StockItemValidator::class);
96+
$this->parentItemProcessorPool = $parentItemProcessorPool;
8997
}
9098

9199
/**
@@ -99,7 +107,10 @@ public function __construct(
99107
*/
100108
public function execute(EventObserver $observer)
101109
{
110+
/** @var Product $product */
102111
$product = $observer->getEvent()->getProduct();
112+
113+
/** @var Item $stockItem */
103114
$stockItem = $this->getStockItemToBeUpdated($product);
104115

105116
if ($product->getStockData() !== null) {
@@ -108,6 +119,7 @@ public function execute(EventObserver $observer)
108119
}
109120
$this->stockItemValidator->validate($product, $stockItem);
110121
$this->stockRegistry->updateStockItemBySku($product->getSku(), $stockItem);
122+
$this->processParents($product);
111123
}
112124

113125
/**
@@ -156,4 +168,17 @@ private function getStockData(Product $product)
156168
}
157169
return $stockData;
158170
}
171+
172+
/**
173+
* Process stock data for parent products
174+
*
175+
* @param Product $product
176+
* @return void
177+
*/
178+
private function processParents(Product $product)
179+
{
180+
foreach ($this->parentItemProcessorPool as $processor) {
181+
$processor->process($product);
182+
}
183+
}
159184
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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\Model\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+
17+
/**
18+
* Process parent stock item
19+
*/
20+
class ParentItemProcessor
21+
{
22+
/**
23+
* @var Configurable
24+
*/
25+
private $configurableType;
26+
27+
/**
28+
* @var StockItemCriteriaInterfaceFactory
29+
*/
30+
private $criteriaInterfaceFactory;
31+
32+
/**
33+
* @var StockItemRepositoryInterface
34+
*/
35+
private $stockItemRepository;
36+
37+
/**
38+
* @var StockConfigurationInterface
39+
*/
40+
private $stockConfiguration;
41+
42+
/**
43+
* @param Configurable $configurableType
44+
* @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory
45+
* @param StockItemRepositoryInterface $stockItemRepository
46+
* @param StockConfigurationInterface $stockConfiguration
47+
*/
48+
public function __construct(
49+
Configurable $configurableType,
50+
StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory,
51+
StockItemRepositoryInterface $stockItemRepository,
52+
StockConfigurationInterface $stockConfiguration
53+
) {
54+
$this->configurableType = $configurableType;
55+
$this->criteriaInterfaceFactory = $criteriaInterfaceFactory;
56+
$this->stockItemRepository = $stockItemRepository;
57+
$this->stockConfiguration = $stockConfiguration;
58+
}
59+
60+
/**
61+
* Process parent products
62+
*
63+
* @param Product $product
64+
* @return void
65+
*/
66+
public function process(Product $product)
67+
{
68+
$parentIds = $this->configurableType->getParentIdsByChild($product->getId());
69+
foreach ($parentIds as $productId) {
70+
$this->processStockForParent((int)$productId);
71+
}
72+
}
73+
74+
/**
75+
* Change stock item for parent product depending on children stock items
76+
*
77+
* @param int $productId
78+
* @return void
79+
*/
80+
private function processStockForParent(int $productId)
81+
{
82+
$criteria = $this->criteriaInterfaceFactory->create();
83+
$criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId());
84+
85+
$criteria->setProductsFilter($productId);
86+
$stockItemCollection = $this->stockItemRepository->getList($criteria);
87+
$allItems = $stockItemCollection->getItems();
88+
if (empty($allItems)) {
89+
return;
90+
}
91+
$parentStockItem = array_shift($allItems);
92+
93+
$childrenIds = $this->configurableType->getChildrenIds($productId);
94+
$criteria->setProductsFilter($childrenIds);
95+
$stockItemCollection = $this->stockItemRepository->getList($criteria);
96+
$allItems = $stockItemCollection->getItems();
97+
98+
$childrenIsInStock = false;
99+
100+
foreach ($allItems as $childItem) {
101+
if ($childItem->getIsInStock() === true) {
102+
$childrenIsInStock = true;
103+
break;
104+
}
105+
}
106+
107+
if ($this->isNeedToUpdateParent($parentStockItem, $childrenIsInStock)) {
108+
$parentStockItem->setIsInStock($childrenIsInStock);
109+
$parentStockItem->setStockStatusChangedAuto(1);
110+
$this->stockItemRepository->save($parentStockItem);
111+
}
112+
}
113+
114+
/**
115+
* Check is parent item should be updated
116+
*
117+
* @param StockItemInterface $parentStockItem
118+
* @param bool $childrenIsInStock
119+
* @return bool
120+
*/
121+
private function isNeedToUpdateParent(
122+
StockItemInterface $parentStockItem,
123+
bool $childrenIsInStock
124+
): bool {
125+
return $parentStockItem->getIsInStock() !== $childrenIsInStock &&
126+
($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto());
127+
}
128+
}

app/code/Magento/ConfigurableProduct/Model/LinkManagement.php

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,17 @@ public function addChild($sku, $childSku)
131131
throw new StateException(__('Parent product does not have configurable product options'));
132132
}
133133

134-
$attributeIds = [];
134+
$attributeData = [];
135135
foreach ($configurableProductOptions as $configurableProductOption) {
136136
$attributeCode = $configurableProductOption->getProductAttribute()->getAttributeCode();
137137
if (!$child->getData($attributeCode)) {
138138
throw new StateException(__('Child product does not have attribute value %1', $attributeCode));
139139
}
140-
$attributeIds[] = $configurableProductOption->getAttributeId();
140+
$attributeData[$configurableProductOption->getAttributeId()] = [
141+
'position' => $configurableProductOption->getPosition()
142+
];
141143
}
142-
$configurableOptionData = $this->getConfigurableAttributesData($attributeIds);
144+
$configurableOptionData = $this->getConfigurableAttributesData($attributeData);
143145

144146
/** @var \Magento\ConfigurableProduct\Helper\Product\Options\Factory $optionFactory */
145147
$optionFactory = $this->getOptionsFactory();
@@ -203,16 +205,16 @@ private function getOptionsFactory()
203205
/**
204206
* Get Configurable Attribute Data
205207
*
206-
* @param int[] $attributeIds
208+
* @param int[] $attributeData
207209
* @return array
208210
*/
209-
private function getConfigurableAttributesData($attributeIds)
211+
private function getConfigurableAttributesData($attributeData)
210212
{
211213
$configurableAttributesData = [];
212214
$attributeValues = [];
213215
$attributes = $this->attributeFactory->create()
214216
->getCollection()
215-
->addFieldToFilter('attribute_id', $attributeIds)
217+
->addFieldToFilter('attribute_id', array_keys($attributeData))
216218
->getItems();
217219
foreach ($attributes as $attribute) {
218220
foreach ($attribute->getOptions() as $option) {
@@ -229,6 +231,7 @@ private function getConfigurableAttributesData($attributeIds)
229231
'attribute_id' => $attribute->getId(),
230232
'code' => $attribute->getAttributeCode(),
231233
'label' => $attribute->getStoreLabel(),
234+
'position' => $attributeData[$attribute->getId()]['position'],
232235
'values' => $attributeValues,
233236
];
234237
}

app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public function testAddChild()
158158
->getMock();
159159
$optionMock = $this->getMockBuilder(\Magento\ConfigurableProduct\Api\Data\Option::class)
160160
->disableOriginalConstructor()
161-
->setMethods(['getProductAttribute', 'getAttributeId'])
161+
->setMethods(['getProductAttribute', 'getPosition', 'getAttributeId'])
162162
->getMock();
163163
$productAttributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class)
164164
->disableOriginalConstructor()
@@ -216,13 +216,15 @@ public function testAddChild()
216216
$productAttributeMock->expects($this->any())->method('getAttributeCode')->willReturn('color');
217217
$simple->expects($this->any())->method('getData')->willReturn('color');
218218
$optionMock->expects($this->any())->method('getAttributeId')->willReturn('1');
219+
$optionMock->expects($this->any())->method('getPosition')->willReturn('0');
219220

220221
$optionsFactoryMock->expects($this->any())->method('create')->willReturn([$optionMock]);
221222
$attributeFactoryMock->expects($this->any())->method('create')->willReturn($attributeMock);
222223
$attributeMock->expects($this->any())->method('getCollection')->willReturn($attributeCollectionMock);
223224
$attributeCollectionMock->expects($this->any())->method('addFieldToFilter')->willReturnSelf();
224225
$attributeCollectionMock->expects($this->any())->method('getItems')->willReturn([$attributeMock]);
225226

227+
$attributeMock->expects($this->any())->method('getId')->willReturn(1);
226228
$attributeMock->expects($this->any())->method('getOptions')->willReturn([$attributeOptionMock]);
227229

228230
$extensionAttributesMock->expects($this->any())->method('setConfigurableProductOptions');

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@
4343
</argument>
4444
</arguments>
4545
</type>
46+
<type name="Magento\CatalogInventory\Observer\SaveInventoryDataObserver">
47+
<arguments>
48+
<argument name="parentItemProcessorPool" xsi:type="array">
49+
<item name="configurable" xsi:type="object"> Magento\ConfigurableProduct\Model\Inventory\ParentItemProcessor</item>
50+
</argument>
51+
</arguments>
52+
</type>
4653
<type name="Magento\Sales\Model\ResourceModel\Report\Bestsellers">
4754
<arguments>
4855
<argument name="ignoredProductTypes" xsi:type="array">

app/code/Magento/Search/Model/ResourceModel/SynonymReader.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ private function queryByPhrase($phrase)
8787
{
8888
$matchQuery = $this->fullTextSelect->getMatchQuery(
8989
['synonyms' => 'synonyms'],
90-
$phrase,
90+
$this->escapePhrase($phrase),
9191
Fulltext::FULLTEXT_MODE_BOOLEAN
9292
);
9393
$query = $this->getConnection()->select()->from(
@@ -97,6 +97,18 @@ private function queryByPhrase($phrase)
9797
return $this->getConnection()->fetchAll($query);
9898
}
9999

100+
/**
101+
* Cut trailing plus or minus sign, and @ symbol, using of which causes InnoDB to report a syntax error.
102+
*
103+
* @see https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html
104+
* @param string $phrase
105+
* @return string
106+
*/
107+
private function escapePhrase(string $phrase): string
108+
{
109+
return preg_replace('/@+|[@+-]+$|[<>]/', '', $phrase);
110+
}
111+
100112
/**
101113
* A private helper function to retrieve matching synonym groups per scope
102114
*

app/code/Magento/Shipping/Model/Shipping.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,7 @@ public function collectRates(\Magento\Quote\Model\Quote\Address\RateRequest $req
251251
*/
252252
public function collectCarrierRates($carrierCode, $request)
253253
{
254-
/* @var $carrier \Magento\Shipping\Model\Carrier\AbstractCarrier */
255-
$carrier = $this->_carrierFactory->createIfActive($carrierCode, $request->getQuoteStoreId());
254+
$carrier = $this->_carrierFactory->create($carrierCode, $request->getQuoteStoreId());
256255
if (!$carrier) {
257256
return $this;
258257
}

0 commit comments

Comments
 (0)