diff --git a/app/code/Magento/Catalog/Api/Data/ProductWebsiteLinkInterface.php b/app/code/Magento/Catalog/Api/Data/ProductWebsiteLinkInterface.php new file mode 100644 index 0000000000000..08c8f6b970583 --- /dev/null +++ b/app/code/Magento/Catalog/Api/Data/ProductWebsiteLinkInterface.php @@ -0,0 +1,39 @@ +_get(self::KEY_SKU); + } + + /** + * {@inheritdoc} + */ + public function getWebsiteId() + { + return $this->_get(self::WEBSITE_ID); + } + + /** + * @param string $sku + * @return $this + */ + public function setSku($sku) + { + return $this->setData(self::KEY_SKU, $sku); + } + + /** + * {@inheritdoc} + */ + public function setWebsiteId($websiteId) + { + return $this->setData(self::WEBSITE_ID, $websiteId); + } +} diff --git a/app/code/Magento/Catalog/Model/ProductWebsiteLinkRepository.php b/app/code/Magento/Catalog/Model/ProductWebsiteLinkRepository.php new file mode 100644 index 0000000000000..4e24fc5ef9c9b --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductWebsiteLinkRepository.php @@ -0,0 +1,84 @@ +productRepository = $productRepository; + } + + /** + * {@inheritdoc} + */ + public function save(ProductWebsiteLinkInterface $productWebsiteLink) + { + if (!$productWebsiteLink->getWebsiteId()) { + throw new InputException(__('There are not websites for assign to product')); + } + $product = $this->productRepository->get($productWebsiteLink->getSku()); + $product->setWebsiteIds(array_merge($product->getWebsiteIds(), [$productWebsiteLink->getWebsiteId()])); + try { + $product->save(); + } catch (\Exception $e) { + throw new CouldNotSaveException( + __( + 'Could not assign product "%1" to websites "%2"', + $product->getId(), + $productWebsiteLink->getWebsiteId() + ), + $e + ); + } + return true; + } + + /** + * {@inheritdoc} + */ + public function delete(ProductWebsiteLinkInterface $productLink) + { + return $this->deleteById($productLink->getSku(), $productLink->getSku()); + } + + /** + * {@inheritdoc} + */ + public function deleteById($sku, $websiteId) + { + $product = $this->productRepository->get($sku); + $product->setWebsiteIds(array_diff($product->getWebsiteIds(), [$websiteId])); + + try { + $product->save(); + } catch (\Exception $e) { + throw new CouldNotSaveException( + __( + 'Could not save product "%1" with websites %2', + $product->getId(), + implode(', ', $product->getWebsiteIds()) + ), + $e + ); + } + return true; + } +} diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 7bd251153314e..3ffee93604698 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -459,6 +459,8 @@ + + diff --git a/app/code/Magento/Catalog/etc/webapi.xml b/app/code/Magento/Catalog/etc/webapi.xml index 24492cbc019b9..d447b748a9ef4 100644 --- a/app/code/Magento/Catalog/etc/webapi.xml +++ b/app/code/Magento/Catalog/etc/webapi.xml @@ -385,4 +385,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php index d978d726a58c7..69b955cf78a39 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php @@ -73,7 +73,9 @@ public function getUrlPath($category) return $category->getUrlPath(); } if ($this->isNeedToGenerateUrlPathForParent($category)) { - $parentPath = $this->getUrlPath($this->categoryRepository->get($category->getParentId())); + $parentPath = $this->getUrlPath( + $this->categoryRepository->get($category->getParentId(), $category->getStoreId()) + ); $path = $parentPath === '' ? $path : $parentPath . '/' . $path; } return $path; @@ -141,7 +143,7 @@ public function getCanonicalUrlPath($category) * @param \Magento\Catalog\Model\Category $category * @return string */ - public function generateUrlKey($category) + public function getUrlKey($category) { $urlKey = $category->getUrlKey(); return $category->formatUrlKey($urlKey === '' || $urlKey === null ? $category->getName() : $urlKey); diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php index 90e65b9094f5e..7656610422d4a 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlPathGenerator.php @@ -114,7 +114,7 @@ public function getCanonicalUrlPath($product, $category = null) * @param \Magento\Catalog\Model\Product $product * @return string */ - public function generateUrlKey($product) + public function getUrlKey($product) { return $product->getUrlKey() === false ? false : $this->prepareProductUrlKey($product); } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php index e72b5df170485..1a903432cc1ee 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php @@ -7,9 +7,11 @@ use Magento\Catalog\Model\Category; use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; +use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; use Magento\Framework\Event\Observer; use Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider; use Magento\Framework\Event\ObserverInterface; +use Magento\Store\Model\Store; class CategoryUrlPathAutogeneratorObserver implements ObserverInterface { @@ -19,16 +21,22 @@ class CategoryUrlPathAutogeneratorObserver implements ObserverInterface /** @var \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider */ protected $childrenCategoriesProvider; + /** @var StoreViewService */ + protected $storeViewService; + /** * @param CategoryUrlPathGenerator $categoryUrlPathGenerator * @param ChildrenCategoriesProvider $childrenCategoriesProvider + * @param \Magento\CatalogUrlRewrite\Service\V1\StoreViewService $storeViewService */ public function __construct( CategoryUrlPathGenerator $categoryUrlPathGenerator, - ChildrenCategoriesProvider $childrenCategoriesProvider + ChildrenCategoriesProvider $childrenCategoriesProvider, + StoreViewService $storeViewService ) { $this->categoryUrlPathGenerator = $categoryUrlPathGenerator; $this->childrenCategoriesProvider = $childrenCategoriesProvider; + $this->storeViewService = $storeViewService; } /** @@ -40,7 +48,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) /** @var Category $category */ $category = $observer->getEvent()->getCategory(); if ($category->getUrlKey() !== false) { - $category->setUrlKey($this->categoryUrlPathGenerator->generateUrlKey($category)) + $category->setUrlKey($this->categoryUrlPathGenerator->getUrlKey($category)) ->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); if (!$category->isObjectNew()) { $category->getResource()->saveAttribute($category, 'url_path'); @@ -57,10 +65,48 @@ public function execute(\Magento\Framework\Event\Observer $observer) */ protected function updateUrlPathForChildren(Category $category) { - foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) { - $childCategory->unsUrlPath(); - $childCategory->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($childCategory)); - $childCategory->getResource()->saveAttribute($childCategory, 'url_path'); + $children = $this->childrenCategoriesProvider->getChildren($category, true); + + if ($this->isGlobalScope($category->getStoreId())) { + foreach ($children as $child) { + foreach ($category->getStoreIds() as $storeId) { + if ($this->storeViewService->doesEntityHaveOverriddenUrlPathForStore( + $storeId, + $child->getId(), + Category::ENTITY + )) { + $child->setStoreId($storeId); + $this->updateUrlPathForCategory($child); + } + } + } + } else { + foreach ($children as $child) { + $child->setStoreId($category->getStoreId()); + $this->updateUrlPathForCategory($child); + } } } + + /** + * Check is global scope + * + * @param int|null $storeId + * @return bool + */ + protected function isGlobalScope($storeId) + { + return null === $storeId || $storeId == Store::DEFAULT_STORE_ID; + } + + /** + * @param Category $category + * @return void + */ + protected function updateUrlPathForCategory(Category $category) + { + $category->unsUrlPath(); + $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); + $category->getResource()->saveAttribute($category, 'url_path'); + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductUrlKeyAutogeneratorObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductUrlKeyAutogeneratorObserver.php index 5a3cd8bb47a0a..525cc1568a77c 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductUrlKeyAutogeneratorObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProductUrlKeyAutogeneratorObserver.php @@ -31,6 +31,6 @@ public function execute(\Magento\Framework\Event\Observer $observer) { /** @var Product $product */ $product = $observer->getEvent()->getProduct(); - $product->setUrlKey($this->productUrlPathGenerator->generateUrlKey($product)); + $product->setUrlKey($this->productUrlPathGenerator->getUrlKey($product)); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Service/V1/StoreViewService.php b/app/code/Magento/CatalogUrlRewrite/Service/V1/StoreViewService.php index 958e882c46013..71e33a7411101 100644 --- a/app/code/Magento/CatalogUrlRewrite/Service/V1/StoreViewService.php +++ b/app/code/Magento/CatalogUrlRewrite/Service/V1/StoreViewService.php @@ -46,14 +46,43 @@ public function __construct( */ public function doesEntityHaveOverriddenUrlKeyForStore($storeId, $entityId, $entityType) { - $attribute = $this->eavConfig->getAttribute($entityType, 'url_key'); + return $this->doesEntityHaveOverriddenUrlAttributeForStore($storeId, $entityId, $entityType, 'url_key'); + } + + /** + * Check that entity has overridden url path for specific store + * + * @param int $storeId + * @param int $entityId + * @param string $entityType + * @throws \InvalidArgumentException + * @return bool + */ + public function doesEntityHaveOverriddenUrlPathForStore($storeId, $entityId, $entityType) + { + return $this->doesEntityHaveOverriddenUrlAttributeForStore($storeId, $entityId, $entityType, 'url_path'); + } + + /** + * Check that entity has overridden url attribute for specific store + * + * @param int $storeId + * @param int $entityId + * @param string $entityType + * @param mixed $attributeName + * @throws \InvalidArgumentException + * @return bool + */ + protected function doesEntityHaveOverriddenUrlAttributeForStore($storeId, $entityId, $entityType, $attributeName) + { + $attribute = $this->eavConfig->getAttribute($entityType, $attributeName); if (!$attribute) { throw new \InvalidArgumentException(sprintf('Cannot retrieve attribute for entity type "%s"', $entityType)); } $select = $this->connection->select() - ->from($attribute->getBackendTable(), 'store_id') - ->where('attribute_id = ?', $attribute->getId()) - ->where('entity_id = ?', $entityId); + ->from($attribute->getBackendTable(), 'store_id') + ->where('attribute_id = ?', $attribute->getId()) + ->where('entity_id = ?', $entityId); return in_array($storeId, $this->connection->fetchCol($select)); } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlPathGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlPathGeneratorTest.php index 94dc7f9c33ae2..41300634caaa4 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlPathGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlPathGeneratorTest.php @@ -235,7 +235,7 @@ public function testGetCanonicalUrlPath() /** * @return array */ - public function generateUrlKeyDataProvider() + public function getUrlKeyDataProvider() { return [ ['url-key', null, 'url-key'], @@ -244,17 +244,17 @@ public function generateUrlKeyDataProvider() } /** - * @dataProvider generateUrlKeyDataProvider - * @param string $urlKey - * @param string $name + * @dataProvider getUrlKeyDataProvider + * @param string|null|bool $urlKey + * @param string|null|bool $name * @param string $result */ - public function testGenerateUrlKey($urlKey, $name, $result) + public function testGetUrlKey($urlKey, $name, $result) { $this->category->expects($this->once())->method('getUrlKey')->will($this->returnValue($urlKey)); $this->category->expects($this->any())->method('getName')->will($this->returnValue($name)); $this->category->expects($this->once())->method('formatUrlKey')->will($this->returnArgument(0)); - $this->assertEquals($result, $this->categoryUrlPathGenerator->generateUrlKey($this->category)); + $this->assertEquals($result, $this->categoryUrlPathGenerator->getUrlKey($this->category)); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php index 7b733303bf4d4..fe4895c721bb2 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductUrlPathGeneratorTest.php @@ -85,11 +85,11 @@ public function getUrlPathDataProvider() /** * @dataProvider getUrlPathDataProvider - * @param $urlKey - * @param $productName - * @param $result + * @param string|null|bool $urlKey + * @param string|null|bool $productName + * @param string $result */ - public function testGenerateUrlPath($urlKey, $productName, $result) + public function testGetUrlPath($urlKey, $productName, $result) { $this->product->expects($this->once())->method('getData')->with('url_path') ->will($this->returnValue(null)); @@ -101,19 +101,21 @@ public function testGenerateUrlPath($urlKey, $productName, $result) } /** - * @param $productUrlKey - * @param $expectedUrlKey - * - * @dataProvider generateUrlKeyDataProvider + * @param string|bool $productUrlKey + * @param string|bool $expectedUrlKey + * @dataProvider getUrlKeyDataProvider */ - public function testGenerateUrlKey($productUrlKey, $expectedUrlKey) + public function testGetUrlKey($productUrlKey, $expectedUrlKey) { $this->product->expects($this->any())->method('getUrlKey')->will($this->returnValue($productUrlKey)); $this->product->expects($this->any())->method('formatUrlKey')->will($this->returnValue($productUrlKey)); - $this->assertEquals($expectedUrlKey, $this->productUrlPathGenerator->generateUrlKey($this->product)); + $this->assertEquals($expectedUrlKey, $this->productUrlPathGenerator->getUrlKey($this->product)); } - public function generateUrlKeyDataProvider() + /** + * @return array + */ + public function getUrlKeyDataProvider() { return [ 'URL Key use default' => [false, false], @@ -121,17 +123,10 @@ public function generateUrlKeyDataProvider() ]; } - public function testGetUrlPath() - { - $this->product->expects($this->once())->method('getData')->with('url_path') - ->will($this->returnValue('url-path')); - $this->product->expects($this->never())->method('getUrlKey'); - - $this->assertEquals('url-path', $this->productUrlPathGenerator->getUrlPath($this->product, null)); - } - /** - * + * @param string|null|bool $storedUrlKey + * @param string|null|bool $productName + * @param string $expectedUrlKey * @dataProvider getUrlPathDefaultUrlKeyDataProvider */ public function testGetUrlPathDefaultUrlKey($storedUrlKey, $productName, $expectedUrlKey) @@ -144,13 +139,15 @@ public function testGetUrlPathDefaultUrlKey($storedUrlKey, $productName, $expect $this->assertEquals($expectedUrlKey, $this->productUrlPathGenerator->getUrlPath($this->product, null)); } + /** + * @return array + */ public function getUrlPathDefaultUrlKeyDataProvider() { return [ ['default-store-view-url-key', null, 'default-store-view-url-key'], [false, 'default-store-view-product-name', 'default-store-view-product-name'] ]; - } public function testGetUrlPathWithCategory() diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php index 8088fcf653a18..e400fe924c686 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php @@ -24,6 +24,16 @@ class CategoryUrlPathAutogeneratorObserverTest extends \PHPUnit_Framework_TestCa /** @var \PHPUnit_Framework_MockObject_MockObject */ protected $category; + /** + * @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService|\PHPUnit_Framework_MockObject_MockObject + */ + protected $storeViewService; + + /** + * @var \Magento\Catalog\Model\ResourceModel\Category|\PHPUnit_Framework_MockObject_MockObject + */ + protected $categoryResource; + protected function setUp() { $this->observer = $this->getMock( @@ -33,13 +43,15 @@ protected function setUp() '', false ); + $this->categoryResource = $this->getMock('Magento\Catalog\Model\ResourceModel\Category', [], [], '', false); $this->category = $this->getMock( 'Magento\Catalog\Model\Category', - ['setUrlKey', 'setUrlPath', 'dataHasChangedFor', 'isObjectNew', 'getResource', 'getUrlKey'], + ['setUrlKey', 'setUrlPath', 'dataHasChangedFor', 'isObjectNew', 'getResource', 'getUrlKey', 'getStoreId'], [], '', false ); + $this->category->expects($this->any())->method('getResource')->willReturn($this->categoryResource); $this->observer->expects($this->any())->method('getEvent')->willReturnSelf(); $this->observer->expects($this->any())->method('getCategory')->willReturn($this->category); $this->categoryUrlPathGenerator = $this->getMock( @@ -53,11 +65,20 @@ protected function setUp() 'Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider' ); + $this->storeViewService = $this->getMock( + 'Magento\CatalogUrlRewrite\Service\V1\StoreViewService', + [], + [], + '', + false + ); + $this->categoryUrlPathAutogeneratorObserver = (new ObjectManagerHelper($this))->getObject( 'Magento\CatalogUrlRewrite\Observer\CategoryUrlPathAutogeneratorObserver', [ 'categoryUrlPathGenerator' => $this->categoryUrlPathGenerator, - 'childrenCategoriesProvider' => $this->childrenCategoriesProvider + 'childrenCategoriesProvider' => $this->childrenCategoriesProvider, + 'storeViewService' => $this->storeViewService, ] ); } @@ -65,7 +86,7 @@ protected function setUp() public function testSetCategoryUrlAndCategoryPath() { $this->category->expects($this->once())->method('getUrlKey')->willReturn('category'); - $this->categoryUrlPathGenerator->expects($this->once())->method('generateUrlKey')->willReturn('urk_key'); + $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlKey')->willReturn('urk_key'); $this->category->expects($this->once())->method('setUrlKey')->with('urk_key')->willReturnSelf(); $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlPath')->willReturn('url_path'); $this->category->expects($this->once())->method('setUrlPath')->with('url_path')->willReturnSelf(); @@ -74,7 +95,7 @@ public function testSetCategoryUrlAndCategoryPath() $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); } - public function testExecuteWithoutGeneration() + public function testExecuteWithoutUrlKeyAndUrlPathUpdating() { $this->category->expects($this->once())->method('getUrlKey')->willReturn(false); $this->category->expects($this->never())->method('setUrlKey'); @@ -82,30 +103,103 @@ public function testExecuteWithoutGeneration() $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); } - public function testUpdateUrlPathForChildren() + public function testUrlKeyAndUrlPathUpdating() { - $this->category->expects($this->once())->method('getUrlKey')->willReturn('category'); - $this->category->expects($this->once())->method('setUrlKey')->willReturnSelf(); - $this->category->expects($this->once())->method('setUrlPath')->willReturnSelf(); + $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlKey')->with($this->category) + ->willReturn('url_key'); + $this->categoryUrlPathGenerator->expects($this->once())->method('getUrlPath')->with($this->category) + ->willReturn('url_path'); + + $this->category->expects($this->once())->method('getUrlKey')->willReturn('not_formatted_url_key'); + $this->category->expects($this->once())->method('setUrlKey')->with('url_key')->willReturnSelf(); + $this->category->expects($this->once())->method('setUrlPath')->with('url_path')->willReturnSelf(); + // break code execution + $this->category->expects($this->once())->method('isObjectNew')->willReturn(true); + + $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); + } + + public function testUrlPathAttributeNoUpdatingIfCategoryIsNew() + { + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlKey')->willReturn('url_key'); + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPath')->willReturn('url_path'); + + $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); + $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); + $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); + + $this->category->expects($this->once())->method('isObjectNew')->willReturn(true); + $this->categoryResource->expects($this->never())->method('saveAttribute'); + + $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); + } + + public function testUrlPathAttributeUpdating() + { + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlKey')->willReturn('url_key'); + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPath')->willReturn('url_path'); + + $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); + $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); + $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); $this->category->expects($this->once())->method('isObjectNew')->willReturn(false); - $this->category->expects($this->once())->method('dataHasChangedFor')->with('url_path')->willReturn(true); - $categoryResource = $this->getMockBuilder('Magento\Catalog\Model\ResourceModel\Category') - ->disableOriginalConstructor()->getMock(); - $this->category->expects($this->once())->method('getResource')->willReturn($categoryResource); - $categoryResource->expects($this->once())->method('saveAttribute')->with($this->category, 'url_path'); + $this->categoryResource->expects($this->once())->method('saveAttribute')->with($this->category, 'url_path'); + + // break code execution + $this->category->expects($this->once())->method('dataHasChangedFor')->with('url_path')->willReturn(false); + + $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); + } + + public function testChildrenUrlPathAttributeNoUpdatingIfParentUrlPathIsNotChanged() + { + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlKey')->willReturn('url_key'); + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPath')->willReturn('url_path'); + + $this->categoryResource->expects($this->once())->method('saveAttribute')->with($this->category, 'url_path'); + + $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); + $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); + $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); + $this->category->expects($this->once())->method('isObjectNew')->willReturn(false); + // break code execution + $this->category->expects($this->once())->method('dataHasChangedFor')->with('url_path')->willReturn(false); + + $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); + } + + public function testChildrenUrlPathAttributeUpdatingForSpecificStore() + { + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlKey')->willReturn('generated_url_key'); + $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPath')->willReturn('generated_url_path'); + + $this->category->expects($this->any())->method('getUrlKey')->willReturn('not_formatted_url_key'); + $this->category->expects($this->any())->method('setUrlKey')->willReturnSelf(); + $this->category->expects($this->any())->method('setUrlPath')->willReturnSelf(); + $this->category->expects($this->any())->method('isObjectNew')->willReturn(false); + $this->category->expects($this->any())->method('dataHasChangedFor')->willReturn(true); + // only for specific store + $this->category->expects($this->atLeastOnce())->method('getStoreId')->willReturn(1); + + $childCategoryResource = $this->getMockBuilder('Magento\Catalog\Model\ResourceModel\Category') + ->disableOriginalConstructor()->getMock(); $childCategory = $this->getMockBuilder('Magento\Catalog\Model\Category') - ->setMethods(['getUrlPath', 'setUrlPath', 'getResource', 'unsUrlPath']) + ->setMethods([ + 'getUrlPath', + 'setUrlPath', + 'getResource', + 'getStore', + 'getStoreId', + 'setStoreId' + ]) ->disableOriginalConstructor()->getMock(); + $childCategory->expects($this->any())->method('getResource')->willReturn($childCategoryResource); + $childCategory->expects($this->once())->method('setStoreId')->with(1); $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->willReturn([$childCategory]); - $childCategoryResource = $this->getMockBuilder('Magento\Catalog\Model\ResourceModel\Category') - ->disableOriginalConstructor()->getMock(); - $childCategory->expects($this->once())->method('unsUrlPath')->willReturnSelf(); - $childCategory->expects($this->once())->method('getResource')->willReturn($childCategoryResource); + $childCategory->expects($this->once())->method('setUrlPath')->with('generated_url_path')->willReturnSelf(); $childCategoryResource->expects($this->once())->method('saveAttribute')->with($childCategory, 'url_path'); - $childCategory->expects($this->once())->method('setUrlPath')->with('category-url_path')->willReturnSelf(); - $this->categoryUrlPathGenerator->expects($this->any())->method('getUrlPath')->willReturn('category-url_path'); $this->categoryUrlPathAutogeneratorObserver->execute($this->observer); } diff --git a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php index fce1979115e66..356f9b2f2b8cd 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php @@ -184,15 +184,15 @@ public function getJsonConfig() 'optionPrices' => $this->getOptionPrices(), 'prices' => [ 'oldPrice' => [ - 'amount' => $this->_registerJsPrice($this->_convertPrice($regularPrice->getAmount()->getValue())), + 'amount' => $this->_registerJsPrice($regularPrice->getAmount()->getValue()), ], 'basePrice' => [ 'amount' => $this->_registerJsPrice( - $this->_convertPrice($finalPrice->getAmount()->getBaseAmount()) + $finalPrice->getAmount()->getBaseAmount() ), ], 'finalPrice' => [ - 'amount' => $this->_registerJsPrice($this->_convertPrice($finalPrice->getAmount()->getValue())), + 'amount' => $this->_registerJsPrice($finalPrice->getAmount()->getValue()), ], ], 'productId' => $currentProduct->getId(), @@ -223,17 +223,17 @@ protected function getOptionPrices() [ 'oldPrice' => [ 'amount' => $this->_registerJsPrice( - $this->_convertPrice($priceInfo->getPrice('regular_price')->getAmount()->getValue()) + $priceInfo->getPrice('regular_price')->getAmount()->getValue() ), ], 'basePrice' => [ 'amount' => $this->_registerJsPrice( - $this->_convertPrice($priceInfo->getPrice('final_price')->getAmount()->getBaseAmount()) + $priceInfo->getPrice('final_price')->getAmount()->getBaseAmount() ), ], 'finalPrice' => [ 'amount' => $this->_registerJsPrice( - $this->_convertPrice($priceInfo->getPrice('final_price')->getAmount()->getValue()) + $priceInfo->getPrice('final_price')->getAmount()->getValue() ), ] ]; @@ -251,25 +251,4 @@ protected function _registerJsPrice($price) { return str_replace(',', '.', $price); } - - /** - * Convert price from default currency to current currency - * - * @param float $price - * @param bool $round - * @return float - */ - protected function _convertPrice($price, $round = false) - { - if (empty($price)) { - return 0; - } - - $price = $this->priceCurrency->convert($price); - if ($round) { - $price = $this->priceCurrency->round($price); - } - - return $price; - } } diff --git a/app/code/Magento/ConfigurableProduct/Helper/Data.php b/app/code/Magento/ConfigurableProduct/Helper/Data.php index 91dd722424219..8ab8a52a9ee65 100644 --- a/app/code/Magento/ConfigurableProduct/Helper/Data.php +++ b/app/code/Magento/ConfigurableProduct/Helper/Data.php @@ -40,7 +40,7 @@ public function getGalleryImages(\Magento\Catalog\Api\Data\ProductInterface $pro { $images = $product->getMediaGalleryImages(); if ($images instanceof \Magento\Framework\Data\Collection) { - foreach ($images as &$image) { + foreach ($images as $image) { /** @var $image \Magento\Catalog\Model\Product\Image */ $image->setData( 'small_image_url', diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php index bd74b867c8610..aeae1374a9813 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php @@ -7,8 +7,6 @@ */ namespace Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use Magento\Framework\Pricing\PriceCurrencyInterface; - class Price extends \Magento\Catalog\Model\Product\Type\Price { /** @@ -24,17 +22,27 @@ public function getFinalPrice($qty, $product) return $product->getCalculatedFinalPrice(); } if ($product->getCustomOption('simple_product')) { - $simpleProduct = $product->getCustomOption('simple_product')->getProduct(); - $product->setSelectedConfigurableOption($simpleProduct); - $priceInfo = $simpleProduct->getPriceInfo(); + return parent::getFinalPrice($qty, $product->getCustomOption('simple_product')->getProduct()); } else { $priceInfo = $product->getPriceInfo(); + $finalPrice = $priceInfo->getPrice('final_price')->getAmount()->getValue(); + $finalPrice = $this->_applyOptionsPrice($product, $qty, $finalPrice); + $finalPrice = max(0, $finalPrice); + $product->setFinalPrice($finalPrice); + + return $finalPrice; } - $finalPrice = $priceInfo->getPrice('final_price')->getAmount()->getValue(); - $finalPrice = $this->_applyOptionsPrice($product, $qty, $finalPrice); - $finalPrice = max(0, $finalPrice); - $product->setFinalPrice($finalPrice); + } - return $finalPrice; + /** + * {@inheritdoc} + */ + public function getPrice($product) + { + if ($product->getCustomOption('simple_product')) { + return $product->getCustomOption('simple_product')->getProduct()->getPrice(); + } else { + return 0; + } } } diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/BasePrice.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/BasePrice.php deleted file mode 100644 index 76f2ef5cdd900..0000000000000 --- a/app/code/Magento/ConfigurableProduct/Pricing/Price/BasePrice.php +++ /dev/null @@ -1,68 +0,0 @@ -product->getSelectedConfigurableOption(); - $productId = $selectedConfigurableOption ? $selectedConfigurableOption->getId() : $this->product->getId(); - if (!isset($this->values[$productId])) { - $this->value = null; - if (!$selectedConfigurableOption) { - $this->values[$productId] = parent::getValue(); - } else { - if (false !== $this->getMinimumAdditionalPrice()) { - $this->values[$productId] = $this->getMinimumAdditionalPrice(); - } else { - $this->values[$productId] = parent::getValue(); - } - } - } - return $this->values[$productId]; - } - - /** - * @return bool|float - */ - protected function getMinimumAdditionalPrice() - { - if (null === $this->minimumAdditionalPrice) { - $priceCodes = [ - \Magento\Catalog\Pricing\Price\SpecialPrice::PRICE_CODE, - \Magento\Catalog\Pricing\Price\TierPrice::PRICE_CODE, - ]; - $this->minimumAdditionalPrice = false; - foreach ($priceCodes as $priceCode) { - $price = $this->product->getPriceInfo()->getPrice($priceCode); - if ($price instanceof BasePriceProviderInterface && $price->getValue() !== false) { - $this->minimumAdditionalPrice = min( - $price->getValue(), - $this->minimumAdditionalPrice ?: $price->getValue() - ); - } - } - } - return $this->minimumAdditionalPrice; - } -} diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php index d0f2461c96a3e..a4fe3d76da22b 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php @@ -37,22 +37,22 @@ public function __construct( } /** - * @param \Magento\Framework\Pricing\SaleableInterface $product + * @param \Magento\Framework\Pricing\SaleableInterface|\Magento\Catalog\Model\Product $product * @return float + * @throws \Magento\Framework\Exception\LocalizedException */ public function resolvePrice(\Magento\Framework\Pricing\SaleableInterface $product) { - $selectedConfigurableOption = $product->getSelectedConfigurableOption(); - if ($selectedConfigurableOption) { - $price = $this->priceResolver->resolvePrice($selectedConfigurableOption); - } else { - $price = null; - foreach ($this->configurable->getUsedProducts($product) as $subProduct) { - $productPrice = $this->priceResolver->resolvePrice($subProduct); - $price = $price ? min($price, $productPrice) : $productPrice; - } + $price = null; + foreach ($this->configurable->getUsedProducts($product) as $subProduct) { + $productPrice = $this->priceResolver->resolvePrice($subProduct); + $price = $price ? min($price, $productPrice) : $productPrice; } - $priceInCurrentCurrency = $this->priceCurrency->convertAndRound($price); - return $priceInCurrentCurrency ? (float)$priceInCurrentCurrency : false; + if (!$price) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Configurable product "%1" do not have sub-products', $product->getName()) + ); + } + return (float)$price; } } diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php index de5ea0dcb0716..936698cef3567 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php @@ -61,20 +61,19 @@ public function __construct( */ public function getValue() { - $selectedConfigurableOption = $this->product->getSelectedConfigurableOption(); - $productId = $selectedConfigurableOption ? $selectedConfigurableOption->getId() : $this->product->getId(); - if (!isset($this->values[$productId])) { - $this->values[$productId] = $this->priceResolver->resolvePrice($this->product); + if (!isset($this->values[$this->product->getId()])) { + $this->values[$this->product->getId()] = $this->priceResolver->resolvePrice($this->product); } - return $this->values[$productId]; + return $this->values[$this->product->getId()]; } + /** * {@inheritdoc} */ public function getAmount() { - return $this->getMinRegularAmount($this->product); + return $this->getMinRegularAmount(); } /** @@ -116,7 +115,6 @@ public function getMinRegularAmount() $this->minRegularAmount = $this->doGetMinRegularAmount() ?: false; } return $this->minRegularAmount; - } /** diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/FinalPrice.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/FinalPrice.php index 3d7cfd251d54e..834df6ffd429a 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Price/FinalPrice.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/FinalPrice.php @@ -34,28 +34,15 @@ public function __construct( $this->priceResolver = $priceResolver; } - /** - * {@inheritdoc} - */ - public function getAmount() - { - if ($this->product->getSelectedConfigurableOption()) { - $this->amount = null; - } - return parent::getAmount(); - } - /** * {@inheritdoc} */ public function getValue() { - $selectedConfigurableOption = $this->product->getSelectedConfigurableOption(); - $productId = $selectedConfigurableOption ? $selectedConfigurableOption->getId() : $this->product->getId(); - if (!isset($this->values[$productId])) { - $this->values[$productId] = $this->priceResolver->resolvePrice($this->product); + if (!isset($this->values[$this->product->getId()])) { + $this->values[$this->product->getId()] = $this->priceResolver->resolvePrice($this->product); } - return $this->values[$productId]; + return $this->values[$this->product->getId()]; } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php index 7d14671ec0d7d..7757b43562442 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php @@ -31,11 +31,7 @@ public function testGetFinalPrice() $qty = 1; $configurableProduct = $this->getMockBuilder('Magento\Catalog\Model\Product') ->disableOriginalConstructor() - ->setMethods(['getCustomOption', 'setSelectedConfigurableOption', 'setFinalPrice', '__wakeUp']) - ->getMock(); - $childProduct = $this->getMockBuilder('Magento\Catalog\Model\Product') - ->disableOriginalConstructor() - ->setMethods(['getPriceInfo', '__wakeUp']) + ->setMethods(['getCustomOption', 'getPriceInfo', 'setFinalPrice', '__wakeUp']) ->getMock(); $customOption = $this->getMockBuilder('Magento\Catalog\Model\Product\Configuration\Item\Option') ->disableOriginalConstructor() @@ -52,24 +48,14 @@ public function testGetFinalPrice() ->disableOriginalConstructor() ->getMock(); - $configurableProduct->expects($this->at(0)) - ->method('getCustomOption') - ->with('simple_product') - ->willReturn($customOption); - $configurableProduct->expects($this->at(1)) + $configurableProduct->expects($this->any()) ->method('getCustomOption') - ->with('simple_product') - ->willReturn($customOption); - $customOption->expects($this->once())->method('getProduct')->willReturn($childProduct); - $configurableProduct->expects($this->once()) - ->method('setSelectedConfigurableOption') - ->with($childProduct) - ->willReturnSelf(); - $childProduct->expects($this->once())->method('getPriceInfo')->willReturn($priceInfo); + ->willReturnMap([['simple_product', false], ['option_ids', false]]); + $customOption->expects($this->never())->method('getProduct'); + $configurableProduct->expects($this->once())->method('getPriceInfo')->willReturn($priceInfo); $priceInfo->expects($this->once())->method('getPrice')->with('final_price')->willReturn($price); $price->expects($this->once())->method('getAmount')->willReturn($amount); $amount->expects($this->once())->method('getValue')->willReturn($finalPrice); - $configurableProduct->expects($this->at(3))->method('getCustomOption')->with('option_ids')->willReturn(false); $configurableProduct->expects($this->once())->method('setFinalPrice')->with($finalPrice)->willReturnSelf(); $this->assertEquals($finalPrice, $this->model->getFinalPrice($qty, $configurableProduct)); diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index ff1173c2e0908..e2463e52daa81 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -70,7 +70,6 @@ Magento\ConfigurableProduct\Pricing\Price\ConfigurableRegularPrice Magento\ConfigurableProduct\Pricing\Price\FinalPrice - Magento\ConfigurableProduct\Pricing\Price\BasePrice Magento\Catalog\Pricing\Price\Pool diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/bulk.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/bulk.js index 499962f8e59f8..788d4eed83235 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/bulk.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/bulk.js @@ -55,7 +55,7 @@ define([ type: ko.observable('none'), value: ko.observable(), attribute: ko.observable(), - currencySymbol: this.variationsComponent().getCurrencySymbol() + currencySymbol: '' }, quantity: { label: 'quantity', @@ -64,6 +64,11 @@ define([ attribute: ko.observable() } }); + + this.variationsComponent(function (variationsComponent) { + this.sections().price.currencySymbol = variationsComponent.getCurrencySymbol() + }.bind(this)); + this.makeOptionSections = function () { this.images = new self.makeImages(null); this.price = self.price; diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php index fb92f2bc3fffd..ee184dff0e6d5 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php @@ -157,27 +157,13 @@ public function getPurchasedSeparatelyAttribute() } /** - * Retrieve Purchased Separately HTML select + * Get Links can be purchased separately value for current product * - * @return string + * @return bool */ - public function getPurchasedSeparatelySelect() + public function isProductLinksCanBePurchasedSeparately() { - $select = $this->getLayout()->createBlock( - 'Magento\Framework\View\Element\Html\Select' - )->setName( - 'product[links_purchased_separately]' - )->setId( - 'downloadable_link_purchase_type' - )->setOptions( - $this->_sourceModel->toOptionArray() - )->setValue( - $this->getProduct()->getLinksPurchasedSeparately() - )->setClass( - 'admin__control-select' - ); - - return $select->getHtml(); + return (bool) $this->getProduct()->getData('links_purchased_separately'); } /** diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml index dac2edad7be49..dd2cf918d704e 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml @@ -25,17 +25,6 @@ require([ // var uploaderTemplate = '
' + - '
' + - '' + - '<%= data.name %>' + - ' ' + - '(<%- data.size %>)' + - '' + - '
' + - '
' + - '
' + - '
' + - '
' + '
' + '
' + '<%- data.percent %>% <%- data.uploaded %> / <%- data.total %>' + diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/links.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/links.phtml index cf43aa7d3f4f6..909ab56b20049 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/links.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/links.phtml @@ -34,7 +34,34 @@
isSingleStoreMode() ? ' data-config-scope="' . __('[GLOBAL]') . '"' : ''; ?>>
- getPurchasedSeparatelySelect()?> +
@@ -357,9 +384,9 @@ require([ } }, togglePriceFields : function(){ - var toogleTo = $('downloadable_link_purchase_type').value; + var toogleTo = jQuery('#link-switcher1').is(':checked'); var disableFlag = true; - if (toogleTo == '1') { + if (toogleTo) { disableFlag = false; } $$('.link-prices[type="text"]').each(function(elm){ @@ -442,9 +469,10 @@ require([ })(jQuery); }; - - if ($('downloadable_link_purchase_type')) { - Event.observe('downloadable_link_purchase_type', 'change', linkItems.togglePriceFields.bind()); + if (jQuery('input[name="product[links_purchased_separately]"]')) { + jQuery('input[name="product[links_purchased_separately]"]').on('change', function () { + linkItems.togglePriceFields.bind() + }); } if($('add_link_item')) { diff --git a/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php index e8719f6f4e276..21f313bcf0c35 100644 --- a/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php @@ -126,7 +126,6 @@ public function testCreateConfigurableProduct() $response = $this->createConfigurableProduct(); $this->assertEquals(self::CONFIGURABLE_PRODUCT_SKU, $response[ProductInterface::SKU]); - $this->assertEquals(50, $response['price']); $this->assertTrue( isset($response[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]["configurable_product_options"]) ); diff --git a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.xml b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.xml index bd1e9e70f43f1..6d6f3231f3e8e 100644 --- a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.xml +++ b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.xml @@ -14,7 +14,7 @@ [name="product[links_purchased_separately]"] css selector - select + checkbox