Skip to content

Commit 8a66196

Browse files
authored
Merge pull request #550 from magento-dragons/MAGETWO-59953
[Dragons] Bug fixing - 2.1: - MAGETWO-59953 [Backport] Configurable product option price is displayed incorrectly per website for 2.1.3 - MAGETWO-60103 [[Backport] Configurable variation is displayed on category/product page when is out of stock - 2.1
2 parents 5de4ce6 + 7a18799 commit 8a66196

File tree

22 files changed

+448
-220
lines changed

22 files changed

+448
-220
lines changed

app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
use Magento\Eav\Model\Config;
1212
use Magento\Framework\DB\Select;
1313
use Magento\Framework\EntityManager\MetadataPool;
14+
use Magento\Store\Api\StoreResolverInterface;
15+
use Magento\Store\Model\Store;
1416

1517
/**
1618
* Class StatusBaseSelectProcessor
@@ -27,16 +29,24 @@ class StatusBaseSelectProcessor implements BaseSelectProcessorInterface
2729
*/
2830
private $metadataPool;
2931

32+
/**
33+
* @var StoreResolverInterface
34+
*/
35+
private $storeResolver;
36+
3037
/**
3138
* @param Config $eavConfig
3239
* @param MetadataPool $metadataPool
40+
* @param StoreResolverInterface $storeResolver
3341
*/
3442
public function __construct(
3543
Config $eavConfig,
36-
MetadataPool $metadataPool
44+
MetadataPool $metadataPool,
45+
StoreResolverInterface $storeResolver
3746
) {
3847
$this->eavConfig = $eavConfig;
3948
$this->metadataPool = $metadataPool;
49+
$this->storeResolver = $storeResolver;
4050
}
4151

4252
/**
@@ -48,13 +58,23 @@ public function process(Select $select)
4858
$linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
4959
$statusAttribute = $this->eavConfig->getAttribute(Product::ENTITY, ProductInterface::STATUS);
5060

51-
$select->join(
61+
$select->joinLeft(
62+
['status_global_attr' => $statusAttribute->getBackendTable()],
63+
"status_global_attr.{$linkField} = " . self::PRODUCT_TABLE_ALIAS . ".{$linkField}"
64+
. ' AND status_global_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId()
65+
. ' AND status_global_attr.store_id = ' . Store::DEFAULT_STORE_ID,
66+
[]
67+
);
68+
69+
$select->joinLeft(
5270
['status_attr' => $statusAttribute->getBackendTable()],
53-
sprintf('status_attr.%s = %s.%1$s', $linkField, self::PRODUCT_TABLE_ALIAS),
71+
"status_attr.{$linkField} = " . self::PRODUCT_TABLE_ALIAS . ".{$linkField}"
72+
. ' AND status_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId()
73+
. ' AND status_attr.store_id = ' . $this->storeResolver->getCurrentStoreId(),
5474
[]
55-
)
56-
->where('status_attr.attribute_id = ?', $statusAttribute->getAttributeId())
57-
->where('status_attr.value = ?', Status::STATUS_ENABLED);
75+
);
76+
77+
$select->where('IFNULL(status_attr.value, status_global_attr.value) = ?', Status::STATUS_ENABLED);
5878

5979
return $select;
6080
}

app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use Magento\Framework\EntityManager\EntityMetadataInterface;
1717
use Magento\Framework\EntityManager\MetadataPool;
1818
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
19+
use Magento\Store\Api\StoreResolverInterface;
20+
use Magento\Store\Model\Store;
1921

2022
class StatusBaseSelectProcessorTest extends \PHPUnit_Framework_TestCase
2123
{
@@ -29,6 +31,11 @@ class StatusBaseSelectProcessorTest extends \PHPUnit_Framework_TestCase
2931
*/
3032
private $metadataPool;
3133

34+
/**
35+
* @var StoreResolverInterface|\PHPUnit_Framework_MockObject_MockObject
36+
*/
37+
private $storeResolver;
38+
3239
/**
3340
* @var Select|\PHPUnit_Framework_MockObject_MockObject
3441
*/
@@ -43,19 +50,22 @@ protected function setUp()
4350
{
4451
$this->eavConfig = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock();
4552
$this->metadataPool = $this->getMockBuilder(MetadataPool::class)->disableOriginalConstructor()->getMock();
53+
$this->storeResolver = $this->getMockBuilder(StoreResolverInterface::class)->getMock();
4654
$this->select = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock();
4755

4856
$this->statusBaseSelectProcessor = (new ObjectManager($this))->getObject(StatusBaseSelectProcessor::class, [
4957
'eavConfig' => $this->eavConfig,
5058
'metadataPool' => $this->metadataPool,
59+
'storeResolver' => $this->storeResolver,
5160
]);
5261
}
5362

5463
public function testProcess()
5564
{
5665
$linkField = 'link_field';
5766
$backendTable = 'backend_table';
58-
$attributeId = 'attribute_id';
67+
$attributeId = 2;
68+
$currentStoreId = 1;
5969

6070
$metadata = $this->getMock(EntityMetadataInterface::class);
6171
$metadata->expects($this->once())
@@ -66,35 +76,49 @@ public function testProcess()
6676
->with(ProductInterface::class)
6777
->willReturn($metadata);
6878

79+
/** @var AttributeInterface|\PHPUnit_Framework_MockObject_MockObject $statusAttribute */
6980
$statusAttribute = $this->getMockBuilder(AttributeInterface::class)
7081
->setMethods(['getBackendTable', 'getAttributeId'])
7182
->getMock();
72-
$statusAttribute->expects($this->once())
83+
$statusAttribute->expects($this->atLeastOnce())
7384
->method('getBackendTable')
7485
->willReturn($backendTable);
75-
$statusAttribute->expects($this->once())
86+
$statusAttribute->expects($this->atLeastOnce())
7687
->method('getAttributeId')
7788
->willReturn($attributeId);
7889
$this->eavConfig->expects($this->once())
7990
->method('getAttribute')
8091
->with(Product::ENTITY, ProductInterface::STATUS)
8192
->willReturn($statusAttribute);
8293

83-
$this->select->expects($this->once())
84-
->method('join')
94+
$this->storeResolver->expects($this->once())
95+
->method('getCurrentStoreId')
96+
->willReturn($currentStoreId);
97+
98+
$this->select->expects($this->at(0))
99+
->method('joinLeft')
85100
->with(
86-
['status_attr' => $backendTable],
87-
sprintf('status_attr.%s = %s.%1$s', $linkField, BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS),
101+
['status_global_attr' => $backendTable],
102+
"status_global_attr.{$linkField} = "
103+
. BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS . ".{$linkField}"
104+
. " AND status_global_attr.attribute_id = {$attributeId}"
105+
. ' AND status_global_attr.store_id = ' . Store::DEFAULT_STORE_ID,
88106
[]
89107
)
90108
->willReturnSelf();
91109
$this->select->expects($this->at(1))
92-
->method('where')
93-
->with('status_attr.attribute_id = ?', $attributeId)
110+
->method('joinLeft')
111+
->with(
112+
['status_attr' => $backendTable],
113+
"status_attr.{$linkField} = " . BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS . ".{$linkField}"
114+
. " AND status_attr.attribute_id = {$attributeId}"
115+
. " AND status_attr.store_id = {$currentStoreId}",
116+
[]
117+
)
94118
->willReturnSelf();
95119
$this->select->expects($this->at(2))
96120
->method('where')
97-
->with('status_attr.value = ?', Status::STATUS_ENABLED)
121+
->with('IFNULL(status_attr.value, status_global_attr.value) = ?', Status::STATUS_ENABLED)
98122
->willReturnSelf();
99123

100124
$this->assertEquals($this->select, $this->statusBaseSelectProcessor->process($this->select));

app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ public function save(\Magento\CatalogInventory\Api\Data\StockItemInterface $stoc
190190
$this->resource->save($stockItem);
191191

192192
$this->indexProcessor->reindexRow($stockItem->getProductId());
193+
$this->getStockRegistryStorage()->removeStockItem($stockItem->getProductId());
194+
$this->getStockRegistryStorage()->removeStockStatus($stockItem->getProductId());
193195
} catch (\Exception $exception) {
194196
throw new CouldNotSaveException(__('Unable to save Stock Item'), $exception);
195197
}

app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,11 @@ public function hasOptions()
129129
public function getAllowProducts()
130130
{
131131
if (!$this->hasAllowProducts()) {
132-
$products = [];
133132
$skipSaleableCheck = $this->catalogProduct->getSkipSaleableCheck();
134-
$allProducts = $this->getProduct()->getTypeInstance()->getUsedProducts($this->getProduct(), null);
135-
foreach ($allProducts as $product) {
136-
if ($product->isSaleable() || $skipSaleableCheck) {
137-
$products[] = $product;
138-
}
139-
}
133+
134+
$products = $skipSaleableCheck ?
135+
$this->getProduct()->getTypeInstance()->getUsedProducts($this->getProduct(), null) :
136+
$this->getProduct()->getTypeInstance()->getSalableUsedProducts($this->getProduct(), null);
140137
$this->setAllowProducts($products);
141138
}
142139
return $this->getData('allow_products');

app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
use Magento\Catalog\Api\Data\ProductInterface;
99
use Magento\Catalog\Api\ProductRepositoryInterface;
1010
use Magento\Catalog\Model\Config;
11+
use Magento\Catalog\Model\Product;
12+
use Magento\CatalogInventory\Api\StockRegistryInterface;
13+
use Magento\CatalogInventory\Model\Stock\Status;
1114
use Magento\Framework\App\ObjectManager;
1215
use Magento\Framework\EntityManager\MetadataPool;
1316
use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler;
@@ -162,6 +165,11 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType
162165
*/
163166
private $customerSession;
164167

168+
/**
169+
* @var StockRegistryInterface
170+
*/
171+
private $stockRegistry;
172+
165173
/**
166174
* @codingStandardsIgnoreStart/End
167175
*
@@ -204,7 +212,8 @@ public function __construct(
204212
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
205213
\Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor,
206214
\Magento\Framework\Cache\FrontendInterface $cache = null,
207-
\Magento\Customer\Model\Session $customerSession = null
215+
\Magento\Customer\Model\Session $customerSession = null,
216+
StockRegistryInterface $stockRegistry = null
208217
) {
209218
$this->typeConfigurableFactory = $typeConfigurableFactory;
210219
$this->_eavAttributeFactory = $eavAttributeFactory;
@@ -227,7 +236,8 @@ public function __construct(
227236
$logger,
228237
$productRepository
229238
);
230-
239+
$this->stockRegistry = $stockRegistry ?: ObjectManager::getInstance()
240+
->get(StockRegistryInterface::class);
231241
}
232242

233243
/**
@@ -799,17 +809,10 @@ public function isSalable($product)
799809
$salable = parent::isSalable($product);
800810

801811
if ($salable !== false) {
802-
$salable = false;
803812
if (!is_null($product)) {
804813
$this->setStoreFilter($product->getStoreId(), $product);
805814
}
806-
/** @var \Magento\Catalog\Model\Product $child */
807-
foreach ($this->getUsedProducts($product) as $child) {
808-
if ($child->isSalable()) {
809-
$salable = true;
810-
break;
811-
}
812-
}
815+
$salable = count($this->getSalableUsedProducts($product)) > 0;
813816
}
814817

815818
return $salable;
@@ -1280,4 +1283,24 @@ private function getCatalogConfig()
12801283
}
12811284
return $this->catalogConfig;
12821285
}
1286+
1287+
/**
1288+
* Retrieve array of salable "subproducts"
1289+
*
1290+
* @param Product $product
1291+
* @param array|null $requiredAttributeIds
1292+
* @return Product[]
1293+
*/
1294+
public function getSalableUsedProducts(Product $product, $requiredAttributeIds = null)
1295+
{
1296+
$usedProducts = $this->getUsedProducts($product, $requiredAttributeIds);
1297+
$usedSalableProducts = array_filter($usedProducts, function (Product $product) {
1298+
$stockStatus = $this->stockRegistry->getStockStatus(
1299+
$product->getId(),
1300+
$product->getStore()->getWebsiteId()
1301+
);
1302+
return (int)$stockStatus->getStockStatus() === Status::STATUS_IN_STOCK && $product->isSalable();
1303+
});
1304+
return $usedSalableProducts;
1305+
}
12831306
}

app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,41 @@
99

1010
use Magento\Catalog\Api\Data\ProductInterface;
1111
use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus;
12+
use Magento\Catalog\Model\Product\Attribute\Source\Status;
13+
use Magento\Store\Api\StoreResolverInterface;
14+
use Magento\Store\Model\Store;
1215

1316
class Configurable extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice
1417
{
18+
/**
19+
* @var StoreResolverInterface
20+
*/
21+
private $storeResolver;
22+
23+
/**
24+
* Class constructor
25+
*
26+
* @param \Magento\Framework\Model\ResourceModel\Db\Context $context
27+
* @param \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy
28+
* @param \Magento\Eav\Model\Config $eavConfig
29+
* @param \Magento\Framework\Event\ManagerInterface $eventManager
30+
* @param \Magento\Framework\Module\Manager $moduleManager
31+
* @param string $connectionName
32+
*/
33+
public function __construct(
34+
\Magento\Framework\Model\ResourceModel\Db\Context $context,
35+
\Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy,
36+
\Magento\Eav\Model\Config $eavConfig,
37+
\Magento\Framework\Event\ManagerInterface $eventManager,
38+
\Magento\Framework\Module\Manager $moduleManager,
39+
$connectionName = null,
40+
StoreResolverInterface $storeResolver = null
41+
) {
42+
parent::__construct($context, $tableStrategy, $eavConfig, $eventManager, $moduleManager, $connectionName);
43+
$this->storeResolver = $storeResolver ?:
44+
\Magento\Framework\App\ObjectManager::getInstance()->get(StoreResolverInterface::class);
45+
}
46+
1547
/**
1648
* Reindex temporary (price result data) for all products
1749
*
@@ -190,16 +222,20 @@ protected function _applyConfigurableOption()
190222
[]
191223
)->where(
192224
'le.required_options=0'
193-
)->join(
194-
['product_status' => $this->getTable($statusAttribute->getBackend()->getTable())],
195-
sprintf(
196-
'le.%1$s = product_status.%1$s AND product_status.attribute_id = %2$s',
197-
$linkField,
198-
$statusAttribute->getAttributeId()
199-
),
225+
)->joinLeft(
226+
['status_global_attr' => $statusAttribute->getBackendTable()],
227+
"status_global_attr.{$linkField} = le.{$linkField}"
228+
. ' AND status_global_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId()
229+
. ' AND status_global_attr.store_id = ' . Store::DEFAULT_STORE_ID,
230+
[]
231+
)->joinLeft(
232+
['status_attr' => $statusAttribute->getBackendTable()],
233+
"status_attr.{$linkField} = le.{$linkField}"
234+
. ' AND status_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId()
235+
. ' AND status_attr.store_id = ' . $this->storeResolver->getCurrentStoreId(),
200236
[]
201237
)->where(
202-
'product_status.value=' . ProductStatus::STATUS_ENABLED
238+
'IFNULL(status_attr.value, status_global_attr.value) = ?', Status::STATUS_ENABLED
203239
)->group(
204240
['e.entity_id', 'i.customer_group_id', 'i.website_id', 'l.product_id']
205241
);

0 commit comments

Comments
 (0)