Skip to content

Commit eb2ca66

Browse files
committed
MAGETWO-60103: [Backport] Configurable variation is displayed on category/product page when is out of stock - 2.1
2 parents 1f3c80b + 1265d9c commit eb2ca66

File tree

19 files changed

+278
-198
lines changed

19 files changed

+278
-198
lines changed

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/Test/Unit/Model/Product/Type/ConfigurableTest.php

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
use Magento\Catalog\Api\Data\ProductExtensionInterface;
1212
use Magento\Catalog\Api\Data\ProductInterface;
1313
use Magento\Catalog\Model\Config;
14+
use Magento\CatalogInventory\Model\Stock\Status;
1415
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
1516
use Magento\Framework\EntityManager\EntityMetadata;
1617
use Magento\Framework\EntityManager\MetadataPool;
1718
use Magento\Customer\Model\Session;
18-
use Magento\Framework\Cache\FrontendInterface;
1919

2020
/**
2121
* Class \Magento\ConfigurableProduct\Test\Unit\Model\Product\Type\ConfigurableTest
@@ -94,6 +94,11 @@ class ConfigurableTest extends \PHPUnit_Framework_TestCase
9494
*/
9595
protected $catalogConfig;
9696

97+
/**
98+
* @var \PHPUnit_Framework_MockObject_MockObject
99+
*/
100+
private $stockRegistry;
101+
97102
/**
98103
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
99104
*/
@@ -162,6 +167,10 @@ protected function setUp()
162167
->disableOriginalConstructor()
163168
->getMock();
164169

170+
$this->stockRegistry = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockRegistryInterface::class)
171+
->disableOriginalConstructor()
172+
->getMockForAbstractClass();
173+
165174
$this->_model = $this->_objectHelper->getObject(
166175
Configurable::class,
167176
[
@@ -179,6 +188,7 @@ protected function setUp()
179188
'customerSession' => $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(),
180189
'cache' => $this->cache,
181190
'catalogConfig' => $this->catalogConfig,
191+
'stockRegistry' => $this->stockRegistry,
182192
]
183193
);
184194
$refClass = new \ReflectionClass(Configurable::class);
@@ -592,26 +602,100 @@ public function testHasOptionsFalse()
592602

593603
public function testIsSalable()
594604
{
595-
$productMock = $this->getMockBuilder('\Magento\Catalog\Model\Product')
605+
$productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
596606
->setMethods(['__wakeup', 'getStatus', 'hasData', 'getData', 'getStoreId', 'setData'])
597607
->disableOriginalConstructor()
598608
->getMock();
599-
$childProductMock = $this->getMockBuilder('\Magento\Catalog\Model\Product')
600-
->setMethods(['__wakeup', 'isSalable'])
609+
$childProductMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
610+
->setMethods(['__wakeup', 'isSalable', 'getStore'])
601611
->disableOriginalConstructor()
602612
->getMock();
603613

614+
$storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)
615+
->setMethods(['getWebsiteId'])
616+
->getMockForAbstractClass();
617+
618+
$stockStatus = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockStatusInterface::class)
619+
->setMethods(['getStockStatus'])
620+
->getMockForAbstractClass();
621+
604622
$productMock->expects($this->once())->method('getStatus')->willReturn(1);
605623
$productMock->expects($this->any())->method('hasData')->willReturn(true);
606624
$productMock->expects($this->at(2))->method('getData')->with('is_salable')->willReturn(true);
607625
$productMock->expects($this->once())->method('getStoreId')->willReturn(1);
608626
$productMock->expects($this->once())->method('setData')->willReturnSelf();
609627
$productMock->expects($this->at(6))->method('getData')->willReturn([$childProductMock]);
628+
$childProductMock->expects($this->once())->method('getStore')->willReturn($storeMock);
629+
$this->stockRegistry->expects($this->once())->method('getStockStatus')->willReturn($stockStatus);
630+
$stockStatus->expects($this->once())->method('getStockStatus')->willReturn(1);
610631
$childProductMock->expects($this->once())->method('isSalable')->willReturn(true);
611632

612633
$this->assertTrue($this->_model->isSalable($productMock));
613634
}
614635

636+
/**
637+
* @param $stockStatusValue
638+
* @param $isSalable
639+
* @param $expectedCount
640+
* @dataProvider salableUsedProductsDataProvider
641+
*/
642+
public function testGetSalableUsedProducts($stockStatusValue, $isSalable, $expectedCount)
643+
{
644+
$productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
645+
->setMethods(['hasData', 'getData'])
646+
->disableOriginalConstructor()
647+
->getMock();
648+
$childProductMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
649+
->setMethods(['isSalable', 'getStore'])
650+
->disableOriginalConstructor()
651+
->getMock();
652+
653+
$storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)
654+
->setMethods(['getWebsiteId'])
655+
->getMockForAbstractClass();
656+
657+
$stockStatus = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockStatusInterface::class)
658+
->setMethods(['getStockStatus'])
659+
->getMockForAbstractClass();
660+
661+
$productMock->expects($this->at(0))->method('hasData')->willReturn(true);
662+
$productMock->expects($this->at(1))->method('getData')->willReturn([$childProductMock]);
663+
$childProductMock->expects($this->once())->method('getStore')->willReturn($storeMock);
664+
$this->stockRegistry->expects($this->once())->method('getStockStatus')->willReturn($stockStatus);
665+
$stockStatus->expects($this->once())->method('getStockStatus')->willReturn($stockStatusValue);
666+
$childProductMock->expects($this->any())->method('isSalable')->willReturn($isSalable);
667+
668+
$this->assertCount($expectedCount, $this->_model->getSalableUsedProducts($productMock));
669+
}
670+
671+
/**
672+
* @return array
673+
*/
674+
public function salableUsedProductsDataProvider()
675+
{
676+
return [
677+
[Status::STATUS_OUT_OF_STOCK, false, 0],
678+
[Status::STATUS_OUT_OF_STOCK, true, 0],
679+
[Status::STATUS_IN_STOCK, false, 0],
680+
[Status::STATUS_IN_STOCK, true, 1],
681+
];
682+
}
683+
684+
/**
685+
* No child products assigned
686+
*/
687+
public function testGetSalableUsedProductsEmpty()
688+
{
689+
$productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
690+
->setMethods(['hasData', 'getData'])
691+
->disableOriginalConstructor()
692+
->getMock();
693+
694+
$productMock->expects($this->at(0))->method('hasData')->willReturn(true);
695+
$productMock->expects($this->at(1))->method('getData')->willReturn([]);
696+
$this->assertCount(0, $this->_model->getSalableUsedProducts($productMock));
697+
}
698+
615699
public function testGetSelectedAttributesInfo()
616700
{
617701
$productMock = $this->getMockBuilder('\Magento\Catalog\Model\Product')

app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/ConfigurableTest.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,9 @@ public function testSetIsProductListingContext()
154154
private function prepareGetJsonSwatchConfig()
155155
{
156156
$product1 = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false);
157-
$product1->expects($this->atLeastOnce())->method('isSaleable')->willReturn(true);
158157
$product1->expects($this->atLeastOnce())->method('getData')->with('code')->willReturn(1);
159158

160159
$product2 = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false);
161-
$product2->expects($this->atLeastOnce())->method('isSaleable')->willReturn(true);
162160
$product2->expects($this->atLeastOnce())->method('getData')->with('code')->willReturn(3);
163161

164162
$simpleProducts = [$product1, $product2];
@@ -169,7 +167,7 @@ private function prepareGetJsonSwatchConfig()
169167
'',
170168
false
171169
);
172-
$configurableType->expects($this->atLeastOnce())->method('getUsedProducts')->with($this->product, null)
170+
$configurableType->expects($this->atLeastOnce())->method('getSalableUsedProducts')->with($this->product, null)
173171
->willReturn($simpleProducts);
174172
$this->product->expects($this->any())->method('getTypeInstance')->willReturn($configurableType);
175173

app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,11 +179,9 @@ public function testGetJsonSwatchUsedInProductListing()
179179
private function prepareGetJsonSwatchConfig()
180180
{
181181
$product1 = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false);
182-
$product1->expects($this->atLeastOnce())->method('isSaleable')->willReturn(true);
183182
$product1->expects($this->any())->method('getData')->with('code')->willReturn(1);
184183

185184
$product2 = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false);
186-
$product2->expects($this->atLeastOnce())->method('isSaleable')->willReturn(true);
187185
$product2->expects($this->any())->method('getData')->with('code')->willReturn(3);
188186

189187
$simpleProducts = [$product1, $product2];
@@ -194,7 +192,7 @@ private function prepareGetJsonSwatchConfig()
194192
'',
195193
false
196194
);
197-
$configurableType->expects($this->atLeastOnce())->method('getUsedProducts')->with($this->product, null)
195+
$configurableType->expects($this->atLeastOnce())->method('getSalableUsedProducts')->with($this->product, null)
198196
->willReturn($simpleProducts);
199197
$this->product->expects($this->any())->method('getTypeInstance')->willReturn($configurableType);
200198

dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public function testReindexEntitiesForConfigurableProduct()
5858
/** @var ProductRepositoryInterface $productRepository */
5959
$productRepository = Bootstrap::getObjectManager()
6060
->create(ProductRepositoryInterface::class);
61+
$product = $productRepository->get('configurable');
6162

6263
/** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attr **/
6364
$attr = Bootstrap::getObjectManager()->get('Magento\Eav\Model\Config')
@@ -75,7 +76,7 @@ public function testReindexEntitiesForConfigurableProduct()
7576

7677
$connection = $this->productResource->getConnection();
7778
$select = $connection->select()->from($this->productResource->getTable('catalog_product_index_eav'))
78-
->where('entity_id = ?', 1)
79+
->where('entity_id = ?', $product->getId())
7980
->where('attribute_id = ?', $attr->getId())
8081
->where('value IN (?)', $optionIds);
8182

dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Export/RowCustomizerTest.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
*/
66
namespace Magento\ConfigurableImportExport\Model\Export;
77

8+
use Magento\TestFramework\Helper\Bootstrap;
9+
use Magento\Catalog\Api\ProductRepositoryInterface;
10+
811
/**
912
* @magentoAppArea adminhtml
1013
*/
@@ -33,11 +36,14 @@ protected function setUp()
3336
*/
3437
public function testPrepareData()
3538
{
39+
$productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class);
40+
$product = $productRepository->get('configurable');
41+
3642
$collection = $this->objectManager->get('Magento\Catalog\Model\ResourceModel\Product\Collection');
3743
$select = (string)$collection->getSelect();
38-
$this->model->prepareData($collection, [1, 2, 3, 4]);
44+
$this->model->prepareData($collection, [$product->getId(), 2, 3, 4]);
3945
$this->assertEquals($select, (string)$collection->getSelect());
40-
$result = $this->model->addData([], 1);
46+
$result = $this->model->addData([], $product->getId());
4147
$this->assertArrayHasKey('configurable_variations', $result);
4248
$this->assertArrayHasKey('configurable_variation_labels', $result);
4349
$this->assertEquals(

dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/MatrixTest.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66
namespace Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config;
77

8+
use Magento\TestFramework\Helper\Bootstrap;
9+
use Magento\Catalog\Api\ProductRepositoryInterface;
810

911
/**
1012
* @magentoAppArea adminhtml
@@ -20,13 +22,13 @@ class MatrixTest extends \Magento\TestFramework\TestCase\AbstractBackendControll
2022
*/
2123
public function testGetVariations()
2224
{
25+
$productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class);
26+
$product = $productRepository->get('configurable');
2327
$this->_objectManager->get(
2428
'Magento\Framework\Registry'
2529
)->register(
2630
'current_product',
27-
\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(
28-
'Magento\Catalog\Model\Product'
29-
)->load(1)
31+
$product
3032
);
3133
\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(
3234
'Magento\Framework\View\LayoutInterface'

0 commit comments

Comments
 (0)