From d3ab31fd2ad9b8d7b938e38c6064d62d30152749 Mon Sep 17 00:00:00 2001 From: Sathakathulla-S Date: Fri, 25 Apr 2025 18:51:34 +0530 Subject: [PATCH] Problems with price scope and config.php issue fixed --- .../Cron/DeleteOutdatedPriceValuesTest.php | 131 ++++ .../Price/System/Config/PriceScope.php | 18 +- .../Product/Attribute/Backend/PriceTest.php | 324 ++++++++ ...witchPriceAttributeScopeOnConfigChange.php | 43 +- .../product_with_price_on_second_website.php | 73 ++ ..._with_price_on_second_website_rollback.php | 33 + app/code/Magento/Catalog/etc/events.xml | 3 - .../Model/Export/ProductTest.php | 729 ++++++++++++++++++ ...e_product_with_price_on_second_website.php | 122 +++ ..._with_price_on_second_website_rollback.php | 30 + ...cond_website_with_base_second_currency.php | 36 + 11 files changed, 1508 insertions(+), 34 deletions(-) create mode 100644 app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValuesTest.php create mode 100644 app/code/Magento/Catalog/Model/Product/Attribute/Backend/PriceTest.php create mode 100644 app/code/Magento/Catalog/_files/product_with_price_on_second_website.php create mode 100644 app/code/Magento/Catalog/_files/product_with_price_on_second_website_rollback.php create mode 100644 app/code/Magento/CatalogImportExport/Model/Export/ProductTest.php create mode 100644 app/code/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php create mode 100644 app/code/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website_rollback.php create mode 100644 app/code/Magento/Store/_files/second_website_with_base_second_currency.php diff --git a/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValuesTest.php b/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValuesTest.php new file mode 100644 index 0000000000000..b9c2b308046dd --- /dev/null +++ b/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValuesTest.php @@ -0,0 +1,131 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->store = $this->objectManager->create(\Magento\Store\Model\Store::class); + $this->cron = $this->objectManager->create(\Magento\Catalog\Cron\DeleteOutdatedPriceValues::class); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoConfigFixture current_store catalog/price/scope 1 + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + */ + public function testExecute() + { + $defaultStorePrice = 10.00; + $secondStorePrice = 9.99; + $secondStoreId = $this->store->load('fixture_second_store')->getId(); + /** @var \Magento\Catalog\Model\Product\Action $productAction */ + $productAction = $this->objectManager->create( + \Magento\Catalog\Model\Product\Action::class + ); + /** @var ReinitableConfigInterface $reinitiableConfig */ + $reinitiableConfig = $this->objectManager->get(ReinitableConfigInterface::class); + $reinitiableConfig->setValue( + 'catalog/price/scope', + \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE + ); + + $reflection = new \ReflectionClass(\Magento\Catalog\Model\Attribute\ScopeOverriddenValue::class); + $paths = $reflection->getProperty('attributesValues'); + $paths->setAccessible(true); + $paths->setValue($this->objectManager->get(\Magento\Catalog\Model\Attribute\ScopeOverriddenValue::class), null); + $paths->setAccessible(false); + $product = $this->productRepository->get('simple'); + $productResource = $this->objectManager->create(\Magento\Catalog\Model\ResourceModel\Product::class); + $productId = $product->getId(); + $productAction->updateWebsites( + [$productId], + [$this->store->load('fixture_second_store')->getWebsiteId()], + 'add' + ); + $product->setStoreId($secondStoreId); + $product->setPrice($secondStorePrice); + $productResource->save($product); + $attribute = $this->objectManager->get(\Magento\Eav\Model\Config::class) + ->getAttribute( + 'catalog_product', + 'price' + ); + $this->assertEquals( + $secondStorePrice, + $productResource->getAttributeRawValue($productId, $attribute->getId(), $secondStoreId) + ); + /** @var MutableScopeConfigInterface $config */ + $config = $this->objectManager->get( + MutableScopeConfigInterface::class + ); + $config->setValue( + \Magento\Store\Model\Store::XML_PATH_PRICE_SCOPE, + null, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ); + $this->cron->execute(); + $this->assertEquals( + $secondStorePrice, + $productResource->getAttributeRawValue($productId, $attribute->getId(), $secondStoreId) + ); + $config->setValue( + \Magento\Store\Model\Store::XML_PATH_PRICE_SCOPE, + \Magento\Store\Model\Store::PRICE_SCOPE_GLOBAL, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ); + /** @var \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig */ + $this->cron->execute(); + $this->assertEquals( + $defaultStorePrice, + $productResource->getAttributeRawValue($productId, $attribute->getId(), $secondStoreId) + ); + } + protected function tearDown(): void + { + parent::tearDown(); + /** @var ReinitableConfigInterface $reinitiableConfig */ + $reinitiableConfig = $this->objectManager->get(ReinitableConfigInterface::class); + $reinitiableConfig->setValue( + 'catalog/price/scope', + \Magento\Store\Model\Store::PRICE_SCOPE_GLOBAL + ); + } +} diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/System/Config/PriceScope.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/System/Config/PriceScope.php index bd6f7923441c8..ef84d2d3f7dd9 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/System/Config/PriceScope.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/System/Config/PriceScope.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Model\Indexer\Product\Price\System\Config; +use Magento\Catalog\Model\Config\PriceScopeChange; + /** * Price scope backend model */ @@ -15,15 +17,21 @@ class PriceScope extends \Magento\Framework\App\Config\Value */ protected $indexerRegistry; + /** + * @var PriceScopeChange + */ + private $priceScopeChange; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\App\Config\ScopeConfigInterface $config * @param \Magento\Framework\App\Cache\TypeListInterface $cacheTypeList * @param \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry - * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource - * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection + * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource + * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data + * @param PriceScopeChange|null $priceScopeChange */ public function __construct( \Magento\Framework\Model\Context $context, @@ -33,10 +41,13 @@ public function __construct( \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry, ?\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, ?\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + PriceScopeChange $priceScopeChange = null ) { parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); $this->indexerRegistry = $indexerRegistry; + $this->priceScopeChange = $priceScopeChange ?: + \Magento\Framework\App\ObjectManager::getInstance()->get(PriceScopeChange::class); } /** @@ -61,5 +72,6 @@ public function processValue() $this->indexerRegistry->get(\Magento\Catalog\Model\Indexer\Product\Price\Processor::INDEXER_ID) ->invalidate(); } + $this->priceScopeChange->changeScope((int)$this->getValue()); } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/PriceTest.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/PriceTest.php new file mode 100644 index 0000000000000..85b4f678f4a8e --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/PriceTest.php @@ -0,0 +1,324 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var ReinitableConfigInterface $reinitiableConfig */ + $reinitiableConfig = $this->objectManager->get(ReinitableConfigInterface::class); + $reinitiableConfig->setValue( + 'catalog/price/scope', + \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE + ); + + $this->model = $this->objectManager->create( + \Magento\Catalog\Model\Product\Attribute\Backend\Price::class + ); + $this->productRepository = $this->objectManager->create( + ProductRepositoryInterface::class + ); + $this->productResource = $this->objectManager->create( + Product::class + ); + $this->fixtures = $this->objectManager->get(DataFixtureStorageManager::class)->getStorage(); + $this->model->setAttribute( + $this->objectManager->get( + \Magento\Eav\Model\Config::class + )->getAttribute( + 'catalog_product', + 'price' + ) + ); + } + + /** + * @magentoDbIsolation disabled + */ + public function testSetScopeDefault() + { + /* validate result of setAttribute */ + $this->assertEquals( + \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL, + $this->model->getAttribute()->getIsGlobal() + ); + $this->model->setScope($this->model->getAttribute()); + $this->assertEquals( + \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL, + $this->model->getAttribute()->getIsGlobal() + ); + } + + /** + * @magentoDbIsolation disabled + * @magentoConfigFixture current_store catalog/price/scope 1 + */ + public function testSetScope() + { + $this->model->setScope($this->model->getAttribute()); + $this->assertEquals( + \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_WEBSITE, + $this->model->getAttribute()->getIsGlobal() + ); + } + + /** + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture current_store catalog/price/scope 1 + * @magentoConfigFixture current_store currency/options/base GBP + */ + public function testAfterSave() + { + /** @var \Magento\Store\Model\Store $store */ + $store = $this->objectManager->create(\Magento\Store\Model\Store::class); + $globalStoreId = $store->load('admin')->getId(); + $product = $this->productRepository->get('simple'); + $product->setPrice('9.99'); + $product->setStoreId($globalStoreId); + $this->productResource->save($product); + $product = $this->productRepository->get('simple', false, $globalStoreId, true); + $this->assertEquals('9.990000', $product->getPrice()); + } + + /** + * @magentoDataFixture Magento\Store\Test\Fixture\Website as:website2 + * @magentoDataFixture Magento\Store\Test\Fixture\Group with:{"website_id":"$website2.id$"} as:store_group2 + * @magentoDataFixture Magento\Store\Test\Fixture\Store with:{"store_group_id":"$store_group2.id$"} as:store2 + * @magentoDataFixture Magento\Store\Test\Fixture\Store with:{"store_group_id":"$store_group2.id$"} as:store3 + * @magentoDataFixture Magento\Catalog\Test\Fixture\Product as:product + * @magentoConfigFixture current_store catalog/price/scope 1 + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + */ + public function testAfterSaveWithDifferentStores() + { + /** @var \Magento\Store\Model\Store $store */ + $store = $this->objectManager->create( + \Magento\Store\Model\Store::class + ); + $globalStoreId = $store->load('admin')->getId(); + $secondStoreId = $this->fixtures->get('store2')->getId(); + $thirdStoreId = $this->fixtures->get('store3')->getId(); + $productSku = $this->fixtures->get('product')->getSku(); + /** @var \Magento\Catalog\Model\Product\Action $productAction */ + $productAction = $this->objectManager->create( + \Magento\Catalog\Model\Product\Action::class + ); + + $product = $this->productRepository->get($productSku); + $productId = $product->getId(); + $productAction->updateWebsites([$productId], [$store->load('fixture_second_store')->getWebsiteId()], 'add'); + $product->setStoreId($secondStoreId); + $product->setPrice('9.99'); + $this->productResource->save($product); + + $product = $this->productRepository->get($productSku, false, $globalStoreId, true); + $this->assertEquals(10, $product->getPrice()); + + $product = $this->productRepository->get($productSku, false, $secondStoreId, true); + $this->assertEquals('9.990000', $product->getPrice()); + + $product = $this->productRepository->get($productSku, false, $thirdStoreId, true); + $this->assertEquals('9.990000', $product->getPrice()); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoConfigFixture current_store catalog/price/scope 1 + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + */ + public function testAfterSaveWithSameCurrency() + { + /** @var \Magento\Store\Model\Store $store */ + $store = $this->objectManager->create( + \Magento\Store\Model\Store::class + ); + $globalStoreId = $store->load('admin')->getId(); + $secondStoreId = $store->load('fixture_second_store')->getId(); + $thirdStoreId = $store->load('fixture_third_store')->getId(); + /** @var \Magento\Catalog\Model\Product\Action $productAction */ + $productAction = $this->objectManager->create( + \Magento\Catalog\Model\Product\Action::class + ); + + $product = $this->productRepository->get('simple'); + $productId = $product->getId(); + $productAction->updateWebsites([$productId], [$store->load('fixture_second_store')->getWebsiteId()], 'add'); + $product->setOrigData(); + $product->setStoreId($secondStoreId); + $product->setPrice('9.99'); + $this->productResource->save($product); + + $product = $this->productRepository->get('simple', false, $globalStoreId, true); + $this->assertEquals(10, $product->getPrice()); + + $product = $this->productRepository->get('simple', false, $secondStoreId, true); + $this->assertEquals('9.990000', $product->getPrice()); + + $product = $this->productRepository->get('simple', false, $thirdStoreId, true); + $this->assertEquals('9.990000', $product->getPrice()); + } + + /** + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoConfigFixture current_store catalog/price/scope 1 + */ + public function testAfterSaveWithUseDefault() + { + /** @var \Magento\Store\Model\Store $store */ + $store = $this->objectManager->create( + \Magento\Store\Model\Store::class + ); + $globalStoreId = $store->load('admin')->getId(); + $secondStoreId = $store->load('fixture_second_store')->getId(); + $thirdStoreId = $store->load('fixture_third_store')->getId(); + /** @var \Magento\Catalog\Model\Product\Action $productAction */ + $productAction = $this->objectManager->create( + \Magento\Catalog\Model\Product\Action::class + ); + + $product = $this->productRepository->get('simple'); + $productId = $product->getId(); + $productAction->updateWebsites([$productId], [$store->load('fixture_second_store')->getWebsiteId()], 'add'); + $product->setOrigData(); + $product->setStoreId($secondStoreId); + $product->setPrice('9.99'); + $this->productResource->save($product); + + $product = $this->productRepository->get('simple', false, $globalStoreId, true); + $this->assertEquals(10, $product->getPrice()); + + $product = $this->productRepository->get('simple', false, $secondStoreId, true); + $this->assertEquals('9.990000', $product->getPrice()); + + $product = $this->productRepository->get('simple', false, $thirdStoreId, true); + $this->assertEquals('9.990000', $product->getPrice()); + + $product->setStoreId($thirdStoreId); + $product->setPrice(null); + $this->productResource->save($product); + + $product = $this->productRepository->get('simple', false, $globalStoreId, true); + $this->assertEquals(10, $product->getPrice()); + + $product = $this->productRepository->get('simple', false, $secondStoreId, true); + $this->assertEquals(10, $product->getPrice()); + + $product = $this->productRepository->get('simple', false, $thirdStoreId, true); + $this->assertEquals(10, $product->getPrice()); + } + + /** + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoConfigFixture default_store catalog/price/scope 1 + */ + public function testAfterSaveForWebsitesWithDifferentCurrencies() + { + /** @var \Magento\Store\Model\Store $store */ + $store = $this->objectManager->create( + \Magento\Store\Model\Store::class + ); + + /** @var \Magento\Directory\Model\ResourceModel\Currency $rate */ + $rate = $this->objectManager->create(\Magento\Directory\Model\ResourceModel\Currency::class); + $rate->saveRates([ + 'USD' => ['EUR' => 2], + 'EUR' => ['USD' => 0.5] + ]); + + $globalStoreId = $store->load('admin')->getId(); + $secondStore = $store->load('fixture_second_store'); + $secondStoreId = $store->load('fixture_second_store')->getId(); + $thirdStoreId = $store->load('fixture_third_store')->getId(); + + /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ + $config = $this->objectManager->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class); + $config->setValue( + 'currency/options/default', + 'EUR', + \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES, + 'test' + ); + + $productAction = $this->objectManager->create( + \Magento\Catalog\Model\Product\Action::class + ); + $product = $this->productRepository->get('simple'); + $productId = $product->getId(); + $productAction->updateWebsites([$productId], [$secondStore->getWebsiteId()], 'add'); + $product->setOrigData(); + $product->setStoreId($globalStoreId); + $product->setPrice(100); + $this->productResource->save($product); + + $product = $this->productRepository->get('simple', false, $globalStoreId, true); + $this->assertEquals(100, $product->getPrice()); + + $product = $this->productRepository->get('simple', false, $secondStoreId, true); + $this->assertEquals(100, $product->getPrice()); + + $product = $this->productRepository->get('simple', false, $thirdStoreId, true); + $this->assertEquals(100, $product->getPrice()); + } + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + /** @var ReinitableConfigInterface $reinitiableConfig */ + $reinitiableConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + ReinitableConfigInterface::class + ); + $reinitiableConfig->setValue( + 'catalog/price/scope', + \Magento\Store\Model\Store::PRICE_SCOPE_GLOBAL + ); + } +} diff --git a/app/code/Magento/Catalog/Observer/SwitchPriceAttributeScopeOnConfigChange.php b/app/code/Magento/Catalog/Observer/SwitchPriceAttributeScopeOnConfigChange.php index 7d7eb54ac1f7b..9e49bfe67dd7a 100644 --- a/app/code/Magento/Catalog/Observer/SwitchPriceAttributeScopeOnConfigChange.php +++ b/app/code/Magento/Catalog/Observer/SwitchPriceAttributeScopeOnConfigChange.php @@ -3,26 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Catalog\Observer; -use Magento\Framework\Event\Observer as EventObserver; -use Magento\Framework\Event\ObserverInterface; +namespace Magento\Catalog\Model\Config; + use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Api\ProductAttributeRepositoryInterface; -use Magento\Store\Model\Store; -use Magento\Framework\App\Config\ReinitableConfigInterface; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Store\Model\Store; -/** - * Observer is responsible for changing scope for all price attributes in system - * depending on 'Catalog Price Scope' configuration parameter - */ -class SwitchPriceAttributeScopeOnConfigChange implements ObserverInterface +class PriceScopeChange { /** - * @var ReinitableConfigInterface + * @var SearchCriteriaBuilder */ - private $config; + private $searchCriteriaBuilder; /** * @var ProductAttributeRepositoryInterface @@ -30,40 +24,33 @@ class SwitchPriceAttributeScopeOnConfigChange implements ObserverInterface private $productAttributeRepository; /** - * @var SearchCriteriaBuilder - */ - private $searchCriteriaBuilder; - - /** - * @param ReinitableConfigInterface $config * @param ProductAttributeRepositoryInterface $productAttributeRepository * @param SearchCriteriaBuilder $searchCriteriaBuilder */ public function __construct( - ReinitableConfigInterface $config, ProductAttributeRepositoryInterface $productAttributeRepository, SearchCriteriaBuilder $searchCriteriaBuilder ) { - $this->config = $config; $this->productAttributeRepository = $productAttributeRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; } /** - * Change scope for all price attributes according to - * 'Catalog Price Scope' configuration parameter value + * Updates the price attributes scope + * + * @param int $value + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\StateException * - * @param EventObserver $observer - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @retrun void */ - public function execute(EventObserver $observer) + public function changeScope(int $value): void { $this->searchCriteriaBuilder->addFilter('frontend_input', 'price'); $criteria = $this->searchCriteriaBuilder->create(); - $scope = $this->config->getValue(Store::XML_PATH_PRICE_SCOPE); - $scope = ($scope == Store::PRICE_SCOPE_WEBSITE) + $scope = ($value === Store::PRICE_SCOPE_WEBSITE) ? ProductAttributeInterface::SCOPE_WEBSITE_TEXT : ProductAttributeInterface::SCOPE_GLOBAL_TEXT; diff --git a/app/code/Magento/Catalog/_files/product_with_price_on_second_website.php b/app/code/Magento/Catalog/_files/product_with_price_on_second_website.php new file mode 100644 index 0000000000000..3f640d64e30fb --- /dev/null +++ b/app/code/Magento/Catalog/_files/product_with_price_on_second_website.php @@ -0,0 +1,73 @@ +requireDataFixture('Magento/Store/_files/second_website_with_store_group_and_store.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Config $configResource */ +$configResource = $objectManager->get(Config::class); +$configResource->saveConfig(Data::XML_PATH_PRICE_SCOPE, Store::PRICE_SCOPE_WEBSITE, 'default', 0); +$objectManager->get(ReinitableConfigInterface::class)->reinit(); +/** @var ProductInterfaceFactory $productFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$websiteId = $websiteRepository->get('test')->getId(); +$defaultWebsiteId = $websiteRepository->get('base')->getId(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$secondStoreId = $storeManager->getStore('fixture_second_store')->getId(); +/** @var $product \Magento\Catalog\Model\Product */ +$product = $productFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$defaultWebsiteId, $websiteId]) + ->setName('Second website price product') + ->setSku('second-website-price-product') + ->setPrice(20) + ->setSpecialPrice(15) + ->setWeight(1) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_in_stock' => 1 + ] + ); +$productRepository->save($product); + +try { + $currentStoreCode = $storeManager->getStore()->getCode(); + $storeManager->setCurrentStore('fixture_second_store'); + $product = $productRepository->get('second-website-price-product', false, $secondStoreId, true); + $product->setPrice(10) + ->setSpecialPrice(5.99); + $productRepository->save($product); +} finally { + $storeManager->setCurrentStore($currentStoreCode); +} diff --git a/app/code/Magento/Catalog/_files/product_with_price_on_second_website_rollback.php b/app/code/Magento/Catalog/_files/product_with_price_on_second_website_rollback.php new file mode 100644 index 0000000000000..10157b8eae4fe --- /dev/null +++ b/app/code/Magento/Catalog/_files/product_with_price_on_second_website_rollback.php @@ -0,0 +1,33 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var Config $configResource */ +$configResource = $objectManager->get(Config::class); +$configResource->deleteConfig(Data::XML_PATH_PRICE_SCOPE, 'default', 0); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +try { + $productRepository->deleteById('second-website-price-product'); +} catch (NoSuchEntityException $e) { + //product already deleted +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); +Resolver::getInstance()->requireDataFixture( + 'Magento/Store/_files/second_website_with_store_group_and_store_rollback.php' +); diff --git a/app/code/Magento/Catalog/etc/events.xml b/app/code/Magento/Catalog/etc/events.xml index ee5643e9ddb11..657ce1b5f6065 100644 --- a/app/code/Magento/Catalog/etc/events.xml +++ b/app/code/Magento/Catalog/etc/events.xml @@ -52,9 +52,6 @@ - - - diff --git a/app/code/Magento/CatalogImportExport/Model/Export/ProductTest.php b/app/code/Magento/CatalogImportExport/Model/Export/ProductTest.php new file mode 100644 index 0000000000000..0d13cf4c8ea4b --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Model/Export/ProductTest.php @@ -0,0 +1,729 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->fileSystem = $this->objectManager->get(\Magento\Framework\Filesystem::class); + $this->model = $this->objectManager->create( + \Magento\CatalogImportExport\Model\Export\Product::class + ); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + } + + /** + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_data.php + * @magentoDbIsolation enabled + * + * @return void + */ + public function testExport(): void + { + $this->model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + $exportData = $this->model->export(); + $this->assertStringContainsString('New Product', $exportData); + + $this->assertStringContainsString('Option 1 & Value 1"', $exportData); + $this->assertStringContainsString('Option 1 & Value 2"', $exportData); + $this->assertStringContainsString('Option 1 & Value 3"', $exportData); + $this->assertStringContainsString('Option 4 ""!@#$%^&*', $exportData); + $this->assertStringContainsString('test_option_code_2', $exportData); + $this->assertStringContainsString('max_characters=10', $exportData); + $this->assertStringContainsString('text_attribute=!@#$%^&*()_+1234567890-=|\\:;""\'<,>.?/', $exportData); + $occurrencesCount = substr_count($exportData, 'Hello "" &"" Bring the water bottle when you can!'); + $this->assertEquals(1, $occurrencesCount); + } + + /** + * Verify successful export of the product with custom attributes containing json and markup + * + * @magentoDataFixture Magento/Catalog/_files/product_text_attribute.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @magentoDbIsolation enabled + * @dataProvider exportWithJsonAndMarkupTextAttributeDataProvider + * @param string $attributeData + * @param string $expectedResult + * @return void + */ + public function testExportWithJsonAndMarkupTextAttribute(string $attributeData, string $expectedResult): void + { + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $product = $productRepository->get('simple2'); + + /** @var \Magento\Eav\Model\Config $eavConfig */ + $eavConfig = $objectManager->get(\Magento\Eav\Model\Config::class); + $eavConfig->clear(); + $attribute = $eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'text_attribute'); + $attribute->setDefaultValue($attributeData); + /** @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface $productAttributeRepository */ + $productAttributeRepository = $objectManager->get( + \Magento\Catalog\Api\ProductAttributeRepositoryInterface::class + ); + $productAttributeRepository->save($attribute); + $product->setCustomAttribute('text_attribute', $attribute->getDefaultValue()); + $productRepository->save($product); + + $this->model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + $exportData = $this->model->export(); + $this->assertStringContainsString('Simple Product2', $exportData); + $this->assertStringContainsString($expectedResult, $exportData); + } + + /** + * @return array + */ + public function exportWithJsonAndMarkupTextAttributeDataProvider(): array + { + return [ + 'json' => [ + '{"type": "basic", "unit": "inch", "sign": "(")", "size": "1.5""}', + '"text_attribute={""type"": ""basic"", ""unit"": ""inch"", ""sign"": ""("")"", ""size"": ""1.5""""}"' + ], + 'markup' => [ + '
Element type is basic, measured in inches ' . + '(marked with sign (")) with size 1.5", mid-price range
', + '"text_attribute=
Element type is basic, measured in inches ' . + '(marked with sign ("")) with size 1.5"", mid-price range
"' + ], + ]; + } + + /** + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_data_special_chars.php + * @magentoDbIsolation enabled + * + * @return void + */ + public function testExportSpecialChars(): void + { + $this->model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + $exportData = $this->model->export(); + $this->assertStringContainsString('simple ""1""', $exportData); + $this->assertStringContainsString('Category with slash\/ symbol', $exportData); + } + + /** + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_with_product_links_data.php + * @magentoDbIsolation enabled + * + * @return void + */ + public function testExportWithProductLinks(): void + { + $this->model->setWriter( + \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + $this->assertNotEmpty($this->model->export()); + } + + /** + * Verify that all stock item attribute values are exported (aren't equal to empty string) + * + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @covers \Magento\CatalogImportExport\Model\Export\Product::export + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_data.php + * + * @return void + */ + public function testExportStockItemAttributesAreFilled(): void + { + $this->markTestSkipped('Test needs to be skipped.'); + $fileWrite = $this->createMock(\Magento\Framework\Filesystem\File\Write::class); + $directoryMock = $this->createPartialMock( + \Magento\Framework\Filesystem\Directory\Write::class, + ['getParentDirectory', 'isWritable', 'isFile', 'readFile', 'openFile'] + ); + $directoryMock->expects($this->any())->method('getParentDirectory')->willReturn('some#path'); + $directoryMock->expects($this->any())->method('isWritable')->willReturn(true); + $directoryMock->expects($this->any())->method('isFile')->willReturn(true); + $directoryMock->expects( + $this->any() + )->method( + 'readFile' + )->willReturn( + 'some string read from file' + ); + $directoryMock->expects($this->once())->method('openFile')->willReturn($fileWrite); + + $filesystemMock = $this->createPartialMock(\Magento\Framework\Filesystem::class, ['getDirectoryWrite']); + $filesystemMock->expects($this->once())->method('getDirectoryWrite')->willReturn($directoryMock); + + $exportAdapter = new \Magento\ImportExport\Model\Export\Adapter\Csv($filesystemMock); + + $this->model->setWriter($exportAdapter)->export(); + } + + /** + * Verify header columns (that stock item attributes column headers are present) + * + * @param array $headerColumns + * @return void + */ + public function verifyHeaderColumns(array $headerColumns): void + { + foreach (self::$stockItemAttributes as $stockItemAttribute) { + $this->assertStringContainsString( + $stockItemAttribute, + $headerColumns, + "Stock item attribute {$stockItemAttribute} is absent among header columns" + ); + } + } + + /** + * Verify row data (stock item attribute values) + * + * @param array $rowData + * @return void + */ + public function verifyRow(array $rowData): void + { + foreach (self::$stockItemAttributes as $stockItemAttribute) { + $this->assertNotSame( + '', + $rowData[$stockItemAttribute], + "Stock item attribute {$stockItemAttribute} value is empty string" + ); + } + } + + /** + * Verifies if exception processing works properly + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_data.php + * + * @return void + */ + public function testExceptionInGetExportData(): void + { + $this->markTestSkipped('Test needs to be skipped.'); + $exception = new \Exception('Error'); + + $rowCustomizerMock = + $this->getMockBuilder(\Magento\CatalogImportExport\Model\Export\RowCustomizerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $loggerMock = $this->getMockBuilder(\Psr\Log\LoggerInterface::class)->getMock(); + + $directoryMock = $this->createPartialMock( + \Magento\Framework\Filesystem\Directory\Write::class, + ['getParentDirectory', 'isWritable'] + ); + $directoryMock->expects($this->any())->method('getParentDirectory')->willReturn('some#path'); + $directoryMock->expects($this->any())->method('isWritable')->willReturn(true); + + $filesystemMock = $this->createPartialMock(\Magento\Framework\Filesystem::class, ['getDirectoryWrite']); + $filesystemMock->expects($this->once())->method('getDirectoryWrite')->willReturn($directoryMock); + + $exportAdapter = new \Magento\ImportExport\Model\Export\Adapter\Csv($filesystemMock); + + $rowCustomizerMock->expects($this->once())->method('prepareData')->willThrowException($exception); + $loggerMock->expects($this->once())->method('critical')->with($exception); + + $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Product\Collection::class + ); + + /** @var \Magento\CatalogImportExport\Model\Export\Product $model */ + $model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\CatalogImportExport\Model\Export\Product::class, + [ + 'rowCustomizer' => $rowCustomizerMock, + 'logger' => $loggerMock, + 'collection' => $collection + ] + ); + + $data = $model->setWriter($exportAdapter)->export(); + $this->assertEmpty($data); + } + + /** + * Verify if fields wrapping works correct when "Fields Enclosure" option enabled + * + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_data.php + * + * @return void + */ + public function testExportWithFieldsEnclosure(): void + { + $this->model->setParameters( + [ + \Magento\ImportExport\Model\Export::FIELDS_ENCLOSURE => 1 + ] + ); + + $this->model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + $exportData = $this->model->export(); + + $this->assertStringContainsString('""Option 2""', $exportData); + $this->assertStringContainsString('""Option 3""', $exportData); + $this->assertStringContainsString('""Option 4 """"!@#$%^&*""', $exportData); + $this->assertStringContainsString('text_attribute=""!@#$%^&*()_+1234567890-=|\:;""""\'<,>.?/', $exportData); + } + + /** + * Verify that "category ids" filter correctly applies to export result + * + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_with_categories.php + * + * @return void + */ + public function testCategoryIdsFilter(): void + { + $this->model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + + $this->model->setParameters( + [ + \Magento\ImportExport\Model\Export::FILTER_ELEMENT_GROUP => [ + 'category_ids' => '2,13' + ] + ] + ); + + $exportData = $this->model->export(); + + $this->assertStringContainsString('Simple Product', $exportData); + $this->assertStringContainsString('Simple Product Three', $exportData); + $this->assertStringNotContainsString('Simple Product Two', $exportData); + $this->assertStringNotContainsString('Simple Product Not Visible On Storefront', $exportData); + } + + /** + * Verify that export processed successfully with wrong category path + * + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_with_broken_categories_path.php + * + * @return void + */ + public function testExportWithWrongCategoryPath(): void + { + $this->model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + + $this->model->export(); + } + + /** + * Test 'hide from product page' export for non-default store. + * + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_with_images.php + * + * @return void + */ + public function testExportWithMedia(): void + { + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $product = $productRepository->get('simple', 1); + $mediaGallery = $product->getData('media_gallery'); + $image = array_shift($mediaGallery['images']); + $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_image.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_image.csv')); + foreach ($data[0] as $columnNumber => $columnName) { + if ($columnName === 'hide_from_product_page') { + self::assertSame($image['file'], $data[2][$columnNumber]); + } + } + } + + /** + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_data.php + * + * @return void + */ + public function testExportWithCustomOptions(): void + { + $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')); + $keys = array_shift($data); + $products = []; + foreach ($data as $productData) { + $products[] = array_combine($keys, $productData); + } + $products = array_filter($products, function (array $product) { + return $product['sku'] === 'simple'; + }); + $customOptionData = []; + + foreach ($products as $product) { + $storeCode = $product['store_view_code'] ?: 'admin_store'; + $customOptionData[$storeCode] = $this->parseExportedCustomOption($product['custom_options']); + } + + self::assertSame($expectedData, $customOptionData); + } + + /** + * Check that no duplicate entities when multiple custom options used + * + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_options.php + * + * @return void + */ + public function testExportWithMultipleOptions(): void + { + $expectedCount = 1; + $resultsFilename = 'export_results.csv'; + $this->model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + $exportData = $this->model->export(); + + $varDirectory = $this->objectManager->get(\Magento\Framework\Filesystem::class) + ->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::VAR_DIR); + $varDirectory->writeFile($resultsFilename, $exportData); + /** @var \Magento\Framework\File\Csv $csv */ + $csv = $this->objectManager->get(\Magento\Framework\File\Csv::class); + $data = $csv->getData($varDirectory->getAbsolutePath($resultsFilename)); + $actualCount = count($data) - 1; + + $this->assertSame($expectedCount, $actualCount); + } + + /** + * Parse exported custom options + * + * @param string $exportedCustomOption + * @return array + */ + private function parseExportedCustomOption(string $exportedCustomOption): array + { + $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; + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoConfigFixture current_store catalog/price/scope 1 + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + * + * @return void + */ + public function testExportProductWithTwoWebsites(): void + { + $globalStoreCode = 'admin'; + $secondStoreCode = 'fixture_second_store'; + + $expectedData = [ + $globalStoreCode => 10.0, + $secondStoreCode => 9.99 + ]; + + /** @var \Magento\Store\Model\Store $store */ + $store = $this->objectManager->create(\Magento\Store\Model\Store::class); + $reinitiableConfig = $this->objectManager->get(ReinitableConfigInterface::class); + /** @var \Magento\Catalog\Model\Product\Action $productAction */ + $productAction = $this->objectManager->create(\Magento\Catalog\Model\Product\Action::class); + /** @var \Magento\Framework\File\Csv $csv */ + $csv = $this->objectManager->get(\Magento\Framework\File\Csv::class); + /** @var $varDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ + $varDirectory = $this->objectManager->get(\Magento\Framework\Filesystem::class) + ->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::VAR_DIR); + $secondStore = $store->load($secondStoreCode); + + $this->model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + + $reinitiableConfig->setValue('catalog/price/scope', \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE); + + $product = $this->productRepository->get('simple'); + $productId = $product->getId(); + $productAction->updateWebsites([$productId], [$secondStore->getWebsiteId()], 'add'); + $product->setStoreId($secondStore->getId()); + $product->setPrice('9.99'); + $this->productRepository->save($product); + + $exportData = $this->model->export(); + + $varDirectory->writeFile('test_product_with_two_websites.csv', $exportData); + $data = $csv->getData($varDirectory->getAbsolutePath('test_product_with_two_websites.csv')); + + $columnNumber = array_search('price', $data[0]); + $this->assertNotFalse($columnNumber); + + $pricesData = [ + $globalStoreCode => (float)$data[1][$columnNumber], + $secondStoreCode => (float)$data[2][$columnNumber], + ]; + + self::assertSame($expectedData, $pricesData); + + $reinitiableConfig->setValue('catalog/price/scope', \Magento\Store\Model\Store::PRICE_SCOPE_GLOBAL); + } + + /** + * Verify that "stock status" filter correctly applies to export result + * + * @magentoDataFixture Magento/Catalog/_files/multiple_products_with_few_out_of_stock.php + * @dataProvider filterByQuantityAndStockStatusDataProvider + * + * @param string $value + * @param array $productsIncluded + * @param array $productsNotIncluded + * @return void + */ + public function testFilterByQuantityAndStockStatus( + string $value, + array $productsIncluded, + array $productsNotIncluded + ): void { + $exportData = $this->doExport(['quantity_and_stock_status' => $value]); + foreach ($productsIncluded as $productName) { + $this->assertStringContainsString($productName, $exportData); + } + foreach ($productsNotIncluded as $productName) { + $this->assertStringNotContainsString($productName, $exportData); + } + } + /** + * @return array + */ + public function filterByQuantityAndStockStatusDataProvider(): array + { + return [ + [ + '', + [ + 'Simple Product OOS', + 'Simple Product Not Visible', + 'Simple Product Visible and InStock', + ], + [ + ], + ], + [ + '1', + [ + 'Simple Product Not Visible', + 'Simple Product Visible and InStock', + ], + [ + 'Simple Product OOS', + ], + ], + [ + '0', + [ + 'Simple Product OOS', + ], + [ + 'Simple Product Not Visible', + 'Simple Product Visible and InStock', + ], + ], + ]; + } + + /** + * Test that Product Export takes into account filtering by Website + * + * Fixtures provide two products, one assigned to default website only, + * and the other is assigned to to default and custom websites. Only product assigned custom website is exported + * + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_options.php + * @magentoDataFixture Magento/Catalog/_files/product_with_two_websites.php + */ + public function testExportProductWithRestrictedWebsite(): void + { + $websiteRepository = $this->objectManager->get(\Magento\Store\Api\WebsiteRepositoryInterface::class); + $website = $websiteRepository->get('second_website'); + + $exportData = $this->doExport(['website_id' => $website->getId()]); + + $this->assertStringContainsString('"Simple Product"', $exportData); + $this->assertStringNotContainsString('"Virtual Product With Custom Options"', $exportData); + } + + /** + * Perform export + * + * @param array $filters + * @return string + */ + private function doExport(array $filters = []): string + { + $this->model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + $this->model->setParameters( + [ + \Magento\ImportExport\Model\Export::FILTER_ELEMENT_GROUP => $filters + ] + ); + return $this->model->export(); + } +} diff --git a/app/code/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php b/app/code/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php new file mode 100644 index 0000000000000..19aab846efa05 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php @@ -0,0 +1,122 @@ +requireDataFixture('Magento/Store/_files/second_website_with_store_group_and_store.php'); +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductAttributeRepositoryInterface $productAttributeRepository */ +$productAttributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->get(ProductFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var Factory $optionsFactory */ +$optionsFactory = $objectManager->get(Factory::class); +/** @var ProductExtensionFactory $extensionAttributesFactory */ +$extensionAttributesFactory = $objectManager->get(ProductExtensionFactory::class); +/** @var Config $configResource */ +$configResource = $objectManager->get(Config::class); +/** @var DefaultCategory $categoryHelper */ +$categoryHelper = $objectManager->get(DefaultCategory::class); + +$attribute = $productAttributeRepository->get('test_configurable'); +$options = $attribute->getOptions(); +$baseWebsite = $websiteRepository->get('base'); +$secondWebsite = $websiteRepository->get('test'); +$attributeValues = []; +$associatedProductIds = []; +array_shift($options); +foreach ($options as $option) { + $product = $productFactory->create(); + $product->setTypeId(ProductType::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$baseWebsite->getId(), $secondWebsite->getId()]) + ->setName('Configurable Option ' . $option->getLabel()) + ->setSku(strtolower(str_replace(' ', '_', 'simple ' . $option->getLabel()))) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setPrice(150) + ->setCategoryIds([$categoryHelper->getId(), 333]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product = $productRepository->save($product); + $associatedProductIds[] = $product->getId(); + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; +} +$configurableAttributesData = [ + [ + 'values' => $attributeValues, + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + ], +]; +$configurableOptions = $optionsFactory->create($configurableAttributesData); +$product = $productFactory->create(); +$extensionConfigurableAttributes = $product->getExtensionAttributes() ?: $extensionAttributesFactory->create(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); +$product->setExtensionAttributes($extensionConfigurableAttributes); +$product->setTypeId(Configurable::TYPE_CODE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$baseWebsite->getId(), $secondWebsite->getId()]) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([$categoryHelper->getId(), 333]) + ->setSku('configurable') + ->setName('Configurable Product') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->save($product); + +$configResource->saveConfig(Data::XML_PATH_PRICE_SCOPE, Store::PRICE_SCOPE_WEBSITE, 'default', 0); +$objectManager->get(ReinitableConfigInterface::class)->reinit(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$secondStoreId = $storeManager->getStore('fixture_second_store')->getId(); +try { + $currentStoreCode = $storeManager->getStore()->getCode(); + $storeManager->setCurrentStore('fixture_second_store'); + $firstChild = $productRepository->get('simple_option_1', false, $secondStoreId, true); + $firstChild->setPrice(20) + ->setSpecialPrice(10); + $productRepository->save($firstChild); + $secondChild = $productRepository->get('simple_option_2', false, $secondStoreId, true); + $secondChild->setPrice(40) + ->setSpecialPrice(30); + $productRepository->save($secondChild); +} finally { + $storeManager->setCurrentStore($currentStoreCode); +} diff --git a/app/code/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website_rollback.php b/app/code/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website_rollback.php new file mode 100644 index 0000000000000..c1e2a5f810853 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website_rollback.php @@ -0,0 +1,30 @@ +get(DeleteConfigurableProduct::class); +$deleteConfigurableProduct->execute('configurable'); +/** @var Config $configResource */ +$configResource = $objectManager->get(Config::class); +$configResource->deleteConfig(Data::XML_PATH_PRICE_SCOPE, 'default', 0); +$objectManager->get(ReinitableConfigInterface::class)->reinit(); + +Resolver::getInstance()->requireDataFixture( + 'Magento/Store/_files/second_website_with_store_group_and_store_rollback.php' +); +Resolver::getInstance()->requireDataFixture( + 'Magento/ConfigurableProduct/_files/configurable_attribute_rollback.php' +); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category_rollback.php'); diff --git a/app/code/Magento/Store/_files/second_website_with_base_second_currency.php b/app/code/Magento/Store/_files/second_website_with_base_second_currency.php new file mode 100644 index 0000000000000..26ea436495e9d --- /dev/null +++ b/app/code/Magento/Store/_files/second_website_with_base_second_currency.php @@ -0,0 +1,36 @@ +requireDataFixture('Magento/Store/_files/second_website_with_two_stores.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$websiteId = $websiteRepository->get('test')->getId(); +/** @var \Magento\Config\Model\ResourceModel\Config $configResource */ +$configResource = $objectManager->get(\Magento\Config\Model\ResourceModel\Config::class); +$configResource->saveConfig( + \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, + 'EUR', + \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES, + $websiteId +); +$configResource->saveConfig( + \Magento\Catalog\Helper\Data::XML_PATH_PRICE_SCOPE, + \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE, + 'default', + 0 +); + +/** + * Configuration cache clean is required to reload currency setting + */ +/** @var Magento\Config\App\Config\Type\System $config */ +$config = $objectManager->get(\Magento\Config\App\Config\Type\System::class); +$config->clean();