From 95daeecb67a98502f49cebfef63a1e6dc9b36582 Mon Sep 17 00:00:00 2001 From: Yaroslav Onischenko Date: Tue, 29 Nov 2016 18:55:08 +0200 Subject: [PATCH] MAGETWO-54704: [Backport] [GITHUB] Product prices not scoped to Website - scoping seems to be at store view level #5133 #7251 - for 2.1 --- .../Cron/DeleteOutdatedPriceValues.php | 74 +++++++ .../Model/Product/Attribute/Backend/Price.php | 48 ++--- .../Model/Product/Attribute/Repository.php | 4 - ...witchPriceAttributeScopeOnConfigChange.php | 77 +++++++ .../Magento/Catalog/Setup/UpgradeData.php | 29 ++- .../Product/Attribute/Backend/PriceTest.php | 129 ++++++++--- app/code/Magento/Catalog/etc/crontab.xml | 3 + app/code/Magento/Catalog/etc/events.xml | 3 + app/code/Magento/Catalog/etc/module.xml | 2 +- app/code/Magento/Msrp/Setup/UpgradeData.php | 65 ++++++ app/code/Magento/Msrp/etc/module.xml | 2 +- .../Cron/DeleteOutdatedPriceValuesTest.php | 95 ++++++++ .../Product/Attribute/Backend/PriceTest.php | 204 ++++++++++++++++-- ...hPriceAttributeScopeOnConfigChangeTest.php | 67 ++++++ .../_files/second_website_with_two_stores.php | 63 ++++++ ...econd_website_with_two_stores_rollback.php | 29 +++ .../Framework/App/ReinitableConfig.php | 1 + 17 files changed, 812 insertions(+), 83 deletions(-) create mode 100644 app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php create mode 100644 app/code/Magento/Catalog/Observer/SwitchPriceAttributeScopeOnConfigChange.php create mode 100644 app/code/Magento/Msrp/Setup/UpgradeData.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Cron/DeleteOutdatedPriceValuesTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Observer/SwitchPriceAttributeScopeOnConfigChangeTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores.php create mode 100644 dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php diff --git a/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php b/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php new file mode 100644 index 0000000000000..f9f25c42c6eea --- /dev/null +++ b/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php @@ -0,0 +1,74 @@ +resource = $resource; + $this->attributeRepository = $attributeRepository; + $this->scopeConfig = $scopeConfig; + } + + /** + * Delete all price values for non-admin stores if PRICE_SCOPE is global + * + * @return void + */ + public function execute() + { + $priceScope = $this->scopeConfig->getValue(Store::XML_PATH_PRICE_SCOPE); + if ($priceScope == Store::PRICE_SCOPE_GLOBAL) { + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $priceAttribute */ + $priceAttribute = $this->attributeRepository + ->get(ProductAttributeInterface::ENTITY_TYPE_CODE, ProductAttributeInterface::CODE_PRICE); + $connection = $this->resource->getConnection(); + $conditions = [ + $connection->quoteInto('attribute_id = ?', $priceAttribute->getId()), + $connection->quoteInto('store_id != ?', Store::DEFAULT_STORE_ID), + ]; + + $connection->delete( + $priceAttribute->getBackend()->getTable(), + $conditions + ); + } + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Price.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Price.php index 1cc512aae6031..058b8def8bbbc 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Price.php @@ -5,7 +5,7 @@ */ namespace Magento\Catalog\Model\Product\Attribute\Backend; -use \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; +use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; /** * Catalog product price attribute backend model @@ -102,43 +102,27 @@ public function setScope($attribute) } /** - * After Save Attribute manipulation + * After Save Price Attribute manipulation + * Processes product price attributes if price scoped to website and updates data when: + * * Price changed for non-default store view - will update price for all stores assigned to current website. + * * Price will be changed according to store currency even if price changed in product with default store id. + * * In a case when price was removed for non-default store (use default option checked) the default store price + * * will be used instead * * @param \Magento\Catalog\Model\Product $object * @return $this - * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function afterSave($object) { - $value = $object->getData($this->getAttribute()->getAttributeCode()); - /** - * Orig value is only for existing objects - */ - $oridData = $object->getOrigData(); - $origValueExist = $oridData && array_key_exists($this->getAttribute()->getAttributeCode(), $oridData); - if ($object->getStoreId() != 0 || !$value || $origValueExist) { - return $this; - } - - if ($this->getAttribute()->getIsGlobal() == ScopedAttributeInterface::SCOPE_WEBSITE) { - $baseCurrency = $this->_config->getValue( - \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, - 'default' - ); - - $storeIds = $object->getStoreIds(); - if (is_array($storeIds)) { - foreach ($storeIds as $storeId) { - $storeCurrency = $this->_storeManager->getStore($storeId)->getBaseCurrencyCode(); - if ($storeCurrency == $baseCurrency) { - continue; - } - $rate = $this->_currencyFactory->create()->load($baseCurrency)->getRate($storeCurrency); - if (!$rate) { - $rate = 1; - } - $newValue = $value * $rate; - $object->addAttributeUpdate($this->getAttribute()->getAttributeCode(), $newValue, $storeId); + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = $this->getAttribute(); + $attributeCode = $attribute->getAttributeCode(); + $value = $object->getData($attributeCode); + if ($value && $value != $object->getOrigData($attributeCode)) { + if ($attribute->isScopeWebsite()) { + foreach ((array)$object->getWebsiteStoreIds() as $storeId) { + /** @var $object \Magento\Catalog\Model\Product */ + $object->addAttributeUpdate($attributeCode, $value, $storeId); } } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php index 3d5e49985d27b..669e14be81317 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php @@ -128,10 +128,6 @@ public function save(\Magento\Catalog\Api\Data\ProductAttributeInterface $attrib } $attribute->setDefaultFrontendLabel($frontendLabel); } - if (!$attribute->getIsUserDefined()) { - // Unset attribute field for system attributes - $attribute->setApplyTo(null); - } } else { $attribute->setAttributeId(null); diff --git a/app/code/Magento/Catalog/Observer/SwitchPriceAttributeScopeOnConfigChange.php b/app/code/Magento/Catalog/Observer/SwitchPriceAttributeScopeOnConfigChange.php new file mode 100644 index 0000000000000..31e649bb73e12 --- /dev/null +++ b/app/code/Magento/Catalog/Observer/SwitchPriceAttributeScopeOnConfigChange.php @@ -0,0 +1,77 @@ +config = $config; + $this->productAttributeRepository = $productAttributeRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + } + + /** + * Change scope for all price attributes according to + * 'Catalog Price Scope' configuration parameter value + * + * @param EventObserver $observer + * @return void + */ + public function execute(EventObserver $observer) + { + $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) + ? ProductAttributeInterface::SCOPE_WEBSITE_TEXT + : ProductAttributeInterface::SCOPE_GLOBAL_TEXT; + + $priceAttributes = $this->productAttributeRepository->getList($criteria)->getItems(); + + /** @var ProductAttributeInterface $priceAttribute */ + foreach ($priceAttributes as $priceAttribute) { + $priceAttribute->setScope($scope); + $this->productAttributeRepository->save($priceAttribute); + } + } +} diff --git a/app/code/Magento/Catalog/Setup/UpgradeData.php b/app/code/Magento/Catalog/Setup/UpgradeData.php index ab32e8c5a6c7d..f61e28a10bce3 100644 --- a/app/code/Magento/Catalog/Setup/UpgradeData.php +++ b/app/code/Magento/Catalog/Setup/UpgradeData.php @@ -340,7 +340,7 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface } if (version_compare($context->getVersion(), '2.0.7') < 0) { - /** @var EavSetup $eavSetupF */ + /** @var EavSetup $eavSetup */ $eavSetup= $this->eavSetupFactory->create(['setup' => $setup]); $eavSetup->updateAttribute( @@ -351,7 +351,32 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface ] ); } - + + if (version_compare($context->getVersion(), '2.1.3') < 0) { + /** @var \Magento\Catalog\Setup\CategorySetup $categorySetup */ + $categorySetup = $this->categorySetupFactory->create(['setup' => $setup]); + $this->changePriceAttributeDefaultScope($categorySetup); + } + $setup->endSetup(); } + + /** + * @param \Magento\Catalog\Setup\CategorySetup $categorySetup + * @return void + */ + private function changePriceAttributeDefaultScope($categorySetup) + { + $entityTypeId = $categorySetup->getEntityTypeId(\Magento\Catalog\Model\Product::ENTITY); + foreach (['price', 'cost', 'special_price'] as $attributeCode) { + $attribute = $categorySetup->getAttribute($entityTypeId, $attributeCode); + $categorySetup->updateAttribute( + $entityTypeId, + $attribute['attribute_id'], + 'is_global', + \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL + ); + + } + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/PriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/PriceTest.php index 8dc8817786545..722cc1819029b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/PriceTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/PriceTest.php @@ -5,43 +5,57 @@ */ namespace Magento\Catalog\Test\Unit\Model\Product\Attribute\Backend; +/** + * Class PriceTest + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class PriceTest extends \PHPUnit_Framework_TestCase { /** * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Price */ - protected $model; + private $model; + + /** + * @var \Magento\Eav\Model\Entity\Attribute\AbstractAttribute|\PHPUnit_Framework_MockObject_MockObject + */ + private $attribute; + + /** + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + + /** + * @var \Magento\Catalog\Model\Attribute\ScopeOverriddenValue|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeOverriddenValue; protected function setUp() { $objectHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - // we want to use an actual implementation of \Magento\Framework\Locale\FormatInterface - $scopeResolver = $this->getMockForAbstractClass('\Magento\Framework\App\ScopeResolverInterface', [], '', false); - $localeResolver = $this->getMockForAbstractClass('\Magento\Framework\Locale\ResolverInterface', [], '', false); - $currencyFactory = $this->getMock('\Magento\Directory\Model\CurrencyFactory', [], [], '', false); - $localeFormat = $objectHelper->getObject( - 'Magento\Framework\Locale\Format', + $localeFormat = $objectHelper->getObject(\Magento\Framework\Locale\Format::class); + $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->getMockForAbstractClass(); + $this->scopeOverriddenValue = $this->getMockBuilder( + \Magento\Catalog\Model\Attribute\ScopeOverriddenValue::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->model = $objectHelper->getObject( + \Magento\Catalog\Model\Product\Attribute\Backend\Price::class, [ - 'scopeResolver' => $scopeResolver, - 'localeResolver' => $localeResolver, - 'currencyFactory' => $currencyFactory, + 'localeFormat' => $localeFormat, + 'storeManager' => $this->storeManager, + 'scopeOverriddenValue' => $this->scopeOverriddenValue ] ); - // the model we are testing - $this->model = $objectHelper->getObject( - 'Magento\Catalog\Model\Product\Attribute\Backend\Price', - ['localeFormat' => $localeFormat] - ); - - $attribute = $this->getMockForAbstractClass( - '\Magento\Eav\Model\Entity\Attribute\AbstractAttribute', - [], - '', - false - ); - $this->model->setAttribute($attribute); + $this->attribute = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) + ->setMethods(['getAttributeCode', 'isScopeWebsite']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->model->setAttribute($this->attribute); } /** @@ -81,7 +95,7 @@ public function dataProviderValidate() */ public function testValidateForFailure($value) { - $object = $this->getMock('Magento\Catalog\Model\Product', [], [], '', false); + $object = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); $object->expects($this->once())->method('getData')->willReturn($value); $this->model->validate($object); @@ -101,4 +115,69 @@ public function dataProviderValidateForFailure() 'negative Lebanon' => ['-1 234'], ]; } + + public function testAfterSaveWithDifferentStores() + { + $newPrice = '9.99'; + $attributeCode = 'price'; + $defaultStoreId = 0; + $websiteStoreIds = [1, 2, 3]; + $object = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)->disableOriginalConstructor()->getMock(); + $object->expects($this->any())->method('getData')->with($attributeCode)->willReturn($newPrice); + $object->expects($this->any())->method('getOrigData')->with($attributeCode)->willReturn('7.77'); + $object->expects($this->any())->method('getStoreId')->willReturn($defaultStoreId); + $object->expects($this->any())->method('getWebsiteStoreIds')->willReturn($websiteStoreIds); + $this->attribute->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode); + $this->attribute->expects($this->any())->method('isScopeWebsite') + ->willReturn(\Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_WEBSITE); + + $object->expects($this->any())->method('addAttributeUpdate')->withConsecutive( + [ + $this->equalTo($attributeCode), + $this->equalTo($newPrice), + $this->equalTo($websiteStoreIds[0]) + ], + [ + $this->equalTo($attributeCode), + $this->equalTo($newPrice), + $this->equalTo($websiteStoreIds[1]) + ], + [ + $this->equalTo($attributeCode), + $this->equalTo($newPrice), + $this->equalTo($websiteStoreIds[2]) + ] + ); + $this->assertEquals($this->model, $this->model->afterSave($object)); + } + + public function testAfterSaveWithOldPrice() + { + $attributeCode = 'price'; + + $object = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)->disableOriginalConstructor()->getMock(); + $object->expects($this->any())->method('getData')->with($attributeCode)->willReturn('7.77'); + $object->expects($this->any())->method('getOrigData')->with($attributeCode)->willReturn('7.77'); + $this->attribute->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode); + $this->attribute->expects($this->any())->method('getIsGlobal') + ->willReturn(\Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_WEBSITE); + + $object->expects($this->never())->method('addAttributeUpdate'); + $this->assertEquals($this->model, $this->model->afterSave($object)); + } + + public function testAfterSaveWithGlobalPrice() + { + $attributeCode = 'price'; + + $object = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)->disableOriginalConstructor()->getMock(); + $object->expects($this->any())->method('getData')->with($attributeCode)->willReturn('9.99'); + $object->expects($this->any())->method('getOrigData')->with($attributeCode)->willReturn('7.77'); + $this->attribute->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode); + $this->attribute->expects($this->any())->method('getIsGlobal') + ->willReturn(\Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL); + + $object->expects($this->never())->method('addAttributeUpdate'); + $this->assertEquals($this->model, $this->model->afterSave($object)); + } } diff --git a/app/code/Magento/Catalog/etc/crontab.xml b/app/code/Magento/Catalog/etc/crontab.xml index 2288ed4ebd8a8..b7d3b40de7cfe 100644 --- a/app/code/Magento/Catalog/etc/crontab.xml +++ b/app/code/Magento/Catalog/etc/crontab.xml @@ -13,5 +13,8 @@ 0 0 * * * + + * * * * * + diff --git a/app/code/Magento/Catalog/etc/events.xml b/app/code/Magento/Catalog/etc/events.xml index 544abf6b9e069..9d618b9a4653c 100644 --- a/app/code/Magento/Catalog/etc/events.xml +++ b/app/code/Magento/Catalog/etc/events.xml @@ -51,4 +51,7 @@ + + + diff --git a/app/code/Magento/Catalog/etc/module.xml b/app/code/Magento/Catalog/etc/module.xml index 87e82543fc65b..b33181d063f6a 100644 --- a/app/code/Magento/Catalog/etc/module.xml +++ b/app/code/Magento/Catalog/etc/module.xml @@ -6,7 +6,7 @@ */ --> - + diff --git a/app/code/Magento/Msrp/Setup/UpgradeData.php b/app/code/Magento/Msrp/Setup/UpgradeData.php new file mode 100644 index 0000000000000..fef6dc192f97e --- /dev/null +++ b/app/code/Magento/Msrp/Setup/UpgradeData.php @@ -0,0 +1,65 @@ +categorySetupFactory = $categorySetupFactory; + } + + /** + * {@inheritdoc} + */ + public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context) + { + /** @var \Magento\Catalog\Setup\CategorySetup $categorySetup */ + $categorySetup = $this->categorySetupFactory->create(['setup' => $setup]); + $entityTypeId = $categorySetup->getEntityTypeId(\Magento\Catalog\Model\Product::ENTITY); + + if (version_compare($context->getVersion(), '2.1.3', '<')) { + $this->changePriceAttributeDefaultScope($categorySetup, $entityTypeId); + } + $setup->endSetup(); + } + + /** + * @param \Magento\Catalog\Setup\CategorySetup $categorySetup + * @param int $entityTypeId + * @return void + */ + private function changePriceAttributeDefaultScope($categorySetup, $entityTypeId) + { + $attribute = $categorySetup->getAttribute($entityTypeId, 'msrp'); + $categorySetup->updateAttribute( + $entityTypeId, + $attribute['attribute_id'], + 'is_global', + \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL + ); + + } +} diff --git a/app/code/Magento/Msrp/etc/module.xml b/app/code/Magento/Msrp/etc/module.xml index 16265db3fcbb4..1232c3422bd5f 100644 --- a/app/code/Magento/Msrp/etc/module.xml +++ b/app/code/Magento/Msrp/etc/module.xml @@ -6,7 +6,7 @@ */ --> - + diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Cron/DeleteOutdatedPriceValuesTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Cron/DeleteOutdatedPriceValuesTest.php new file mode 100644 index 0000000000000..de4129ddfaa5e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Cron/DeleteOutdatedPriceValuesTest.php @@ -0,0 +1,95 @@ +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 2 + * @magentoDbIsolation enabled + */ + public function testExecute() + { + $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 + ); + + $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->setOrigData(); + $product->setStoreId($secondStoreId); + $product->setPrice(9.99); + + $productResource->save($product); + $attribute = $this->objectManager->get(\Magento\Eav\Model\Config::class) + ->getAttribute( + 'catalog_product', + 'price' + ); + $this->assertEquals( + '9.99', + $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, + \Magento\Store\Model\Store::PRICE_SCOPE_GLOBAL, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ); + /** @var \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig */ + $this->cron->execute(); + $this->assertEquals( + '10.0000', + $productResource->getAttributeRawValue($productId, $attribute->getId(), $secondStoreId) + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/PriceTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/PriceTest.php index fb746835d7b38..1153a92ff6ea9 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/PriceTest.php @@ -8,23 +8,30 @@ /** * Test class for \Magento\Catalog\Model\Product\Attribute\Backend\Price. * - * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml */ class PriceTest extends \PHPUnit_Framework_TestCase { /** * @var \Magento\Catalog\Model\Product\Attribute\Backend\Price */ - protected $_model; + private $model; + + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; protected function setUp() { - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Catalog\Model\Product\Attribute\Backend\Price' + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->model = $this->objectManager->create( + \Magento\Catalog\Model\Product\Attribute\Backend\Price::class ); - $this->_model->setAttribute( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - 'Magento\Eav\Model\Config' + $this->model->setAttribute( + $this->objectManager->get( + \Magento\Eav\Model\Config::class )->getAttribute( 'catalog_product', 'price' @@ -37,50 +44,211 @@ public function testSetScopeDefault() /* validate result of setAttribute */ $this->assertEquals( \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL, - $this->_model->getAttribute()->getIsGlobal() + $this->model->getAttribute()->getIsGlobal() ); - $this->_model->setScope($this->_model->getAttribute()); + $this->model->setScope($this->model->getAttribute()); $this->assertEquals( \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL, - $this->_model->getAttribute()->getIsGlobal() + $this->model->getAttribute()->getIsGlobal() ); } /** + * @magentoDbIsolation enabled * @magentoConfigFixture current_store catalog/price/scope 1 */ public function testSetScope() { - $this->_model->setScope($this->_model->getAttribute()); + $this->model->setScope($this->model->getAttribute()); $this->assertEquals( \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_WEBSITE, - $this->_model->getAttribute()->getIsGlobal() + $this->model->getAttribute()->getIsGlobal() ); } /** + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @magentoConfigFixture current_store catalog/price/scope 1 * @magentoConfigFixture current_store currency/options/base GBP */ public function testAfterSave() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Catalog\Model\ProductRepository' + $repository = $this->objectManager->create( + \Magento\Catalog\Model\ProductRepository::class ); $product = $repository->get('simple'); $product->setOrigData(); $product->setPrice(9.99); $product->setStoreId(0); - $product->save(); + $repository->save($product); $this->assertEquals( '9.99', $product->getResource()->getAttributeRawValue( $product->getId(), - $this->_model->getAttribute()->getId(), - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - 'Magento\Store\Model\StoreManagerInterface' + $this->model->getAttribute()->getId(), + $this->objectManager->get( + \Magento\Store\Model\StoreManagerInterface::class )->getStore()->getId() ) ); } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoConfigFixture current_store catalog/price/scope 2 + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + */ + public function testAfterSaveWithDifferentStores() + { + $repository = $this->objectManager->create( + \Magento\Catalog\Model\ProductRepository::class + ); + /** @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 = $repository->get('simple'); + $productId = $product->getId(); + $productResource = $product->getResource(); + $productAction->updateWebsites([$productId], [$store->load('fixture_second_store')->getWebsiteId()], 'add'); + $product->setOrigData(); + $product->setStoreId($secondStoreId); + $product->setPrice(9.99); + $productResource->save($product); + + $this->assertEquals( + '10.00', + $productResource->getAttributeRawValue($productId, $this->model->getAttribute()->getId(), $globalStoreId) + ); + $this->assertEquals( + '9.99', + $productResource->getAttributeRawValue($productId, $this->model->getAttribute()->getId(), $secondStoreId) + ); + $this->assertEquals( + '9.99', + $productResource->getAttributeRawValue($productId, $this->model->getAttribute()->getId(), $thirdStoreId) + ); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoConfigFixture current_store catalog/price/scope 2 + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + */ + public function testAfterSaveWithSameCurrency() + { + $repository = $this->objectManager->create( + \Magento\Catalog\Model\ProductRepository::class + ); + /** @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 = $repository->get('simple'); + $productId = $product->getId(); + $productResource = $product->getResource(); + $productAction->updateWebsites([$productId], [$store->load('fixture_second_store')->getWebsiteId()], 'add'); + $product->setOrigData(); + $product->setStoreId($secondStoreId); + $product->setPrice(9.99); + $productResource->save($product); + + $this->assertEquals( + '10.00', + $productResource->getAttributeRawValue($productId, $this->model->getAttribute()->getId(), $globalStoreId) + ); + $this->assertEquals( + '9.99', + $productResource->getAttributeRawValue($productId, $this->model->getAttribute()->getId(), $secondStoreId) + ); + $this->assertEquals( + '9.99', + $productResource->getAttributeRawValue($productId, $this->model->getAttribute()->getId(), $thirdStoreId) + ); + } + + /** + * @magentoDbIsolation enabled + * @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 2 + */ + public function testAfterSaveWithUseDefault() + { + $repository = $this->objectManager->create( + \Magento\Catalog\Model\ProductRepository::class + ); + /** @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 = $repository->get('simple'); + $productId = $product->getId(); + $productResource = $product->getResource(); + $productAction->updateWebsites([$productId], [$store->load('fixture_second_store')->getWebsiteId()], 'add'); + $product->setOrigData(); + $product->setStoreId($secondStoreId); + $product->setPrice(9.99); + $productResource->save($product); + + $this->assertEquals( + '10.00', + $productResource->getAttributeRawValue($productId, $this->model->getAttribute()->getId(), $globalStoreId) + ); + $this->assertEquals( + '9.99', + $productResource->getAttributeRawValue($productId, $this->model->getAttribute()->getId(), $secondStoreId) + ); + $this->assertEquals( + '9.99', + $productResource->getAttributeRawValue($productId, $this->model->getAttribute()->getId(), $thirdStoreId) + ); + + $product->setStoreId($thirdStoreId); + $product->setPrice(null); + $productResource->save($product); + + $this->assertEquals( + '10.00', + $productResource->getAttributeRawValue($productId, $this->model->getAttribute()->getId(), $globalStoreId) + ); + $this->assertEquals( + '9.99', + $productResource->getAttributeRawValue($productId, $this->model->getAttribute()->getId(), $secondStoreId) + ); + $this->assertEquals( + '10.00', + $productResource->getAttributeRawValue($productId, $this->model->getAttribute()->getId(), $thirdStoreId) + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Observer/SwitchPriceAttributeScopeOnConfigChangeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Observer/SwitchPriceAttributeScopeOnConfigChangeTest.php new file mode 100644 index 0000000000000..0de309a8dcf99 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Observer/SwitchPriceAttributeScopeOnConfigChangeTest.php @@ -0,0 +1,67 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + */ + public function testPriceAttributeHasScopeGlobal() + { + foreach (['price', 'cost', 'special_price'] as $attributeCode) { + $attribute = $this->objectManager->get(\Magento\Eav\Model\Config::class)->getAttribute( + 'catalog_product', + $attributeCode + ); + $this->assertTrue($attribute->isScopeGlobal()); + } + } + + /** + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + */ + public function testPriceAttributeHasScopeWebsite() + { + /** @var ReinitableConfigInterface $config */ + $config = $this->objectManager->get( + ReinitableConfigInterface::class + ); + $config->setValue( + \Magento\Store\Model\Store::XML_PATH_PRICE_SCOPE, + \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ); + + $eventManager = $this->objectManager->get(\Magento\Framework\Event\ManagerInterface::class); + $eventManager->dispatch( + "admin_system_config_changed_section_catalog", + ['website' => 0, 'store' => 0] + ); + foreach (['price', 'cost', 'special_price'] as $attributeCode) { + $attribute = $this->objectManager->get(\Magento\Eav\Model\Config::class)->getAttribute( + 'catalog_product', + $attributeCode + ); + $this->assertTrue($attribute->isScopeWebsite()); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores.php new file mode 100644 index 0000000000000..385b6452efba8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores.php @@ -0,0 +1,63 @@ +create(\Magento\Store\Model\Website::class); +/** @var $website \Magento\Store\Model\Website */ +if (!$website->load('test', 'code')->getId()) { + $website->setData(['code' => 'test', 'name' => 'Test Website', 'default_group_id' => '1', 'is_default' => '0']); + $website->save(); +} +$websiteId = $website->getId(); +$store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); +if (!$store->load('fixture_second_store', 'code')->getId()) { + $groupId = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Store\Model\StoreManagerInterface::class + )->getWebsite()->getDefaultGroupId(); + $store->setCode( + 'fixture_second_store' + )->setWebsiteId( + $websiteId + )->setGroupId( + $groupId + )->setName( + 'Fixture Second Store' + )->setSortOrder( + 10 + )->setIsActive( + 1 + ); + $store->save(); +} + +$store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); +if (!$store->load('fixture_third_store', 'code')->getId()) { + $groupId = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Store\Model\StoreManagerInterface::class + )->getWebsite()->getDefaultGroupId(); + $store->setCode( + 'fixture_third_store' + )->setWebsiteId( + $websiteId + )->setGroupId( + $groupId + )->setName( + 'Fixture Third Store' + )->setSortOrder( + 11 + )->setIsActive( + 1 + ); + $store->save(); +} +/* Refresh stores memory cache */ +\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Store\Model\StoreManagerInterface::class +)->reinitStores(); + +/* Refresh CatalogSearch index */ +/** @var \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry */ +$indexerRegistry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Framework\Indexer\IndexerRegistry::class); +$indexerRegistry->get(\Magento\CatalogSearch\Model\Indexer\Fulltext::INDEXER_ID)->reindexAll(); diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php new file mode 100644 index 0000000000000..c6d03a1fe4556 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php @@ -0,0 +1,29 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$website = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Website::class); +/** @var $website \Magento\Store\Model\Website */ +if ($website->load('test', 'code')->getId()) { + $website->delete(); +} +$websiteId = $website->getId(); +$store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); +if ($store->load('fixture_second_store', 'code')->getId()) { + $store->delete(); +} + +$store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); +if ($store->load('fixture_third_store', 'code')->getId()) { + $store->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/lib/internal/Magento/Framework/App/ReinitableConfig.php b/lib/internal/Magento/Framework/App/ReinitableConfig.php index b98ede77fe133..3b75d163ff154 100644 --- a/lib/internal/Magento/Framework/App/ReinitableConfig.php +++ b/lib/internal/Magento/Framework/App/ReinitableConfig.php @@ -18,6 +18,7 @@ class ReinitableConfig extends MutableScopeConfig implements ReinitableConfigInt public function reinit() { $this->_scopePool->clean(); + $this->clean(); return $this; } }