Skip to content

Commit b57eb03

Browse files
committed
#13126: 2.2.2 - Duplicating Bundle Product Removes Bundle Options From Original Product
1 parent 8848d94 commit b57eb03

File tree

3 files changed

+277
-7
lines changed
  • app/code/Magento/Bundle
  • dev/tests/integration/testsuite/Magento/Bundle/Controller/Adminhtml

3 files changed

+277
-7
lines changed

app/code/Magento/Bundle/Model/Product/CopyConstructor/Bundle.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,16 @@ public function build(Product $product, Product $duplicate)
2727
$bundleOptions = $product->getExtensionAttributes()->getBundleProductOptions() ?: [];
2828
$duplicatedBundleOptions = [];
2929
foreach ($bundleOptions as $key => $bundleOption) {
30-
$duplicatedBundleOptions[$key] = clone $bundleOption;
30+
$duplicatedBundleOption = clone $bundleOption;
31+
/**
32+
* Set option and selection ids to 'null' in order to create new option(selection) for duplicated product,
33+
* but not modifying existing one, which led to lost of option(selection) in original product.
34+
*/
35+
foreach ($duplicatedBundleOption->getProductLinks() as $productLink) {
36+
$productLink->setSelectionId(null);
37+
}
38+
$duplicatedBundleOption->setOptionId(null);
39+
$duplicatedBundleOptions[$key] = $duplicatedBundleOption;
3140
}
3241
$duplicate->getExtensionAttributes()->setBundleProductOptions($duplicatedBundleOptions);
3342
}

app/code/Magento/Bundle/Test/Unit/Model/Product/CopyConstructor/BundleTest.php

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
namespace Magento\Bundle\Test\Unit\Model\Product\CopyConstructor;
77

88
use Magento\Bundle\Api\Data\BundleOptionInterface;
9+
use Magento\Bundle\Model\Link;
910
use Magento\Bundle\Model\Product\CopyConstructor\Bundle;
1011
use Magento\Catalog\Api\Data\ProductExtensionInterface;
1112
use Magento\Catalog\Model\Product;
@@ -45,6 +46,7 @@ public function testBuildNegative()
4546
*/
4647
public function testBuildPositive()
4748
{
49+
/** @var Product|\PHPUnit_Framework_MockObject_MockObject $product */
4850
$product = $this->getMockBuilder(Product::class)
4951
->disableOriginalConstructor()
5052
->getMock();
@@ -60,18 +62,42 @@ public function testBuildPositive()
6062
->method('getExtensionAttributes')
6163
->willReturn($extensionAttributesProduct);
6264

65+
$productLink = $this->getMockBuilder(Link::class)
66+
->setMethods(['setSelectionId'])
67+
->disableOriginalConstructor()
68+
->getMock();
69+
$productLink->expects($this->exactly(2))
70+
->method('setSelectionId')
71+
->with($this->identicalTo(null));
72+
$firstOption = $this->getMockBuilder(BundleOptionInterface::class)
73+
->setMethods(['getProductLinks'])
74+
->disableOriginalConstructor()
75+
->getMockForAbstractClass();
76+
$firstOption->expects($this->once())
77+
->method('getProductLinks')
78+
->willReturn([$productLink]);
79+
$firstOption->expects($this->once())
80+
->method('setOptionId')
81+
->with($this->identicalTo(null));
82+
$secondOption = $this->getMockBuilder(BundleOptionInterface::class)
83+
->setMethods(['getProductLinks'])
84+
->disableOriginalConstructor()
85+
->getMockForAbstractClass();
86+
$secondOption->expects($this->once())
87+
->method('getProductLinks')
88+
->willReturn([$productLink]);
89+
$secondOption->expects($this->once())
90+
->method('setOptionId')
91+
->with($this->identicalTo(null));
6392
$bundleOptions = [
64-
$this->getMockBuilder(BundleOptionInterface::class)
65-
->disableOriginalConstructor()
66-
->getMockForAbstractClass(),
67-
$this->getMockBuilder(BundleOptionInterface::class)
68-
->disableOriginalConstructor()
69-
->getMockForAbstractClass()
93+
$firstOption,
94+
$secondOption
7095
];
7196
$extensionAttributesProduct->expects($this->once())
7297
->method('getBundleProductOptions')
7398
->willReturn($bundleOptions);
7499

100+
/** @var Product|\PHPUnit_Framework_MockObject_MockObject $duplicate */
75101
$duplicate = $this->getMockBuilder(Product::class)
76102
->disableOriginalConstructor()
77103
->getMock();
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Bundle\Controller\Adminhtml;
8+
9+
use Magento\Bundle\Api\Data\OptionInterface;
10+
use Magento\Catalog\Api\ProductRepositoryInterface;
11+
use Magento\Catalog\Model\Product;
12+
use Magento\Catalog\Model\Product\Type;
13+
use Magento\Framework\Data\Form\FormKey;
14+
use Magento\Framework\Message\MessageInterface;
15+
use Magento\TestFramework\Helper\Bootstrap;
16+
use Magento\TestFramework\TestCase\AbstractBackendController;
17+
18+
/**
19+
* Provide tests for product admin controllers.
20+
* @magentoAppArea adminhtml
21+
*/
22+
class ProductTest extends AbstractBackendController
23+
{
24+
/**
25+
* Test bundle product duplicate won't remove bundle options from original product.
26+
*
27+
* @magentoDataFixture Magento/Catalog/_files/products_new.php
28+
* @return void
29+
*/
30+
public function testDuplicateProduct()
31+
{
32+
$params = $this->getRequestParamsForDuplicate();
33+
$this->getRequest()->setParams(['type' => Type::TYPE_BUNDLE]);
34+
$this->getRequest()->setPostValue($params);
35+
$this->dispatch('backend/catalog/product/save');
36+
$this->assertSessionMessages(
37+
$this->equalTo(
38+
[
39+
'You saved the product.',
40+
'You duplicated the product.',
41+
]
42+
),
43+
MessageInterface::TYPE_SUCCESS
44+
);
45+
$this->assertOptions();
46+
}
47+
48+
/**
49+
* Get necessary request post params for creating and duplicating bundle product.
50+
*
51+
* @return array
52+
*/
53+
private function getRequestParamsForDuplicate()
54+
{
55+
$product = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class)->get('simple');
56+
return [
57+
'product' =>
58+
[
59+
'attribute_set_id' => '4',
60+
'gift_message_available' => '0',
61+
'use_config_gift_message_available' => '1',
62+
'stock_data' =>
63+
[
64+
'min_qty_allowed_in_shopping_cart' =>
65+
[
66+
[
67+
'record_id' => '0',
68+
'customer_group_id' => '32000',
69+
'min_sale_qty' => '',
70+
],
71+
],
72+
'min_qty' => '0',
73+
'max_sale_qty' => '10000',
74+
'notify_stock_qty' => '1',
75+
'min_sale_qty' => '1',
76+
'qty_increments' => '1',
77+
'use_config_manage_stock' => '1',
78+
'manage_stock' => '1',
79+
'use_config_min_qty' => '1',
80+
'use_config_max_sale_qty' => '1',
81+
'use_config_backorders' => '1',
82+
'backorders' => '0',
83+
'use_config_notify_stock_qty' => '1',
84+
'use_config_enable_qty_inc' => '1',
85+
'enable_qty_increments' => '0',
86+
'use_config_qty_increments' => '1',
87+
'use_config_min_sale_qty' => '1',
88+
'is_qty_decimal' => '0',
89+
'is_decimal_divided' => '0',
90+
],
91+
'status' => '1',
92+
'affect_product_custom_options' => '1',
93+
'name' => 'b1',
94+
'price' => '',
95+
'weight' => '',
96+
'url_key' => '',
97+
'special_price' => '',
98+
'quantity_and_stock_status' =>
99+
[
100+
'qty' => '',
101+
'is_in_stock' => '1',
102+
],
103+
'sku_type' => '0',
104+
'price_type' => '0',
105+
'weight_type' => '0',
106+
'website_ids' =>
107+
[
108+
1 => '1',
109+
],
110+
'sku' => 'b1',
111+
'meta_title' => 'b1',
112+
'meta_keyword' => 'b1',
113+
'meta_description' => 'b1 ',
114+
'tax_class_id' => '2',
115+
'product_has_weight' => '1',
116+
'visibility' => '4',
117+
'country_of_manufacture' => '',
118+
'page_layout' => '',
119+
'options_container' => 'container2',
120+
'custom_design' => '',
121+
'custom_layout' => '',
122+
'price_view' => '0',
123+
'shipment_type' => '0',
124+
'news_from_date' => '',
125+
'news_to_date' => '',
126+
'custom_design_from' => '',
127+
'custom_design_to' => '',
128+
'special_from_date' => '',
129+
'special_to_date' => '',
130+
'description' => '',
131+
'short_description' => '',
132+
'custom_layout_update' => '',
133+
'image' => '',
134+
'small_image' => '',
135+
'thumbnail' => '',
136+
],
137+
'bundle_options' =>
138+
[
139+
'bundle_options' =>
140+
[
141+
[
142+
'record_id' => '0',
143+
'type' => 'select',
144+
'required' => '1',
145+
'title' => 'test option title',
146+
'position' => '1',
147+
'option_id' => '',
148+
'delete' => '',
149+
'bundle_selections' =>
150+
[
151+
[
152+
'product_id' => $product->getId(),
153+
'name' => $product->getName(),
154+
'sku' => $product->getSku(),
155+
'price' => $product->getPrice(),
156+
'delete' => '',
157+
'selection_can_change_qty' => '',
158+
'selection_id' => '',
159+
'selection_price_type' => '0',
160+
'selection_price_value' => '',
161+
'selection_qty' => '1',
162+
'position' => '1',
163+
'option_id' => '',
164+
'record_id' => '1',
165+
'is_default' => '0',
166+
],
167+
],
168+
'bundle_button_proxy' =>
169+
[
170+
[
171+
'entity_id' => '1',
172+
],
173+
],
174+
],
175+
],
176+
],
177+
'affect_bundle_product_selections' => '1',
178+
'back' => 'duplicate',
179+
'form_key' => Bootstrap::getObjectManager()->get(FormKey::class)->getFormKey(),
180+
];
181+
}
182+
183+
/**
184+
* Check options in created and duplicated products.
185+
*
186+
* @return void
187+
*/
188+
private function assertOptions()
189+
{
190+
$createdOptions = $this->getProductOptions('b1');
191+
$createdOption = array_shift($createdOptions);
192+
$duplicatedOptions = $this->getProductOptions('b1-1');
193+
$duplicatedOption = array_shift($duplicatedOptions);
194+
$this->assertNotEmpty($createdOption);
195+
$this->assertNotEmpty($duplicatedOption);
196+
$optionFields = ['type', 'title', 'position', 'required', 'default_title'];
197+
foreach ($optionFields as $field) {
198+
$this->assertSame($createdOption->getData($field), $duplicatedOption->getData($field));
199+
}
200+
$createdLinks = $createdOption->getProductLinks();
201+
$createdLink = array_shift($createdLinks);
202+
$duplicatedLinks = $duplicatedOption->getProductLinks();
203+
$duplicatedLink = array_shift($duplicatedLinks);
204+
$this->assertNotEmpty($createdLink);
205+
$this->assertNotEmpty($duplicatedLink);
206+
$linkFields = [
207+
'entity_id',
208+
'sku',
209+
'position',
210+
'is_default',
211+
'price',
212+
'qty',
213+
'selection_can_change_quantity',
214+
'price_type',
215+
];
216+
foreach ($linkFields as $field) {
217+
$this->assertSame($createdLink->getData($field), $duplicatedLink->getData($field));
218+
}
219+
}
220+
221+
/**
222+
* Get options for given product.
223+
*
224+
* @param string $sku
225+
* @return OptionInterface[]
226+
*/
227+
private function getProductOptions(string $sku)
228+
{
229+
$product = Bootstrap::getObjectManager()->create(Product::class);
230+
$productId = $product->getResource()->getIdBySku($sku);
231+
$product->load($productId);
232+
233+
return $product->getExtensionAttributes()->getBundleProductOptions();
234+
}
235+
}

0 commit comments

Comments
 (0)