Skip to content

Commit a8dc219

Browse files
author
Yaroslav Onischenko
committed
MAGETWO-56978: [GITHUB] Product prices not scoped to Website - scoping seems to be at store view level magento#5133 magento#7251
1 parent 1856c28 commit a8dc219

File tree

29 files changed

+1252
-114
lines changed

29 files changed

+1252
-114
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
/**
3+
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
namespace Magento\Catalog\Cron;
7+
8+
use Magento\Framework\App\ResourceConnection;
9+
use Magento\Eav\Api\AttributeRepositoryInterface as AttributeRepository;
10+
use Magento\Framework\App\Config\MutableScopeConfigInterface as ScopeConfig;
11+
use Magento\Catalog\Api\Data\ProductAttributeInterface;
12+
use Magento\Store\Model\Store;
13+
14+
/**
15+
* Cron operation is responsible for deleting all product prices on WEBSITE level
16+
* in case 'Catalog Price Scope' configuratoin parameter is set to GLOBAL.
17+
*/
18+
class DeleteOutdatedPriceValues
19+
{
20+
/**
21+
* @var ResourceConnection
22+
*/
23+
private $resource;
24+
25+
/**
26+
* @var AttributeRepository
27+
*/
28+
private $attributeRepository;
29+
30+
/**
31+
* @var ScopeConfig
32+
*/
33+
private $scopeConfig;
34+
35+
/**
36+
* @param ResourceConnection $resource
37+
* @param AttributeRepository $attributeRepository
38+
* @param ScopeConfig $scopeConfig
39+
*/
40+
public function __construct(
41+
ResourceConnection $resource,
42+
AttributeRepository $attributeRepository,
43+
ScopeConfig $scopeConfig
44+
) {
45+
$this->resource = $resource;
46+
$this->attributeRepository = $attributeRepository;
47+
$this->scopeConfig = $scopeConfig;
48+
}
49+
50+
/**
51+
* Delete all price values for non-admin stores if PRICE_SCOPE is global
52+
*
53+
* @return void
54+
*/
55+
public function execute()
56+
{
57+
$priceScope = $this->scopeConfig->getValue(Store::XML_PATH_PRICE_SCOPE);
58+
if ($priceScope == Store::PRICE_SCOPE_GLOBAL) {
59+
/** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $priceAttribute */
60+
$priceAttribute = $this->attributeRepository
61+
->get(ProductAttributeInterface::ENTITY_TYPE_CODE, ProductAttributeInterface::CODE_PRICE);
62+
$connection = $this->resource->getConnection();
63+
$conditions = [
64+
$connection->quoteInto('attribute_id = ?', $priceAttribute->getId()),
65+
$connection->quoteInto('store_id != ?', Store::DEFAULT_STORE_ID),
66+
];
67+
68+
$connection->delete(
69+
$priceAttribute->getBackend()->getTable(),
70+
$conditions
71+
);
72+
}
73+
}
74+
}

app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
namespace Magento\Catalog\Model\Product\Attribute\Backend\GroupPrice;
1010

1111
use Magento\Catalog\Api\Data\ProductInterface;
12+
use Magento\Catalog\Model\Attribute\ScopeOverriddenValue;
1213
use Magento\Catalog\Model\Product\Attribute\Backend\Price;
1314
use Magento\Customer\Api\GroupManagementInterface;
1415

@@ -59,6 +60,7 @@ abstract protected function _getDuplicateErrorMessage();
5960
* @param \Magento\Framework\Locale\FormatInterface $localeFormat
6061
* @param \Magento\Catalog\Model\Product\Type $catalogProductType
6162
* @param GroupManagementInterface $groupManagement
63+
* @param ScopeOverriddenValue|null $scopeOverriddenValue
6264
*/
6365
public function __construct(
6466
\Magento\Directory\Model\CurrencyFactory $currencyFactory,
@@ -67,11 +69,19 @@ public function __construct(
6769
\Magento\Framework\App\Config\ScopeConfigInterface $config,
6870
\Magento\Framework\Locale\FormatInterface $localeFormat,
6971
\Magento\Catalog\Model\Product\Type $catalogProductType,
70-
GroupManagementInterface $groupManagement
72+
GroupManagementInterface $groupManagement,
73+
ScopeOverriddenValue $scopeOverriddenValue = null
7174
) {
7275
$this->_catalogProductType = $catalogProductType;
7376
$this->_groupManagement = $groupManagement;
74-
parent::__construct($currencyFactory, $storeManager, $catalogData, $config, $localeFormat);
77+
parent::__construct(
78+
$currencyFactory,
79+
$storeManager,
80+
$catalogData,
81+
$config,
82+
$localeFormat,
83+
$scopeOverriddenValue
84+
);
7585
}
7686

7787
/**

app/code/Magento/Catalog/Model/Product/Attribute/Backend/Price.php

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
namespace Magento\Catalog\Model\Product\Attribute\Backend;
77

88
use \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface;
9+
use Magento\Catalog\Model\Attribute\ScopeOverriddenValue;
910

1011
/**
11-
* Catalog product price attribute backend model
12+
* Backend model for set of EAV attributes with 'frontend_input' equals 'price'.
1213
*
1314
* @author Magento Core Team <core@magentocommerce.com>
1415
*/
@@ -48,26 +49,33 @@ class Price extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend
4849
protected $localeFormat;
4950

5051
/**
51-
* Construct
52-
*
52+
* @var \Magento\Catalog\Model\Attribute\ScopeOverriddenValue
53+
*/
54+
private $scopeOverriddenValue;
55+
56+
/**
5357
* @param \Magento\Directory\Model\CurrencyFactory $currencyFactory
5458
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
5559
* @param \Magento\Catalog\Helper\Data $catalogData
5660
* @param \Magento\Framework\App\Config\ScopeConfigInterface $config
5761
* @param \Magento\Framework\Locale\FormatInterface $localeFormat
62+
* @param ScopeOverriddenValue|null $scopeOverriddenValue
5863
*/
5964
public function __construct(
6065
\Magento\Directory\Model\CurrencyFactory $currencyFactory,
6166
\Magento\Store\Model\StoreManagerInterface $storeManager,
6267
\Magento\Catalog\Helper\Data $catalogData,
6368
\Magento\Framework\App\Config\ScopeConfigInterface $config,
64-
\Magento\Framework\Locale\FormatInterface $localeFormat
69+
\Magento\Framework\Locale\FormatInterface $localeFormat,
70+
ScopeOverriddenValue $scopeOverriddenValue = null
6571
) {
6672
$this->_currencyFactory = $currencyFactory;
6773
$this->_storeManager = $storeManager;
6874
$this->_helper = $catalogData;
6975
$this->_config = $config;
7076
$this->localeFormat = $localeFormat;
77+
$this->scopeOverriddenValue = $scopeOverriddenValue
78+
?: \Magento\Framework\App\ObjectManager::getInstance()->get(ScopeOverriddenValue::class);
7179
}
7280

7381
/**
@@ -102,50 +110,52 @@ public function setScope($attribute)
102110
}
103111

104112
/**
105-
* After Save Attribute manipulation
113+
* After Save Price Attribute manipulation
114+
* Processes product price attributes if price scoped to website and updates data when:
115+
* * Price changed for non-default store view - will update price for all stores assigned to current website.
116+
* * Price will be changed according to store currency even if price changed in product with default store id.
117+
* * In a case when price was removed for non-default store (use default option checked) the default store price
118+
* * will be used instead
106119
*
107120
* @param \Magento\Catalog\Model\Product $object
108121
* @return $this
109-
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
110122
*/
111123
public function afterSave($object)
112124
{
113-
$value = $object->getData($this->getAttribute()->getAttributeCode());
114-
/**
115-
* Orig value is only for existing objects
116-
*/
117-
$oridData = $object->getOrigData();
118-
$origValueExist = $oridData && array_key_exists($this->getAttribute()->getAttributeCode(), $oridData);
119-
if ($object->getStoreId() != 0 || !$value || $origValueExist) {
120-
return $this;
121-
}
122-
123-
if ($this->getAttribute()->getIsGlobal() == ScopedAttributeInterface::SCOPE_WEBSITE) {
124-
$baseCurrency = $this->_config->getValue(
125-
\Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE,
126-
'default'
127-
);
128-
129-
$storeIds = $object->getStoreIds();
130-
if (is_array($storeIds)) {
131-
foreach ($storeIds as $storeId) {
132-
$storeCurrency = $this->_storeManager->getStore($storeId)->getBaseCurrencyCode();
133-
if ($storeCurrency == $baseCurrency) {
134-
continue;
135-
}
136-
$rate = $this->_currencyFactory->create()->load($baseCurrency)->getRate($storeCurrency);
137-
if (!$rate) {
138-
$rate = 1;
139-
}
140-
$newValue = $value * $rate;
141-
$object->addAttributeUpdate($this->getAttribute()->getAttributeCode(), $newValue, $storeId);
125+
/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */
126+
$attribute = $this->getAttribute();
127+
$attributeCode = $attribute->getAttributeCode();
128+
$value = $object->getData($attributeCode);
129+
if ((float)$value > 0) {
130+
if ($attribute->isScopeWebsite() && $object->getStoreId() != \Magento\Store\Model\Store::DEFAULT_STORE_ID) {
131+
if ($this->isUseDefault($object)) {
132+
$value = null;
133+
}
134+
foreach ((array)$object->getWebsiteStoreIds() as $storeId) {
135+
$object->addAttributeUpdate($attributeCode, $value, $storeId);
142136
}
143137
}
144138
}
145139

146140
return $this;
147141
}
148142

143+
/**
144+
* Check whether product uses default attribute's value in selected scope
145+
* @param \Magento\Catalog\Model\Product $object
146+
* @return bool
147+
*/
148+
private function isUseDefault($object)
149+
{
150+
$overridden = $this->scopeOverriddenValue->containsValue(
151+
\Magento\Catalog\Api\Data\ProductInterface::class,
152+
$object,
153+
$this->getAttribute()->getAttributeCode(),
154+
$object->getStoreId()
155+
);
156+
return !$overridden;
157+
}
158+
149159
/**
150160
* Validate
151161
*

app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
*/
1212
namespace Magento\Catalog\Model\Product\Attribute\Backend;
1313

14+
use Magento\Catalog\Model\Attribute\ScopeOverriddenValue;
15+
1416
class Tierprice extends \Magento\Catalog\Model\Product\Attribute\Backend\GroupPrice\AbstractGroupPrice
1517
{
1618
/**
@@ -29,6 +31,7 @@ class Tierprice extends \Magento\Catalog\Model\Product\Attribute\Backend\GroupPr
2931
* @param \Magento\Catalog\Model\Product\Type $catalogProductType
3032
* @param \Magento\Customer\Api\GroupManagementInterface $groupManagement
3133
* @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice $productAttributeTierprice
34+
* @param ScopeOverriddenValue|null $scopeOverriddenValue
3235
*/
3336
public function __construct(
3437
\Magento\Directory\Model\CurrencyFactory $currencyFactory,
@@ -38,7 +41,8 @@ public function __construct(
3841
\Magento\Framework\Locale\FormatInterface $localeFormat,
3942
\Magento\Catalog\Model\Product\Type $catalogProductType,
4043
\Magento\Customer\Api\GroupManagementInterface $groupManagement,
41-
\Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice $productAttributeTierprice
44+
\Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice $productAttributeTierprice,
45+
ScopeOverriddenValue $scopeOverriddenValue = null
4246
) {
4347
$this->_productAttributeBackendTierprice = $productAttributeTierprice;
4448
parent::__construct(
@@ -48,7 +52,8 @@ public function __construct(
4852
$config,
4953
$localeFormat,
5054
$catalogProductType,
51-
$groupManagement
55+
$groupManagement,
56+
$scopeOverriddenValue
5257
);
5358
}
5459

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,6 @@ public function save(\Magento\Catalog\Api\Data\ProductAttributeInterface $attrib
134134
}
135135
$attribute->setDefaultFrontendLabel($frontendLabel);
136136
}
137-
if (!$attribute->getIsUserDefined()) {
138-
// Unset attribute field for system attributes
139-
$attribute->setApplyTo(null);
140-
}
141137
} else {
142138
$attribute->setAttributeId(null);
143139

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,4 +645,17 @@ private function getProductCategoryLink()
645645
}
646646
return $this->productCategoryLink;
647647
}
648+
649+
/**
650+
* Extends parent method to be appropriate for product.
651+
* Store id is required to correctly identify attribute value we are working with.
652+
*
653+
* {@inheritdoc}
654+
*/
655+
protected function getAttributeRow($entity, $object, $attribute)
656+
{
657+
$data = parent::getAttributeRow($entity, $object, $attribute);
658+
$data['store_id'] = $object->getStoreId();
659+
return $data;
660+
}
648661
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
/**
3+
* Copyright © 2013-2017 Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
namespace Magento\Catalog\Observer;
7+
8+
use Magento\Framework\Event\Observer as EventObserver;
9+
use Magento\Framework\Event\ObserverInterface;
10+
use Magento\Catalog\Api\Data\ProductAttributeInterface;
11+
use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
12+
use Magento\Store\Model\Store;
13+
use Magento\Framework\App\Config\ReinitableConfigInterface;
14+
use Magento\Framework\Api\SearchCriteriaBuilder;
15+
16+
/**
17+
* Observer is responsible for changing scope for all price attributes in system
18+
* depending on 'Catalog Price Scope' configuration parameter
19+
*/
20+
class SwitchPriceAttributeScopeOnConfigChange implements ObserverInterface
21+
{
22+
/**
23+
* @var ReinitableConfigInterface
24+
*/
25+
private $config;
26+
27+
/**
28+
* @var ProductAttributeRepositoryInterface
29+
*/
30+
private $productAttributeRepository;
31+
32+
/**
33+
* @var SearchCriteriaBuilder
34+
*/
35+
private $searchCriteriaBuilder;
36+
37+
/**
38+
* @param ReinitableConfigInterface $config
39+
* @param ProductAttributeRepositoryInterface $productAttributeRepository
40+
* @param SearchCriteriaBuilder $searchCriteriaBuilder
41+
*/
42+
public function __construct(
43+
ReinitableConfigInterface $config,
44+
ProductAttributeRepositoryInterface $productAttributeRepository,
45+
SearchCriteriaBuilder $searchCriteriaBuilder
46+
) {
47+
$this->config = $config;
48+
$this->productAttributeRepository = $productAttributeRepository;
49+
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
50+
}
51+
52+
/**
53+
* Change scope for all price attributes according to
54+
* 'Catalog Price Scope' configuration parameter value
55+
*
56+
* @param EventObserver $observer
57+
* @return void
58+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
59+
*/
60+
public function execute(EventObserver $observer)
61+
{
62+
$this->searchCriteriaBuilder->addFilter('frontend_input', 'price');
63+
$criteria = $this->searchCriteriaBuilder->create();
64+
65+
$scope = $this->config->getValue(Store::XML_PATH_PRICE_SCOPE);
66+
$scope = ($scope == Store::PRICE_SCOPE_WEBSITE)
67+
? ProductAttributeInterface::SCOPE_WEBSITE_TEXT
68+
: ProductAttributeInterface::SCOPE_GLOBAL_TEXT;
69+
70+
$priceAttributes = $this->productAttributeRepository->getList($criteria)->getItems();
71+
72+
/** @var ProductAttributeInterface $priceAttribute */
73+
foreach ($priceAttributes as $priceAttribute) {
74+
$priceAttribute->setScope($scope);
75+
$this->productAttributeRepository->save($priceAttribute);
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)