Skip to content

Commit f631c90

Browse files
committed
#12970: [Forwardport] Can't add grouped product, with out of stock sub product, to cart.
1 parent 9186f4a commit f631c90

File tree

6 files changed

+311
-16
lines changed

6 files changed

+311
-16
lines changed

app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ protected function getProductInfo(\Magento\Framework\DataObject $buyRequest, $pr
347347
if ($isStrictProcessMode && !$subProduct->getQty()) {
348348
return __('Please specify the quantity of product(s).')->render();
349349
}
350-
$productsInfo[$subProduct->getId()] = (int)$subProduct->getQty();
350+
$productsInfo[$subProduct->getId()] = $subProduct->isSalable() ? (float)$subProduct->getQty() : 0;
351351
}
352352
}
353353

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
/** @var $product \Magento\Catalog\Model\Product */
8+
$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class);
9+
$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL)
10+
->setId(31)
11+
->setAttributeSetId(4)
12+
->setWebsiteIds([1])
13+
->setName('Virtual Product Out')
14+
->setSku('virtual-product-out')
15+
->setPrice(10)
16+
->setTaxClassId(0)
17+
->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH)
18+
->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED)
19+
->setStockData(['is_in_stock' => 0])
20+
->save();
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
/** @var \Magento\Framework\Registry $registry */
8+
$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
9+
->get(\Magento\Framework\Registry::class);
10+
11+
$registry->unregister('isSecureArea');
12+
$registry->register('isSecureArea', true);
13+
14+
/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
15+
$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
16+
->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);
17+
18+
try {
19+
$product = $productRepository->get('virtual-product-out', false, null, true);
20+
$productRepository->delete($product);
21+
} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) {
22+
//Product already removed
23+
}
24+
25+
$registry->unregister('isSecureArea');
26+
$registry->register('isSecureArea', false);

dev/tests/integration/testsuite/Magento/GroupedProduct/Model/Product/Type/GroupedTest.php

Lines changed: 179 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,19 @@
55
*/
66
namespace Magento\GroupedProduct\Model\Product\Type;
77

8+
use Magento\Catalog\Api\ProductRepositoryInterface;
9+
use Magento\Catalog\Model\Product;
10+
use Magento\CatalogInventory\Model\Configuration;
11+
use Magento\Framework\App\Config\ReinitableConfigInterface;
12+
use Magento\Framework\App\Config\Value;
13+
814
class GroupedTest extends \PHPUnit\Framework\TestCase
915
{
16+
/**
17+
* @var ReinitableConfigInterface
18+
*/
19+
private $reinitableConfig;
20+
1021
/**
1122
* @var \Magento\Framework\ObjectManagerInterface
1223
*/
@@ -20,16 +31,21 @@ class GroupedTest extends \PHPUnit\Framework\TestCase
2031
protected function setUp()
2132
{
2233
$this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
23-
2434
$this->_productType = $this->objectManager->get(\Magento\Catalog\Model\Product\Type::class);
35+
$this->reinitableConfig = $this->objectManager->get(ReinitableConfigInterface::class);
36+
}
37+
38+
protected function tearDown()
39+
{
40+
$this->dropConfigValue(Configuration::XML_PATH_SHOW_OUT_OF_STOCK);
2541
}
2642

2743
public function testFactory()
2844
{
2945
$product = new \Magento\Framework\DataObject();
30-
$product->setTypeId(\Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE);
46+
$product->setTypeId(Grouped::TYPE_CODE);
3147
$type = $this->_productType->factory($product);
32-
$this->assertInstanceOf(\Magento\GroupedProduct\Model\Product\Type\Grouped::class, $type);
48+
$this->assertInstanceOf(Grouped::class, $type);
3349
}
3450

3551
/**
@@ -38,12 +54,12 @@ public function testFactory()
3854
*/
3955
public function testGetAssociatedProducts()
4056
{
41-
$productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);
57+
$productRepository = $this->objectManager->create(ProductRepositoryInterface::class);
4258

43-
/** @var \Magento\Catalog\Model\Product $product */
59+
/** @var Product $product */
4460
$product = $productRepository->get('grouped-product');
4561
$type = $product->getTypeInstance();
46-
$this->assertInstanceOf(\Magento\GroupedProduct\Model\Product\Type\Grouped::class, $type);
62+
$this->assertInstanceOf(Grouped::class, $type);
4763

4864
$associatedProducts = $type->getAssociatedProducts($product);
4965
$this->assertCount(2, $associatedProducts);
@@ -53,7 +69,7 @@ public function testGetAssociatedProducts()
5369
}
5470

5571
/**
56-
* @param \Magento\Catalog\Model\Product $product
72+
* @param Product $product
5773
*/
5874
private function assertProductInfo($product)
5975
{
@@ -92,25 +108,25 @@ public function testPrepareProduct()
92108
\Magento\Framework\DataObject::class,
93109
['data' => ['value' => ['qty' => 2]]]
94110
);
95-
/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
96-
$productRepository = $this->objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class);
111+
/** @var ProductRepositoryInterface $productRepository */
112+
$productRepository = $this->objectManager->get(ProductRepositoryInterface::class);
97113
$product = $productRepository->get('grouped-product');
98114

99-
/** @var \Magento\GroupedProduct\Model\Product\Type\Grouped $type */
100-
$type = $this->objectManager->get(\Magento\GroupedProduct\Model\Product\Type\Grouped::class);
115+
/** @var Grouped $type */
116+
$type = $this->objectManager->get(Grouped::class);
101117

102118
$processModes = [
103-
\Magento\GroupedProduct\Model\Product\Type\Grouped::PROCESS_MODE_FULL,
104-
\Magento\GroupedProduct\Model\Product\Type\Grouped::PROCESS_MODE_LITE
119+
Grouped::PROCESS_MODE_FULL,
120+
Grouped::PROCESS_MODE_LITE
105121
];
106122
$expectedData = [
107-
\Magento\GroupedProduct\Model\Product\Type\Grouped::PROCESS_MODE_FULL => [
123+
Grouped::PROCESS_MODE_FULL => [
108124
1 => '{"super_product_config":{"product_type":"grouped","product_id":"'
109125
. $product->getId() . '"}}',
110126
21 => '{"super_product_config":{"product_type":"grouped","product_id":"'
111127
. $product->getId() . '"}}',
112128
],
113-
\Magento\GroupedProduct\Model\Product\Type\Grouped::PROCESS_MODE_LITE => [
129+
Grouped::PROCESS_MODE_LITE => [
114130
$product->getId() => '{"value":{"qty":2}}',
115131
]
116132
];
@@ -127,4 +143,152 @@ public function testPrepareProduct()
127143
}
128144
}
129145
}
146+
147+
/**
148+
* Test adding grouped product to cart when one of subproducts is out of stock.
149+
*
150+
* @magentoDataFixture Magento/GroupedProduct/_files/product_grouped_with_out_of_stock.php
151+
* @magentoAppArea frontend
152+
* @magentoAppIsolation enabled
153+
* @magentoDbIsolation enabled
154+
* @dataProvider outOfStockSubProductDataProvider
155+
* @param bool $outOfStockShown
156+
* @param array $data
157+
* @param array $expected
158+
*/
159+
public function testOutOfStockSubProduct(bool $outOfStockShown, array $data, array $expected)
160+
{
161+
$this->changeConfigValue(Configuration::XML_PATH_SHOW_OUT_OF_STOCK, $outOfStockShown);
162+
$buyRequest = new \Magento\Framework\DataObject($data);
163+
$productRepository = $this->objectManager->create(ProductRepositoryInterface::class);
164+
/** @var Product $product */
165+
$product = $productRepository->get('grouped-product');
166+
/** @var Grouped $groupedProduct */
167+
$groupedProduct = $this->objectManager->get(Grouped::class);
168+
$actual = $groupedProduct->prepareForCartAdvanced($buyRequest, $product, Grouped::PROCESS_MODE_FULL);
169+
self::assertEquals(
170+
count($expected),
171+
count($actual)
172+
);
173+
/** @var Product $product */
174+
foreach ($actual as $product) {
175+
$sku = $product->getSku();
176+
self::assertEquals(
177+
$expected[$sku],
178+
$product->getCartQty(),
179+
"Failed asserting that Product Cart Quantity matches expected"
180+
);
181+
}
182+
}
183+
184+
/**
185+
* Data provider for testOutOfStockSubProduct.
186+
*
187+
* @return array
188+
*/
189+
public function outOfStockSubProductDataProvider()
190+
{
191+
return [
192+
'Out of stock product are shown #1' => [
193+
true,
194+
[
195+
'product' => 3,
196+
'qty' => 1,
197+
'super_group' => [
198+
1 => 4,
199+
21 => 5,
200+
],
201+
],
202+
[
203+
'virtual-product' => 5,
204+
'simple' => 4
205+
],
206+
],
207+
'Out of stock product are shown #2' => [
208+
true,
209+
[
210+
'product' => 3,
211+
'qty' => 1,
212+
'super_group' => [
213+
1 => 0,
214+
],
215+
],
216+
[
217+
'virtual-product' => 2.5, // This is a default quantity.
218+
],
219+
],
220+
'Out of stock product are hidden #1' => [
221+
false,
222+
[
223+
'product' => 3,
224+
'qty' => 1,
225+
'super_group' => [
226+
1 => 4,
227+
21 => 5,
228+
],
229+
],
230+
[
231+
'virtual-product' => 5,
232+
'simple' => 4,
233+
],
234+
],
235+
'Out of stock product are hidden #2' => [
236+
false,
237+
[
238+
'product' => 3,
239+
'qty' => 1,
240+
'super_group' => [
241+
1 => 0,
242+
],
243+
],
244+
[
245+
'virtual-product' => 2.5, // This is a default quantity.
246+
],
247+
],
248+
];
249+
}
250+
251+
/**
252+
* Write config value to database.
253+
*
254+
* @param string $path
255+
* @param string $value
256+
* @param string $scope
257+
* @param int $scopeId
258+
*/
259+
private function changeConfigValue(string $path, string $value, string $scope = 'default', int $scopeId = 0)
260+
{
261+
$configValue = $this->objectManager->create(Value::class);
262+
$configValue->setPath($path)
263+
->setValue($value)
264+
->setScope($scope)
265+
->setScopeId($scopeId)
266+
->save();
267+
$this->reinitConfig();
268+
}
269+
270+
/**
271+
* Delete config value from database.
272+
*
273+
* @param string $path
274+
*/
275+
private function dropConfigValue(string $path)
276+
{
277+
$configValue = $this->objectManager->create(Value::class);
278+
try {
279+
$configValue->load($path, 'path');
280+
$configValue->delete();
281+
} catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
282+
// do nothing
283+
}
284+
$this->reinitConfig();
285+
}
286+
287+
/**
288+
* Reinit config.
289+
*/
290+
private function reinitConfig()
291+
{
292+
$this->reinitableConfig->reinit();
293+
}
130294
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
\<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
require realpath(__DIR__ . '/../../') . '/Catalog/_files/product_associated.php';
8+
require realpath(__DIR__ . '/../../') . '/Catalog/_files/product_virtual_in_stock.php';
9+
require realpath(__DIR__ . '/../../') . '/Catalog/_files/product_virtual_out_of_stock.php';
10+
11+
$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
12+
$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class);
13+
14+
/** @var $product \Magento\Catalog\Model\Product */
15+
$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class);
16+
$product->isObjectNew(true);
17+
$product->setTypeId(
18+
\Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE
19+
)->setAttributeSetId(
20+
4
21+
)->setWebsiteIds(
22+
[1]
23+
)->setName(
24+
'Grouped Product'
25+
)->setSku(
26+
'grouped-product'
27+
)->setPrice(
28+
100
29+
)->setTaxClassId(
30+
0
31+
)->setVisibility(
32+
\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH
33+
)->setStatus(
34+
\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED
35+
);
36+
37+
$newLinks = [];
38+
$productLinkFactory = $objectManager->get(\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory::class);
39+
40+
/** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */
41+
$productLink = $productLinkFactory->create();
42+
$linkedProduct = $productRepository->getById(1);
43+
$productLink->setSku($product->getSku())
44+
->setLinkType('associated')
45+
->setLinkedProductSku($linkedProduct->getSku())
46+
->setLinkedProductType($linkedProduct->getTypeId())
47+
->setPosition(1)
48+
->getExtensionAttributes()
49+
->setQty(1);
50+
$newLinks[] = $productLink;
51+
52+
$subProductsSkus = ['virtual-product', 'virtual-product-out'];
53+
foreach ($subProductsSkus as $sku) {
54+
/** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */
55+
$productLink = $productLinkFactory->create();
56+
$linkedProduct = $productRepository->get($sku);
57+
$productLink->setSku($product->getSku())
58+
->setLinkType('associated')
59+
->setLinkedProductSku($sku)
60+
->setLinkedProductType($linkedProduct->getTypeId())
61+
->getExtensionAttributes()
62+
->setQty(2.5);
63+
$newLinks[] = $productLink;
64+
}
65+
$product->setProductLinks($newLinks);
66+
$product->save();
67+
68+
/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */
69+
$categoryLinkManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
70+
->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class);
71+
72+
$categoryLinkManagement->assignProductToCategories(
73+
$product->getSku(),
74+
[2]
75+
);

0 commit comments

Comments
 (0)