diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php index 656998113fdb92e50e5177db4876651cfcd2023a..381e09f5d740850e3d2462434cf26841b24de01e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php @@ -11,6 +11,8 @@ use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Eav\Model\Config; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Store\Api\StoreResolverInterface; +use Magento\Store\Model\Store; /** * Class StatusBaseSelectProcessor @@ -27,16 +29,24 @@ class StatusBaseSelectProcessor implements BaseSelectProcessorInterface */ private $metadataPool; + /** + * @var StoreResolverInterface + */ + private $storeResolver; + /** * @param Config $eavConfig * @param MetadataPool $metadataPool + * @param StoreResolverInterface $storeResolver */ public function __construct( Config $eavConfig, - MetadataPool $metadataPool + MetadataPool $metadataPool, + StoreResolverInterface $storeResolver ) { $this->eavConfig = $eavConfig; $this->metadataPool = $metadataPool; + $this->storeResolver = $storeResolver; } /** @@ -48,13 +58,23 @@ class StatusBaseSelectProcessor implements BaseSelectProcessorInterface $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); $statusAttribute = $this->eavConfig->getAttribute(Product::ENTITY, ProductInterface::STATUS); - $select->join( + $select->joinLeft( + ['status_global_attr' => $statusAttribute->getBackendTable()], + "status_global_attr.{$linkField} = " . self::PRODUCT_TABLE_ALIAS . ".{$linkField}" + . ' AND status_global_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() + . ' AND status_global_attr.store_id = ' . Store::DEFAULT_STORE_ID, + [] + ); + + $select->joinLeft( ['status_attr' => $statusAttribute->getBackendTable()], - sprintf('status_attr.%s = %s.%1$s', $linkField, self::PRODUCT_TABLE_ALIAS), + "status_attr.{$linkField} = " . self::PRODUCT_TABLE_ALIAS . ".{$linkField}" + . ' AND status_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() + . ' AND status_attr.store_id = ' . $this->storeResolver->getCurrentStoreId(), [] - ) - ->where('status_attr.attribute_id = ?', $statusAttribute->getAttributeId()) - ->where('status_attr.value = ?', Status::STATUS_ENABLED); + ); + + $select->where('IFNULL(status_attr.value, status_global_attr.value) = ?', Status::STATUS_ENABLED); return $select; } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php index 0909f754a01c2c937e8c3692c52b1a256a27b0c3..c1c78c41ea602ddc560134bacabad5d9ccf8e5ec 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php @@ -16,6 +16,8 @@ use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\EntityMetadataInterface; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Api\StoreResolverInterface; +use Magento\Store\Model\Store; class StatusBaseSelectProcessorTest extends \PHPUnit_Framework_TestCase { @@ -29,6 +31,11 @@ class StatusBaseSelectProcessorTest extends \PHPUnit_Framework_TestCase */ private $metadataPool; + /** + * @var StoreResolverInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeResolver; + /** * @var Select|\PHPUnit_Framework_MockObject_MockObject */ @@ -43,11 +50,13 @@ class StatusBaseSelectProcessorTest extends \PHPUnit_Framework_TestCase { $this->eavConfig = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock(); $this->metadataPool = $this->getMockBuilder(MetadataPool::class)->disableOriginalConstructor()->getMock(); + $this->storeResolver = $this->getMockBuilder(StoreResolverInterface::class)->getMock(); $this->select = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); $this->statusBaseSelectProcessor = (new ObjectManager($this))->getObject(StatusBaseSelectProcessor::class, [ 'eavConfig' => $this->eavConfig, 'metadataPool' => $this->metadataPool, + 'storeResolver' => $this->storeResolver, ]); } @@ -55,7 +64,8 @@ class StatusBaseSelectProcessorTest extends \PHPUnit_Framework_TestCase { $linkField = 'link_field'; $backendTable = 'backend_table'; - $attributeId = 'attribute_id'; + $attributeId = 2; + $currentStoreId = 1; $metadata = $this->getMock(EntityMetadataInterface::class); $metadata->expects($this->once()) @@ -66,13 +76,14 @@ class StatusBaseSelectProcessorTest extends \PHPUnit_Framework_TestCase ->with(ProductInterface::class) ->willReturn($metadata); + /** @var AttributeInterface|\PHPUnit_Framework_MockObject_MockObject $statusAttribute */ $statusAttribute = $this->getMockBuilder(AttributeInterface::class) ->setMethods(['getBackendTable', 'getAttributeId']) ->getMock(); - $statusAttribute->expects($this->once()) + $statusAttribute->expects($this->atLeastOnce()) ->method('getBackendTable') ->willReturn($backendTable); - $statusAttribute->expects($this->once()) + $statusAttribute->expects($this->atLeastOnce()) ->method('getAttributeId') ->willReturn($attributeId); $this->eavConfig->expects($this->once()) @@ -80,21 +91,34 @@ class StatusBaseSelectProcessorTest extends \PHPUnit_Framework_TestCase ->with(Product::ENTITY, ProductInterface::STATUS) ->willReturn($statusAttribute); - $this->select->expects($this->once()) - ->method('join') + $this->storeResolver->expects($this->once()) + ->method('getCurrentStoreId') + ->willReturn($currentStoreId); + + $this->select->expects($this->at(0)) + ->method('joinLeft') ->with( - ['status_attr' => $backendTable], - sprintf('status_attr.%s = %s.%1$s', $linkField, BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), + ['status_global_attr' => $backendTable], + "status_global_attr.{$linkField} = " + . BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS . ".{$linkField}" + . " AND status_global_attr.attribute_id = {$attributeId}" + . ' AND status_global_attr.store_id = ' . Store::DEFAULT_STORE_ID, [] ) ->willReturnSelf(); $this->select->expects($this->at(1)) - ->method('where') - ->with('status_attr.attribute_id = ?', $attributeId) + ->method('joinLeft') + ->with( + ['status_attr' => $backendTable], + "status_attr.{$linkField} = " . BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS . ".{$linkField}" + . " AND status_attr.attribute_id = {$attributeId}" + . " AND status_attr.store_id = {$currentStoreId}", + [] + ) ->willReturnSelf(); $this->select->expects($this->at(2)) ->method('where') - ->with('status_attr.value = ?', Status::STATUS_ENABLED) + ->with('IFNULL(status_attr.value, status_global_attr.value) = ?', Status::STATUS_ENABLED) ->willReturnSelf(); $this->assertEquals($this->select, $this->statusBaseSelectProcessor->process($this->select)); diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php index fbb69798ff94fbdf8caafc7486e564da662cf655..07936cefdebb194c5b894c049fd357c969fcdbc5 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -9,9 +9,41 @@ namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Store\Api\StoreResolverInterface; +use Magento\Store\Model\Store; class Configurable extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice { + /** + * @var StoreResolverInterface + */ + private $storeResolver; + + /** + * Class constructor + * + * @param \Magento\Framework\Model\ResourceModel\Db\Context $context + * @param \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy + * @param \Magento\Eav\Model\Config $eavConfig + * @param \Magento\Framework\Event\ManagerInterface $eventManager + * @param \Magento\Framework\Module\Manager $moduleManager + * @param string $connectionName + */ + public function __construct( + \Magento\Framework\Model\ResourceModel\Db\Context $context, + \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy, + \Magento\Eav\Model\Config $eavConfig, + \Magento\Framework\Event\ManagerInterface $eventManager, + \Magento\Framework\Module\Manager $moduleManager, + $connectionName = null, + StoreResolverInterface $storeResolver = null + ) { + parent::__construct($context, $tableStrategy, $eavConfig, $eventManager, $moduleManager, $connectionName); + $this->storeResolver = $storeResolver ?: + \Magento\Framework\App\ObjectManager::getInstance()->get(StoreResolverInterface::class); + } + /** * Reindex temporary (price result data) for all products * @@ -190,16 +222,20 @@ class Configurable extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\ [] )->where( 'le.required_options=0' - )->join( - ['product_status' => $this->getTable($statusAttribute->getBackend()->getTable())], - sprintf( - 'le.%1$s = product_status.%1$s AND product_status.attribute_id = %2$s', - $linkField, - $statusAttribute->getAttributeId() - ), + )->joinLeft( + ['status_global_attr' => $statusAttribute->getBackendTable()], + "status_global_attr.{$linkField} = le.{$linkField}" + . ' AND status_global_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() + . ' AND status_global_attr.store_id = ' . Store::DEFAULT_STORE_ID, + [] + )->joinLeft( + ['status_attr' => $statusAttribute->getBackendTable()], + "status_attr.{$linkField} = le.{$linkField}" + . ' AND status_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() + . ' AND status_attr.store_id = ' . $this->storeResolver->getCurrentStoreId(), [] )->where( - 'product_status.value=' . ProductStatus::STATUS_ENABLED + 'IFNULL(status_attr.value, status_global_attr.value) = ?', Status::STATUS_ENABLED )->group( ['e.entity_id', 'i.customer_group_id', 'i.website_id', 'l.product_id'] ); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php index 5db97cc36614924ae915b7b924b2c7a1f575e344..2e339e4347cf65ad13474de26ba61c3237cfafe9 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php @@ -67,4 +67,40 @@ class ConfigurableTest extends \PHPUnit_Framework_TestCase ->getFirstItem(); $this->assertEquals(20, $configurableProduct->getMinimalPrice()); } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + */ + public function testGetProductFinalPriceIfOneOfChildIsDisabledPerStore() + { + /** @var Collection $collection */ + $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class) + ->create(); + $configurableProduct = $collection + ->addIdFilter([1]) + ->addMinimalPrice() + ->load() + ->getFirstItem(); + $this->assertEquals(10, $configurableProduct->getMinimalPrice()); + + $childProduct = $this->productRepository->getById(10, false, null, true); + $childProduct->setStatus(Status::STATUS_DISABLED); + + // update in default store scope + $currentStoreId = $this->storeManager->getStore()->getId(); + $defaultStore = $this->storeManager->getDefaultStoreView(); + $this->storeManager->setCurrentStore($defaultStore->getId()); + $this->productRepository->save($childProduct); + $this->storeManager->setCurrentStore($currentStoreId); + + /** @var Collection $collection */ + $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class) + ->create(); + $configurableProduct = $collection + ->addIdFilter([1]) + ->addMinimalPrice() + ->load() + ->getFirstItem(); + $this->assertEquals(20, $configurableProduct->getMinimalPrice()); + } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionProviderTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionProviderTest.php index 50787c7962412b0611edf093825563061da67119..58df9f50f7f0c10a8750c1fdf31c68561b6fdb87 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionProviderTest.php @@ -68,6 +68,38 @@ class LowestPriceOptionProviderTest extends \PHPUnit_Framework_TestCase $this->assertEquals(20, $lowestPriceChildrenProduct->getPrice()); } + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + */ + public function testGetProductsIfOneOfChildIsDisabledPerStore() + { + $configurableProduct = $this->productRepository->getById(1, false, null, true); + $lowestPriceChildrenProducts = $this->lowestPriceOptionsProvider->getProducts($configurableProduct); + $this->assertCount(1, $lowestPriceChildrenProducts); + $lowestPriceChildrenProduct = reset($lowestPriceChildrenProducts); + $this->assertEquals(10, $lowestPriceChildrenProduct->getPrice()); + + // load full aggregation root + $lowestPriceChildProduct = $this->productRepository->get( + $lowestPriceChildrenProduct->getSku(), + false, + null, + true + ); + $lowestPriceChildProduct->setStatus(Status::STATUS_DISABLED); + // update in default store scope + $currentStoreId = $this->storeManager->getStore()->getId(); + $defaultStore = $this->storeManager->getDefaultStoreView(); + $this->storeManager->setCurrentStore($defaultStore->getId()); + $this->productRepository->save($lowestPriceChildProduct); + $this->storeManager->setCurrentStore($currentStoreId); + + $lowestPriceChildrenProducts = $this->lowestPriceOptionsProvider->getProducts($configurableProduct); + $this->assertCount(1, $lowestPriceChildrenProducts); + $lowestPriceChildrenProduct = reset($lowestPriceChildrenProducts); + $this->assertEquals(20, $lowestPriceChildrenProduct->getPrice()); + } + /** * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php */