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/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/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/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 6d0d953e44676..d136661e917cb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -149,6 +149,15 @@ EavStockItem CustomAttributeProductAttribute + + 50 + + + 60 + + + 70 + api-simple-product-two simple 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/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/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/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/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/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/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 72dd1d8bbecbe..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,11 +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 () { @@ -90,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()); @@ -105,6 +117,7 @@ define([ /** * Init info popup + * * @private */ initInfoPopup: function () { @@ -213,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); @@ -224,6 +241,7 @@ define([ }, /** + * Toggle MAP popup visibility * * @param {HTMLElement} $elem * @private @@ -240,6 +258,7 @@ define([ }, /** + * Close MAP information popup * * @param {HTMLElement} $elem */ @@ -274,6 +293,90 @@ define([ $(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 * @@ -284,6 +387,7 @@ define([ $(this.options.cartButtonId).prop('disabled', true); } } + }); return $.mage.addToCart; 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/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/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/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 7b69edf6b9314..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'); }, 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/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/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; } }