diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php index 0228b48f7f11..25cfb61d658c 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php @@ -6,7 +6,12 @@ */ namespace Magento\Backend\Controller\Adminhtml\System\Design; -class Save extends \Magento\Backend\Controller\Adminhtml\System\Design +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Save design action. + */ +class Save extends \Magento\Backend\Controller\Adminhtml\System\Design implements HttpPostActionInterface { /** * Filtering posted data. Converting localized data if needed @@ -26,6 +31,8 @@ protected function _filterPostData($data) } /** + * Save design action. + * * @return \Magento\Backend\Model\View\Result\Redirect */ public function execute() @@ -54,10 +61,10 @@ public function execute() } catch (\Exception $e) { $this->messageManager->addErrorMessage($e->getMessage()); $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setDesignData($data); - return $resultRedirect->setPath('adminhtml/*/', ['id' => $design->getId()]); + return $resultRedirect->setPath('*/*/edit', ['id' => $design->getId()]); } } - return $resultRedirect->setPath('adminhtml/*/'); + return $resultRedirect->setPath('*/*/'); } } 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 d3c84e69c954..e296c8d3b897 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 2bb10d3b31a2..893000544a72 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 707ebbb2964c..23f612582f42 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 0d82ba3817df..0082b376bc4a 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 000000000000..f7cd2e707628 --- /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 03a004e500ae..bf0762b4b031 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 6d0d953e4467..d136661e917c 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 0a1804aa284d..697648cedb7b 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 000000000000..e159a4ce5c0b --- /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 337a3dd53f59..3f67e4b087cc 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 178e58ef2d64..f35eb63ee0e0 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 6a4ac0d7683c..4114b199715c 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 000000000000..282331924bca --- /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/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php index 86f1db2022cc..f8f82511cc12 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php @@ -11,6 +11,7 @@ use Magento\Catalog\Model\Config\Source\Product\Options\Price as ProductOptionsPrice; use Magento\Framework\UrlInterface; use Magento\Framework\Stdlib\ArrayManager; +use Magento\Ui\Component\Form\Element\Hidden; use Magento\Ui\Component\Modal; use Magento\Ui\Component\Container; use Magento\Ui\Component\DynamicRows; @@ -867,10 +868,9 @@ protected function getPositionFieldConfig($sortOrder) 'data' => [ 'config' => [ 'componentType' => Field::NAME, - 'formElement' => Input::NAME, + 'formElement' => Hidden::NAME, 'dataScope' => static::FIELD_SORT_ORDER_NAME, 'dataType' => Number::NAME, - 'visible' => false, 'sortOrder' => $sortOrder, ], ], diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml index 65090fa3ac46..578281f44c4c 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml @@ -190,6 +190,13 @@ <label translate="true">Websites</label> </settings> </column> + <column name="cost" class="Magento\Catalog\Ui\Component\Listing\Columns\Price" sortOrder="120"> + <settings> + <addField>true</addField> + <filter>textRange</filter> + <label translate="true">Cost</label> + </settings> + </column> <actionsColumn name="actions" class="Magento\Catalog\Ui\Component\Listing\Columns\ProductActions" sortOrder="200"> <settings> <indexField>entity_id</indexField> diff --git a/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php b/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php index 6d343fe149d2..fabe504fbe31 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 ab650c94a0f0..0db178b2a0a6 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 fe4042e8a2e9..b0c4f2d8a609 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 71bdfe0613bb..5b75708d1ae0 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 7cfb5bf40be5..635260888e7f 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 000000000000..053a8c33e640 --- /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 711d5a2da9ff..9e47830debfc 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 dc6e100ab1ad..a78975379572 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 000000000000..2fa1b86a6157 --- /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 000000000000..885310d9399a --- /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 8559334238d2..ff6167ffc10e 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 82411faddfed..eefaf5f3b539 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 2502b79921e9..e07879e93a6b 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 f98075f2294c..46f10608bc95 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 25d8412c9105..c5c2368720b9 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/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml index a8712cdc183d..190ecccbfdb7 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml @@ -17,9 +17,9 @@ <legend class="legend admin__legend"> <span><?= /* @escapeNotVerified */ __('Associated Products') ?></span> </legend> - <div class="product-options"> - <div class="field admin__field _required required"> - <?php foreach ($_attributes as $_attribute): ?> + <div class="product-options fieldset admin__fieldset"> + <?php foreach ($_attributes as $_attribute): ?> + <div class="field admin__field _required required"> <label class="label admin__field-label"><?php /* @escapeNotVerified */ echo $_attribute->getProductAttribute() ->getStoreLabel($_product->getStoreId()); @@ -34,8 +34,8 @@ <option><?= /* @escapeNotVerified */ __('Choose an Option...') ?></option> </select> </div> - <?php endforeach; ?> - </div> + </div> + <?php endforeach; ?> </div> </fieldset> <script> 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 1df84d27a5c3..e73296042154 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/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php index 9f8f728a85ea..dd2b84e1da53 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php @@ -9,6 +9,7 @@ use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Catalog\Model\Product; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; @@ -45,12 +46,14 @@ public function resolve( $data = []; foreach ($value['options'] as $option) { $code = $option['attribute_code']; - if (!isset($value['product']['model'][$code])) { + /** @var Product|null $model */ + $model = $value['product']['model'] ?? null; + if (!$model || !$model->getData($code)) { continue; } foreach ($option['values'] as $optionValue) { - if ($optionValue['value_index'] != $value['product']['model'][$code]) { + if ($optionValue['value_index'] != $model->getData($code)) { continue; } $data[] = [ diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index 267a94a1d434..df95632c4b60 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -1,5 +1,8 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. +type Mutation { + addConfigurableProductsToCart(input: AddConfigurableProductsToCartInput): AddConfigurableProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") +} type ConfigurableProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "ConfigurableProduct defines basic features of a configurable product and its simple product variants") { variants: [ConfigurableVariant] @doc(description: "An array of variants of products") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableVariant") @@ -35,3 +38,30 @@ type ConfigurableProductOptionsValues @doc(description: "ConfigurableProductOpti store_label: String @doc(description: "The label of the product on the current store") use_default_value: Boolean @doc(description: "Indicates whether to use the default_label") } + +input AddConfigurableProductsToCartInput { + cart_id: String! + cartItems: [ConfigurableProductCartItemInput!]! +} + +type AddConfigurableProductsToCartOutput { + cart: Cart! +} + +input ConfigurableProductCartItemInput { + data: CartItemDetailsInput! + variant_sku: String! + customizable_options:[CustomizableOptionInput!] +} + +type ConfigurableCartItem implements CartItemInterface { + customizable_options: [SelectedCustomizableOption]! + configurable_options: [SelectedConfigurableOption!]! +} + +type SelectedConfigurableOption { + id: Int! + option_label: String! + value_id: Int! + value_label: String! +} diff --git a/app/code/Magento/Cookie/Helper/Cookie.php b/app/code/Magento/Cookie/Helper/Cookie.php index 05ab02d7a2a1..8bab596ab4c1 100644 --- a/app/code/Magento/Cookie/Helper/Cookie.php +++ b/app/code/Magento/Cookie/Helper/Cookie.php @@ -42,7 +42,8 @@ class Cookie extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param array $data * - * @throws \InvalidArgumentException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function __construct( \Magento\Framework\App\Helper\Context $context, diff --git a/app/code/Magento/Customer/Model/ResourceModel/Group.php b/app/code/Magento/Customer/Model/ResourceModel/Group.php index 80203e742e09..987723c5c9f5 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Group.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Group.php @@ -29,8 +29,8 @@ class Group extends \Magento\Framework\Model\ResourceModel\Db\VersionControl\Abs /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param Snapshot $entitySnapshot, - * @param RelationComposite $entityRelationComposite, + * @param Snapshot $entitySnapshot + * @param RelationComposite $entityRelationComposite * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement * @param Customer\CollectionFactory $customersFactory * @param string $connectionName @@ -110,6 +110,8 @@ protected function _afterDelete(\Magento\Framework\Model\AbstractModel $group) } /** + * Create customers collection. + * * @return \Magento\Customer\Model\ResourceModel\Customer\Collection */ protected function _createCustomersCollection() @@ -131,7 +133,7 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $group) } /** - * {@inheritdoc} + * @inheritdoc */ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) { diff --git a/app/code/Magento/Customer/Model/Vat.php b/app/code/Magento/Customer/Model/Vat.php index f608a6cf4c11..123a9eef4b75 100644 --- a/app/code/Magento/Customer/Model/Vat.php +++ b/app/code/Magento/Customer/Model/Vat.php @@ -179,18 +179,21 @@ public function checkVatNumber($countryCode, $vatNumber, $requesterCountryCode = return $gatewayResponse; } + $countryCodeForVatNumber = $this->getCountryCodeForVatNumber($countryCode); + $requesterCountryCodeForVatNumber = $this->getCountryCodeForVatNumber($requesterCountryCode); + try { $soapClient = $this->createVatNumberValidationSoapClient(); $requestParams = []; - $requestParams['countryCode'] = $countryCode; + $requestParams['countryCode'] = $countryCodeForVatNumber; $vatNumberSanitized = $this->isCountryInEU($countryCode) - ? str_replace([' ', '-', $countryCode], ['', '', ''], $vatNumber) + ? str_replace([' ', '-', $countryCodeForVatNumber], ['', '', ''], $vatNumber) : str_replace([' ', '-'], ['', ''], $vatNumber); $requestParams['vatNumber'] = $vatNumberSanitized; - $requestParams['requesterCountryCode'] = $requesterCountryCode; + $requestParams['requesterCountryCode'] = $requesterCountryCodeForVatNumber; $reqVatNumSanitized = $this->isCountryInEU($requesterCountryCode) - ? str_replace([' ', '-', $requesterCountryCode], ['', '', ''], $requesterVatNumber) + ? str_replace([' ', '-', $requesterCountryCodeForVatNumber], ['', '', ''], $requesterVatNumber) : str_replace([' ', '-'], ['', ''], $requesterVatNumber); $requestParams['requesterVatNumber'] = $reqVatNumSanitized; // Send request to service @@ -301,4 +304,22 @@ public function isCountryInEU($countryCode, $storeId = null) ); return in_array($countryCode, $euCountries); } + + /** + * Returns the country code to use in the VAT number which is not always the same as the normal country code + * + * @param string $countryCode + * @return string + */ + private function getCountryCodeForVatNumber(string $countryCode): string + { + // Greece uses a different code for VAT numbers then its country code + // See: http://ec.europa.eu/taxation_customs/vies/faq.html#item_11 + // And https://en.wikipedia.org/wiki/VAT_identification_number: + // "The full identifier starts with an ISO 3166-1 alpha-2 (2 letters) country code + // (except for Greece, which uses the ISO 639-1 language code EL for the Greek language, + // instead of its ISO 3166-1 alpha-2 country code GR)" + + return $countryCode === 'GR' ? 'EL' : $countryCode; + } } diff --git a/app/code/Magento/Directory/Model/CurrencyConfig.php b/app/code/Magento/Directory/Model/CurrencyConfig.php index fdb561c22417..f7230df6e86e 100644 --- a/app/code/Magento/Directory/Model/CurrencyConfig.php +++ b/app/code/Magento/Directory/Model/CurrencyConfig.php @@ -57,7 +57,7 @@ public function __construct( */ public function getConfigCurrencies(string $path) { - $result = $this->appState->getAreaCode() === Area::AREA_ADMINHTML + $result = in_array($this->appState->getAreaCode(), [Area::AREA_ADMINHTML, Area::AREA_CRONTAB]) ? $this->getConfigForAllStores($path) : $this->getConfigForCurrentStore($path); sort($result); diff --git a/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php b/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php index 827a32dcea4f..4ec34a3842fa 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php @@ -327,7 +327,7 @@ private function addDefaultCountryToOptions(array &$options) foreach ($options as $key => $option) { if (isset($defaultCountry[$option['value']])) { - $options[$key]['is_default'] = $defaultCountry[$option['value']]; + $options[$key]['is_default'] = !empty($defaultCountry[$option['value']]); } } } diff --git a/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php b/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php index 9b52bae26f90..e594be90b26d 100644 --- a/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php +++ b/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php @@ -68,7 +68,7 @@ protected function setUp() } /** - * Test get currency config for admin and storefront areas. + * Test get currency config for admin, crontab and storefront areas. * * @dataProvider getConfigCurrenciesDataProvider * @return void @@ -91,7 +91,7 @@ public function testGetConfigCurrencies(string $areCode) ->method('getCode') ->willReturn('testCode'); - if ($areCode === Area::AREA_ADMINHTML) { + if (in_array($areCode, [Area::AREA_ADMINHTML, Area::AREA_CRONTAB])) { $this->storeManager->expects(self::once()) ->method('getStores') ->willReturn([$store]); @@ -121,6 +121,7 @@ public function getConfigCurrenciesDataProvider() { return [ ['areaCode' => Area::AREA_ADMINHTML], + ['areaCode' => Area::AREA_CRONTAB], ['areaCode' => Area::AREA_FRONTEND], ]; } 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 0991b3f9f4b2..56188ab997b7 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 cd2fe7477ca6..7f6dfa2a5e9a 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/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml b/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml index 900d4a1bd5bb..0be71f20a382 100644 --- a/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml +++ b/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml @@ -33,8 +33,8 @@ </thead> <?php if ($_hasAssociatedProducts): ?> - <?php foreach ($_associatedProducts as $_item): ?> <tbody> + <?php foreach ($_associatedProducts as $_item): ?> <tr> <td data-th="<?= $block->escapeHtml(__('Product Name')) ?>" class="col item"> <strong class="product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> @@ -80,8 +80,8 @@ </td> </tr> <?php endif; ?> - </tbody> <?php endforeach; ?> + </tbody> <?php else: ?> <tbody> <tr> diff --git a/app/code/Magento/Msrp/Helper/Data.php b/app/code/Magento/Msrp/Helper/Data.php index b4ec34ebee19..393383bb2e77 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 000000000000..3922bb486891 --- /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 000000000000..be91a548ad90 --- /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 000000000000..a874de3b223a --- /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 6e7bf61063a2..e3099aa2f14d 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 dd5abd433073..a951c14cf4c7 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 72dd1d8bbecb..a0bd3ec132de 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/Newsletter/view/adminhtml/layout/newsletter_problem_block.xml b/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_problem_block.xml index 3eb7de194d24..5cc268333de7 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_problem_block.xml +++ b/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_problem_block.xml @@ -15,6 +15,7 @@ <argument name="message_block_visibility" xsi:type="string">true</argument> <argument name="use_ajax" xsi:type="string">true</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> + <argument name="grid_url" xsi:type="url" path="*/*/grid"/> </arguments> <block class="Magento\Backend\Block\Widget\Grid\ColumnSet" name="adminhtml.newslettrer.problem.grid.columnSet" as="grid.columnSet"> <arguments> diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote.php b/app/code/Magento/Quote/Model/ResourceModel/Quote.php index 946c0e0c5f3b..ae26407c7452 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote.php @@ -23,8 +23,8 @@ class Quote extends AbstractDb /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param Snapshot $entitySnapshot, - * @param RelationComposite $entityRelationComposite, + * @param Snapshot $entitySnapshot + * @param RelationComposite $entityRelationComposite * @param \Magento\SalesSequence\Model\Manager $sequenceManager * @param string $connectionName */ @@ -296,7 +296,7 @@ public function markQuotesRecollect($productIds) } /** - * {@inheritdoc} + * @inheritdoc */ public function save(\Magento\Framework\Model\AbstractModel $object) { diff --git a/app/code/Magento/Quote/Setup/Patch/Data/ConvertSerializedDataToJson.php b/app/code/Magento/Quote/Setup/Patch/Data/ConvertSerializedDataToJson.php index f53728027222..6c23379a37cf 100644 --- a/app/code/Magento/Quote/Setup/Patch/Data/ConvertSerializedDataToJson.php +++ b/app/code/Magento/Quote/Setup/Patch/Data/ConvertSerializedDataToJson.php @@ -3,18 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Quote\Setup\Patch\Data; -use Magento\Framework\App\ResourceConnection; use Magento\Quote\Setup\ConvertSerializedDataToJsonFactory; use Magento\Quote\Setup\QuoteSetupFactory; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class ConvertSerializedDataToJson - * @package Magento\Quote\Setup\Patch + * Convert quote serialized data to json. */ class ConvertSerializedDataToJson implements DataPatchInterface, PatchVersionInterface { @@ -36,6 +33,8 @@ class ConvertSerializedDataToJson implements DataPatchInterface, PatchVersionInt /** * PatchInitial constructor. * @param \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup + * @param QuoteSetupFactory $quoteSetupFactory + * @param ConvertSerializedDataToJsonFactory $convertSerializedDataToJsonFactory */ public function __construct( \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup, @@ -48,7 +47,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -57,7 +56,7 @@ public function apply() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -67,7 +66,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -75,7 +74,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php index 96259f226494..005cf3a10ca8 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php @@ -45,6 +45,8 @@ public function __construct( * @param Quote $cart * @param array $cartItems * @throws GraphQlInputException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException */ public function execute(Quote $cart, array $cartItems): void { diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php index aa5b41daebdc..2303d2aa14f0 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -61,6 +61,7 @@ public function __construct( * @return void * @throws GraphQlNoSuchEntityException * @throws GraphQlInputException + * @throws \Magento\Framework\Exception\LocalizedException */ public function execute(Quote $cart, array $cartItemData): void { diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/Address/BillingAddressDataProvider.php b/app/code/Magento/QuoteGraphQl/Model/Cart/Address/BillingAddressDataProvider.php new file mode 100644 index 000000000000..bcd781025a6d --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/Address/BillingAddressDataProvider.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart\Address; + +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\QuoteGraphQl\Model\Cart\Address\Mapper\Address; + +/** + * Collect and return information about a billing address + */ +class BillingAddressDataProvider +{ + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @var Address + */ + private $addressMapper; + + /** + * @var ExtensibleDataObjectConverter + */ + private $dataObjectConverter; + + /** + * AddressDataProvider constructor. + * + * @param CartRepositoryInterface $cartRepository + * @param Address $addressMapper + * @param ExtensibleDataObjectConverter $dataObjectConverter + */ + public function __construct( + CartRepositoryInterface $cartRepository, + Address $addressMapper, + ExtensibleDataObjectConverter $dataObjectConverter + ) { + $this->cartRepository = $cartRepository; + $this->addressMapper = $addressMapper; + $this->dataObjectConverter = $dataObjectConverter; + } + + /** + * Collect and return information about a billing addresses + * + * @param CartInterface $cart + * @return null|array + */ + public function getCartAddresses(CartInterface $cart): ?array + { + $cart = $this->cartRepository->get($cart->getId()); + $billingAddress = $cart->getBillingAddress(); + + if (!$billingAddress) { + return null; + } + $billingData = $this->dataObjectConverter->toFlatArray($billingAddress, [], AddressInterface::class); + $addressData = array_merge($billingData, $this->addressMapper->toNestedArray($billingAddress)); + + return $addressData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/Address/Mapper/Address.php b/app/code/Magento/QuoteGraphQl/Model/Cart/Address/Mapper/Address.php new file mode 100644 index 000000000000..64ac407e2692 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/Address/Mapper/Address.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart\Address\Mapper; + +use Magento\Quote\Model\Quote\Address as QuoteAddress; + +/** + * Class Address + * + * Extract the necessary address fields from an Address model + */ +class Address +{ + /** + * Converts Address model data to nested array + * + * @param QuoteAddress $address + * @return array + */ + public function toNestedArray(QuoteAddress $address): array + { + $addressData = [ + 'country' => [ + 'code' => $address->getCountryId(), + 'label' => $address->getCountry() + ], + 'region' => [ + 'code' => $address->getRegionCode(), + 'label' => $address->getRegion() + ], + 'street' => $address->getStreet(), + 'selected_shipping_method' => [ + 'code' => $address->getShippingMethod(), + 'label' => $address->getShippingDescription(), + 'free_shipping' => $address->getFreeShipping(), + ], + 'items_weight' => $address->getWeight(), + 'customer_notes' => $address->getCustomerNotes() + ]; + + if (!$address->hasItems()) { + return $addressData; + } + + $addressItemsData = []; + foreach ($address->getAllItems() as $addressItem) { + $addressItemsData[] = [ + 'cart_item_id' => $addressItem->getQuoteItemId(), + 'quantity' => $addressItem->getQty() + ]; + } + $addressData['cart_items'] = $addressItemsData; + + return $addressData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/Address/ShippingAddressesDataProvider.php b/app/code/Magento/QuoteGraphQl/Model/Cart/Address/ShippingAddressesDataProvider.php new file mode 100644 index 000000000000..eb9d76080959 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/Address/ShippingAddressesDataProvider.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart\Address; + +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\QuoteGraphQl\Model\Cart\Address\Mapper\Address; + +/** + * Class AddressDataProvider + * + * Collect and return information about cart shipping and billing addresses + */ +class ShippingAddressesDataProvider +{ + /** + * @var ExtensibleDataObjectConverter + */ + private $dataObjectConverter; + + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @var Address + */ + private $addressMapper; + + /** + * AddressDataProvider constructor. + * + * @param ExtensibleDataObjectConverter $dataObjectConverter + * @param CartRepositoryInterface $cartRepository + * @param Address $addressMapper + */ + public function __construct( + ExtensibleDataObjectConverter $dataObjectConverter, + CartRepositoryInterface $cartRepository, + Address $addressMapper + ) { + $this->dataObjectConverter = $dataObjectConverter; + $this->cartRepository = $cartRepository; + $this->addressMapper = $addressMapper; + } + + /** + * Collect and return information about shipping addresses + * + * @param CartInterface $cart + * @return array + */ + public function getCartAddresses(CartInterface $cart): array + { + $cart = $this->cartRepository->get($cart->getId()); + $addressData = []; + $shippingAddresses = $cart->getAllShippingAddresses(); + + if ($shippingAddresses) { + foreach ($shippingAddresses as $shippingAddress) { + $shippingData = $this->dataObjectConverter->toFlatArray($shippingAddress, [], AddressInterface::class); + $addressData[] = array_merge($shippingData, $this->addressMapper->toNestedArray($shippingAddress)); + } + } + + return $addressData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php new file mode 100644 index 000000000000..97b1ed09decc --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Customer\Api\Data\AddressInterface; +use Magento\CustomerGraphQl\Model\Customer\CheckCustomerAccount; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Api\BillingAddressManagementInterface; +use Magento\Customer\Api\AddressRepositoryInterface; + +/** + * Set billing address for a specified shopping cart + */ +class SetBillingAddressOnCart +{ + /** + * @var BillingAddressManagementInterface + */ + private $billingAddressManagement; + + /** + * @var AddressRepositoryInterface + */ + private $addressRepository; + + /** + * @var Address + */ + private $addressModel; + + /** + * @var CheckCustomerAccount + */ + private $checkCustomerAccount; + + /** + * @param BillingAddressManagementInterface $billingAddressManagement + * @param AddressRepositoryInterface $addressRepository + * @param Address $addressModel + * @param CheckCustomerAccount $checkCustomerAccount + */ + public function __construct( + BillingAddressManagementInterface $billingAddressManagement, + AddressRepositoryInterface $addressRepository, + Address $addressModel, + CheckCustomerAccount $checkCustomerAccount + ) { + $this->billingAddressManagement = $billingAddressManagement; + $this->addressRepository = $addressRepository; + $this->addressModel = $addressModel; + $this->checkCustomerAccount = $checkCustomerAccount; + } + + /** + * @inheritdoc + */ + public function execute(ContextInterface $context, CartInterface $cart, array $billingAddress): void + { + $customerAddressId = $billingAddress['customer_address_id'] ?? null; + $addressInput = $billingAddress['address'] ?? null; + $useForShipping = $billingAddress['use_for_shipping'] ?? false; + + if (null === $customerAddressId && null === $addressInput) { + throw new GraphQlInputException( + __('The billing address must contain either "customer_address_id" or "address".') + ); + } + if ($customerAddressId && $addressInput) { + throw new GraphQlInputException( + __('The billing address cannot contain "customer_address_id" and "address" at the same time.') + ); + } + $addresses = $cart->getAllShippingAddresses(); + if ($useForShipping && count($addresses) > 1) { + throw new GraphQlInputException( + __('Using the "use_for_shipping" option with multishipping is not possible.') + ); + } + if (null === $customerAddressId) { + $billingAddress = $this->addressModel->addData($addressInput); + } else { + $this->checkCustomerAccount->execute($context->getUserId(), $context->getUserType()); + + /** @var AddressInterface $customerAddress */ + $customerAddress = $this->addressRepository->getById($customerAddressId); + $billingAddress = $this->addressModel->importCustomerAddressData($customerAddress); + } + + $this->billingAddressManagement->assign($cart->getId(), $billingAddress, $useForShipping); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.php new file mode 100644 index 000000000000..7de5a4ac05c2 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\QuoteGraphQl\Model\Cart\Address\BillingAddressDataProvider; + +/** + * @inheritdoc + */ +class BillingAddress implements ResolverInterface +{ + /** + * @var BillingAddressDataProvider + */ + private $addressDataProvider; + + /** + * @param BillingAddressDataProvider $addressDataProvider + */ + public function __construct( + BillingAddressDataProvider $addressDataProvider + ) { + $this->addressDataProvider = $addressDataProvider; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + $cart = $value['model']; + + return $this->addressDataProvider->getCartAddresses($cart); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php new file mode 100644 index 000000000000..01a35f4b4152 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\Stdlib\ArrayManager; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\QuoteGraphQl\Model\Cart\SetBillingAddressOnCart as SetBillingAddressOnCartModel; + +/** + * Class SetBillingAddressOnCart + * + * Mutation resolver for setting billing address for shopping cart + */ +class SetBillingAddressOnCart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var ArrayManager + */ + private $arrayManager; + + /** + * @var SetBillingAddressOnCartModel + */ + private $setBillingAddressOnCart; + + /** + * @param GetCartForUser $getCartForUser + * @param ArrayManager $arrayManager + * @param SetBillingAddressOnCartModel $setBillingAddressOnCart + */ + public function __construct( + GetCartForUser $getCartForUser, + ArrayManager $arrayManager, + SetBillingAddressOnCartModel $setBillingAddressOnCart + ) { + $this->getCartForUser = $getCartForUser; + $this->arrayManager = $arrayManager; + $this->setBillingAddressOnCart = $setBillingAddressOnCart; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + $billingAddress = $this->arrayManager->get('input/billing_address', $args); + $maskedCartId = $this->arrayManager->get('input/cart_id', $args); + + if (!$maskedCartId) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); + } + if (!$billingAddress) { + throw new GraphQlInputException(__('Required parameter "billing_address" is missing')); + } + + $maskedCartId = $args['input']['cart_id']; + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); + + $this->setBillingAddressOnCart->execute($context, $cart, $billingAddress); + + return [ + 'cart' => [ + 'cart_id' => $maskedCartId, + 'model' => $cart, + ] + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php new file mode 100644 index 000000000000..caa0eee22d70 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\QuoteGraphQl\Model\Cart\Address\ShippingAddressesDataProvider; + +/** + * @inheritdoc + */ +class ShippingAddresses implements ResolverInterface +{ + /** + * @var ShippingAddressesDataProvider + */ + private $addressDataProvider; + + /** + * @param ShippingAddressesDataProvider $addressDataProvider + */ + public function __construct( + ShippingAddressesDataProvider $addressDataProvider + ) { + $this->addressDataProvider = $addressDataProvider; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + $cart = $value['model']; + + return $this->addressDataProvider->getCartAddresses($cart); + } +} diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 4c1101a5f90a..3448d640618d 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -12,7 +12,7 @@ type Mutation { setShippingAddressesOnCart(input: SetShippingAddressesOnCartInput): SetShippingAddressesOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetShippingAddressesOnCart") applyCouponToCart(input: ApplyCouponToCartInput): ApplyCouponToCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ApplyCouponToCart") removeCouponFromCart(input: RemoveCouponFromCartInput): RemoveCouponFromCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\RemoveCouponFromCart") - setBillingAddressOnCart(input: SetBillingAddressOnCartInput): SetBillingAddressOnCartOutput + setBillingAddressOnCart(input: SetBillingAddressOnCartInput): SetBillingAddressOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetBillingAddressOnCart") setShippingMethodsOnCart(input: SetShippingMethodsOnCartInput): SetShippingMethodsOnCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetShippingMethodsOnCart") addSimpleProductsToCart(input: AddSimpleProductsToCartInput): AddSimpleProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") } @@ -35,9 +35,13 @@ input CartItemQuantityInput { input SetBillingAddressOnCartInput { cart_id: String! + billing_address: BillingAddressInput! +} + +input BillingAddressInput { customer_address_id: Int address: CartAddressInput - # TODO: consider adding "Same as shipping" option + use_for_shipping: Boolean } input CartAddressInput { @@ -97,7 +101,8 @@ type Cart { cart_id: String items: [CartItemInterface] applied_coupon: AppliedCoupon - addresses: [CartAddress]! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartAddresses") + shipping_addresses: [CartAddress]! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddresses") + billing_address: CartAddress! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\BillingAddress") } type CartAddress { @@ -214,3 +219,8 @@ type CartItemSelectedOptionValuePrice { units: String! type: PriceTypeEnum! } + +input CartItemDetailsInput { + sku: String! + qty: Float! +} diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshLifetime.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshLifetime.php index 1b7ae6398d30..b86839459355 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshLifetime.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshLifetime.php @@ -1,12 +1,17 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Reports\Controller\Adminhtml\Report\Statistics; -class RefreshLifetime extends \Magento\Reports\Controller\Adminhtml\Report\Statistics +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Reports\Controller\Adminhtml\Report\Statistics; + +/** + * Refresh statistics action. + */ +class RefreshLifetime extends Statistics implements HttpPostActionInterface { /** * Refresh statistics for all period diff --git a/app/code/Magento/Review/Block/Adminhtml/Edit.php b/app/code/Magento/Review/Block/Adminhtml/Edit.php index d6868eae6fcb..f6f0ccef9b4e 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Edit.php +++ b/app/code/Magento/Review/Block/Adminhtml/Edit.php @@ -159,13 +159,13 @@ protected function _construct() } if ($this->getRequest()->getParam('ret', false) == 'pending') { - $this->buttonList->update('back', 'onclick', 'setLocation(\'' . $this->getUrl('catalog/*/pending') . '\')'); + $this->buttonList->update('back', 'onclick', 'setLocation(\'' . $this->getUrl('review/*/pending') . '\')'); $this->buttonList->update( 'delete', 'onclick', 'deleteConfirm(' . '\'' . __( 'Are you sure you want to do this?' - ) . '\' ' . '\'' . $this->getUrl( + ) . '\', ' . '\'' . $this->getUrl( '*/*/delete', [$this->_objectId => $this->getRequest()->getParam($this->_objectId), 'ret' => 'pending'] ) . '\'' . ')' diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php index 35187e46933b..6217729f53e5 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php @@ -10,9 +10,14 @@ use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\LocalizedException; +/** + * Save Review action. + */ class Save extends ProductController implements HttpPostActionInterface { /** + * Save Review action. + * * @return \Magento\Backend\Model\View\Result\Redirect * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -64,7 +69,7 @@ public function execute() if ($nextId) { $resultRedirect->setPath('review/*/edit', ['id' => $nextId]); } elseif ($this->getRequest()->getParam('ret') == 'pending') { - $resultRedirect->setPath('*/*/pending'); + $resultRedirect->setPath('review/*/pending'); } else { $resultRedirect->setPath('*/*/'); } diff --git a/app/code/Magento/Review/etc/acl.xml b/app/code/Magento/Review/etc/acl.xml index 397cc1cce61d..09b80750da14 100644 --- a/app/code/Magento/Review/etc/acl.xml +++ b/app/code/Magento/Review/etc/acl.xml @@ -17,7 +17,7 @@ <resource id="Magento_Backend::marketing"> <resource id="Magento_Backend::marketing_user_content"> <resource id="Magento_Review::reviews_all" title="Reviews" translate="title" sortOrder="10"/> - <resource id="Magento_Review::pending" title="Reviews" translate="title" sortOrder="20"/> + <resource id="Magento_Review::pending" title="Pending Reviews" translate="title" sortOrder="20"/> </resource> </resource> </resource> diff --git a/app/code/Magento/Review/etc/adminhtml/menu.xml b/app/code/Magento/Review/etc/adminhtml/menu.xml index e3532483f88a..0a2e49450e0c 100644 --- a/app/code/Magento/Review/etc/adminhtml/menu.xml +++ b/app/code/Magento/Review/etc/adminhtml/menu.xml @@ -9,6 +9,7 @@ <menu> <add id="Magento_Review::catalog_reviews_ratings_ratings" title="Rating" translate="title" module="Magento_Review" sortOrder="60" parent="Magento_Backend::stores_attributes" action="review/rating/" resource="Magento_Review::ratings"/> <add id="Magento_Review::catalog_reviews_ratings_reviews_all" title="Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="10" action="review/product/index" resource="Magento_Review::reviews_all"/> + <add id="Magento_Review::catalog_reviews_ratings_pending" title="Pending Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="20" action="review/product/pending" resource="Magento_Review::pending"/> <add id="Magento_Review::report_review" title="Reviews" translate="title" module="Magento_Reports" sortOrder="20" parent="Magento_Reports::report" resource="Magento_Reports::review"/> <add id="Magento_Review::report_review_customer" title="By Customers" translate="title" sortOrder="10" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/customer" resource="Magento_Reports::review_customer"/> <add id="Magento_Review::report_review_product" title="By Products" translate="title" sortOrder="20" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/product" resource="Magento_Reports::review_product"/> diff --git a/app/code/Magento/Rule/Model/Condition/AbstractCondition.php b/app/code/Magento/Rule/Model/Condition/AbstractCondition.php index d2be99757df4..6729fe722de5 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 6267e30a7a6d..33e1bf97c347 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 8e36562ebd7f..202337c39da3 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 000000000000..2041bf8f3c9a --- /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 11673f1f0fe2..beb566b20806 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 53aeeb62c6b7..5c2ff296ebee 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/Search/Model/EngineResolver.php b/app/code/Magento/Search/Model/EngineResolver.php index 720df0e0fda9..9e4ebf543635 100644 --- a/app/code/Magento/Search/Model/EngineResolver.php +++ b/app/code/Magento/Search/Model/EngineResolver.php @@ -10,6 +10,8 @@ use Psr\Log\LoggerInterface; /** + * Search engine resolver model. + * * @api * @since 100.1.0 */ @@ -61,6 +63,7 @@ class EngineResolver implements EngineResolverInterface /** * @param ScopeConfigInterface $scopeConfig * @param array $engines + * @param LoggerInterface $logger * @param string $path * @param string $scopeType * @param string $scopeCode diff --git a/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml index bee9a79abeb7..8004b750a4d1 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 7b69edf6b931..bd611d0cc186 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/Model/UiComponentGenerator.php b/app/code/Magento/Ui/Model/UiComponentGenerator.php index f699cff7aa52..ce51c4241e86 100644 --- a/app/code/Magento/Ui/Model/UiComponentGenerator.php +++ b/app/code/Magento/Ui/Model/UiComponentGenerator.php @@ -32,7 +32,6 @@ class UiComponentGenerator * UiComponentGenerator constructor. * @param ContextFactory $contextFactory * @param UiComponentFactory $uiComponentFactory - * @param array $data */ public function __construct( ContextFactory $contextFactory, @@ -48,6 +47,7 @@ public function __construct( * @param string $name * @param \Magento\Framework\View\LayoutInterface $layout * @return UiComponentInterface + * @throws \Magento\Framework\Exception\LocalizedException */ public function generateUiComponent($name, \Magento\Framework\View\LayoutInterface $layout) { diff --git a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/record.js b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/record.js index 54309ca06851..3987507ece54 100644 --- a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/record.js +++ b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/record.js @@ -25,7 +25,7 @@ define([ }, listens: { position: 'initPosition', - elems: 'setColumnVisibileListener' + elems: 'setColumnVisibleListener' }, links: { position: '${ $.name }.${ $.positionProvider }:value' @@ -123,7 +123,7 @@ define([ /** * Set column visibility listener */ - setColumnVisibileListener: function () { + setColumnVisibleListener: function () { var elem = _.find(this.elems(), function (curElem) { return !curElem.hasOwnProperty('visibleListener'); }); 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 19536e7ff8c1..999e3262dbbd 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 3ef64fd4b537..36a3232c3e61 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 b52669e2cd28..521ce9fc806a 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 39d996e05c3a..13b82a93eca2 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 c5d87a4b16c4..610d78e00b81 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 1aeb48b7c769..d11d4aa24373 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/adminhtml/Magento/backend/Magento_AdminNotification/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_AdminNotification/web/css/source/_module.less index c1b684aef354..afd91ed3dbde 100644 --- a/app/design/adminhtml/Magento/backend/Magento_AdminNotification/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_AdminNotification/web/css/source/_module.less @@ -83,7 +83,7 @@ .message-system-short-wrapper { overflow: hidden; - padding: 0 1.5rem 0 @indent__l; + padding: 0 1.5rem 0 1rem; } .message-system-collapsible { diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less index ad407160034a..22a584f1c8b8 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less @@ -235,6 +235,7 @@ .store-view { &:not(.store-switcher) { float: left; + margin-top: 13px; } .store-switcher-label { diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less index 03caa1c543bb..4621349eb280 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less @@ -111,7 +111,7 @@ content: @alert-icon__error__content; font-size: @alert-icon__error__font-size; left: 2.2rem; - margin-top: .5rem; + margin-top: -.9rem; } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less index 9cc38532cf3c..5d9bf80ce225 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less @@ -253,7 +253,7 @@ label.mage-error { .captcha-reload { float: right; - vertical-align: middle; + margin-top: 15px; } } } diff --git a/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less index 4d9cb41b3ada..d4a81027cc3d 100644 --- a/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less @@ -451,7 +451,8 @@ .form.password.reset, .form.send.confirmation, .form.password.forget, - .form.create.account { + .form.create.account, + .form.form-orders-search { min-width: 600px; width: 50%; } 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 f0dd8a957e9b..6e2069c6e88e 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_Customer/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less index 749a388ac235..ec6762de3d46 100755 --- a/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less @@ -406,7 +406,8 @@ .form.password.reset, .form.send.confirmation, .form.password.forget, - .form.create.account { + .form.create.account, + .form.form-orders-search { min-width: 600px; width: 50%; } 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 112184b45fe8..475361c56afc 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/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less index 4324e5f1d6a9..4b5e03f8013b 100644 --- a/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less @@ -363,6 +363,7 @@ .label { font-weight: @font-weight__semibold; margin-right: @indent__s; + vertical-align: middle; } } } diff --git a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less index 1e4a92fa0701..ab2eb1764f78 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less @@ -343,16 +343,22 @@ } .product-item-name { - display: inline-block; + float: left; + width: calc(100% - 20px); + } + .product-item::after { + clear: both; + content: ''; + display: table; } - .product-item { .label { &:extend(.abs-visually-hidden all); } .field.item { - display: inline-block; + float: left; + width: 20px; } } } diff --git a/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less index baf5468b1848..3435736a54a6 100644 --- a/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less @@ -10,6 +10,14 @@ & when (@media-common = true) { .form.send.friend { &:extend(.abs-add-fields all); + + .fieldset { + .field { + .control { + width: 100%; + } + } + } } .product-social-links .action.mailto.friend { @@ -44,3 +52,18 @@ } } } +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .form.send.friend { + .fieldset { + padding-bottom: @indent__xs; + } + + .action { + &.remove { + margin-left: 0; + right: 0; + top: 100%; + } + } + } +} 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 6b8388e2f434..237574dd6e22 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/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php index 2c2a4a0c60d9..c25eed1fd651 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php @@ -206,7 +206,6 @@ public function testQueryConfigurableProductLinks() /** * @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); $product = $productRepository->get($productSku, false, null, true); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartTest.php index 78204aaa567b..f0038351bcdc 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartTest.php @@ -70,7 +70,7 @@ public function testGetOwnCartForRegisteredCustomer() self::assertArrayHasKey('Cart', $response); self::assertNotEmpty($response['Cart']['items']); - self::assertNotEmpty($response['Cart']['addresses']); + self::assertNotEmpty($response['Cart']['shipping_addresses']); } /** @@ -141,10 +141,9 @@ private function prepareGetCartQuery( items { id } - addresses { + shipping_addresses { firstname, - lastname, - address_type + lastname } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetBillingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetBillingAddressOnCartTest.php new file mode 100644 index 000000000000..62fae71fa79f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetBillingAddressOnCartTest.php @@ -0,0 +1,407 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Multishipping\Helper\Data; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\TestFramework\ObjectManager; + +/** + * Test for set billing address on cart mutation + */ +class SetBillingAddressOnCartTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var Quote + */ + private $quote; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->create(QuoteResource::class); + $this->quote = $objectManager->create(Quote::class); + $this->quoteIdToMaskedId = $objectManager->create(QuoteIdToMaskedQuoteIdInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testSetNewGuestBillingAddressOnCart() + { + $this->quoteResource->load( + $this->quote, + 'test_order_with_simple_product_without_address', + 'reserved_order_id' + ); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + $this->assertNewAddressFields($billingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testSetNewGuestBillingAddressOnUseForShippingCart() + { + $this->quoteResource->load( + $this->quote, + 'test_order_with_simple_product_without_address', + 'reserved_order_id' + ); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + use_for_shipping: true + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + } + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertNewAddressFields($billingAddressResponse); + $this->assertNewAddressFields($shippingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testSetSavedBillingAddressOnCartByGuest() + { + $this->quoteResource->load( + $this->quote, + 'test_order_with_simple_product_without_address', + 'reserved_order_id' + ); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + } + } + } +} +QUERY; + self::expectExceptionMessage('The current customer isn\'t authorized.'); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testSetNewRegisteredCustomerBillingAddressOnCart() + { + $this->quoteResource->load( + $this->quote, + 'test_order_with_simple_product_without_address', + 'reserved_order_id' + ); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $this->quoteResource->load( + $this->quote, + 'test_order_with_simple_product_without_address', + 'reserved_order_id' + ); + $this->quote->setCustomerId(1); + $this->quoteResource->save($this->quote); + + $headerMap = $this->getHeaderMap(); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + $this->assertNewAddressFields($billingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + */ + public function testSetSavedRegisteredCustomerBillingAddressOnCart() + { + $this->quoteResource->load( + $this->quote, + 'test_order_with_simple_product_without_address', + 'reserved_order_id' + ); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $this->quoteResource->load( + $this->quote, + 'test_order_with_simple_product_without_address', + 'reserved_order_id' + ); + $this->quote->setCustomerId(1); + $this->quoteResource->save($this->quote); + + $headerMap = $this->getHeaderMap(); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + $this->assertSavedBillingAddressFields($billingAddressResponse); + } + + /** + * Verify the all the whitelisted fields for a New Address Object + * + * @param array $billingAddressResponse + */ + private function assertNewAddressFields(array $billingAddressResponse): void + { + $assertionMap = [ + ['response_field' => 'firstname', 'expected_value' => 'test firstname'], + ['response_field' => 'lastname', 'expected_value' => 'test lastname'], + ['response_field' => 'company', 'expected_value' => 'test company'], + ['response_field' => 'street', 'expected_value' => [0 => 'test street 1', 1 => 'test street 2']], + ['response_field' => 'city', 'expected_value' => 'test city'], + ['response_field' => 'postcode', 'expected_value' => '887766'], + ['response_field' => 'telephone', 'expected_value' => '88776655'] + ]; + + $this->assertResponseFields($billingAddressResponse, $assertionMap); + } + + /** + * Verify the all the whitelisted fields for a Address Object + * + * @param array $billingAddressResponse + */ + private function assertSavedBillingAddressFields(array $billingAddressResponse): void + { + $assertionMap = [ + ['response_field' => 'firstname', 'expected_value' => 'John'], + ['response_field' => 'lastname', 'expected_value' => 'Smith'], + ['response_field' => 'company', 'expected_value' => 'CompanyName'], + ['response_field' => 'street', 'expected_value' => [0 => 'Green str, 67']], + ['response_field' => 'city', 'expected_value' => 'CityM'], + ['response_field' => 'postcode', 'expected_value' => '75477'], + ['response_field' => 'telephone', 'expected_value' => '3468676'] + ]; + + $this->assertResponseFields($billingAddressResponse, $assertionMap); + } + + /** + * @param string $username + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com'): array + { + $password = 'password'; + /** @var CustomerTokenServiceInterface $customerTokenService */ + $customerTokenService = ObjectManager::getInstance() + ->get(CustomerTokenServiceInterface::class); + $customerToken = $customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } + + public function tearDown() + { + /** @var \Magento\Config\Model\ResourceModel\Config $config */ + $config = ObjectManager::getInstance()->get(\Magento\Config\Model\ResourceModel\Config::class); + + //default state of multishipping config + $config->saveConfig( + Data::XML_PATH_CHECKOUT_MULTIPLE_AVAILABLE, + 1, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 0 + ); + + /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ + $config = ObjectManager::getInstance()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $config->reinit(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php index a023d37895c2..d60876e7c0be 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingAddressOnCartTest.php @@ -7,7 +7,9 @@ namespace Magento\GraphQl\Quote; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Multishipping\Helper\Data; use Magento\Quote\Model\Quote; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; @@ -79,7 +81,7 @@ public function testSetNewGuestShippingAddressOnCart() } ) { cart { - addresses { + shipping_addresses { firstname lastname company @@ -96,8 +98,8 @@ public function testSetNewGuestShippingAddressOnCart() self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); $cartResponse = $response['setShippingAddressesOnCart']['cart']; - self::assertArrayHasKey('addresses', $cartResponse); - $shippingAddressResponse = current($cartResponse['addresses']); + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); $this->assertNewShippingAddressFields($shippingAddressResponse); } @@ -126,7 +128,7 @@ public function testSetSavedShippingAddressOnCartByGuest() } ) { cart { - addresses { + shipping_addresses { firstname lastname company @@ -171,7 +173,7 @@ public function testSetMultipleShippingAddressesOnCartByGuest() } ) { cart { - addresses { + shipping_addresses { firstname lastname company @@ -184,6 +186,18 @@ public function testSetMultipleShippingAddressesOnCartByGuest() } } QUERY; + /** @var \Magento\Config\Model\ResourceModel\Config $config */ + $config = ObjectManager::getInstance()->get(\Magento\Config\Model\ResourceModel\Config::class); + $config->saveConfig( + Data::XML_PATH_CHECKOUT_MULTIPLE_AVAILABLE, + null, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 0 + ); + /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ + $config = ObjectManager::getInstance()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $config->reinit(); + self::expectExceptionMessage('You cannot specify multiple shipping addresses.'); $this->graphQlQuery($query); } @@ -225,7 +239,7 @@ public function testSetSavedAndNewShippingAddressOnCartAtTheSameTime() } ) { cart { - addresses { + shipping_addresses { firstname lastname company @@ -267,7 +281,7 @@ public function testSetShippingAddressOnCartWithNoAddresses() } ) { cart { - addresses { + shipping_addresses { firstname lastname company @@ -332,7 +346,7 @@ public function testSetNewRegisteredCustomerShippingAddressOnCart() } ) { cart { - addresses { + shipping_addresses { firstname lastname company @@ -349,8 +363,8 @@ public function testSetNewRegisteredCustomerShippingAddressOnCart() self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); $cartResponse = $response['setShippingAddressesOnCart']['cart']; - self::assertArrayHasKey('addresses', $cartResponse); - $shippingAddressResponse = current($cartResponse['addresses']); + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); $this->assertNewShippingAddressFields($shippingAddressResponse); } @@ -390,7 +404,7 @@ public function testSetSavedRegisteredCustomerShippingAddressOnCart() } ) { cart { - addresses { + shipping_addresses { firstname lastname company @@ -407,8 +421,8 @@ public function testSetSavedRegisteredCustomerShippingAddressOnCart() self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); $cartResponse = $response['setShippingAddressesOnCart']['cart']; - self::assertArrayHasKey('addresses', $cartResponse); - $shippingAddressResponse = current($cartResponse['addresses']); + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); $this->assertSavedShippingAddressFields($shippingAddressResponse); } @@ -466,4 +480,22 @@ private function getHeaderMap(string $username = 'customer@example.com'): array $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; return $headerMap; } + + public function tearDown() + { + /** @var \Magento\Config\Model\ResourceModel\Config $config */ + $config = ObjectManager::getInstance()->get(\Magento\Config\Model\ResourceModel\Config::class); + + //default state of multishipping config + $config->saveConfig( + Data::XML_PATH_CHECKOUT_MULTIPLE_AVAILABLE, + 1, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 0 + ); + + /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ + $config = ObjectManager::getInstance()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $config->reinit(); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingMethodOnCartTest.php index 7e77284c6b22..1c6679ee30f2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingMethodOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/SetShippingMethodOnCartTest.php @@ -79,8 +79,8 @@ public function testSetShippingMethodOnCart() self::assertArrayHasKey('setShippingMethodsOnCart', $response); self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); self::assertEquals($maskedQuoteId, $response['setShippingMethodsOnCart']['cart']['cart_id']); - $addressesInformation = $response['setShippingMethodsOnCart']['cart']['addresses']; - self::assertCount(2, $addressesInformation); + $addressesInformation = $response['setShippingMethodsOnCart']['cart']['shipping_addresses']; + self::assertCount(1, $addressesInformation); self::assertEquals( $addressesInformation[0]['selected_shipping_method']['code'], $shippingCarrierCode . '_' . $shippingMethodCode @@ -221,7 +221,7 @@ private function prepareMutationQuery( cart { cart_id, - addresses { + shipping_addresses { selected_shipping_method { code label diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml index f7bd155fd2d5..d89fb3ddf88a 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml @@ -8,7 +8,7 @@ <mapping strict="0"> <fields> <attribute> - <selector>//div[@class="product-options"]//label[.="%s"]//following-sibling::*//select</selector> + <selector>//div[contains(@class, "product-options")]//div//label[.="%s"]//following-sibling::*//select</selector> <strategy>xpath</strategy> <input>select</input> </attribute> diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Catalog/Product/View.php b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Catalog/Product/View.php index 5627a9d887bc..c47df8c5463e 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Catalog/Product/View.php +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Catalog/Product/View.php @@ -27,14 +27,15 @@ class View extends ParentView * * @var string */ - protected $formatTierPrice = "//tbody[%row-number%]//ul[contains(@class,'tier')]//*[@class='item'][%line-number%]"; + protected $formatTierPrice = + "//tr[@class='row-tier-price'][%row-number%]//ul[contains(@class,'tier')]//*[@class='item'][%line-number%]"; /** * This member holds the class name of the special price block. * * @var string */ - protected $formatSpecialPrice = '//tbody[%row-number%]//*[contains(@class,"price-box")]'; + protected $formatSpecialPrice = '//tbody//tr[%row-number%]//*[contains(@class,"price-box")]'; /** * Get grouped product block diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php index 3e99c5cad3c3..2ba798e4811a 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php @@ -39,4 +39,35 @@ public function testExecute() \Magento\Framework\Message\MessageInterface::TYPE_ERROR ); } + + /** + * Testing by adding a valid coupon to cart + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_virtual_product_and_address.php + * @magentoDataFixture Magento/Usps/Fixtures/cart_rule_coupon_free_shipping.php + * @return void + */ + public function testAddingValidCoupon(): void + { + /** @var $session \Magento\Checkout\Model\Session */ + $session = $this->_objectManager->create(\Magento\Checkout\Model\Session::class); + $quote = $session->getQuote(); + $quote->setData('trigger_recollect', 1)->setTotalsCollectedFlag(true); + + $couponCode = 'IMPHBR852R61'; + $inputData = [ + 'remove' => 0, + 'coupon_code' => $couponCode + ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($inputData); + $this->dispatch( + 'checkout/cart/couponPost/' + ); + + $this->assertSessionMessages( + $this->equalTo(['You used coupon code "' . $couponCode . '".']), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php b/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php index b620d9097b4b..10f2749ddace 100644 --- a/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php @@ -56,7 +56,7 @@ protected function setUp() } /** - * Test get currency config for admin and storefront areas. + * Test get currency config for admin, crontab and storefront areas. * * @dataProvider getConfigCurrenciesDataProvider * @magentoDataFixture Magento/Store/_files/store.php @@ -77,7 +77,7 @@ public function testGetConfigCurrencies(string $areaCode, array $expected) $storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); $storeManager->setCurrentStore($store->getId()); - if ($areaCode === Area::AREA_ADMINHTML) { + if (in_array($areaCode, [Area::AREA_ADMINHTML, Area::AREA_CRONTAB])) { self::assertEquals($expected['allowed'], $this->currency->getConfigAllowCurrencies()); self::assertEquals($expected['base'], $this->currency->getConfigBaseCurrencies()); self::assertEquals($expected['default'], $this->currency->getConfigDefaultCurrencies()); @@ -118,6 +118,14 @@ public function getConfigCurrenciesDataProvider() 'default' => ['BDT', 'USD'], ], ], + [ + 'areaCode' => Area::AREA_CRONTAB, + 'expected' => [ + 'allowed' => ['BDT', 'BNS', 'BTD', 'EUR', 'USD'], + 'base' => ['BDT', 'USD'], + 'default' => ['BDT', 'USD'], + ], + ], [ 'areaCode' => Area::AREA_FRONTEND, 'expected' => [ diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php index cf3b9f05cbe0..403c45dde71a 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php @@ -8,6 +8,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\TestFramework\Helper\CacheCleaner; use Magento\Framework\DB\Ddl\Table; +use Magento\TestFramework\Helper\Bootstrap; class MysqlTest extends \PHPUnit\Framework\TestCase { @@ -19,7 +20,7 @@ class MysqlTest extends \PHPUnit\Framework\TestCase protected function setUp() { set_error_handler(null); - $this->resourceConnection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + $this->resourceConnection = Bootstrap::getObjectManager() ->get(ResourceConnection::class); CacheCleaner::cleanAll(); } @@ -40,7 +41,6 @@ public function testWaitTimeout() $this->markTestSkipped('This test is for \Magento\Framework\DB\Adapter\Pdo\Mysql'); } try { - $defaultWaitTimeout = $this->getWaitTimeout(); $minWaitTimeout = 1; $this->setWaitTimeout($minWaitTimeout); $this->assertEquals($minWaitTimeout, $this->getWaitTimeout(), 'Wait timeout was not changed'); @@ -49,17 +49,8 @@ public function testWaitTimeout() sleep($minWaitTimeout + 1); $result = $this->executeQuery('SELECT 1'); $this->assertInstanceOf(\Magento\Framework\DB\Statement\Pdo\Mysql::class, $result); - // Restore wait_timeout - $this->setWaitTimeout($defaultWaitTimeout); - $this->assertEquals( - $defaultWaitTimeout, - $this->getWaitTimeout(), - 'Default wait timeout was not restored' - ); - } catch (\Exception $e) { - // Reset connection on failure to restore global variables + } finally { $this->getDbAdapter()->closeConnection(); - throw $e; } } @@ -87,30 +78,14 @@ private function setWaitTimeout($waitTimeout) /** * Execute SQL query and return result statement instance * - * @param string $sql - * @return \Zend_Db_Statement_Interface - * @throws \Exception + * @param $sql + * @return void|\Zend_Db_Statement_Pdo + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Zend_Db_Adapter_Exception */ private function executeQuery($sql) { - /** - * Suppress PDO warnings to work around the bug https://bugs.php.net/bug.php?id=63812 - */ - $phpErrorReporting = error_reporting(); - /** @var $pdoConnection \PDO */ - $pdoConnection = $this->getDbAdapter()->getConnection(); - $pdoWarningsEnabled = $pdoConnection->getAttribute(\PDO::ATTR_ERRMODE) & \PDO::ERRMODE_WARNING; - if (!$pdoWarningsEnabled) { - error_reporting($phpErrorReporting & ~E_WARNING); - } - try { - $result = $this->getDbAdapter()->query($sql); - error_reporting($phpErrorReporting); - } catch (\Exception $e) { - error_reporting($phpErrorReporting); - throw $e; - } - return $result; + return $this->getDbAdapter()->query($sql); } /** diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Controller/SendmailTest.php b/dev/tests/integration/testsuite/Magento/SendFriend/Controller/SendmailTest.php new file mode 100644 index 000000000000..a075398e9cdb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Controller/SendmailTest.php @@ -0,0 +1,143 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SendFriend\Controller; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Customer\Model\Session; +use Magento\Framework\Data\Form\FormKey; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Class SendmailTest + */ +class SendmailTest extends AbstractController +{ + /** + * Share the product to friend as logged in customer + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/SendFriend/_files/disable_allow_guest_config.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testSendActionAsLoggedIn() + { + $product = $this->getProduct(); + $this->login(1); + $this->prepareRequestData(); + + $this->dispatch('sendfriend/product/sendmail/id/' . $product->getId()); + $this->assertSessionMessages( + $this->equalTo(['The link to a friend was sent.']), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * Share the product to friend as guest customer + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store sendfriend/email/enabled 1 + * @magentoConfigFixture default_store sendfriend/email/allow_guest 1 + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testSendActionAsGuest() + { + $product = $this->getProduct(); + $this->prepareRequestData(); + + $this->dispatch('sendfriend/product/sendmail/id/' . $product->getId()); + $this->assertSessionMessages( + $this->equalTo(['The link to a friend was sent.']), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * Share the product to friend as guest customer with invalid post data + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store sendfriend/email/enabled 1 + * @magentoConfigFixture default_store sendfriend/email/allow_guest 1 + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testSendActionAsGuestWithInvalidData() + { + $product = $this->getProduct(); + $this->prepareRequestData(true); + + $this->dispatch('sendfriend/product/sendmail/id/' . $product->getId()); + $this->assertSessionMessages( + $this->equalTo(['Invalid Sender Email']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * @return ProductInterface + */ + private function getProduct() + { + return $this->_objectManager->get(ProductRepositoryInterface::class)->get('custom-design-simple-product'); + } + + /** + * Login the user + * + * @param string $customerId Customer to mark as logged in for the session + * @return void + */ + protected function login($customerId) + { + /** @var Session $session */ + $session = Bootstrap::getObjectManager() + ->get(Session::class); + $session->loginById($customerId); + } + + /** + * @param bool $invalidData + * @return void + */ + private function prepareRequestData($invalidData = false) + { + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'sender' => [ + 'name' => 'Test', + 'email' => 'test@example.com', + 'message' => 'Message', + ], + 'recipients' => [ + 'name' => [ + 'Recipient 1', + 'Recipient 2' + ], + 'email' => [ + 'r1@example.com', + 'r2@example.com' + ] + ], + 'form_key' => $formKey->getFormKey(), + ]; + if ($invalidData) { + unset($post['sender']['email']); + } + + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setPostValue($post); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/_files/disable_allow_guest_config.php b/dev/tests/integration/testsuite/Magento/SendFriend/_files/disable_allow_guest_config.php new file mode 100644 index 000000000000..202a39613248 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/_files/disable_allow_guest_config.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\App\Config\Value; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Value $config */ +$config = Bootstrap::getObjectManager()->create(Value::class); +$config->setPath('sendfriend/email/enabled'); +$config->setScope('default'); +$config->setScopeId(0); +$config->setValue(1); +$config->save(); + +/** @var Value $config */ +$config = Bootstrap::getObjectManager()->create(Value::class); +$config->setPath('sendfriend/email/allow_guest'); +$config->setScope('default'); +$config->setScopeId(0); +$config->setValue(0); +$config->save(); diff --git a/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php b/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php index 3ce78177d875..f1d093b7deaf 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/DB/Statement/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Statement/Pdo/Mysql.php index 7b8314a76f32..d24bc5fef6ef 100644 --- a/lib/internal/Magento/Framework/DB/Statement/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Statement/Pdo/Mysql.php @@ -3,21 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +namespace Magento\Framework\DB\Statement\Pdo; + +use Magento\Framework\DB\Statement\Parameter; /** * Mysql DB Statement * * @author Magento Core Team <core@magentocommerce.com> */ -namespace Magento\Framework\DB\Statement\Pdo; - -use Magento\Framework\DB\Statement\Parameter; - class Mysql extends \Zend_Db_Statement_Pdo { + /** - * Executes statement with binding values to it. - * Allows transferring specific options to DB driver. + * Executes statement with binding values to it. Allows transferring specific options to DB driver. * * @param array $params Array of values to bind to parameter placeholders. * @return bool @@ -61,11 +60,9 @@ public function _executeWithBinding(array $params) $statement->bindParam($paramName, $bindValues[$name], $dataType, $length, $driverOptions); } - try { + return $this->tryExecute(function () use ($statement) { return $statement->execute(); - } catch (\PDOException $e) { - throw new \Zend_Db_Statement_Exception($e->getMessage(), (int)$e->getCode(), $e); - } + }); } /** @@ -90,7 +87,29 @@ public function _execute(array $params = null) if ($specialExecute) { return $this->_executeWithBinding($params); } else { - return parent::_execute($params); + return $this->tryExecute(function () use ($params) { + return $params !== null ? $this->_stmt->execute($params) : $this->_stmt->execute(); + }); + } + } + + /** + * Executes query and avoid warnings. + * + * @param callable $callback + * @return bool + * @throws \Zend_Db_Statement_Exception + */ + private function tryExecute($callback) + { + $previousLevel = error_reporting(\E_ERROR); // disable warnings for PDO bugs #63812, #74401 + try { + return $callback(); + } catch (\PDOException $e) { + $message = sprintf('%s, query was: %s', $e->getMessage(), $this->_stmt->queryString); + throw new \Zend_Db_Statement_Exception($message, (int)$e->getCode(), $e); + } finally { + error_reporting($previousLevel); } } } diff --git a/lib/internal/Magento/Framework/DB/Test/Unit/DB/Statement/MysqlTest.php b/lib/internal/Magento/Framework/DB/Test/Unit/DB/Statement/MysqlTest.php new file mode 100644 index 000000000000..714dfe6bb105 --- /dev/null +++ b/lib/internal/Magento/Framework/DB/Test/Unit/DB/Statement/MysqlTest.php @@ -0,0 +1,154 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\DB\Test\Unit\DB\Statement; + +use Magento\Framework\DB\Statement\Parameter; +use Magento\Framework\DB\Statement\Pdo\Mysql; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @inheritdoc + */ +class MysqlTest extends TestCase +{ + /** + * @var \Zend_Db_Adapter_Abstract|MockObject + */ + private $adapterMock; + + /** + * @var \PDO|MockObject + */ + private $pdoMock; + + /** + * @var \Zend_Db_Profiler|MockObject + */ + private $zendDbProfilerMock; + + /** + * @var \PDOStatement|MockObject + */ + private $pdoStatementMock; + + /** + * @inheritdoc + */ + public function setUp() + { + $this->adapterMock = $this->getMockForAbstractClass( + \Zend_Db_Adapter_Abstract::class, + [], + '', + false, + true, + true, + ['getConnection', 'getProfiler'] + ); + $this->pdoMock = $this->createMock(\PDO::class); + $this->adapterMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->pdoMock); + $this->zendDbProfilerMock = $this->createMock(\Zend_Db_Profiler::class); + $this->adapterMock->expects($this->once()) + ->method('getProfiler') + ->willReturn($this->zendDbProfilerMock); + $this->pdoStatementMock = $this->createMock(\PDOStatement::class); + } + + public function testExecuteWithoutParams() + { + $query = 'SET @a=1;'; + $this->pdoMock->expects($this->once()) + ->method('prepare') + ->with($query) + ->willReturn($this->pdoStatementMock); + $this->pdoStatementMock->expects($this->once()) + ->method('execute'); + (new Mysql($this->adapterMock, $query))->_execute(); + } + + public function testExecuteWhenThrowPDOException() + { + $this->expectException(\Zend_Db_Statement_Exception::class); + $this->expectExceptionMessage('test message, query was:'); + $errorReporting = error_reporting(); + $query = 'SET @a=1;'; + $this->pdoMock->expects($this->once()) + ->method('prepare') + ->with($query) + ->willReturn($this->pdoStatementMock); + $this->pdoStatementMock->expects($this->once()) + ->method('execute') + ->willThrowException(new \PDOException('test message')); + + $this->assertEquals($errorReporting, error_reporting(), 'Error report level was\'t restored'); + + (new Mysql($this->adapterMock, $query))->_execute(); + } + + public function testExecuteWhenParamsAsPrimitives() + { + $params = [':param1' => 'value1', ':param2' => 'value2']; + $query = 'UPDATE `some_table1` SET `col1`=\'val1\' WHERE `param1`=\':param1\' AND `param2`=\':param2\';'; + $this->pdoMock->expects($this->once()) + ->method('prepare') + ->with($query) + ->willReturn($this->pdoStatementMock); + $this->pdoStatementMock->expects($this->never()) + ->method('bindParam'); + $this->pdoStatementMock->expects($this->once()) + ->method('execute') + ->with($params); + + (new Mysql($this->adapterMock, $query))->_execute($params); + } + + public function testExecuteWhenParamsAsParameterObject() + { + $param1 = $this->createMock(Parameter::class); + $param1Value = 'SomeValue'; + $param1DataType = 'dataType'; + $param1Length = '9'; + $param1DriverOptions = 'some driver options'; + $param1->expects($this->once()) + ->method('getIsBlob') + ->willReturn(false); + $param1->expects($this->once()) + ->method('getDataType') + ->willReturn($param1DataType); + $param1->expects($this->once()) + ->method('getLength') + ->willReturn($param1Length); + $param1->expects($this->once()) + ->method('getDriverOptions') + ->willReturn($param1DriverOptions); + $param1->expects($this->once()) + ->method('getValue') + ->willReturn($param1Value); + $params = [ + ':param1' => $param1, + ':param2' => 'value2', + ]; + $query = 'UPDATE `some_table1` SET `col1`=\'val1\' WHERE `param1`=\':param1\' AND `param2`=\':param2\';'; + $this->pdoMock->expects($this->once()) + ->method('prepare') + ->with($query) + ->willReturn($this->pdoStatementMock); + $this->pdoStatementMock->expects($this->exactly(2)) + ->method('bindParam') + ->withConsecutive( + [':param1', $param1Value, $param1DataType, $param1Length, $param1DriverOptions], + [':param2', 'value2', \PDO::PARAM_STR, null, null] + ); + $this->pdoStatementMock->expects($this->once()) + ->method('execute'); + + (new Mysql($this->adapterMock, $query))->_execute($params); + } +} diff --git a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php index 4dd358783a50..3ecf360f3689 100644 --- a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php +++ b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php @@ -14,7 +14,10 @@ use Zend\Uri\UriInterface; /** + * HTTP Request for current PHP environment. + * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Request extends \Zend\Http\PhpEnvironment\Request { @@ -586,6 +589,7 @@ public function setPostValue($name, $value = null) /** * Access values contained in the superglobals as public members + * * Order of precedence: 1. GET, 2. POST, 3. COOKIE, 4. SERVER, 5. ENV * * @see http://msdn.microsoft.com/en-us/library/system.web.httprequest.item.aspx @@ -683,7 +687,7 @@ public function has($key) * * @param string $name Header name to retrieve. * @param mixed|null $default Default value to use when the requested header is missing. - * @return bool|HeaderInterface + * @return bool|string */ public function getHeader($name, $default = false) { @@ -795,6 +799,8 @@ public function getBaseUrl() } /** + * Get flag value for whether the request is forwarded or not. + * * @return bool * @codeCoverageIgnore */ @@ -804,6 +810,8 @@ public function isForwarded() } /** + * Set flag value for whether the request is forwarded or not. + * * @param bool $forwarded * @return $this * @codeCoverageIgnore diff --git a/pub/media/.htaccess b/pub/media/.htaccess index 28e65b490fbb..d8793a891430 100644 --- a/pub/media/.htaccess +++ b/pub/media/.htaccess @@ -23,6 +23,9 @@ SetHandler default-handler Options +FollowSymLinks RewriteEngine on + ## you can put here your pub/media folder path relative to web root + #RewriteBase /magento/pub/media/ + ############################################ ## never rewrite for existing files RewriteCond %{REQUEST_FILENAME} !-f