Skip to content

Commit aa22b36

Browse files
ENGCOM-5953: Forward port fix to keep bundle options in bundle after duplicating it #24703
- Merge Pull Request #24703 from hostep/magento2:forward-port-ce-pr-1217 - Merged commits: 1. f1a187e 2. bf36def 3. 629fcdf
2 parents 267db35 + 629fcdf commit aa22b36

File tree

3 files changed

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

3 files changed

+284
-7
lines changed

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
use Magento\Catalog\Model\Product;
99
use Magento\Catalog\Model\Product\Type;
1010

11+
/**
12+
* Provides duplicating bundle options and selections
13+
*/
1114
class Bundle implements \Magento\Catalog\Model\Product\CopyConstructorInterface
1215
{
1316
/**
@@ -27,7 +30,17 @@ public function build(Product $product, Product $duplicate)
2730
$bundleOptions = $product->getExtensionAttributes()->getBundleProductOptions() ?: [];
2831
$duplicatedBundleOptions = [];
2932
foreach ($bundleOptions as $key => $bundleOption) {
30-
$duplicatedBundleOptions[$key] = clone $bundleOption;
33+
$duplicatedBundleOption = clone $bundleOption;
34+
/**
35+
* Set option and selection ids to 'null' in order to create new option(selection) for duplicated product,
36+
* but not modifying existing one, which led to lost of option(selection) in original product.
37+
*/
38+
$productLinks = $duplicatedBundleOption->getProductLinks() ?: [];
39+
foreach ($productLinks as $productLink) {
40+
$productLink->setSelectionId(null);
41+
}
42+
$duplicatedBundleOption->setOptionId(null);
43+
$duplicatedBundleOptions[$key] = $duplicatedBundleOption;
3144
}
3245
$duplicate->getExtensionAttributes()->setBundleProductOptions($duplicatedBundleOptions);
3346
}

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

0 commit comments

Comments
 (0)