From 58e8542f6710c716d65d099f212e0cc65b8cdb49 Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh Date: Thu, 12 Nov 2020 18:00:07 +0200 Subject: [PATCH 1/3] MC-38590: "Out of Stock" Configurable Product Shows Up on Storefront Category Page --- .../Indexer/SelectBuilder.php | 40 ++++- .../Indexer/SelectBuilder.php | 43 +++++- ...VisibleAfterDisablingChildProductsTest.xml | 140 ++++++++++++++++++ 3 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 InventoryConfigurableProductIndexer/Test/Mftf/Test/ConfigurableProductNotVisibleAfterDisablingChildProductsTest.xml diff --git a/InventoryBundleProductIndexer/Indexer/SelectBuilder.php b/InventoryBundleProductIndexer/Indexer/SelectBuilder.php index dc78c4bfdb8b..12d1d08676f1 100644 --- a/InventoryBundleProductIndexer/Indexer/SelectBuilder.php +++ b/InventoryBundleProductIndexer/Indexer/SelectBuilder.php @@ -9,6 +9,10 @@ use Exception; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Model\Config; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\MetadataPool; @@ -20,6 +24,8 @@ /** * Get bundle product for given stock select builder. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SelectBuilder { @@ -43,22 +49,30 @@ class SelectBuilder */ private $metadataPool; + /** + * @var Config + */ + private $eavConfig; + /** * @param ResourceConnection $resourceConnection * @param IndexNameBuilder $indexNameBuilder * @param IndexNameResolverInterface $indexNameResolver * @param MetadataPool $metadataPool + * @param Config $eavConfig */ public function __construct( ResourceConnection $resourceConnection, IndexNameBuilder $indexNameBuilder, IndexNameResolverInterface $indexNameResolver, - MetadataPool $metadataPool + MetadataPool $metadataPool, + Config $eavConfig ) { $this->resourceConnection = $resourceConnection; $this->indexNameBuilder = $indexNameBuilder; $this->indexNameResolver = $indexNameResolver; $this->metadataPool = $metadataPool; + $this->eavConfig = $eavConfig; } /** @@ -103,9 +117,29 @@ public function execute(int $stockId): Select ['parent_product_entity' => $this->resourceConnection->getTableName('catalog_product_entity')], 'parent_product_entity.' . $linkField . ' = parent_link.parent_product_id', [] - ) - ->group(['parent_product_entity.sku']); + )->joinInner( + ['cpe' => $this->resourceConnection->getTableName('catalog_product_entity')], + 'cpe.entity_id = product_entity.entity_id', + [] + )->joinInner( + ['cpei' => $this->resourceConnection->getTableName('catalog_product_entity_int')], + 'cpe.' . $linkField . ' = cpei.row_id' + . ' AND cpei.attribute_id = ' . $this->getAttribute('status')->getId() + . ' AND cpei.value = ' . ProductStatus::STATUS_ENABLED, + [] + )->group(['parent_product_entity.sku']); return $select; } + + /** + * Retrieve catalog_product attribute instance by attribute code + * + * @param string $attributeCode + * @return Attribute + */ + private function getAttribute($attributeCode): Attribute + { + return $this->eavConfig->getAttribute(Product::ENTITY, $attributeCode); + } } diff --git a/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php b/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php index fa52a5c316d8..d2b31e9dd462 100644 --- a/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php +++ b/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php @@ -9,6 +9,10 @@ use Exception; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Model\Config; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\MetadataPool; @@ -20,6 +24,11 @@ use Magento\InventoryMultiDimensionalIndexerApi\Model\IndexNameResolverInterface; use Magento\InventoryIndexer\Indexer\SelectBuilderInterface; +/** + * Get configurable product for given stock select builder + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SelectBuilder implements SelectBuilderInterface { /** @@ -46,25 +55,33 @@ class SelectBuilder implements SelectBuilderInterface */ private $defaultStockProvider; + /** + * @var Config + */ + private $eavConfig; + /** * @param ResourceConnection $resourceConnection * @param IndexNameBuilder $indexNameBuilder * @param IndexNameResolverInterface $indexNameResolver * @param MetadataPool $metadataPool * @param DefaultStockProviderInterface $defaultStockProvider + * @param Config $eavConfig */ public function __construct( ResourceConnection $resourceConnection, IndexNameBuilder $indexNameBuilder, IndexNameResolverInterface $indexNameResolver, MetadataPool $metadataPool, - DefaultStockProviderInterface $defaultStockProvider + DefaultStockProviderInterface $defaultStockProvider, + Config $eavConfig ) { $this->resourceConnection = $resourceConnection; $this->indexNameBuilder = $indexNameBuilder; $this->indexNameResolver = $indexNameResolver; $this->metadataPool = $metadataPool; $this->defaultStockProvider = $defaultStockProvider; + $this->eavConfig = $eavConfig; } /** @@ -114,9 +131,29 @@ public function execute(int $stockId): Select 'inventory_stock_item.product_id = parent_product_entity.entity_id' . ' AND inventory_stock_item.stock_id = ' . $this->defaultStockProvider->getId(), [] - ) - ->group(['parent_product_entity.sku']); + )->joinInner( + ['cpe' => $this->resourceConnection->getTableName('catalog_product_entity')], + 'cpe.entity_id = product_entity.entity_id', + [] + )->joinInner( + ['cpei' => $this->resourceConnection->getTableName('catalog_product_entity_int')], + 'cpe.' . $linkField . ' = cpei.row_id' + . ' AND cpei.attribute_id = ' . $this->getAttribute('status')->getId() + . ' AND cpei.value = ' . ProductStatus::STATUS_ENABLED, + [] + )->group(['parent_product_entity.sku']); return $select; } + + /** + * Retrieve catalog_product attribute instance by attribute code + * + * @param string $attributeCode + * @return Attribute + */ + private function getAttribute($attributeCode): Attribute + { + return $this->eavConfig->getAttribute(Product::ENTITY, $attributeCode); + } } diff --git a/InventoryConfigurableProductIndexer/Test/Mftf/Test/ConfigurableProductNotVisibleAfterDisablingChildProductsTest.xml b/InventoryConfigurableProductIndexer/Test/Mftf/Test/ConfigurableProductNotVisibleAfterDisablingChildProductsTest.xml new file mode 100644 index 000000000000..ee45d5db6cd8 --- /dev/null +++ b/InventoryConfigurableProductIndexer/Test/Mftf/Test/ConfigurableProductNotVisibleAfterDisablingChildProductsTest.xml @@ -0,0 +1,140 @@ + + + + + + + + + <description value="Verify, configurable product is not displayed on category page after disabling first child product and set out of stock to second"/> + <testCaseId value="MC-38896"/> + <useCaseId value="MC-38590"/> + <severity value="AVERAGE"/> + <group value="msi"/> + </annotations> + <before> + <!--Create test data.--> + <!-- Create the category to put the product in --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createFirstConfigProductAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createSecondConfigProductAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getFirstConfigAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getSecondConfigAttributeOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOne" stepKey="createFirstConfigChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getFirstConfigAttributeOption"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createSecondConfigChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getSecondConfigAttributeOption"/> + </createData> + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getFirstConfigAttributeOption"/> + <requiredEntity createDataKey="getSecondConfigAttributeOption"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createFirstConfigProductAddChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createFirstConfigChildProduct"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createSecondConfigProductAddChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createSecondConfigChildProduct"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <createData entity="_minimalSource" stepKey="createSource"/> + <createData entity="BasicMsiStockWithMainWebsite1" stepKey="stock"/> + <createData entity="SourceStockLinked1" stepKey="linkStockAndSource"> + <requiredEntity createDataKey="stock"/> + <requiredEntity createDataKey="createSource"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> + <!--Assign additional source to configurable product.--> + <amOnPage url="{{AdminProductEditPage.url($createFirstConfigChildProduct.id$)}}" stepKey="openProductEditPage"/> + <actionGroup ref="UnassignSourceFromProductActionGroup" stepKey="unassignDefaultSourceFromProduct"> + <argument name="sourceCode" value="{{_defaultSource.name}}"/> + </actionGroup> + <actionGroup ref="AdminAssignSourceToProductAndSetSourceQuantityActionGroup" stepKey="assignCreatedSourceToFirstChildProduct"> + <argument name="sourceCode" value="$createSource.source[source_code]$"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveFirstChildProduct"/> + <!--Assign additional source to configurable product second.--> + <amOnPage url="{{AdminProductEditPage.url($createSecondConfigChildProduct.id$)}}" stepKey="openSecondProductEditPage"/> + <actionGroup ref="UnassignSourceFromProductActionGroup" stepKey="unassignDefaultSourceFromSecondProduct"> + <argument name="sourceCode" value="{{_defaultSource.name}}"/> + </actionGroup> + <actionGroup ref="AdminAssignSourceToProductAndSetSourceQuantityActionGroup" stepKey="assignCreatedSourceToSecondChildProduct"> + <argument name="sourceCode" value="$createSource.source[source_code]$"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSecondChildProduct"/> + <actionGroup ref="AdminReindexAndFlushCache" stepKey="reindexAndFlushCache"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createFirstConfigChildProduct" stepKey="deleteFirstConfigChildProduct"/> + <deleteData createDataKey="createSecondConfigChildProduct" stepKey="deleteSecondConfigChildProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <!--Assign Default Stock to Main Website.--> + <actionGroup ref="AssignWebsiteToStockActionGroup" stepKey="assignMainWebsiteToDefaultStock"> + <argument name="stockName" value="{{_defaultStock.name}}"/> + <argument name="websiteName" value="{{_defaultWebsite.name}}"/> + </actionGroup> + <deleteData createDataKey="stock" stepKey="deleteStock"/> + <!--Disable source.--> + <actionGroup ref="DisableAllSourcesActionGroup" stepKey="disableSources"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdminArea"/> + <!-- Reindex invalidated indices after product attribute has been created/deleted --> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </after> + <!--Verify product is visible on storefront.--> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openCategoryPageOnFrontend"> + <argument name="category" value="$createCategory$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductIsPresentOnCategoryPageActionGroup" stepKey="checkProductOnCategoryPage"> + <argument name="productName" value="$$createConfigProduct.name$$"/> + </actionGroup> + <!--Open first child product in Admin. Make it disabled (Enable Product = No)--> + <amOnPage url="{{AdminProductEditPage.url($$createFirstConfigChildProduct.id$$)}}" stepKey="openProductEditPageForDisablingProduct"/> + <actionGroup ref="AdminSetProductDisabledActionGroup" stepKey="disableProduct"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="clickSaveProduct"/> + <!--Open second child product and set product stock status to out of stock--> + <amOnPage url="{{AdminProductEditPage.url($$createSecondConfigChildProduct.id$$)}}" stepKey="openProductEditPageForDisablingSource"/> + <actionGroup ref="AdminChangeSourceStockStatusActionGroup" stepKey="setProductStatusToOutOfStock"> + <argument name="sourceCode" value="$createSource.source[source_code]$"/> + <argument name="sourceStatus" value="{{SourceStatusOutOfStock.value}}"/> + </actionGroup> + <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="saveProduct"/> + <!--Verify product is not visible on storefront.--> + <actionGroup ref="AssertStorefrontProductAbsentOnCategoryPageActionGroup" stepKey="doNotSeeProductOnCategoryPage"> + <argument name="categoryUrlKey" value="$$createCategory.name$$"/> + <argument name="productName" value="$$createConfigProduct.name$$"/> + </actionGroup> + </test> +</tests> From e13bffd61c45804db7ec46e779706c96a8090b6b Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Fri, 13 Nov 2020 13:08:30 +0200 Subject: [PATCH 2/3] MC-38590: "Out of Stock" Configurable Product Shows Up on Storefront Category Page --- .../Indexer/SelectBuilder.php | 13 +++++-------- InventoryBundleProductIndexer/composer.json | 1 + .../Indexer/SelectBuilder.php | 13 +++++-------- InventoryConfigurableProductIndexer/composer.json | 1 + 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/InventoryBundleProductIndexer/Indexer/SelectBuilder.php b/InventoryBundleProductIndexer/Indexer/SelectBuilder.php index 12d1d08676f1..a9d10e3f250f 100644 --- a/InventoryBundleProductIndexer/Indexer/SelectBuilder.php +++ b/InventoryBundleProductIndexer/Indexer/SelectBuilder.php @@ -96,6 +96,7 @@ public function execute(int $stockId): Select $metadata = $this->metadataPool->getMetadata(ProductInterface::class); $linkField = $metadata->getLinkField(); + $statusAttributeId = $this->getAttribute(ProductInterface::STATUS)->getId(); $select = $connection->select() ->from( @@ -118,14 +119,10 @@ public function execute(int $stockId): Select 'parent_product_entity.' . $linkField . ' = parent_link.parent_product_id', [] )->joinInner( - ['cpe' => $this->resourceConnection->getTableName('catalog_product_entity')], - 'cpe.entity_id = product_entity.entity_id', - [] - )->joinInner( - ['cpei' => $this->resourceConnection->getTableName('catalog_product_entity_int')], - 'cpe.' . $linkField . ' = cpei.row_id' - . ' AND cpei.attribute_id = ' . $this->getAttribute('status')->getId() - . ' AND cpei.value = ' . ProductStatus::STATUS_ENABLED, + ['product_status' => $this->resourceConnection->getTableName('catalog_product_entity_int')], + "product_entity.entity_id = product_status.$linkField" + . " AND product_status.attribute_id = $statusAttributeId" + . ' AND product_status.value = ' . ProductStatus::STATUS_ENABLED, [] )->group(['parent_product_entity.sku']); diff --git a/InventoryBundleProductIndexer/composer.json b/InventoryBundleProductIndexer/composer.json index 9650c2c35df5..19b5fa1c08aa 100644 --- a/InventoryBundleProductIndexer/composer.json +++ b/InventoryBundleProductIndexer/composer.json @@ -6,6 +6,7 @@ "magento/framework": "*", "magento/module-bundle": "*", "magento/module-catalog": "*", + "magento/module-eav": "*", "magento/module-inventory-api": "*", "magento/module-inventory-catalog-api": "*", "magento/module-inventory-indexer": "*", diff --git a/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php b/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php index d2b31e9dd462..7407680783bd 100644 --- a/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php +++ b/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php @@ -105,6 +105,7 @@ public function execute(int $stockId): Select $metadata = $this->metadataPool->getMetadata(ProductInterface::class); $linkField = $metadata->getLinkField(); + $statusAttributeId = $this->getAttribute(ProductInterface::STATUS)->getId(); $select = $connection->select() ->from( @@ -132,14 +133,10 @@ public function execute(int $stockId): Select . ' AND inventory_stock_item.stock_id = ' . $this->defaultStockProvider->getId(), [] )->joinInner( - ['cpe' => $this->resourceConnection->getTableName('catalog_product_entity')], - 'cpe.entity_id = product_entity.entity_id', - [] - )->joinInner( - ['cpei' => $this->resourceConnection->getTableName('catalog_product_entity_int')], - 'cpe.' . $linkField . ' = cpei.row_id' - . ' AND cpei.attribute_id = ' . $this->getAttribute('status')->getId() - . ' AND cpei.value = ' . ProductStatus::STATUS_ENABLED, + ['product_status' => $this->resourceConnection->getTableName('catalog_product_entity_int')], + "product_entity.entity_id = product_status.$linkField" + . " AND product_status.attribute_id = $statusAttributeId" + . ' AND product_status.value = ' . ProductStatus::STATUS_ENABLED, [] )->group(['parent_product_entity.sku']); diff --git a/InventoryConfigurableProductIndexer/composer.json b/InventoryConfigurableProductIndexer/composer.json index 7cddd52ebb9e..7598562735a2 100644 --- a/InventoryConfigurableProductIndexer/composer.json +++ b/InventoryConfigurableProductIndexer/composer.json @@ -5,6 +5,7 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-catalog": "*", + "magento/module-eav": "*", "magento/module-inventory-api": "*", "magento/module-inventory-catalog-api": "*", "magento/module-inventory-indexer": "*", From 9a58f89e6b9d813ef23913c7ddd28de68746bebc Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Fri, 13 Nov 2020 13:37:30 +0200 Subject: [PATCH 3/3] MC-38590: "Out of Stock" Configurable Product Shows Up on Storefront Category Page --- InventoryBundleProductIndexer/Indexer/SelectBuilder.php | 2 +- InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InventoryBundleProductIndexer/Indexer/SelectBuilder.php b/InventoryBundleProductIndexer/Indexer/SelectBuilder.php index a9d10e3f250f..f0bda0b2f053 100644 --- a/InventoryBundleProductIndexer/Indexer/SelectBuilder.php +++ b/InventoryBundleProductIndexer/Indexer/SelectBuilder.php @@ -120,7 +120,7 @@ public function execute(int $stockId): Select [] )->joinInner( ['product_status' => $this->resourceConnection->getTableName('catalog_product_entity_int')], - "product_entity.entity_id = product_status.$linkField" + "product_entity.$linkField = product_status.$linkField" . " AND product_status.attribute_id = $statusAttributeId" . ' AND product_status.value = ' . ProductStatus::STATUS_ENABLED, [] diff --git a/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php b/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php index 7407680783bd..3b282ad2614b 100644 --- a/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php +++ b/InventoryConfigurableProductIndexer/Indexer/SelectBuilder.php @@ -134,7 +134,7 @@ public function execute(int $stockId): Select [] )->joinInner( ['product_status' => $this->resourceConnection->getTableName('catalog_product_entity_int')], - "product_entity.entity_id = product_status.$linkField" + "product_entity.$linkField = product_status.$linkField" . " AND product_status.attribute_id = $statusAttributeId" . ' AND product_status.value = ' . ProductStatus::STATUS_ENABLED, []