Unravelling Magento’s collectTotals: The Core Process

Previous entries

In this series, we’re looking at Magento’s process for calculating and displaying totals for a cart. In our introduction, we briefly covered what a “total” or “total collector” is and saw how one is defined in config.xml. Now it’s time to take a closer look at the models and data structure involved. The job of a total collector model can be divided into two processes: Calculation and display.

You’re highly encouraged to open up the Magento codebase and follow along with the referenced code.

The collectTotals process

As mentioned in the previous article, the calculation of cart totals is kicked off with a call to Mage_Sales_Model_Quote::collectTotals. Before taking a look at the code, there are a few things to take note of about the underlying data structure for totals:

  • While it’s not required, many totals have database fields dedicated to them – usually “totalname_amount” and “base_totalname_amount” for the store and base currencies. (Ex., “gift_cards_amount” and “base_gift_cards_amount”)
  • Since totals calculations are done on each individual quote address, these fields are typically in sales_flat_quote_address, and there are methods for automatically adding to these amounts and incorporating them into the quote address’s total.
  • It’s not uncommon for fields of the same name to exist in sales_flat_quote_address_item and be managed directly by the total collector.
  • There are no restrictions on what values a particular total collector can modify. Example: The “subtotal” collector modifies several values, including “price” and “row_total” on quote items.
  • sales_flat_quote does contain “subtotal” and “grand_total,” which are special in that collectTotals automatically takes care of populating them after all total collectors have run.

With that out of the way, it’s time to examine the general flow of execution for collectTotals. collectTotals is run at various points, almost always immediately before the quote is saved. Examples would include during a visit to the cart page, upon the submission of each step during the checkout, and immediately before final order placement. Here’s a truncated version of collectTotals, with the main areas of code we’re concerned with:

    public function collectTotals()
    {
        /**
         * Protect double totals collection
         */
        if ($this->getTotalsCollectedFlag()) {
            return $this;
        }

        . . .

        foreach ($this->getAllAddresses() as $address) {
            $address->setSubtotal(0);
            $address->setBaseSubtotal(0);

            $address->setGrandTotal(0);
            $address->setBaseGrandTotal(0);

            $address->collectTotals();

            $this->setSubtotal((float) $this->getSubtotal() 
                + $address->getSubtotal());
            $this->setBaseSubtotal((float) $this->getBaseSubtotal() 
                + $address->getBaseSubtotal());

            $this->setSubtotalWithDiscount(
                (float) $this->getSubtotalWithDiscount() 
                    + $address->getSubtotalWithDiscount()
            );
            $this->setBaseSubtotalWithDiscount(
                (float) $this->getBaseSubtotalWithDiscount() 
                    + $address->getBaseSubtotalWithDiscount()
            );

            $this->setGrandTotal((float) $this->getGrandTotal() 
                + $address->getGrandTotal());
            $this->setBaseGrandTotal((float) $this->getBaseGrandTotal() 
                + $address->getBaseGrandTotal());
        }

        . . .

        $this->setTotalsCollectedFlag(true);
        return $this;
    }

The parts I’ve truncated deal with dispatching events, zeroing out values before the process begins, validation, and setting item quantities on the quote. Here’s an explanation of the main collection process:

  • Mage_Sales_Model_Quote::collectTotals loops through each quote address and calls Mage_Sales_Model_Quote_Address::collectTotals.
  • In collectTotals for an address, getTotalCollector()->getCollectors is used to get a sorted array of the defined total models and loop through them.
  • The “collect” method is run on each total model (all of which extend Mage_Sales_Model_Quote_Address_Total_Abstract). This method accepts the address as a parameter and does the heavy lifting of totals calculation.
    • The logic almost always involves looping through the address’s quote items via _getAddressItems. The handy methods _addAmount and _addBaseAmount can be used to add to the value of a field on the address matching the name of the total collector (like “discount_amount”), which will also add to the grand total for the address.
    • Any modification to the values on the quoted address itself is done here as well.
  • In Mage_Sales_Model_Quote::collectTotals, subtotal and grand_total are accumulated from the totals on each address.
  • With few exceptions, the quote (and by extension its addresses and items) is immediately saved after collectTotals is run.

To re-emphasize, no price information exists on the quote at all until this process has run the first time. When a product is added to the cart, it is added without price info, and the appropriate price is looked up and applied in one of the total collectors.

It may seem strange that totals are collected for both shipping and billing addresses on a quote and then summed. First of all, it’s appropriate for calculations to be done in the context of a specific address, because address information may affect things like tax and shipping. But if the two addresses’ totals are added together, how can the result be correct? The key is in _getAddressItems on the total model, which will return items only for the relevant address. (Typically the shipping address, or the billing address on a virtual quote.) Thus you end up with $0 totals on the address for which no items are returned. And for multi-shipping, quote items are explicitly associated with one address or another, which makes the general collectTotals process work out nicely.

Displaying totals

The process for calculating totals on a quote may still seem a bit opaque, but if you spend some time examining the various total models defined in the core, the general functionality should begin to click. There’s one other key component to a total model, however: Displaying totals in the cart or checkout (or wherever). It’s not required for a total to be displayed, but totals like “subtotal,” “discount,” “tax” and “grand_total” should be itemized on the page.

You’ve seen that collectTotals on the quote model and “collect” on a total model are the key methods involved with calculating and saving info. For displaying, getTotals on the quote model and “fetch” on the total model comprise the main event. Here’s the basic process:

  • Mage_Checkout_Block_Cart_Totals is the block included in the layout. The corresponding template outputs the results of renderTotals on this block.
  • Mage_Checkout_Block_Cart_Totals::renderTotals loops through the results of the block’s own getTotals method, which in turn calls Mage_Sales_Model_Quote::getTotals.
  • As comes as no surprise at this point, a getTotals method is called on each quote address. And parallel with the process we saw with collection, getTotalCollector()->getRetrievers is used to get the sorted total models. (This is distinct from getCollectors. In this case, admin configuration can actually be used to dictate the order in which totals are displayed.)
  • The “fetch” method is called on each total model, once again with the address passed as a parameter.
    • Ultimately, this method calls addTotal on the quote address, passing an array with the total’s code, a display title, and the value to display.
    • What is ultimately stored on the quote address is an instance of Mage_Sales_Model_Quote_Address_Total, which is a Varien_Object with an added “merge” method to facilitate summing the values of two such objects.
  • As Mage_Sales_Model_Quote::getTotals loops through each address, it uses the “merge” method mentioned above to reconcile each address’s data for a particular total into one final set of information to display.
  • Finally, Mage_Checkout_Block_Cart_Totals::renderTotals calls renderTotal for each total, which instantiates the appropriate block (more on this in a moment) and returns the results of toHtml.

Exactly which block class is used to render a specific total is determined as follows:

  • A block can be defined directly in layout XML for any total. It should simply have the name “{totalcode}_total_renderer,” and this block will be used to render the corresponding total.
  • If no renderer is defined in layout, the next place checked is the global config XML. We’ve already seen where totals are defined (in the node path “global/sales/quote/totals”). The node for a specific total can define <renderer> in addition to <class>, <before> and <after>. The expected value is a block code. An example can be found in config.xml for Mage_Checkout, where the renderer “checkout/total_nominal” is specified for the “nominal” total.
  • If neither of the above defined a renderer, the default Mage_Checkout_Block_Total_Default is used.

And there you have it! The collection and display processes are as simple as that. All right, so “simple” may not be the word to describe them. But with the outlines above, you should be well equipped to examine the code of the various models that are involved and get a feel for the flow of execution.

A quick example

All of the native total collectors have their own quirks, complexities, and “exceptions” to the rules, so finding a straight-forward example is a challenge. To simplify as much as possible, I’ll present the following pseudo example of how the two key methods on a total work. Most totals follow the same basic process in some fashion.

class MySite_MyModule_Model_Sales_Quote_Address_Total_Mytotal 
    extends Mage_Sales_Model_Quote_Address_Total_Abstract
{
    public function __construct()
    {
        $this->setCode('mytotal');
    }
    
    public function collect(Mage_Sales_Model_Quote_Address $address)
    {
        parent::collect($address);
        
        foreach ($this->_getAddressItems($address) as $item) {
            // These two lines represent whatever logic you're 
            // using to calculate these amounts
            $baseAmt = ...
            $amt = ...
        
            // Set the item's total
            $item->setBaseMytotalAmount($baseAmt);
            $item->setMytotalAmount($amt);
        
            // These methods automatically take care of summing 
            // "mytotal_amount" on the quote address
            $this->_addBaseAmount($baseAmt);
            $this->_addAmount($amt);
        }
        return $this;
    }
    
    public function fetch(Mage_Sales_Model_Quote_Address $address)
    {
        // Naturally, this exists on the quote address because "collect" ran already
        $amt = $address->getMytotalAmount();
        
        if ($amt!=0) {
            $address->addTotal(array(
                    'code' => $this->getCode(),
                    'title' => Mage::helper('mysite_mymodule')->__('My Total'),
                    'value' => $amt
            ));
        }
        return $this;
    }
}

In the next part, we’ll discuss some final points that are worth consideration before Magento totals can be considered well and truly conquered.

Next entries

Share it

Topics

Related Posts

Google and Yahoo Have New Requirements for Email Senders

What ROAS Really Means

Everything You Need to Know About Updating to Google Analytics 4

Contact Us