diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php index 23fc2026ab111..82a0086ad67ec 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php @@ -100,6 +100,8 @@ public function getChildren($item) } /** + * Check if item can be shipped separately + * * @param mixed $item * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -136,6 +138,8 @@ public function isShipmentSeparately($item = null) } /** + * Check if child items calculated + * * @param mixed $item * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -174,6 +178,8 @@ public function isChildCalculated($item = null) } /** + * Retrieve selection attributes values + * * @param mixed $item * @return mixed|null */ @@ -191,6 +197,8 @@ public function getSelectionAttributes($item) } /** + * Retrieve order item options array + * * @return array */ public function getOrderOptions() @@ -212,6 +220,8 @@ public function getOrderOptions() } /** + * Retrieve order item + * * @return mixed */ public function getOrderItem() @@ -223,6 +233,8 @@ public function getOrderItem() } /** + * Get html info for item + * * @param mixed $item * @return string */ @@ -245,6 +257,8 @@ public function getValueHtml($item) } /** + * Check if we can show price info for this item + * * @param object $item * @return bool */ diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml index ff26d67bd8378..12da960a9c6cf 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml @@ -28,8 +28,17 @@ + getOrderItem()->getProductType() == \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) ? + !$_item->getOrderItem()->isShipSeparately() : !$_item->getOrderItem()->getParentItem()->isShipSeparately() + ?> setPriceDataObject($_item) ?> getOrderItem()->getParentItem()): ?> + getSelectionAttributes($_item) ?> @@ -60,14 +69,14 @@ - canShowPriceInfo($_item)): ?> + canShowPriceInfo($_item) || $shipTogether): ?> getColumnHtml($_item, 'price') ?>   - canShowPriceInfo($_item)): ?> + canShowPriceInfo($_item) || $shipTogether): ?> @@ -116,7 +125,7 @@
- canShowPriceInfo($_item)): ?> + canShowPriceInfo($_item) || $shipTogether): ?> canEditQty()) : ?> getNodeById($rootId); - if ($root && $rootId != \Magento\Catalog\Model\Category::TREE_ROOT_ID) { + if ($root) { $root->setIsVisible(true); - } elseif ($root && $root->getId() == \Magento\Catalog\Model\Category::TREE_ROOT_ID) { - $root->setName(__('Root')); + if ($root->getId() == \Magento\Catalog\Model\Category::TREE_ROOT_ID) { + $root->setName(__('Root')); + } } $this->_coreRegistry->register('root', $root); @@ -162,6 +175,8 @@ public function getRoot($parentNodeCategory = null, $recursionLevel = 3) } /** + * Get Default Store Id + * * @return int */ protected function _getDefaultStoreId() @@ -170,6 +185,8 @@ protected function _getDefaultStoreId() } /** + * Get category collection + * * @return \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection */ public function getCategoryCollection() @@ -227,6 +244,8 @@ public function getRootByIds($ids) } /** + * Get category node for tree + * * @param mixed $parentNodeCategory * @param int $recursionLevel * @return Node @@ -249,6 +268,8 @@ public function getNode($parentNodeCategory, $recursionLevel = 2) } /** + * Get category save url + * * @param array $args * @return string */ @@ -260,6 +281,8 @@ public function getSaveUrl(array $args = []) } /** + * Get category edit url + * * @return string */ public function getEditUrl() diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php index d3c84e69c9540..e296c8d3b8978 100644 --- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php @@ -58,22 +58,38 @@ public function build(Filter $filter): string $conditionValue = $this->mapConditionValue($conditionType, $filter->getValue()); // NOTE: store scope was ignored intentionally to perform search across all stores - $attributeSelect = $this->resourceConnection->getConnection() - ->select() - ->from( - [$tableAlias => $attribute->getBackendTable()], - $tableAlias . '.' . $attribute->getEntityIdField() - )->where( - $this->resourceConnection->getConnection()->prepareSqlCondition( - $tableAlias . '.' . $attribute->getIdFieldName(), - ['eq' => $attribute->getAttributeId()] - ) - )->where( - $this->resourceConnection->getConnection()->prepareSqlCondition( - $tableAlias . '.value', - [$conditionType => $conditionValue] - ) - ); + if ($conditionType == 'is_null') { + $entityResourceModel = $attribute->getEntity(); + $attributeSelect = $this->resourceConnection->getConnection() + ->select() + ->from( + [Collection::MAIN_TABLE_ALIAS => $entityResourceModel->getEntityTable()], + Collection::MAIN_TABLE_ALIAS . '.' . $entityResourceModel->getEntityIdField() + )->joinLeft( + [$tableAlias => $attribute->getBackendTable()], + $tableAlias . '.' . $attribute->getEntityIdField() . '=' . Collection::MAIN_TABLE_ALIAS . + '.' . $entityResourceModel->getEntityIdField() . ' AND ' . $tableAlias . '.' . + $attribute->getIdFieldName() . '=' . $attribute->getAttributeId(), + '' + )->where($tableAlias . '.value is null'); + } else { + $attributeSelect = $this->resourceConnection->getConnection() + ->select() + ->from( + [$tableAlias => $attribute->getBackendTable()], + $tableAlias . '.' . $attribute->getEntityIdField() + )->where( + $this->resourceConnection->getConnection()->prepareSqlCondition( + $tableAlias . '.' . $attribute->getIdFieldName(), + ['eq' => $attribute->getAttributeId()] + ) + )->where( + $this->resourceConnection->getConnection()->prepareSqlCondition( + $tableAlias . '.value', + [$conditionType => $conditionValue] + ) + ); + } return $this->resourceConnection ->getConnection() @@ -86,6 +102,8 @@ public function build(Filter $filter): string } /** + * Get attribute entity by its code + * * @param string $field * @return Attribute * @throws \Magento\Framework\Exception\LocalizedException diff --git a/app/code/Magento/Catalog/Model/CategoryList.php b/app/code/Magento/Catalog/Model/CategoryList.php index 790ea6b921fbe..e3318db505489 100644 --- a/app/code/Magento/Catalog/Model/CategoryList.php +++ b/app/code/Magento/Catalog/Model/CategoryList.php @@ -15,6 +15,9 @@ use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +/** + * Class for getting category list. + */ class CategoryList implements CategoryListInterface { /** @@ -64,7 +67,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getList(SearchCriteriaInterface $searchCriteria) { @@ -73,10 +76,11 @@ public function getList(SearchCriteriaInterface $searchCriteria) $this->extensionAttributesJoinProcessor->process($collection); $this->collectionProcessor->process($searchCriteria, $collection); + $collection->load(); $items = []; - foreach ($collection->getAllIds() as $id) { - $items[] = $this->categoryRepository->get($id); + foreach ($collection->getItems() as $category) { + $items[] = $this->categoryRepository->get($category->getId()); } /** @var CategorySearchResultsInterface $searchResult */ diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php b/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php index 2bb10d3b31a24..893000544a728 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php @@ -113,27 +113,28 @@ private function customizeAttributeCode($meta) */ private function customizeFrontendLabels($meta) { + $labelConfigs = []; + foreach ($this->storeRepository->getList() as $store) { $storeId = $store->getId(); if (!$storeId) { continue; } - - $meta['manage-titles']['children'] = [ - 'frontend_label[' . $storeId . ']' => $this->arrayManager->set( - 'arguments/data/config', - [], - [ - 'formElement' => Input::NAME, - 'componentType' => Field::NAME, - 'label' => $store->getName(), - 'dataType' => Text::NAME, - 'dataScope' => 'frontend_label[' . $storeId . ']' - ] - ), - ]; + $labelConfigs['frontend_label[' . $storeId . ']'] = $this->arrayManager->set( + 'arguments/data/config', + [], + [ + 'formElement' => Input::NAME, + 'componentType' => Field::NAME, + 'label' => $store->getName(), + 'dataType' => Text::NAME, + 'dataScope' => 'frontend_label[' . $storeId . ']' + ] + ); } + $meta['manage-titles']['children'] = $labelConfigs; + return $meta; } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Repository.php b/app/code/Magento/Catalog/Model/Product/Option/Repository.php index 9dc9695daffd1..bb4e247de32db 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Repository.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Repository.php @@ -14,6 +14,8 @@ use Magento\Framework\App\ObjectManager; /** + * Product custom options repository + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Repository implements \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface @@ -83,7 +85,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getList($sku) { @@ -92,7 +94,7 @@ public function getList($sku) } /** - * {@inheritdoc} + * @inheritdoc */ public function getProductOptions(ProductInterface $product, $requiredOnly = false) { @@ -104,7 +106,7 @@ public function getProductOptions(ProductInterface $product, $requiredOnly = fal } /** - * {@inheritdoc} + * @inheritdoc */ public function get($sku, $optionId) { @@ -117,7 +119,7 @@ public function get($sku, $optionId) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $entity) { @@ -126,7 +128,7 @@ public function delete(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $e } /** - * {@inheritdoc} + * @inheritdoc */ public function duplicate( \Magento\Catalog\Api\Data\ProductInterface $product, @@ -142,7 +144,7 @@ public function duplicate( } /** - * {@inheritdoc} + * @inheritdoc */ public function save(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $option) { @@ -184,7 +186,7 @@ public function save(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $opt } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteByIdentifier($sku, $optionId) { @@ -209,8 +211,8 @@ public function deleteByIdentifier($sku, $optionId) /** * Mark original values for removal if they are absent among new values * - * @param $newValues array - * @param $originalValues \Magento\Catalog\Model\Product\Option\Value[] + * @param array $newValues + * @param \Magento\Catalog\Model\Product\Option\Value[] $originalValues * @return array */ protected function markRemovedValues($newValues, $originalValues) @@ -234,6 +236,8 @@ protected function markRemovedValues($newValues, $originalValues) } /** + * Get hydrator pool + * * @return \Magento\Framework\EntityManager\HydratorPool * @deprecated 101.0.0 */ diff --git a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php index c4a2d60414a7b..0941aa2478935 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php @@ -28,6 +28,8 @@ public function __construct( } /** + * Perform action on relation/extension attribute + * * @param object $entity * @param array $arguments * @return \Magento\Catalog\Api\Data\ProductInterface|object @@ -35,6 +37,10 @@ public function __construct( */ public function execute($entity, $arguments = []) { + if ($entity->getOptionsSaved()) { + return $entity; + } + $options = $entity->getOptions(); $optionIds = []; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php index b9e629912a5b3..9cf1431317944 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php @@ -7,6 +7,10 @@ namespace Magento\Catalog\Model\ResourceModel; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; +use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; +use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; /** * Catalog entity abstract model @@ -37,16 +41,18 @@ abstract class AbstractResource extends \Magento\Eav\Model\Entity\AbstractEntity * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\Factory $modelFactory * @param array $data + * @param UniqueValidationInterface|null $uniqueValidator */ public function __construct( \Magento\Eav\Model\Entity\Context $context, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Catalog\Model\Factory $modelFactory, - $data = [] + $data = [], + UniqueValidationInterface $uniqueValidator = null ) { $this->_storeManager = $storeManager; $this->_modelFactory = $modelFactory; - parent::__construct($context, $data); + parent::__construct($context, $data, $uniqueValidator); } /** @@ -86,9 +92,7 @@ protected function _isApplicableAttribute($object, $attribute) /** * Check whether attribute instance (attribute, backend, frontend or source) has method and applicable * - * @param AbstractAttribute|\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend - * |\Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend - * |\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource $instance + * @param AbstractAttribute|AbstractBackend|AbstractFrontend|AbstractSource $instance * @param string $method * @param array $args array of arguments * @return boolean @@ -112,6 +116,7 @@ protected function _isCallableAttributeInstance($instance, $method, $args) /** * Retrieve select object for loading entity attributes values + * * Join attribute store value * * @param \Magento\Framework\DataObject $object @@ -244,6 +249,7 @@ protected function _saveAttributeValue($object, $attribute, $value) /** * Check if attribute present for non default Store View. + * * Prevent "delete" query locking in a case when nothing to delete * * @param AbstractAttribute $attribute diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php index 707ebbb2964cc..23f612582f42e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php @@ -236,6 +236,8 @@ public function afterSave() ) { $this->_indexerEavProcessor->markIndexerAsInvalid(); } + + $this->_source = null; return parent::afterSave(); } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php index d71ec23881982..24174391be829 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php @@ -8,6 +8,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; /** * Product entity resource model @@ -101,6 +102,7 @@ class Product extends AbstractResource * @param \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes * @param array $data * @param TableMaintainer|null $tableMaintainer + * @param UniqueValidationInterface|null $uniqueValidator * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -115,7 +117,8 @@ public function __construct( \Magento\Eav\Model\Entity\TypeFactory $typeFactory, \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes, $data = [], - TableMaintainer $tableMaintainer = null + TableMaintainer $tableMaintainer = null, + UniqueValidationInterface $uniqueValidator = null ) { $this->_categoryCollectionFactory = $categoryCollectionFactory; $this->_catalogCategory = $catalogCategory; @@ -127,7 +130,8 @@ public function __construct( $context, $storeManager, $modelFactory, - $data + $data, + $uniqueValidator ); $this->connectionName = 'catalog'; $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); @@ -289,7 +293,7 @@ protected function _afterSave(\Magento\Framework\DataObject $product) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete($object) { @@ -593,7 +597,7 @@ public function countAll() } /** - * {@inheritdoc} + * @inheritdoc */ public function validate($object) { @@ -633,7 +637,7 @@ public function load($object, $entityId, $attributes = []) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.UnusedLocalVariable) * @since 101.0.0 */ @@ -675,6 +679,8 @@ public function save(\Magento\Framework\Model\AbstractModel $object) } /** + * Retrieve entity manager object + * * @return \Magento\Framework\EntityManager\EntityManager */ private function getEntityManager() @@ -687,6 +693,8 @@ private function getEntityManager() } /** + * Retrieve ProductWebsiteLink object + * * @deprecated 101.1.0 * @return ProductWebsiteLink */ @@ -696,6 +704,8 @@ private function getProductWebsiteLink() } /** + * Retrieve CategoryLink object + * * @deprecated 101.1.0 * @return \Magento\Catalog\Model\ResourceModel\Product\CategoryLink */ @@ -710,9 +720,10 @@ private function getProductCategoryLink() /** * Extends parent method to be appropriate for product. + * * Store id is required to correctly identify attribute value we are working with. * - * {@inheritdoc} + * @inheritdoc * @since 101.1.0 */ protected function getAttributeRow($entity, $object, $attribute) diff --git a/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php b/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php new file mode 100644 index 0000000000000..dd750cfbc696e --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php @@ -0,0 +1,56 @@ +productRepository = $productRepository; + } + + /** + * Update product 'has_options' and 'required_options' attributes after option save + * + * @param \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface $subject + * @param \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option + * + * @return \Magento\Catalog\Api\Data\ProductCustomOptionInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave( + \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface $subject, + \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option + ) { + $product = $this->productRepository->get($option->getProductSku()); + if (!$product->getHasOptions() || + ($option->getIsRequire() && !$product->getRequiredOptions())) { + $product->setCanSaveCustomOptions(true); + $product->setOptionsSaved(true); + $currentOptions = array_filter($product->getOptions(), function ($iOption) use ($option) { + return $option->getOptionId() != $iOption->getOptionId(); + }); + $currentOptions[] = $option; + $product->setOptions($currentOptions); + $product->save(); + } + + return $option; + } +} diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml index 0d82ba3817df9..0082b376bc4a6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml @@ -30,6 +30,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml new file mode 100644 index 0000000000000..f7cd2e7076288 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml index 03a004e500aef..bf0762b4b0319 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml @@ -115,6 +115,27 @@ true ProductAttributeFrontendLabel + + attribute + boolean + global + false + false + true + true + true + true + true + true + true + true + true + true + true + true + true + ProductAttributeFrontendLabel + attribute text diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index 50e3e5864f4c3..d136661e917cb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -35,6 +35,9 @@ EavStockItem CustomAttributeCategoryIds + + 100 + ApiProductDescription ApiProductShortDescription @@ -146,6 +149,15 @@ EavStockItem CustomAttributeProductAttribute + + 50 + + + 60 + + + 70 + api-simple-product-two simple diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml index ef6fb99e88eed..14e714cb2b6b7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml @@ -13,7 +13,7 @@ - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml index 05be20b14acc0..ee6af87b8e2c5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml @@ -78,6 +78,7 @@ type="button" selector="#advanced_fieldset-wrapper"/> + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml index 0a1804aa284dc..697648cedb7ba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml @@ -20,6 +20,7 @@ + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAttributeSection.xml new file mode 100644 index 0000000000000..e159a4ce5c0b6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAttributeSection.xml @@ -0,0 +1,24 @@ + + + + +
+ +
+
+ + + + + + + + +
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml index 337a3dd53f593..3f67e4b087cc4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml @@ -189,4 +189,9 @@ +
+ + + +
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml index 178e58ef2d649..f35eb63ee0e0a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml @@ -16,6 +16,7 @@ + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 6a4ac0d7683c7..4114b199715cb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -31,6 +31,8 @@ + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml new file mode 100644 index 0000000000000..282331924bca3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml @@ -0,0 +1,68 @@ + + + + + + + + + <description value="Check that New Attribute from Product is create"/> + <severity value="MAJOR"/> + <testCaseId value="MC-12296"/> + <useCaseId value="MAGETWO-59055"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + + <!--Create product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + </before> + <after> + <!--Delete create data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + + <!--Delete store views--> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteFirstStoreView"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteSecondStoreView"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + + <!--Delete Attribute--> + <actionGroup ref="deleteProductAttribute" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="productDropDownAttribute"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create 2 store views--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createFirstStoreView"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + + <!--Go to created product page and create new attribute--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="openAdminEditPage"/> + <actionGroup ref="AdminCreateAttributeWithValueWithTwoStoreViesFromProductPage" stepKey="createAttribute"> + <argument name="attributeName" value="{{productDropDownAttribute.attribute_code}}"/> + <argument name="attributeType" value="Dropdown"/> + <argument name="firstStoreViewName" value="{{customStoreEN.name}}"/> + <argument name="secondStoreViewName" value="{{customStoreFR.name}}"/> + </actionGroup> + + <!--Check attribute existence in product page attribute section--> + <conditionalClick selector="{{AdminProductAttributeSection.attributeSectionHeader}}" dependentSelector="{{AdminProductAttributeSection.attributeSection}}" visible="false" stepKey="openAttributeSection"/> + <seeElement selector="{{AdminProductAttributeSection.dropDownAttribute(productDropDownAttribute.attribute_code)}}" stepKey="seeNewAttributeInProductPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryListTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryListTest.php index b8b76524099f4..f78c0ad924954 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryListTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryListTest.php @@ -93,7 +93,7 @@ public function testGetList() $collection = $this->getMockBuilder(Collection::class)->disableOriginalConstructor()->getMock(); $collection->expects($this->once())->method('getSize')->willReturn($totalCount); - $collection->expects($this->once())->method('getAllIds')->willReturn([$categoryIdFirst, $categoryIdSecond]); + $collection->expects($this->once())->method('getItems')->willReturn([$categoryFirst, $categorySecond]); $this->collectionProcessorMock->expects($this->once()) ->method('process') diff --git a/app/code/Magento/Catalog/etc/webapi_rest/di.xml b/app/code/Magento/Catalog/etc/webapi_rest/di.xml index 2a5d60222e9f8..44cdd473bf74e 100644 --- a/app/code/Magento/Catalog/etc/webapi_rest/di.xml +++ b/app/code/Magento/Catalog/etc/webapi_rest/di.xml @@ -19,4 +19,7 @@ <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> </type> + <type name="Magento\Catalog\Api\ProductCustomOptionRepositoryInterface"> + <plugin name="updateProductCustomOptionsAttributes" type="Magento\Catalog\Plugin\Model\Product\Option\UpdateProductCustomOptionsAttributes"/> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/webapi_soap/di.xml b/app/code/Magento/Catalog/etc/webapi_soap/di.xml index 2a5d60222e9f8..44cdd473bf74e 100644 --- a/app/code/Magento/Catalog/etc/webapi_soap/di.xml +++ b/app/code/Magento/Catalog/etc/webapi_soap/di.xml @@ -19,4 +19,7 @@ <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> </type> + <type name="Magento\Catalog\Api\ProductCustomOptionRepositoryInterface"> + <plugin name="updateProductCustomOptionsAttributes" type="Magento\Catalog\Plugin\Model\Product\Option\UpdateProductCustomOptionsAttributes"/> + </type> </config> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml index 00a1580923a7b..ee67acd0ebd46 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml @@ -20,7 +20,7 @@ "categoryCheckboxTree": { "dataUrl": "<?= $block->escapeUrl($block->getLoadTreeUrl()) ?>", "divId": "<?= /* @noEscape */ $divId ?>", - "rootVisible": <?= /* @noEscape */ $block->getRoot()->getIsVisible() ? 'true' : 'false' ?>, + "rootVisible": false, "useAjax": <?= $block->escapeHtml($block->getUseAjax()) ?>, "currentNodeId": <?= (int)$block->getCategoryId() ?>, "jsFormObject": "<?= /* @noEscape */ $block->getJsFormObject() ?>", @@ -28,7 +28,7 @@ "checked": "<?= $block->escapeHtml($block->getRoot()->getChecked()) ?>", "allowdDrop": <?= /* @noEscape */ $block->getRoot()->getIsVisible() ? 'true' : 'false' ?>, "rootId": <?= (int)$block->getRoot()->getId() ?>, - "expanded": <?= (int)$block->getIsWasExpanded() ?>, + "expanded": true, "categoryId": <?= (int)$block->getCategoryId() ?>, "treeJson": <?= /* @noEscape */ $block->getTreeJson() ?> } diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml index 93666470b1b2c..f448edc692ce2 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml @@ -302,6 +302,7 @@ } <?php endif;?> //updateContent(url); //commented since ajax requests replaced with http ones to load a category + jQuery('#tree-div').find('.x-tree-node-el').first().remove(); } jQuery(function () { diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml index dbe66ef1aecd3..69737b8a37c1c 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml @@ -160,7 +160,7 @@ jQuery(function() loader: categoryLoader, enableDD: false, containerScroll: true, - rootVisible: '<?= /* @escapeNotVerified */ $block->getRoot()->getIsVisible() ?>', + rootVisible: false, useAjax: true, currentNodeId: <?= (int) $block->getCategoryId() ?>, addNodeTo: false @@ -177,7 +177,7 @@ jQuery(function() text: 'Psw', draggable: false, id: <?= (int) $block->getRoot()->getId() ?>, - expanded: <?= (int) $block->getIsWasExpanded() ?>, + expanded: true, category_id: <?= (int) $block->getCategoryId() ?> }; diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 52ab8d49ec546..75249e4907862 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -351,6 +351,7 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity /** * Product constructor. + * * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Eav\Model\Config $config * @param \Magento\Framework\App\ResourceConnection $resource @@ -941,15 +942,17 @@ protected function getExportData() protected function loadCollection(): array { $data = []; - $collection = $this->_getEntityCollection(); foreach (array_keys($this->_storeIdToCode) as $storeId) { + $collection->setOrder('entity_id', 'asc'); + $this->_prepareEntityCollection($collection); $collection->setStoreId($storeId); + $collection->load(); foreach ($collection as $itemId => $item) { $data[$itemId][$storeId] = $item; } + $collection->clear(); } - $collection->clear(); return $data; } diff --git a/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php b/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php index 6d343fe149d21..fabe504fbe31c 100644 --- a/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php +++ b/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php @@ -71,6 +71,8 @@ public function mapConditionsToSearchCriteria(CombinedCondition $conditions): Se } /** + * Convert condition to filter group + * * @param ConditionInterface $condition * @return null|\Magento\Framework\Api\CombinedFilterGroup|\Magento\Framework\Api\Filter * @throws InputException @@ -89,6 +91,8 @@ private function mapConditionToFilterGroup(ConditionInterface $condition) } /** + * Convert combined condition to filter group + * * @param Combine $combinedCondition * @return null|\Magento\Framework\Api\CombinedFilterGroup * @throws InputException @@ -121,6 +125,8 @@ private function mapCombinedConditionToFilterGroup(CombinedCondition $combinedCo } /** + * Convert simple condition to filter group + * * @param ConditionInterface $productCondition * @return FilterGroup|Filter * @throws InputException @@ -139,6 +145,8 @@ private function mapSimpleConditionToFilterGroup(ConditionInterface $productCond } /** + * Convert simple condition with array value to filter group + * * @param ConditionInterface $productCondition * @return FilterGroup * @throws InputException @@ -161,6 +169,8 @@ private function processSimpleConditionWithArrayValue(ConditionInterface $produc } /** + * Get glue for multiple values by operator + * * @param string $operator * @return string */ @@ -211,6 +221,8 @@ private function reverseSqlOperatorInFilter(Filter $filter) } /** + * Convert filters array into combined filter group + * * @param array $filters * @param string $combinationMode * @return FilterGroup @@ -227,6 +239,8 @@ private function createCombinedFilterGroup(array $filters, string $combinationMo } /** + * Creating of filter object by filtering params + * * @param string $field * @param string $value * @param string $conditionType @@ -264,6 +278,7 @@ private function mapRuleOperatorToSQLCondition(string $ruleOperator): string '!{}' => 'nlike', // does not contains '()' => 'in', // is one of '!()' => 'nin', // is not one of + '<=>' => 'is_null' ]; if (!array_key_exists($ruleOperator, $operatorsMap)) { diff --git a/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php b/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php index ab650c94a0f08..0db178b2a0a6d 100644 --- a/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php +++ b/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php @@ -4,12 +4,11 @@ * See COPYING.txt for license details. */ -/** - * Catalog Rule Product Condition data model - */ namespace Magento\CatalogRule\Model\Rule\Condition; /** + * Catalog Rule Product Condition data model + * * @method string getAttribute() Returns attribute code */ class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct @@ -29,6 +28,9 @@ public function validate(\Magento\Framework\Model\AbstractModel $model) $oldAttrValue = $model->getData($attrCode); if ($oldAttrValue === null) { + if ($this->getOperator() === '<=>') { + return true; + } return false; } diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml index fe4042e8a2e9f..b0c4f2d8a609f 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml @@ -36,6 +36,41 @@ <waitForPageLoad stepKey="waitForApplied"/> </actionGroup> + + <actionGroup name="createCatalogPriceRule"> + <arguments> + <argument name="catalogRule" defaultValue="_defaultCatalogRule"/> + </arguments> + + <click stepKey="addNewRule" selector="{{AdminGridMainControls.add}}"/> + <fillField selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}" stepKey="fillName" /> + <fillField selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}" stepKey="fillDescription" /> + <selectOption selector="{{AdminNewCatalogPriceRule.websites}}" parameterArray="{{catalogRule.website_ids}}" stepKey="selectSite" /> + <click stepKey="openActionDropdown" selector="{{AdminNewCatalogPriceRule.actionsTab}}"/> + <fillField stepKey="fillDiscountValue" selector="{{AdminNewCatalogPriceRuleActions.discountAmount}}" userInput="{{catalogRule.discount_amount}}"/> + + <scrollToTopOfPage stepKey="scrollToTop"/> + <waitForPageLoad stepKey="waitForApplied"/> + </actionGroup> + + <actionGroup name="CreateCatalogPriceRuleConditionWithAttribute"> + <arguments> + <argument name="attributeName" type="string"/> + <argument name="targetValue" type="string"/> + <argument name="targetSelectValue" type="string"/> + </arguments> + + <click selector="{{AdminNewCatalogPriceRule.conditionsTab}}" stepKey="openConditionsTab"/> + <waitForPageLoad stepKey="waitForConditionTabOpened"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.newCondition}}" stepKey="addNewCondition"/> + <selectOption selector="{{AdminNewCatalogPriceRuleConditions.conditionSelect('1')}}" userInput="{{attributeName}}" stepKey="selectTypeCondition"/> + <waitForElement selector="{{AdminNewCatalogPriceRuleConditions.targetEllipsisValue('1', targetValue)}}" stepKey="waitForIsTarget"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.targetEllipsisValue('1', 'is')}}" stepKey="clickOnIs"/> + <selectOption selector="{{AdminNewCatalogPriceRuleConditions.targetSelect('1')}}" userInput="{{targetSelectValue}}" stepKey="selectTargetCondition"/> + <click selector="{{AdminNewCatalogPriceRule.fromDateButton}}" stepKey="clickFromCalender"/> + <click selector="{{AdminNewCatalogPriceRule.todayDate}}" stepKey="clickFromToday"/> + </actionGroup> + <!-- Apply all of the saved catalog price rules --> <actionGroup name="applyCatalogPriceRules"> <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> @@ -77,4 +112,8 @@ <actionGroup name="selectGeneralCustomerGroupActionGroup"> <selectOption selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="General" stepKey="selectCustomerGroup"/> </actionGroup> + + <actionGroup name="selectNotLoggedInCustomerGroupActionGroup"> + <selectOption selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml index 71bdfe0613bb7..5b75708d1ae0a 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml @@ -77,4 +77,21 @@ <data key="simple_action">by_percent</data> <data key="discount_amount">96</data> </entity> + + <entity name="CatalogRuleWithAllCustomerGroups" type="catalogRule"> + <data key="name" unique="suffix">CatalogPriceRule</data> + <data key="description">Catalog Price Rule Description</data> + <data key="is_active">1</data> + <array key="customer_group_ids"> + <item>0</item> + <item>1</item> + <item>2</item> + <item>3</item> + </array> + <array key="website_ids"> + <item>1</item> + </array> + <data key="simple_action">by_percent</data> + <data key="discount_amount">10</data> + </entity> </entities> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml index 7cfb5bf40be55..635260888e7fb 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml @@ -41,6 +41,8 @@ <element name="newCondition" type="button" selector=".rule-param.rule-param-new-child"/> <element name="conditionSelect" type="select" selector="select#conditions__{{var}}__new_child" parameterized="true"/> <element name="targetEllipsis" type="button" selector="//li[{{var}}]//a[@class='label'][text() = '...']" parameterized="true"/> + <element name="targetEllipsisValue" type="button" selector="//ul[@id='conditions__{{var}}__children']//a[contains(text(), '{{var1}}')]" parameterized="true" timeout="30"/> + <element name="targetSelect" type="select" selector="//ul[@id='conditions__{{var}}__children']//select" parameterized="true" timeout="30"/> <element name="targetInput" type="input" selector="input#conditions__{{var1}}--{{var2}}__value" parameterized="true"/> <element name="applyButton" type="button" selector="#conditions__{{var1}}__children li:nth-of-type({{var2}}) a.rule-param-apply" parameterized="true"/> </section> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml new file mode 100644 index 0000000000000..053a8c33e640c --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml @@ -0,0 +1,136 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnableAttributeIsUndefinedCatalogPriceRuleTest"> + <annotations> + <features value="CatalogRule"/> + <title value="Enable 'is undefined' condition to Scope Catalog Price rules by custom product attribute"/> + <description value="Enable 'is undefined' condition to Scope Catalog Price rules by custom product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13654"/> + <useCaseId value="MC-10971"/> + <group value="CatalogRule"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + + <createData entity="ApiCategory" stepKey="createFirstCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createFirstProduct"> + <requiredEntity createDataKey="createFirstCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSecondProduct"> + <requiredEntity createDataKey="createFirstCategory"/> + </createData> + <createData entity="productYesNoAttribute" stepKey="createProductAttribute"/> + <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle"> + <requiredEntity createDataKey="createProductAttribute"/> + </createData> + + <createData entity="SimpleSubCategory" stepKey="createSecondCategory"/> + <createData entity="SimpleProduct3" stepKey="createThirdProduct"> + <requiredEntity createDataKey="createSecondCategory"/> + </createData> + <createData entity="SimpleProduct4" stepKey="createForthProduct"> + <requiredEntity createDataKey="createSecondCategory"/> + </createData> + <createData entity="productDropDownAttribute" stepKey="createSecondProductAttribute"> + <field key="scope">website</field> + </createData> + </before> + <after> + + <!--Delete created data--> + <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> + <argument name="name" value="{{CatalogRuleWithAllCustomerGroups.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + <click stepKey="resetFilters" selector="{{AdminSecondaryGridSection.resetFilters}}"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createFirstCategory" stepKey="deleteFirstCategory"/> + <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> + <deleteData createDataKey="createForthProduct" stepKey="deleteForthProduct"/> + <deleteData createDataKey="createSecondCategory" stepKey="deleteSecondCategory"/> + <deleteData createDataKey="createSecondProductAttribute" stepKey="deleteSecondProductAttribute"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create catalog price rule--> + <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> + <waitForPageLoad stepKey="waitForPriceRulePage"/> + <actionGroup ref="createCatalogPriceRule" stepKey="createCatalogPriceRule"> + <argument name="catalogRule" value="CatalogRuleWithAllCustomerGroups"/> + </actionGroup> + <actionGroup ref="selectNotLoggedInCustomerGroupActionGroup" stepKey="selectCustomerGroup"/> + <actionGroup ref="CreateCatalogPriceRuleConditionWithAttribute" stepKey="createCatalogPriceRuleCondition"> + <argument name="attributeName" value="$$createProductAttribute.attribute[frontend_labels][0][label]$$"/> + <argument name="targetValue" value="is"/> + <argument name="targetSelectValue" value="is undefined"/> + </actionGroup> + <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!--Check Catalog Price Rule for first product--> + <amOnPage url="{{StorefrontProductPage.url($$createFirstProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToFirstProductPage"/> + <waitForPageLoad stepKey="waitForFirstProductPageLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabFirstProductUpdatedPrice"/> + <assertEquals expected='$110.70' expectedType="string" actual="($grabFirstProductUpdatedPrice)" stepKey="assertFirstProductUpdatedPrice"/> + + <!--Check Catalog Price Rule for second product--> + <amOnPage url="{{StorefrontProductPage.url($$createSecondProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSecondProductPage"/> + <waitForPageLoad stepKey="waitForSecondProductPageLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabSecondProductUpdatedPrice"/> + <assertEquals expected='$110.70' expectedType="string" actual="($grabFirstProductUpdatedPrice)" stepKey="assertSecondProductUpdatedPrice"/> + + <!--Delete previous attribute and Catalog Price Rule--> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> + <argument name="name" value="{{CatalogRuleWithAllCustomerGroups.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + + <!--Add new attribute to Default set--> + <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle1"> + <requiredEntity createDataKey="createSecondProductAttribute"/> + </createData> + + <!--Create new Catalog Price Rule--> + <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage1"/> + <waitForPageLoad stepKey="waitForPriceRulePage1"/> + <actionGroup ref="createCatalogPriceRule" stepKey="createCatalogPriceRule1"> + <argument name="catalogRule" value="CatalogRuleWithAllCustomerGroups"/> + </actionGroup> + <actionGroup ref="selectNotLoggedInCustomerGroupActionGroup" stepKey="selectCustomerGroup1"/> + <actionGroup ref="CreateCatalogPriceRuleConditionWithAttribute" stepKey="createCatalogPriceRuleCondition1"> + <argument name="attributeName" value="$$createSecondProductAttribute.attribute[frontend_labels][0][label]$$"/> + <argument name="targetValue" value="is"/> + <argument name="targetSelectValue" value="is undefined"/> + </actionGroup> + <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules1"/> + <magentoCLI command="indexer:reindex" stepKey="reindex1"/> + <magentoCLI command="cache:flush" stepKey="flushCache1"/> + + <!--Check Catalog Price Rule for third product--> + <amOnPage url="{{StorefrontProductPage.url($$createThirdProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToThirdProductPage"/> + <waitForPageLoad stepKey="waitForThirdProductPageLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabThirdProductUpdatedPrice"/> + <assertEquals expected='$110.70' expectedType="string" actual="($grabThirdProductUpdatedPrice)" stepKey="assertThirdProductUpdatedPrice"/> + + <!--Check Catalog Price Rule for forth product--> + <amOnPage url="{{StorefrontProductPage.url($$createForthProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToForthProductPage"/> + <waitForPageLoad stepKey="waitForForthProductPageLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabForthProductUpdatedPrice"/> + <assertEquals expected='$110.70' expectedType="string" actual="($grabForthProductUpdatedPrice)" stepKey="assertForthProductUpdatedPrice"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php index 711d5a2da9ff5..9e47830debfc4 100644 --- a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php +++ b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php @@ -197,6 +197,7 @@ public function getCacheKeyInfo() $this->httpContext->getValue(\Magento\Customer\Model\Context::CONTEXT_GROUP), (int) $this->getRequest()->getParam($this->getData('page_var_name'), 1), $this->getProductsPerPage(), + $this->getProductsCount(), $conditions, $this->json->serialize($this->getRequest()->getParams()), $this->getTemplate(), diff --git a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php index dc6e100ab1ad8..a789753795724 100644 --- a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php +++ b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php @@ -167,6 +167,7 @@ public function testGetCacheKeyInfo() 'context_group', 1, 5, + 10, 'some_serialized_conditions', json_encode('request_params'), 'test_template', diff --git a/app/code/Magento/Checkout/Block/Cart/Sidebar.php b/app/code/Magento/Checkout/Block/Cart/Sidebar.php index 92ba6bf2bbbb1..c5e309df3cad6 100644 --- a/app/code/Magento/Checkout/Block/Cart/Sidebar.php +++ b/app/code/Magento/Checkout/Block/Cart/Sidebar.php @@ -82,11 +82,14 @@ public function getConfig() 'baseUrl' => $this->getBaseUrl(), 'minicartMaxItemsVisible' => $this->getMiniCartMaxItemsCount(), 'websiteId' => $this->_storeManager->getStore()->getWebsiteId(), - 'maxItemsToDisplay' => $this->getMaxItemsToDisplay() + 'maxItemsToDisplay' => $this->getMaxItemsToDisplay(), + 'storeId' => $this->_storeManager->getStore()->getId() ]; } /** + * Get serialized config + * * @return string * @since 100.2.0 */ @@ -96,6 +99,8 @@ public function getSerializedConfig() } /** + * Get image html template + * * @return string */ public function getImageHtmlTemplate() @@ -130,6 +135,7 @@ public function getShoppingCartUrl() * * @return string * @codeCoverageIgnore + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getUpdateItemQtyUrl() { @@ -141,6 +147,7 @@ public function getUpdateItemQtyUrl() * * @return string * @codeCoverageIgnore + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getRemoveItemUrl() { @@ -210,6 +217,7 @@ private function getMiniCartMaxItemsCount() /** * Returns maximum cart items to display + * * This setting regulates how many items will be displayed in minicart * * @return int diff --git a/app/code/Magento/Checkout/CustomerData/Cart.php b/app/code/Magento/Checkout/CustomerData/Cart.php index 01e91d75c02d9..169be4cc62f01 100644 --- a/app/code/Magento/Checkout/CustomerData/Cart.php +++ b/app/code/Magento/Checkout/CustomerData/Cart.php @@ -10,6 +10,8 @@ /** * Cart source + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Cart extends \Magento\Framework\DataObject implements SectionSourceInterface { @@ -98,7 +100,8 @@ public function getSectionData() 'items' => $this->getRecentItems(), 'extra_actions' => $this->layout->createBlock(\Magento\Catalog\Block\ShortcutButtons::class)->toHtml(), 'isGuestCheckoutAllowed' => $this->isGuestCheckoutAllowed(), - 'website_id' => $this->getQuote()->getStore()->getWebsiteId() + 'website_id' => $this->getQuote()->getStore()->getWebsiteId(), + 'storeId' => $this->getQuote()->getStore()->getStoreId() ]; } diff --git a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php index d2bd680aa38f3..e0de45a3f0dea 100644 --- a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php @@ -118,7 +118,9 @@ public function savePaymentInformation( $shippingAddress = $quote->getShippingAddress(); if ($shippingAddress && $shippingAddress->getShippingMethod()) { $shippingRate = $shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()); - $shippingAddress->setLimitCarrier($shippingRate->getCarrier()); + $shippingAddress->setLimitCarrier( + $shippingRate ? $shippingRate->getCarrier() : $shippingAddress->getShippingMethod() + ); } } $this->paymentMethodManagement->set($cartId, $paymentMethod); diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml index 0d3c6e419cc07..dcfb12fd4e965 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml @@ -26,6 +26,7 @@ parameterized="true"/> <element name="RemoveItem" type="button" selector="//table[@id='shopping-cart-table']//tbody//tr[contains(@class,'item-actions')]//a[contains(@class,'action-delete')]"/> + <element name="productName" type="text" selector="//tbody[@class='cart item']//strong[@class='product-item-name']"/> <element name="nthItemOption" type="block" selector=".item:nth-of-type({{numElement}}) .item-options" parameterized="true"/> <element name="nthEditButton" type="block" selector=".item:nth-of-type({{numElement}}) .action-edit" parameterized="true"/> <element name="nthBundleOptionName" type="text" selector=".product-item-details .item-options:nth-of-type({{numOption}}) dt" parameterized="true"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml index ad2a43eb90c8c..6838824400b96 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml @@ -19,5 +19,9 @@ <element name="telephone" type="input" selector="input[name=telephone]"/> <element name="next" type="button" selector="button.button.action.continue.primary" timeout="30"/> <element name="firstShippingMethod" type="radio" selector=".row:nth-of-type(1) .col-method .radio"/> + + <!--Order Summary--> + <element name="itemInCart" type="button" selector="//div[@class='title']"/> + <element name="productName" type="text" selector="//strong[@class='product-item-name']"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml index a894f2fbb1af9..bdb02835c6276 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml @@ -13,6 +13,7 @@ <element name="productLinkByName" type="button" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details']//a[contains(text(), '{{var1}}')]" parameterized="true"/> <element name="productPriceByName" type="text" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//span[@class='price']" parameterized="true"/> <element name="productImageByName" type="text" selector="//header//ol[@id='mini-cart']//span[@class='product-image-container']//img[@alt='{{var1}}']" parameterized="true"/> + <element name="productName" type="text" selector=".product-item-name"/> <element name="productOptionsDetailsByName" type="button" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//span[.='See Details']" parameterized="true"/> <element name="productOptionByNameAndAttribute" type="text" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//dt[@class='label' and .='{{var2}}']/following-sibling::dd[@class='values']//span" parameterized="true"/> <element name="showCart" type="button" selector="a.showcart"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml new file mode 100644 index 0000000000000..3401369a8c749 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest"> + <annotations> + <features value="Checkout"/> + <title value="Checking Product name in Minicart and on Checkout page with different store views"/> + <description value="Checking Product name in Minicart and on Checkout page with different store views"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96466"/> + <useCaseId value="MAGETWO-96421"/> + <group value="checkout"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create a product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <!--Go to created product page--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="goToEditPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + + <!--Switch to second store view and change the product name--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="switchToCustomStoreView"> + <argument name="storeViewName" value="{{customStore.name}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="$$createProduct.name$$-new" stepKey="fillProductName"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + + <!--Add product to cart--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$$"/> + </actionGroup> + + <!--Switch to second store view--> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreView"> + <argument name="storeView" value="customStore"/> + </actionGroup> + <waitForPageLoad stepKey="waitForStoreView"/> + + <!--Check product name in Minicart--> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> + <grabTextFrom selector="{{StorefrontMinicartSection.productName}}" stepKey="grabProductNameMinicart"/> + <assertContains expected="$$createProduct.name$$" actual="$grabProductNameMinicart" stepKey="assertProductNameMinicart"/> + <assertContains expectedType="string" expected="-new" actual="$grabProductNameMinicart" stepKey="assertProductNameMinicart1"/> + + <!--Check product name in Shopping Cart page--> + <click selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="clickViewAndEdit"/> + <waitForPageLoad stepKey="waitForShoppingCartPageLoad"/> + <grabTextFrom selector="{{CheckoutCartProductSection.productName}}" stepKey="grabProductNameCart"/> + <assertContains expected="$$createProduct.name$$" actual="$grabProductNameCart" stepKey="assertProductNameCart"/> + <assertContains expectedType="string" expected="-new" actual="$grabProductNameCart" stepKey="assertProductNameCart1"/> + + <!--Proceed to checkout and check product name in Order Summary area--> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="proceedToCheckout"/> + <waitForPageLoad stepKey="waitForShippingPageLoad"/> + <click selector="{{CheckoutShippingGuestInfoSection.itemInCart}}" stepKey="clickItemInCart"/> + <grabTextFrom selector="{{CheckoutShippingGuestInfoSection.productName}}" stepKey="grabProductNameShipping"/> + <assertContains expected="$$createProduct.name$$" actual="$grabProductNameShipping" stepKey="assertProductNameShipping"/> + <assertContains expectedType="string" expected="-new" actual="$grabProductNameShipping" stepKey="assertProductNameShipping1"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php index 1c5224d007ec8..f69ced3b094c7 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php @@ -144,7 +144,8 @@ public function testGetConfig() 'baseUrl' => $baseUrl, 'minicartMaxItemsVisible' => 3, 'websiteId' => 100, - 'maxItemsToDisplay' => 8 + 'maxItemsToDisplay' => 8, + 'storeId' => null ]; $valueMap = [ @@ -161,7 +162,7 @@ public function testGetConfig() $this->urlBuilderMock->expects($this->exactly(4)) ->method('getUrl') ->willReturnMap($valueMap); - $this->storeManagerMock->expects($this->exactly(2))->method('getStore')->willReturn($storeMock); + $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); $storeMock->expects($this->once())->method('getBaseUrl')->willReturn($baseUrl); $this->scopeConfigMock->expects($this->at(0)) diff --git a/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php b/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php index 75e181cbabd08..e3e13cc5b1e69 100644 --- a/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php +++ b/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php @@ -113,7 +113,7 @@ public function testGetSectionData() $storeMock = $this->createPartialMock(\Magento\Store\Model\System\Store::class, ['getWebsiteId']); $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); - $quoteMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $quoteMock->expects($this->any())->method('getStore')->willReturn($storeMock); $productMock = $this->createPartialMock( \Magento\Catalog\Model\Product::class, @@ -162,6 +162,7 @@ public function testGetSectionData() 'isGuestCheckoutAllowed' => 1, 'website_id' => $websiteId, 'subtotalAmount' => 200, + 'storeId' => null ]; $this->assertEquals($expectedResult, $this->model->getSectionData()); } @@ -199,7 +200,7 @@ public function testGetSectionDataWithCompositeProduct() $storeMock = $this->createPartialMock(\Magento\Store\Model\System\Store::class, ['getWebsiteId']); $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); - $quoteMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $quoteMock->expects($this->any())->method('getStore')->willReturn($storeMock); $this->checkoutCartMock->expects($this->once())->method('getSummaryQty')->willReturn($summaryQty); $this->checkoutHelperMock->expects($this->once()) @@ -265,6 +266,7 @@ public function testGetSectionDataWithCompositeProduct() 'isGuestCheckoutAllowed' => 1, 'website_id' => $websiteId, 'subtotalAmount' => 200, + 'storeId' => null ]; $this->assertEquals($expectedResult, $this->model->getSectionData()); } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js index a2f8c8c56ff33..5e29fa209a641 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js @@ -81,6 +81,7 @@ define([ maxItemsToDisplay: window.checkout.maxItemsToDisplay, cart: {}, + // jscs:disable requireCamelCaseOrUpperCaseIdentifiers /** * @override */ @@ -101,12 +102,16 @@ define([ self.isLoading(true); }); - if (cartData()['website_id'] !== window.checkout.websiteId) { + if (cartData().website_id !== window.checkout.websiteId || + cartData().store_id !== window.checkout.storeId + ) { customerData.reload(['cart'], false); } return this._super(); }, + //jscs:enable requireCamelCaseOrUpperCaseIdentifiers + isLoading: ko.observable(false), initSidebar: initSidebar, diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/ClearWidgetsFromCMSContentActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/ClearWidgetsFromCMSContentActionGroup.xml new file mode 100644 index 0000000000000..2fa1b86a61572 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/ClearWidgetsFromCMSContentActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ClearWidgetsFromCMSContent"> + <amOnPage url="{{CmsPageEditPage.url('2')}}" stepKey="navigateToEditHomePagePage"/> + <waitForPageLoad stepKey="waitEditHomePagePageToLoad"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab" /> + <waitForElementNotVisible selector="{{CmsWYSIWYGSection.CheckIfTabExpand}}" stepKey="waitForTabExpand"/> + <executeJS function="jQuery('[id=\'cms_page_form_content_ifr\']').attr('name', 'preview-iframe')" stepKey="setPreviewFrameName"/> + <switchToIFrame selector="preview-iframe" stepKey="switchToIframe"/> + <fillField selector="{{TinyMCESection.EditorContent}}" userInput="Hello TinyMCE4!" stepKey="clearWidgets"/> + <switchToIFrame stepKey="switchOutFromIframe"/> + <executeJS function="tinyMCE.activeEditor.setContent('Hello TinyMCE4!');" stepKey="executeJSFillContent1"/> + <click selector="{{InsertWidgetSection.save}}" stepKey="saveWidget"/> + <waitForPageLoad stepKey="waitSaveToBeApplied"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the page." stepKey="seeSaveSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/CmsPageEditPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/CmsPageEditPage.xml new file mode 100644 index 0000000000000..885310d9399ae --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/CmsPageEditPage.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="CmsPageEditPage" area="admin" url="admin/cms_page/edit/page_id/{{var}}" parameterized="true"> + <section name="CmsNewPagePageActionsSection"/> + <section name="CmsNewPagePageBasicFieldsSection"/> + <section name="CmsNewPagePageContentSection"/> + <section name="CmsNewPagePageSeoSection"/> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml index 8559334238d2f..ff6167ffc10e0 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml @@ -31,6 +31,8 @@ <element name="InsertImage" type="button" selector=".mce-i-image" /> <element name="InsertTable" type="button" selector=".mce-i-table" /> <element name="SpecialCharacter" type="button" selector=".mce-i-charmap" /> + <element name="WidgetButton" type="button" selector="span[class*='magento-widget mceNonEditable']"/> + <element name="EditorContent" type="input" selector="#tinymce"/> </section> <section name="MediaGallerySection"> <element name="Browse" type="button" selector=".mce-i-browse"/> @@ -99,6 +101,7 @@ <element name="AddParam" type="button" selector=".rule-param-add"/> <element name="ConditionsDropdown" type="select" selector="#conditions__1__new_child"/> <element name="RuleParam" type="button" selector="//a[text()='...']"/> + <element name="RuleParam1" type="button" selector="(//span[@class='rule-param']//a)[{{var}}]" parameterized="true"/> <element name="RuleParamSelect" type="select" selector="//ul[contains(@class,'rule-param-children')]/li[{{arg1}}]//*[contains(@class,'rule-param')][{{arg2}}]//select" parameterized="true"/> <element name="RuleParamInput" type="input" selector="//ul[contains(@class,'rule-param-children')]/li[{{arg1}}]//*[contains(@class,'rule-param')][{{arg2}}]//input" parameterized="true"/> <element name="RuleParamLabel" type="input" selector="//ul[contains(@class,'rule-param-children')]/li[{{arg1}}]//*[contains(@class,'rule-param')][{{arg2}}]//a" parameterized="true"/> @@ -111,6 +114,7 @@ <element name="CompareBtn" type="button" selector=".action.tocompare"/> <element name="ClearCompare" type="button" selector="#compare-clear-all"/> <element name="AcceptClear" type="button" selector=".action-primary.action-accept" /> + <element name="ChooserName" type="input" selector="input[name='chooser_name']" /> <element name="SelectPageButton" type="button" selector="//button[@title='Select Page...']"/> <element name="SelectPageFilterInput" type="input" selector="input.admin__control-text[name='{{filterName}}']" parameterized="true"/> </section> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml index 06c041fabeb35..1a7b641070ad8 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml @@ -28,4 +28,32 @@ <click selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="collapseTaxClassesTab"/> <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfiguration"/> </actionGroup> + <actionGroup name="SetTaxApplyOnSetting"> + <arguments> + <argument name="userInput" type="string"/> + </arguments> + <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationAlgorithm}}" visible="false" stepKey="openTaxCalcSettingsSection"/> + <scrollTo selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" stepKey="goToCheckbox"/> + <uncheckOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" stepKey="enableApplyTaxOnSetting"/> + <selectOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOn}}" userInput="{{userInput}}" stepKey="setApplyTaxOn"/> + <scrollTo selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="scrollToTop"/> + <click selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" stepKey="collapseCalcSettingsTab"/> + <click selector="{{AdminConfigureTaxSection.save}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForConfigSaved"/> + <see userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + </actionGroup> + <actionGroup name="DisableTaxApplyOnOriginalPrice"> + <arguments> + <argument name="userInput" type="string"/> + </arguments> + <amOnPage url="{{AdminSalesTaxClassPage.url}}" stepKey="navigateToSalesTaxPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationAlgorithm}}" visible="false" stepKey="openTaxCalcSettingsSection"/> + <scrollTo selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" stepKey="goToCheckbox"/> + <selectOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOn}}" userInput="{{userInput}}" stepKey="setApplyTaxOff"/> + <checkOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" stepKey="disableApplyTaxOnSetting"/> + <click selector="{{AdminConfigureTaxSection.save}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForConfigSaved"/> + <see userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml index 82411faddfed7..eefaf5f3b539c 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml @@ -38,4 +38,15 @@ <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> <waitForPageLoad stepKey="waitForPageLoad2" /> </actionGroup> + <actionGroup name="EnabledWYSIWYGEditor"> + <amOnPage url="{{AdminContentManagementPage.url}}" stepKey="navigateToConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.EnableWYSIWYG}}" visible="false" stepKey="expandWYSIWYGOptionsTab"/> + <waitForElementVisible selector="{{ContentManagementSection.EnableWYSIWYG}}" stepKey="waitTabToExpand"/> + <uncheckOption selector="{{ContentManagementSection.EnableSystemValue}}" stepKey="enableEnableSystemValue"/> + <selectOption selector="{{ContentManagementSection.EnableWYSIWYG}}" userInput="Enabled by Default" stepKey="enableWYSIWYG"/> + <click selector="{{ContentManagementSection.WYSIWYGOptions}}" stepKey="collapseWYSIWYGOptionsTab"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="clickSaveConfig" /> + <see stepKey="seeSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the configuration."/> + </actionGroup> </actionGroups> 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 2502b79921e99..e07879e93a6b4 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php @@ -15,6 +15,8 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; /** + * Confugurable product view type + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api @@ -276,6 +278,8 @@ protected function getOptionImages() } /** + * Collect price options + * * @return array */ protected function getOptionPrices() @@ -314,6 +318,11 @@ protected function getOptionPrices() ), ], 'tierPrices' => $tierPrices, + 'msrpPrice' => [ + 'amount' => $this->localeFormat->getNumber( + $product->getMsrp() + ), + ], ]; } return $prices; diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index f98075f2294cc..46f10608bc95e 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -24,6 +24,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @api * @since 100.0.2 */ @@ -1385,7 +1386,7 @@ function ($item) { */ private function getUsedProductsCacheKey($keyParts) { - return md5(implode('_', $keyParts)); + return sha1(implode('_', $keyParts)); } /** diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php index 25d8412c91056..c5c2368720b98 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php @@ -379,6 +379,9 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage): 'percentage' => $percentage, ], ], + 'msrpPrice' => [ + 'amount' => null , + ] ], ], 'priceFormat' => [], diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js index 1df84d27a5c30..e732960421541 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js @@ -609,6 +609,13 @@ define([ } else { $(this.options.slyOldPriceSelector).hide(); } + + $(document).trigger('updateMsrpPriceBlock', + [ + optionId, + this.options.spConfig.optionPrices + ] + ); }, /** diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php index 3f2c7cda7608d..1bc6bb1da3680 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php @@ -71,7 +71,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function _construct() { @@ -94,7 +94,7 @@ protected function _prepareCollection() $quote = $this->getQuote(); if ($quote) { - $collection = $quote->getItemsCollection(false); + $collection = $quote->getItemsCollection(true); } else { $collection = $this->_dataCollectionFactory->create(); } @@ -106,7 +106,7 @@ protected function _prepareCollection() } /** - * {@inheritdoc} + * @inheritdoc */ protected function _prepareColumns() { @@ -144,7 +144,7 @@ protected function _prepareColumns() } /** - * {@inheritdoc} + * @inheritdoc */ public function getRowUrl($row) { @@ -152,7 +152,7 @@ public function getRowUrl($row) } /** - * {@inheritdoc} + * @inheritdoc */ public function getHeadersVisibility() { diff --git a/app/code/Magento/Customer/Block/Widget/Name.php b/app/code/Magento/Customer/Block/Widget/Name.php index d50045f4a4092..6f1b051af7465 100644 --- a/app/code/Magento/Customer/Block/Widget/Name.php +++ b/app/code/Magento/Customer/Block/Widget/Name.php @@ -55,7 +55,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ public function _construct() { @@ -245,10 +245,13 @@ public function getStoreLabel($attributeCode) */ public function getAttributeValidationClass($attributeCode) { - return $this->_addressHelper->getAttributeValidationClass($attributeCode); + $attributeMetadata = $this->_getAttribute($attributeCode); + return $attributeMetadata ? $attributeMetadata->getFrontendClass() : ''; } /** + * Check if attribute is required + * * @param string $attributeCode * @return bool */ @@ -259,6 +262,8 @@ private function _isAttributeRequired($attributeCode) } /** + * Check if attribute is visible + * * @param string $attributeCode * @return bool */ diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml index 7482193031091..0e31f0e0c7782 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCustomerSidebarSection"> <element name="sidebarTab" type="text" selector="//div[@id='block-collapsible-nav']//a[text()='{{var1}}']" parameterized="true"/> + <element name="sidebarCurrentTab" type="text" selector="//div[@id='block-collapsible-nav']//strong[contains(text(), '{{var}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php b/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php index 5cdcc6eb99af5..b752eaa111fa4 100644 --- a/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php +++ b/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php @@ -206,12 +206,15 @@ private function getElementsWithAutogeneratedName(Schema $schema, string $tableN foreach ($tableData[$elementType] as $tableElementData) { if ($tableElementData['type'] === 'foreign') { $referenceTable = $schema->getTableByName($tableElementData['referenceTable']); - $constraintName = $this->elementNameResolver->getFullFKName( - $table, - $table->getColumnByName($tableElementData['column']), - $referenceTable, - $referenceTable->getColumnByName($tableElementData['referenceColumn']) - ); + $column = $table->getColumnByName($tableElementData['column']); + $referenceColumn = $referenceTable->getColumnByName($tableElementData['referenceColumn']); + $constraintName = ($column !== false && $referenceColumn !== false) ? + $this->elementNameResolver->getFullFKName( + $table, + $column, + $referenceTable, + $referenceColumn + ) : null; } else { $constraintName = $this->elementNameResolver->getFullIndexName( $table, @@ -219,7 +222,9 @@ private function getElementsWithAutogeneratedName(Schema $schema, string $tableN $tableElementData['type'] ); } - $declaredStructure[$elementType][$constraintName] = true; + if ($constraintName) { + $declaredStructure[$elementType][$constraintName] = true; + } } } diff --git a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php index 0522ea0432176..d0a5e8de53ae9 100644 --- a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php +++ b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php @@ -10,6 +10,7 @@ use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; use Magento\Framework\App\Config\Element; use Magento\Framework\DataObject; use Magento\Framework\DB\Adapter\DuplicateException; @@ -215,12 +216,21 @@ abstract class AbstractEntity extends AbstractResource implements EntityInterfac */ protected $objectRelationProcessor; + /** + * @var UniqueValidationInterface + */ + private $uniqueValidator; + /** * @param Context $context * @param array $data + * @param UniqueValidationInterface|null $uniqueValidator */ - public function __construct(Context $context, $data = []) - { + public function __construct( + Context $context, + $data = [], + UniqueValidationInterface $uniqueValidator = null + ) { $this->_eavConfig = $context->getEavConfig(); $this->_resource = $context->getResource(); $this->_attrSetEntity = $context->getAttributeSetEntity(); @@ -229,6 +239,8 @@ public function __construct(Context $context, $data = []) $this->_universalFactory = $context->getUniversalFactory(); $this->transactionManager = $context->getTransactionManager(); $this->objectRelationProcessor = $context->getObjectRelationProcessor(); + $this->uniqueValidator = $uniqueValidator ?: + ObjectManager::getInstance()->get(UniqueValidationInterface::class); parent::__construct(); $properties = get_object_vars($this); foreach ($data as $key => $value) { @@ -488,6 +500,7 @@ public function addAttribute(AbstractAttribute $attribute, $object = null) /** * Get attributes by scope * + * @param string $suffix * @return array */ private function getAttributesByScope($suffix) @@ -958,12 +971,8 @@ public function checkAttributeUniqueValue(AbstractAttribute $attribute, $object) $data = $connection->fetchCol($select, $bind); - $objectId = $object->getData($entityIdField); - if ($objectId) { - if (isset($data[0])) { - return $data[0] == $objectId; - } - return true; + if ($object->getData($entityIdField)) { + return $this->uniqueValidator->validate($attribute, $object, $this, $entityIdField, $data); } return !count($data); @@ -1972,7 +1981,8 @@ public function afterDelete(DataObject $object) /** * Load attributes for object - * if the object will not pass all attributes for this entity type will be loaded + * + * If the object will not pass all attributes for this entity type will be loaded * * @param array $attributes * @param AbstractEntity|null $object diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php b/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php index 0991b3f9f4b23..56188ab997b76 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php @@ -80,6 +80,8 @@ public function getOptionText($value) } /** + * Get option id. + * * @param string $value * @return null|string */ diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php new file mode 100644 index 0000000000000..b68e79d7b7d20 --- /dev/null +++ b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Eav\Model\Entity\Attribute; + +use Magento\Framework\DataObject; +use Magento\Eav\Model\Entity\AbstractEntity; + +/** + * Interface for unique attribute validator + */ +interface UniqueValidationInterface +{ + /** + * Validate if attribute value is unique + * + * @param AbstractAttribute $attribute + * @param DataObject $object + * @param AbstractEntity $entity + * @param string $entityLinkField + * @param array $entityIds + * @return bool + */ + public function validate( + AbstractAttribute $attribute, + DataObject $object, + AbstractEntity $entity, + $entityLinkField, + array $entityIds + ); +} diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidator.php b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidator.php new file mode 100644 index 0000000000000..b1888b42bef92 --- /dev/null +++ b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidator.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Eav\Model\Entity\Attribute; + +use Magento\Framework\DataObject; +use Magento\Eav\Model\Entity\AbstractEntity; + +/** + * Class for validate unique attribute value + */ +class UniqueValidator implements UniqueValidationInterface +{ + /** + * @inheritdoc + */ + public function validate( + AbstractAttribute $attribute, + DataObject $object, + AbstractEntity $entity, + $entityLinkField, + array $entityIds + ) { + if (isset($entityIds[0])) { + return $entityIds[0] == $object->getData($entityLinkField); + } + return true; + } +} diff --git a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php index cd2fe7477ca60..7f6dfa2a5e9ab 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php +++ b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php @@ -5,13 +5,19 @@ */ namespace Magento\Eav\Model\ResourceModel; +use Magento\Eav\Model\Config; use Magento\Framework\DataObject; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\UnionExpression; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\EntityManager\Operation\AttributeInterface; use Magento\Framework\Model\Entity\ScopeInterface; use Magento\Framework\Model\Entity\ScopeResolver; use Psr\Log\LoggerInterface; +/** + * EAV read handler + */ class ReadHandler implements AttributeInterface { /** @@ -30,23 +36,21 @@ class ReadHandler implements AttributeInterface private $logger; /** - * @var \Magento\Eav\Model\Config + * @var Config */ private $config; /** - * ReadHandler constructor. - * * @param MetadataPool $metadataPool * @param ScopeResolver $scopeResolver * @param LoggerInterface $logger - * @param \Magento\Eav\Model\Config $config + * @param Config $config */ public function __construct( MetadataPool $metadataPool, ScopeResolver $scopeResolver, LoggerInterface $logger, - \Magento\Eav\Model\Config $config + Config $config ) { $this->metadataPool = $metadataPool; $this->scopeResolver = $scopeResolver; @@ -86,6 +90,8 @@ private function getEntityAttributes(string $entityType, DataObject $entity): ar } /** + * Get context variables + * * @param ScopeInterface $scope * @return array */ @@ -99,6 +105,8 @@ protected function getContextVariables(ScopeInterface $scope) } /** + * Execute read handler + * * @param string $entityType * @param array $entityData * @param array $arguments @@ -129,33 +137,40 @@ public function execute($entityType, $entityData, $arguments = []) } } if (count($attributeTables)) { - $attributeTables = array_keys($attributeTables); - foreach ($attributeTables as $attributeTable) { + $identifiers = null; + foreach ($attributeTables as $attributeTable => $attributeIds) { $select = $connection->select() ->from( ['t' => $attributeTable], ['value' => 't.value', 'attribute_id' => 't.attribute_id'] ) - ->where($metadata->getLinkField() . ' = ?', $entityData[$metadata->getLinkField()]); + ->where($metadata->getLinkField() . ' = ?', $entityData[$metadata->getLinkField()]) + ->where('attribute_id IN (?)', $attributeIds); + $attributeIdentifiers = []; foreach ($context as $scope) { //TODO: if (in table exists context field) $select->where( - $metadata->getEntityConnection()->quoteIdentifier($scope->getIdentifier()) . ' IN (?)', + $connection->quoteIdentifier($scope->getIdentifier()) . ' IN (?)', $this->getContextVariables($scope) - )->order('t.' . $scope->getIdentifier() . ' DESC'); + ); + $attributeIdentifiers[] = $scope->getIdentifier(); } + $attributeIdentifiers = array_unique($attributeIdentifiers); + $identifiers = array_intersect($identifiers ?? $attributeIdentifiers, $attributeIdentifiers); $selects[] = $select; } - $unionSelect = new \Magento\Framework\DB\Sql\UnionExpression( - $selects, - \Magento\Framework\DB\Select::SQL_UNION_ALL - ); - foreach ($connection->fetchAll($unionSelect) as $attributeValue) { + $this->applyIdentifierForSelects($selects, $identifiers); + $unionSelect = new UnionExpression($selects, Select::SQL_UNION_ALL, '( %s )'); + $orderedUnionSelect = $connection->select(); + $orderedUnionSelect->from(['u' => $unionSelect]); + $this->applyIdentifierForUnion($orderedUnionSelect, $identifiers); + $attributes = $connection->fetchAll($orderedUnionSelect); + foreach ($attributes as $attributeValue) { if (isset($attributesMap[$attributeValue['attribute_id']])) { $entityData[$attributesMap[$attributeValue['attribute_id']]] = $attributeValue['value']; } else { $this->logger->warning( - "Attempt to load value of nonexistent EAV attribute '{$attributeValue['attribute_id']}' + "Attempt to load value of nonexistent EAV attribute '{$attributeValue['attribute_id']}' for entity type '$entityType'." ); } @@ -163,4 +178,32 @@ public function execute($entityType, $entityData, $arguments = []) } return $entityData; } + + /** + * Apply identifiers column on select array + * + * @param Select[] $selects + * @param array $identifiers + */ + private function applyIdentifierForSelects(array $selects, array $identifiers) + { + foreach ($selects as $select) { + foreach ($identifiers as $identifier) { + $select->columns($identifier, 't'); + } + } + } + + /** + * Apply identifiers order on union select + * + * @param Select $unionSelect + * @param array $identifiers + */ + private function applyIdentifierForUnion(Select $unionSelect, array $identifiers) + { + foreach ($identifiers as $identifier) { + $unionSelect->order($identifier); + } + } } diff --git a/app/code/Magento/Eav/etc/di.xml b/app/code/Magento/Eav/etc/di.xml index 8e897b979d2f0..a4c89dcfab2af 100644 --- a/app/code/Magento/Eav/etc/di.xml +++ b/app/code/Magento/Eav/etc/di.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\Eav\Model\Entity\Setup\PropertyMapperInterface" type="Magento\Eav\Model\Entity\Setup\PropertyMapper\Composite" /> <preference for="Magento\Eav\Model\Entity\AttributeLoaderInterface" type="Magento\Eav\Model\Entity\AttributeLoader" /> + <preference for="Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface" type="Magento\Eav\Model\Entity\Attribute\UniqueValidator" /> <preference for="Magento\Eav\Api\Data\AttributeInterface" type="Magento\Eav\Model\Entity\Attribute" /> <preference for="Magento\Eav\Api\AttributeRepositoryInterface" type="Magento\Eav\Model\AttributeRepository" /> <preference for="Magento\Eav\Api\Data\AttributeGroupInterface" type="Magento\Eav\Model\Entity\Attribute\Group" /> diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php index e4f5de46c4c86..270ca37e2d42c 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php @@ -12,12 +12,18 @@ use Magento\Elasticsearch\Model\Adapter\BatchDataMapperInterface; use Magento\Elasticsearch\Model\Adapter\FieldType\Date as DateFieldType; use Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProviderInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; /** * Map product index data to search engine metadata */ class ProductDataMapper implements BatchDataMapperInterface { + /** + * @var AttributeOptionInterface[] + */ + private $attributeOptionsCache; + /** * @var Builder */ @@ -95,6 +101,7 @@ public function __construct( $this->excludedAttributes = array_merge($this->defaultExcludedAttributes, $excludedAttributes); $this->additionalFieldsProvider = $additionalFieldsProvider; $this->dataProvider = $dataProvider; + $this->attributeOptionsCache = []; } /** @@ -272,7 +279,13 @@ private function isAttributeDate(Attribute $attribute): bool private function getValuesLabels(Attribute $attribute, array $attributeValues): array { $attributeLabels = []; - foreach ($attribute->getOptions() as $option) { + + $options = $this->getAttributeOptions($attribute); + if (empty($options)) { + return $attributeLabels; + } + + foreach ($options as $option) { if (\in_array($option->getValue(), $attributeValues)) { $attributeLabels[] = $option->getLabel(); } @@ -281,6 +294,22 @@ private function getValuesLabels(Attribute $attribute, array $attributeValues): return $attributeLabels; } + /** + * Retrieve options for attribute + * + * @param Attribute $attribute + * @return array + */ + private function getAttributeOptions(Attribute $attribute): array + { + if (!isset($this->attributeOptionsCache[$attribute->getId()])) { + $options = $attribute->getOptions() ?? []; + $this->attributeOptionsCache[$attribute->getId()] = $options; + } + + return $this->attributeOptionsCache[$attribute->getId()]; + } + /** * Retrieve value for field. If field have only one value this method return it. * Otherwise will be returned array of these values. diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php index c7e2a4beabb5c..9e2659a757924 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php @@ -126,7 +126,7 @@ public function getFields(array $context = []): array foreach ($groups as $group) { $groupPriceKey = $this->fieldNameResolver->getFieldName( $priceAttribute, - ['customerGroupId' => $group->getId(), 'websiteId' => $context['websiteId']] + array_merge($context, ['customerGroupId' => $group->getId()]) ); $allAttributes[$groupPriceKey] = [ 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_FLOAT), diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php index bcfb7f5565b86..eeb48f805bccf 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php @@ -8,10 +8,13 @@ use Magento\Framework\Search\Request\BucketInterface as RequestBucketInterface; use Magento\Framework\Search\Dynamic\DataProviderInterface; +/** + * Builder for term buckets. + */ class Term implements BucketBuilderInterface { /** - * {@inheritdoc} + * @inheritdoc */ public function build( RequestBucketInterface $bucket, @@ -19,13 +22,15 @@ public function build( array $queryResult, DataProviderInterface $dataProvider ) { + $buckets = $queryResult['aggregations'][$bucket->getName()]['buckets'] ?? []; $values = []; - foreach ($queryResult['aggregations'][$bucket->getName()]['buckets'] as $resultBucket) { + foreach ($buckets as $resultBucket) { $values[$resultBucket['key']] = [ 'value' => $resultBucket['key'], 'count' => $resultBucket['doc_count'], ]; } + return $values; } } diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php index f1c3451482bab..e83c49941bcc2 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php @@ -5,11 +5,16 @@ */ namespace Magento\Elasticsearch\SearchAdapter\Query\Builder; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Search\Request\Query\BoolExpression; use Magento\Framework\Search\Request\QueryInterface as RequestQueryInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; use Magento\Framework\Search\Adapter\Preprocessor\PreprocessorInterface; +/** + * Builder for match query. + */ class Match implements QueryInterface { /** @@ -23,24 +28,35 @@ class Match implements QueryInterface private $fieldMapper; /** + * @deprecated + * @see \Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer * @var PreprocessorInterface[] */ protected $preprocessorContainer; + /** + * @var ValueTransformerPool + */ + private $valueTransformerPool; + /** * @param FieldMapperInterface $fieldMapper * @param PreprocessorInterface[] $preprocessorContainer + * @param ValueTransformerPool|null $valueTransformerPool */ public function __construct( FieldMapperInterface $fieldMapper, - array $preprocessorContainer + array $preprocessorContainer, + ValueTransformerPool $valueTransformerPool = null ) { $this->fieldMapper = $fieldMapper; $this->preprocessorContainer = $preprocessorContainer; + $this->valueTransformerPool = $valueTransformerPool ?? ObjectManager::getInstance() + ->get(ValueTransformerPool::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function build(array $selectQuery, RequestQueryInterface $requestQuery, $conditionType) { @@ -61,16 +77,14 @@ public function build(array $selectQuery, RequestQueryInterface $requestQuery, $ } /** + * Prepare query. + * * @param string $queryValue * @param string $conditionType * @return array */ protected function prepareQuery($queryValue, $conditionType) { - $queryValue = $this->escape($queryValue); - foreach ($this->preprocessorContainer as $preprocessor) { - $queryValue = $preprocessor->process($queryValue); - } $condition = $conditionType === BoolExpression::QUERY_CONDITION_NOT ? self::QUERY_CONDITION_MUST_NOT : $conditionType; return [ @@ -99,21 +113,34 @@ protected function buildQueries(array $matches, array $queryValue) // Checking for quoted phrase \"phrase test\", trim escaped surrounding quotes if found $count = 0; - $value = preg_replace('#^\\\\"(.*)\\\\"$#m', '$1', $queryValue['value'], -1, $count); + $value = preg_replace('#^"(.*)"$#m', '$1', $queryValue['value'], -1, $count); $condition = ($count) ? 'match_phrase' : 'match'; + $attributesTypes = $this->fieldMapper->getAllAttributesTypes(); + $transformedTypes = []; foreach ($matches as $match) { $resolvedField = $this->fieldMapper->getFieldName( $match['field'], ['type' => FieldMapperInterface::TYPE_QUERY] ); + $valueTransformer = $this->valueTransformerPool->get($attributesTypes[$resolvedField]['type'] ?? 'text'); + $valueTransformerHash = \spl_object_hash($valueTransformer); + if (!isset($transformedTypes[$valueTransformerHash])) { + $transformedTypes[$valueTransformerHash] = $valueTransformer->transform($value); + } + $transformedValue = $transformedTypes[$valueTransformerHash]; + if (null === $transformedValue) { + //Value is incompatible with this field type. + continue; + } + $conditions[] = [ 'condition' => $queryValue['condition'], 'body' => [ $condition => [ $resolvedField => [ - 'query' => $value, - 'boost' => isset($match['boost']) ? $match['boost'] : 1, + 'query' => $transformedValue, + 'boost' => $match['boost'] ?? 1, ], ], ], @@ -124,18 +151,15 @@ protected function buildQueries(array $matches, array $queryValue) } /** - * Cut trailing plus or minus sign, and @ symbol, using of which causes InnoDB to report a syntax error. - * @link https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html Fulltext-boolean search docs. - * * Escape a value for special query characters such as ':', '(', ')', '*', '?', etc. * + * @deprecated + * @see \Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer * @param string $value * @return string */ protected function escape($value) { - $value = preg_replace('/@+|[@+-]+$/', '', $value); - $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\*|\?|:|\\\)/'; $replace = '\\\$1'; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/DateTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/DateTransformer.php new file mode 100644 index 0000000000000..49eca6e9d82a6 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/DateTransformer.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\Model\Adapter\FieldType\Date; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; + +/** + * Value transformer for date type fields. + */ +class DateTransformer implements ValueTransformerInterface +{ + /** + * @var Date + */ + private $dateFieldType; + + /** + * @param Date $dateFieldType + */ + public function __construct(Date $dateFieldType) + { + $this->dateFieldType = $dateFieldType; + } + + /** + * @inheritdoc + */ + public function transform(string $value): ?string + { + try { + $formattedDate = $this->dateFieldType->formatDate(null, $value); + } catch (\Exception $e) { + $formattedDate = null; + } + + return $formattedDate; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/FloatTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/FloatTransformer.php new file mode 100644 index 0000000000000..5e330076d3df7 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/FloatTransformer.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; + +/** + * Value transformer for float type fields. + */ +class FloatTransformer implements ValueTransformerInterface +{ + /** + * @inheritdoc + */ + public function transform(string $value): ?float + { + return \is_numeric($value) ? (float) $value : null; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/IntegerTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/IntegerTransformer.php new file mode 100644 index 0000000000000..0846ff3a9bd86 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/IntegerTransformer.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; + +/** + * Value transformer for integer type fields. + */ +class IntegerTransformer implements ValueTransformerInterface +{ + /** + * @inheritdoc + */ + public function transform(string $value): ?int + { + return \is_numeric($value) ? (int) $value : null; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php new file mode 100644 index 0000000000000..68bec2580f621 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; +use Magento\Framework\Search\Adapter\Preprocessor\PreprocessorInterface; + +/** + * Value transformer for fields with text types. + */ +class TextTransformer implements ValueTransformerInterface +{ + /** + * @var PreprocessorInterface[] + */ + private $preprocessors; + + /** + * @param PreprocessorInterface[] $preprocessors + */ + public function __construct(array $preprocessors = []) + { + foreach ($preprocessors as $preprocessor) { + if (!$preprocessor instanceof PreprocessorInterface) { + throw new \InvalidArgumentException( + \sprintf('"%s" is not a instance of ValueTransformerInterface.', get_class($preprocessor)) + ); + } + } + + $this->preprocessors = $preprocessors; + } + + /** + * @inheritdoc + */ + public function transform(string $value): string + { + $value = $this->escape($value); + foreach ($this->preprocessors as $preprocessor) { + $value = $preprocessor->process($value); + } + + return $value; + } + + /** + * Escape a value for special query characters such as ':', '(', ')', '*', '?', etc. + * + * @param string $value + * @return string + */ + private function escape(string $value): string + { + $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\*|\?|:|\\\)/'; + $replace = '\\\$1'; + + return preg_replace($pattern, $replace, $value); + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerInterface.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerInterface.php new file mode 100644 index 0000000000000..c84ddc69cc7a8 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query; + +/** + * Value transformer of search term for matching with ES field types. + */ +interface ValueTransformerInterface +{ + /** + * Transform value according to field type. + * + * @param string $value + * @return mixed + */ + public function transform(string $value); +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerPool.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerPool.php new file mode 100644 index 0000000000000..11a35d79ce1fd --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerPool.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query; + +/** + * Pool of value transformers. + */ +class ValueTransformerPool +{ + /** + * @var ValueTransformerInterface[] + */ + private $transformers; + + /** + * @param ValueTransformerInterface[] $valueTransformers + */ + public function __construct(array $valueTransformers = []) + { + foreach ($valueTransformers as $valueTransformer) { + if (!$valueTransformer instanceof ValueTransformerInterface) { + throw new \InvalidArgumentException( + \sprintf('"%s" is not a instance of ValueTransformerInterface.', get_class($valueTransformer)) + ); + } + } + + $this->transformers = $valueTransformers; + } + + /** + * Get value transformer related to field type. + * + * @param string $fieldType + * @return ValueTransformerInterface + */ + public function get(string $fieldType): ValueTransformerInterface + { + return $this->transformers[$fieldType] ?? $this->transformers['default']; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php index 8114feb09d35d..c8aa3db39bd5e 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php @@ -7,6 +7,8 @@ use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; use Magento\Elasticsearch\SearchAdapter\Query\Builder\Match as MatchQueryBuilder; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool; use Magento\Framework\Search\Request\Query\Match as MatchRequestQuery; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit_Framework_MockObject_MockObject as MockObject; @@ -23,46 +25,48 @@ class MatchTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { + $valueTransformerPoolMock = $this->createMock(ValueTransformerPool::class); + $valueTransformerMock = $this->createMock(ValueTransformerInterface::class); + $valueTransformerPoolMock->method('get') + ->willReturn($valueTransformerMock); + $valueTransformerMock->method('transform') + ->willReturnArgument(0); + $this->matchQueryBuilder = (new ObjectManager($this))->getObject( MatchQueryBuilder::class, [ 'fieldMapper' => $this->getFieldMapper(), 'preprocessorContainer' => [], + 'valueTransformerPool' => $valueTransformerPoolMock, ] ); } /** * Tests that method constructs a correct select query. - * @see MatchQueryBuilder::build * - * @dataProvider queryValuesInvariantsProvider - * - * @param string $rawQueryValue - * @param string $errorMessage + * @see MatchQueryBuilder::build */ - public function testBuild($rawQueryValue, $errorMessage) + public function testBuild() { - $this->assertSelectQuery( - $this->matchQueryBuilder->build([], $this->getMatchRequestQuery($rawQueryValue), 'not'), - $errorMessage - ); - } + $rawQueryValue = 'query_value'; + $selectQuery = $this->matchQueryBuilder->build([], $this->getMatchRequestQuery($rawQueryValue), 'not'); - /** - * @link https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html Fulltext-boolean search docs. - * - * @return array - */ - public function queryValuesInvariantsProvider() - { - return [ - ['query_value', 'Select query field must match simple raw query value.'], - ['query_value+', 'Specifying a trailing plus sign causes InnoDB to report a syntax error.'], - ['query_value-', 'Specifying a trailing minus sign causes InnoDB to report a syntax error.'], - ['query_@value', 'The @ symbol is reserved for use by the @distance proximity search operator.'], - ['query_value+@', 'The @ symbol is reserved for use by the @distance proximity search operator.'], + $expectedSelectQuery = [ + 'bool' => [ + 'must_not' => [ + [ + 'match' => [ + 'some_field' => [ + 'query' => $rawQueryValue, + 'boost' => 43, + ], + ], + ], + ], + ], ]; + $this->assertEquals($expectedSelectQuery, $selectQuery); } /** @@ -111,30 +115,6 @@ public function matchProvider() ]; } - /** - * @param array $selectQuery - * @param string $errorMessage - */ - private function assertSelectQuery($selectQuery, $errorMessage) - { - $expectedSelectQuery = [ - 'bool' => [ - 'must_not' => [ - [ - 'match' => [ - 'some_field' => [ - 'query' => 'query_value', - 'boost' => 43, - ], - ], - ], - ], - ], - ]; - - $this->assertEquals($expectedSelectQuery, $selectQuery, $errorMessage); - } - /** * Gets fieldMapper mock object. * diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index 7e219bb2f918f..05a67605ba0e6 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -421,4 +421,22 @@ </argument> </arguments> </type> + <type name="Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool"> + <arguments> + <argument name="valueTransformers" xsi:type="array"> + <item name="default" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer</item> + <item name="date" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\DateTransformer</item> + <item name="float" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\FloatTransformer</item> + <item name="integer" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\IntegerTransformer</item> + </argument> + </arguments> + </type> + <type name="Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer"> + <arguments> + <argument name="preprocessors" xsi:type="array"> + <item name="stopwordsPreprocessor" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\Preprocessor\Stopwords</item> + <item name="synonymsPreprocessor" xsi:type="object">Magento\Search\Adapter\Query\Preprocessor\Synonyms</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Msrp/Helper/Data.php b/app/code/Magento/Msrp/Helper/Data.php index b4ec34ebee19c..393383bb2e772 100644 --- a/app/code/Magento/Msrp/Helper/Data.php +++ b/app/code/Magento/Msrp/Helper/Data.php @@ -11,6 +11,7 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; /** * Msrp data helper @@ -70,8 +71,7 @@ public function __construct( } /** - * Check if can apply Minimum Advertise price to product - * in specific visibility + * Check if can apply Minimum Advertise price to product in specific visibility * * @param int|Product $product * @param int|null $visibility Check displaying price in concrete place (by default generally) @@ -135,6 +135,8 @@ public function isShowPriceOnGesture($product) } /** + * Check if we should show MAP proce before order confirmation + * * @param int|Product $product * @return bool */ @@ -144,6 +146,8 @@ public function isShowBeforeOrderConfirm($product) } /** + * Check if any MAP price is larger than as low as value. + * * @param int|Product $product * @return bool|float */ @@ -155,10 +159,19 @@ public function isMinimalPriceLessMsrp($product) $msrp = $product->getMsrp(); $price = $product->getPriceInfo()->getPrice(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE); if ($msrp === null) { - if ($product->getTypeId() !== \Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE) { - return false; - } else { + if ($product->getTypeId() === \Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE) { $msrp = $product->getTypeInstance()->getChildrenMsrp($product); + } elseif ($product->getTypeId() === Configurable::TYPE_CODE) { + $prices = []; + foreach ($product->getTypeInstance()->getUsedProducts($product) as $item) { + if ($item->getMsrp() !== null) { + $prices[] = $item->getMsrp(); + } + } + + $msrp = $prices ? max($prices) : 0; + } else { + return false; } } if ($msrp) { diff --git a/app/code/Magento/Msrp/Test/Mftf/Data/MsrpSettingsData.xml b/app/code/Magento/Msrp/Test/Mftf/Data/MsrpSettingsData.xml new file mode 100644 index 0000000000000..3922bb4868914 --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/Data/MsrpSettingsData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="MsrpEnableMAP" type="msrp_settings_config"> + <requiredEntity type="enabled">EnableMAP</requiredEntity> + </entity> + <entity name="EnableMAP" type="msrp_settings_config"> + <data key="value">1</data> + </entity> + + <entity name="MsrpDisableMAP" type="msrp_settings_config"> + <requiredEntity type="enabled">DisableMAP</requiredEntity> + </entity> + <entity name="DisableMAP" type="msrp_settings_config"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Msrp/Test/Mftf/Metadata/msrp_settings-meta.xml b/app/code/Magento/Msrp/Test/Mftf/Metadata/msrp_settings-meta.xml new file mode 100644 index 0000000000000..be91a548ad909 --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/Metadata/msrp_settings-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="MsrpSettingsConfig" dataType="msrp_settings_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/sales/" method="POST"> + <object key="groups" dataType="msrp_settings_config"> + <object key="msrp" dataType="msrp_settings_config"> + <object key="fields" dataType="msrp_settings_config"> + <object key="enabled" dataType="enabled"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> \ No newline at end of file diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml new file mode 100644 index 0000000000000..a874de3b223a2 --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml @@ -0,0 +1,157 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontProductWithMapAssignedConfigProductIsCorrectTest"> + <annotations> + <features value="Msrp"/> + <title value="Check that simple products with MAP assigned to configurable product displayed correctly"/> + <description value="Check that simple products with MAP assigned to configurable product displayed correctly"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-12292"/> + <useCaseId value="MC-10973"/> + <group value="Msrp"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleProductWithPrice50" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleProductWithPrice60" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ApiSimpleProductWithPrice70" stepKey="createConfigChildProduct3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct3"/> + </createData> + + <!--Enable Minimum advertised Price--> + <createData entity="MsrpEnableMAP" stepKey="enableMAP"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + + <!--Disable Minimum advertised Price--> + <createData entity="MsrpDisableMAP" stepKey="disableMAP"/> + + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + + <!-- Set Manufacturer's Suggested Retail Price to products--> + <amOnPage url="{{AdminProductEditPage.url($$createConfigChildProduct1.id$$)}}" stepKey="goToFirstChildProductEditPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.msrp}}" stepKey="waitForMsrp"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.msrp}}" userInput="55" stepKey="setMsrpForFirstChildProduct"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct1"/> + + <amOnPage url="{{AdminProductEditPage.url($$createConfigChildProduct2.id$$)}}" stepKey="goToSecondChildProductEditPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad1"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton1"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.msrp}}" stepKey="waitForMsrp1"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.msrp}}" userInput="66" stepKey="setMsrpForSecondChildProduct"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton1"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Clear cache--> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!--Go to store front and check msrp for products--> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToConfigProductPage"/> + <waitForPageLoad stepKey="waitForLoadConfigProductPage"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="grabMapPrice"/> + <assertEquals expected='$66.00' expectedType="string" actual="($grabMapPrice)" stepKey="assertMapPrice"/> + <seeElement selector="{{StorefrontProductInfoMainSection.clickForPriceLink}}" stepKey="checkClickForPriceLink"/> + + <!--Check msrp for second child product--> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption2.value$$" stepKey="selectSecondOption"/> + <waitForElement selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="waitForLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="grabSecondProductMapPrice"/> + <assertEquals expected='$66.00' expectedType="string" actual="($grabSecondProductMapPrice)" stepKey="assertSecondProductMapPrice"/> + <seeElement selector="{{StorefrontProductInfoMainSection.clickForPriceLink}}" stepKey="checkClickForPriceLinkForSecondProduct"/> + + <!--Check msrp for first child product--> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption1.value$$" stepKey="selectFirstOption"/> + <waitForElement selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="waitForLoad1"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="grabFirstProductMapPrice"/> + <assertEquals expected='$55.00' expectedType="string" actual="($grabFirstProductMapPrice)" stepKey="assertFirstProductMapPrice"/> + <seeElement selector="{{StorefrontProductInfoMainSection.clickForPriceLink}}" stepKey="checkClickForPriceLinkForFirstProduct"/> + + <!--Check price for third child product--> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption3.value$$" stepKey="selectThirdOption"/> + <waitForElement selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="waitForLoad2"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="grabThirdProductMapPrice"/> + <assertEquals expected='$70.00' expectedType="string" actual="($grabThirdProductMapPrice)" stepKey="assertThirdProductMapPrice"/> + <dontSeeElement selector="{{StorefrontProductInfoMainSection.clickForPriceLink}}" stepKey="checkClickForPriceLinkForThirdProduct"/> + </test> +</tests> diff --git a/app/code/Magento/Msrp/composer.json b/app/code/Magento/Msrp/composer.json index 6e7bf61063a2a..e3099aa2f14d6 100644 --- a/app/code/Magento/Msrp/composer.json +++ b/app/code/Magento/Msrp/composer.json @@ -11,6 +11,7 @@ "magento/module-downloadable": "*", "magento/module-eav": "*", "magento/module-grouped-product": "*", + "magento/module-configurable-product": "*", "magento/module-store": "*", "magento/module-tax": "*" }, diff --git a/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml b/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml index dd5abd433073d..a951c14cf4c70 100644 --- a/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml +++ b/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml @@ -20,8 +20,23 @@ $priceType = $block->getPrice(); /** @var $product \Magento\Catalog\Model\Product */ $product = $block->getSaleableItem(); $productId = $product->getId(); + +$amount = 0; +if ($product->getMsrp()) { + $amount = $product->getMsrp(); +} elseif ($product->getTypeId() === \Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE) { + $amount = $product->getTypeInstance()->getChildrenMsrp($product); +} elseif ($product->getTypeId() === \Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE) { + foreach ($product->getTypeInstance()->getUsedProducts($product) as $item) { + if ($item->getMsrp() !== null) { + $prices[] = $item->getMsrp(); + } + } + $amount = $prices ? max($prices) : 0; +} + $msrpPrice = $block->renderAmount( - $priceType->getCustomAmount($product->getMsrp() ?: $product->getTypeInstance()->getChildrenMsrp($product)), + $priceType->getCustomAmount($amount), [ 'price_id' => $block->getPriceId() ? $block->getPriceId() : 'old-price-' . $productId, 'include_container' => false, @@ -29,54 +44,56 @@ $msrpPrice = $block->renderAmount( ] ); $priceElementIdPrefix = $block->getPriceElementIdPrefix() ? $block->getPriceElementIdPrefix() : 'product-price-'; - -$addToCartUrl = ''; -if ($product->isSaleable()) { - /** @var Magento\Catalog\Block\Product\AbstractProduct $addToCartUrlGenerator */ - $addToCartUrlGenerator = $block->getLayout()->getBlockSingleton('Magento\Catalog\Block\Product\AbstractProduct'); - $addToCartUrl = $addToCartUrlGenerator->getAddToCartUrl( - $product, - ['_query' => [ - \Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED => - $this->helper('Magento\Framework\Url\Helper\Data')->getEncodedUrl( - $addToCartUrlGenerator->getAddToCartUrl($product) - ), - ]] - ); -} ?> -<?php if ($product->getMsrp()): ?> + +<?php if ($amount): ?> <span class="old-price map-old-price"><?= /* @escapeNotVerified */ $msrpPrice ?></span> + <span class="map-fallback-price normal-price"><?= /* @escapeNotVerified */ $msrpPrice ?></span> <?php endif; ?> <?php if ($priceType->isShowPriceOnGesture()): ?> <?php - $priceElementId = $priceElementIdPrefix . $productId . $block->getIdSuffix(); - $popupId = 'msrp-popup-' . $productId . $block->getRandomString(20); - $data = ['addToCart' => [ - 'origin'=> 'msrp', - 'popupId' => '#' . $popupId, - 'productName' => $block->escapeJs($block->escapeHtml($product->getName())), - 'productId' => $productId, - 'productIdInput' => 'input[type="hidden"][name="product"]', - 'realPrice' => $block->getRealPriceHtml(), - 'isSaleable' => $product->isSaleable(), - 'msrpPrice' => $msrpPrice, - 'priceElementId' => $priceElementId, - 'closeButtonId' => '#map-popup-close', - 'addToCartUrl' => $addToCartUrl, - 'paymentButtons' => '[data-label=or]' - ]]; - if ($block->getRequest()->getFullActionName() === 'catalog_product_view') { - $data['addToCart']['addToCartButton'] = '#product_addtocart_form [type=submit]'; - } else { - $data['addToCart']['addToCartButton'] = sprintf( - 'form:has(input[type="hidden"][name="product"][value="%s"]) button[type="submit"]', - (int) $productId) . ',' . - sprintf('.block.widget .price-box[data-product-id=%s]+.product-item-actions button.tocart', - (int) $productId - ); - } + + $addToCartUrl = ''; + if ($product->isSaleable()) { + /** @var Magento\Catalog\Block\Product\AbstractProduct $addToCartUrlGenerator */ + $addToCartUrlGenerator = $block->getLayout()->getBlockSingleton('Magento\Catalog\Block\Product\AbstractProduct'); + $addToCartUrl = $addToCartUrlGenerator->getAddToCartUrl( + $product, + ['_query' => [ + \Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED => + $this->helper('Magento\Framework\Url\Helper\Data')->getEncodedUrl( + $addToCartUrlGenerator->getAddToCartUrl($product) + ), + ]] + ); + } + + $priceElementId = $priceElementIdPrefix . $productId . $block->getIdSuffix(); + $popupId = 'msrp-popup-' . $productId . $block->getRandomString(20); + $data = ['addToCart' => [ + 'origin'=> 'msrp', + 'popupId' => '#' . $popupId, + 'productName' => $block->escapeJs($block->escapeHtml($product->getName())), + 'productId' => $productId, + 'productIdInput' => 'input[type="hidden"][name="product"]', + 'realPrice' => $block->getRealPriceHtml(), + 'isSaleable' => $product->isSaleable(), + 'msrpPrice' => $msrpPrice, + 'priceElementId' => $priceElementId, + 'closeButtonId' => '#map-popup-close', + 'addToCartUrl' => $addToCartUrl, + 'paymentButtons' => '[data-label=or]' + ]]; + if ($block->getRequest()->getFullActionName() === 'catalog_product_view') { + $data['addToCart']['addToCartButton'] = '#product_addtocart_form [type=submit]'; + } else { + $data['addToCart']['addToCartButton'] = sprintf( + 'form:has(input[type="hidden"][name="product"][value="%s"]) button[type="submit"]', + (int) $productId . ',' . + sprintf('.block.widget .price-box[data-product-id=%s]+.product-item-actions button.tocart', + (int) $productId)); + } ?> <span id="<?= /* @escapeNotVerified */ $block->getPriceId() ? $block->getPriceId() : $priceElementId ?>" style="display:none"></span> <a href="javascript:void(0);" @@ -100,4 +117,4 @@ if ($product->isSaleable()) { "productName": "<?= $block->escapeJs($block->escapeHtml($product->getName())) ?>", "closeButtonId": "#map-popup-close"}}'><span><?= /* @escapeNotVerified */ __("What's this?") ?></span> </a> -<?php endif; ?> +<?php endif; ?> \ No newline at end of file diff --git a/app/code/Magento/Msrp/view/base/web/js/msrp.js b/app/code/Magento/Msrp/view/base/web/js/msrp.js index deeadd9b55b82..a0bd3ec132de6 100644 --- a/app/code/Magento/Msrp/view/base/web/js/msrp.js +++ b/app/code/Magento/Msrp/view/base/web/js/msrp.js @@ -4,11 +4,12 @@ */ define([ 'jquery', + 'Magento_Catalog/js/price-utils', 'underscore', 'jquery/ui', 'mage/dropdown', 'mage/template' -], function ($) { +], function ($, priceUtils, _) { 'use strict'; $.widget('mage.addToCart', { @@ -24,7 +25,14 @@ define([ // Selectors cartForm: '.form.map.checkout', msrpLabelId: '#map-popup-msrp', + msrpPriceElement: '#map-popup-msrp .price-wrapper', priceLabelId: '#map-popup-price', + priceElement: '#map-popup-price .price', + mapInfoLinks: '.map-show-info', + displayPriceElement: '.old-price.map-old-price .price-wrapper', + fallbackPriceElement: '.normal-price.map-fallback-price .price-wrapper', + displayPriceContainer: '.old-price.map-old-price', + fallbackPriceContainer: '.normal-price.map-fallback-price', popUpAttr: '[data-role=msrp-popup-template]', popupCartButtonId: '#map-popup-button', paypalCheckoutButons: '[data-action=checkout-form-submit]', @@ -59,9 +67,11 @@ define([ shadowHinter: 'popup popup-pointer' }, popupOpened: false, + wasOpened: false, /** * Creates widget instance + * * @private */ _create: function () { @@ -73,10 +83,13 @@ define([ this.initTierPopup(); } $(this.options.cartButtonId).on('click', this._addToCartSubmit.bind(this)); + $(document).on('updateMsrpPriceBlock', this.onUpdateMsrpPrice.bind(this)); + $(this.options.cartForm).on('submit', this._onSubmitForm.bind(this)); }, /** * Init msrp popup + * * @private */ initMsrpPopup: function () { @@ -89,7 +102,7 @@ define([ $msrpPopup.find('button') .on('click', - this.handleMsrpAddToCart.bind(this)) + this.handleMsrpAddToCart.bind(this)) .filter(this.options.popupCartButtonId) .text($(this.options.addToCartButton).text()); @@ -104,6 +117,7 @@ define([ /** * Init info popup + * * @private */ initInfoPopup: function () { @@ -212,8 +226,12 @@ define([ var options = this.tierOptions || this.options; this.popUpOptions.position.of = $(event.target); - this.$popup.find(this.options.msrpLabelId).html(options.msrpPrice); - this.$popup.find(this.options.priceLabelId).html(options.realPrice); + + if (!this.wasOpened) { + this.$popup.find(this.options.msrpLabelId).html(options.msrpPrice); + this.$popup.find(this.options.priceLabelId).html(options.realPrice); + this.wasOpened = true; + } this.$popup.dropdownDialog(this.popUpOptions).dropdownDialog('open'); this._toggle(this.$popup); @@ -223,6 +241,7 @@ define([ }, /** + * Toggle MAP popup visibility * * @param {HTMLElement} $elem * @private @@ -239,6 +258,7 @@ define([ }, /** + * Close MAP information popup * * @param {HTMLElement} $elem */ @@ -249,8 +269,10 @@ define([ /** * Handler for addToCart action + * + * @param {Object} e */ - _addToCartSubmit: function () { + _addToCartSubmit: function (e) { this.element.trigger('addToCart', this.element); if (this.element.data('stop-processing')) { @@ -266,9 +288,106 @@ define([ if (this.options.addToCartUrl) { $('.mage-dropdown-dialog > .ui-dialog-content').dropdownDialog('close'); } + + e.preventDefault(); $(this.options.cartForm).submit(); + }, + /** + * Call on event updatePrice. Proxy to updateMsrpPrice method. + * + * @param {Event} event + * @param {mixed} priceIndex + * @param {Object} prices + */ + onUpdateMsrpPrice: function onUpdateMsrpPrice(event, priceIndex, prices) { + + var defaultMsrp, + defaultPrice, + msrpPrice, + finalPrice; + + defaultMsrp = _.chain(prices).map(function (price) { + return price.msrpPrice.amount; + }).reject(function (p) { + return p === null; + }).max().value(); + + defaultPrice = _.chain(prices).map(function (p) { + return p.finalPrice.amount; + }).min().value(); + + if (typeof priceIndex !== 'undefined') { + msrpPrice = prices[priceIndex].msrpPrice.amount; + finalPrice = prices[priceIndex].finalPrice.amount; + + if (msrpPrice === null || msrpPrice <= finalPrice) { + this.updateNonMsrpPrice(priceUtils.formatPrice(finalPrice)); + } else { + this.updateMsrpPrice( + priceUtils.formatPrice(finalPrice), + priceUtils.formatPrice(msrpPrice), + false); + } + } else { + this.updateMsrpPrice( + priceUtils.formatPrice(defaultPrice), + priceUtils.formatPrice(defaultMsrp), + true); + } + }, + + /** + * Update prices for configurable product with MSRP enabled + * + * @param {String} finalPrice + * @param {String} msrpPrice + * @param {Boolean} useDefaultPrice + */ + updateMsrpPrice: function (finalPrice, msrpPrice, useDefaultPrice) { + var options = this.tierOptions || this.options; + + $(this.options.fallbackPriceContainer).hide(); + $(this.options.displayPriceContainer).show(); + $(this.options.mapInfoLinks).show(); + + if (useDefaultPrice || !this.wasOpened) { + this.$popup.find(this.options.msrpLabelId).html(options.msrpPrice); + this.$popup.find(this.options.priceLabelId).html(options.realPrice); + $(this.options.displayPriceElement).html(msrpPrice); + this.wasOpened = true; + } + + if (!useDefaultPrice) { + this.$popup.find(this.options.msrpPriceElement).html(msrpPrice); + this.$popup.find(this.options.priceElement).html(finalPrice); + $(this.options.displayPriceElement).html(msrpPrice); + } + }, + + /** + * Display non MAP price for irrelevant products + * + * @param {String} price + */ + updateNonMsrpPrice: function (price) { + $(this.options.fallbackPriceElement).html(price); + $(this.options.displayPriceContainer).hide(); + $(this.options.mapInfoLinks).hide(); + $(this.options.fallbackPriceContainer).show(); + }, + + /** + * Handler for submit form + * + * @private + */ + _onSubmitForm: function () { + if ($(this.options.cartForm).valid()) { + $(this.options.cartButtonId).prop('disabled', true); + } } + }); return $.mage.addToCart; diff --git a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php index bb81f9ebb475f..373d64afc8cc3 100644 --- a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php +++ b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php @@ -227,12 +227,12 @@ public function getCode($type, $code = '') $codes = [ 'condition_name' => [ 'package_weight' => __('Weight vs. Destination'), - 'package_value' => __('Price vs. Destination'), + 'package_value_with_discount' => __('Price vs. Destination'), 'package_qty' => __('# of Items vs. Destination'), ], 'condition_name_short' => [ 'package_weight' => __('Weight (and above)'), - 'package_value' => __('Order Subtotal (and above)'), + 'package_value_with_discount' => __('Order Subtotal (and above)'), 'package_qty' => __('# of Items (and above)'), ], ]; diff --git a/app/code/Magento/OfflineShipping/Setup/Patch/Data/UpdateShippingTablerate.php b/app/code/Magento/OfflineShipping/Setup/Patch/Data/UpdateShippingTablerate.php new file mode 100644 index 0000000000000..070105846fdd8 --- /dev/null +++ b/app/code/Magento/OfflineShipping/Setup/Patch/Data/UpdateShippingTablerate.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\OfflineShipping\Setup\Patch\Data; + +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\OfflineShipping\Model\Carrier\Tablerate; + +/** + * Update for shipping_tablerate table for using price with discount in condition. + */ +class UpdateShippingTablerate implements DataPatchInterface +{ + /** + * @var \Magento\Framework\Setup\ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * PatchInitial constructor. + * @param \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct( + \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup + ) { + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * @inheritdoc + */ + public function apply() + { + $this->moduleDataSetup->getConnection()->startSetup(); + $connection = $this->moduleDataSetup->getConnection(); + $connection->update( + $this->moduleDataSetup->getTable('shipping_tablerate'), + ['condition_name' => 'package_value_with_discount'], + [new \Zend_Db_Expr('condition_name = \'package_value\'')] + ); + $connection->update( + $this->moduleDataSetup->getTable('core_config_data'), + ['value' => 'package_value_with_discount'], + [ + new \Zend_Db_Expr('value = \'package_value\''), + new \Zend_Db_Expr('path = \'carriers/tablerate/condition_name\'') + ] + ); + $this->moduleDataSetup->getConnection()->endSetup(); + + $connection->endSetup(); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/OfflineShipping/etc/db_schema.xml b/app/code/Magento/OfflineShipping/etc/db_schema.xml index 0510ce9b9b8eb..5129e8a29b2a1 100644 --- a/app/code/Magento/OfflineShipping/etc/db_schema.xml +++ b/app/code/Magento/OfflineShipping/etc/db_schema.xml @@ -18,7 +18,7 @@ default="0" comment="Destination Region Id"/> <column xsi:type="varchar" name="dest_zip" nullable="false" length="10" default="*" comment="Destination Post Code (Zip)"/> - <column xsi:type="varchar" name="condition_name" nullable="false" length="20" comment="Rate Condition name"/> + <column xsi:type="varchar" name="condition_name" nullable="false" length="30" comment="Rate Condition name"/> <column xsi:type="decimal" name="condition_value" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Rate condition value"/> <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="false" default="0" diff --git a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js index 20deba5b9b46a..cd0f3b3d630a6 100644 --- a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js +++ b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js @@ -177,12 +177,14 @@ define([ * @private */ clearEvents: function () { - this.fotoramaItem.off( - 'fotorama:show.' + this.PV + - ' fotorama:showend.' + this.PV + - ' fotorama:fullscreenenter.' + this.PV + - ' fotorama:fullscreenexit.' + this.PV - ); + if (this.fotoramaItem !== undefined) { + this.fotoramaItem.off( + 'fotorama:show.' + this.PV + + ' fotorama:showend.' + this.PV + + ' fotorama:fullscreenenter.' + this.PV + + ' fotorama:fullscreenexit.' + this.PV + ); + } }, /** diff --git a/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php b/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php index e18ab8587fc71..60e5ad9f4caff 100644 --- a/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php +++ b/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php @@ -79,7 +79,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc * * @param int $cartId The cart ID. * @return Totals Quote totals data. diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php index 17d3b9205625f..b1f68d0411cf0 100644 --- a/app/code/Magento/Quote/Model/Quote.php +++ b/app/code/Magento/Quote/Model/Quote.php @@ -1375,14 +1375,13 @@ public function addShippingAddress(\Magento\Quote\Api\Data\AddressInterface $add * * @param bool $useCache * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getItemsCollection($useCache = true) { - if ($this->hasItemsCollection()) { + if ($this->hasItemsCollection() && $useCache) { return $this->getData('items_collection'); } - if (null === $this->_items) { + if (null === $this->_items || !$useCache) { $this->_items = $this->_quoteItemCollectionFactory->create(); $this->extensionAttributesJoinProcessor->process($this->_items); $this->_items->setQuote($this); diff --git a/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php b/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php new file mode 100644 index 0000000000000..19a7e03264d8a --- /dev/null +++ b/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Plugin; + +use Magento\Checkout\Model\Session; +use Magento\Quote\Model\QuoteRepository; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcherInterface; + +/** + * Updates quote items store id. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class UpdateQuoteItemStore +{ + /** + * @var QuoteRepository + */ + private $quoteRepository; + + /** + * @var Session + */ + private $checkoutSession; + + /** + * @param QuoteRepository $quoteRepository + * @param Session $checkoutSession + */ + public function __construct( + QuoteRepository $quoteRepository, + Session $checkoutSession + ) { + $this->quoteRepository = $quoteRepository; + $this->checkoutSession = $checkoutSession; + } + + /** + * Update store id in active quote after store view switching. + * + * @param StoreSwitcherInterface $subject + * @param string $result + * @param StoreInterface $fromStore store where we came from + * @param StoreInterface $targetStore store where to go to + * @param string $redirectUrl original url requested for redirect after switching + * @return string url to be redirected after switching + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSwitch( + StoreSwitcherInterface $subject, + $result, + StoreInterface $fromStore, + StoreInterface $targetStore, + string $redirectUrl + ): string { + $quote = $this->checkoutSession->getQuote(); + if ($quote->getIsActive()) { + $quote->setStoreId( + $targetStore->getId() + ); + $quote->getItemsCollection(false); + $this->quoteRepository->save($quote); + } + return $result; + } +} diff --git a/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php b/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php index 1e999cb5e523e..804f0863d2d2a 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php @@ -77,7 +77,8 @@ protected function setUp() 'getAllVisibleItems', 'getBaseCurrencyCode', 'getQuoteCurrencyCode', - 'getItemsQty' + 'getItemsQty', + 'collectTotals' ]); $this->quoteRepositoryMock = $this->createMock(\Magento\Quote\Api\CartRepositoryInterface::class); $this->addressMock = $this->createPartialMock( diff --git a/app/code/Magento/Quote/etc/frontend/di.xml b/app/code/Magento/Quote/etc/frontend/di.xml index 125afb96f20fd..ecad94fbbc249 100644 --- a/app/code/Magento/Quote/etc/frontend/di.xml +++ b/app/code/Magento/Quote/etc/frontend/di.xml @@ -12,6 +12,9 @@ <argument name="checkoutSession" xsi:type="object">Magento\Checkout\Model\Session\Proxy</argument> </arguments> </type> + <type name="Magento\Store\Model\StoreSwitcherInterface"> + <plugin name="update_quote_item_store_after_switch_store_view" type="Magento\Quote\Plugin\UpdateQuoteItemStore"/> + </type> <type name="Magento\Store\Api\StoreCookieManagerInterface"> <plugin name="update_quote_store_after_switch_store_view" type="Magento\Quote\Plugin\UpdateQuoteStore"/> </type> diff --git a/app/code/Magento/Quote/etc/sales.xml b/app/code/Magento/Quote/etc/sales.xml index 3d54a6375c8d9..3db72a1226236 100644 --- a/app/code/Magento/Quote/etc/sales.xml +++ b/app/code/Magento/Quote/etc/sales.xml @@ -9,7 +9,7 @@ <section name="quote"> <group name="totals"> <item name="subtotal" instance="Magento\Quote\Model\Quote\Address\Total\Subtotal" sort_order="100"/> - <item name="shipping" instance="Magento\Quote\Model\Quote\Address\Total\Shipping" sort_order="250"/> + <item name="shipping" instance="Magento\Quote\Model\Quote\Address\Total\Shipping" sort_order="350"/> <item name="grand_total" instance="Magento\Quote\Model\Quote\Address\Total\Grand" sort_order="550"/> </group> </section> diff --git a/app/code/Magento/Rule/Model/Condition/AbstractCondition.php b/app/code/Magento/Rule/Model/Condition/AbstractCondition.php index d2be99757df47..6729fe722de56 100644 --- a/app/code/Magento/Rule/Model/Condition/AbstractCondition.php +++ b/app/code/Magento/Rule/Model/Condition/AbstractCondition.php @@ -106,8 +106,8 @@ public function getDefaultOperatorInputByType() 'string' => ['==', '!=', '>=', '>', '<=', '<', '{}', '!{}', '()', '!()'], 'numeric' => ['==', '!=', '>=', '>', '<=', '<', '()', '!()'], 'date' => ['==', '>=', '<='], - 'select' => ['==', '!='], - 'boolean' => ['==', '!='], + 'select' => ['==', '!=', '<=>'], + 'boolean' => ['==', '!=', '<=>'], 'multiselect' => ['{}', '!{}', '()', '!()'], 'grid' => ['()', '!()'], ]; @@ -137,6 +137,7 @@ public function getDefaultOperatorOptions() '!{}' => __('does not contain'), '()' => __('is one of'), '!()' => __('is not one of'), + '<=>' => __('is undefined'), ]; } return $this->_defaultOperatorOptions; diff --git a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php index 6267e30a7a6d5..33e1bf97c3474 100644 --- a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php +++ b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php @@ -250,8 +250,30 @@ public function attachConditionToCollection( $this->_joinTablesToCollection($collection, $combine); $whereExpression = (string)$this->_getMappedSqlCombination($combine); if (!empty($whereExpression)) { - // Select ::where method adds braces even on empty expression - $collection->getSelect()->where($whereExpression); + if (!empty($combine->getConditions())) { + $conditions = ''; + $attributeField = ''; + foreach ($combine->getConditions() as $condition) { + if ($condition->getData('attribute') === \Magento\Catalog\Api\Data\ProductInterface::SKU) { + $conditions = $condition->getData('value'); + $attributeField = $condition->getMappedSqlField(); + } + } + + $collection->getSelect()->where($whereExpression); + + if (!empty($conditions) && !empty($attributeField)) { + $conditions = explode(',', $conditions); + foreach ($conditions as &$condition) { + $condition = "'" . trim($condition) . "'"; + } + $conditions = implode(', ', $conditions); + $collection->getSelect()->order("FIELD($attributeField, $conditions)"); + } + } else { + // Select ::where method adds braces even on empty expression + $collection->getSelect()->where($whereExpression); + } } } } diff --git a/app/code/Magento/Rule/view/adminhtml/web/rules.js b/app/code/Magento/Rule/view/adminhtml/web/rules.js index 8e36562ebd7fe..202337c39da35 100644 --- a/app/code/Magento/Rule/view/adminhtml/web/rules.js +++ b/app/code/Magento/Rule/view/adminhtml/web/rules.js @@ -101,6 +101,9 @@ define([ if (!elem.multiple) { Event.observe(elem, 'change', this.hideParamInputField.bind(this, container)); + + this.changeVisibilityForValueRuleParam(elem); + } Event.observe(elem, 'blur', this.hideParamInputField.bind(this, container)); } @@ -262,6 +265,8 @@ define([ label.innerHTML = str != '' ? str : '...'; } + this.changeVisibilityForValueRuleParam(elem); + elem = Element.down(container, 'input.input-text'); if (elem) { @@ -293,6 +298,23 @@ define([ this.shownElement = null; }, + changeVisibilityForValueRuleParam: function(elem) { + let parsedElementId = elem.id.split('__'); + if (parsedElementId[2] != 'operator') { + return false; + } + + let valueElement = jQuery('#' + parsedElementId[0] + '__' + parsedElementId[1] + '__value'); + + if(elem.value == '<=>') { + valueElement.closest('.rule-param').hide(); + } else { + valueElement.closest('.rule-param').show(); + } + + return true; + }, + addRuleNewChild: function (elem) { var parent_id = elem.id.replace(/^.*__(.*)__.*$/, '$1'); var children_ul_id = elem.id.replace(/__/g, ':').replace(/[^:]*$/, 'children').replace(/:/g, '__'); diff --git a/app/code/Magento/Sales/Model/Service/InvoiceService.php b/app/code/Magento/Sales/Model/Service/InvoiceService.php index 791274d3f23bd..ba6ae7eb14ba7 100644 --- a/app/code/Magento/Sales/Model/Service/InvoiceService.php +++ b/app/code/Magento/Sales/Model/Service/InvoiceService.php @@ -7,9 +7,14 @@ use Magento\Sales\Api\InvoiceManagementInterface; use Magento\Sales\Model\Order; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Catalog\Model\Product\Type; /** * Class InvoiceService + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class InvoiceService implements InvoiceManagementInterface { @@ -58,6 +63,13 @@ class InvoiceService implements InvoiceManagementInterface */ protected $orderConverter; + /** + * Serializer interface instance. + * + * @var Json + */ + private $serializer; + /** * Constructor * @@ -68,6 +80,7 @@ class InvoiceService implements InvoiceManagementInterface * @param \Magento\Sales\Model\Order\InvoiceNotifier $notifier * @param \Magento\Sales\Api\OrderRepositoryInterface $orderRepository * @param \Magento\Sales\Model\Convert\Order $orderConverter + * @param Json|null $serializer */ public function __construct( \Magento\Sales\Api\InvoiceRepositoryInterface $repository, @@ -76,7 +89,8 @@ public function __construct( \Magento\Framework\Api\FilterBuilder $filterBuilder, \Magento\Sales\Model\Order\InvoiceNotifier $notifier, \Magento\Sales\Api\OrderRepositoryInterface $orderRepository, - \Magento\Sales\Model\Convert\Order $orderConverter + \Magento\Sales\Model\Convert\Order $orderConverter, + Json $serializer = null ) { $this->repository = $repository; $this->commentRepository = $commentRepository; @@ -85,6 +99,7 @@ public function __construct( $this->invoiceNotifier = $notifier; $this->orderRepository = $orderRepository; $this->orderConverter = $orderConverter; + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); } /** @@ -172,13 +187,13 @@ private function prepareItemsQty(Order $order, array $qtys = []) { foreach ($order->getAllItems() as $orderItem) { if (empty($qtys[$orderItem->getId()])) { - $parentId = $orderItem->getParentItemId(); - if ($parentId && array_key_exists($parentId, $qtys)) { - $qtys[$orderItem->getId()] = $qtys[$parentId]; + if ($orderItem->getProductType() == Type::TYPE_BUNDLE && !$orderItem->isShipSeparately()) { + $qtys[$orderItem->getId()] = $orderItem->getQtyOrdered() - $orderItem->getQtyInvoiced(); } else { continue; } } + $this->prepareItemQty($orderItem, $qtys); } @@ -186,20 +201,27 @@ private function prepareItemsQty(Order $order, array $qtys = []) } /** - * Prepare qty to invoice item. + * Prepare qty_invoiced for order item * - * @param Order\Item $orderItem + * @param \Magento\Sales\Api\Data\OrderItemInterface $orderItem * @param array $qtys - * @return void */ private function prepareItemQty(\Magento\Sales\Api\Data\OrderItemInterface $orderItem, &$qtys) { + $this->prepareBundleQty($orderItem, $qtys); + if ($orderItem->isDummy()) { if ($orderItem->getHasChildren()) { foreach ($orderItem->getChildrenItems() as $child) { if (!isset($qtys[$child->getId()])) { $qtys[$child->getId()] = $child->getQtyToInvoice(); } + $parentId = $orderItem->getParentItemId(); + if ($parentId && array_key_exists($parentId, $qtys)) { + $qtys[$orderItem->getId()] = $qtys[$parentId]; + } else { + continue; + } } } elseif ($orderItem->getParentItem()) { $parent = $orderItem->getParentItem(); @@ -210,6 +232,26 @@ private function prepareItemQty(\Magento\Sales\Api\Data\OrderItemInterface $orde } } + /** + * Prepare qty to invoice for bundle products + * + * @param \Magento\Sales\Api\Data\OrderItemInterface $orderItem + * @param array $qtys + */ + private function prepareBundleQty(\Magento\Sales\Api\Data\OrderItemInterface $orderItem, &$qtys) + { + if ($orderItem->getProductType() == Type::TYPE_BUNDLE && !$orderItem->isShipSeparately()) { + foreach ($orderItem->getChildrenItems() as $childItem) { + $bundleSelectionAttributes = $childItem->getProductOptionByCode('bundle_selection_attributes'); + if (is_string($bundleSelectionAttributes)) { + $bundleSelectionAttributes = $this->serializer->unserialize($bundleSelectionAttributes); + } + + $qtys[$childItem->getId()] = $qtys[$orderItem->getId()] * $bundleSelectionAttributes['qty']; + } + } + } + /** * Check if order item can be invoiced. * diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/OrderAndReturnActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontOrderActionGroupActionGroup.xml similarity index 64% rename from app/code/Magento/Sales/Test/Mftf/ActionGroup/OrderAndReturnActionGroup.xml rename to app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontOrderActionGroupActionGroup.xml index c46dd612022fd..fcea25f997591 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/OrderAndReturnActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontOrderActionGroupActionGroup.xml @@ -9,11 +9,11 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!--Fill order information fields and click continue--> - <actionGroup name="StorefrontFillOrderInformationActionGroup"> + <actionGroup name="StorefrontSearchGuestOrderActionGroup"> <arguments> <argument name="orderId" type="string"/> - <argument name="orderLastName"/> - <argument name="orderEmail"/> + <argument name="orderLastName" type="string"/> + <argument name="orderEmail" type="string"/> </arguments> <amOnPage url="{{StorefrontOrdersAndReturnsPage.url}}" stepKey="navigateToOrderAndReturnPage"/> <waitForPageLoad stepKey="waitForPageLoad"/> @@ -24,13 +24,4 @@ <waitForPageLoad stepKey="waitForOrderInformationPageLoad"/> <seeInCurrentUrl url="{{StorefrontOrderInformationPage.url}}" stepKey="seeOrderInformationUrl"/> </actionGroup> - - <!--Enter quantity to return and submit--> - <actionGroup name="StorefrontFillQuantityToReturnActionGroup"> - <click selector="{{StorefrontOrderInformationMainSection.return}}" stepKey="gotToCreateNewReturnPage"/> - <waitForPageLoad stepKey="waitForReturnPageLoad"/> - <fillField selector="{{StorefrontCreateNewReturnMainSection.quantityToReturn}}" userInput="1" stepKey="fillQuantityToReturn"/> - <click selector="{{StorefrontCreateNewReturnMainSection.submit}}" stepKey="clickSubmit"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/SalesEnableRMAStorefrontConfigData.xml b/app/code/Magento/Sales/Test/Mftf/Data/SalesEnableRMAStorefrontConfigData.xml deleted file mode 100644 index 76ff20813483e..0000000000000 --- a/app/code/Magento/Sales/Test/Mftf/Data/SalesEnableRMAStorefrontConfigData.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - <entity name="EnableRMA" type="sales_rma_config"> - <requiredEntity type="enabled">EnableRMAStorefront</requiredEntity> - </entity> - <entity name="EnableRMAStorefront" type="enabled"> - <data key="value">1</data> - </entity> - - <entity name="DisableRMA" type="sales_rma_config"> - <requiredEntity type="enabled">DisableRMAStorefront</requiredEntity> - </entity> - <entity name="DisableRMAStorefront" type="enabled"> - <data key="value">0</data> - </entity> -</entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Metadata/sales_enable_rma_config-meta.xml b/app/code/Magento/Sales/Test/Mftf/Metadata/sales_enable_rma_config-meta.xml deleted file mode 100644 index 86226265dd146..0000000000000 --- a/app/code/Magento/Sales/Test/Mftf/Metadata/sales_enable_rma_config-meta.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="SalesRMAConfig" dataType="sales_rma_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/sales/" method="POST"> - <object key="groups" dataType="sales_rma_config"> - <object key="rma" dataType="sales_rma_config"> - <object key="fields" dataType="sales_rma_config"> - <object key="enabled" dataType="enabled"> - <field key="value">string</field> - </object> - </object> - </object> - </object> - </operation> -</operations> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderProcessDataPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderProcessDataPage.xml new file mode 100644 index 0000000000000..2041bf8f3c9ae --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderProcessDataPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminOrderProcessDataPage" url="sales/order_create/processData" area="admin" module="Magento_Sales"> + <section name="AdminOrderFormItemsOrderedSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontOrdersAndReturnsPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontOrdersAndReturnsPage.xml index 32d94c3175807..ee546174d9680 100644 --- a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontOrdersAndReturnsPage.xml +++ b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontOrdersAndReturnsPage.xml @@ -9,7 +9,6 @@ <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="StorefrontOrdersAndReturnsPage" url="sales/guest/form" area="guest" module="Magento_Sales"> - <section name="OrderAndReturnsMainSection"/> - <section name="OrderInformationSection"/> + <section name="StorefrontOrderAndReturnInformationSection"/> </page> </pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml index 11673f1f0fe26..beb566b20806c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml @@ -15,5 +15,6 @@ <element name="configureProductQtyField" type="input" selector="//*[@id='super-product-table']/tbody/tr[{{arg}}]/td[5]/input[1]" parameterized="true"/> <element name="addProductToOrder" type="input" selector="//*[@title='Add Products to Order']"/> <element name="itemsOrderedSummaryText" type="textarea" selector="//table[@class='data-table admin__table-primary order-tables']/tfoot/tr"/> + <element name="configureSelectAttribute" type="select" selector="select[id*=attribute]"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderItemsOrderedSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderItemsOrderedSection.xml index 53aeeb62c6b70..5c2ff296ebeee 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderItemsOrderedSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderItemsOrderedSection.xml @@ -23,8 +23,10 @@ <element name="productNameColumn" type="text" selector=".edit-order-table .col-product .product-title"/> <element name="productNameOptions" type="text" selector=".edit-order-table .col-product .item-options"/> + <element name="productName" type="text" selector="#order-items_grid span[id*=order_item]"/> <element name="productNameOptionsLink" type="text" selector="//table[contains(@class, 'edit-order-table')]//td[contains(@class, 'col-product')]//a[text() = '{{var1}}']" parameterized="true"/> <element name="productSkuColumn" type="text" selector=".edit-order-table .col-product .product-sku-block"/> + <element name="productTotal" type="text" selector="#order-items_grid .col-total"/> <element name="statusColumn" type="text" selector=".edit-order-table .col-status"/> <element name="originalPriceColumn" type="text" selector=".edit-order-table .col-original-price .price"/> <element name="priceColumn" type="text" selector=".edit-order-table .col-price .price"/> @@ -35,4 +37,4 @@ <element name="discountAmountColumn" type="text" selector=".edit-order-table .col-discont .price"/> <element name="totalColumn" type="text" selector=".edit-order-table .col-total .price"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCreateNewReturnMainSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCreateNewReturnMainSection.xml deleted file mode 100644 index fe8391cf3c28f..0000000000000 --- a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCreateNewReturnMainSection.xml +++ /dev/null @@ -1,18 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="StorefrontCreateNewReturnMainSection"> - <element name="quantityToReturn" type="input" selector="#items:qty_requested0"/> - <element name="submit" type="submit" selector="//span[contains(text(), 'Submit')]"/> - <element name="resolutionError" type="text" selector="//*[@id='items:resolution0']/following-sibling::div[contains(text(),'Please select an option')]"/> - <element name="conditionError" type="text" selector="//*[@id='items:condition0']/following-sibling::div[contains(text(),'Please select an option')]"/> - <element name="reasonError" type="text" selector="//*[@id='items:reason0']/following-sibling::div[contains(text(),'Please select an option')]"/> - </section> -</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml new file mode 100644 index 0000000000000..7c83f35468ce6 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCorrectnessInvoicedItemInBundleProductTest"> + <annotations> + <features value="Sales"/> + <title value="Check correctness of invoiced items in a Bundle Product"/> + <description value="Check correctness of invoiced items in a Bundle Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11059"/> + <useCaseId value="MC-10969"/> + <group value="sales"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create category and simple product--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!--Create bundle product--> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="createSimpleProduct"/> + <field key="qty">10</field> + </createData> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> + + <actionGroup ref="logout" stepKey="logOut"/> + </after> + + <!--Complete Bundle product creation--> + <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="goToProductEditPage"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Go to bundle product page--> + <amOnPage url="{{StorefrontProductPage.url($$createCategory.name$$)}}" stepKey="navigateToBundleProductPage"/> + + <!--Place order bundle product with 10 options--> + <actionGroup ref="StorefrontAddCategoryBundleProductToCartActionGroup" stepKey="addBundleProductToCart"> + <argument name="product" value="$$createBundleProduct$$"/> + <argument name="quantity" value="10"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + + <!--Go to order page submit invoice--> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForCreatedOrderPageOpened"/> + <actionGroup ref="goToInvoiceIntoOrder" stepKey="goToInvoiceIntoOrderPage"/> + <fillField selector="{{AdminInvoiceItemsSection.qtyToInvoiceColumn}}" userInput="5" stepKey="ChangeQtyToInvoice"/> + <click selector="{{AdminInvoiceItemsSection.updateQty}}" stepKey="updateQunatity"/> + <waitForPageLoad stepKey="waitPageToBeLoaded"/> + <actionGroup ref="submitInvoiceIntoOrder" stepKey="submitInvoice"/> + + <!--Verify invoiced items qty in ship tab--> + <actionGroup ref="goToShipmentIntoOrder" stepKey="goToShipment"/> + <grabTextFrom selector="{{AdminShipmentItemsSection.itemQtyInvoiced('1')}}" stepKey="grabInvoicedItemQty"/> + <assertEquals expected="5" expectedType="string" actual="$grabInvoicedItemQty" stepKey="assertInvoicedItemsQty"/> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Model/Quote/Address/Total/ShippingDiscount.php b/app/code/Magento/SalesRule/Model/Quote/Address/Total/ShippingDiscount.php new file mode 100644 index 0000000000000..c37ca276e0ee2 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Quote/Address/Total/ShippingDiscount.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Quote\Address\Total; + +use Magento\Quote\Api\Data\ShippingAssignmentInterface as ShippingAssignment; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address\Total; +use Magento\SalesRule\Model\Quote\Discount as DiscountCollector; +use Magento\SalesRule\Model\Validator; + +/** + * Total collector for shipping discounts. + */ +class ShippingDiscount extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal +{ + /** + * @var Validator + */ + private $calculator; + + /** + * @param Validator $calculator + */ + public function __construct(Validator $calculator) + { + $this->calculator = $calculator; + } + + /** + * @inheritdoc + * + * @param Quote $quote + * @param ShippingAssignment $shippingAssignment + * @param Total $total + * @return ShippingDiscount + */ + public function collect(Quote $quote, ShippingAssignment $shippingAssignment, Total $total): self + { + parent::collect($quote, $shippingAssignment, $total); + + $address = $shippingAssignment->getShipping()->getAddress(); + $this->calculator->reset($address); + + $items = $shippingAssignment->getItems(); + if (!count($items)) { + return $this; + } + + $address->setShippingDiscountAmount(0); + $address->setBaseShippingDiscountAmount(0); + if ($address->getShippingAmount()) { + $this->calculator->processShippingAmount($address); + $total->addTotalAmount(DiscountCollector::COLLECTOR_TYPE_CODE, -$address->getShippingDiscountAmount()); + $total->addBaseTotalAmount( + DiscountCollector::COLLECTOR_TYPE_CODE, + -$address->getBaseShippingDiscountAmount() + ); + $total->setShippingDiscountAmount($address->getShippingDiscountAmount()); + $total->setBaseShippingDiscountAmount($address->getBaseShippingDiscountAmount()); + + $this->calculator->prepareDescription($address); + $total->setDiscountDescription($address->getDiscountDescription()); + $total->setSubtotalWithDiscount($total->getSubtotal() + $total->getDiscountAmount()); + $total->setBaseSubtotalWithDiscount($total->getBaseSubtotal() + $total->getBaseDiscountAmount()); + + $address->setDiscountAmount($total->getDiscountAmount()); + $address->setBaseDiscountAmount($total->getBaseDiscountAmount()); + } + + return $this; + } + + /** + * @inheritdoc + * + * @param \Magento\Quote\Model\Quote $quote + * @param \Magento\Quote\Model\Quote\Address\Total $total + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function fetch(Quote $quote, Total $total): array + { + $result = []; + $amount = $total->getDiscountAmount(); + + if ($amount != 0) { + $description = $total->getDiscountDescription() ?: ''; + $result = [ + 'code' => DiscountCollector::COLLECTOR_TYPE_CODE, + 'title' => strlen($description) ? __('Discount (%1)', $description) : __('Discount'), + 'value' => $amount + ]; + } + return $result; + } +} diff --git a/app/code/Magento/SalesRule/Model/Quote/Discount.php b/app/code/Magento/SalesRule/Model/Quote/Discount.php index 693a61b272f66..315ce874513a3 100644 --- a/app/code/Magento/SalesRule/Model/Quote/Discount.php +++ b/app/code/Magento/SalesRule/Model/Quote/Discount.php @@ -5,8 +5,13 @@ */ namespace Magento\SalesRule\Model\Quote; +/** + * Discount totals calculation model. + */ class Discount extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal { + const COLLECTOR_TYPE_CODE = 'discount'; + /** * Discount calculation object * @@ -43,7 +48,7 @@ public function __construct( \Magento\SalesRule\Model\Validator $validator, \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency ) { - $this->setCode('discount'); + $this->setCode(self::COLLECTOR_TYPE_CODE); $this->eventManager = $eventManager; $this->calculator = $validator; $this->storeManager = $storeManager; @@ -124,21 +129,14 @@ public function collect( } } - /** Process shipping amount discount */ - $address->setShippingDiscountAmount(0); - $address->setBaseShippingDiscountAmount(0); - if ($address->getShippingAmount()) { - $this->calculator->processShippingAmount($address); - $total->addTotalAmount($this->getCode(), -$address->getShippingDiscountAmount()); - $total->addBaseTotalAmount($this->getCode(), -$address->getBaseShippingDiscountAmount()); - $total->setShippingDiscountAmount($address->getShippingDiscountAmount()); - $total->setBaseShippingDiscountAmount($address->getBaseShippingDiscountAmount()); - } - $this->calculator->prepareDescription($address); $total->setDiscountDescription($address->getDiscountDescription()); $total->setSubtotalWithDiscount($total->getSubtotal() + $total->getDiscountAmount()); $total->setBaseSubtotalWithDiscount($total->getBaseSubtotal() + $total->getBaseDiscountAmount()); + + $address->setDiscountAmount($total->getDiscountAmount()); + $address->setBaseDiscountAmount($total->getBaseDiscountAmount()); + return $this; } diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php index 59f24fa8b6e03..5e6f3847c8e31 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php @@ -80,6 +80,8 @@ protected function _construct() } /** + * Map data for associated entities + * * @param string $entityType * @param string $objectField * @throws \Magento\Framework\Exception\LocalizedException @@ -114,6 +116,8 @@ protected function mapAssociatedEntities($entityType, $objectField) } /** + * Add website ids and customer group ids to rules data + * * @return $this * @throws \Exception * @since 100.1.0 @@ -158,60 +162,15 @@ public function setValidationFilter( $connection = $this->getConnection(); if (strlen($couponCode)) { - $select->joinLeft( - ['rule_coupons' => $this->getTable('salesrule_coupon')], - $connection->quoteInto( - 'main_table.rule_id = rule_coupons.rule_id AND main_table.coupon_type != ?', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ), - ['code'] - ); - $noCouponWhereCondition = $connection->quoteInto( - 'main_table.coupon_type = ? ', + 'main_table.coupon_type = ?', \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON ); - - $autoGeneratedCouponCondition = [ - $connection->quoteInto( - "main_table.coupon_type = ?", - \Magento\SalesRule\Model\Rule::COUPON_TYPE_AUTO - ), - $connection->quoteInto( - "rule_coupons.type = ?", - \Magento\SalesRule\Api\Data\CouponInterface::TYPE_GENERATED - ), - ]; - - $orWhereConditions = [ - "(" . implode($autoGeneratedCouponCondition, " AND ") . ")", - $connection->quoteInto( - '(main_table.coupon_type = ? AND main_table.use_auto_generation = 1 AND rule_coupons.type = 1)', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC - ), - $connection->quoteInto( - '(main_table.coupon_type = ? AND main_table.use_auto_generation = 0 AND rule_coupons.type = 0)', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC - ), - ]; - - $andWhereConditions = [ - $connection->quoteInto( - 'rule_coupons.code = ?', - $couponCode - ), - $connection->quoteInto( - '(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)', - $this->_date->date()->format('Y-m-d') - ), - ]; - - $orWhereCondition = implode(' OR ', $orWhereConditions); - $andWhereCondition = implode(' AND ', $andWhereConditions); + $relatedRulesIds = $this->getCouponRelatedRuleIds($couponCode); $select->where( - $noCouponWhereCondition . ' OR ((' . $orWhereCondition . ') AND ' . $andWhereCondition . ')', - null, + $noCouponWhereCondition . ' OR main_table.rule_id IN (?)', + $relatedRulesIds, Select::TYPE_CONDITION ); } else { @@ -227,6 +186,75 @@ public function setValidationFilter( return $this; } + /** + * Get rules ids related to coupon code + * + * @param string $couponCode + * @return array + */ + private function getCouponRelatedRuleIds(string $couponCode): array + { + $connection = $this->getConnection(); + $select = $connection->select()->from( + ['main_table' => $this->getTable('salesrule')], + 'rule_id' + ); + $select->joinLeft( + ['rule_coupons' => $this->getTable('salesrule_coupon')], + $connection->quoteInto( + 'main_table.rule_id = rule_coupons.rule_id AND main_table.coupon_type != ?', + \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON, + null + ) + ); + + $autoGeneratedCouponCondition = [ + $connection->quoteInto( + "main_table.coupon_type = ?", + \Magento\SalesRule\Model\Rule::COUPON_TYPE_AUTO + ), + $connection->quoteInto( + "rule_coupons.type = ?", + \Magento\SalesRule\Api\Data\CouponInterface::TYPE_GENERATED + ), + ]; + + $orWhereConditions = [ + "(" . implode($autoGeneratedCouponCondition, " AND ") . ")", + $connection->quoteInto( + '(main_table.coupon_type = ? AND main_table.use_auto_generation = 1 AND rule_coupons.type = 1)', + \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC + ), + $connection->quoteInto( + '(main_table.coupon_type = ? AND main_table.use_auto_generation = 0 AND rule_coupons.type = 0)', + \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC + ), + ]; + + $andWhereConditions = [ + $connection->quoteInto( + 'rule_coupons.code = ?', + $couponCode + ), + $connection->quoteInto( + '(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)', + $this->_date->date()->format('Y-m-d') + ), + ]; + + $orWhereCondition = implode(' OR ', $orWhereConditions); + $andWhereCondition = implode(' AND ', $andWhereConditions); + + $select->where( + '(' . $orWhereCondition . ') AND ' . $andWhereCondition, + null, + Select::TYPE_CONDITION + ); + $select->group('main_table.rule_id'); + + return $connection->fetchCol($select); + } + /** * Filter collection by website(s), customer group(s) and date. * Filter collection to only active rules. @@ -366,6 +394,8 @@ public function addCustomerGroupFilter($customerGroupId) } /** + * Getter for _associatedEntitiesMap property + * * @return array * @deprecated 100.1.0 */ @@ -380,6 +410,8 @@ private function getAssociatedEntitiesMap() } /** + * Getter for dateApplier property + * * @return DateApplier * @deprecated 100.1.0 */ diff --git a/app/code/Magento/SalesRule/etc/sales.xml b/app/code/Magento/SalesRule/etc/sales.xml index 3ab197d40b0df..d2db664224873 100644 --- a/app/code/Magento/SalesRule/etc/sales.xml +++ b/app/code/Magento/SalesRule/etc/sales.xml @@ -8,7 +8,8 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Sales:etc/sales.xsd"> <section name="quote"> <group name="totals"> - <item name="discount" instance="Magento\SalesRule\Model\Quote\Discount" sort_order="400"/> + <item name="discount" instance="Magento\SalesRule\Model\Quote\Discount" sort_order="300"/> + <item name="shipping_discount" instance="Magento\SalesRule\Model\Quote\Address\Total\ShippingDiscount" sort_order="400"/> </group> </section> </config> diff --git a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php index 46e794a1954cf..45eee0a4001d1 100644 --- a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php +++ b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php @@ -87,7 +87,7 @@ private function queryByPhrase($phrase) { $matchQuery = $this->fullTextSelect->getMatchQuery( ['synonyms' => 'synonyms'], - $phrase, + $this->escapePhrase($phrase), Fulltext::FULLTEXT_MODE_BOOLEAN ); $query = $this->getConnection()->select()->from( @@ -97,6 +97,18 @@ private function queryByPhrase($phrase) return $this->getConnection()->fetchAll($query); } + /** + * Cut trailing plus or minus sign, and @ symbol, using of which causes InnoDB to report a syntax error. + * + * @see https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html + * @param string $phrase + * @return string + */ + private function escapePhrase(string $phrase): string + { + return preg_replace('/@+|[@+-]+$/', '', $phrase); + } + /** * A private helper function to retrieve matching synonym groups per scope * diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml index a7bf82588f7c7..0345c3f2949f4 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml @@ -15,5 +15,6 @@ <element name="itemQtyToShip" type="input" selector=".order-shipment-table tbody:nth-of-type({{var1}}) .col-qty input.qty-item" parameterized="true"/> <element name="nameColumn" type="text" selector=".order-shipment-table .col-product .product-title"/> <element name="skuColumn" type="text" selector=".order-shipment-table .col-product .product-sku-block"/> + <element name="itemQtyInvoiced" type="text" selector="(//*[@class='col-ordered-qty']//th[contains(text(), 'Invoiced')]/following-sibling::td)[{{var}}]" parameterized="true"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml index bee9a79abeb77..8004b750a4d1f 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontHeaderSection"> <element name="storeViewSwitcher" type="button" selector="#switcher-language-trigger"/> - <element name="storeViewDropdown" type="button" selector="ul.switcher-dropdown"/> + <element name="storeViewDropdown" type="button" selector=".active ul.switcher-dropdown"/> <element name="storeViewOption" type="button" selector="li.view-{{var1}}>a" parameterized="true"/> <element name="storeView" type="button" selector="//div[@class='actions dropdown options switcher-options active']//ul//li//a[contains(text(),'{{var}}')]" parameterized="true"/> <element name="storeViewList" type="button" selector="//li[contains(.,'{{storeViewName}}')]//a" parameterized="true"/> diff --git a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js index 962ac8b171692..bd611d0cc1863 100644 --- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js @@ -493,7 +493,7 @@ define([ return ''; } - $.each(config.options, function () { + $.each(config.options, function (index) { var id, type, value, @@ -523,6 +523,7 @@ define([ label = this.label ? this.label : ''; attr = ' id="' + controlId + '-item-' + id + '"' + + ' index="' + index + '"' + ' aria-checked="false"' + ' aria-describedby="' + controlId + '"' + ' tabindex="0"' + @@ -745,6 +746,12 @@ define([ $widget._UpdatePrice(); } + $(document).trigger('updateMsrpPriceBlock', + [ + parseInt($this.attr('index'), 10) + 1, + $widget.options.jsonConfig.optionPrices + ]); + $widget._loadMedia(); $input.trigger('change'); }, @@ -1229,7 +1236,10 @@ define([ } imagesToUpdate = this._setImageIndex(imagesToUpdate); - gallery.updateData(imagesToUpdate); + + if (!_.isUndefined(gallery)) { + gallery.updateData(imagesToUpdate); + } if (isInitial) { $(this.options.mediaGallerySelector).AddFotoramaVideoEvents(); diff --git a/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php b/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php index bad64260cf58a..939facd02c02d 100644 --- a/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php +++ b/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php @@ -7,6 +7,9 @@ use Magento\Tax\Api\Data\QuoteDetailsItemInterface; +/** + * Abstract aggregate calculator. + */ abstract class AbstractAggregateCalculator extends AbstractCalculator { /** @@ -106,11 +109,12 @@ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $ $rowTaxes = []; $rowTaxesBeforeDiscount = []; $appliedTaxes = []; + $rowTotalForTaxCalculation = $this->getPriceForTaxCalculation($item, $price) * $quantity; //Apply each tax rate separately foreach ($appliedRates as $appliedRate) { $taxId = $appliedRate['id']; $taxRate = $appliedRate['percent']; - $rowTaxPerRate = $this->calculationTool->calcTaxAmount($rowTotal, $taxRate, false, false); + $rowTaxPerRate = $this->calculationTool->calcTaxAmount($rowTotalForTaxCalculation, $taxRate, false, false); $deltaRoundingType = self::KEY_REGULAR_DELTA_ROUNDING; if ($applyTaxAfterDiscount) { $deltaRoundingType = self::KEY_TAX_BEFORE_DISCOUNT_DELTA_ROUNDING; @@ -121,7 +125,10 @@ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $ //Handle discount if ($applyTaxAfterDiscount) { //TODO: handle originalDiscountAmount - $taxableAmount = max($rowTotal - $discountAmount, 0); + $taxableAmount = max($rowTotalForTaxCalculation - $discountAmount, 0); + if ($taxableAmount && !$applyTaxAfterDiscount) { + $taxableAmount = $rowTotalForTaxCalculation; + } $rowTaxAfterDiscount = $this->calculationTool->calcTaxAmount( $taxableAmount, $taxRate, @@ -168,6 +175,26 @@ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $ ->setAppliedTaxes($appliedTaxes); } + /** + * Get price for tax calculation. + * + * @param QuoteDetailsItemInterface $item + * @param float $price + * @return float + */ + private function getPriceForTaxCalculation(QuoteDetailsItemInterface $item, float $price) + { + if ($item->getExtensionAttributes() && $item->getExtensionAttributes()->getPriceForTaxCalculation()) { + $priceForTaxCalculation = $this->calculationTool->round( + $item->getExtensionAttributes()->getPriceForTaxCalculation() + ); + } else { + $priceForTaxCalculation = $price; + } + + return $priceForTaxCalculation; + } + /** * Round amount * diff --git a/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php b/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php index 0901e1b7bc78c..bff489ee50c2f 100644 --- a/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php +++ b/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php @@ -15,12 +15,17 @@ use Magento\Quote\Model\Quote\Item\AbstractItem; use Magento\Store\Model\Store; use Magento\Tax\Api\Data\QuoteDetailsInterfaceFactory; +use Magento\Tax\Api\Data\QuoteDetailsItemInterface; use Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory; use Magento\Tax\Api\Data\TaxClassKeyInterface; use Magento\Tax\Api\Data\TaxDetailsInterface; use Magento\Tax\Api\Data\TaxDetailsItemInterface; use Magento\Tax\Api\Data\QuoteDetailsInterface; use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Framework\App\ObjectManager; +use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterface; +use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterfaceFactory; /** * Tax totals calculation model @@ -129,6 +134,16 @@ class CommonTaxCollector extends AbstractTotal */ protected $quoteDetailsItemDataObjectFactory; + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * @var QuoteDetailsItemExtensionInterfaceFactory + */ + private $quoteDetailsItemExtensionFactory; + /** * Class constructor * @@ -139,6 +154,8 @@ class CommonTaxCollector extends AbstractTotal * @param \Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory $taxClassKeyDataObjectFactory * @param CustomerAddressFactory $customerAddressFactory * @param CustomerAddressRegionFactory $customerAddressRegionFactory + * @param TaxHelper|null $taxHelper + * @param QuoteDetailsItemExtensionInterfaceFactory|null $quoteDetailsItemExtensionInterfaceFactory */ public function __construct( \Magento\Tax\Model\Config $taxConfig, @@ -147,7 +164,9 @@ public function __construct( \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $quoteDetailsItemDataObjectFactory, \Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory $taxClassKeyDataObjectFactory, CustomerAddressFactory $customerAddressFactory, - CustomerAddressRegionFactory $customerAddressRegionFactory + CustomerAddressRegionFactory $customerAddressRegionFactory, + TaxHelper $taxHelper = null, + QuoteDetailsItemExtensionInterfaceFactory $quoteDetailsItemExtensionInterfaceFactory = null ) { $this->taxCalculationService = $taxCalculationService; $this->quoteDetailsDataObjectFactory = $quoteDetailsDataObjectFactory; @@ -156,6 +175,9 @@ public function __construct( $this->quoteDetailsItemDataObjectFactory = $quoteDetailsItemDataObjectFactory; $this->customerAddressFactory = $customerAddressFactory; $this->customerAddressRegionFactory = $customerAddressRegionFactory; + $this->taxHelper = $taxHelper ?: ObjectManager::getInstance()->get(TaxHelper::class); + $this->quoteDetailsItemExtensionFactory = $quoteDetailsItemExtensionInterfaceFactory ?: + ObjectManager::getInstance()->get(QuoteDetailsItemExtensionInterfaceFactory::class); } /** @@ -186,7 +208,7 @@ public function mapAddress(QuoteAddress $address) * @param bool $priceIncludesTax * @param bool $useBaseCurrency * @param string $parentCode - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface + * @return QuoteDetailsItemInterface */ public function mapItem( \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $itemDataObjectFactory, @@ -199,7 +221,7 @@ public function mapItem( $sequence = 'sequence-' . $this->getNextIncrement(); $item->setTaxCalculationItemId($sequence); } - /** @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface $itemDataObject */ + /** @var QuoteDetailsItemInterface $itemDataObject */ $itemDataObject = $itemDataObjectFactory->create(); $itemDataObject->setCode($item->getTaxCalculationItemId()) ->setQuantity($item->getQty()) @@ -215,12 +237,28 @@ public function mapItem( if (!$item->getBaseTaxCalculationPrice()) { $item->setBaseTaxCalculationPrice($item->getBaseCalculationPriceOriginal()); } + + if ($this->taxHelper->applyTaxOnOriginalPrice()) { + $baseTaxCalculationPrice = $item->getBaseOriginalPrice(); + } else { + $baseTaxCalculationPrice = $item->getBaseCalculationPriceOriginal(); + } + $this->setPriceForTaxCalculation($itemDataObject, (float)$baseTaxCalculationPrice); + $itemDataObject->setUnitPrice($item->getBaseTaxCalculationPrice()) ->setDiscountAmount($item->getBaseDiscountAmount()); } else { if (!$item->getTaxCalculationPrice()) { $item->setTaxCalculationPrice($item->getCalculationPriceOriginal()); } + + if ($this->taxHelper->applyTaxOnOriginalPrice()) { + $taxCalculationPrice = $item->getOriginalPrice(); + } else { + $taxCalculationPrice = $item->getCalculationPriceOriginal(); + } + $this->setPriceForTaxCalculation($itemDataObject, (float)$taxCalculationPrice); + $itemDataObject->setUnitPrice($item->getTaxCalculationPrice()) ->setDiscountAmount($item->getDiscountAmount()); } @@ -230,6 +268,23 @@ public function mapItem( return $itemDataObject; } + /** + * Set price for tax calculation. + * + * @param QuoteDetailsItemInterface $quoteDetailsItem + * @param float $taxCalculationPrice + * @return void + */ + private function setPriceForTaxCalculation(QuoteDetailsItemInterface $quoteDetailsItem, float $taxCalculationPrice) + { + $extensionAttributes = $quoteDetailsItem->getExtensionAttributes(); + if (!$extensionAttributes) { + $extensionAttributes = $this->quoteDetailsItemExtensionFactory->create(); + } + $extensionAttributes->setPriceForTaxCalculation($taxCalculationPrice); + $quoteDetailsItem->setExtensionAttributes($extensionAttributes); + } + /** * Map item extra taxables * @@ -237,7 +292,7 @@ public function mapItem( * @param AbstractItem $item * @param bool $priceIncludesTax * @param bool $useBaseCurrency - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface[] + * @return QuoteDetailsItemInterface[] */ public function mapItemExtraTaxables( \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $itemDataObjectFactory, @@ -260,7 +315,7 @@ public function mapItemExtraTaxables( } else { $unitPrice = $extraTaxable[self::KEY_ASSOCIATED_TAXABLE_UNIT_PRICE]; } - /** @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface $itemDataObject */ + /** @var QuoteDetailsItemInterface $itemDataObject */ $itemDataObject = $itemDataObjectFactory->create(); $itemDataObject->setCode($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_CODE]) ->setType($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_TYPE]) @@ -283,9 +338,9 @@ public function mapItemExtraTaxables( * Add quote items * * @param ShippingAssignmentInterface $shippingAssignment - * @param bool $useBaseCurrency * @param bool $priceIncludesTax - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface[] + * @param bool $useBaseCurrency + * @return QuoteDetailsItemInterface[] */ public function mapItems( ShippingAssignmentInterface $shippingAssignment, @@ -361,10 +416,12 @@ public function populateAddressData(QuoteDetailsInterface $quoteDetails, QuoteAd } /** + * Get shipping data object. + * * @param ShippingAssignmentInterface $shippingAssignment * @param QuoteAddress\Total $total * @param bool $useBaseCurrency - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface + * @return QuoteDetailsItemInterface */ public function getShippingDataObject( ShippingAssignmentInterface $shippingAssignment, @@ -379,7 +436,7 @@ public function getShippingDataObject( $total->setBaseShippingTaxCalculationAmount($total->getBaseShippingAmount()); } if ($total->getShippingTaxCalculationAmount() !== null) { - /** @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface $itemDataObject */ + /** @var QuoteDetailsItemInterface $itemDataObject */ $itemDataObject = $this->quoteDetailsItemDataObjectFactory->create() ->setType(self::ITEM_TYPE_SHIPPING) ->setCode(self::ITEM_CODE_SHIPPING) @@ -414,7 +471,7 @@ public function getShippingDataObject( * Populate QuoteDetails object from quote address object * * @param ShippingAssignmentInterface $shippingAssignment - * @param \Magento\Tax\Api\Data\QuoteDetailsItemInterface[] $itemDataObjects + * @param QuoteDetailsItemInterface[] $itemDataObjects * @return \Magento\Tax\Api\Data\QuoteDetailsInterface */ protected function prepareQuoteDetails(ShippingAssignmentInterface $shippingAssignment, $itemDataObjects) @@ -543,6 +600,7 @@ protected function processProductItems( * Process applied taxes for items and quote * * @param QuoteAddress\Total $total + * @param ShippingAssignmentInterface $shippingAssignment * @param array $itemsByType * @return $this */ @@ -846,8 +904,9 @@ protected function saveAppliedTaxes() } /** - * Increment and return counter. This function is intended to be used to generate temporary - * id for an item. + * Increment and return counter. + * + * This function is intended to be used to generate temporary id for an item. * * @return int */ diff --git a/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml b/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml index 887203a76fdad..4409ea0a21df6 100644 --- a/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml +++ b/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml @@ -106,4 +106,8 @@ <data key="zip_is_range">0</data> <data key="rate">0.1</data> </entity> + <entity name="taxRateForPensylvannia" extends="defaultTaxRate"> + <data key="tax_region_id">51</data> + <data key="rate">6</data> + </entity> </entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontCreateNewReturnPage.xml b/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRatePage.xml similarity index 63% rename from app/code/Magento/Sales/Test/Mftf/Page/StorefrontCreateNewReturnPage.xml rename to app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRatePage.xml index 2a14f814eac16..26152d5497a98 100644 --- a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontCreateNewReturnPage.xml +++ b/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRatePage.xml @@ -8,7 +8,7 @@ <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - <page name="StorefrontCreateNewReturnPage" url="rma/guest/create/order_id/" area="guest" module="Magento_Sales"> - <section name="StorefrontCreateNewReturnMainSection"/> + <page name="AdminEditTaxRatePage" url="tax/rate/edit/rate/{{var1}}/" module="Magento_Tax" area="admin" parameterized="true"> + <section name="AdminTaxRateFormSection"/> </page> </pages> diff --git a/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRulePage.xml b/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRulePage.xml new file mode 100644 index 0000000000000..c0e4958619c89 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRulePage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminEditTaxRulePage" url="tax/rule/edit/rule/{{var}}/" module="Magento_Tax" area="admin" parameterized="true"> + <section name="AdminTaxRulesSection"/> + </page> +</pages> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/AdminConfigureTaxSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/AdminConfigureTaxSection.xml index bfb082c145f07..e69bfbaebbfd9 100644 --- a/app/code/Magento/Tax/Test/Mftf/Section/AdminConfigureTaxSection.xml +++ b/app/code/Magento/Tax/Test/Mftf/Section/AdminConfigureTaxSection.xml @@ -28,6 +28,8 @@ <element name="taxCalculationPrices" type="select" selector="#tax_calculation_price_includes_tax"/> <element name="taxCalculationPricesDisabled" type="select" selector="#tax_calculation_price_includes_tax[disabled='disabled']"/> <element name="taxCalculationPricesInherit" type="checkbox" selector="#tax_calculation_price_includes_tax_inherit"/> + <element name="taxCalculationApplyTaxOn" type="select" selector="#tax_calculation_apply_tax_on"/> + <element name="taxCalculationApplyTaxOnInherit" type="checkbox" selector="#tax_calculation_apply_tax_on_inherit"/> <element name="defaultDestination" type="block" selector="#tax_defaults-head" timeout="30"/> <element name="systemValueDefaultState" type="checkbox" selector="#row_tax_defaults_region input[type='checkbox']"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesSection.xml index 29c53242b90f6..46d92e30395e0 100644 --- a/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesSection.xml +++ b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesSection.xml @@ -33,5 +33,6 @@ <element name="deleteTaxClass" type="button" selector="//span[contains(text(),'{{var1}}')]/../..//*[@class='mselect-delete']" parameterized="true"/> <element name="popUpDialogOK" type="button" selector="//*[@class='modal-footer']//*[contains(text(),'OK')]"/> <element name="taxRateMultiSelectItems" type="block" selector=".mselect-list-item"/> + <element name="taxRateNumber" type="button" selector="//div[@data-ui-id='tax-rate-form-fieldset-element-form-field-tax-rate']//div[@class='mselect-items-wrapper']//label[{{var}}]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml new file mode 100644 index 0000000000000..732470d2558c7 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminTaxCalcWithApplyTaxOnSettingTest"> + <annotations> + <features value="AdminTaxCalcWithApplyTaxOnSettingTest"/> + <title value="Tax calculation process following 'Apply Tax On' setting"/> + <description value="Tax calculation process following 'Apply Tax On' setting"/> + <severity value="MAJOR"/> + <testCaseId value="MC-11026"/> + <useCaseId value="MC-4316"/> + <group value="Tax"/> + </annotations> + + <before> + <createData entity="taxRateForPensylvannia" stepKey="initialTaxRate"/> + <createData entity="defaultTaxRule" stepKey="createTaxRule"/> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProductWithCustomPrice" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="SetTaxClassForShipping" stepKey="setTaxClass"/> + <actionGroup ref="SetTaxApplyOnSetting" stepKey="setApplyTaxOnSetting"> + <argument name="userInput" value="Original price only"/> + </actionGroup> + <amOnPage url="{{AdminEditTaxRulePage.url($$createTaxRule.id$$)}}" stepKey="navigateToEditTaxRulePage"/> + <waitForPageLoad stepKey="waitEditTaxRulePageToLoad"/> + <click selector="{{AdminTaxRulesSection.taxRateNumber('1')}}" stepKey="clickonTaxRate"/> + <click selector="{{AdminTaxRulesSection.deleteTaxClassName($$initialTaxRate.code$$)}}" stepKey="checkTaxRate"/> + <click selector="{{AdminTaxRulesSection.saveRule}}" stepKey="saveChanges"/> + <waitForPageLoad stepKey="waitTaxRulesToBeSaved"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rule." stepKey="seeSuccessMessage2"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> + <actionGroup ref="DisableTaxApplyOnOriginalPrice" stepKey="setApplyTaxOffSetting"> + <argument name="userInput" value="Custom price if available"/> + </actionGroup> + <actionGroup ref="ResetTaxClassForShipping" stepKey="resetTaxClassForShipping"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="navigateToNewOrderPageNewCustomerSingleStore" stepKey="gotoNewOrderCreationPage"/> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder"> + <argument name="product" value="$$createProduct$$"></argument> + </actionGroup> + <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillEmailField"/> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_CA"/> + </actionGroup> + <scrollTo selector="{{AdminOrderFormAccountSection.email}}" stepKey="scrollToEmailField"/> + <waitForElementVisible selector="{{AdminOrderFormAccountSection.email}}" stepKey="waitEmailFieldToBeVisible"/> + <click selector="{{AdminOrderFormShippingAddressSection.SameAsBilling}}" stepKey="uncheckSameAsBillingAddressCheckbox"/> + <waitForPageLoad stepKey="waitSectionToReload"/> + <selectOption selector="{{AdminOrderFormShippingAddressSection.State}}" stepKey="switchOnVisibleInAdvancedSearch" userInput="Pennsylvania"/> + <click selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="getShippingMethods"/> + <waitForPageLoad stepKey="waitForApplyingShippingMethods"/> + <grabTextFrom selector="{{AdminOrderFormTotalSection.subtotalRow('3')}}" stepKey="grabTaxCost"/> + <assertEquals expected='$6.00' expectedType="string" actual="($grabTaxCost)" stepKey="assertTax"/> + <scrollTo selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="scrollToSubmitButton"/> + <waitForElementVisible selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="waitElementToBeVisble"/> + <click selector="{{AdminOrderFormItemsSection.customPriceCheckbox}}" stepKey="clickOnCustomPriceCheckbox"/> + <fillField selector="{{AdminOrderFormItemsSection.customPriceField}}" userInput="{{SimpleProductNameWithDoubleQuote.price}}" stepKey="changePrice"/> + <click selector="{{AdminOrderFormItemsSection.updateItemsAndQuantities}}" stepKey="updateItemsAndQunatities"/> + <assertEquals expected='$6.00' expectedType="string" actual="($grabTaxCost)" stepKey="assertTaxAfterCustomPrice"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php b/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php index cbd7ed46e38d7..2a7eeb27ee07e 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php @@ -9,6 +9,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Tax\Model\Calculation\RowBaseCalculator; use Magento\Tax\Model\Calculation\TotalBaseCalculator; +use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -66,6 +67,11 @@ class RowBaseAndTotalBaseCalculatorTestCase extends \PHPUnit\Framework\TestCase */ protected $taxDetailsItem; + /** + * @var \Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteDetailsItemExtension; + /** * initialize all mocks * @@ -101,7 +107,14 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->mockItem = $this->getMockBuilder(\Magento\Tax\Api\Data\QuoteDetailsItemInterface::class)->getMock(); + $this->mockItem = $this->getMockBuilder(\Magento\Tax\Api\Data\QuoteDetailsItemInterface::class) + ->disableOriginalConstructor()->setMethods(['getExtensionAttributes', 'getUnitPrice']) + ->getMockForAbstractClass(); + $this->quoteDetailsItemExtension = $this->getMockBuilder(QuoteDetailsItemExtensionInterface::class) + ->disableOriginalConstructor()->setMethods(['getPriceForTaxCalculation']) + ->getMockForAbstractClass(); + $this->mockItem->expects($this->any())->method('getExtensionAttributes') + ->willReturn($this->quoteDetailsItemExtension); $this->appliedTaxDataObjectFactory = $this->createPartialMock( \Magento\Tax\Api\Data\AppliedTaxInterfaceFactory::class, diff --git a/app/code/Magento/Tax/etc/extension_attributes.xml b/app/code/Magento/Tax/etc/extension_attributes.xml index 90a5e6d2ecee3..41af1df836d6f 100644 --- a/app/code/Magento/Tax/etc/extension_attributes.xml +++ b/app/code/Magento/Tax/etc/extension_attributes.xml @@ -20,4 +20,7 @@ <extension_attributes for="Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface"> <attribute code="tax_adjustments" type="Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface" /> </extension_attributes> + <extension_attributes for="Magento\Tax\Api\Data\QuoteDetailsItemInterface"> + <attribute code="price_for_tax_calculation" type="float" /> + </extension_attributes> </config> diff --git a/app/code/Magento/Tax/etc/sales.xml b/app/code/Magento/Tax/etc/sales.xml index 64d29ece898de..15afd499bce3f 100644 --- a/app/code/Magento/Tax/etc/sales.xml +++ b/app/code/Magento/Tax/etc/sales.xml @@ -9,7 +9,7 @@ <section name="quote"> <group name="totals"> <item name="tax_subtotal" instance="Magento\Tax\Model\Sales\Total\Quote\Subtotal" sort_order="200"/> - <item name="tax_shipping" instance="Magento\Tax\Model\Sales\Total\Quote\Shipping" sort_order="300"/> + <item name="tax_shipping" instance="Magento\Tax\Model\Sales\Total\Quote\Shipping" sort_order="375"/> <item name="tax" instance="Magento\Tax\Model\Sales\Total\Quote\Tax" sort_order="450"> <renderer name="adminhtml" instance="Magento\Sales\Block\Adminhtml\Order\Create\Totals\Tax"/> <renderer name="frontend" instance="Magento\Tax\Block\Checkout\Tax"/> diff --git a/app/code/Magento/Translation/Model/Json/PreProcessor.php b/app/code/Magento/Translation/Model/Json/PreProcessor.php index 5d46c3c8b0618..c178a324cb40b 100644 --- a/app/code/Magento/Translation/Model/Json/PreProcessor.php +++ b/app/code/Magento/Translation/Model/Json/PreProcessor.php @@ -6,6 +6,7 @@ namespace Magento\Translation\Model\Json; +use Magento\Framework\App\Area; use Magento\Framework\App\AreaList; use Magento\Framework\App\ObjectManager; use Magento\Framework\TranslateInterface; @@ -13,6 +14,7 @@ use Magento\Framework\View\Asset\PreProcessor\Chain; use Magento\Framework\View\Asset\PreProcessorInterface; use Magento\Framework\View\DesignInterface; +use Magento\Backend\App\Area\FrontNameResolver; use Magento\Translation\Model\Js\Config; use Magento\Translation\Model\Js\DataProviderInterface; @@ -83,7 +85,7 @@ public function process(Chain $chain) $context = $chain->getAsset()->getContext(); $themePath = '*/*'; - $areaCode = \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE; + $areaCode = FrontNameResolver::AREA_CODE; if ($context instanceof FallbackContext) { $themePath = $context->getThemePath(); @@ -92,8 +94,10 @@ public function process(Chain $chain) $this->viewDesign->setDesignTheme($themePath, $areaCode); } - $area = $this->areaList->getArea($areaCode); - $area->load(\Magento\Framework\App\Area::PART_TRANSLATE); + if ($areaCode !== FrontNameResolver::AREA_CODE) { + $area = $this->areaList->getArea($areaCode); + $area->load(Area::PART_TRANSLATE); + } $this->translate->setLocale($context->getLocale())->loadData($areaCode, true); diff --git a/app/code/Magento/Translation/Test/Unit/Model/Json/PreProcessorTest.php b/app/code/Magento/Translation/Test/Unit/Model/Json/PreProcessorTest.php index d9340e03dc996..cbeeefed6be6e 100644 --- a/app/code/Magento/Translation/Test/Unit/Model/Json/PreProcessorTest.php +++ b/app/code/Magento/Translation/Test/Unit/Model/Json/PreProcessorTest.php @@ -8,39 +8,43 @@ use Magento\Translation\Model\Js\Config; use Magento\Translation\Model\Js\DataProvider; use Magento\Translation\Model\Json\PreProcessor; +use Magento\Backend\App\Area\FrontNameResolver; class PreProcessorTest extends \PHPUnit\Framework\TestCase { /** * @var PreProcessor */ - protected $model; + private $model; /** * @var Config|\PHPUnit_Framework_MockObject_MockObject */ - protected $configMock; + private $configMock; /** * @var DataProvider|\PHPUnit_Framework_MockObject_MockObject */ - protected $dataProviderMock; + private $dataProviderMock; /** * @var \Magento\Framework\App\AreaList|\PHPUnit_Framework_MockObject_MockObject */ - protected $areaListMock; + private $areaListMock; /** * @var \Magento\Framework\TranslateInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $translateMock; + private $translateMock; /** * @var \Magento\Framework\View\DesignInterface|\PHPUnit_Framework_MockObject_MockObject */ private $designMock; + /** + * @inheritdoc + */ protected function setUp() { $this->configMock = $this->createMock(\Magento\Translation\Model\Js\Config::class); @@ -57,7 +61,14 @@ protected function setUp() ); } - public function testGetData() + /** + * Test 'process' method. + * + * @param array $data + * @param array $expects + * @dataProvider processDataProvider + */ + public function testProcess(array $data, array $expects) { $chain = $this->createMock(\Magento\Framework\View\Asset\PreProcessor\Chain::class); $asset = $this->createMock(\Magento\Framework\View\Asset\File::class); @@ -66,8 +77,10 @@ public function testGetData() $targetPath = 'path/js-translation.json'; $themePath = '*/*'; $dictionary = ['hello' => 'bonjour']; - $areaCode = 'adminhtml'; + $areaCode = $data['area_code']; + $area = $this->createMock(\Magento\Framework\App\Area::class); + $area->expects($expects['area_load'])->method('load')->willReturnSelf(); $chain->expects($this->once()) ->method('getTargetAssetPath') @@ -93,7 +106,7 @@ public function testGetData() $this->designMock->expects($this->once())->method('setDesignTheme')->with($themePath, $areaCode); - $this->areaListMock->expects($this->once()) + $this->areaListMock->expects($expects['areaList_getArea']) ->method('getArea') ->with($areaCode) ->willReturn($area); @@ -114,4 +127,33 @@ public function testGetData() $this->model->process($chain); } + + /** + * Data provider for 'process' method test. + * + * @return array + */ + public function processDataProvider() + { + return [ + [ + [ + 'area_code' => FrontNameResolver::AREA_CODE + ], + [ + 'areaList_getArea' => $this->never(), + 'area_load' => $this->never(), + ] + ], + [ + [ + 'area_code' => 'frontend' + ], + [ + 'areaList_getArea' => $this->once(), + 'area_load' => $this->once(), + ] + ], + ]; + } } diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js b/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js index 3d02afcc40a9e..ce19899cd12cd 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js @@ -18,6 +18,7 @@ define([ 'use strict'; return Abstract.extend({ + currentWysiwyg: undefined, defaults: { elementSelector: 'textarea', suffixRegExpPattern: '${ $.wysiwygUniqueSuffix }', @@ -53,6 +54,10 @@ define([ // disable editor completely after initialization is field is disabled varienGlobalEvents.attachEventHandler('wysiwygEditorInitialized', function () { + if (!_.isUndefined(window.tinyMceEditors)) { + this.currentWysiwyg = window.tinyMceEditors[this.wysiwygId]; + } + if (this.disabled()) { this.setDisabled(true); } @@ -136,14 +141,9 @@ define([ } /* eslint-disable no-undef */ - if (typeof wysiwyg !== 'undefined' && wysiwyg.activeEditor()) { - if (wysiwyg && disabled) { - wysiwyg.setEnabledStatus(false); - wysiwyg.getPluginButtons().prop('disabled', 'disabled'); - } else if (wysiwyg) { - wysiwyg.setEnabledStatus(true); - wysiwyg.getPluginButtons().removeProp('disabled'); - } + if (!_.isUndefined(this.currentWysiwyg) && this.currentWysiwyg.activeEditor()) { + this.currentWysiwyg.setEnabledStatus(!disabled); + this.currentWysiwyg.getPluginButtons().prop('disabled', disabled); } } }); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/search/search.js b/app/code/Magento/Ui/view/base/web/js/grid/search/search.js index 19536e7ff8c18..999e3262dbbdd 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/search/search.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/search/search.js @@ -18,7 +18,7 @@ define([ return Element.extend({ defaults: { template: 'ui/grid/search/search', - placeholder: $t('Search by keyword'), + placeholder: 'Search by keyword', label: $t('Keyword'), value: '', previews: [], diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html index 3ef64fd4b5371..36a3232c3e61a 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html @@ -6,7 +6,7 @@ --> <div class="admin__action-dropdown-wrap admin__data-grid-action-bookmarks" collapsible> <button class="admin__action-dropdown" type="button" toggleCollapsible> - <span class="admin__action-dropdown-text" text="activeView.label"/> + <span class="admin__action-dropdown-text" translate="activeView.label"/> </button> <ul class="admin__action-dropdown-menu"> <repeat args="foreach: viewsArray, item: '$view'"> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html index b52669e2cd28d..521ce9fc806ac 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html @@ -30,7 +30,7 @@ </div> <div class="action-dropdown-menu-item"> - <a href="" class="action-dropdown-menu-link" text="$view().label" click="applyView.bind($data, $view().index)" closeCollapsible/> + <a href="" class="action-dropdown-menu-link" translate="$view().label" click="applyView.bind($data, $view().index)" closeCollapsible/> <div class="action-dropdown-menu-item-actions" if="$view().editable"> <button class="action-edit" type="button" attr="title: $t('Edit bookmark')" click="editView.bind($data, $view().index)"> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html b/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html index 39d996e05c3a6..13b82a93eca25 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html @@ -10,9 +10,10 @@ </label> <input class="admin__control-text data-grid-search-control" type="text" data-bind=" + i18n: placeholder, attr: { id: index, - placeholder: placeholder + placeholder: $t(placeholder) }, textInput: inputValue, keyboard: { diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html b/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html index c5d87a4b16c4e..610d78e00b81d 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html @@ -6,7 +6,7 @@ --> <ul class="action-submenu" each="data: action.actions, as: 'action'" css="_active: action.visible"> <li css="_visible: $data.visible"> - <span class="action-menu-item" text="label" click="$parent.applyAction.bind($parent, type)"/> + <span class="action-menu-item" translate="label" click="$parent.applyAction.bind($parent, type)"/> <render args="name: $parent.submenuTemplate, data: $parent" if="$data.actions"/> </li> </ul> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html b/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html index 1aeb48b7c7698..d11d4aa243737 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html @@ -11,7 +11,7 @@ <div class="action-menu-items"> <ul class="action-menu" each="data: actions, as: 'action'" css="_active: opened"> <li css="_visible: $data.visible, _parent: $data.actions"> - <span class="action-menu-item" text="label" click="$parent.applyAction.bind($parent, type)"/> + <span class="action-menu-item" translate="label" click="$parent.applyAction.bind($parent, type)"/> <render args="name: $parent.submenuTemplate, data: $parent" if="$data.actions"/> </li> </ul> diff --git a/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less index f0dd8a957e9b5..6e2069c6e88ef 100644 --- a/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less @@ -55,6 +55,10 @@ } } + .map-fallback-price { + display: none; + } + .map-old-price { text-decoration: none; diff --git a/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less index 112184b45fe86..475361c56afc8 100644 --- a/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less @@ -74,6 +74,10 @@ } } + .map-fallback-price { + display: none; + } + .map-old-price, .product-item .map-old-price, .product-info-price .map-show-info { diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php index c335b66505b0e..f3be684f93a4d 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php @@ -146,6 +146,10 @@ public function testGetList() public function testSave($optionData) { $productSku = 'simple'; + /** @var \Magento\Catalog\Model\ProductRepository $productRepository */ + $productRepository = $this->objectManager->create( + \Magento\Catalog\Model\ProductRepository::class + ); $optionDataPost = $optionData; $optionDataPost['product_sku'] = $productSku; @@ -162,6 +166,7 @@ public function testSave($optionData) ]; $result = $this->_webApiCall($serviceInfo, ['option' => $optionDataPost]); + $product = $productRepository->get($productSku); unset($result['product_sku']); unset($result['option_id']); if (!empty($result['values'])) { @@ -169,7 +174,12 @@ public function testSave($optionData) unset($result['values'][$key]['option_type_id']); } } + $this->assertEquals($optionData, $result); + $this->assertTrue($product->getHasOptions() == 1); + if ($optionDataPost['is_require']) { + $this->assertTrue($product->getRequiredOptions() == 1); + } } public function optionDataProvider() diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductSwatchAttributeOptionManagementInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductSwatchAttributeOptionManagementInterfaceTest.php index 6b8388e2f4345..237574dd6e22a 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductSwatchAttributeOptionManagementInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductSwatchAttributeOptionManagementInterfaceTest.php @@ -45,6 +45,9 @@ public function testAdd($optionData) $this->assertNotNull($response); $updatedData = $this->getAttributeOptions($testAttributeCode); $lastOption = array_pop($updatedData); + foreach ($updatedData as $option) { + $this->assertNotContains('id', $option['value']); + } $this->assertEquals( $optionData[AttributeOptionInterface::STORE_LABELS][0][AttributeOptionLabelInterface::LABEL], $lastOption['label'] diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php index a001cae645434..a3ded4f5f125c 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php @@ -12,6 +12,8 @@ use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; class CartTotalRepositoryTest extends WebapiAbstract { @@ -54,36 +56,11 @@ public function testGetTotals() /** @var \Magento\Quote\Model\Quote\Address $shippingAddress */ $shippingAddress = $quote->getShippingAddress(); - $data = [ - Totals::KEY_GRAND_TOTAL => $quote->getGrandTotal(), - Totals::KEY_BASE_GRAND_TOTAL => $quote->getBaseGrandTotal(), - Totals::KEY_SUBTOTAL => $quote->getSubtotal(), - Totals::KEY_BASE_SUBTOTAL => $quote->getBaseSubtotal(), - Totals::KEY_DISCOUNT_AMOUNT => $shippingAddress->getDiscountAmount(), - Totals::KEY_BASE_DISCOUNT_AMOUNT => $shippingAddress->getBaseDiscountAmount(), - Totals::KEY_SUBTOTAL_WITH_DISCOUNT => $quote->getSubtotalWithDiscount(), - Totals::KEY_BASE_SUBTOTAL_WITH_DISCOUNT => $quote->getBaseSubtotalWithDiscount(), - Totals::KEY_SHIPPING_AMOUNT => $shippingAddress->getShippingAmount(), - Totals::KEY_BASE_SHIPPING_AMOUNT => $shippingAddress->getBaseShippingAmount(), - Totals::KEY_SHIPPING_DISCOUNT_AMOUNT => $shippingAddress->getShippingDiscountAmount(), - Totals::KEY_BASE_SHIPPING_DISCOUNT_AMOUNT => $shippingAddress->getBaseShippingDiscountAmount(), - Totals::KEY_TAX_AMOUNT => $shippingAddress->getTaxAmount(), - Totals::KEY_BASE_TAX_AMOUNT => $shippingAddress->getBaseTaxAmount(), - Totals::KEY_SHIPPING_TAX_AMOUNT => $shippingAddress->getShippingTaxAmount(), - Totals::KEY_BASE_SHIPPING_TAX_AMOUNT => $shippingAddress->getBaseShippingTaxAmount(), - Totals::KEY_SUBTOTAL_INCL_TAX => $shippingAddress->getSubtotalInclTax(), - Totals::KEY_BASE_SUBTOTAL_INCL_TAX => $shippingAddress->getBaseSubtotalTotalInclTax(), - Totals::KEY_SHIPPING_INCL_TAX => $shippingAddress->getShippingInclTax(), - Totals::KEY_BASE_SHIPPING_INCL_TAX => $shippingAddress->getBaseShippingInclTax(), - Totals::KEY_BASE_CURRENCY_CODE => $quote->getBaseCurrencyCode(), - Totals::KEY_QUOTE_CURRENCY_CODE => $quote->getQuoteCurrencyCode(), - Totals::KEY_ITEMS_QTY => $quote->getItemsQty(), - Totals::KEY_ITEMS => [$this->getQuoteItemTotalsData($quote)], - ]; + $data = $this->getData($quote, $shippingAddress); + $data = $this->formatTotalsData($data); $requestData = ['cartId' => $cartId]; - $data = $this->formatTotalsData($data); $actual = $this->_webApiCall($this->getServiceInfoForTotalsService($cartId), $requestData); unset($actual['items'][0]['options']); unset($actual['weee_tax_applied_amount']); @@ -213,7 +190,32 @@ public function testGetMyTotals() /** @var \Magento\Quote\Model\Quote\Address $shippingAddress */ $shippingAddress = $quote->getShippingAddress(); - $data = [ + $data = $this->getData($quote, $shippingAddress); + $data = $this->formatTotalsData($data); + + $actual = $this->_webApiCall($serviceInfo); + unset($actual['items'][0]['options']); + unset($actual['weee_tax_applied_amount']); + + /** TODO: cover total segments with separate test */ + unset($actual['total_segments']); + if (array_key_exists('extension_attributes', $actual)) { + unset($actual['extension_attributes']); + } + $this->assertEquals($data, $actual); + } + + /** + * Get expected data. + * + * @param Quote $quote + * @param Address $shippingAddress + * + * @return array + */ + private function getData(Quote $quote, Address $shippingAddress) : array + { + return [ Totals::KEY_GRAND_TOTAL => $quote->getGrandTotal(), Totals::KEY_BASE_GRAND_TOTAL => $quote->getBaseGrandTotal(), Totals::KEY_SUBTOTAL => $quote->getSubtotal(), @@ -239,17 +241,5 @@ public function testGetMyTotals() Totals::KEY_ITEMS_QTY => $quote->getItemsQty(), Totals::KEY_ITEMS => [$this->getQuoteItemTotalsData($quote)], ]; - - $data = $this->formatTotalsData($data); - $actual = $this->_webApiCall($serviceInfo); - unset($actual['items'][0]['options']); - unset($actual['weee_tax_applied_amount']); - - /** TODO: cover total segments with separate test */ - unset($actual['total_segments']); - if (array_key_exists('extension_attributes', $actual)) { - unset($actual['extension_attributes']); - } - $this->assertEquals($data, $actual); } } diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php index 7ad0e62f29dc3..28195cca679f8 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php @@ -63,14 +63,14 @@ public function testGetTotals() $shippingAddress = $quote->getShippingAddress(); $data = [ - Totals::KEY_BASE_GRAND_TOTAL => $quote->getBaseGrandTotal(), Totals::KEY_GRAND_TOTAL => $quote->getGrandTotal(), - Totals::KEY_BASE_SUBTOTAL => $quote->getBaseSubtotal(), + Totals::KEY_BASE_GRAND_TOTAL => $quote->getBaseGrandTotal(), Totals::KEY_SUBTOTAL => $quote->getSubtotal(), - Totals::KEY_BASE_SUBTOTAL_WITH_DISCOUNT => $quote->getBaseSubtotalWithDiscount(), - Totals::KEY_SUBTOTAL_WITH_DISCOUNT => $quote->getSubtotalWithDiscount(), + Totals::KEY_BASE_SUBTOTAL => $quote->getBaseSubtotal(), Totals::KEY_DISCOUNT_AMOUNT => $shippingAddress->getDiscountAmount(), Totals::KEY_BASE_DISCOUNT_AMOUNT => $shippingAddress->getBaseDiscountAmount(), + Totals::KEY_SUBTOTAL_WITH_DISCOUNT => $quote->getSubtotalWithDiscount(), + Totals::KEY_BASE_SUBTOTAL_WITH_DISCOUNT => $quote->getBaseSubtotalWithDiscount(), Totals::KEY_SHIPPING_AMOUNT => $shippingAddress->getShippingAmount(), Totals::KEY_BASE_SHIPPING_AMOUNT => $shippingAddress->getBaseShippingAmount(), Totals::KEY_SHIPPING_DISCOUNT_AMOUNT => $shippingAddress->getShippingDiscountAmount(), @@ -94,6 +94,7 @@ public function testGetTotals() $data = $this->formatTotalsData($data); $actual = $this->_webApiCall($this->getServiceInfoForTotalsService($cartId), $requestData); + $actual = $this->formatTotalsData($actual); unset($actual['items'][0]['options']); unset($actual['weee_tax_applied_amount']); diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/Cart/Item.php b/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/Cart/Item.php index 9b12c467e5775..4d6d06ac6e625 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/Cart/Item.php +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/Cart/Item.php @@ -46,7 +46,7 @@ public function getData($key = null) $optionData = [ 'title' => $checkoutOption['title'], 'value' => "{$qty} x {$value} {$price}", - 'sku' => "{$qty} x {$value}" + 'sku' => "{$value}" ]; $checkoutBundleOptions[$checkoutOptionKey] = $optionData; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/text_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute_rollback.php similarity index 84% rename from dev/tests/integration/testsuite/Magento/Catalog/_files/text_attribute_rollback.php rename to dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute_rollback.php index cbc0476efd1b5..a9ab0e11312b2 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/text_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute_rollback.php @@ -3,13 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + /* Delete attribute with text_attribute code */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ $attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class ); $attribute->load('text_attribute', 'attribute_code'); $attribute->delete(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php index 168073bc6ab74..c57c7c3fd6a92 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php @@ -5,10 +5,10 @@ */ /** Delete all products */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; /** Delete text attribute */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/text_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/product_text_attribute_rollback.php'; -require dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; +include dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; -require dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php index 168073bc6ab74..c57c7c3fd6a92 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php @@ -5,10 +5,10 @@ */ /** Delete all products */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; /** Delete text attribute */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/text_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/product_text_attribute_rollback.php'; -require dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; +include dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; -require dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_shipping_method.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_shipping_method.php index 3c54fe16db7d3..61779da29c65f 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_shipping_method.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_shipping_method.php @@ -11,10 +11,18 @@ require 'quote_with_address_saved.php'; +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$rate = $objectManager->get(\Magento\Quote\Model\Quote\Address\Rate::class); + $quote->load('test_order_1', 'reserved_order_id'); $shippingAddress = $quote->getShippingAddress(); $shippingAddress->setShippingMethod('flatrate_flatrate') ->setShippingDescription('Flat Rate - Fixed') - ->setShippingAmount(10.0) - ->setBaseShippingAmount(12.0) ->save(); + +$rate->setPrice(0) + ->setAddressId($shippingAddress->getId()) + ->save(); +$shippingAddress->setBaseShippingAmount($rate->getPrice()); +$shippingAddress->setShippingAmount($rate->getPrice()); +$rate->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php index dc288a18fadb7..8d80fd8533d6f 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php @@ -369,4 +369,18 @@ public function dateDataProvider() [['from' => '2000-02-01T00:00:00Z', 'to' => ''], 0], ]; } + + public function filterByAttributeValuesDataProvider() + { + $variations = parent::filterByAttributeValuesDataProvider(); + + $variations['quick search by date'] = [ + 'quick_search_container', + [ + 'search_term' => '2000-10-30', + ], + ]; + + return $variations; + } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php index b09af48b5f943..f4f3337a253c0 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php @@ -20,6 +20,10 @@ CategorySetup::class, ['resourceName' => 'catalog_setup'] ); +$productEntityTypeId = $installer->getEntityTypeId( + \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE +); + $selectOptions = []; $selectAttributes = []; foreach (range(1, 2) as $index) { @@ -30,7 +34,7 @@ $selectAttribute->setData( [ 'attribute_code' => 'select_attribute_' . $index, - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'entity_type_id' => $productEntityTypeId, 'is_global' => 1, 'is_user_defined' => 1, 'frontend_input' => 'select', @@ -56,7 +60,8 @@ ); $selectAttribute->save(); /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $selectAttribute->getId()); + $installer->addAttributeToGroup($productEntityTypeId, 'Default', 'General', $selectAttribute->getId()); + /** @var $selectOptions Collection */ $selectOption = Bootstrap::getObjectManager()->create( Collection::class @@ -65,6 +70,26 @@ $selectAttributes[$index] = $selectAttribute; $selectOptions[$index] = $selectOption; } + +$dateAttribute = Bootstrap::getObjectManager()->create(Attribute::class); +$dateAttribute->setData( + [ + 'attribute_code' => 'date_attribute', + 'entity_type_id' => $productEntityTypeId, + 'is_global' => 1, + 'is_filterable' => 1, + 'backend_type' => 'datetime', + 'frontend_input' => 'date', + 'frontend_label' => 'Test Date', + 'is_searchable' => 1, + 'is_filterable_in_search' => 1, + ] +); +$dateAttribute->save(); +/* Assign attribute to attribute set */ +$installer->addAttributeToGroup($productEntityTypeId, 'Default', 'General', $dateAttribute->getId()); + +$productAttributeSetId = $installer->getAttributeSetId($productEntityTypeId, 'Default'); /* Create simple products per each first attribute option */ foreach ($selectOptions[1] as $option) { /** @var $product Product */ @@ -74,7 +99,7 @@ $product->setTypeId( Type::TYPE_SIMPLE )->setAttributeSetId( - $installer->getAttributeSetId('catalog_product', 'Default') + $productAttributeSetId )->setWebsiteIds( [1] )->setName( @@ -92,6 +117,7 @@ )->setStockData( ['use_config_manage_stock' => 1, 'qty' => 5, 'is_in_stock' => 1] )->save(); + Bootstrap::getObjectManager()->get( Action::class )->updateAttributes( @@ -99,6 +125,7 @@ [ $selectAttributes[1]->getAttributeCode() => $option->getId(), $selectAttributes[2]->getAttributeCode() => $selectOptions[2]->getLastItem()->getId(), + $dateAttribute->getAttributeCode() => '10/30/2000', ], $product->getStoreId() ); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php index 18a5372d06d98..fd413726b2637 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php @@ -13,6 +13,7 @@ $registry = Bootstrap::getObjectManager()->get(Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); + /** @var $productCollection \Magento\Catalog\Model\ResourceModel\Product\Collection */ $productCollection = Bootstrap::getObjectManager() ->create(Product::class) @@ -20,17 +21,26 @@ foreach ($productCollection as $product) { $product->delete(); } + /** @var $attribute Attribute */ $attribute = Bootstrap::getObjectManager()->create( Attribute::class ); /** @var $installer CategorySetup */ $installer = Bootstrap::getObjectManager()->create(CategorySetup::class); +$productEntityTypeId = $installer->getEntityTypeId( + \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE +); foreach (range(1, 2) as $index) { - $attribute->loadByCode($installer->getEntityTypeId('catalog_product'), 'select_attribute_' . $index); + $attribute->loadByCode($productEntityTypeId, 'select_attribute_' . $index); if ($attribute->getId()) { $attribute->delete(); } } +$attribute->loadByCode($productEntityTypeId, 'date_attribute'); +if ($attribute->getId()) { + $attribute->delete(); +} + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php index 16a15cfcd2e26..384892d6fd5d2 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php @@ -116,7 +116,6 @@ public function testDispatch() : void */ public function testError() : void { - $this->markTestSkipped('Causes failiure with php unit and php 7.2'); $query = <<<QUERY { diff --git a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php index b9ba89ba53144..2d0020ba22680 100644 --- a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php @@ -48,7 +48,22 @@ public static function loadByPhraseDataProvider() ['synonyms' => 'queen,monarch', 'store_id' => 1, 'website_id' => 0], ['synonyms' => 'british,english', 'store_id' => 1, 'website_id' => 0] ] - ] + ], + [ + 'query_value', [] + ], + [ + 'query_value+', [] + ], + [ + 'query_value-', [] + ], + [ + 'query_@value', [] + ], + [ + 'query_value+@', [] + ], ]; } diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disabling_tables/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disabling_tables/db_schema.xml new file mode 100644 index 0000000000000..5ea5816b1df8e --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disabling_tables/db_schema.xml @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="reference_table" resource="default"> + <column xsi:type="tinyint" name="tinyint_ref" padding="7" nullable="false" identity="true" unsigned="false"/> + <column xsi:type="tinyint" name="tinyint_without_padding" default="0" nullable="false" unsigned="false"/> + <column xsi:type="bigint" name="bigint_without_padding" default="0" nullable="false" unsigned="false"/> + <column xsi:type="smallint" name="smallint_without_padding" default="0" nullable="false" unsigned="false"/> + <column xsi:type="int" name="integer_without_padding" default="0" nullable="false" unsigned="false"/> + <column xsi:type="smallint" name="smallint_with_big_padding" padding="254" default="0" nullable="false" + unsigned="false"/> + <constraint xsi:type="primary" referenceId="tinyint_primary"> + <column name="tinyint_ref"/> + </constraint> + <index referenceId="COMPLEX_INDEX" indexType="btree"> + <column name="tinyint_without_padding"/> + <column name="bigint_without_padding"/> + </index> + </table> + <table name="auto_increment_test" resource="default"> + <column xsi:type="int" name="int_auto_increment_with_nullable" identity="true" padding="12" unsigned="true" + nullable="true"/> + <column xsi:type="smallint" name="int_disabled_auto_increment" default="0" identity="false" padding="12" + unsigned="true" nullable="true"/> + <constraint xsi:type="unique" referenceId="AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE"> + <column name="int_auto_increment_with_nullable"/> + </constraint> + </table> +</schema> diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/InstallSchema.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/InstallSchema.php new file mode 100644 index 0000000000000..c1eaff264df0c --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/InstallSchema.php @@ -0,0 +1,334 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestSetupDeclarationModule8\Setup; + +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\Setup\InstallSchemaInterface; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\SchemaSetupInterface; + +/** + * Install schema script for the TestSetupDeclarationModule8 module. + */ +class InstallSchema implements InstallSchemaInterface +{ + /** + * The name of the main table of Module8. + */ + const MAIN_TABLE = 'module8_test_main_table'; + + /** + * The name of the second table of Module8. + */ + const SECOND_TABLE = 'module8_test_second_table'; + + /** + * The name of the second table of Module8. + */ + const TEMP_TABLE = 'module8_test_install_temp_table'; + + /** + * @inheritdoc + * @throws \Zend_Db_Exception + */ + public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) + { + $setup->startSetup(); + + $this->createTables($setup); + + $setup->endSetup(); + } + + /** + * Create tables. + * + * @param SchemaSetupInterface $installer + * @throws \Zend_Db_Exception + */ + private function createTables(SchemaSetupInterface $installer) + { + $mainTableName = $installer->getTable(self::MAIN_TABLE); + $this->dropTableIfExists($installer, $mainTableName); + $mainTable = $installer->getConnection()->newTable($mainTableName); + $mainTable->setComment('Main Test Table for Module8'); + $this->addColumnsToMainTable($mainTable); + $this->addIndexesToMainTable($mainTable); + $installer->getConnection()->createTable($mainTable); + + $secondTableName = $installer->getTable(self::SECOND_TABLE); + $this->dropTableIfExists($installer, $secondTableName); + $secondTable = $installer->getConnection()->newTable($secondTableName); + $secondTable->setComment('Second Test Table for Module8'); + $this->addColumnsToSecondTable($secondTable); + $this->addIndexesToSecondTable($secondTable); + $this->addConstraintsToSecondTable($secondTable); + $installer->getConnection()->createTable($secondTable); + + $this->createSimpleTable($installer, self::TEMP_TABLE); + } + + /** + * Drop existing tables. + * + * @param SchemaSetupInterface $installer + * @param string $table + */ + private function dropTableIfExists($installer, $table) + { + $connection = $installer->getConnection(); + if ($connection->isTableExists($installer->getTable($table))) { + $connection->dropTable( + $installer->getTable($table) + ); + } + } + + /** + * Add tables to main table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addColumnsToMainTable($table) + { + $table + ->addColumn( + 'module8_email_contact_id', + Table::TYPE_INTEGER, + 10, + [ + 'primary' => true, + 'identity' => true, + 'unsigned' => true, + 'nullable' => false + ], + 'Email Contact ID' + )->addColumn( + 'module8_contact_group_id', + Table::TYPE_INTEGER, + 10, + [ + 'unsigned' => true, + 'nullable' => false + ], + 'Contact Group ID' + )->addColumn( + 'module8_is_guest', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Is Guest' + )->addColumn( + 'module8_contact_id', + Table::TYPE_TEXT, + 15, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Contact ID' + )->addColumn( + 'module8_content', + Table::TYPE_TEXT, + 15, + [ + 'nullable' => false, + ], + 'Content' + ); + } + + /** + * Add indexes to main table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addIndexesToMainTable($table) + { + $table + ->addIndex( + 'MODULE8_INSTALL_INDEX_1', + ['module8_email_contact_id'] + )->addIndex( + 'MODULE8_INSTALL_UNIQUE_INDEX_2', + ['module8_email_contact_id', 'module8_is_guest'], + ['type' => AdapterInterface::INDEX_TYPE_UNIQUE] + )->addIndex( + 'MODULE8_INSTALL_INDEX_3', + ['module8_is_guest'] + )->addIndex( + 'MODULE8_INSTALL_INDEX_4', + ['module8_contact_id'] + )->addIndex( + 'MODULE8_INSTALL_INDEX_TEMP', + ['module8_content'] + )->addIndex( + 'MODULE8_INSTALL_UNIQUE_INDEX_TEMP', + ['module8_contact_group_id'], + ['type' => AdapterInterface::INDEX_TYPE_UNIQUE] + ); + } + + /** + * Add tables to second table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addColumnsToSecondTable($table) + { + $table + ->addColumn( + 'module8_entity_id', + Table::TYPE_INTEGER, + 10, + [ + 'primary' => true, + 'identity' => true, + 'unsigned' => true, + 'nullable' => false + ], + 'Entity ID' + )->addColumn( + 'module8_contact_id', + Table::TYPE_INTEGER, + null, + [], + 'Contact ID' + )->addColumn( + 'module8_address', + Table::TYPE_TEXT, + 15, + [ + 'nullable' => false, + ], + 'Address' + )->addColumn( + 'module8_counter_with_multiline_comment', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true, + 'default' => 0 + ], + 'Empty + Counter + Multiline + Comment' + )->addColumn( + 'module8_second_address', + Table::TYPE_TEXT, + 15, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Second Address' + )->addColumn( + 'module8_temp_column', + Table::TYPE_TEXT, + 15, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Temp column for remove' + ); + } + + /** + * Add indexes to second table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addIndexesToSecondTable($table) + { + $table + ->addIndex( + 'MODULE8_INSTALL_SECOND_TABLE_INDEX_1', + ['module8_entity_id'] + )->addIndex( + 'MODULE8_INSTALL_SECOND_TABLE_INDEX_2', + ['module8_address'] + )->addIndex( + 'MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP', + ['module8_second_address'] + ); + } + + /** + * Add constraints to second table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addConstraintsToSecondTable($table) + { + $table + ->addForeignKey( + 'MODULE8_INSTALL_FK_ENTITY_ID_TEST_MAIN_TABLE_EMAIL_CONTACT_ID', + 'module8_entity_id', + self::MAIN_TABLE, + 'module8_email_contact_id' + )->addForeignKey( + 'MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID', + 'module8_address', + self::MAIN_TABLE, + 'module8_contact_id' + )->addForeignKey( + 'MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_MODULE8_CONTENT_TEMP', + 'module8_address', + self::MAIN_TABLE, + 'module8_content' + ); + } + + /** + * Create a simple table. + * + * @param SchemaSetupInterface $setup + * @param $tableName + * @throws \Zend_Db_Exception + */ + private function createSimpleTable(SchemaSetupInterface $setup, $tableName): void + { + $table = $setup->getConnection()->newTable($tableName); + $table + ->addColumn( + 'module8_entity_id', + Table::TYPE_INTEGER, + null, + [ + 'primary' => true, + 'identity' => true, + 'nullable' => false, + 'unsigned' => true, + ], + 'Entity ID' + )->addColumn( + 'module8_counter', + Table::TYPE_INTEGER, + null, + [ + 'unsigned' => true, + 'nullable' => true, + 'default' => 100 + ], + 'Counter' + ); + $setup->getConnection()->createTable($table); + } +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/UpgradeSchema.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/UpgradeSchema.php new file mode 100644 index 0000000000000..2dc8667a75dc1 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/UpgradeSchema.php @@ -0,0 +1,242 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\TestSetupDeclarationModule8\Setup; + +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\SchemaSetupInterface; +use Magento\Framework\Setup\UpgradeSchemaInterface; + +/** + * Upgrade schema script for the TestSetupDeclarationModule8 module. + */ +class UpgradeSchema implements UpgradeSchemaInterface +{ + /** + * The name of the main table of the Module8. + */ + const UPDATE_TABLE = 'module8_test_update_table'; + + /** + * The name of the temporary table of the Module8. + */ + const TEMP_TABLE = 'module8_test_temp_table'; + + /** + * @inheritdoc + * @throws \Zend_Db_Exception + */ + public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $context) + { + $setup->startSetup(); + + if (version_compare($context->getVersion(), '1.0.1', '<')) { + $tableName = $setup->getTable(self::UPDATE_TABLE); + $table = $setup->getConnection()->newTable($tableName); + $table->setComment('Update Test Table for Module8'); + + $this->addColumns($setup, $table); + $this->addIndexes($table); + $this->addConstraints($table); + $setup->getConnection()->createTable($table); + + $this->createSimpleTable($setup, $setup->getTable(self::TEMP_TABLE)); + } + + if (version_compare($context->getVersion(), '1.0.2', '<')) { + $connection = $setup->getConnection(); + $connection + ->dropTable( + InstallSchema::TEMP_TABLE + ); + $connection + ->dropColumn( + InstallSchema::SECOND_TABLE, + 'module8_temp_column' + ); + $connection + ->dropForeignKey( + InstallSchema::SECOND_TABLE, + 'MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_MODULE8_CONTENT_TEMP' + ); + $connection + ->dropIndex( + InstallSchema::MAIN_TABLE, + 'MODULE8_INSTALL_INDEX_TEMP' + ); + $connection + ->dropIndex( + InstallSchema::MAIN_TABLE, + 'MODULE8_INSTALL_UNIQUE_INDEX_TEMP' + ); + } + + $setup->endSetup(); + } + + /** + * Create columns for tables. + * + * @param SchemaSetupInterface $setup + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addColumns(SchemaSetupInterface $setup, Table $table): void + { + $table + ->addColumn( + 'module8_entity_id', + Table::TYPE_INTEGER, + 10, + [ + 'primary' => true, + 'unsigned' => true, + 'nullable' => false + ], + 'Entity ID' + )->addColumn( + 'module8_entity_row_id', + Table::TYPE_INTEGER, + null, + [ + 'unsigned' => true, + 'nullable' => false + ] + )->addColumn( + 'module8_is_guest', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Is Guest' + )->addColumn( + 'module8_guest_browser_id', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Guest Browser ID' + )->addColumn( + 'module8_column_for_remove', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'For remove' + ); + + $setup->getConnection()->addColumn( + InstallSchema::MAIN_TABLE, + 'module8_update_column', + [ + 'type' => Table::TYPE_INTEGER, + 'nullable' => false, + 'comment' => 'Module_8 Update Column', + ] + ); + } + + /** + * Add indexes. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addIndexes(Table $table): void + { + $table + ->addIndex( + 'MODULE8_UPDATE_IS_GUEST_INDEX', + [ + 'module8_is_guest' + ] + )->addIndex( + 'MODULE8_UPDATE_UNIQUE_INDEX_TEMP', + [ + 'module8_entity_id', + 'module8_is_guest', + + ], + ['type' => AdapterInterface::INDEX_TYPE_UNIQUE] + )->addIndex( + 'MODULE8_UPDATE_TEMP_INDEX', + [ + 'module8_column_for_remove', + 'module8_guest_browser_id' + ] + ); + } + + /** + * Add constraints. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addConstraints(Table $table): void + { + $table + ->addForeignKey( + 'MODULE8_UPDATE_FK_MODULE8_IS_GUEST', + 'module8_is_guest', + InstallSchema::MAIN_TABLE, + 'module8_is_guest', + Table::ACTION_CASCADE + )->addForeignKey( + 'MODULE8_UPDATE_FK_TEMP', + 'module8_column_for_remove', + InstallSchema::MAIN_TABLE, + 'module8_is_guest', + Table::ACTION_CASCADE + ); + } + + /** + * Create a simple table. + * + * @param SchemaSetupInterface $setup + * @param $tableName + * @throws \Zend_Db_Exception + */ + private function createSimpleTable(SchemaSetupInterface $setup, $tableName): void + { + $table = $setup->getConnection()->newTable($tableName); + $table + ->addColumn( + 'module8_entity_id', + Table::TYPE_INTEGER, + null, + [ + 'primary' => true, + 'identity' => true, + 'nullable' => false, + 'unsigned' => true, + ], + 'Entity ID' + )->addColumn( + 'module8_counter', + Table::TYPE_INTEGER, + null, + [ + 'unsigned' => true, + 'nullable' => true, + 'default' => 100 + ], + 'Counter' + ); + $setup->getConnection()->createTable($table); + } +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/module.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/module.xml new file mode 100644 index 0000000000000..b6a57fcb47639 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/module.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_TestSetupDeclarationModule8" setup_version="1.0.2"/> +</config> diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/etc/module.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/etc/module.xml new file mode 100644 index 0000000000000..e472f951b08e9 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/etc/module.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_TestSetupDeclarationModule9" setup_version="1.0.0" /> +</config> diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.php new file mode 100644 index 0000000000000..633185390ae84 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'auto_increment_test' => 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(12) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(12) unsigned DEFAULT \'0\', + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/registration.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/registration.php new file mode 100644 index 0000000000000..f021af556bb73 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/registration.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestSetupDeclarationModule9') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestSetupDeclarationModule9', __DIR__); +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/disabling_tables/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/disabling_tables/db_schema.xml new file mode 100644 index 0000000000000..c22c41b7a5c03 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/disabling_tables/db_schema.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="reference_table" disabled="true"/> +</schema> diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/InstallSchema.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/InstallSchema.php new file mode 100644 index 0000000000000..d78a8e25230b4 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/InstallSchema.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestSetupDeclarationModule9\Setup; + +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\Setup\InstallSchemaInterface; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\SchemaSetupInterface; + +/** + * Install schema script for the TestSetupDeclarationModule9 module. + */ +class InstallSchema implements InstallSchemaInterface +{ + /** + * The name of the main table of Module9. + */ + const MAIN_TABLE = 'module9_test_main_table'; + + /** + * @inheritdoc + * @throws \Zend_Db_Exception + */ + public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) + { + $setup->startSetup(); + + $this->createTables($setup); + + $setup->endSetup(); + } + + /** + * Create tables. + * + * @param SchemaSetupInterface $installer + * @throws \Zend_Db_Exception + */ + private function createTables(SchemaSetupInterface $installer) + { + $mainTableName = $installer->getTable(self::MAIN_TABLE); + $this->dropTableIfExists($installer, $mainTableName); + $mainTable = $installer->getConnection()->newTable($mainTableName); + $mainTable->setComment('Main Test Table for Module9'); + $this->addColumnsToMainTable($mainTable); + $this->addIndexesToMainTable($mainTable); + $installer->getConnection()->createTable($mainTable); + } + + /** + * Drop existing tables. + * + * @param SchemaSetupInterface $installer + * @param string $table + */ + private function dropTableIfExists($installer, $table) + { + $connection = $installer->getConnection(); + if ($connection->isTableExists($installer->getTable($table))) { + $connection->dropTable( + $installer->getTable($table) + ); + } + } + + /** + * Add tables to main table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addColumnsToMainTable($table) + { + $table + ->addColumn( + 'module9_email_contact_id', + Table::TYPE_INTEGER, + 10, + [ + 'primary' => true, + 'identity' => true, + 'unsigned' => true, + 'nullable' => false + ], + 'Entity ID' + )->addColumn( + 'module9_is_guest', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Is Guest' + )->addColumn( + 'module9_guest_id', + Table::TYPE_INTEGER, + null, + [ + 'unsigned' => true + ], + 'Guest ID' + ) + ->addColumn( + 'module9_created_at', + Table::TYPE_DATE, + null, + [], + 'Created At' + ); + } + + /** + * Add indexes to main table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addIndexesToMainTable($table) + { + $table + ->addIndex( + 'MODULE9_INSTALL_UNIQUE_INDEX_1', + ['module9_email_contact_id', 'module9_guest_id'], + ['type' => AdapterInterface::INDEX_TYPE_UNIQUE] + ); + } +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/UpgradeSchema.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/UpgradeSchema.php new file mode 100644 index 0000000000000..0a49349699963 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/UpgradeSchema.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\TestSetupDeclarationModule9\Setup; + +use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\SchemaSetupInterface; +use Magento\Framework\Setup\UpgradeSchemaInterface; +use Magento\TestSetupDeclarationModule8\Setup\InstallSchema as Module8InstallSchema; +use Magento\TestSetupDeclarationModule8\Setup\UpgradeSchema as Module8UpgradeSchema; + +/** + * Upgrade schema script for the TestSetupDeclarationModule9 module. + */ +class UpgradeSchema implements UpgradeSchemaInterface +{ + /** + * The name of the main table of Module9. + */ + const REPLICA_TABLE = 'module9_test_update_replica_table'; + + /** + * @inheritdoc + * @throws \Zend_Db_Exception + */ + public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $context) + { + $setup->startSetup(); + + if (version_compare($context->getVersion(), '2.0.0', '<')) { + $this->addColumns($setup); + $this->addIndexes($setup); + $this->addConstraints($setup); + $this->removeColumns($setup); + $this->removeIndexes($setup); + $this->removeConstraints($setup); + $this->removeTables($setup); + $replicaTable = $setup->getConnection() + ->createTableByDdl(Module8InstallSchema::SECOND_TABLE, self::REPLICA_TABLE); + $setup->getConnection()->createTable($replicaTable); + } + + $setup->endSetup(); + } + + /** + * Create columns for tables. + * + * @param SchemaSetupInterface $setup + */ + private function addColumns(SchemaSetupInterface $setup): void + { + $setup->getConnection()->addColumn( + InstallSchema::MAIN_TABLE, + 'module9_update_column', + [ + 'type' => Table::TYPE_INTEGER, + 'nullable' => false, + 'comment' => 'Module_9 Update Column', + ] + ); + + $setup->getConnection()->addColumn( + Module8InstallSchema::MAIN_TABLE, + 'module9_update_column', + [ + 'type' => Table::TYPE_INTEGER, + 'nullable' => false, + 'comment' => 'Module_9 Update Column', + ] + ); + } + + /** + * Add indexes. + * + * @param SchemaSetupInterface $setup + */ + private function addIndexes(SchemaSetupInterface $setup): void + { + $setup->getConnection() + ->addIndex( + Module8UpgradeSchema::UPDATE_TABLE, + 'MODULE9_UPDATE_MODULE8_GUEST_BROWSER_ID', + [ + 'module8_guest_browser_id' + ] + ); + } + + /** + * Add constraints. + * + * @param SchemaSetupInterface $setup + */ + private function addConstraints(SchemaSetupInterface $setup): void + { + $setup->getConnection() + ->addForeignKey( + 'MODULE9_UPDATE_FK_MODULE9_IS_GUEST', + InstallSchema::MAIN_TABLE, + 'module9_is_guest', + Module8InstallSchema::MAIN_TABLE, + 'module8_is_guest', + Table::ACTION_CASCADE + ); + } + + /** + * Remove columns. + * + * @param SchemaSetupInterface $setup + */ + private function removeColumns(SchemaSetupInterface $setup): void + { + $setup->getConnection() + ->dropColumn( + Module8UpgradeSchema::UPDATE_TABLE, + 'module8_column_for_remove' + ); + } + + /** + * Remove indexes. + * + * @param SchemaSetupInterface $setup + */ + private function removeIndexes(SchemaSetupInterface $setup): void + { + $connection = $setup->getConnection(); + $connection + ->dropIndex( + Module8InstallSchema::SECOND_TABLE, + 'MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP' + ); + } + + /** + * Remove constraints. + * + * @param SchemaSetupInterface $setup + */ + private function removeConstraints(SchemaSetupInterface $setup): void + { + $setup->getConnection() + ->dropForeignKey( + Module8InstallSchema::SECOND_TABLE, + 'MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID' + )->dropIndex( + Module8UpgradeSchema::UPDATE_TABLE, + 'MODULE8_UPDATE_UNIQUE_INDEX_TEMP' + ); + } + + /** + * Remove tables. + * + * @param SchemaSetupInterface $setup + */ + private function removeTables(SchemaSetupInterface $setup): void + { + $setup->getConnection() + ->dropTable( + Module8UpgradeSchema::TEMP_TABLE + ); + } +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/module.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/module.xml new file mode 100644 index 0000000000000..a553b82d78148 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/module.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_TestSetupDeclarationModule9" setup_version="2.0.0"> + <sequence> + <module name="Magento_TestSetupDeclarationModule8"/> + </sequence> + </module> +</config> diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupInstallTest.php b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupInstallTest.php new file mode 100644 index 0000000000000..cf137233ead0f --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupInstallTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Developer\Console\Command; + +use Magento\Framework\Component\ComponentRegistrar; +use Magento\TestFramework\Deploy\CliCommand; +use Magento\TestFramework\Deploy\TestModuleManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\SetupTestCase; + +/** + * Test for Install command. + */ +class SetupInstallTest extends SetupTestCase +{ + /** + * @var TestModuleManager + */ + private $moduleManager; + + /** + * @var CliCommand + */ + private $cliCommand; + + /** + * @var ComponentRegistrar + */ + private $componentRegistrar; + + /** + * @inheritdoc + */ + public function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->cliCommand = $objectManager->get(CliCommand::class); + $this->moduleManager = $objectManager->get(TestModuleManager::class); + $this->componentRegistrar = $objectManager->create( + ComponentRegistrar::class + ); + } + + /** + * @moduleName Magento_TestSetupDeclarationModule8 + * @moduleName Magento_TestSetupDeclarationModule9 + * @throws \Exception + */ + public function testInstallWithConverting() + { + $modules = [ + 'Magento_TestSetupDeclarationModule8', + 'Magento_TestSetupDeclarationModule9', + ]; + + foreach ($modules as $moduleName) { + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'InstallSchema.php', + 'Setup' + ); + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'UpgradeSchema.php', + 'Setup' + ); + + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'module.xml', + 'etc' + ); + } + + $this->cliCommand->install($modules, ['convert-old-scripts' => true]); + + foreach ($modules as $moduleName) { + $modulePath = $this->componentRegistrar->getPath('module', $moduleName); + $schemaFileName = $modulePath + . DIRECTORY_SEPARATOR + . \Magento\Framework\Module\Dir::MODULE_ETC_DIR + . DIRECTORY_SEPARATOR + . 'db_schema.xml'; + $generatedSchema = $this->getSchemaDocument($schemaFileName); + + $expectedSchemaFileName = dirname(__DIR__, 2) + . DIRECTORY_SEPARATOR + . implode( + DIRECTORY_SEPARATOR, + [ + '_files', + 'SetupInstall', + str_replace('Magento_', '', $moduleName), + 'db_schema.xml' + ] + ); + $expectedSchema = $this->getSchemaDocument($expectedSchemaFileName); + + $this->assertEquals($expectedSchema->saveXML(), $generatedSchema->saveXML()); + } + } + + /** + * Convert file content in the DOM document. + * + * @param $schemaFileName + * @return \DOMDocument + */ + private function getSchemaDocument($schemaFileName): \DOMDocument + { + $schemaDocument = new \DOMDocument(); + $schemaDocument->preserveWhiteSpace = false; + $schemaDocument->formatOutput = true; + $schemaDocument->loadXML(file_get_contents($schemaFileName)); + + return $schemaDocument; + } +} diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupUpgradeTest.php b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupUpgradeTest.php new file mode 100644 index 0000000000000..932662b58f3ac --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupUpgradeTest.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Developer\Console\Command; + +use Magento\Framework\Component\ComponentRegistrar; +use Magento\TestFramework\Deploy\CliCommand; +use Magento\TestFramework\Deploy\TestModuleManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\SetupTestCase; + +/** + * Test for Upgrade command. + */ +class SetupUpgradeTest extends SetupTestCase +{ + /** + * @var TestModuleManager + */ + private $moduleManager; + + /** + * @var CliCommand + */ + private $cliCommand; + + /** + * @var ComponentRegistrar + */ + private $componentRegistrar; + + /** + * @inheritdoc + */ + public function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->cliCommand = $objectManager->get(CliCommand::class); + $this->moduleManager = $objectManager->get(TestModuleManager::class); + $this->componentRegistrar = $objectManager->create( + ComponentRegistrar::class + ); + } + + /** + * @moduleName Magento_TestSetupDeclarationModule8 + * @moduleName Magento_TestSetupDeclarationModule9 + * @throws \Exception + */ + public function testUpgradeWithConverting() + { + $modules = [ + 'Magento_TestSetupDeclarationModule8', + 'Magento_TestSetupDeclarationModule9', + ]; + + foreach ($modules as $moduleName) { + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'InstallSchema.php', + 'Setup' + ); + } + + $this->cliCommand->install($modules, ['convert-old-scripts' => true]); + foreach ($modules as $moduleName) { + $this->assertInstallScriptChanges($moduleName); + } + + foreach ($modules as $moduleName) { + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'UpgradeSchema.php', + 'Setup' + ); + + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'module.xml', + 'etc' + ); + } + + $this->cliCommand->upgrade(['convert-old-scripts' => true]); + + foreach ($modules as $moduleName) { + $this->assertUpgradeScriptChanges($moduleName); + } + } + + /** + * Convert file content in the DOM document. + * + * @param string $schemaFileName + * @return \DOMDocument + */ + private function getSchemaDocument(string $schemaFileName): \DOMDocument + { + $schemaDocument = new \DOMDocument(); + $schemaDocument->preserveWhiteSpace = false; + $schemaDocument->formatOutput = true; + $schemaDocument->loadXML(file_get_contents($schemaFileName)); + + return $schemaDocument; + } + + /** + * @param string $moduleName + */ + private function assertInstallScriptChanges(string $moduleName): void + { + $generatedSchema = $this->getGeneratedSchema($moduleName); + $expectedSchema = $this->getSchemaDocument($this->getSchemaFixturePath($moduleName, 'install')); + + $this->assertEquals($expectedSchema->saveXML(), $generatedSchema->saveXML()); + } + + /** + * @param string $moduleName + */ + private function assertUpgradeScriptChanges(string $moduleName): void + { + $generatedSchema = $this->getGeneratedSchema($moduleName); + $expectedSchema = $this->getSchemaDocument($this->getSchemaFixturePath($moduleName, 'upgrade')); + + $this->assertEquals($expectedSchema->saveXML(), $generatedSchema->saveXML()); + } + + /** + * @param string $moduleName + * @return \DOMDocument + */ + private function getGeneratedSchema(string $moduleName): \DOMDocument + { + $modulePath = $this->componentRegistrar->getPath('module', $moduleName); + $schemaFileName = $modulePath + . DIRECTORY_SEPARATOR + . \Magento\Framework\Module\Dir::MODULE_ETC_DIR + . DIRECTORY_SEPARATOR + . 'db_schema.xml'; + + return $this->getSchemaDocument($schemaFileName); + } + + /** + * @param string $moduleName + * @param string $suffix + * @return string + */ + private function getSchemaFixturePath(string $moduleName, string $suffix): string + { + $schemaFixturePath = dirname(__DIR__, 2) + . DIRECTORY_SEPARATOR + . implode( + DIRECTORY_SEPARATOR, + [ + '_files', + 'SetupUpgrade', + str_replace('Magento_', '', $moduleName), + 'db_schema_' . $suffix . '.xml' + ] + ); + + return $schemaFixturePath; + } +} diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule8/db_schema.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule8/db_schema.xml new file mode 100644 index 0000000000000..cdc71980bf50d --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule8/db_schema.xml @@ -0,0 +1,114 @@ +<?xml version="1.0"?> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="module8_test_main_table" resource="default" engine="innodb" comment="Main Test Table for Module8"> + <column xsi:type="int" name="module8_email_contact_id" padding="10" unsigned="true" nullable="false" + identity="true" comment="Email Contact ID"/> + <column xsi:type="int" name="module8_contact_group_id" padding="10" unsigned="true" nullable="false" + identity="false" comment="Contact Group ID"/> + <column xsi:type="smallint" name="module8_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="varchar" name="module8_contact_id" nullable="true" length="15" comment="Contact ID"/> + <column xsi:type="varchar" name="module8_content" nullable="false" length="15" comment="Content"/> + <column xsi:type="int" name="module8_update_column" padding="11" unsigned="false" nullable="false" + identity="false" comment="Module_8 Update Column"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_email_contact_id"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE8_INSTALL_UNIQUE_INDEX_2"> + <column name="module8_email_contact_id"/> + <column name="module8_is_guest"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE8_INSTALL_UNIQUE_INDEX_TEMP" disabled="true"> + <column name="module8_contact_group_id"/> + </constraint> + <index referenceId="MODULE8_INSTALL_INDEX_1" indexType="btree"> + <column name="module8_email_contact_id"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_3" indexType="btree"> + <column name="module8_is_guest"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_4" indexType="btree"> + <column name="module8_contact_id"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_TEMP" indexType="btree" disabled="true"> + <column name="module8_content"/> + </index> + </table> + <table name="module8_test_second_table" resource="default" engine="innodb" comment="Second Test Table for Module8"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Entity ID"/> + <column xsi:type="int" name="module8_contact_id" padding="11" unsigned="false" nullable="true" + identity="false" comment="Contact ID"/> + <column xsi:type="varchar" name="module8_address" nullable="false" length="15" comment="Address"/> + <column xsi:type="smallint" name="module8_counter_with_multiline_comment" padding="5" unsigned="true" + nullable="true" identity="false" default="0" + comment="Empty Counter Multiline Comment"/> + <column xsi:type="varchar" name="module8_second_address" nullable="true" length="15" comment="Second Address"/> + <column xsi:type="varchar" name="module8_temp_column" nullable="true" length="15" + comment="Temp column for remove" disabled="true"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ENTITY_ID_TEST_MAIN_TABLE_EMAIL_CONTACT_ID" + table="module8_test_second_table" column="module8_entity_id" + referenceTable="module8_test_main_table" referenceColumn="module8_email_contact_id" + onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID" + table="module8_test_second_table" column="module8_address" referenceTable="module8_test_main_table" + referenceColumn="module8_contact_id" onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_MODULE8_CONTENT_TEMP" + table="module8_test_second_table" column="module8_address" referenceTable="module8_test_main_table" + referenceColumn="module8_content" onDelete="NO ACTION" disabled="true"/> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_1" indexType="btree"> + <column name="module8_entity_id"/> + </index> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_2" indexType="btree"> + <column name="module8_address"/> + </index> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP" indexType="btree"> + <column name="module8_second_address"/> + </index> + </table> + <table name="module8_test_update_table" resource="default" engine="innodb" comment="Update Test Table for Module8"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="false" + comment="Entity ID"/> + <column xsi:type="int" name="module8_entity_row_id" padding="10" unsigned="true" nullable="false" + identity="false" comment="Module8_entity_row_id"/> + <column xsi:type="smallint" name="module8_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="smallint" name="module8_guest_browser_id" padding="5" unsigned="true" nullable="true" + identity="false" comment="Guest Browser ID"/> + <column xsi:type="smallint" name="module8_column_for_remove" padding="5" unsigned="true" nullable="true" + identity="false" comment="For remove"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="MODULE8_UPDATE_FK_MODULE8_IS_GUEST" + table="module8_test_update_table" column="module8_is_guest" referenceTable="module8_test_main_table" + referenceColumn="module8_is_guest" onDelete="CASCADE"/> + <constraint xsi:type="foreign" referenceId="MODULE8_UPDATE_FK_TEMP" table="module8_test_update_table" + column="module8_column_for_remove" referenceTable="module8_test_main_table" + referenceColumn="module8_is_guest" onDelete="CASCADE"/> + <constraint xsi:type="unique" referenceId="MODULE8_UPDATE_UNIQUE_INDEX_TEMP"> + <column name="module8_entity_id"/> + <column name="module8_is_guest"/> + </constraint> + <index referenceId="MODULE8_UPDATE_IS_GUEST_INDEX" indexType="btree"> + <column name="module8_is_guest"/> + </index> + <index referenceId="MODULE8_UPDATE_TEMP_INDEX" indexType="btree"> + <column name="module8_column_for_remove"/> + <column name="module8_guest_browser_id"/> + </index> + </table> + <table name="module8_test_temp_table" resource="default" engine="innodb" comment="module8_test_temp_table"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Entity ID"/> + <column xsi:type="int" name="module8_counter" padding="10" unsigned="true" nullable="true" identity="false" + default="100" comment="Counter"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + </table> +</schema> diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule9/db_schema.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule9/db_schema.xml new file mode 100644 index 0000000000000..3ded03c9e79f0 --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule9/db_schema.xml @@ -0,0 +1,68 @@ +<?xml version="1.0"?> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="module9_test_main_table" resource="default" engine="innodb" comment="Main Test Table for Module9"> + <column xsi:type="int" name="module9_email_contact_id" padding="10" unsigned="true" nullable="false" + identity="true" comment="Entity ID"/> + <column xsi:type="smallint" name="module9_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="int" name="module9_guest_id" padding="10" unsigned="true" nullable="true" identity="false" + comment="Guest ID"/> + <column xsi:type="date" name="module9_created_at" comment="Created At"/> + <column xsi:type="int" name="module9_update_column" padding="11" unsigned="false" nullable="false" + identity="false" comment="Module_9 Update Column"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module9_email_contact_id"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE9_INSTALL_UNIQUE_INDEX_1"> + <column name="module9_email_contact_id"/> + <column name="module9_guest_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="MODULE9_UPDATE_FK_MODULE9_IS_GUEST" table="module9_test_main_table" + column="module9_is_guest" referenceTable="module8_test_main_table" + referenceColumn="module8_is_guest" onDelete="CASCADE"/> + </table> + <table name="module8_test_main_table" resource="default"> + <column xsi:type="int" name="module9_update_column" padding="11" unsigned="false" nullable="false" + identity="false" comment="Module_9 Update Column"/> + </table> + <table name="module8_test_update_table" resource="default"> + <column name="module8_column_for_remove" disabled="true"/> + <constraint xsi:type="foreign" referenceId="MODULE8_UPDATE_FK_TEMP" disabled="true"/> + <index referenceId="MODULE9_UPDATE_MODULE8_GUEST_BROWSER_ID" indexType="btree"> + <column name="module8_guest_browser_id"/> + </index> + <index referenceId="MODULE8_UPDATE_UNIQUE_INDEX_TEMP" disabled="true"/> + </table> + <table name="module8_test_second_table" resource="default"> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID" + disabled="true"/> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP" disabled="true"/> + </table> + <table name="module8_test_temp_table" disabled="true" resource="default"/> + <table name="module9_test_update_replica_table" resource="default" engine="innodb" + comment="Module9 Test Update Replica Table"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Module8 Entity Id"/> + <column xsi:type="int" name="module8_contact_id" padding="11" unsigned="false" nullable="true" + identity="false" comment="Module8 Contact Id"/> + <column xsi:type="varchar" name="module8_address" nullable="false" length="15" comment="Module8 Address"/> + <column xsi:type="smallint" name="module8_counter_with_multiline_comment" padding="5" unsigned="true" + nullable="true" identity="false" default="0" comment="Module8 Counter With Multiline Comment"/> + <column xsi:type="varchar" name="module8_second_address" nullable="true" length="15" + comment="Module8 Second Address"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="FK_8914AF398964FAFB4ED2E382866ABBF4" + table="module9_test_update_replica_table" column="module8_entity_id" + referenceTable="module8_test_main_table" referenceColumn="module8_email_contact_id" + onDelete="NO ACTION"/> + <index referenceId="MODULE9_TEST_UPDATE_REPLICA_TABLE_MODULE8_ENTITY_ID" indexType="btree"> + <column name="module8_entity_id"/> + </index> + <index referenceId="MODULE9_TEST_UPDATE_REPLICA_TABLE_MODULE8_ADDRESS" indexType="btree"> + <column name="module8_address"/> + </index> + </table> +</schema> diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_install.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_install.xml new file mode 100644 index 0000000000000..2da9901cf9629 --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_install.xml @@ -0,0 +1,81 @@ +<?xml version="1.0"?> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="module8_test_main_table" resource="default" engine="innodb" comment="Main Test Table for Module8"> + <column xsi:type="int" name="module8_email_contact_id" padding="10" unsigned="true" nullable="false" + identity="true" comment="Email Contact ID"/> + <column xsi:type="int" name="module8_contact_group_id" padding="10" unsigned="true" nullable="false" + identity="false" comment="Contact Group ID"/> + <column xsi:type="smallint" name="module8_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="varchar" name="module8_contact_id" nullable="true" length="15" comment="Contact ID"/> + <column xsi:type="varchar" name="module8_content" nullable="false" length="15" comment="Content"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_email_contact_id"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE8_INSTALL_UNIQUE_INDEX_2"> + <column name="module8_email_contact_id"/> + <column name="module8_is_guest"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE8_INSTALL_UNIQUE_INDEX_TEMP"> + <column name="module8_contact_group_id"/> + </constraint> + <index referenceId="MODULE8_INSTALL_INDEX_1" indexType="btree"> + <column name="module8_email_contact_id"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_3" indexType="btree"> + <column name="module8_is_guest"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_4" indexType="btree"> + <column name="module8_contact_id"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_TEMP" indexType="btree"> + <column name="module8_content"/> + </index> + </table> + <table name="module8_test_second_table" resource="default" engine="innodb" comment="Second Test Table for Module8"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Entity ID"/> + <column xsi:type="int" name="module8_contact_id" padding="11" unsigned="false" nullable="true" + identity="false" comment="Contact ID"/> + <column xsi:type="varchar" name="module8_address" nullable="false" length="15" comment="Address"/> + <column xsi:type="smallint" name="module8_counter_with_multiline_comment" padding="5" unsigned="true" + nullable="true" identity="false" default="0" + comment="Empty Counter Multiline Comment"/> + <column xsi:type="varchar" name="module8_second_address" nullable="true" length="15" comment="Second Address"/> + <column xsi:type="varchar" name="module8_temp_column" nullable="true" length="15" + comment="Temp column for remove"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ENTITY_ID_TEST_MAIN_TABLE_EMAIL_CONTACT_ID" + table="module8_test_second_table" column="module8_entity_id" + referenceTable="module8_test_main_table" referenceColumn="module8_email_contact_id" + onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID" + table="module8_test_second_table" column="module8_address" referenceTable="module8_test_main_table" + referenceColumn="module8_contact_id" onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_MODULE8_CONTENT_TEMP" + table="module8_test_second_table" column="module8_address" referenceTable="module8_test_main_table" + referenceColumn="module8_content" onDelete="NO ACTION"/> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_1" indexType="btree"> + <column name="module8_entity_id"/> + </index> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_2" indexType="btree"> + <column name="module8_address"/> + </index> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP" indexType="btree"> + <column name="module8_second_address"/> + </index> + </table> + <table name="module8_test_install_temp_table" resource="default" engine="innodb" + comment="module8_test_install_temp_table"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Entity ID"/> + <column xsi:type="int" name="module8_counter" padding="10" unsigned="true" nullable="true" identity="false" + default="100" comment="Counter"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + </table> +</schema> diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_upgrade.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_upgrade.xml new file mode 100644 index 0000000000000..6deed3105f292 --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_upgrade.xml @@ -0,0 +1,124 @@ +<?xml version="1.0"?> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="module8_test_main_table" resource="default" engine="innodb" comment="Main Test Table for Module8"> + <column xsi:type="int" name="module8_email_contact_id" padding="10" unsigned="true" nullable="false" + identity="true" comment="Email Contact ID"/> + <column xsi:type="int" name="module8_contact_group_id" padding="10" unsigned="true" nullable="false" + identity="false" comment="Contact Group ID"/> + <column xsi:type="smallint" name="module8_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="varchar" name="module8_contact_id" nullable="true" length="15" comment="Contact ID"/> + <column xsi:type="varchar" name="module8_content" nullable="false" length="15" comment="Content"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_email_contact_id"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE8_INSTALL_UNIQUE_INDEX_2"> + <column name="module8_email_contact_id"/> + <column name="module8_is_guest"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE8_INSTALL_UNIQUE_INDEX_TEMP" disabled="true"> + <column name="module8_contact_group_id"/> + </constraint> + <index referenceId="MODULE8_INSTALL_INDEX_1" indexType="btree"> + <column name="module8_email_contact_id"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_3" indexType="btree"> + <column name="module8_is_guest"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_4" indexType="btree"> + <column name="module8_contact_id"/> + </index> + <index referenceId="MODULE8_INSTALL_INDEX_TEMP" indexType="btree" disabled="true"> + <column name="module8_content"/> + </index> + <column xsi:type="int" name="module8_update_column" padding="11" unsigned="false" nullable="false" + identity="false" comment="Module_8 Update Column"/> + </table> + <table name="module8_test_second_table" resource="default" engine="innodb" comment="Second Test Table for Module8"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Entity ID"/> + <column xsi:type="int" name="module8_contact_id" padding="11" unsigned="false" nullable="true" + identity="false" comment="Contact ID"/> + <column xsi:type="varchar" name="module8_address" nullable="false" length="15" comment="Address"/> + <column xsi:type="smallint" name="module8_counter_with_multiline_comment" padding="5" unsigned="true" + nullable="true" identity="false" default="0" + comment="Empty Counter Multiline Comment"/> + <column xsi:type="varchar" name="module8_second_address" nullable="true" length="15" comment="Second Address"/> + <column xsi:type="varchar" name="module8_temp_column" nullable="true" length="15" + comment="Temp column for remove" disabled="true"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ENTITY_ID_TEST_MAIN_TABLE_EMAIL_CONTACT_ID" + table="module8_test_second_table" column="module8_entity_id" + referenceTable="module8_test_main_table" referenceColumn="module8_email_contact_id" + onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID" + table="module8_test_second_table" column="module8_address" referenceTable="module8_test_main_table" + referenceColumn="module8_contact_id" onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_MODULE8_CONTENT_TEMP" + table="module8_test_second_table" column="module8_address" referenceTable="module8_test_main_table" + referenceColumn="module8_content" onDelete="NO ACTION" disabled="true"/> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_1" indexType="btree"> + <column name="module8_entity_id"/> + </index> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_2" indexType="btree"> + <column name="module8_address"/> + </index> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP" indexType="btree"> + <column name="module8_second_address"/> + </index> + </table> + <table name="module8_test_install_temp_table" resource="default" engine="innodb" + comment="module8_test_install_temp_table" disabled="true"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Entity ID"/> + <column xsi:type="int" name="module8_counter" padding="10" unsigned="true" nullable="true" identity="false" + default="100" comment="Counter"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + </table> + <table name="module8_test_update_table" resource="default" engine="innodb" comment="Update Test Table for Module8"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="false" + comment="Entity ID"/> + <column xsi:type="int" name="module8_entity_row_id" padding="10" unsigned="true" nullable="false" + identity="false" comment="Module8_entity_row_id"/> + <column xsi:type="smallint" name="module8_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="smallint" name="module8_guest_browser_id" padding="5" unsigned="true" nullable="true" + identity="false" comment="Guest Browser ID"/> + <column xsi:type="smallint" name="module8_column_for_remove" padding="5" unsigned="true" nullable="true" + identity="false" comment="For remove"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="MODULE8_UPDATE_FK_MODULE8_IS_GUEST" + table="module8_test_update_table" column="module8_is_guest" referenceTable="module8_test_main_table" + referenceColumn="module8_is_guest" onDelete="CASCADE"/> + <constraint xsi:type="foreign" referenceId="MODULE8_UPDATE_FK_TEMP" table="module8_test_update_table" + column="module8_column_for_remove" referenceTable="module8_test_main_table" + referenceColumn="module8_is_guest" onDelete="CASCADE"/> + <constraint xsi:type="unique" referenceId="MODULE8_UPDATE_UNIQUE_INDEX_TEMP"> + <column name="module8_entity_id"/> + <column name="module8_is_guest"/> + </constraint> + <index referenceId="MODULE8_UPDATE_IS_GUEST_INDEX" indexType="btree"> + <column name="module8_is_guest"/> + </index> + <index referenceId="MODULE8_UPDATE_TEMP_INDEX" indexType="btree"> + <column name="module8_column_for_remove"/> + <column name="module8_guest_browser_id"/> + </index> + </table> + <table name="module8_test_temp_table" resource="default" engine="innodb" comment="module8_test_temp_table"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Entity ID"/> + <column xsi:type="int" name="module8_counter" padding="10" unsigned="true" nullable="true" identity="false" + default="100" comment="Counter"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + </table> +</schema> diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_install.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_install.xml new file mode 100644 index 0000000000000..2ac2cc607f0df --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_install.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="module9_test_main_table" resource="default" engine="innodb" comment="Main Test Table for Module9"> + <column xsi:type="int" name="module9_email_contact_id" padding="10" unsigned="true" nullable="false" + identity="true" comment="Entity ID"/> + <column xsi:type="smallint" name="module9_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="int" name="module9_guest_id" padding="10" unsigned="true" nullable="true" identity="false" + comment="Guest ID"/> + <column xsi:type="date" name="module9_created_at" comment="Created At"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module9_email_contact_id"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE9_INSTALL_UNIQUE_INDEX_1"> + <column name="module9_email_contact_id"/> + <column name="module9_guest_id"/> + </constraint> + </table> +</schema> diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_upgrade.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_upgrade.xml new file mode 100644 index 0000000000000..b522224ca07b2 --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_upgrade.xml @@ -0,0 +1,77 @@ +<?xml version="1.0"?> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="module9_test_main_table" resource="default" engine="innodb" comment="Main Test Table for Module9"> + <column xsi:type="int" name="module9_email_contact_id" padding="10" unsigned="true" nullable="false" + identity="true" comment="Entity ID"/> + <column xsi:type="smallint" name="module9_is_guest" padding="5" unsigned="true" nullable="true" + identity="false" comment="Is Guest"/> + <column xsi:type="int" name="module9_guest_id" padding="10" unsigned="true" nullable="true" identity="false" + comment="Guest ID"/> + <column xsi:type="date" name="module9_created_at" comment="Created At"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module9_email_contact_id"/> + </constraint> + <constraint xsi:type="unique" referenceId="MODULE9_INSTALL_UNIQUE_INDEX_1"> + <column name="module9_email_contact_id"/> + <column name="module9_guest_id"/> + </constraint> + <column xsi:type="int" name="module9_update_column" padding="11" unsigned="false" nullable="false" + identity="false" comment="Module_9 Update Column"/> + <constraint xsi:type="foreign" referenceId="MODULE9_UPDATE_FK_MODULE9_IS_GUEST" table="module9_test_main_table" + column="module9_is_guest" referenceTable="module8_test_main_table" + referenceColumn="module8_is_guest" onDelete="CASCADE"/> + </table> + <table name="module8_test_main_table" resource="default"> + <column xsi:type="int" name="module9_update_column" padding="11" unsigned="false" nullable="false" + identity="false" comment="Module_9 Update Column"/> + </table> + <table name="module8_test_update_table" resource="default"> + <column name="module8_column_for_remove" disabled="true"/> + <constraint xsi:type="foreign" referenceId="MODULE8_UPDATE_FK_TEMP" disabled="true"/> + <index referenceId="MODULE9_UPDATE_MODULE8_GUEST_BROWSER_ID" indexType="btree"> + <column name="module8_guest_browser_id"/> + </index> + <index referenceId="MODULE8_UPDATE_UNIQUE_INDEX_TEMP" disabled="true"/> + </table> + <table name="module8_test_second_table" resource="default"> + <constraint xsi:type="foreign" referenceId="MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID" + disabled="true"/> + <index referenceId="MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP" disabled="true"/> + </table> + <table name="module8_test_temp_table" disabled="true" resource="default"/> + <table name="module9_test_update_replica_table" resource="default" engine="innodb" + comment="Module9 Test Update Replica Table"> + <column xsi:type="int" name="module8_entity_id" padding="10" unsigned="true" nullable="false" identity="true" + comment="Module8 Entity Id"/> + <column xsi:type="int" name="module8_contact_id" padding="11" unsigned="false" nullable="true" + identity="false" comment="Module8 Contact Id"/> + <column xsi:type="varchar" name="module8_address" nullable="false" length="15" comment="Module8 Address"/> + <column xsi:type="smallint" name="module8_counter_with_multiline_comment" padding="5" unsigned="true" + nullable="true" identity="false" default="0" comment="Module8 Counter With Multiline Comment"/> + <column xsi:type="varchar" name="module8_second_address" nullable="true" length="15" + comment="Module8 Second Address"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="module8_entity_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="FK_F205D8789B56A8E75BBFC0C68C041E98" + table="module9_test_update_replica_table" column="module8_address" + referenceTable="module8_test_main_table" referenceColumn="module8_content" onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="FK_C7075560727757663A51EC925F4032C9" + table="module9_test_update_replica_table" column="module8_address" + referenceTable="module8_test_main_table" referenceColumn="module8_contact_id" onDelete="NO ACTION"/> + <constraint xsi:type="foreign" referenceId="FK_8914AF398964FAFB4ED2E382866ABBF4" + table="module9_test_update_replica_table" column="module8_entity_id" + referenceTable="module8_test_main_table" referenceColumn="module8_email_contact_id" + onDelete="NO ACTION"/> + <index referenceId="MODULE9_TEST_UPDATE_REPLICA_TABLE_MODULE8_ENTITY_ID" indexType="btree"> + <column name="module8_entity_id"/> + </index> + <index referenceId="MODULE9_TEST_UPDATE_REPLICA_TABLE_MODULE8_ADDRESS" indexType="btree"> + <column name="module8_address"/> + </index> + <index referenceId="MODULE9_TEST_UPDATE_REPLICA_TABLE_MODULE8_SECOND_ADDRESS" indexType="btree"> + <column name="module8_second_address"/> + </index> + </table> +</schema> diff --git a/dev/tests/setup-integration/testsuite/Magento/Setup/DeclarativeInstallerTest.php b/dev/tests/setup-integration/testsuite/Magento/Setup/DeclarativeInstallerTest.php index f6497e8e4b162..6097348d4fabc 100644 --- a/dev/tests/setup-integration/testsuite/Magento/Setup/DeclarativeInstallerTest.php +++ b/dev/tests/setup-integration/testsuite/Magento/Setup/DeclarativeInstallerTest.php @@ -29,7 +29,7 @@ class DeclarativeInstallerTest extends SetupTestCase /** * @var CliCommand */ - private $cliCommad; + private $cliCommand; /** * @var SchemaDiff @@ -51,11 +51,14 @@ class DeclarativeInstallerTest extends SetupTestCase */ private $describeTable; + /** + * @inheritdoc + */ public function setUp() { $objectManager = Bootstrap::getObjectManager(); $this->moduleManager = $objectManager->get(TestModuleManager::class); - $this->cliCommad = $objectManager->get(CliCommand::class); + $this->cliCommand = $objectManager->get(CliCommand::class); $this->describeTable = $objectManager->get(DescribeTable::class); $this->schemaDiff = $objectManager->get(SchemaDiff::class); $this->schemaConfig = $objectManager->get(SchemaConfigInterface::class); @@ -68,7 +71,7 @@ public function setUp() */ public function testInstallation() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); @@ -104,10 +107,11 @@ private function compareStructures() /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/column_modification.php + * @throws \Exception */ public function testInstallationWithColumnsModification() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); @@ -119,7 +123,7 @@ public function testInstallationWithColumnsModification() 'etc' ); - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); @@ -157,14 +161,15 @@ private function updateDbSchemaRevision($revisionName) /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/column_removal.php + * @throws \Exception */ public function testInstallationWithColumnsRemoval() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); $this->updateDbSchemaRevision('column_removals'); - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); @@ -195,14 +200,15 @@ private function getTrimmedData() /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.php + * @throws \Exception */ public function testInstallationWithConstraintsModification() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); $this->updateDbSchemaRevision('constraint_modifications'); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $diff = $this->schemaDiff->diff( $this->schemaConfig->getDeclarationConfig(), @@ -216,10 +222,11 @@ public function testInstallationWithConstraintsModification() /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.php + * @throws \Exception */ public function testInstallationWithDroppingTables() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); @@ -231,7 +238,7 @@ public function testInstallationWithDroppingTables() 'etc' ); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $diff = $this->schemaDiff->diff( $this->schemaConfig->getDeclarationConfig(), @@ -245,6 +252,7 @@ public function testInstallationWithDroppingTables() /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.php + * @throws \Exception */ public function testInstallWithCodeBaseRollback() { @@ -255,7 +263,7 @@ public function testInstallWithCodeBaseRollback() 'db_schema.xml', 'etc' ); - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); $beforeRollback = $this->describeTable->describeShard('default'); @@ -268,7 +276,7 @@ public function testInstallWithCodeBaseRollback() 'etc' ); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $afterRollback = $this->describeTable->describeShard('default'); self::assertEquals($this->getData()['after'], $afterRollback); } @@ -276,6 +284,7 @@ public function testInstallWithCodeBaseRollback() /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.php + * @throws \Exception */ public function testTableRename() { @@ -287,7 +296,7 @@ public function testTableRename() 'db_schema.xml', 'etc' ); - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); $before = $this->describeTable->describeShard('default'); @@ -305,7 +314,7 @@ public function testTableRename() 'etc' ); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $after = $this->describeTable->describeShard('default'); self::assertEquals($this->getData()['after'], $after['some_table_renamed']); $select = $adapter->select() @@ -315,10 +324,11 @@ public function testTableRename() /** * @moduleName Magento_TestSetupDeclarationModule8 + * @throws \Exception */ public function testForeignKeyReferenceId() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule8'] ); $this->moduleManager->updateRevision( @@ -327,7 +337,7 @@ public function testForeignKeyReferenceId() 'db_schema.xml', 'etc' ); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $tableStatements = $this->describeTable->describeShard('default'); $tableSql = $tableStatements['dependent']; $this->assertRegExp('/CONSTRAINT\s`DEPENDENT_PAGE_ID_ON_TEST_TABLE_PAGE_ID`/', $tableSql); @@ -337,10 +347,11 @@ public function testForeignKeyReferenceId() /** * @moduleName Magento_TestSetupDeclarationModule1 * @moduleName Magento_TestSetupDeclarationModule8 + * @throws \Exception */ public function testDisableIndexByExternalModule() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1', 'Magento_TestSetupDeclarationModule8'] ); $this->moduleManager->updateRevision( @@ -367,7 +378,7 @@ public function testDisableIndexByExternalModule() 'module.xml', 'etc' ); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $tableStatements = $this->describeTable->describeShard('default'); $tableSql = $tableStatements['test_table']; $this->assertNotRegExp( @@ -376,4 +387,36 @@ public function testDisableIndexByExternalModule() 'Index is not being disabled by external module' ); } + + /** + * @moduleName Magento_TestSetupDeclarationModule8 + * @moduleName Magento_TestSetupDeclarationModule9 + * @dataProviderFromFile Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.php + * @throws \Exception + */ + public function testInstallationWithDisablingTables() + { + $modules = [ + 'Magento_TestSetupDeclarationModule8', + 'Magento_TestSetupDeclarationModule9', + ]; + + foreach ($modules as $moduleName) { + $this->moduleManager->updateRevision( + $moduleName, + 'disabling_tables', + 'db_schema.xml', + 'etc' + ); + } + $this->cliCommand->install($modules); + + $diff = $this->schemaDiff->diff( + $this->schemaConfig->getDeclarationConfig(), + $this->schemaConfig->getDbConfig() + ); + self::assertNull($diff->getAll()); + $shardData = $this->describeTable->describeShard(Sharding::DEFAULT_CONNECTION); + self::assertEquals($this->getData(), $shardData); + } } diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php index 4ff5d0013892e..242e4ebb22a54 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php @@ -8,5 +8,6 @@ '/pub\/opt\/magento\/var/', '/COPYING\.txt/', '/setup\/src\/Zend\/Mvc\/Controller\/LazyControllerAbstractFactory\.php/', - '/app\/code\/(?!Magento)[^\/]*/' + '/app\/code\/(?!Magento)[^\/]*/', + '#dev/tests/setup-integration/testsuite/Magento/Developer/_files/\S*\.xml$#', ]; diff --git a/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php b/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php index 3ce78177d875f..f1d093b7deafa 100644 --- a/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php +++ b/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php @@ -22,18 +22,25 @@ class UnionExpression extends Expression */ protected $type; + /** + * @var string + */ + protected $pattern; + /** * @param Select[] $parts - * @param string $type + * @param string $type (optional) + * @param string $pattern (optional) */ - public function __construct(array $parts, $type = Select::SQL_UNION) + public function __construct(array $parts, $type = Select::SQL_UNION, $pattern = '') { $this->parts = $parts; $this->type = $type; + $this->pattern = $pattern; } /** - * @return string + * @inheritdoc */ public function __toString() { @@ -45,6 +52,10 @@ public function __toString() $parts[] = $part; } } - return implode($parts, $this->type); + $sql = implode($parts, $this->type); + if ($this->pattern) { + return sprintf($this->pattern, $sql); + } + return $sql; } } diff --git a/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php b/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php index a6ad10dded849..0a0dba36ef0ed 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php @@ -69,7 +69,7 @@ public function process( $operationName )->toArray( $this->exceptionFormatter->shouldShowDetail() ? - \GraphQL\Error\Debug::INCLUDE_DEBUG_MESSAGE | \GraphQL\Error\Debug::INCLUDE_TRACE : false + \GraphQL\Error\Debug::INCLUDE_DEBUG_MESSAGE : false ); } } diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/constraints/constraint.xsd b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/constraints/constraint.xsd index 3eed77c37caac..c379452d65d85 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/constraints/constraint.xsd +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/constraints/constraint.xsd @@ -9,7 +9,7 @@ <xs:include schemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/operations.xsd" /> <xs:attributeGroup name="baseConstraint"> - <xs:attributeGroup ref="basicOperations" /> + <xs:attribute name="disabled" type="xs:boolean" /> <xs:attribute name="referenceId" type="referenceIdType" use="required" /> </xs:attributeGroup> </xs:schema> diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/schema.xsd b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/schema.xsd index a2f8611c4bd33..e3c54413f810b 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/schema.xsd +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/schema.xsd @@ -84,6 +84,7 @@ <xs:element name="constraint" /> <xs:element name="index" type="index" /> </xs:choice> + <xs:attributeGroup ref="basicOperations" /> <xs:attribute name="name" type="xs:string" use="required" /> <xs:attribute name="resource" type="resourceType" /> <xs:attribute name="engine" type="engineType" /> diff --git a/lib/internal/Magento/Framework/Setup/SchemaListener.php b/lib/internal/Magento/Framework/Setup/SchemaListener.php index c6407a2569a20..aabd7dedc911b 100644 --- a/lib/internal/Magento/Framework/Setup/SchemaListener.php +++ b/lib/internal/Magento/Framework/Setup/SchemaListener.php @@ -12,6 +12,9 @@ /** * Listen for all changes and record them in order to reuse later. + * + * @SuppressWarnings(PHPMD.TooManyPublicMethods) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class SchemaListener { @@ -61,7 +64,8 @@ class SchemaListener 'SCALE' => 'scale', 'UNSIGNED' => 'unsigned', 'IDENTITY' => 'identity', - 'PRIMARY' => 'primary' + 'PRIMARY' => 'primary', + 'COMMENT' => 'comment', ]; /** @@ -71,7 +75,6 @@ class SchemaListener 'COLUMN_POSITION', 'COLUMN_TYPE', 'PRIMARY_POSITION', - 'COMMENT' ]; /** @@ -90,8 +93,6 @@ class SchemaListener private $handlers; /** - * Constructor. - * * @param array $definitionMappers * @param array $handlers */ @@ -132,7 +133,9 @@ private function castColumnDefinition($definition, $columnName) $definition = $this->doColumnMapping($definition); $definition['name'] = strtolower($columnName); $definitionType = $definition['type'] === 'int' ? 'integer' : $definition['type']; + $columnComment = $definition['comment'] ?? null; $definition = $this->definitionMappers[$definitionType]->convertToDefinition($definition); + $definition['comment'] = $columnComment; if (isset($definition['default']) && $definition['default'] === false) { $definition['default'] = null; //uniform default values } @@ -214,7 +217,7 @@ private function doColumnMapping(array $definition) * @param string $columnName * @param array $definition * @param string $primaryKeyName - * @param null $onCreate + * @param string|null $onCreate */ public function addColumn($tableName, $columnName, $definition, $primaryKeyName = 'PRIMARY', $onCreate = null) { @@ -448,6 +451,7 @@ private function prepareColumns($tableName, array $tableColumns) * @param array $foreignKeys * @param array $indexes * @param string $tableName + * @param string $engine */ private function prepareConstraintsAndIndexes(array $foreignKeys, array $indexes, $tableName, $engine) { @@ -478,11 +482,16 @@ private function prepareConstraintsAndIndexes(array $foreignKeys, array $indexes * Create table. * * @param Table $table + * @throws \Zend_Db_Exception */ public function createTable(Table $table) { $engine = strtolower($table->getOption('type')); - $this->tables[$this->getModuleName()][strtolower($table->getName())]['engine'] = $engine; + $this->tables[$this->getModuleName()][strtolower($table->getName())] = + [ + 'engine' => $engine, + 'comment' => $table->getComment(), + ]; $this->prepareColumns($table->getName(), $table->getColumns()); $this->prepareConstraintsAndIndexes($table->getForeignKeys(), $table->getIndexes(), $table->getName(), $engine); } @@ -510,7 +519,7 @@ public function toogleIgnore($flag) /** * Drop table. * - * @param $tableName + * @param string $tableName */ public function dropTable($tableName) { diff --git a/lib/internal/Magento/Framework/Setup/SchemaPersistor.php b/lib/internal/Magento/Framework/Setup/SchemaPersistor.php index f3af56b8ac2ca..51f61f1dde13b 100644 --- a/lib/internal/Magento/Framework/Setup/SchemaPersistor.php +++ b/lib/internal/Magento/Framework/Setup/SchemaPersistor.php @@ -6,7 +6,7 @@ namespace Magento\Framework\Setup; use Magento\Framework\Component\ComponentRegistrar; -use Magento\Framework\Shell; +use Magento\Framework\Setup\Declaration\Schema\Sharding; /** * Persist listened schema to db_schema.xml file. @@ -24,8 +24,6 @@ class SchemaPersistor private $xmlPersistor; /** - * Constructor. - * * @param ComponentRegistrar $componentRegistrar * @param XmlPersistor $xmlPersistor */ @@ -64,32 +62,88 @@ public function persist(SchemaListener $schemaListener) continue; } $schemaPatch = sprintf('%s/etc/db_schema.xml', $path); - if (file_exists($schemaPatch)) { - $dom = new \SimpleXMLElement(file_get_contents($schemaPatch)); - } else { - $dom = $this->initEmptyDom(); - } + $dom = $this->processTables($schemaPatch, $tablesData); + $this->persistModule($dom, $schemaPatch); + } + } + + /** + * Convert tables data into XML document. + * + * @param string $schemaPatch + * @param array $tablesData + * @return \SimpleXMLElement + */ + private function processTables(string $schemaPatch, array $tablesData): \SimpleXMLElement + { + if (file_exists($schemaPatch)) { + $dom = new \SimpleXMLElement(file_get_contents($schemaPatch)); + } else { + $dom = $this->initEmptyDom(); + } + $defaultAttributesValues = [ + 'resource' => Sharding::DEFAULT_CONNECTION, + ]; - foreach ($tablesData as $tableName => $tableData) { - $tableData = $this->handleDefinition($tableData); + foreach ($tablesData as $tableName => $tableData) { + $tableData = $this->handleDefinition($tableData); + $table = $dom->xpath("//table[@name='" . $tableName . "']"); + if (!$table) { $table = $dom->addChild('table'); $table->addAttribute('name', $tableName); - $table->addAttribute('resource', $tableData['resource']); - if (isset($tableData['engine']) && $tableData['engine'] !== null) { - $table->addAttribute('engine', $tableData['engine']); - } + } else { + $table = reset($table); + } - $this->processColumns($tableData, $table); - $this->processConstraints($tableData, $table); - $this->processIndexes($tableData, $table); + $attributeNames = ['disabled', 'resource', 'engine', 'comment']; + foreach ($attributeNames as $attributeName) { + $this->updateElementAttribute( + $table, + $attributeName, + $tableData, + $defaultAttributesValues[$attributeName] ?? null + ); } - $this->persistModule($dom, $schemaPatch); + $this->processColumns($tableData, $table); + $this->processConstraints($tableData, $table); + $this->processIndexes($tableData, $table); + } + + return $dom; + } + + /** + * Update element attribute value or create new attribute. + * + * @param \SimpleXMLElement $element + * @param string $attributeName + * @param array $elementData + * @param string|null $defaultValue + */ + private function updateElementAttribute( + \SimpleXMLElement $element, + string $attributeName, + array $elementData, + ?string $defaultValue = null + ) { + $attributeValue = $elementData[$attributeName] ?? $defaultValue; + if ($attributeValue !== null) { + if (is_bool($attributeValue)) { + $attributeValue = $this->castBooleanToString($attributeValue); + } + + if ($element->attributes()[$attributeName]) { + $element->attributes()->$attributeName = $attributeValue; + } else { + $element->addAttribute($attributeName, $attributeValue); + } } } /** * If disabled attribute is set to false it remove it at all. + * * Also handle other generic attributes. * * @param array $definition @@ -124,24 +178,30 @@ private function castBooleanToString($boolean) */ private function processColumns(array $tableData, \SimpleXMLElement $table) { - if (isset($tableData['columns'])) { - foreach ($tableData['columns'] as $columnData) { - $columnData = $this->handleDefinition($columnData); - $domColumn = $table->addChild('column'); - $domColumn->addAttribute('xsi:type', $columnData['xsi:type'], 'xsi'); - unset($columnData['xsi:type']); - - foreach ($columnData as $attributeKey => $attributeValue) { - if ($attributeValue === null) { - continue; - } - - if (is_bool($attributeValue)) { - $attributeValue = $this->castBooleanToString($attributeValue); - } + if (!isset($tableData['columns'])) { + return $table; + } - $domColumn->addAttribute($attributeKey, $attributeValue); + foreach ($tableData['columns'] as $columnName => $columnData) { + $columnData = $this->handleDefinition($columnData); + $domColumn = $table->xpath("column[@name='" . $columnName . "']"); + if (!$domColumn) { + $domColumn = $table->addChild('column'); + if (!empty($columnData['xsi:type'])) { + $domColumn->addAttribute('xsi:type', $columnData['xsi:type'], 'xsi'); } + $domColumn->addAttribute('name', $columnName); + } else { + $domColumn = reset($domColumn); + } + + $attributeNames = array_diff(array_keys($columnData), ['name', 'xsi:type']); + foreach ($attributeNames as $attributeName) { + $this->updateElementAttribute( + $domColumn, + $attributeName, + $columnData + ); } } @@ -160,14 +220,29 @@ private function processIndexes(array $tableData, \SimpleXMLElement $table) if (isset($tableData['indexes'])) { foreach ($tableData['indexes'] as $indexName => $indexData) { $indexData = $this->handleDefinition($indexData); - $domIndex = $table->addChild('index'); - $domIndex->addAttribute('name', $indexName); - if (isset($indexData['disabled']) && $indexData['disabled']) { - $domIndex->addAttribute('disabled', true); - } else { - $domIndex->addAttribute('indexType', $indexData['indexType']); + $domIndex = $table->xpath("index[@referenceId='" . $indexName . "']"); + if (!$domIndex) { + $domIndex = $this->getUniqueIndexByName($table, $indexName); + } + + if (!$domIndex) { + $domIndex = $table->addChild('index'); + $domIndex->addAttribute('referenceId', $indexName); + } elseif (is_array($domIndex)) { + $domIndex = reset($domIndex); + } + $attributeNames = array_diff(array_keys($indexData), ['referenceId', 'columns', 'name']); + foreach ($attributeNames as $attributeName) { + $this->updateElementAttribute( + $domIndex, + $attributeName, + $indexData + ); + } + + if (!empty($indexData['columns'])) { foreach ($indexData['columns'] as $column) { $columnXml = $domIndex->addChild('column'); $columnXml->addAttribute('name', $column); @@ -188,37 +263,48 @@ private function processIndexes(array $tableData, \SimpleXMLElement $table) */ private function processConstraints(array $tableData, \SimpleXMLElement $table) { - if (isset($tableData['constraints'])) { - foreach ($tableData['constraints'] as $constraintType => $constraints) { - if ($constraintType === 'foreign') { - foreach ($constraints as $name => $constraintData) { - $constraintData = $this->handleDefinition($constraintData); - $constraintDom = $table->addChild('constraint'); - $constraintDom->addAttribute('xsi:type', $constraintType, 'xsi'); - $constraintDom->addAttribute('name', $name); - - foreach ($constraintData as $attributeKey => $attributeValue) { - $constraintDom->addAttribute($attributeKey, $attributeValue); - } - } + if (!isset($tableData['constraints'])) { + return $table; + } + + foreach ($tableData['constraints'] as $constraintType => $constraints) { + foreach ($constraints as $constraintName => $constraintData) { + $constraintData = $this->handleDefinition($constraintData); + $domConstraint = $table->xpath("constraint[@referenceId='" . $constraintName . "']"); + if (!$domConstraint) { + $domConstraint = $table->addChild('constraint'); + $domConstraint->addAttribute('xsi:type', $constraintType, 'xsi'); + $domConstraint->addAttribute('referenceId', $constraintName); } else { - foreach ($constraints as $name => $constraintData) { - $constraintData = $this->handleDefinition($constraintData); - $constraintDom = $table->addChild('constraint'); - $constraintDom->addAttribute('xsi:type', $constraintType, 'xsi'); - $constraintDom->addAttribute('name', $name); - $constraintData['columns'] = $constraintData['columns'] ?? []; - - if (isset($constraintData['disabled'])) { - $constraintDom->addAttribute('disabled', (bool) $constraintData['disabled']); - } - - foreach ($constraintData['columns'] as $column) { - $columnXml = $constraintDom->addChild('column'); - $columnXml->addAttribute('name', $column); - } + $domConstraint = reset($domConstraint); + } + + $attributeNames = array_diff( + array_keys($constraintData), + ['referenceId', 'xsi:type', 'disabled', 'columns', 'name', 'type'] + ); + foreach ($attributeNames as $attributeName) { + $this->updateElementAttribute( + $domConstraint, + $attributeName, + $constraintData + ); + } + + if (!empty($constraintData['columns'])) { + foreach ($constraintData['columns'] as $column) { + $columnXml = $domConstraint->addChild('column'); + $columnXml->addAttribute('name', $column); } } + + if (!empty($constraintData['disabled'])) { + $this->updateElementAttribute( + $domConstraint, + 'disabled', + $constraintData + ); + } } } @@ -236,4 +322,26 @@ private function persistModule(\SimpleXMLElement $simpleXmlElementDom, $path) { $this->xmlPersistor->persist($simpleXmlElementDom, $path); } + + /** + * Retrieve unique index declaration by name. + * + * @param \SimpleXMLElement $table + * @param string $indexName + * @return \SimpleXMLElement|null + */ + private function getUniqueIndexByName(\SimpleXMLElement $table, string $indexName): ?\SimpleXMLElement + { + $indexElement = null; + $constraint = $table->xpath("constraint[@referenceId='" . $indexName . "']"); + if ($constraint) { + $constraint = reset($constraint); + $type = $constraint->attributes('xsi', true)->type; + if ($type == 'unique') { + $indexElement = $constraint; + } + } + + return $indexElement; + } } diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaListenerTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaListenerTest.php index 4e34b3aebbf3e..cfde80b12ee3a 100644 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaListenerTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaListenerTest.php @@ -127,6 +127,7 @@ public function testCreateTable() : void 'default' => 'CURRENT_TIMESTAMP', 'disabled' => false, 'onCreate' => null, + 'comment' => 'Column with type timestamp init update', ], 'integer' => [ @@ -139,6 +140,7 @@ public function testCreateTable() : void 'default' => null, 'disabled' => false, 'onCreate' => null, + 'comment' => 'Integer' ], 'decimal' => [ @@ -151,6 +153,7 @@ public function testCreateTable() : void 'default' => null, 'disabled' => false, 'onCreate' => null, + 'comment' => 'Decimal' ], ], $tables['First_Module']['new_table']['columns'] diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaPersistorTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaPersistorTest.php index cc88af15a262b..f65e6c910dc0d 100644 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaPersistorTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaPersistorTest.php @@ -152,13 +152,13 @@ public function schemaListenerTablesDataProvider() : array <column xmlns:xsi="xsi" xsi:type="integer" name="first_column" nullable="1" unsigned="0"/> <column xmlns:xsi="xsi" xsi:type="date" name="second_column" nullable="0"/> - <constraint xmlns:xsi="xsi" xsi:type="foreign" name="some_foreign_constraint" + <constraint xmlns:xsi="xsi" xsi:type="foreign" referenceId="some_foreign_constraint" referenceTable="table" referenceColumn="column" table="first_table" column="first_column"/> - <constraint xmlns:xsi="xsi" xsi:type="primary" name="PRIMARY"> + <constraint xmlns:xsi="xsi" xsi:type="primary" referenceId="PRIMARY"> <column name="second_column"/> </constraint> - <index name="TEST_INDEX" indexType="btree"> + <index referenceId="TEST_INDEX" indexType="btree"> <column name="first_column"/> </index> </table>