📜 ⬆️ ⬇️

Immersion in pricing Magento 2, we remove pennies after discounts


Pricing is perhaps Magento's dignity and the most interesting part of the system.
And for the owner of the store - the most important part, since it is associated with money.
Previously, colleagues were drawing diagrams that could barely fit on the Chinese Wall, trying to fit all the calculation steps. In this article I will try to present only the main stages of calculation, and an example of rounding discounts in favor of the store. Fortunately, compared to Magento 1, innovations touched the deepest, the approach remained unchanged.

The tip of the iceberg


image

When the customer changes the contents of the basket the calculation starts. The speed of calculation depends on a variety of actions "at depth". Let's start the dive with prominent places. at the same time, we will see events and dependencies of the types of goods, delivery methods, price rules of the basket and the catalog.

The article describes the correct pricing approach for the following modules / integrations:
')

We proceed immediately to the calculation, since the formation of the rows of the basket when adding goods is a separate topic, it will be possible in the following articles.
The text will contain Total, Price, Carrier models, they denote a certain type, and then it is easier to refer to.

\ Magento \ Quote \ Model \ Quote :: collectTotals


The journey begins where we go and start calculating.
We ask TotalsCollector to do the calculation, this class was specially separated from the basket, so as not to add more lines to the code.

\ Magento \ Quote \ Model \ Quote \ TotalsCollector :: collect


Where we go to all addresses and ask for the addresses to calculate for all addresses.
This is done for the possibility of placing orders at multiple addresses at once, since this is one of the useful functions for B2B stores that have a centralized purchasing department and orders go "wholesale" but at different places at once.

\ Magento \ Quote \ Model \ Quote \ TotalsCollector :: collectAddressTotals


We ask that we have a responsible TotalsCollectorList class that returns us all the stages of the calculation. All stages are in configuration, ordered. At the end, we’ll look at our little pricing modifier.

\ Magento \ Quote \ Model \ Quote \ TotalsCollectorList :: getCollectors


The result of the execution is an array of classes CollectorInterface, which implements the logic of calculating the cost.

All stages of the calculation are declared for the main entities that are important when calculating the cost: baskets, bills, returns. There are always good examples in the system kernel: vendor / magento / module-sales / etc / sales.xml

The following describes the addition of calculation steps for the order_invoice and order_creditmemo invoice (invoice) and order_creditmemo refund.

In addition, available_product_type (available types of goods for purchase) are added. In modules of specific types of goods types of goods are declared.

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Sales:etc/sales.xsd"> <section name="order_invoice"> <group name="totals"> <item name="subtotal" instance="Magento\Sales\Model\Order\Invoice\Total\Subtotal" sort_order="50"/> <item name="discount" instance="Magento\Sales\Model\Order\Invoice\Total\Discount" sort_order="100"/> <item name="shipping" instance="Magento\Sales\Model\Order\Invoice\Total\Shipping" sort_order="150"/> <item name="tax" instance="Magento\Sales\Model\Order\Invoice\Total\Tax" sort_order="200"/> <item name="cost_total" instance="Magento\Sales\Model\Order\Invoice\Total\Cost" sort_order="250"/> <item name="grand_total" instance="Magento\Sales\Model\Order\Invoice\Total\Grand" sort_order="350"/> </group> </section> <section name="order_creditmemo"> <group name="totals"> <item name="subtotal" instance="Magento\Sales\Model\Order\Creditmemo\Total\Subtotal" sort_order="50"/> <item name="discount" instance="Magento\Sales\Model\Order\Creditmemo\Total\Discount" sort_order="150"/> <item name="shipping" instance="Magento\Sales\Model\Order\Creditmemo\Total\Shipping" sort_order="200"/> <item name="tax" instance="Magento\Sales\Model\Order\Creditmemo\Total\Tax" sort_order="250"/> <item name="cost_total" instance="Magento\Sales\Model\Order\Creditmemo\Total\Cost" sort_order="300"/> <item name="grand_total" instance="Magento\Sales\Model\Order\Creditmemo\Total\Grand" sort_order="400"/> </group> </section> <order> <available_product_type name="simple"/> <available_product_type name="virtual"/> </order> </config> 

Below is a list of names and classes of total-models for a basket:

1. subtotal => \ Magento \ Quote \ Model \ Quote \ Address \ Total \ Subtotal
The calculation of the value of goods before tax discounts and other things.
2. tax_subtotal => \ Magento \ Tax \ Model \ Sales \ Total \ Quote \ Subtotal
Taxes are part of the tax
3. weee => \ Magento \ Weee \ Model \ Total \ Quote \ Weee,
Fixed taxes, excise taxes
4. shipping => \ Magento \ Quote \ Model \ Quote \ Address \ Total \ Shipping
Calculating the cost of delivery, contacting delivery services for online calculation
5. tax_shipping => \ Magento \ Tax \ Model \ Sales \ Total \ Quote \ Shipping
Taxes on delivery, delivery can also be taxed and / or for accounting it is required.
6. discount => \ Magento \ SalesRule \ Model \ Quote \ Discount,
Processing discount rules, applying coupons, promotions, “weather” discounts
7. tax => \ Magento \ Tax \ Model \ Sales \ Total \ Quote \ Tax
Another step in the calculation of taxes, as the discount under the law may not reduce the tax base.
8. weee_tax => \ Magento \ Weee \ Model \ Total \ Quote \ WeeeTax,
Fixed Taxes Another Stage
9. grand_total => \ Magento \ Quote \ Model \ Quote \ Address \ Total \ Grand
The total counting summarizes all that is considered before.

Under water


The most interesting elements are in subtotal, shipping, discount .

\ Magento \ Quote \ Model \ Quote \ Address \ Total \ Subtotal


image

And so, to get the cost of the goods Subtotal asks the product to give him the final price.

But the product itself does not know its price, it goes to its Price- model.
Work Price models is a whole topic for a separate article "How to create your type of product."
But this is already enough to redefine the primary price of any product, it may be part of the simplest integration with personal prices for the client, where all prices are stored in a simple table, perhaps they are loaded there once a day.

\ Magento \ SalesRule \ Model \ Quote \ Discount


image

Discounts are another interesting place where the basket is checked for whether or not a discount can be used. Adding special rules (for example, a discount on the weather in the city) for verification deserves a separate article.

The system checks all active discount rules for the current date. If there are a lot of rules, it can slow down the basket recalculation. Everything will be fine if the rules to

\ Magento \ Quote \ Model \ Quote \ Address \ Total \ Shipping


image

The cost of shipping is calculated by circumventing all shipping methods and calling
\ Magento \ Shipping \ Model \ Carrier \ AbstractCarrierInterface :: collectRates The shipping calculation is saved in the database and only happens for the specified delivery country.

By default, you can set the delivery method and country of delivery to calculate the cost of the order immediately after adding the product to the cart. This approach can be used if in a store we get data about a city or region by IP in some way.

Your calculation is correct


Module \ Project \ Integration


How to create your module colleagues wrote earlier on Habré here .
We will begin to introduce our own recalculation of discounts for order lines, we will remove pennies after calculating discounts.

It is convenient when we have all the goods have prices without kopecks, and a penny from the discounts only hinders us (when calculating the VAT).

In the Project / Integration / etc / sales.xml file we can add our Total-model, or remove the old / unnecessary weee.

sort_order - provides the order of execution, for all Total-models in sales.xml it is also set.

 <?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Sales:etc/sales.xsd"> <section name="quote"> <group name="totals"> <item name="integration_total" instance="Project\Integration\Model\Quote\Address\Total\Custom" sort_order="430"/> <item name="weee" instance="" /> <item name="weee_tax" instance="" /> </group> </section> </config> 

sort_order = "430" - declares the calculation of discounts and before the calculation of taxes.
This is the place where it is best to cut a penny from the discount or to make a request to the cart discount calculation system.

The implementation of the calculation in \ Project \ Integration \ Model \ Quote \ Address \ Total \ Custom

 <?php namespace Project\Integration\Model\Quote\Address\Total; //    class Custom extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal { //    ... public function __construct( //      DI ... ) { //     ... } public function collect( \Magento\Quote\Model\Quote $quote, \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment, \Magento\Quote\Model\Quote\Address\Total $total ) { $address = $shippingAssignment->getShipping()->getAddress(); $quoteItems = $quote->getAllItems(); //       ,    ,   ,  - if ($total->getTotalAmount('discount') == 0 || $quote->getItemsCount() == 0 || !$quote->getId() || $address->getAddressType() == 'billing') return $this; //      ,     ... //     $totalDiscount = 0; $baseTotalDiscount = 0; foreach ($quoteItems as $item) { //  ,     $newDiscountAmount = (int)$item->getDiscountAmount(); $newBaseDiscountAmount = (int)$item->getBaseDiscountAmount(); //     $totalDiscount += $newDiscountAmount; $baseTotalDiscount += $newBaseDiscountAmount; //    $rowTotal = $item->getRowTotal() + $item->getDiscountAmount() - $newDiscountAmount; $baseRowTotal = $item->getBaseRowTotal() + $item->getBaseDiscountAmount() - $newBaseDiscountAmount; //    $item->setDiscountAmount($newDiscountAmount); $item->setBaseDiscountAmount($newBaseDiscountAmount); //     $item->setRowTotal($rowTotal); $item->setBaseRowTotal($baseRowTotal); } //   $total->setTotalAmount('discount', $totalDiscount); $total->setBaseTotalAmount('discount', $baseTotalDiscount); $total->setSubtotalWithDiscount($total->getSubtotal() + $total->getDiscountAmount()); $total->setBaseSubtotalWithDiscount($total->getBaseSubtotal() + $total->getBaseDiscountAmount()); return $this; } } 

Instead of ending
Here we successfully threw a penny. No cents, no problem.

If we use additional price modifiers (our special discounts or surcharges, taxes on Internet Explorer), then we need to worry that all calculations are correct in bills and returns, otherwise you will turn into serial programmers who kill accountants. It is optimal to modify the discount or the base value of the goods to ensure the integrity of the amounts even under the conditions of returns.

Source: https://habr.com/ru/post/336934/


All Articles