diff --git a/app/code/Magento/Catalog/Pricing/Price/TierPrice.php b/app/code/Magento/Catalog/Pricing/Price/TierPrice.php index 1aa43a39af442..0434f0572135b 100644 --- a/app/code/Magento/Catalog/Pricing/Price/TierPrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/TierPrice.php @@ -9,19 +9,27 @@ use Magento\Catalog\Model\Product; use Magento\Customer\Api\GroupManagementInterface; use Magento\Customer\Model\Session; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Pricing\Adjustment\CalculatorInterface; use Magento\Framework\Pricing\Amount\AmountInterface; use Magento\Framework\Pricing\Price\AbstractPrice; use Magento\Framework\Pricing\Price\BasePriceProviderInterface; +use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Pricing\PriceInfoInterface; use Magento\Customer\Model\Group\RetrieverInterface as CustomerGroupRetrieverInterface; +use Magento\Tax\Model\Config; /** * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class TierPrice extends AbstractPrice implements TierPriceInterface, BasePriceProviderInterface { + private const XML_PATH_TAX_DISPLAY_TYPE = 'tax/display/type'; + /** * Price type tier */ @@ -62,35 +70,43 @@ class TierPrice extends AbstractPrice implements TierPriceInterface, BasePricePr */ private $customerGroupRetriever; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * @param Product $saleableItem * @param float $quantity * @param CalculatorInterface $calculator - * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param PriceCurrencyInterface $priceCurrency * @param Session $customerSession * @param GroupManagementInterface $groupManagement * @param CustomerGroupRetrieverInterface|null $customerGroupRetriever + * @param ScopeConfigInterface|null $scopeConfig */ public function __construct( Product $saleableItem, $quantity, CalculatorInterface $calculator, - \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, + PriceCurrencyInterface $priceCurrency, Session $customerSession, GroupManagementInterface $groupManagement, - CustomerGroupRetrieverInterface $customerGroupRetriever = null + CustomerGroupRetrieverInterface $customerGroupRetriever = null, + ?ScopeConfigInterface $scopeConfig = null ) { $quantity = (float)$quantity ? $quantity : 1; parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency); $this->customerSession = $customerSession; $this->groupManagement = $groupManagement; $this->customerGroupRetriever = $customerGroupRetriever - ?? \Magento\Framework\App\ObjectManager::getInstance()->get(CustomerGroupRetrieverInterface::class); + ?? ObjectManager::getInstance()->get(CustomerGroupRetrieverInterface::class); if ($saleableItem->hasCustomerGroupId()) { $this->customerGroup = (int) $saleableItem->getCustomerGroupId(); } else { $this->customerGroup = (int) $this->customerGroupRetriever->getCustomerGroupId(); } + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** @@ -136,6 +152,8 @@ protected function isFirstPriceBetter($firstPrice, $secondPrice) } /** + * Returns tier price count + * * @return int */ public function getTierPriceCount() @@ -144,6 +162,8 @@ public function getTierPriceCount() } /** + * Returns tier price list + * * @return array */ public function getTierPriceList() @@ -155,15 +175,32 @@ public function getTierPriceList() $this->priceList, function (&$priceData) { /* convert string value to float */ - $priceData['price_qty'] = $priceData['price_qty'] * 1; + $priceData['price_qty'] *= 1; + if ($this->getConfigTaxDisplayType() === Config::DISPLAY_TYPE_BOTH) { + $exclTaxPrice = $this->calculator->getAmount($priceData['price'], $this->product, true); + $priceData['excl_tax_price'] = $exclTaxPrice; + } $priceData['price'] = $this->applyAdjustment($priceData['price']); } ); } + return $this->priceList; } /** + * Returns config tax display type + * + * @return int + */ + private function getConfigTaxDisplayType(): int + { + return (int) $this->scopeConfig->getValue(self::XML_PATH_TAX_DISPLAY_TYPE); + } + + /** + * Filters tier prices + * * @param array $priceList * @return array */ @@ -204,6 +241,8 @@ protected function filterTierPrices(array $priceList) } /** + * Returns base price + * * @return float */ protected function getBasePrice() @@ -213,25 +252,22 @@ protected function getBasePrice() } /** - * Calculates savings percentage according to the given tier price amount - * and related product price amount. + * Calculates savings percentage according to the given tier price amount and related product price amount. * * @param AmountInterface $amount - * * @return float */ public function getSavePercent(AmountInterface $amount) { - $productPriceAmount = $this->priceInfo->getPrice( - FinalPrice::PRICE_CODE - )->getAmount(); + $productPriceAmount = $this->priceInfo->getPrice(FinalPrice::PRICE_CODE) + ->getAmount(); - return round( - 100 - ((100 / $productPriceAmount->getValue()) * $amount->getValue()) - ); + return round(100 - ((100 / $productPriceAmount->getValue()) * $amount->getValue())); } /** + * Apply adjustment to price + * * @param float|string $price * @return \Magento\Framework\Pricing\Amount\AmountInterface */ @@ -314,6 +350,8 @@ protected function getStoredTierPrices() } /** + * Return is percentage discount + * * @return bool */ public function isPercentageDiscount() 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 6a9e84e345985..26923fc9df837 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php @@ -7,6 +7,7 @@ */ namespace Magento\ConfigurableProduct\Block\Product\View\Type; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\ConfigurableProduct\Model\ConfigurableAttributeData; use Magento\Customer\Helper\Session\CurrentCustomer; @@ -287,53 +288,70 @@ protected function getOptionPrices() { $prices = []; foreach ($this->getAllowProducts() as $product) { - $tierPrices = []; $priceInfo = $product->getPriceInfo(); - $tierPriceModel = $priceInfo->getPrice('tier_price'); - $tierPricesList = $tierPriceModel->getTierPriceList(); - foreach ($tierPricesList as $tierPrice) { - $tierPrices[] = [ - 'qty' => $this->localeFormat->getNumber($tierPrice['price_qty']), - 'price' => $this->localeFormat->getNumber($tierPrice['price']->getValue()), - 'percentage' => $this->localeFormat->getNumber( - $tierPriceModel->getSavePercent($tierPrice['price']) - ), - ]; - } - $prices[$product->getId()] = - [ - 'baseOldPrice' => [ - 'amount' => $this->localeFormat->getNumber( - $priceInfo->getPrice('regular_price')->getAmount()->getBaseAmount() - ), - ], - 'oldPrice' => [ - 'amount' => $this->localeFormat->getNumber( - $priceInfo->getPrice('regular_price')->getAmount()->getValue() - ), - ], - 'basePrice' => [ - 'amount' => $this->localeFormat->getNumber( - $priceInfo->getPrice('final_price')->getAmount()->getBaseAmount() - ), - ], - 'finalPrice' => [ - 'amount' => $this->localeFormat->getNumber( - $priceInfo->getPrice('final_price')->getAmount()->getValue() - ), - ], - 'tierPrices' => $tierPrices, - 'msrpPrice' => [ - 'amount' => $this->localeFormat->getNumber( - $this->priceCurrency->convertAndRound($product->getMsrp()) - ), - ], - ]; + $prices[$product->getId()] = [ + 'baseOldPrice' => [ + 'amount' => $this->localeFormat->getNumber( + $priceInfo->getPrice('regular_price')->getAmount()->getBaseAmount() + ), + ], + 'oldPrice' => [ + 'amount' => $this->localeFormat->getNumber( + $priceInfo->getPrice('regular_price')->getAmount()->getValue() + ), + ], + 'basePrice' => [ + 'amount' => $this->localeFormat->getNumber( + $priceInfo->getPrice('final_price')->getAmount()->getBaseAmount() + ), + ], + 'finalPrice' => [ + 'amount' => $this->localeFormat->getNumber( + $priceInfo->getPrice('final_price')->getAmount()->getValue() + ), + ], + 'tierPrices' => $this->getTierPricesByProduct($product), + 'msrpPrice' => [ + 'amount' => $this->localeFormat->getNumber( + $this->priceCurrency->convertAndRound($product->getMsrp()) + ), + ], + ]; } + return $prices; } + /** + * Returns product's tier prices list + * + * @param ProductInterface $product + * @return array + */ + private function getTierPricesByProduct(ProductInterface $product): array + { + $tierPrices = []; + $tierPriceModel = $product->getPriceInfo()->getPrice('tier_price'); + foreach ($tierPriceModel->getTierPriceList() as $tierPrice) { + $tierPriceData = [ + 'qty' => $this->localeFormat->getNumber($tierPrice['price_qty']), + 'price' => $this->localeFormat->getNumber($tierPrice['price']->getValue()), + 'percentage' => $this->localeFormat->getNumber( + $tierPriceModel->getSavePercent($tierPrice['price']) + ), + ]; + + if (isset($tierPrice['excl_tax_price'])) { + $excludingTax = $tierPrice['excl_tax_price']; + $tierPriceData['excl_tax_price'] = $this->localeFormat->getNumber($excludingTax->getBaseAmount()); + } + $tierPrices[] = $tierPriceData; + } + + return $tierPrices; + } + /** * Replace ',' on '.' for js * diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontAssertExcludingTierPriceActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontAssertExcludingTierPriceActionGroup.xml new file mode 100644 index 0000000000000..d609fb0346900 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontAssertExcludingTierPriceActionGroup.xml @@ -0,0 +1,25 @@ + + + + + + + Assert product item tier price excluding price. + + + + + + + + {{excludingPrice}} + tierPriceExcluding + + + diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 37c129dc3bbde..460040431bb97 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -23,5 +23,6 @@ + diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductWithTierPriceWithTaxTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductWithTierPriceWithTaxTest.xml new file mode 100644 index 0000000000000..bbd72bbaa02da --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductWithTierPriceWithTaxTest.xml @@ -0,0 +1,126 @@ + + + + + + + + + <description value="Create configurable product with tier price and check excluding tax item price"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37863"/> + <group value="ConfigurableProduct"/> + </annotations> + <before> + <createData entity="productAttributeWithTwoOptionsNotVisible" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOptionOne"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOptionTwo"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOptionOne"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOptionTwo"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <createData entity="ApiSimpleOne" stepKey="createFirstSimpleProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOptionOne"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createSecondSimpleProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOptionTwo"/> + </createData> + + <createData entity="tierProductPrice" stepKey="addTierPrice"> + <requiredEntity createDataKey="createFirstSimpleProduct" /> + </createData> + + <createData entity="CustomerEntityOne" stepKey="createCustomer"/> + <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> + + <magentoCLI command="config:set tax/display/type 3" stepKey="enableShowIncludingExcludingTax"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogOut"/> + <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> + + <magentoCLI command="config:set tax/display/type 0" stepKey="disableShowIncludingExcludingTax"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </after> + + <!-- Create configurable product --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> + <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Fill configurable product values --> + <actionGroup ref="FillMainProductFormActionGroup" stepKey="fillConfigurableProductValues"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Create product configurations and add attribute and select all options --> + <actionGroup ref="GenerateConfigurationsByAttributeCodeActionGroup" stepKey="generateConfigurationsByAttributeCode"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + + <!-- Add associated products to configurations grid --> + <actionGroup ref="AddProductToConfigurationsGridActionGroup" stepKey="addFirstSimpleProduct"> + <argument name="sku" value="$$createFirstSimpleProduct.sku$$"/> + <argument name="name" value="$$createConfigProductAttributeOptionOne.option[store_labels][1][label]$$"/> + </actionGroup> + <actionGroup ref="AddProductToConfigurationsGridActionGroup" stepKey="addSecondSimpleProduct"> + <argument name="sku" value="$$createSecondSimpleProduct.sku$$"/> + <argument name="name" value="$$createConfigProductAttributeOptionTwo.option[store_labels][1][label]$$"/> + </actionGroup> + + <!-- Save configurable product --> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveConfigurableProduct"/> + + <!--Login customer on storefront--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + + <!-- Assert product tier price on product page --> + <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <selectOption userInput="$$createConfigProductAttributeOptionOne.option[store_labels][1][label]$$" + selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" + stepKey="selectOption"/> + + <!-- Assert tier price excluding including price item --> + <actionGroup ref="AssertStorefrontProductDetailPageTierPriceActionGroup" stepKey="assertProductTierPriceExcludingIncludingTax"> + <argument name="tierProductPriceDiscountQuantity" value="2"/> + <argument name="productPriceWithAppliedTierPriceDiscount" value="97.43 ${{tierProductPrice.price}}"/> + <argument name="productSavedPricePercent" value="27"/> + </actionGroup> + <actionGroup ref="StorefrontAssertExcludingTierPriceActionGroup" stepKey="assertTierPriceExcludingPrice"> + <argument name="excludingPrice" value="${{tierProductPrice.price}}" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml b/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml index c68419b955e6d..7ce1fd2ccb451 100644 --- a/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml +++ b/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml @@ -6,22 +6,34 @@ ?> <script type="text/x-magento-template" id="tier-prices-template"> <ul class="prices-tier items"> + <% var exclPrice = ' <span class="price-wrapper price-excluding-tax"' + + 'data-label="<?= $block->escapeHtml(__('Excl. Tax')) ?>">' + + '<span class="price"> %1</span>' + + '</span>' + %> + <% _.each(tierPrices, function(item, key) { %> - <% var priceStr = '<span class="price-container price-tier_price">' - + '<span data-price-amount="' + priceUtils.formatPrice(item.price, currencyFormat) + '"' - + ' data-price-type=""' + ' class="price-wrapper ">' - + '<span class="price">' + priceUtils.formatPrice(item.price, currencyFormat) + '</span>' - + '</span>' - + '</span>'; %> - <li class="item"> - <%= '<?= $block->escapeHtml(__('Buy %1 for %2 each and', '%1', '%2')) ?>' - .replace('%1', item.qty) - .replace('%2', priceStr) %> - <strong class="benefit"> - <?= $block->escapeHtml(__('save')) ?><span + <% var itemExclPrice = item.hasOwnProperty('excl_tax_price') + ? exclPrice.replace('%1', priceUtils.formatPrice(item['excl_tax_price'], currencyFormat)) + : '' + %> + + <% var priceStr = '<span class="price-container price-tier_price">' + + '<span data-price-amount="' + priceUtils.formatPrice(item.price, currencyFormat) + '"' + + ' data-price-type=""' + ' class="price-wrapper price-including-tax">' + + '<span class="price">' + priceUtils.formatPrice(item.price, currencyFormat) + '</span>' + + '</span>' + itemExclPrice + '</span>'; + %> + <li class="item"> + <%= '<?= $block->escapeHtml(__('Buy %1 for %2 each and', '%1', '%2')) ?>' + .replace('%1', item.qty) + .replace('%2', priceStr) + %> + <strong class="benefit"> + <?= $block->escapeHtml(__('save')) ?><span class="percent tier-<%= key %>"> <%= item.percentage %></span>% - </strong> - </li> + </strong> + </li> <% }); %> </ul> </script>