Skip to content

Commit 1e78fe2

Browse files
authored
Merge pull request #630 from magento-tango/MAGETWO-60483
Bugs fixed: * MAGETWO-60483: Catalog broken when all child products of configurable are disabled
2 parents 0bb4d6c + fa60b03 commit 1e78fe2

File tree

9 files changed

+174
-35
lines changed

9 files changed

+174
-35
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
/**
3+
* Copyright © 2016 Magento. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Catalog\Model\Product\Pricing\Renderer;
8+
9+
/**
10+
* Resolvers check whether product available for sale or not
11+
*/
12+
class SalableResolver implements SalableResolverInterface
13+
{
14+
/**
15+
* Check whether product available for sale
16+
*
17+
* @param \Magento\Framework\Pricing\SaleableInterface $salableItem
18+
* @return boolean
19+
*/
20+
public function isSalable(\Magento\Framework\Pricing\SaleableInterface $salableItem)
21+
{
22+
return $salableItem->getCanShowPrice() !== false && $salableItem->isSalable();
23+
}
24+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
/**
3+
* Copyright © 2016 Magento. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Catalog\Model\Product\Pricing\Renderer;
8+
9+
/**
10+
* Interface resolver checks whether product available for sale
11+
*/
12+
interface SalableResolverInterface
13+
{
14+
/**
15+
* Check whether product available for sale
16+
*
17+
* @param \Magento\Framework\Pricing\SaleableInterface $salableItem
18+
* @return boolean
19+
*/
20+
public function isSalable(\Magento\Framework\Pricing\SaleableInterface $salableItem);
21+
}

app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
use Magento\Framework\Pricing\Render;
1111
use Magento\Framework\Pricing\Render\PriceBox as BasePriceBox;
1212
use Magento\Msrp\Pricing\Price\MsrpPrice;
13+
use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface;
14+
use Magento\Framework\View\Element\Template\Context;
15+
use Magento\Framework\Pricing\SaleableInterface;
16+
use Magento\Framework\Pricing\Price\PriceInterface;
17+
use Magento\Framework\Pricing\Render\RendererPool;
1318

1419
/**
1520
* Class for final_price rendering
@@ -19,12 +24,38 @@
1924
*/
2025
class FinalPriceBox extends BasePriceBox
2126
{
27+
/**
28+
* @var SalableResolverInterface
29+
*/
30+
private $salableResolver;
31+
32+
/**
33+
* @param Context $context
34+
* @param SaleableInterface $saleableItem
35+
* @param PriceInterface $price
36+
* @param RendererPool $rendererPool
37+
* @param array $data
38+
* @param SalableResolverInterface $salableResolver
39+
*/
40+
public function __construct(
41+
Context $context,
42+
SaleableInterface $saleableItem,
43+
PriceInterface $price,
44+
RendererPool $rendererPool,
45+
array $data = [],
46+
SalableResolverInterface $salableResolver = null
47+
) {
48+
parent::__construct($context, $saleableItem, $price, $rendererPool, $data);
49+
$this->salableResolver = $salableResolver ?: \Magento\Framework\App\ObjectManager::getInstance()
50+
->get(SalableResolverInterface::class);
51+
}
52+
2253
/**
2354
* @return string
2455
*/
2556
protected function _toHtml()
2657
{
27-
if (!$this->getSaleableItem() || $this->getSaleableItem()->getCanShowPrice() === false) {
58+
if (!$this->salableResolver->isSalable($this->getSaleableItem())) {
2859
return '';
2960
}
3061

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
/**
3+
* Copyright © 2016 Magento. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Catalog\Test\Unit\Model\Product\Pricing\Renderer;
8+
9+
class SalableResolverTest extends \PHPUnit_Framework_TestCase
10+
{
11+
/**
12+
* @var \Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolver
13+
*/
14+
protected $object;
15+
16+
/**
17+
* @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject
18+
*/
19+
protected $product;
20+
21+
protected function setUp()
22+
{
23+
$this->product = $this->getMock(
24+
\Magento\Catalog\Model\Product::class,
25+
['__wakeup', 'getCanShowPrice', 'isSalable'],
26+
[],
27+
'',
28+
false
29+
);
30+
31+
$objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
32+
$this->object = $objectManager->getObject(
33+
\Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolver::class
34+
);
35+
}
36+
37+
public function testSalableItem()
38+
{
39+
$this->product->expects($this->any())
40+
->method('getCanShowPrice')
41+
->willReturn(true);
42+
43+
$this->product->expects($this->any())->method('isSalable')->willReturn(true);
44+
45+
$result = $this->object->isSalable($this->product);
46+
$this->assertTrue($result);
47+
}
48+
49+
public function testNotSalableItem()
50+
{
51+
$this->product->expects($this->any())
52+
->method('getCanShowPrice')
53+
->willReturn(true);
54+
55+
$this->product->expects($this->any())->method('isSalable')->willReturn(false);
56+
57+
$result = $this->object->isSalable($this->product);
58+
$this->assertFalse($result);
59+
}
60+
}

app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
namespace Magento\Catalog\Test\Unit\Pricing\Render;
88

9+
use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface;
10+
911
/**
1012
* Class FinalPriceBoxTest
1113
*
@@ -58,11 +60,16 @@ class FinalPriceBoxTest extends \PHPUnit_Framework_TestCase
5860
*/
5961
protected $price;
6062

63+
/**
64+
* @var SalableResolverInterface|\PHPUnit_Framework_MockObject_MockObject
65+
*/
66+
private $salableResolverMock;
67+
6168
protected function setUp()
6269
{
6370
$this->product = $this->getMock(
6471
\Magento\Catalog\Model\Product::class,
65-
['getPriceInfo', '__wakeup', 'getCanShowPrice'],
72+
['getPriceInfo', '__wakeup', 'getCanShowPrice', 'isSalable'],
6673
[],
6774
'',
6875
false
@@ -78,9 +85,7 @@ protected function setUp()
7885
$this->priceBox = $this->getMock(\Magento\Framework\Pricing\Render\PriceBox::class, [], [], '', false);
7986
$this->logger = $this->getMock(\Psr\Log\LoggerInterface::class);
8087

81-
$this->layout->expects($this->any())
82-
->method('getBlock')
83-
->will($this->returnValue($this->priceBox));
88+
$this->layout->expects($this->any())->method('getBlock')->willReturn($this->priceBox);
8489

8590
$cacheState = $this->getMockBuilder(\Magento\Framework\App\Cache\StateInterface::class)
8691
->getMockForAbstractClass();
@@ -93,12 +98,9 @@ protected function setUp()
9398
->disableOriginalConstructor()
9499
->getMock();
95100

96-
$urlBuilder = $this->getMockBuilder(\Magento\Framework\UrlInterface::class)
97-
->getMockForAbstractClass();
98-
99-
$store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)
100-
->getMockForAbstractClass();
101+
$urlBuilder = $this->getMockBuilder(\Magento\Framework\UrlInterface::class)->getMockForAbstractClass();
101102

103+
$store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)->getMockForAbstractClass();
102104
$storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class)
103105
->setMethods(['getStore', 'getCode'])
104106
->getMockForAbstractClass();
@@ -144,14 +146,19 @@ protected function setUp()
144146
->will($this->returnValue(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE));
145147

146148
$objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
149+
$this->salableResolverMock = $this->getMockBuilder(SalableResolverInterface::class)
150+
->disableOriginalConstructor()
151+
->getMockForAbstractClass();
152+
147153
$this->object = $objectManager->getObject(
148154
\Magento\Catalog\Pricing\Render\FinalPriceBox::class,
149155
[
150156
'context' => $context,
151157
'saleableItem' => $this->product,
152158
'rendererPool' => $this->rendererPool,
153159
'price' => $this->price,
154-
'data' => ['zone' => 'test_zone', 'list_category_page' => true]
160+
'data' => ['zone' => 'test_zone', 'list_category_page' => true],
161+
'salableResolver' => $this->salableResolverMock
155162
]
156163
);
157164
}
@@ -169,6 +176,8 @@ public function testRenderMsrpDisabled()
169176
->with($this->equalTo($this->product))
170177
->will($this->returnValue(false));
171178

179+
$this->salableResolverMock->expects($this->once())->method('isSalable')->with($this->product)->willReturn(true);
180+
172181
$result = $this->object->toHtml();
173182

174183
//assert price wrapper
@@ -177,6 +186,18 @@ public function testRenderMsrpDisabled()
177186
$this->assertRegExp('/[final_price]/', $result);
178187
}
179188

189+
public function testNotSalableItem()
190+
{
191+
$this->salableResolverMock
192+
->expects($this->once())
193+
->method('isSalable')
194+
->with($this->product)
195+
->willReturn(false);
196+
$result = $this->object->toHtml();
197+
198+
$this->assertEmpty($result);
199+
}
200+
180201
public function testRenderMsrpEnabled()
181202
{
182203
$priceType = $this->getMock(\Magento\Msrp\Pricing\Price\MsrpPrice::class, [], [], '', false);
@@ -211,6 +232,8 @@ public function testRenderMsrpEnabled()
211232
->with('msrp_price', $this->product, $arguments)
212233
->will($this->returnValue($priceBoxRender));
213234

235+
$this->salableResolverMock->expects($this->once())->method('isSalable')->with($this->product)->willReturn(true);
236+
214237
$result = $this->object->toHtml();
215238

216239
//assert price wrapper
@@ -230,6 +253,8 @@ public function testRenderMsrpNotRegisteredException()
230253
->with($this->equalTo('msrp_price'))
231254
->will($this->throwException(new \InvalidArgumentException()));
232255

256+
$this->salableResolverMock->expects($this->once())->method('isSalable')->with($this->product)->willReturn(true);
257+
233258
$result = $this->object->toHtml();
234259

235260
//assert price wrapper

app/code/Magento/Catalog/etc/di.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
<preference for="Magento\Catalog\Api\Data\CategorySearchResultsInterface" type="Magento\Framework\Api\SearchResults" />
4949
<preference for="Magento\Catalog\Model\Config\Source\ProductPriceOptionsInterface" type="Magento\Catalog\Model\Config\Source\Product\Options\Price"/>
5050
<preference for="Magento\Catalog\Model\Indexer\Product\Flat\Table\BuilderInterface" type="Magento\Catalog\Model\Indexer\Product\Flat\Table\Builder"/>
51+
<preference for="Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface" type="Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolver"/>
5152
<type name="Magento\Customer\Model\ResourceModel\Visitor">
5253
<plugin name="catalogLog" type="Magento\Catalog\Model\Plugin\Log" />
5354
</type>

app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,6 @@ public function resolvePrice(\Magento\Framework\Pricing\SaleableInterface $produ
6464
$productPrice = $this->priceResolver->resolvePrice($subProduct);
6565
$price = $price ? min($price, $productPrice) : $productPrice;
6666
}
67-
if ($price === null) {
68-
throw new \Magento\Framework\Exception\LocalizedException(
69-
__('Configurable product "%1" does not have sub-products', $product->getSku())
70-
);
71-
}
7267

7368
return (float)$price;
7469
}

app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,24 +51,6 @@ protected function setUp()
5151
);
5252
}
5353

54-
/**
55-
* situation: There are no used products, thus there are no prices
56-
*
57-
* @expectedException \Magento\Framework\Exception\LocalizedException
58-
*/
59-
public function testResolvePriceWithNoPrices()
60-
{
61-
$product = $this->getMockBuilder(
62-
\Magento\Catalog\Model\Product::class
63-
)->disableOriginalConstructor()->getMock();
64-
65-
$product->expects($this->once())->method('getSku')->willReturn('Kiwi');
66-
67-
$this->lowestPriceOptionsProvider->expects($this->once())->method('getProducts')->willReturn([]);
68-
69-
$this->resolver->resolvePrice($product);
70-
}
71-
7254
/**
7355
* situation: one product is supplying the price, which could be a price of zero (0)
7456
*

dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@
120120
<constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal"/>
121121
</variation>
122122
<variation name="OnePageCheckoutTestVariation5" summary="Guest Checkout using Check/Money Order and Free Shipping with Prices/Taxes Verifications" ticketId="MAGETWO-12412">
123-
<data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data>
123+
<data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, stable:no</data>
124124
<data name="products/0" xsi:type="string">catalogProductSimple::product_10_dollar</data>
125125
<data name="products/1" xsi:type="string">configurableProduct::with_one_option</data>
126126
<data name="products/2" xsi:type="string">bundleProduct::bundle_fixed_100_dollar_product</data>

0 commit comments

Comments
 (0)