diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php
index 1e7c800ea1c38..85fee62eb4303 100644
--- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php
+++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php
@@ -10,6 +10,7 @@
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ResourceConnection;
+use Magento\Framework\EntityManager\MetadataPool;
/**
* Abstract action reindex class
@@ -70,25 +71,33 @@ abstract class AbstractAction
*/
private $cacheCleaner;
+ /**
+ * @var MetadataPool
+ */
+ private $metadataPool;
+
/**
* @param ResourceConnection $resource
* @param \Magento\CatalogInventory\Model\ResourceModel\Indexer\StockFactory $indexerFactory
* @param \Magento\Catalog\Model\Product\Type $catalogProductType
* @param \Magento\Framework\Indexer\CacheContext $cacheContext
* @param \Magento\Framework\Event\ManagerInterface $eventManager
+ * @param MetadataPool|null $metadataPool
*/
public function __construct(
ResourceConnection $resource,
\Magento\CatalogInventory\Model\ResourceModel\Indexer\StockFactory $indexerFactory,
\Magento\Catalog\Model\Product\Type $catalogProductType,
\Magento\Framework\Indexer\CacheContext $cacheContext,
- \Magento\Framework\Event\ManagerInterface $eventManager
+ \Magento\Framework\Event\ManagerInterface $eventManager,
+ MetadataPool $metadataPool = null
) {
$this->_resource = $resource;
$this->_indexerFactory = $indexerFactory;
$this->_catalogProductType = $catalogProductType;
$this->cacheContext = $cacheContext;
$this->eventManager = $eventManager;
+ $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class);
}
/**
@@ -154,10 +163,15 @@ protected function _getTable($entityName)
public function getRelationsByChild($childIds)
{
$connection = $this->_getConnection();
- $select = $connection->select()
- ->from($this->_getTable('catalog_product_relation'), 'parent_id')
- ->where('child_id IN(?)', $childIds);
-
+ $linkField = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class)
+ ->getLinkField();
+ $select = $connection->select()->from(
+ ['cpe' => $this->_getTable('catalog_product_entity')],
+ 'entity_id'
+ )->join(
+ ['relation' => $this->_getTable('catalog_product_relation')],
+ 'relation.parent_id = cpe.' . $linkField
+ )->where('child_id IN(?)', $childIds);
return $connection->fetchCol($select);
}
@@ -230,7 +244,8 @@ protected function _reindexRows($productIds = [])
if (!is_array($productIds)) {
$productIds = [$productIds];
}
-
+ $parentIds = $this->getRelationsByChild($productIds);
+ $productIds = $parentIds ? array_unique(array_merge($parentIds, $productIds)) : $productIds;
$this->getCacheCleaner()->clean($productIds, function () use ($productIds) {
$this->doReindex($productIds);
});
@@ -248,13 +263,10 @@ private function doReindex($productIds = [])
{
$connection = $this->_getConnection();
- $parentIds = $this->getRelationsByChild($productIds);
- $processIds = $parentIds ? array_merge($parentIds, $productIds) : $productIds;
-
// retrieve product types by processIds
$select = $connection->select()
->from($this->_getTable('catalog_product_entity'), ['entity_id', 'type_id'])
- ->where('entity_id IN(?)', $processIds);
+ ->where('entity_id IN(?)', $productIds);
$pairs = $connection->fetchPairs($select);
$byType = [];
diff --git a/app/code/Magento/CatalogInventory/Model/Plugin/ReindexUpdatedProducts.php b/app/code/Magento/CatalogInventory/Model/Plugin/ReindexUpdatedProducts.php
new file mode 100644
index 0000000000000..f10afcd4ea329
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Model/Plugin/ReindexUpdatedProducts.php
@@ -0,0 +1,46 @@
+indexerProcessor = $indexerProcessor;
+ }
+
+ /**
+ * Reindex on product attribute mass change
+ *
+ * @param ProductAction $subject
+ * @param ProductAction $action
+ * @param array $productIds
+ * @return ProductAction
+ *
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterUpdateAttributes(
+ ProductAction $subject,
+ ProductAction $action,
+ $productIds
+ ) {
+ $this->indexerProcessor->reindexList(array_unique($productIds));
+ return $action;
+ }
+}
diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml
index 04935b11ce02b..65bc277121429 100644
--- a/app/code/Magento/CatalogInventory/etc/di.xml
+++ b/app/code/Magento/CatalogInventory/etc/di.xml
@@ -78,6 +78,9 @@
+
+
+
Magento\CatalogInventory\Model\Indexer\Stock\Processor
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ActionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ActionTest.php
index 90825ab573d09..39905aeae10f5 100644
--- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ActionTest.php
+++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ActionTest.php
@@ -84,6 +84,73 @@ public function testUpdateWebsites()
}
}
+ /**
+ * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php
+ * @magentoAppArea adminhtml
+ * @param string $status
+ * @param string $productsCount
+ * @dataProvider updateAttributesDataProvider
+ */
+ public function testUpdateAttributes($status, $productsCount)
+ {
+ /** @var \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry */
+ $indexerRegistry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
+ ->get(\Magento\Framework\Indexer\IndexerRegistry::class);
+ $indexerRegistry->get(Fulltext::INDEXER_ID)->setScheduled(false);
+
+ /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
+ $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);
+
+ /** @var \Magento\Catalog\Model\Product $product */
+ $product = $productRepository->get('configurable');
+ $productAttributesOptions = $product->getExtensionAttributes()->getConfigurableProductLinks();
+ $attrData = ['status' => $status];
+ $configurableOptionsId = [];
+ if (isset($productAttributesOptions)) {
+ foreach ($productAttributesOptions as $configurableOption) {
+ $configurableOptionsId[] = $configurableOption;
+ }
+ }
+ $this->action->updateAttributes($configurableOptionsId, $attrData, $product->getStoreId());
+
+ $categoryFactory = $this->objectManager->create(\Magento\Catalog\Model\CategoryFactory::class);
+ /** @var \Magento\Catalog\Block\Product\ListProduct $listProduct */
+ $listProduct = $this->objectManager->create(\Magento\Catalog\Block\Product\ListProduct::class);
+ $category = $categoryFactory->create()->load(2);
+ $layer = $listProduct->getLayer();
+ $layer->setCurrentCategory($category);
+ $productCollection = $layer->getProductCollection();
+ $productCollection->joinField(
+ 'qty',
+ 'cataloginventory_stock_status',
+ 'qty',
+ 'product_id=entity_id',
+ '{{table}}.stock_id=1',
+ 'left'
+ );
+
+ $this->assertEquals($productsCount, $productCollection->count());
+ }
+
+ /**
+ * DataProvider for testUpdateAttributes
+ *
+ * @return array
+ */
+ public function updateAttributesDataProvider()
+ {
+ return [
+ [
+ 'status' => 2,
+ 'expected_count' => 0
+ ],
+ [
+ 'status' => 1,
+ 'expected_count' => 1
+ ],
+ ];
+ }
+
public static function tearDownAfterClass()
{
/** @var \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry */
diff --git a/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_simple.php b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_simple.php
new file mode 100644
index 0000000000000..fc01daf2381bc
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_simple.php
@@ -0,0 +1,66 @@
+get(ProductRepositoryInterface::class);
+
+$productLinkFactory = Bootstrap::getObjectManager()
+ ->get(\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory::class);
+$productIds = ['11', '22'];
+
+foreach ($productIds as $productId) {
+ /** @var $product Product */
+ $product = Bootstrap::getObjectManager()->create(Product::class);
+ $product->setTypeId(Type::TYPE_SIMPLE)
+ ->setId($productId)
+ ->setWebsiteIds([1])
+ ->setAttributeSetId(4)
+ ->setName('Simple ' . $productId)
+ ->setSku('simple_' . $productId)
+ ->setPrice(100)
+ ->setVisibility(Visibility::VISIBILITY_BOTH)
+ ->setStatus(Status::STATUS_ENABLED)
+ ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]);
+
+ $linkedProducts[] = $productRepository->save($product);
+}
+
+/** @var $product Product */
+$product = Bootstrap::getObjectManager()->create(Product::class);
+
+$product->setTypeId(Grouped::TYPE_CODE)
+ ->setId(1)
+ ->setWebsiteIds([1])
+ ->setAttributeSetId(4)
+ ->setName('Grouped Product')
+ ->setSku('grouped')
+ ->setVisibility(Visibility::VISIBILITY_BOTH)
+ ->setStatus(Status::STATUS_ENABLED)
+ ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]);
+
+foreach ($linkedProducts as $linkedProduct) {
+ /** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */
+ $productLink = $productLinkFactory->create();
+ $productLink->setSku($product->getSku())
+ ->setLinkType('associated')
+ ->setLinkedProductSku($linkedProduct->getSku())
+ ->setLinkedProductType($linkedProduct->getTypeId())
+ ->getExtensionAttributes()
+ ->setQty(1);
+ $newLinks[] = $productLink;
+}
+
+$product->setProductLinks($newLinks);
+
+$productRepository->save($product);
diff --git a/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_simple_rollback.php b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_simple_rollback.php
new file mode 100644
index 0000000000000..bc2f04d7b19d0
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_simple_rollback.php
@@ -0,0 +1,34 @@
+get(\Magento\Framework\Registry::class);
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+
+/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
+$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
+ ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class);
+
+$skuList = ['simple_11', 'simple_22', 'grouped'];
+foreach ($skuList as $sku) {
+ try {
+ $product = $productRepository->get($sku, false, null, true);
+
+ $stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class);
+ $stockStatus->load($product->getEntityId(), 'product_id');
+ $stockStatus->delete();
+
+ $productRepository->delete($product);
+ } catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
+ //Product already removed
+ }
+}
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);