How to Collect Totals In Magento 2 Application

Likes (1)   Dislikes (0)  
  (1 reviews)

For a merchant the default Magento business implementation is not always enough. They always demand something that can add to their revenueđź’°. As a developer we should have this knowledge to implement or showcase them the way Magento 2 adds additional costsđź’˛on top of the existing features. In this tutorial we will learn about the way Magento 2 implements the “Collect Totals” and how we can intercept into this process to add our custom charges for our client’s business requirement.

What is Collect Totals in Magento?

The “Collect Totals” is a complex feature in Magento 2 that accumulates all different sorts of charges to make the grand total. Basic different charges includes Shipping Charges/Tax etc. These are added to the SubTotal Amount to calculate the Grand Total to Pay for the Order.

SubTotal + Shipping + Tax = Grand Total

Whenever any changes made to the cart this calculation process gets triggered and re-calculates the grand total again to reflect the correct amount to pay on the cart or the checkout page. And, this is called Quote Totals Collection because this happens on the cart/quote entity in Magento Framework.

Magento has following 3 types of Collect Totals.

  1. Quote Totals
  2. Invoice Totals
  3. Credit Memo Totals

We just talked about the quote totals. Let’s know about the other two. The invoice totals is collected after an order is placed to send the order invoice mail to customer. This also gets calculated while in the order details page and invoice page in Magento 2 adminhtml area. Similarly the most neglected credit memo collect totals happens when the merchant initiates a refund process in Magento 2 admin area.

Why Collect Totals Is Important From A Developers Point Of View?

We already discussed a lot about this but still it is all about the client’s requirement. This is a most demanding feature in any e-commerce application. As a developer we have to fulfill their requirement and make them happy. If we do not know exactly how this is implemented in core Magento then it is gonna be an embarrassing moment for us. So having this idea under our belt is always a plus point. We could positively say “Yes, I can implement this for you” instead of saying “Yeah! I have to look into this and do some RnD”.

How Magento 2 Collects Totals For Different Entities?

It’s all starts from a configuration file like any other feature in Magento 2. Yeah! I know Magento is explicit and configuration based and Magento 2 is yet more configuration based. So its all starts with the sales.xml configuration file in Magento 2. Checkout the below core file XML configuration syntax. Some internal parts are removed so that you can go ahead and check the same file in your own Magento 2 instance.

vendor/magento/module_sales/etc/sales.xml
<?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="order_invoice">
        <group name="totals">
            <item name="subtotal" instance="Magento\Sales\Model\Order\Invoice\Total\Subtotal" sort_order="50"/>
            ...
        </group>
    </section>
    <section name="order_creditmemo">
        <group name="totals">
            ...
        </group>
    </section>
    ...
</config>

Use of Object Pool Design Pattern

Here Magento 2 uses the Object Pool Design Pattern. Inside each sections tag there is an XML child named group. Group contains another level of nested child item tag. The item tag has a unique name attribute and an instance attribute with the fully qualified class names to be instantiated while running the pool.

The Object Pool Design Pattern is used at places where we do not have any prior idea about what things/objects of similar type (implementing the same interface) can come into the picture and be a part of the current context.

Magento 2 configuration reader library comes handy to read this XML configuration and feed the list of the participant items to the Object Pool Pattern. Usage of this beautiful design pattern makes a scope for us as a developer to dive straight into the pool and add our own items over there in our custom module sales.xml.

As we all know that all the XML configuration files are merged at the Magento startup process (the old school grammar from Magento 1), hence our custom XML will also be merged and pushed to the pool.

The section names are the key words to put our custom items to the collect totals pool I must say. Each section have a different purpose, I mean it collects totals for a different entity. Hence, all the classes defined in the pool extends different classes.

Section NameUsed For Collecting Totals For EntityClasses in Pool extends from
order_invoiceInvoice\Magento\Sales\Model\Order\Total\AbstractTotal
order_creditmemoCredit Memo\Magento\Sales\Model\Order\Creditmemo\Total\AbstractTotal
quoteQuote\Magento\Quote\Model\Quote\Address\Total\AbstractTotal

The totals section for quote can be found in vendor\magento\module-quote\etc\sales.xml.

All the abstract classes have one method named “collect” but each of them have different function signature.

How To Add Your Custom line Item Collect Totals In Magento 2 Application?

I am not going to explain how to add a custom line item in your cart totals section UI or in checkout totals section UI. I am just going to explain how this additional line item in the UI can end up in calculating totals for the UI section update.

Let’s say we have a requirement from client (a liquor shop owner) that we need to add an additional totals line item that says “Fragile Care”. Obviously wine bottles should be handled with care during transit and this is a legit requirement for this shop.

For demo purpose let’s say we have to add a fixed amount of 3.7$ per cart/order. And this is optional, customer may or may not pay this additional charge. Let’s see how could we add custom code for this requirement.

Collecting Custom Quote Totals

Collecting custom quote totals requires you to create a sales.xml file in your custom module and then declaring the totals object pool items in that file. After declaring the configuration you need to create appropriate classes as per your declaration.

Declaring custom quote totals collector class in the object pool XML configuration

In a custom module we will define a sales.xml file with the following content in it. Of course you have to change this XML configuration according to your requirements. Basically you need to change the name and the instance attribute values.

app/code/Training/Core/etc/sales.xml
<?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="fragile_care" instance="Training\Core\Model\Quote\Address\Total\FragileCare" sort_order="110"/>
        </group>
    </section>
</config>

If you notice, there is a sort order attribute for each of the totals item. This sort order defines the processing sequence number for the item in the pool. In the core Magento quote totals pool the subtotal comes at sort order 100. We have added our custom collect totals pool item at 110 sort order.

The importance of the sort order.

Sort order for the pool item is extremely important. Let’s say your store also supports tax and may be you decided for giving some discount. In that case you have to think carefully wheather you want your additional charge/cost to be considered for tax calculation or it is tax free. Similarly you may wish to deside wheather to give discount or not on the additional charge that you are applying to cart total.

If you want your additional charge to be taxable then you have to give that pool line item a lower sort order number. And if you don’t want that then you can give your pool line item a higher sort order number than tax line item in the pool. Similar is the case with discount.

Refer to this core class \Magento\Quote\Model\Quote\Address\Total\Collector and look for the method named “getCollectors” and it’s definition. Look for the doc block that says “Get total models array ordered for right calculation logic”.

vendor/magento/module-quote/Model/Quote/Address/Total/Collector.php
/**
 * Get total models array ordered for right calculation logic
 *
 * @return array
 */
public function getCollectors()
{
    return $this->_collectors;
}

Initialisation of All Quote Totals Collector Classes

Now, how this list of collectors gets initialised? For that look for the constructor of the same class that says something like bellow code block. This is something that you don’t have to code. It is already implemented by Magento core.

vendor/magento/module-quote/Model/Quote/Address/Total/Collector.php
$this->_initModels()->_initCollectors()->_initRetrievers();

The bellow code block is the actual implementation of the logic that reads the configuration and stores in cache for later use and provide the same to the class that calls it. Yes, caching is used here, because reading the XML configuration from the file for each and every request is slow.

vendor/magento/module-sales/Model/Config/Ordered.php
/**
 * Initialize collectors array.
 * Collectors array is array of total models ordered based on configuration settings
 *
 * @return $this
 */
protected function _initCollectors()
{
    $sortedCodes = [];
    $cachedData = $this->_configCacheType->load($this->_collectorsCacheKey);
    if ($cachedData) {
        $sortedCodes = $this->serializer->unserialize($cachedData);
    }
    if (!$sortedCodes) {
        $sortedCodes = $this->_getSortedCollectorCodes($this->_modelsConfig);
        $this->_configCacheType->save($this->serializer->serialize($sortedCodes), $this->_collectorsCacheKey);
    }
    foreach ($sortedCodes as $code) {
        $this->_collectors[$code] = $this->_models[$code];
    }

    return $this;
}

Looping over the sorted collect total items and executing them all in the order

Let’s see how the Object Pool Pattern loops over the Pool Items and executes them. Refer to the core file given in bellow code block. This is also implemented by Magento core and you don’t have to do anything here. This explanation is just for understanding purpose.

vendor/magento/module-quote/Model/Quote/TotalsCollector.php
foreach ($this->collectorList->getCollectors($quote->getStoreId()) as $collector) {
    /** @var CollectorInterface $collector */
    $collector->collect($quote, $shippingAssignment, $total);
}

Defining the custom quote total collector class

Finally, of course we have to define our instance Training\Core\Model\Quote\Address\Total\FragileCare.

Training\Core\Model\Quote\Address\Total\FragileCare
/**
 * Collect Fragile Care Fee For Quote
 *
 * @param \Magento\Quote\Model\Quote $quote
 * @param \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment
 * @param Address\Total $total
 * @return $this
 */
public function collect(
    \Magento\Quote\Model\Quote $quote,
    \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment,
    \Magento\Quote\Model\Quote\Address\Total $total
) {
    parent::collect($quote, $shippingAssignment, $total);
   
    $code = 'fragile_care';
    $amount = 3.7;
    $total->setTotalAmount($code, $amount);
    $total->setBaseTotalAmount($code, $amount);
    
    return $this;
}

SubTotal + Fragile Care + Shipping + Tax = Grand Total

Magento2 Quote Collect Totals
Quote Totals Collected 3.7$

The total object passed here in the 3rd argument is an in-memory data object which stores all sorts of total amounts and base total amounts in an internal array.

vendor/magento/module-quote/Model/Quote/Address/Total.php
<?php
namespace Magento\Quote\Model\Quote\Address;

class Total extends \Magento\Framework\DataObject
{
    /**
     * @var array
     */
    protected $totalAmounts = [];

    /**
     * @var array
     */
    protected $baseTotalAmounts = [];
    
    ...
    ...

The grand total collector is the last item in the object pool list as per the sort order number. This class is responsible for accumulating all of those totals data stored in the total data object and sums them up to get grand total value. Remember not to put any custom totals collection configuration sort order higher than grand total collector. If you do so then the totals will be collected but not included in the grand total.

vendor/magento/module-quote/Model/Quote/Address/Total/Grand.php
/**
 * Collect grand total address amount
 *
 * @param Quote $quote
 * @param ShippingAssignment $shippingAssignment
 * @param Total $total
 * @return Grand
 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
 */
public function collect(Quote $quote, ShippingAssignment $shippingAssignment, Total $total): Grand
{
    $totals = array_sum($total->getAllTotalAmounts());
    $baseTotals = array_sum($total->getAllBaseTotalAmounts());
    $grandTotal = $this->priceRounder->roundPrice($total->getGrandTotal() + $totals, 4);
    $baseGrandTotal = $this->priceRounder->roundPrice($total->getBaseGrandTotal() + $baseTotals, 4);

    $total->setGrandTotal($grandTotal);
    $total->setBaseGrandTotal($baseGrandTotal);
    return $this;
}


Collecting Custom Invoice Totals

Again, the process is similar to the quote totals. There exists the same Object Pool Design Pattern and we have to add our custom class instance to the pool in a specific sort order. Just find the core XML configuration file and copy it over to your module and make required changes like given bellow. Also don’t forget to define your instance class at proper place.

app/code/Training/Core/etc/sales.xml
<?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="order_invoice">
        <group name="totals">
            <item name="fragile_care" instance="Training\Core\Model\Order\Invoice\Total\FragileCare" sort_order="60"/>
        </group>
    </section>
    ...

Class definition for Invoice Collect Totals.

PHP
/**
 * Collect Fragile Care Fee For Invoice
 *
 * @param \Magento\Sales\Model\Order\Invoice $invoice
 * @return $this
 */
public function collect(\Magento\Sales\Model\Order\Invoice $invoice)
{
    $amount = 3.7;
    $invoice->setGrandTotal($invoice->getGrandTotal() + $amount);
    $invoice->setBaseGrandTotal($invoice->getBaseGrandTotal() + $amount);
    return $this;
}

Invoice Totals Collected 3.7$

Collecting Custom Credit Memo Totals

XML
<section name="order_creditmemo">
    <group name="totals">
         <item name="fragile_care" instance="Training\Core\Model\Order\Creditmemo\Total\FragileCare" sort_order="60"/>
    </group>
</section>

Class definition of credit memo collect totals.

PHP
/**
 * Collect Fragile Care Fee For Credit Memo
 *
 * @param \Magento\Sales\Model\Order\Creditmemo $creditmemo
 * @return $this
 */
public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo)
{
    $amount = 3.7;
    $creditmemo->setGrandTotal($creditmemo->getGrandTotal() + $amount);
    $creditmemo->setBaseGrandTotal($creditmemo->getBaseGrandTotal() + $amount);
    return $this;
}

Conclusion

This technique can also be used for giving discount. Remember this is just for the calculation part of the totals. This exercise will not help you adding additional line items in totals area UI.

1 thought on “How to Collect Totals In Magento 2 Application”

  1. Your explanation of the topic is truly commendable. Your efforts in simplifying the complex concepts are highly appreciated. I would like to suggest an idea that could further enhance the value of your explanation. It would be great if you could demonstrate how to display the total row in both the cart total section and order level. This would undoubtedly provide an even more comprehensive understanding of the topic. Thank you for your excellent work and dedication to improving the quality of educational content.

    Reply

Leave a Comment

Share via
Copy link
Powered by Social Snap