Skip to content

Commit

Permalink
Merge pull request magento#4380 from magento-tango/PR_2019_06_18
Browse files Browse the repository at this point in the history
[tango] MAGETWO-99930: [Magento Cloud] Configuarable product stock status stays 'In Stock'
  • Loading branch information
viktym authored Jun 19, 2019
2 parents 01a441d + 73ebe79 commit 8c02c91
Show file tree
Hide file tree
Showing 12 changed files with 628 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ class SaveInventoryDataObserver implements ObserverInterface
*/
private $stockItemValidator;

/**
* @var array
*/
private $parentItemProcessorPool;

/**
* @var array
*/
Expand Down Expand Up @@ -77,15 +82,18 @@ class SaveInventoryDataObserver implements ObserverInterface
* @param StockConfigurationInterface $stockConfiguration
* @param StockRegistryInterface $stockRegistry
* @param StockItemValidator $stockItemValidator
* @param array $parentItemProcessorPool
*/
public function __construct(
StockConfigurationInterface $stockConfiguration,
StockRegistryInterface $stockRegistry,
StockItemValidator $stockItemValidator = null
StockItemValidator $stockItemValidator = null,
array $parentItemProcessorPool = []
) {
$this->stockConfiguration = $stockConfiguration;
$this->stockRegistry = $stockRegistry;
$this->stockItemValidator = $stockItemValidator ?: ObjectManager::getInstance()->get(StockItemValidator::class);
$this->parentItemProcessorPool = $parentItemProcessorPool;
}

/**
Expand All @@ -99,7 +107,10 @@ public function __construct(
*/
public function execute(EventObserver $observer)
{
/** @var Product $product */
$product = $observer->getEvent()->getProduct();

/** @var Item $stockItem */
$stockItem = $this->getStockItemToBeUpdated($product);

if ($product->getStockData() !== null) {
Expand All @@ -108,6 +119,7 @@ public function execute(EventObserver $observer)
}
$this->stockItemValidator->validate($product, $stockItem);
$this->stockRegistry->updateStockItemBySku($product->getSku(), $stockItem);
$this->processParents($product);
}

/**
Expand Down Expand Up @@ -156,4 +168,17 @@ private function getStockData(Product $product)
}
return $stockData;
}

/**
* Process stock data for parent products
*
* @param Product $product
* @return void
*/
private function processParents(Product $product)
{
foreach ($this->parentItemProcessorPool as $processor) {
$processor->process($product);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\ConfigurableProduct\Model\Inventory;

use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Catalog\Model\Product;
use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory;
use Magento\CatalogInventory\Api\StockItemRepositoryInterface;
use Magento\CatalogInventory\Api\StockConfigurationInterface;
use Magento\CatalogInventory\Api\Data\StockItemInterface;

/**
* Process parent stock item
*/
class ParentItemProcessor
{
/**
* @var Configurable
*/
private $configurableType;

/**
* @var StockItemCriteriaInterfaceFactory
*/
private $criteriaInterfaceFactory;

/**
* @var StockItemRepositoryInterface
*/
private $stockItemRepository;

/**
* @var StockConfigurationInterface
*/
private $stockConfiguration;

/**
* @param Configurable $configurableType
* @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory
* @param StockItemRepositoryInterface $stockItemRepository
* @param StockConfigurationInterface $stockConfiguration
*/
public function __construct(
Configurable $configurableType,
StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory,
StockItemRepositoryInterface $stockItemRepository,
StockConfigurationInterface $stockConfiguration
) {
$this->configurableType = $configurableType;
$this->criteriaInterfaceFactory = $criteriaInterfaceFactory;
$this->stockItemRepository = $stockItemRepository;
$this->stockConfiguration = $stockConfiguration;
}

/**
* Process parent products
*
* @param Product $product
* @return void
*/
public function process(Product $product)
{
$parentIds = $this->configurableType->getParentIdsByChild($product->getId());
foreach ($parentIds as $productId) {
$this->processStockForParent((int)$productId);
}
}

/**
* Change stock item for parent product depending on children stock items
*
* @param int $productId
* @return void
*/
private function processStockForParent(int $productId)
{
$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);

$childrenIds = $this->configurableType->getChildrenIds($productId);
$criteria->setProductsFilter($childrenIds);
$stockItemCollection = $this->stockItemRepository->getList($criteria);
$allItems = $stockItemCollection->getItems();

$childrenIsInStock = false;

foreach ($allItems as $childItem) {
if ($childItem->getIsInStock() === true) {
$childrenIsInStock = true;
break;
}
}

if ($this->isNeedToUpdateParent($parentStockItem, $childrenIsInStock)) {
$parentStockItem->setIsInStock($childrenIsInStock);
$parentStockItem->setStockStatusChangedAuto(1);
$this->stockItemRepository->save($parentStockItem);
}
}

/**
* Check is 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());
}
}
15 changes: 9 additions & 6 deletions app/code/Magento/ConfigurableProduct/Model/LinkManagement.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,17 @@ public function addChild($sku, $childSku)
throw new StateException(__('Parent product does not have configurable product options'));
}

$attributeIds = [];
$attributeData = [];
foreach ($configurableProductOptions as $configurableProductOption) {
$attributeCode = $configurableProductOption->getProductAttribute()->getAttributeCode();
if (!$child->getData($attributeCode)) {
throw new StateException(__('Child product does not have attribute value %1', $attributeCode));
}
$attributeIds[] = $configurableProductOption->getAttributeId();
$attributeData[$configurableProductOption->getAttributeId()] = [
'position' => $configurableProductOption->getPosition()
];
}
$configurableOptionData = $this->getConfigurableAttributesData($attributeIds);
$configurableOptionData = $this->getConfigurableAttributesData($attributeData);

/** @var \Magento\ConfigurableProduct\Helper\Product\Options\Factory $optionFactory */
$optionFactory = $this->getOptionsFactory();
Expand Down Expand Up @@ -203,16 +205,16 @@ private function getOptionsFactory()
/**
* Get Configurable Attribute Data
*
* @param int[] $attributeIds
* @param int[] $attributeData
* @return array
*/
private function getConfigurableAttributesData($attributeIds)
private function getConfigurableAttributesData($attributeData)
{
$configurableAttributesData = [];
$attributeValues = [];
$attributes = $this->attributeFactory->create()
->getCollection()
->addFieldToFilter('attribute_id', $attributeIds)
->addFieldToFilter('attribute_id', array_keys($attributeData))
->getItems();
foreach ($attributes as $attribute) {
foreach ($attribute->getOptions() as $option) {
Expand All @@ -229,6 +231,7 @@ private function getConfigurableAttributesData($attributeIds)
'attribute_id' => $attribute->getId(),
'code' => $attribute->getAttributeCode(),
'label' => $attribute->getStoreLabel(),
'position' => $attributeData[$attribute->getId()]['position'],
'values' => $attributeValues,
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public function testAddChild()
->getMock();
$optionMock = $this->getMockBuilder(\Magento\ConfigurableProduct\Api\Data\Option::class)
->disableOriginalConstructor()
->setMethods(['getProductAttribute', 'getAttributeId'])
->setMethods(['getProductAttribute', 'getPosition', 'getAttributeId'])
->getMock();
$productAttributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class)
->disableOriginalConstructor()
Expand Down Expand Up @@ -216,13 +216,15 @@ public function testAddChild()
$productAttributeMock->expects($this->any())->method('getAttributeCode')->willReturn('color');
$simple->expects($this->any())->method('getData')->willReturn('color');
$optionMock->expects($this->any())->method('getAttributeId')->willReturn('1');
$optionMock->expects($this->any())->method('getPosition')->willReturn('0');

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

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

$extensionAttributesMock->expects($this->any())->method('setConfigurableProductOptions');
Expand Down
7 changes: 7 additions & 0 deletions app/code/Magento/ConfigurableProduct/etc/di.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@
</argument>
</arguments>
</type>
<type name="Magento\CatalogInventory\Observer\SaveInventoryDataObserver">
<arguments>
<argument name="parentItemProcessorPool" xsi:type="array">
<item name="configurable" xsi:type="object"> Magento\ConfigurableProduct\Model\Inventory\ParentItemProcessor</item>
</argument>
</arguments>
</type>
<type name="Magento\Sales\Model\ResourceModel\Report\Bestsellers">
<arguments>
<argument name="ignoredProductTypes" xsi:type="array">
Expand Down
14 changes: 13 additions & 1 deletion app/code/Magento/Search/Model/ResourceModel/SynonymReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ private function queryByPhrase($phrase)
{
$matchQuery = $this->fullTextSelect->getMatchQuery(
['synonyms' => 'synonyms'],
$phrase,
$this->escapePhrase($phrase),
Fulltext::FULLTEXT_MODE_BOOLEAN
);
$query = $this->getConnection()->select()->from(
Expand All @@ -97,6 +97,18 @@ private function queryByPhrase($phrase)
return $this->getConnection()->fetchAll($query);
}

/**
* Cut trailing plus or minus sign, and @ symbol, using of which causes InnoDB to report a syntax error.
*
* @see https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html
* @param string $phrase
* @return string
*/
private function escapePhrase(string $phrase): string
{
return preg_replace('/@+|[@+-]+$|[<>]/', '', $phrase);
}

/**
* A private helper function to retrieve matching synonym groups per scope
*
Expand Down
3 changes: 1 addition & 2 deletions app/code/Magento/Shipping/Model/Shipping.php
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,7 @@ public function collectRates(\Magento\Quote\Model\Quote\Address\RateRequest $req
*/
public function collectCarrierRates($carrierCode, $request)
{
/* @var $carrier \Magento\Shipping\Model\Carrier\AbstractCarrier */
$carrier = $this->_carrierFactory->createIfActive($carrierCode, $request->getQuoteStoreId());
$carrier = $this->_carrierFactory->create($carrierCode, $request->getQuoteStoreId());
if (!$carrier) {
return $this;
}
Expand Down
Loading

0 comments on commit 8c02c91

Please sign in to comment.