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);