diff --git a/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php index 9b8518a41ffffcc348aa40302288930ed94e5588..ca885b21856cf8704222377108b183cc50abe351 100644 --- a/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php +++ b/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php @@ -9,9 +9,10 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\CatalogImportExport\Model\Export\RowCustomizerInterface; use Magento\CatalogImportExport\Model\Import\Product as ImportProductModel; use Magento\Bundle\Model\ResourceModel\Selection\Collection as SelectionCollection; -use Magento\ImportExport\Controller\Adminhtml\Import; use Magento\ImportExport\Model\Import as ImportModel; use \Magento\Catalog\Model\Product\Type\AbstractType; +use \Magento\Framework\App\ObjectManager; +use \Magento\Store\Model\StoreManagerInterface; /** * Class RowCustomizer @@ -105,6 +106,35 @@ class RowCustomizer implements RowCustomizerInterface AbstractType::SHIPMENT_SEPARATELY => 'separately', ]; + /** + * @var \Magento\Bundle\Model\ResourceModel\Option\Collection[] + */ + private $optionCollections = []; + + /** + * @var array + */ + private $storeIdToCode = []; + + /** + * @var string + */ + private $optionCollectionCacheKey = '_cache_instance_options_collection'; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + * @throws \RuntimeException + */ + public function __construct(StoreManagerInterface $storeManager) + { + $this->storeManager = $storeManager; + } + /** * Retrieve list of bundle specific columns * @return array @@ -207,15 +237,13 @@ class RowCustomizer implements RowCustomizerInterface */ protected function getFormattedBundleOptionValues($product) { - /** @var \Magento\Bundle\Model\ResourceModel\Option\Collection $optionsCollection */ - $optionsCollection = $product->getTypeInstance() - ->getOptionsCollection($product) - ->setOrder('position', Collection::SORT_ORDER_ASC); - + $optionCollections = $this->getProductOptionCollections($product); $bundleData = ''; - foreach ($optionsCollection as $option) { + $optionTitles = $this->getBundleOptionTitles($product); + foreach ($optionCollections->getItems() as $option) { + $optionValues = $this->getFormattedOptionValues($option, $optionTitles); $bundleData .= $this->getFormattedBundleSelections( - $this->getFormattedOptionValues($option), + $optionValues, $product->getTypeInstance() ->getSelectionsCollection([$option->getId()], $product) ->setOrder('position', Collection::SORT_ORDER_ASC) @@ -266,16 +294,23 @@ class RowCustomizer implements RowCustomizerInterface * Retrieve option value of bundle product * * @param \Magento\Bundle\Model\Option $option + * @param string[] $optionTitles * @return string */ - protected function getFormattedOptionValues($option) + protected function getFormattedOptionValues($option, $optionTitles = []) { - return 'name' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR - . $option->getTitle() . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR - . 'type' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR - . $option->getType() . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR - . 'required' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR - . $option->getRequired(); + $names = implode(ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, array_map( + function ($title, $storeName) { + return $storeName . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR . $title; + }, + $optionTitles[$option->getOptionId()], + array_keys($optionTitles[$option->getOptionId()]) + )); + return $names . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR + . 'type' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR + . $option->getType() . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR + . 'required' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR + . $option->getRequired(); } /** @@ -380,4 +415,82 @@ class RowCustomizer implements RowCustomizerInterface } return $preparedAttributes; } + + /** + * Get product options titles. + * + * Values for all store views (default) should be specified with 'name' key. + * If user want to specify value or change existing for non default store views it should be specified with + * 'name_' prefix and needed store view suffix. + * + * For example: + * - 'name=All store views name' for all store views + * - 'name_specific_store=Specific store name' for store view with 'specific_store' store code + * + * @param \Magento\Catalog\Model\Product $product $product + * @return array + */ + private function getBundleOptionTitles($product): array + { + $optionCollections = $this->getProductOptionCollections($product); + $optionsTitles = []; + /** @var \Magento\Bundle\Model\Option $option */ + foreach ($optionCollections->getItems() as $option) { + $optionsTitles[$option->getId()]['name'] = $option->getTitle(); + } + $storeIds = $product->getStoreIds(); + if (count($storeIds) > 1) { + foreach ($storeIds as $storeId) { + $optionCollections = $this->getProductOptionCollections($product, $storeId); + /** @var \Magento\Bundle\Model\Option $option */ + foreach ($optionCollections->getItems() as $option) { + $optionTitle = $option->getTitle(); + if ($optionsTitles[$option->getId()]['name'] != $optionTitle) { + $optionsTitles[$option->getId()]['name_' . $this->getStoreCodeById($storeId)] = $optionTitle; + } + } + } + } + return $optionsTitles; + } + + /** + * Get product options collection by provided product model. + * + * Set given store id to the product if it was defined (default store id will be set if was not). + * + * @param \Magento\Catalog\Model\Product $product $product + * @param integer $storeId + * @return \Magento\Bundle\Model\ResourceModel\Option\Collection + */ + private function getProductOptionCollections( + \Magento\Catalog\Model\Product $product, + $storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID + ): \Magento\Bundle\Model\ResourceModel\Option\Collection { + $productSku = $product->getSku(); + if (!isset($this->optionCollections[$productSku][$storeId])) { + $product->unsetData($this->optionCollectionCacheKey); + $product->setStoreId($storeId); + $this->optionCollections[$productSku][$storeId] = $product->getTypeInstance() + ->getOptionsCollection($product) + ->setOrder('position', Collection::SORT_ORDER_ASC); + } + return $this->optionCollections[$productSku][$storeId]; + } + + /** + * Retrieve store code by it's ID. + * + * Collect store id in $storeIdToCode[] private variable if it was not initialized earlier. + * + * @param $storeId + * @return string + */ + private function getStoreCodeById($storeId): string + { + if (!isset($this->storeIdToCode[$storeId])) { + $this->storeIdToCode[$storeId] = $this->storeManager->getStore($storeId)->getCode(); + } + return $this->storeIdToCode[$storeId]; + } } diff --git a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php index 96b7c7b1430b05186974340e22a363fa6549d0e2..6d427a17d694c5913ca15a18998f2a63c1ffe194 100644 --- a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php +++ b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php @@ -11,12 +11,14 @@ namespace Magento\BundleImportExport\Model\Import\Product\Type; use \Magento\Framework\App\ObjectManager; use \Magento\Bundle\Model\Product\Price as BundlePrice; use \Magento\Catalog\Model\Product\Type\AbstractType; -use Magento\CatalogImportExport\Model\Import\Product; +use \Magento\CatalogImportExport\Model\Import\Product; +use \Magento\Store\Model\StoreManagerInterface; /** * Class Bundle * @package Magento\BundleImportExport\Model\Import\Product\Type * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType { @@ -136,6 +138,16 @@ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abst */ private $relationsDataSaver; + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var array + */ + private $storeCodeToId = []; + /** * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFac * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $prodAttrColFac @@ -143,6 +155,9 @@ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abst * @param array $params * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool * @param Bundle\RelationsDataSaver|null $relationsDataSaver + * @param StoreManagerInterface $storeManager + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \RuntimeException */ public function __construct( \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFac, @@ -150,12 +165,14 @@ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abst \Magento\Framework\App\ResourceConnection $resource, array $params, \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, - Bundle\RelationsDataSaver $relationsDataSaver = null + Bundle\RelationsDataSaver $relationsDataSaver = null, + StoreManagerInterface $storeManager = null ) { parent::__construct($attrSetColFac, $prodAttrColFac, $resource, $params, $metadataPool); - $this->relationsDataSaver = $relationsDataSaver ?: ObjectManager::getInstance()->get(Bundle\RelationsDataSaver::class); + $this->storeManager = $storeManager + ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); } /** @@ -261,20 +278,28 @@ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abst * @param array $option * @param int $optionId * @param int $storeId - * - * @return array|bool + * @return array */ protected function populateOptionValueTemplate($option, $optionId, $storeId = 0) { - if (!isset($option['name']) || !isset($option['parent_id']) || !$optionId) { - return false; + $optionValues = []; + if (isset($option['name']) && isset($option['parent_id']) && $optionId) { + $pattern = '/^name[_]?(.*)/'; + $keys = array_keys($option); + $optionNames = preg_grep($pattern, $keys); + foreach ($optionNames as $optionName) { + preg_match($pattern, $optionName, $storeCodes); + $storeCode = array_pop($storeCodes); + $storeId = $storeCode ? $this->getStoreIdByCode($storeCode) : $storeId; + $optionValues[] = [ + 'option_id' => $optionId, + 'parent_product_id' => $option['parent_id'], + 'store_id' => $storeId, + 'title' => $option[$optionName], + ]; + } } - return [ - 'option_id' => $optionId, - 'parent_product_id' => $option['parent_id'], - 'store_id' => $storeId, - 'title' => $option['name'], - ]; + return $optionValues; } /** @@ -284,7 +309,7 @@ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abst * @param int $optionId * @param int $parentId * @param int $index - * @return array + * @return array|bool * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -564,21 +589,24 @@ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abst */ protected function populateInsertOptionValues($optionIds) { - $insertValues = []; + $optionValues = []; foreach ($this->_cachedOptions as $entityId => $options) { foreach ($options as $key => $option) { foreach ($optionIds as $optionId => $assoc) { if ($assoc['position'] == $this->_cachedOptions[$entityId][$key]['index'] && $assoc['parent_id'] == $entityId) { $option['parent_id'] = $entityId; - $insertValues[] = $this->populateOptionValueTemplate($option, $optionId); + $optionValues = array_merge( + $optionValues, + $this->populateOptionValueTemplate($option, $optionId) + ); $this->_cachedOptions[$entityId][$key]['option_id'] = $optionId; break; } } } } - return $insertValues; + return $optionValues; } /** @@ -695,4 +723,21 @@ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abst $this->_cachedSkuToProducts = []; return $this; } + + /** + * Get store id by store code. + * + * @param string $storeCode + * @return int + */ + private function getStoreIdByCode(string $storeCode): int + { + if (!isset($this->storeIdToCode[$storeCode])) { + /** @var $store \Magento\Store\Model\Store */ + foreach ($this->storeManager->getStores() as $store) { + $this->storeCodeToId[$store->getCode()] = $store->getId(); + } + } + return $this->storeCodeToId[$storeCode]; + } } diff --git a/app/code/Magento/BundleImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php b/app/code/Magento/BundleImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php index e76e9e1ba565f4adce29d593ee28a80d56acb239..027d30e0da39d44f9213c77079b202c719f0fe96 100644 --- a/app/code/Magento/BundleImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php +++ b/app/code/Magento/BundleImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php @@ -52,14 +52,24 @@ class RowCustomizerTest extends \PHPUnit\Framework\TestCase */ protected $selection; + /** @var \Magento\Framework\App\ScopeResolverInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $scopeResolver; + /** * Set up */ protected function setUp() { $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->scopeResolver = $this->getMockBuilder(\Magento\Framework\App\ScopeResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getScope']) + ->getMockForAbstractClass(); $this->rowCustomizerMock = $this->objectManagerHelper->getObject( - \Magento\BundleImportExport\Model\Export\RowCustomizer::class + \Magento\BundleImportExport\Model\Export\RowCustomizer::class, + [ + 'scopeResolver' => $this->scopeResolver + ] ); $this->productResourceCollection = $this->createPartialMock( \Magento\Catalog\Model\ResourceModel\Product\Collection::class, @@ -72,6 +82,8 @@ class RowCustomizerTest extends \PHPUnit\Framework\TestCase 'getPriceType', 'getShipmentType', 'getSkuType', + 'getSku', + 'getStoreIds', 'getPriceView', 'getWeightType', 'getTypeInstance', @@ -79,6 +91,7 @@ class RowCustomizerTest extends \PHPUnit\Framework\TestCase 'getSelectionsCollection' ] ); + $this->product->expects($this->any())->method('getStoreIds')->willReturn([1]); $this->product->expects($this->any())->method('getEntityId')->willReturn(1); $this->product->expects($this->any())->method('getPriceType')->willReturn(1); $this->product->expects($this->any())->method('getShipmentType')->willReturn(1); @@ -88,19 +101,20 @@ class RowCustomizerTest extends \PHPUnit\Framework\TestCase $this->product->expects($this->any())->method('getTypeInstance')->willReturnSelf(); $this->optionsCollection = $this->createPartialMock( \Magento\Bundle\Model\ResourceModel\Option\Collection::class, - ['setOrder', 'getIterator'] + ['setOrder', 'getItems'] ); $this->product->expects($this->any())->method('getOptionsCollection')->willReturn($this->optionsCollection); $this->optionsCollection->expects($this->any())->method('setOrder')->willReturnSelf(); $this->option = $this->createPartialMock( \Magento\Bundle\Model\Option::class, - ['getId', 'getTitle', 'getType', 'getRequired'] + ['getId', 'getOptionId', 'getTitle', 'getType', 'getRequired'] ); $this->option->expects($this->any())->method('getId')->willReturn(1); + $this->option->expects($this->any())->method('getOptionId')->willReturn(1); $this->option->expects($this->any())->method('getTitle')->willReturn('title'); $this->option->expects($this->any())->method('getType')->willReturn(1); $this->option->expects($this->any())->method('getRequired')->willReturn(1); - $this->optionsCollection->expects($this->any())->method('getIterator')->will( + $this->optionsCollection->expects($this->any())->method('getItems')->will( $this->returnValue(new \ArrayIterator([$this->option])) ); $this->selection = $this->createPartialMock( @@ -122,6 +136,7 @@ class RowCustomizerTest extends \PHPUnit\Framework\TestCase $this->product->expects($this->any())->method('getSelectionsCollection')->willReturn( $this->selectionsCollection ); + $this->product->expects($this->any())->method('getSku')->willReturn(1); $this->productResourceCollection->expects($this->any())->method('addAttributeToFilter')->willReturnSelf(); $this->productResourceCollection->expects($this->any())->method('getIterator')->will( $this->returnValue(new \ArrayIterator([$this->product])) @@ -133,6 +148,9 @@ class RowCustomizerTest extends \PHPUnit\Framework\TestCase */ public function testPrepareData() { + $scope = $this->getMockBuilder(\Magento\Framework\App\ScopeInterface::class)->getMockForAbstractClass(); + $this->scopeResolver->expects($this->any())->method('getScope') + ->willReturn($scope); $result = $this->rowCustomizerMock->prepareData($this->productResourceCollection, [1]); $this->assertNotNull($result); } @@ -160,6 +178,9 @@ class RowCustomizerTest extends \PHPUnit\Framework\TestCase */ public function testAddData() { + $scope = $this->getMockBuilder(\Magento\Framework\App\ScopeInterface::class)->getMockForAbstractClass(); + $this->scopeResolver->expects($this->any())->method('getScope') + ->willReturn($scope); $preparedData = $this->rowCustomizerMock->prepareData($this->productResourceCollection, [1]); $attributes = 'attribute=1,sku_type=1,attribute2="Text",price_type=1,price_view=1,weight_type=1,' . 'values=values,shipment_type=1,attribute3=One,Two,Three'; diff --git a/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php b/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php index 8e1243b5eb3afc7c3b831d3107f6b84dc41e5ec6..773fa6a5349a506ef7105709b265a756c4fc8adf 100644 --- a/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php +++ b/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php @@ -58,6 +58,9 @@ class BundleTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractIm */ protected $setCollection; + /** @var \Magento\Framework\App\ScopeResolverInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $scopeResolver; + /** * * @return void @@ -170,14 +173,18 @@ class BundleTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractIm 0 => $this->entityModel, 1 => 'bundle' ]; - + $this->scopeResolver = $this->getMockBuilder(\Magento\Framework\App\ScopeResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getScope']) + ->getMockForAbstractClass(); $this->bundle = $this->objectManagerHelper->getObject( \Magento\BundleImportExport\Model\Import\Product\Type\Bundle::class, [ 'attrSetColFac' => $this->attrSetColFac, 'prodAttrColFac' => $this->prodAttrColFac, 'resource' => $this->resource, - 'params' => $this->params + 'params' => $this->params, + 'scopeResolver' => $this->scopeResolver ] ); @@ -214,7 +221,8 @@ class BundleTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractIm $this->entityModel->expects($this->any())->method('isRowAllowedToImport')->will($this->returnValue( $allowImport )); - + $scope = $this->getMockBuilder(\Magento\Framework\App\ScopeInterface::class)->getMockForAbstractClass(); + $this->scopeResolver->expects($this->any())->method('getScope')->willReturn($scope); $this->connection->expects($this->any())->method('fetchAssoc')->with($this->select)->will($this->returnValue([ '1' => [ 'option_id' => '1', diff --git a/app/code/Magento/BundleImportExport/composer.json b/app/code/Magento/BundleImportExport/composer.json index f5ad6143dc50133a4591b62916dee6f159c7aad2..407665b0e942e6ab33161a67d8b5a483b6c524b2 100644 --- a/app/code/Magento/BundleImportExport/composer.json +++ b/app/code/Magento/BundleImportExport/composer.json @@ -7,6 +7,7 @@ "magento/module-import-export": "100.2.*", "magento/module-catalog-import-export": "100.2.*", "magento/module-bundle": "100.2.*", + "magento/module-store": "100.2.*", "magento/module-eav": "101.0.*", "magento/framework": "101.0.*" }, diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 45530ed6d7bae0264f6fdd9a49e7b90ef448baa6..6a14eb8b7a817e393550eb6af82480fa785f9c8e 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -1346,6 +1346,12 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity } /** + * Collect custom options data for products that will be exported. + * + * Option name and type will be collected for all store views, all other data (which can't be changed on store view + * level will be collected for DEFAULT_STORE_ID only. + * Store view specified data will be saved to the additional store view row. + * * @param int[] $productIds * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -1356,13 +1362,12 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity $customOptionsData = []; foreach (array_keys($this->_storeIdToCode) as $storeId) { - if (Store::DEFAULT_STORE_ID != $storeId) { - continue; - } $options = $this->_optionColFactory->create(); /* @var \Magento\Catalog\Model\ResourceModel\Product\Option\Collection $options*/ - $options->addOrder('sort_order'); - $options->reset()->addOrder('sort_order')->addTitleToResult( + $options->reset()->addOrder( + 'sort_order', + \Magento\Catalog\Model\ResourceModel\Product\Option\Collection::SORT_ORDER_ASC + )->addTitleToResult( $storeId )->addPriceToResult( $storeId @@ -1375,34 +1380,36 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity foreach ($options as $option) { $row = []; $productId = $option['product_id']; - $row['name'] = $option['title']; $row['type'] = $option['type']; - $row['required'] = $option['is_require']; - $row['price'] = $option['price']; - $row['price_type'] = ($option['price_type'] == 'percent') ? $option['price_type'] : 'fixed'; - $row['sku'] = $option['sku']; - if ($option['max_characters']) { - $row['max_characters'] = $option['max_characters']; - } - - foreach (['file_extension', 'image_size_x', 'image_size_y'] as $fileOptionKey) { - if (!isset($option[$fileOptionKey])) { - continue; + if (Store::DEFAULT_STORE_ID === $storeId) { + $row['required'] = $option['is_require']; + $row['price'] = $option['price']; + $row['price_type'] = ($option['price_type'] === 'percent') ? 'percent' : 'fixed'; + $row['sku'] = $option['sku']; + if ($option['max_characters']) { + $row['max_characters'] = $option['max_characters']; } - $row[$fileOptionKey] = $option[$fileOptionKey]; - } + foreach (['file_extension', 'image_size_x', 'image_size_y'] as $fileOptionKey) { + if (!isset($option[$fileOptionKey])) { + continue; + } + $row[$fileOptionKey] = $option[$fileOptionKey]; + } + } $values = $option->getValues(); if ($values) { foreach ($values as $value) { - $valuePriceType = ($value['price_type'] == 'percent') ? $value['price_type'] : 'fixed'; $row['option_title'] = $value['title']; - $row['price'] = $value['price']; - $row['price_type'] = $valuePriceType; - $row['sku'] = $value['sku']; + if (Store::DEFAULT_STORE_ID === $storeId) { + $row['option_title'] = $value['title']; + $row['price'] = $value['price']; + $row['price_type'] = ($value['price_type'] === 'percent') ? 'percent' : 'fixed'; + $row['sku'] = $value['sku']; + } $customOptionsData[$productId][$storeId][] = $this->optionRowToCellString($row); } } else { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php index aa3f46a433a4d37a8692e729b8995a77b17fc572..be027405e6d1cd44511149f97fa46f0d1d2bb16a 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php @@ -14,6 +14,7 @@ use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorI use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection as ProductOptionValueCollection; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\CollectionFactory as ProductOptionValueCollectionFactory; +use Magento\Store\Model\Store; /** * Entity class which provide possibility to import product custom options @@ -23,6 +24,8 @@ use Magento\Catalog\Model\ResourceModel\Product\Option\Value\CollectionFactory a * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) * @since 100.0.2 */ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity @@ -761,7 +764,7 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity ksort($newOptionTitles); $existingOptions = $this->_oldCustomOptions[$productId]; foreach ($existingOptions as $optionId => $optionData) { - if ($optionData['type'] == $newOptionData['type'] && $optionData['titles'] == $newOptionTitles) { + if ($optionData['type'] == $newOptionData['type'] && $optionData['titles'][0] == $newOptionTitles[0]) { return $optionId; } } @@ -1124,13 +1127,19 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity { $result = [ self::COLUMN_TYPE => $name ? $optionRow['type'] : '', - self::COLUMN_IS_REQUIRED => $optionRow['required'], - self::COLUMN_ROW_SKU => $optionRow['sku'], - self::COLUMN_PREFIX . 'sku' => $optionRow['sku'], self::COLUMN_ROW_TITLE => '', self::COLUMN_ROW_PRICE => '' ]; - + if (isset($optionRow['_custom_option_store'])) { + $result[self::COLUMN_STORE] = $optionRow['_custom_option_store']; + } + if (isset($optionRow['required'])) { + $result[self::COLUMN_IS_REQUIRED] = $optionRow['required']; + } + if (isset($optionRow['sku'])) { + $result[self::COLUMN_ROW_SKU] = $optionRow['sku']; + $result[self::COLUMN_PREFIX . 'sku'] = $optionRow['sku']; + } if (isset($optionRow['option_title'])) { $result[self::COLUMN_ROW_TITLE] = $optionRow['option_title']; } @@ -1175,7 +1184,8 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity } /** - * Import data rows + * Import data rows. + * Additional store view data (option titles) will be sought in store view specified import file rows * * @return boolean * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -1189,7 +1199,8 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity $this->_tables['catalog_product_option_type_value'] ); $prevOptionId = 0; - + $optionId = null; + $valueId = null; while ($bunch = $this->_dataSourceModel->getNextBunch()) { $products = []; $options = []; @@ -1202,11 +1213,14 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity $childCount = []; foreach ($bunch as $rowNumber => $rowData) { - + if (isset($optionId, $valueId) && empty($rowData[PRODUCT::COL_STORE_VIEW_CODE])) { + $nextOptionId = $optionId; + $nextValueId = $valueId; + } + $optionId = $nextOptionId; + $valueId = $nextValueId; $multiRowData = $this->_getMultiRowFormat($rowData); - foreach ($multiRowData as $optionData) { - $combinedData = array_merge($rowData, $optionData); if (!$this->isRowAllowedToImport($combinedData, $rowNumber)) { @@ -1218,7 +1232,7 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity $optionData = $this->_collectOptionMainData( $combinedData, $prevOptionId, - $nextOptionId, + $optionId, $products, $prices ); @@ -1228,7 +1242,7 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity $this->_collectOptionTypeData( $combinedData, $prevOptionId, - $nextValueId, + $valueId, $typeValues, $typePrices, $typeTitles, @@ -1311,15 +1325,12 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity $optionData = null; if ($this->_rowIsMain) { - $optionData = $this->_getOptionData($rowData, $this->_rowProductId, $nextOptionId, $this->_rowType); - - if (!$this->_isRowHasSpecificType( - $this->_rowType - ) && ($priceData = $this->_getPriceData( - $rowData, - $nextOptionId, - $this->_rowType - )) + $optionData = empty($rowData[Product::COL_STORE_VIEW_CODE]) + ? $this->_getOptionData($rowData, $this->_rowProductId, $nextOptionId, $this->_rowType) + : ''; + + if (!$this->_isRowHasSpecificType($this->_rowType) + && ($priceData = $this->_getPriceData($rowData, $nextOptionId, $this->_rowType)) ) { $prices[$nextOptionId] = $priceData; } @@ -1347,6 +1358,7 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity * @param array &$childCount * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function _collectOptionTypeData( array $rowData, @@ -1365,39 +1377,27 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity $typeValues[$prevOptionId][] = $specificTypeData['value']; // ensure default title is set - if (!isset($typeTitles[$nextValueId][\Magento\Store\Model\Store::DEFAULT_STORE_ID])) { - $typeTitles[$nextValueId][\Magento\Store\Model\Store::DEFAULT_STORE_ID] = $specificTypeData['title']; + if (!isset($typeTitles[$nextValueId][Store::DEFAULT_STORE_ID])) { + $typeTitles[$nextValueId][Store::DEFAULT_STORE_ID] = $specificTypeData['title']; } if ($specificTypeData['price']) { if ($this->_isPriceGlobal) { - $typePrices[$nextValueId][\Magento\Store\Model\Store::DEFAULT_STORE_ID] = $specificTypeData['price']; + $typePrices[$nextValueId][Store::DEFAULT_STORE_ID] = $specificTypeData['price']; } else { // ensure default price is set - if (!isset($typePrices[$nextValueId][\Magento\Store\Model\Store::DEFAULT_STORE_ID])) { - $typePrices[$nextValueId][\Magento\Store\Model\Store::DEFAULT_STORE_ID] = $specificTypeData['price']; + if (!isset($typePrices[$nextValueId][Store::DEFAULT_STORE_ID])) { + $typePrices[$nextValueId][Store::DEFAULT_STORE_ID] = $specificTypeData['price']; } $typePrices[$nextValueId][$this->_rowStoreId] = $specificTypeData['price']; } } - $nextValueId++; - if (isset($parentCount[$prevOptionId])) { - $parentCount[$prevOptionId]++; - } else { - $parentCount[$prevOptionId] = 1; - } - } - - if (!isset($childCount[$this->_rowStoreId][$prevOptionId])) { - $childCount[$this->_rowStoreId][$prevOptionId] = 0; } - $parentValueId = $nextValueId - $parentCount[$prevOptionId] + $childCount[$this->_rowStoreId][$prevOptionId]; - $specificTypeData = $this->_getSpecificTypeData($rowData, $parentValueId, false); + $specificTypeData = $this->_getSpecificTypeData($rowData, 0, false); //For others stores if ($specificTypeData) { - $typeTitles[$parentValueId][$this->_rowStoreId] = $specificTypeData['title']; - $childCount[$this->_rowStoreId][$prevOptionId]++; + $typeTitles[$nextValueId++][$this->_rowStoreId] = $specificTypeData['title']; } } } @@ -1412,7 +1412,7 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ protected function _collectOptionTitle(array $rowData, $prevOptionId, array &$titles) { - $defaultStoreId = \Magento\Store\Model\Store::DEFAULT_STORE_ID; + $defaultStoreId = Store::DEFAULT_STORE_ID; if (!empty($rowData[self::COLUMN_TITLE])) { if (!isset($titles[$prevOptionId][$defaultStoreId])) { // ensure default title is set @@ -1536,7 +1536,7 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity } $this->_rowStoreId = $this->_storeCodeToId[$rowData[self::COLUMN_STORE]]; } else { - $this->_rowStoreId = \Magento\Store\Model\Store::DEFAULT_STORE_ID; + $this->_rowStoreId = Store::DEFAULT_STORE_ID; } // Init option type and set param which tell that row is main if (!empty($rowData[self::COLUMN_TYPE])) { @@ -1655,7 +1655,7 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity ) { $priceData = [ 'option_id' => $optionId, - 'store_id' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, + 'store_id' => Store::DEFAULT_STORE_ID, 'price_type' => 'fixed', ]; diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product.php index 147b4b67765968daed81e5373c634bc15a44f77b..ce4fca6207203c41290b80e80c659e517500b779 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/_files/product.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product.php @@ -9,6 +9,7 @@ * bundled items should not contain products with required custom options. * However, if to create such a bundle product, it will be always out of stock. */ +/** Create simple product */ require __DIR__ . '/../../../Magento/Catalog/_files/products.php'; /** @var $objectManager \Magento\TestFramework\ObjectManager */ diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options.php index efe15f5977a582b9ef659e65cab3bcbafe0894a8..a8fdaa26380a20a61db96d7d353d341c53eed9e3 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options.php @@ -47,6 +47,7 @@ $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) 'default_title' => 'Option 1', 'type' => 'select', 'required' => 1, + 'position' => 1, 'delete' => '', ], // Required "Radio Buttons" option @@ -55,6 +56,7 @@ $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) 'default_title' => 'Option 2', 'type' => 'radio', 'required' => 1, + 'position' => 2, 'delete' => '', ], // Required "Checkbox" option @@ -63,6 +65,7 @@ $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) 'default_title' => 'Option 3', 'type' => 'checkbox', 'required' => 1, + 'position' => 3, 'delete' => '', ], // Required "Multiple Select" option @@ -71,6 +74,7 @@ $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) 'default_title' => 'Option 4', 'type' => 'multi', 'required' => 1, + 'position' => 4, 'delete' => '', ], // Non-required "Multiple Select" option @@ -79,6 +83,7 @@ $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) 'default_title' => 'Option 5', 'type' => 'multi', 'required' => 0, + 'position' => 5, 'delete' => '', ] ] diff --git a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Export/RowCustomizerTest.php b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Export/RowCustomizerTest.php index 6f81421c902f6135dcc07e27ef3d897e7cbe6bd7..b7dc0d1e4d06b69e2e42a9ac56cd9a9cb504e9e6 100644 --- a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Export/RowCustomizerTest.php +++ b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Export/RowCustomizerTest.php @@ -56,4 +56,52 @@ class RowCustomizerTest extends \PHPUnit\Framework\TestCase $this->assertEquals([], $this->model->addData([], $ids['simple'])); $this->assertEquals($parsedAdditionalAttributes, $result['additional_attributes']); } + + /** + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDataFixture Magento/Bundle/_files/product.php + */ + public function testPrepareDataWithDifferentStoreValues() + { + $this->markTestSkipped('Test is blocked by MAGETWO-84209.'); + $storeCode = 'default'; + $expectedNames = [ + 'name' => 'Bundle Product Items', + 'name_' . $storeCode => 'Bundle Product Items_' . $storeCode + ]; + $parsedAdditionalAttributes = 'text_attribute=!@#$%^&*()_+1234567890-=|\\:;"\'<,>.?/' + . ',text_attribute2=,'; + $allAdditionalAttributes = $parsedAdditionalAttributes . ',weight_type=0,price_type=1'; + $collection = $this->objectManager->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); + /** @var \Magento\Store\Model\Store $store */ + $store = $this->objectManager->create(\Magento\Store\Model\Store::class); + $store->load($storeCode, 'code'); + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $product = $productRepository->get('bundle-product', 1, $store->getId()); + + $extension = $product->getExtensionAttributes(); + $options = $extension->getBundleProductOptions(); + + foreach ($options as $productOption) { + $productOption->setTitle($productOption->getTitle() . '_' . $store->getCode()); + } + $extension->setBundleProductOptions($options); + $product->setExtensionAttributes($extension); + $productRepository->save($product); + $this->model->prepareData($collection, [$product->getId()]); + $result = $this->model->addData(['additional_attributes' => $allAdditionalAttributes], $product->getId()); + $bundleValues = array_map( + function ($input) { + $data = explode('=', $input); + return [$data[0] => $data[1]]; + }, + explode(',', $result['bundle_values']) + ); + $actualNames = [ + 'name' => array_column($bundleValues, 'name')[0], + 'name' . '_' . $store->getCode() => array_column($bundleValues, 'name' . '_' . $store->getCode())[0] + ]; + self::assertSame($expectedNames, $actualNames); + } } diff --git a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/Product/Type/BundleTest.php b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/Product/Type/BundleTest.php index 19923daf81c88b51dc289213ed525438f193a340..aada927559de416a33a4d70b572e2a3dab79943c 100644 --- a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/Product/Type/BundleTest.php +++ b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/Product/Type/BundleTest.php @@ -107,4 +107,66 @@ class BundleTest extends \PHPUnit\Framework\TestCase } } } + + /** + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoAppArea adminhtml + */ + public function testBundleImportWithMultipleStoreViews() + { + // import data from CSV file + $pathToFile = __DIR__ . '/../../_files/import_bundle_multiple_store_views.csv'; + $filesystem = $this->objectManager->create( + \Magento\Framework\Filesystem::class + ); + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => $pathToFile, + 'directory' => $directory + ] + ); + $errors = $this->model->setSource( + $source + )->setParameters( + [ + 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, + 'entity' => 'catalog_product' + ] + )->validateData(); + $this->assertTrue($errors->getErrorsCount() == 0); + $this->model->importData(); + $resource = $this->objectManager->get(\Magento\Catalog\Model\ResourceModel\Product::class); + $productId = $resource->getIdBySku(self::TEST_PRODUCT_NAME); + $this->assertTrue(is_numeric($productId)); + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); + $product->load($productId); + $this->assertFalse($product->isObjectNew()); + $this->assertEquals(self::TEST_PRODUCT_NAME, $product->getName()); + $this->assertEquals(self::TEST_PRODUCT_TYPE, $product->getTypeId()); + $this->assertEquals(1, $product->getShipmentType()); + $optionIdList = $resource->getProductsIdsBySkus($this->optionSkuList); + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $i = 0; + foreach ($product->getStoreIds() as $storeId) { + $bundleOptionCollection = $productRepository->get(self::TEST_PRODUCT_NAME, false, $storeId) + ->getExtensionAttributes()->getBundleProductOptions(); + $this->assertEquals(2, count($bundleOptionCollection)); + $i++; + foreach ($bundleOptionCollection as $optionKey => $option) { + $this->assertEquals('checkbox', $option->getData('type')); + $this->assertEquals('Option ' . $i . ' ' . ($optionKey + 1), $option->getData('title')); + $this->assertEquals(self::TEST_PRODUCT_NAME, $option->getData('sku')); + $this->assertEquals($optionKey + 1, count($option->getData('product_links'))); + foreach ($option->getData('product_links') as $linkKey => $productLink) { + $optionSku = 'Simple ' . ($optionKey + 1 + $linkKey); + $this->assertEquals($optionIdList[$optionSku], $productLink->getData('entity_id')); + $this->assertEquals($optionSku, $productLink->getData('sku')); + } + } + } + } } diff --git a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/_files/import_bundle_multiple_store_views.csv b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/_files/import_bundle_multiple_store_views.csv new file mode 100644 index 0000000000000000000000000000000000000000..8bedb4ff681675a0fc08cc0265a245ce4a44cb9c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/_files/import_bundle_multiple_store_views.csv @@ -0,0 +1,5 @@ +sku,store_view_code,attribute_set_code,product_type,product_websites,name,product_online,price,additional_attributes,qty,out_of_stock_qty,website_id,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values +Simple 1,,Default,simple,base,Simple 1,1,100,,1000,0,1,,,,, +Simple 2,,Default,simple,base,Simple 2,1,200,,1000,0,1,,,,, +Simple 3,,Default,simple,base,Simple 3,1,300,,1000,0,1,,,,, +Bundle 1,,Default,bundle,base,Bundle 1,1,,shipment_type=separately,0,0,1,dynamic,dynamic,Price range,dynamic,"name=Option 1,name_default=Option 1 1,name_fixture_second_store=Option 2 1,type=checkbox,required=1,sku=Simple 1,price=0.0000,default=0,default_qty=1.0000,price_type=fixed|name=Option 2,name_default=Option 1 2,name_fixture_second_store=Option 2 2,type=checkbox,required=1,sku=Simple 2,price=0.0000,default=0,default_qty=1.0000,price_type=fixed|name=Option 2,type=checkbox,required=1,sku=Simple 3,price=0.0000,default=0,default_qty=1.0000,price_type=fixed" diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php index efbc8c36409af51b30aa620ab1e625e02017953a..897955340c296836f581a18b1d7be71e7540bc72 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php @@ -7,6 +7,7 @@ namespace Magento\CatalogImportExport\Model; use Magento\Framework\App\Bootstrap; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Store\Model\Store; /** * Abstract class for testing product export and import scenarios @@ -110,21 +111,22 @@ abstract class AbstractProductExportImportTestCase extends \PHPUnit\Framework\Te $index = 0; $ids = []; $origProducts = []; + /** @var \Magento\CatalogInventory\Model\StockRegistryStorage $stockRegistryStorage */ + $stockRegistryStorage = $this->objectManager->get(\Magento\CatalogInventory\Model\StockRegistryStorage::class); + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); while (isset($skus[$index])) { $ids[$index] = $this->productResource->getIdBySku($skus[$index]); - $origProducts[$index] = $this->objectManager->create(\Magento\Catalog\Model\Product::class) - ->load($ids[$index]); + $origProducts[$index] = $productRepository->get($skus[$index], false, Store::DEFAULT_STORE_ID); $index++; } $csvfile = $this->exportProducts(); $this->importProducts($csvfile, \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND); - while ($index > 0) { $index--; - $newProduct = $this->objectManager->create(\Magento\Catalog\Model\Product::class) - ->load($ids[$index]); - + $stockRegistryStorage->removeStockItem($ids[$index]); + $newProduct = $productRepository->get($skus[$index], false, Store::DEFAULT_STORE_ID, true); // @todo uncomment or remove after MAGETWO-49806 resolved //$this->assertEquals(count($origProductData[$index]), count($newProductData)); @@ -294,10 +296,11 @@ abstract class AbstractProductExportImportTestCase extends \PHPUnit\Framework\Te $index = 0; $ids = []; $origProducts = []; + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); while (isset($skus[$index])) { $ids[$index] = $this->productResource->getIdBySku($skus[$index]); - $origProducts[$index] = $this->objectManager->create(\Magento\Catalog\Model\Product::class) - ->load($ids[$index]); + $origProducts[$index] = $productRepository->get($skus[$index], false, Store::DEFAULT_STORE_ID); $index++; } @@ -317,10 +320,8 @@ abstract class AbstractProductExportImportTestCase extends \PHPUnit\Framework\Te while ($index > 0) { $index--; - - $id = $this->productResource->getIdBySku($skus[$index]); - $newProduct = $this->objectManager->create(\Magento\Catalog\Model\Product::class)->load($id); - + $productRepository->cleanCache(); + $newProduct = $productRepository->get($skus[$index], false, Store::DEFAULT_STORE_ID, true); // check original product is deleted $origProduct = $this->objectManager->create(\Magento\Catalog\Model\Product::class)->load($ids[$index]); $this->assertNull($origProduct->getId()); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php index 66dc304388a9481765d0917df8188d670f81531c..81592b6901f1cf6e197bc71b389eb91d5797cede 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php @@ -322,4 +322,89 @@ class ProductTest extends \PHPUnit\Framework\TestCase } } } + + /** + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_data.php + */ + public function testExportWithCustomOptions() + { + $storeCode = 'default'; + $expectedData = []; + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $store = $this->objectManager->create(\Magento\Store\Model\Store::class); + $store->load('default', 'code'); + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productRepository->get('simple', 1, $store->getStoreId()); + $newCustomOptions = []; + foreach ($product->getOptions() as $customOption) { + $defaultOptionTitle = $customOption->getTitle(); + $secondStoreOptionTitle = $customOption->getTitle() . '_' . $storeCode; + $expectedData['admin_store'][$defaultOptionTitle] = []; + $expectedData[$storeCode][$secondStoreOptionTitle] = []; + $customOption->setTitle($secondStoreOptionTitle); + if ($customOption->getValues()) { + $newOptionValues = []; + foreach ($customOption->getValues() as $customOptionValue) { + $valueTitle = $customOptionValue->getTitle(); + $expectedData['admin_store'][$defaultOptionTitle][] = $valueTitle; + $expectedData[$storeCode][$secondStoreOptionTitle][] = $valueTitle . '_' . $storeCode; + $newOptionValues[] = $customOptionValue->setTitle($valueTitle . '_' . $storeCode); + } + $customOption->setValues($newOptionValues); + } + $newCustomOptions[] = $customOption; + } + $product->setOptions($newCustomOptions); + $productRepository->save($product); + $this->model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + $exportData = $this->model->export(); + /** @var $varDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ + $varDirectory = $this->objectManager->get(\Magento\Framework\Filesystem::class) + ->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::VAR_DIR); + $varDirectory->writeFile('test_product_with_custom_options_and_second_store.csv', $exportData); + /** @var \Magento\Framework\File\Csv $csv */ + $csv = $this->objectManager->get(\Magento\Framework\File\Csv::class); + $data = $csv->getData($varDirectory->getAbsolutePath('test_product_with_custom_options_and_second_store.csv')); + $customOptionData = []; + foreach ($data[0] as $columnNumber => $columnName) { + if ($columnName === 'custom_options') { + $customOptionData['admin_store'] = $this->parseExportedCustomOption($data[1][$columnNumber]); + $customOptionData[$storeCode] = $this->parseExportedCustomOption($data[2][$columnNumber]); + } + } + self::assertSame($expectedData, $customOptionData); + } + + /** + * @param $exportedCustomOption + * @return array + */ + private function parseExportedCustomOption($exportedCustomOption) + { + $customOptions = explode('|', $exportedCustomOption); + $optionItems = []; + foreach ($customOptions as $customOption) { + $parsedOptions = array_values( + array_map( + function ($input) { + $data = explode('=', $input); + return [$data[0] => $data[1]]; + }, + explode(',', $customOption) + ) + ); + $optionName = array_column($parsedOptions, 'name')[0]; + if (!empty(array_column($parsedOptions, 'option_title'))) { + $optionItems[$optionName][] = array_column($parsedOptions, 'option_title')[0]; + } else { + $optionItems[$optionName] = []; + } + } + return $optionItems; + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index 6c673ad4712a176e200ede4f633edb27e51f7c6e..7348d0be8f00acf95d21a5e0448394f131347418 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -87,11 +87,11 @@ class ProductTest extends \Magento\TestFramework\Indexer\TestCase * @var array */ protected $_assertOptions = [ - 'is_require' => '_custom_option_is_required', - 'price' => '_custom_option_price', - 'sku' => '_custom_option_sku', - 'sort_order' => '_custom_option_sort_order', - 'max_characters' => '_custom_option_max_characters', + 'is_require' => 'required', + 'price' => 'price', + 'sku' => 'sku', + 'sort_order' => 'order', + 'max_characters' => 'max_characters', ]; /** @@ -99,7 +99,23 @@ class ProductTest extends \Magento\TestFramework\Indexer\TestCase * * @var array */ - protected $_assertOptionValues = ['title', 'price', 'sku']; + protected $_assertOptionValues = [ + 'title' => 'option_title', + 'price' => 'price', + 'sku' => 'sku' + ]; + + /** + * List of specific custom option types + * + * @var array + */ + private $specificTypes = [ + 'drop_down', + 'radio', + 'checkbox', + 'multiple', + ]; /** * Test if visibility properly saved after import @@ -321,6 +337,78 @@ class ProductTest extends \Magento\TestFramework\Indexer\TestCase $this->assertEquals($customOptionValues, $this->getCustomOptionValues($sku)); } + /** + * Tests adding of custom options with multiple store views + * + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoAppIsolation enabled + */ + public function testSaveCustomOptionsWithMultipleStoreViews() + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ + $storeManager = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); + $storeCodes = [ + 'admin', + 'default', + 'fixture_second_store' + ]; + /** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ + $importFile = 'product_with_custom_options_and_multiple_store_views.csv'; + $sku = 'simple'; + $pathToFile = __DIR__ . '/_files/' . $importFile; + $importModel = $this->createImportModel($pathToFile); + $errors = $importModel->validateData(); + $this->assertTrue($errors->getErrorsCount() == 0); + $importModel->importData(); + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Api\ProductRepositoryInterface::class + ); + foreach ($storeCodes as $storeCode) { + $storeManager->setCurrentStore($storeCode); + $product = $productRepository->get($sku); + $options = $product->getOptionInstance()->getProductOptions($product); + $expectedData = $this->getExpectedOptionsData($pathToFile, $storeCode); + $expectedData = $this->mergeWithExistingData($expectedData, $options); + $actualData = $this->getActualOptionsData($options); + // assert of equal type+titles + $expectedOptions = $expectedData['options']; + // we need to save key values + $actualOptions = $actualData['options']; + sort($expectedOptions); + sort($actualOptions); + $this->assertEquals($expectedOptions, $actualOptions); + + // assert of options data + $this->assertCount(count($expectedData['data']), $actualData['data']); + $this->assertCount(count($expectedData['values']), $actualData['values']); + foreach ($expectedData['options'] as $expectedId => $expectedOption) { + $elementExist = false; + // find value in actual options and values + foreach ($actualData['options'] as $actualId => $actualOption) { + if ($actualOption == $expectedOption) { + $elementExist = true; + $this->assertEquals($expectedData['data'][$expectedId], $actualData['data'][$actualId]); + if (array_key_exists($expectedId, $expectedData['values'])) { + $this->assertEquals($expectedData['values'][$expectedId], $actualData['values'][$actualId]); + } + unset($actualData['options'][$actualId]); + // remove value in case of duplicating key values + break; + } + } + $this->assertTrue($elementExist, 'Element must exist.'); + } + + // Make sure that after importing existing options again, option IDs and option value IDs are not changed + $customOptionValues = $this->getCustomOptionValues($sku); + $this->createImportModel($pathToFile)->importData(); + $this->assertEquals($customOptionValues, $this->getCustomOptionValues($sku)); + } + } + /** * @return array */ @@ -462,9 +550,12 @@ class ProductTest extends \Magento\TestFramework\Indexer\TestCase * Returns expected product data: current id, options, options data and option values * * @param string $pathToFile + * @param string $storeCode * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ - protected function getExpectedOptionsData($pathToFile) + protected function getExpectedOptionsData($pathToFile, $storeCode = '') { $productData = $this->csvToArray(file_get_contents($pathToFile)); $expectedOptionId = 0; @@ -473,31 +564,53 @@ class ProductTest extends \Magento\TestFramework\Indexer\TestCase $expectedData = []; // array of option data $expectedValues = []; - // array of option values data - foreach ($productData['data'] as $data) { - if (!empty($data['_custom_option_type']) && !empty($data['_custom_option_title'])) { - $lastOptionKey = $data['_custom_option_type'] . '|' . $data['_custom_option_title']; - $expectedOptionId++; - $expectedOptions[$expectedOptionId] = $lastOptionKey; - $expectedData[$expectedOptionId] = []; - foreach ($this->_assertOptions as $assertKey => $assertFieldName) { - if (array_key_exists($assertFieldName, $data)) { - $expectedData[$expectedOptionId][$assertKey] = $data[$assertFieldName]; + $storeRowId = null; + foreach ($productData['data'] as $rowId => $rowData) { + $storeCode = ($storeCode == 'admin') ? '' : $storeCode; + if ($rowData['store_view_code'] == $storeCode) { + $storeRowId = $rowId; + break; + } + } + foreach (explode('|', $productData['data'][$storeRowId]['custom_options']) as $optionData) { + $option = array_values( + array_map( + function ($input) { + $data = explode('=',$input); + return [$data[0] => $data[1]]; + }, + explode(',', $optionData) + ) + ); + $option = call_user_func_array('array_merge', $option); + + if (!empty($option['type']) && !empty($option['name'])) { + $lastOptionKey = $option['type'] . '|' . $option['name']; + if (!isset($expectedOptions[$expectedOptionId]) + || $expectedOptions[$expectedOptionId] != $lastOptionKey) { + $expectedOptionId++; + $expectedOptions[$expectedOptionId] = $lastOptionKey; + $expectedData[$expectedOptionId] = []; + foreach ($this->_assertOptions as $assertKey => $assertFieldName) { + if (array_key_exists($assertFieldName, $option) + && !(($assertFieldName == 'price' || $assertFieldName == 'sku') + && in_array($option['type'], $this->specificTypes)) + ) { + $expectedData[$expectedOptionId][$assertKey] = $option[$assertFieldName]; + } + } } } - } - if (!empty($data['_custom_option_row_title']) && empty($data['_custom_option_store'])) { - $optionData = []; - foreach ($this->_assertOptionValues as $assertKey) { - $valueKey = \Magento\CatalogImportExport\Model\Import\Product\Option::COLUMN_PREFIX . - 'row_' . - $assertKey; - $optionData[$assertKey] = $data[$valueKey]; + $optionValue = []; + if (!empty($option['name']) && !empty($option['option_title'])) { + foreach ($this->_assertOptionValues as $assertKey => $assertFieldName) { + if (isset($option[$assertFieldName])) { + $optionValue[$assertKey] = $option[$assertFieldName]; + } + } + $expectedValues[$expectedOptionId][] = $optionValue; } - $expectedValues[$expectedOptionId][] = $optionData; } - } - return [ 'id' => $expectedOptionId, 'options' => $expectedOptions, @@ -523,13 +636,27 @@ class ProductTest extends \Magento\TestFramework\Indexer\TestCase $expectedValues = $expected['values']; foreach ($options as $option) { $optionKey = $option->getType() . '|' . $option->getTitle(); + $optionValues = $this->getOptionValues($option); if (!in_array($optionKey, $expectedOptions)) { $expectedOptionId++; $expectedOptions[$expectedOptionId] = $optionKey; $expectedData[$expectedOptionId] = $this->getOptionData($option); - if ($optionValues = $this->getOptionValues($option)) { + if ($optionValues) { $expectedValues[$expectedOptionId] = $optionValues; } + } else { + $existingOptionId = array_search($optionKey, $expectedOptions); + $expectedData[$existingOptionId] = array_merge( + $this->getOptionData($option), + $expectedData[$existingOptionId] + + ); + if ($optionValues) { + foreach ($optionValues as $optionKey => $optionValue) + $expectedValues[$existingOptionId][$optionKey] = array_merge( + $optionValue, $expectedValues[$existingOptionId][$optionKey] + ); + } } } @@ -605,7 +732,7 @@ class ProductTest extends \Magento\TestFramework\Indexer\TestCase /** @var $value \Magento\Catalog\Model\Product\Option\Value */ foreach ($values as $value) { $optionData = []; - foreach ($this->_assertOptionValues as $assertKey) { + foreach (array_keys($this->_assertOptionValues) as $assertKey) { if ($value->hasData($assertKey)) { $optionData[$assertKey] = $value->getData($assertKey); } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options.csv index 2fb3e879a8aedddafdeb885d338c56b4a619d1ad..ac701022a0815c5ee1e670e2eab78dd883a0005b 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options.csv +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options.csv @@ -1,2 +1,2 @@ sku,website_code,store_view_code,attribute_set_code,product_type,name,description,short_description,weight,product_online,visibility,product_websites,categories,price,special_price,special_price_from_date,special_price_to_date,tax_class_name,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,additional_images,additional_image_labels,configurable_variation_labels,configurable_variations,bundle_price_type,bundle_sku_type,bundle_weight_type,bundle_values,downloadble_samples,downloadble_links,associated_skus,related_skus,crosssell_skus,upsell_skus,custom_options,additional_attributes,manage_stock,is_in_stock,qty,out_of_stock_qty,is_qty_decimal,allow_backorders,min_cart_qty,max_cart_qty,notify_on_stock_below,qty_increments,enable_qty_increments,is_decimal_divided,new_from_date,new_to_date,gift_message_available,created_at,updated_at,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_price,msrp_display_actual_price_type,map_enabled -simple,base,,Default,simple,New Product,,,9,1,"Catalog, Search",base,,10,,,,Taxable Goods,new-product,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Field Title,type=field,required=1;sku=1-text,price=0,price_type=fixed,max_characters=10|name=Test Date and Time Title,type=date_time,required=1,price=2,option_title=custom option 1,sku=2-date|name=Test Select,type=drop_down,required=1,price=3,option_title=Option 1,sku=3-1-select|name=Test Select,type=drop_down,required=1,price=3,option_title=Option 2,sku=3-2-select|name=Test Radio,type=radio,required=1,price=3,option_title=Option 1,sku=4-1-radio|name=Test Radio,type=radio,required=1,price=3,option_title=Option 2,sku=4-2-radio",,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,, +simple,base,,Default,simple,New Product,,,9,1,"Catalog, Search",base,,10,,,,Taxable Goods,new-product,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Field Title,type=field,required=1,sku=1-text,price=0,price_type=fixed,max_characters=10|name=Test Date and Time Title,type=date_time,required=1,price=2,sku=2-date|name=Test Select,type=drop_down,required=1,price=3,option_title=Option 1,sku=3-1-select|name=Test Select,type=drop_down,required=1,price=3,option_title=Option 2,sku=3-2-select|name=Test Radio,type=radio,required=1,price=3,option_title=Option 1,sku=4-1-radio|name=Test Radio,type=radio,required=1,price=3,option_title=Option 2,sku=4-2-radio",,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,, diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options_and_multiple_store_views.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options_and_multiple_store_views.csv new file mode 100644 index 0000000000000000000000000000000000000000..69d460cde472e5240931430a2d2188388a6a7851 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options_and_multiple_store_views.csv @@ -0,0 +1,4 @@ +sku,website_code,store_view_code,attribute_set_code,product_type,name,description,short_description,weight,product_online,visibility,product_websites,categories,price,special_price,special_price_from_date,special_price_to_date,tax_class_name,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,additional_images,additional_image_labels,configurable_variation_labels,configurable_variations,bundle_price_type,bundle_sku_type,bundle_weight_type,bundle_values,downloadble_samples,downloadble_links,associated_skus,related_skus,crosssell_skus,upsell_skus,custom_options,additional_attributes,manage_stock,is_in_stock,qty,out_of_stock_qty,is_qty_decimal,allow_backorders,min_cart_qty,max_cart_qty,notify_on_stock_below,qty_increments,enable_qty_increments,is_decimal_divided,new_from_date,new_to_date,gift_message_available,created_at,updated_at,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_price,msrp_display_actual_price_type,map_enabled +simple,base,,Default,simple,New Product,,,9,1,"Catalog, Search",base,,10,,,,Taxable Goods,new-product,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Select,type=drop_down,required=1,price=3,option_title=Select Option 1,sku=3-1-select|name=Test Select,type=drop_down,required=1,price=3,option_title=Select Option 2,sku=3-2-select|name=Test Field Title,type=field,required=1,sku=1-text,price=0,price_type=fixed,max_characters=10|name=Test Date and Time Title,type=date_time,required=1,price=2,sku=2-date|name=Test Checkbox,type=checkbox,required=1,price=3,option_title=Checkbox Option 1,sku=4-1-select|name=Test Checkbox,type=checkbox,required=1,price=3,option_title=Checkbox Option 2,sku=4-2-select|name=Test Radio,type=radio,required=1,price=3,option_title=Radio Option 1,sku=5-1-radio|name=Test Radio,type=radio,required=1,price=3,option_title=Radio Option 2,sku=5-2-radio",,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,, +simple,,default,Default,simple,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Select_default,type=drop_down,option_title=Select Option 1_default|name=Test Select_default,type=drop_down,option_title=Select Option 2_default|name=Test Field Title_default,type=field|name=Test Date and Time Title_default,type=date_time|name=Test Checkbox_default,type=checkbox,option_title=Checkbox Option 1_default|name=Test Checkbox_default,type=checkbox,option_title=Checkbox Option 2_default|name=Test Radio_default,type=radio,option_title=Radio Option 1_default|name=Test Radio_default,type=radio,option_title=Radio Option 2_default",,,,,,,,,,,,,,,,,,,,,,,,,,, +simple,,fixture_second_store,Default,simple,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Select_fixture_second_store,type=drop_down,option_title=Select Option 1_fixture_second_store|name=Test Select_fixture_second_store,type=drop_down,option_title=Select Option 2_fixture_second_store|name=Test Field Title_fixture_second_store,type=field|name=Test Date and Time Title_fixture_second_store,type=date_time|name=Test Checkbox_second_store,type=checkbox,option_title=Checkbox Option 1_second_store|name=Test Checkbox_second_store,type=checkbox,option_title=Checkbox Option 2_second_store|name=Test Radio_fixture_second_store,type=radio,option_title=Radio Option 1_fixture_second_store|name=Test Radio_fixture_second_store,type=radio,option_title=Radio Option 2_fixture_second_store",,,,,,,,,,,,,,,,,,,,,,,,,,,