Skip to content

Commit e52de40

Browse files
committed
ACP2E-4243: Issue with Update bundle option price per website via Import
1 parent e885088 commit e52de40

File tree

8 files changed

+830
-209
lines changed

8 files changed

+830
-209
lines changed

app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php

Lines changed: 173 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,40 @@
55
*/
66
namespace Magento\BundleImportExport\Model\Export;
77

8+
use Magento\Catalog\Helper\Data as CatalogData;
89
use Magento\Catalog\Model\ResourceModel\Product\Collection;
910
use Magento\CatalogImportExport\Model\Export\RowCustomizerInterface;
1011
use Magento\CatalogImportExport\Model\Import\Product as ImportProductModel;
1112
use Magento\Bundle\Model\ResourceModel\Selection\Collection as SelectionCollection;
13+
use Magento\Framework\App\ObjectManager;
1214
use Magento\ImportExport\Model\Import as ImportModel;
1315
use Magento\Catalog\Model\Product\Type\AbstractType;
1416
use Magento\Store\Model\StoreManagerInterface;
1517

1618
/**
17-
* Class RowCustomizer
19+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
1820
*/
1921
class RowCustomizer implements RowCustomizerInterface
2022
{
21-
const BUNDLE_PRICE_TYPE_COL = 'bundle_price_type';
23+
public const BUNDLE_PRICE_TYPE_COL = 'bundle_price_type';
2224

23-
const BUNDLE_SKU_TYPE_COL = 'bundle_sku_type';
25+
public const BUNDLE_SKU_TYPE_COL = 'bundle_sku_type';
2426

25-
const BUNDLE_PRICE_VIEW_COL = 'bundle_price_view';
27+
public const BUNDLE_PRICE_VIEW_COL = 'bundle_price_view';
2628

27-
const BUNDLE_WEIGHT_TYPE_COL = 'bundle_weight_type';
29+
public const BUNDLE_WEIGHT_TYPE_COL = 'bundle_weight_type';
2830

29-
const BUNDLE_VALUES_COL = 'bundle_values';
31+
public const BUNDLE_VALUES_COL = 'bundle_values';
3032

31-
const VALUE_FIXED = 'fixed';
33+
public const VALUE_FIXED = 'fixed';
3234

33-
const VALUE_DYNAMIC = 'dynamic';
35+
public const VALUE_DYNAMIC = 'dynamic';
3436

35-
const VALUE_PERCENT = 'percent';
37+
public const VALUE_PERCENT = 'percent';
3638

37-
const VALUE_PRICE_RANGE = 'Price range';
39+
public const VALUE_PRICE_RANGE = 'Price range';
3840

39-
const VALUE_AS_LOW_AS = 'As low as';
41+
public const VALUE_AS_LOW_AS = 'As low as';
4042

4143
/**
4244
* Mapping for bundle types
@@ -125,16 +127,26 @@ class RowCustomizer implements RowCustomizerInterface
125127
*/
126128
private $storeManager;
127129

130+
/**
131+
* @var CatalogData
132+
*/
133+
private $catalogData;
134+
128135
/**
129136
* @param StoreManagerInterface $storeManager
137+
* @param CatalogData|null $catalogData
130138
*/
131-
public function __construct(StoreManagerInterface $storeManager)
132-
{
139+
public function __construct(
140+
StoreManagerInterface $storeManager,
141+
?CatalogData $catalogData = null
142+
) {
133143
$this->storeManager = $storeManager;
144+
$this->catalogData = $catalogData ?? ObjectManager::getInstance()->get(CatalogData::class);
134145
}
135146

136147
/**
137148
* Retrieve list of bundle specific columns
149+
*
138150
* @return array
139151
*/
140152
private function getBundleColumns()
@@ -223,6 +235,8 @@ protected function populateBundleData($collection)
223235
$this->bundleData[$id][self::BUNDLE_PRICE_VIEW_COL] = $this->getPriceViewValue($product->getPriceView());
224236
$this->bundleData[$id][self::BUNDLE_WEIGHT_TYPE_COL] = $this->getTypeValue($product->getWeightType());
225237
$this->bundleData[$id][self::BUNDLE_VALUES_COL] = $this->getFormattedBundleOptionValues($product);
238+
// cleanup memory
239+
unset($this->optionCollections[$product->getSku()]);
226240
}
227241
return $this;
228242
}
@@ -238,13 +252,18 @@ protected function getFormattedBundleOptionValues(\Magento\Catalog\Model\Product
238252
$optionCollections = $this->getProductOptionCollection($product);
239253
$bundleData = '';
240254
$optionTitles = $this->getBundleOptionTitles($product);
255+
$optionsRawSelections = $this->getBundleOptionSelections($product);
241256
foreach ($optionCollections->getItems() as $option) {
242257
$optionValues = $this->getFormattedOptionValues($option, $optionTitles);
243-
$bundleData .= $this->getFormattedBundleSelections(
244-
$optionValues,
245-
$product->getTypeInstance()
246-
->getSelectionsCollection([$option->getId()], $product)
247-
->setOrder('position', Collection::SORT_ORDER_ASC)
258+
$bundleData .= implode(
259+
'',
260+
array_map(
261+
fn ($selectionData) => $optionValues
262+
. ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
263+
. $this->serialize($selectionData)
264+
. ImportProductModel::PSEUDO_MULTI_LINE_SEPARATOR,
265+
$optionsRawSelections[$option->getOptionId()] ?? []
266+
)
248267
);
249268
}
250269

@@ -257,6 +276,7 @@ protected function getFormattedBundleOptionValues(\Magento\Catalog\Model\Product
257276
* @param string $optionValues
258277
* @param SelectionCollection $selections
259278
* @return string
279+
* @deprecared Not used anymore
260280
*/
261281
protected function getFormattedBundleSelections($optionValues, SelectionCollection $selections)
262282
{
@@ -273,16 +293,7 @@ protected function getFormattedBundleSelections($optionValues, SelectionCollecti
273293
];
274294
$bundleData .= $optionValues
275295
. ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
276-
. implode(
277-
ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR,
278-
array_map(
279-
function ($value, $key) {
280-
return $key . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR . $value;
281-
},
282-
$selectionData,
283-
array_keys($selectionData)
284-
)
285-
)
296+
. $this->serialize($selectionData)
286297
. ImportProductModel::PSEUDO_MULTI_LINE_SEPARATOR;
287298
}
288299

@@ -300,18 +311,38 @@ protected function getFormattedOptionValues(
300311
\Magento\Bundle\Model\Option $option,
301312
array $optionTitles = []
302313
): string {
303-
$names = implode(ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, array_map(
304-
function ($title, $storeName) {
305-
return $storeName . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR . $title;
306-
},
307-
$optionTitles[$option->getOptionId()],
308-
array_keys($optionTitles[$option->getOptionId()])
309-
));
310-
return $names . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
311-
. 'type' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
312-
. $option->getType() . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
313-
. 'required' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
314-
. $option->getRequired();
314+
$data = [
315+
...[
316+
'name' => $option->getTitle()
317+
],
318+
...($optionTitles[$option->getOptionId()] ?? []),
319+
...[
320+
'type' => $option->getType(),
321+
'required' => $option->getRequired()
322+
]
323+
];
324+
325+
return $this->serialize($data);
326+
}
327+
328+
/**
329+
* Format associative array to serialized string as name1=value1,name2=value2 format
330+
*
331+
* @param array $data
332+
* @return string
333+
*/
334+
private function serialize(array $data): string
335+
{
336+
return implode(
337+
ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR,
338+
array_map(
339+
function ($value, $key) {
340+
return $key . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR . $value;
341+
},
342+
$data,
343+
array_keys($data)
344+
)
345+
);
315346
}
316347

317348
/**
@@ -456,6 +487,72 @@ private function getBundleOptionTitles(\Magento\Catalog\Model\Product $product):
456487
return $optionsTitles;
457488
}
458489

490+
/**
491+
* Get bundle product options selections data
492+
*
493+
* The selection price data for the global scope is stored under the 'price' and 'price_type' keys,
494+
* while for a specific website it is stored under
495+
* public the 'price_website_<website-code>' and 'price_type_website_<website-code>' keys.
496+
*
497+
* @param \Magento\Catalog\Model\Product $product
498+
* @return array
499+
*/
500+
private function getBundleOptionSelections(\Magento\Catalog\Model\Product $product): array
501+
{
502+
$selections = $this->getBundleOptionSelectionsData($product);
503+
504+
if (!$this->catalogData->isPriceGlobal()) {
505+
foreach ($product->getWebsiteIds() as $websiteId) {
506+
$websiteCode = $this->getWebsiteCodeById((int) $websiteId);
507+
$storeId = $this->getWebsiteDefaultStoreId((int) $websiteId);
508+
foreach ($this->getProductOptionCollection($product, $storeId) as $option) {
509+
foreach ($option->getSelections() as $selection) {
510+
$selectionData = $selections[$option->getOptionId()][$selection->getSelectionId()] ?? [];
511+
if ($selectionData && $selection->getPriceScope() == $websiteId) {
512+
$selections[$option->getOptionId()][$selection->getSelectionId()] = [
513+
...$selectionData,
514+
'price_website_' . $websiteCode => $selection->getSelectionPriceValue(),
515+
'price_type_website_' . $websiteCode =>
516+
$this->getPriceTypeValue($selection->getSelectionPriceType())
517+
];
518+
}
519+
}
520+
}
521+
}
522+
}
523+
524+
return $selections;
525+
}
526+
527+
/**
528+
* Get bundle product options selections data.
529+
*
530+
* @param \Magento\Catalog\Model\Product $product
531+
* @param int $storeId
532+
* @return array
533+
*/
534+
private function getBundleOptionSelectionsData(
535+
\Magento\Catalog\Model\Product $product,
536+
int $storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID
537+
): array {
538+
$data = [];
539+
foreach ($this->getProductOptionCollection($product, $storeId) as $option) {
540+
/** @var \Magento\Bundle\Model\Option $option*/
541+
foreach ($option->getSelections() as $selection) {
542+
/** @var \Magento\Bundle\Model\Selection $selection*/
543+
$data[$option->getOptionId()][$selection->getSelectionId()] = [
544+
'sku' => $selection->getSku(),
545+
'price' => $selection->getSelectionPriceValue(),
546+
'default' => $selection->getIsDefault(),
547+
'default_qty' => $selection->getSelectionQty(),
548+
'price_type' => $this->getPriceTypeValue($selection->getSelectionPriceType()),
549+
'can_change_qty' => $selection->getSelectionCanChangeQty(),
550+
];
551+
}
552+
}
553+
return $data;
554+
}
555+
459556
/**
460557
* Get product options collection by provided product model.
461558
*
@@ -473,13 +570,48 @@ private function getProductOptionCollection(
473570
if (!isset($this->optionCollections[$productSku][$storeId])) {
474571
$product->unsetData($this->optionCollectionCacheKey);
475572
$product->setStoreId($storeId);
476-
$this->optionCollections[$productSku][$storeId] = $product->getTypeInstance()
573+
$optionCollection = $product->getTypeInstance()
477574
->getOptionsCollection($product)
478575
->setOrder('position', Collection::SORT_ORDER_ASC);
576+
$selectionCollection = $product->getTypeInstance()
577+
->getSelectionsCollection(
578+
$product->getTypeInstance()->getOptionsIds($product),
579+
$product
580+
)
581+
->setOrder('position', Collection::SORT_ORDER_ASC)
582+
->addAttributeToSort('position', Collection::SORT_ORDER_ASC);
583+
$optionCollection->appendSelections($selectionCollection, true);
584+
$this->optionCollections[$productSku][$storeId] = $optionCollection;
479585
}
480586
return $this->optionCollections[$productSku][$storeId];
481587
}
482588

589+
/**
590+
* Retrieve default store id for website
591+
*
592+
* @param int $websiteId
593+
* @return int
594+
* @throws \Magento\Framework\Exception\LocalizedException
595+
*/
596+
private function getWebsiteDefaultStoreId(int $websiteId): int
597+
{
598+
return (int) $this->storeManager
599+
->getGroup($this->storeManager->getWebsite($websiteId)->getDefaultGroupId())
600+
->getDefaultStoreId();
601+
}
602+
603+
/**
604+
* Retrieve website code by its ID.
605+
*
606+
* @param int $websiteId
607+
* @return string
608+
* @throws \Magento\Framework\Exception\LocalizedException
609+
*/
610+
private function getWebsiteCodeById(int $websiteId): string
611+
{
612+
return $this->storeManager->getWebsite($websiteId)->getCode();
613+
}
614+
483615
/**
484616
* Retrieve store code by it's ID.
485617
*

0 commit comments

Comments
 (0)