The focus of this series is Magento’s process for calculating and displaying cart totals. We have taken a thorough look at the code involved with the core process, but there are a few important points left to cover.
Transferring totals to the order
What we’ve seen so far all relates to what’s happening when a cart is still in progress, and it makes sense that that’s where the truly hard work is done. But at some point (we hope), a cart will become an order. What happens then? There are several relevant points:
- Frequently, the same database fields we’ve seen in the quote tables (ex. “gift_cards_amount” and “base_gift_cards_amount”) will show up in the order tables as well.
- Where these fields are present in sales_flat_quote_item, you’ll most likely see them duplicated on sales_flat_order_item as well.
- As we’ve seen, the most common pattern is for a cumulative total to be stored on sales_flat_quote_address. But observing the order tables will reveal that most such fields have moved directly to sales_flat_order. This makes sense. During the cart process, it’s important for calculations to be done on an address level, since address info can affect things like tax and shipping. Once an order has been placed, all that’s relevant is the final total that was, in fact, applied.
- With the appropriate fields present on the order tables, making sure the data makes it from the quote to the order is actually accomplished very simply in a module’s config.xml (see below).
- The display of totals in an order confirmation email and in the admin order screens bears little resemblance to the process for displaying them in the cart. The block Mage_Sales_Block_Order_Totals and its descendants are involved.
In a quick examination of config.xml in Mage_Sales, you’ll see the node “fieldset” defined with a slew of information about the fields that should be copied from quote to order or vice versa. The following shows the info relevant to copying discount data:
<global> <fieldsets> . . . <sales_convert_quote_address> . . . <discount_amount> <to_order>*</to_order> </discount_amount> . . . <base_discount_amount> <to_order>*</to_order> </base_discount_amount> . . . </sales_convert_quote_address> . . . <sales_convert_quote_item> . . . <discount_amount> <to_order_item>*</to_order_item> </discount_amount> <base_discount_amount> <to_order_item>*</to_order_item> </base_discount_amount> . . . </sales_convert_quote_item> . . . </fieldsets> . . . </global>
That’s all there is to it. With no more work than that, totals data is copied from quote to order. Note the fields that are being copied from quote_address directly to order (rather than order_address).
Finally, on the subject of displaying order totals: The process is unfortunately not as graceful as we’ve seen in the cart (where the setting of display order in the admin, the fetch method on the total models, and the ability to set custom renderers afforded us a great deal of flexibility). Mage_Sales_Block_Order_Totals (used itself for totals in order confirmation emails) defines the method _initTotals, where total values are retrieved directly from the order and accumulated in an array. The choice of totals to be included is entirely hard-coded. Mage_Adminhtml_Block_Sales_Totals and its descendants (Mage_Adminhtml_Block_Sales_Order_Totals, Mage_Adminhtml_Block_Sales_Order_Creditmemo_Totals and Mage_Adminhtml_Block_Sales_Order_Invoice_Totals) make their own modifications to this method. For customization in this area, we’ll have to resort to block rewriting.
UPDATE: Thanks to Vinai Kopp for correcting me on that last part. He pointed out what I missed: Mage_Sales_Block_Order_Totals calls initTotals on any of its child blocks that have such a method, which can in turn grab their parent and call addTotal or addTotalBefore to modify the totals array, thus eliminating the need for block rewrites.
An unexpected caveat of collectTotals: Quote item caching
Obviously, we’ve taken a fairly high level view of the code involved with collectTotals. You’re highly encouraged to dig into the code yourself and discover the fine details. There is one such detail, however, that I feel is well worth covering here, to prevent it tripping you up.
Now that you understand what occurs during the totals collection process, you may find it convenient or necessary to call it directly yourself. Before you start feeling too confident with using collectTotals for your own purposes, though, keep the following rule in mind:
Products cannot be added to the quote after collectTotals is run!
. . . unless the quote addresses’ item caches are cleared.
Nearly every total model’s “collect” method relies on fetching the quote items from the address and looping through them. The first time getAllItems is run on a quote address, the item collection is actually cached with a unique key, and it’s this cached collection that is returned on subsequent calls.
This presents no problem for the quote items that already existed. Their totals will be processed and modified as usual. But new quote items that have been added aren’t part of the cached collection. The result: The new quote items will be saved all right, because they were successfully added to the quote’s real item collection, but you’ll find $0 in every field where you expect to see a calculated total, and you’ll pull your hair out trying to figure out why!
That concludes our exploration of the details of collectTotals. However, we’re not done yet! Since doing can be much more instructive than observing, the final part in this series will contain a complete walkthrough for adding a total collector of your own to your site.