Skip to content

Commit 0942a9a

Browse files
committed
MAGETWO-47698: [Github] Custom options not displayed correctly on a store view level #2908 #5885
2 parents 7216b82 + ad5f6e0 commit 0942a9a

File tree

18 files changed

+353
-107
lines changed

18 files changed

+353
-107
lines changed

app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,16 @@ public function initializeFromData(\Magento\Catalog\Model\Product $product, arra
199199
$customOptions = [];
200200
foreach ($options as $customOptionData) {
201201
if (empty($customOptionData['is_delete'])) {
202+
if (empty($customOptionData['option_id'])) {
203+
$customOptionData['option_id'] = null;
204+
}
202205
if (isset($customOptionData['values'])) {
203206
$customOptionData['values'] = array_filter($customOptionData['values'], function ($valueData) {
204207
return empty($valueData['is_delete']);
205208
});
206209
}
207210
$customOption = $this->getCustomOptionFactory()->create(['data' => $customOptionData]);
208211
$customOption->setProductSku($product->getSku());
209-
$customOption->setOptionId(null);
210212
$customOptions[] = $customOption;
211213
}
212214
}
@@ -255,7 +257,7 @@ protected function setProductLinks(\Magento\Catalog\Model\Product $product)
255257

256258
foreach ($linkTypes as $linkType => $readonly) {
257259
if (isset($links[$linkType]) && !$readonly) {
258-
foreach ((array) $links[$linkType] as $linkData) {
260+
foreach ((array)$links[$linkType] as $linkData) {
259261
if (empty($linkData['id'])) {
260262
continue;
261263
}
@@ -321,9 +323,11 @@ public function mergeProductOptions($productOptions, $overwriteOptions)
321323

322324
if (isset($option['values']) && isset($overwriteOptions[$optionId]['values'])) {
323325
foreach ($option['values'] as $valueIndex => $value) {
324-
$valueId = $value['option_type_id'];
325-
$value = $this->overwriteValue($valueId, $value, $overwriteOptions[$optionId]['values']);
326-
$option['values'][$valueIndex] = $value;
326+
if (isset($value['option_type_id'])) {
327+
$valueId = $value['option_type_id'];
328+
$value = $this->overwriteValue($valueId, $value, $overwriteOptions[$optionId]['values']);
329+
$option['values'][$valueIndex] = $value;
330+
}
327331
}
328332
}
329333

@@ -347,6 +351,9 @@ private function overwriteValue($optionId, $option, $overwriteOptions)
347351
foreach ($overwriteOptions[$optionId] as $fieldName => $overwrite) {
348352
if ($overwrite && isset($option[$fieldName]) && isset($option['default_' . $fieldName])) {
349353
$option[$fieldName] = $option['default_' . $fieldName];
354+
if ('title' == $fieldName) {
355+
$option['is_delete_store_title'] = 1;
356+
}
350357
}
351358
}
352359
}

app/code/Magento/Catalog/Model/Product/Option/Repository.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,33 @@ public function save(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $opt
137137
if (!$productSku) {
138138
throw new CouldNotSaveException(__('ProductSku should be specified'));
139139
}
140+
/** @var \Magento\Catalog\Model\Product $product */
140141
$product = $this->productRepository->get($productSku);
141142
$metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class);
142143
$option->setData('product_id', $product->getData($metadata->getLinkField()));
143-
$option->setOptionId(null);
144+
$option->setData('store_id', $product->getStoreId());
145+
146+
if ($option->getOptionId()) {
147+
$options = $product->getOptions();
148+
if (!$options) {
149+
$options = $this->getProductOptions($product);
150+
}
151+
152+
$persistedOption = array_filter($options, function ($iOption) use ($option) {
153+
return $option->getOptionId() == $iOption->getOptionId();
154+
});
155+
$persistedOption = reset($persistedOption);
156+
157+
if (!$persistedOption) {
158+
throw new NoSuchEntityException();
159+
}
160+
$originalValues = $persistedOption->getValues();
161+
$newValues = $option->getData('values');
162+
if ($newValues) {
163+
$newValues = $this->markRemovedValues($newValues, $originalValues);
164+
$option->setData('values', $newValues);
165+
}
166+
}
144167
$option->save();
145168
return $option;
146169
}

app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ class SaveHandler implements ExtensionInterface
2020

2121
/**
2222
* @param OptionRepository $optionRepository
23-
* @param MetadataPool $metadataPool
2423
*/
2524
public function __construct(
2625
OptionRepository $optionRepository
@@ -36,15 +35,28 @@ public function __construct(
3635
*/
3736
public function execute($entity, $arguments = [])
3837
{
38+
$options = $entity->getOptions();
39+
$optionIds = [];
40+
41+
if ($options) {
42+
$optionIds = array_map(function ($option) {
43+
/** @var \Magento\Catalog\Model\Product\Option $option */
44+
return $option->getOptionId();
45+
}, $options);
46+
}
47+
3948
/** @var \Magento\Catalog\Api\Data\ProductInterface $entity */
4049
foreach ($this->optionRepository->getProductOptions($entity) as $option) {
41-
$this->optionRepository->delete($option);
50+
if (!in_array($option->getOptionId(), $optionIds)) {
51+
$this->optionRepository->delete($option);
52+
}
4253
}
43-
if ($entity->getOptions()) {
44-
foreach ($entity->getOptions() as $option) {
54+
if ($options) {
55+
foreach ($options as $option) {
4556
$this->optionRepository->save($option);
4657
}
4758
}
59+
4860
return $entity;
4961
}
5062
}

app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ protected function validateOptionValue(Option $option)
5151
$storeId = $option->getProduct()->getStoreId();
5252
}
5353
foreach ($values as $value) {
54+
if (isset($value['is_delete']) && (bool)$value['is_delete']) {
55+
continue;
56+
}
5457
$type = isset($value['price_type']) ? $value['price_type'] : null;
5558
$price = isset($value['price']) ? $value['price'] : null;
5659
$title = isset($value['title']) ? $value['title'] : null;

app/code/Magento/Catalog/Model/Product/Option/Value.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public function saveValues()
200200
'store_id',
201201
$this->getOption()->getStoreId()
202202
);
203-
$this->unsetData('option_type_id');
203+
204204
if ($this->getData('is_delete') == '1') {
205205
if ($this->getId()) {
206206
$this->deleteValues($this->getId());

app/code/Magento/Catalog/Model/ResourceModel/Product/Option.php

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -247,14 +247,21 @@ protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $obje
247247
$titleTableName = $this->getTable('catalog_product_option_title');
248248
foreach ([\Magento\Store\Model\Store::DEFAULT_STORE_ID, $object->getStoreId()] as $storeId) {
249249
$existInCurrentStore = $this->getColFromOptionTable($titleTableName, (int)$object->getId(), (int)$storeId);
250-
$existInDefaultStore = $this->getColFromOptionTable(
251-
$titleTableName,
252-
(int)$object->getId(),
253-
\Magento\Store\Model\Store::DEFAULT_STORE_ID
254-
);
250+
$existInDefaultStore = (int)$storeId == \Magento\Store\Model\Store::DEFAULT_STORE_ID ?
251+
$existInCurrentStore :
252+
$this->getColFromOptionTable(
253+
$titleTableName,
254+
(int)$object->getId(),
255+
\Magento\Store\Model\Store::DEFAULT_STORE_ID
256+
);
257+
255258
if ($object->getTitle()) {
259+
$isDeleteStoreTitle = (bool)$object->getData('is_delete_store_title');
256260
if ($existInCurrentStore) {
257-
if ($object->getStoreId() == $storeId) {
261+
if ($isDeleteStoreTitle && (int)$storeId != \Magento\Store\Model\Store::DEFAULT_STORE_ID) {
262+
$connection->delete($titleTableName, ['option_title_id = ?' => $existInCurrentStore]);
263+
264+
} elseif ($object->getStoreId() == $storeId) {
258265
$data = $this->_prepareDataForTable(
259266
new \Magento\Framework\DataObject(['title' => $object->getTitle()]),
260267
$titleTableName
@@ -270,8 +277,13 @@ protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $obje
270277
}
271278
} else {
272279
// we should insert record into not default store only of if it does not exist in default store
273-
if (($storeId == \Magento\Store\Model\Store::DEFAULT_STORE_ID && !$existInDefaultStore)
274-
|| ($storeId != \Magento\Store\Model\Store::DEFAULT_STORE_ID && !$existInCurrentStore)
280+
if (
281+
($storeId == \Magento\Store\Model\Store::DEFAULT_STORE_ID && !$existInDefaultStore) ||
282+
(
283+
$storeId != \Magento\Store\Model\Store::DEFAULT_STORE_ID &&
284+
!$existInCurrentStore &&
285+
!$isDeleteStoreTitle
286+
)
275287
) {
276288
$data = $this->_prepareDataForTable(
277289
new \Magento\Framework\DataObject(

app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,11 @@ protected function _saveValueTitles(\Magento\Framework\Model\AbstractModel $obje
231231
);
232232
$optionTypeId = $this->getConnection()->fetchOne($select);
233233
$existInCurrentStore = $this->getOptionIdFromOptionTable($titleTable, (int)$object->getId(), (int)$storeId);
234+
235+
if ($storeId != \Magento\Store\Model\Store::DEFAULT_STORE_ID && $object->getData('is_delete_store_title')) {
236+
$object->unsetData('title');
237+
}
238+
234239
if ($object->getTitle()) {
235240
if ($existInCurrentStore) {
236241
if ($storeId == $object->getStoreId()) {

app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@
66
namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Initialization;
77

88
use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory;
9-
use Magento\Catalog\Api\ProductRepositoryInterface\Proxy as ProductRepository;
109
use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper;
1110
use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter;
1211
use Magento\Catalog\Model\Product;
12+
use Magento\Catalog\Model\Product\Option;
13+
use Magento\Catalog\Model\ProductRepository;
1314
use Magento\Framework\App\RequestInterface;
1415
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
1516
use Magento\Store\Api\Data\StoreInterface;
1617
use Magento\Store\Api\Data\WebsiteInterface;
1718
use Magento\Store\Model\StoreManagerInterface;
1819
use Magento\Framework\Stdlib\DateTime\Filter\Date as DateFilter;
1920
use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory;
20-
use Magento\Catalog\Api\Data\ProductCustomOptionInterface;
2121
use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks;
2222

2323
/**
@@ -95,7 +95,7 @@ class HelperTest extends \PHPUnit_Framework_TestCase
9595
protected $customOptionFactoryMock;
9696

9797
/**
98-
* @var ProductCustomOptionInterface|\PHPUnit_Framework_MockObject_MockObject
98+
* @var Option|\PHPUnit_Framework_MockObject_MockObject
9999
*/
100100
protected $customOptionMock;
101101

@@ -136,16 +136,13 @@ protected function setUp()
136136
->getMock();
137137
$this->productMock = $this->getMockBuilder(Product::class)
138138
->setMethods([
139-
'setData',
140-
'addData',
141139
'getId',
142140
'setWebsiteIds',
143141
'isLockedAttribute',
144142
'lockAttribute',
145143
'getAttributes',
146144
'unlockAttribute',
147145
'getOptionsReadOnly',
148-
'setOptions',
149146
'setCanSaveCustomOptions',
150147
'__sleep',
151148
'__wakeup',
@@ -159,9 +156,10 @@ protected function setUp()
159156
->disableOriginalConstructor()
160157
->setMethods(['create'])
161158
->getMock();
162-
$this->customOptionMock = $this->getMockBuilder(ProductCustomOptionInterface::class)
159+
$this->customOptionMock = $this->getMockBuilder(Option::class)
163160
->disableOriginalConstructor()
164-
->getMockForAbstractClass();
161+
->setMethods(null)
162+
->getMock();
165163
$this->productLinksMock = $this->getMockBuilder(ProductLinks::class)
166164
->disableOriginalConstructor()
167165
->getMock();
@@ -196,14 +194,10 @@ protected function setUp()
196194
*/
197195
public function testInitialize()
198196
{
199-
$this->customOptionMock->expects($this->once())
200-
->method('setProductSku');
201-
$this->customOptionMock->expects($this->once())
202-
->method('setOptionId');
203-
204197
$optionsData = [
205-
'option1' => ['is_delete' => true, 'name' => 'name1', 'price' => 'price1'],
206-
'option2' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1'],
198+
'option1' => ['is_delete' => true, 'name' => 'name1', 'price' => 'price1', 'option_id' => ''],
199+
'option2' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1', 'option_id' => '13'],
200+
'option3' => ['is_delete' => false, 'name' => 'name1', 'price' => 'price1', 'option_id' => '14']
207201
];
208202
$productData = [
209203
'stock_data' => ['stock_data'],
@@ -277,14 +271,7 @@ public function testInitialize()
277271
->method('getAttributes')
278272
->willReturn($attributesArray);
279273

280-
$productData['category_ids'] = [];
281-
$productData['website_ids'] = [];
282-
unset($productData['options']);
283-
284-
$this->productMock->expects($this->once())
285-
->method('addData')
286-
->with($productData);
287-
$this->productMock->expects($this->once())
274+
$this->productMock->expects($this->any())
288275
->method('getSku')
289276
->willReturn('sku');
290277
$this->productMock->expects($this->any())
@@ -293,13 +280,25 @@ public function testInitialize()
293280

294281
$this->customOptionFactoryMock->expects($this->any())
295282
->method('create')
296-
->with(['data' => $optionsData['option2']])
297-
->willReturn($this->customOptionMock);
298-
$this->productMock->expects($this->once())
299-
->method('setOptions')
300-
->with([$this->customOptionMock]);
283+
->willReturnMap([
284+
[
285+
['data' => $optionsData['option2']],
286+
(clone $this->customOptionMock)->setData($optionsData['option2'])
287+
], [
288+
['data' => $optionsData['option3']],
289+
(clone $this->customOptionMock)->setData($optionsData['option3'])
290+
]
291+
]);
301292

302293
$this->assertEquals($this->productMock, $this->helper->initialize($this->productMock));
294+
295+
$productOptions = $this->productMock->getOptions();
296+
$this->assertTrue(2 == count($productOptions));
297+
list($option2, $option3) = $productOptions;
298+
$this->assertTrue($option2->getOptionId() == $optionsData['option2']['option_id']);
299+
$this->assertTrue('sku' == $option2->getData('product_sku'));
300+
$this->assertTrue($option3->getOptionId() == $optionsData['option3']['option_id']);
301+
$this->assertTrue('sku' == $option2->getData('product_sku'));
303302
}
304303

305304
/**
@@ -362,9 +361,9 @@ public function mergeProductOptionsDataProvider()
362361
[
363362
'option_id' => '5',
364363
'key1' => 'val1',
365-
'key2' => 'val2',
364+
'title' => 'val2',
366365
'default_key1' => 'val3',
367-
'default_key2' => 'val4',
366+
'default_title' => 'val4',
368367
'values' => [
369368
[
370369
'option_type_id' => '2',
@@ -379,17 +378,18 @@ public function mergeProductOptionsDataProvider()
379378
[
380379
5 => [
381380
'key1' => '0',
382-
'key2' => '1',
381+
'title' => '1',
383382
'values' => [2 => ['key1' => 1]]
384383
]
385384
],
386385
[
387386
[
388387
'option_id' => '5',
389388
'key1' => 'val1',
390-
'key2' => 'val4',
389+
'title' => 'val4',
391390
'default_key1' => 'val3',
392-
'default_key2' => 'val4',
391+
'default_title' => 'val4',
392+
'is_delete_store_title' => 1,
393393
'values' => [
394394
[
395395
'option_type_id' => '2',
@@ -413,8 +413,9 @@ public function mergeProductOptionsDataProvider()
413413
[
414414
'option_type_id' => '2',
415415
'key1' => 'val1',
416-
'key2' => 'val2',
417-
'default_key1' => 'val11'
416+
'title' => 'val2',
417+
'default_key1' => 'val11',
418+
'default_title' => 'val22'
418419
]
419420
]
420421
]
@@ -423,7 +424,7 @@ public function mergeProductOptionsDataProvider()
423424
7 => [
424425
'key1' => '1',
425426
'key2' => '1',
426-
'values' => [2 => ['key1' => 1, 'key2' => 1]]
427+
'values' => [2 => ['key1' => 0, 'title' => 1]]
427428
]
428429
],
429430
[
@@ -435,9 +436,11 @@ public function mergeProductOptionsDataProvider()
435436
'values' => [
436437
[
437438
'option_type_id' => '2',
438-
'key1' => 'val11',
439-
'key2' => 'val2',
440-
'default_key1' => 'val11'
439+
'key1' => 'val1',
440+
'title' => 'val22',
441+
'default_key1' => 'val11',
442+
'default_title' => 'val22',
443+
'is_delete_store_title' => 1
441444
]
442445
]
443446
]

0 commit comments

Comments
 (0)