Unravelling Magento’s collectTotals: Invoices and Credit Memos

Previous Entries

Quite some time ago (well over a year, in fact), I published a series of articles on Magento’s cart totals collection process (linked above), how it works, and how to customize it. The series provided a guide for introducing entirely new totals, but any intrepid developers trying out this procedure might have discovered a missing piece: While the previous articles offered a step-by-step for getting new totals into the cart and onto the final order, more effort is required for these totals to make it onto the order’s invoices and credit memos. If your order fulfillment process occurs within Magento, fancy custom totals can hardly be of any use without support for these components. Therefore, it’s about time to explore these final pieces.

Why Invoice and Credit Memo Collectors?

The chief complexities of the totals process we’ve looked at have been in the collector models that perform the necessary calculations while an order is still in the cart/checkout stage. Once a cart becomes an order, totals are just a series of numbers that need to be copied to that order.

One might expect the process to be just that simple – as simple as copying numbers – when an invoice or a credit memo is created. And if an order only ever produced a single invoice, this might be the case. Complexity enters in again, however, due to the ability to invoice or refund only a portion of an order. If I’m currently invoicing only 2 of the 5 items for an order, Magento needs to know how to calculate how much of each and every total should be part of the invoice. For the native shipping total, the entire amount is always included in the first invoice. For the tax total, however, the process is more complicated, requiring calculation to be done for specific invoiced items.

This level of complexity is starting to sound like what we faced in the cart process. And in a manner of speaking, that’s exactly what we have again – a “cart” being built toward the creation of an invoice or a credit memo rather than an order, and potentially involving different calculations. After all, we’re going to have to consider things like what amount has been invoiced previously. So you’ve probably guessed by this point what we’re dealing with: dedicated collector models just for this purpose.

A Familiar Process

Luckily, having peered under the hood of how to quote (i.e., cart) totals are declared and managed, we’re well equipped to understand invoice and credit memo totals because the process is much the same. Step one is to declare these totals and the models associated with them in configuration XML. Here are examples from the core:

    . . .
    
        
            
                . . .
                
                    sales/order_invoice_total_shipping
                    subtotal,discount
                    grand_total,tax
                
                . . .
            
        
        
            
                . . .
                
                    sales/order_creditmemo_total_shipping
                    subtotal,discount
                    grand_total,tax
                
                . . .
            
        
    
    . . .

And as with quotes and orders, the majority of totals will involve new fields to store the appropriate amounts on the invoice and credit memo tables, and in some cases, the invoice item and credit memo item tables as well. As an example, the fields “tax_amount” and “base_tax_amount” exist in the following tables:

  • sales_flat_invoice
  • sales_flat_invoice_item
  • sales_flat_creditmemo
  • sales_flat_creditmemo_item

The declared total models contain a “collect” method, just like their quote counterparts. The “fetch” method that was necessary for quote totals, however, isn’t present in this case. This is because the display process for invoice and credit memo totals cleaves much closer to that of orders than to the way things are handled in the cart. (I’ll delve into that shortly.)

A final note about the basic process of defining totals: We previously examined the use of the config node “fieldsets” to transfer amounts from quote to order. (The node “global/fieldsets/sales_convert_quote_address/shipping_amount/to_order” is responsible for a quote’s shipping total making it to the resulting order.) You won’t find the same procedure for order-to-invoice or order-to-credit-memo, though. This process will work; defining the node “global/fieldsets/sales_convert_order/shipping_amount/to_invoice” will indeed result in an order’s shipping amount being copied to every invoice created from it. This isn’t really what we want, though. To reiterate, the totals calculation for invoices and credit memos isn’t simply a matter of copying numbers; if I create three credit memos from the same order, I hardly want the order’s full tax amount refunded on each. (And doing so without a collector model still won’t successfully affect the credit memo’s grand total, as we’ll see.)

A note for clarity: Just as the collectTotals process is run each time we view a cart on the front-end, re-calculating all amounts with each change, the same is true when the creation of an invoice or credit memo is in progress. As the invoice is being prepared and updated (by the admin user changing quantities to be invoiced, for example), the collection process runs over and over to update the totals we see. It’s not a process that occurs only on the final creation of the invoice (or credit memo).

A Look at Displaying Totals

The block class Mage_Sales_Block_Order_Totals and its children, Mage_Sales_Block_Order_Invoice_Totals and Mage_Sales_Block_Order_Creditmemo_Totals, are responsible for the rendering of totals in order confirmation emails and the account section in the front-end. The related classes Mage_Adminhtml_Block_Sales_Order_Totals, Mage_Adminhtml_Block_Sales_Order_Invoice_Totals, and Mage_Adminhtml_Block_Sales_Order_Creditmemo_Totals take this job for the admin. Here is an example of a block declaration in a layout XML file:

<adminhtml_sales_order_view>
    . . .
    <reference name="left">
        <block type="adminhtml/sales_order_view_tabs" name="sales_order_tabs">
            <block type="adminhtml/sales_order_view_tab_info" name="order_tab_info" template="sales/order/view/tab/info.phtml">
                . . .
                <block type="adminhtml/sales_order_totals" name="order_totals" template="sales/order/totals.phtml">
                . . .
            </block>
        . . .
        </block>
    </reference>
    . . .
</adminhtml_sales_order_view>

In the previous entry “Orders and Caveats” in this series, I initially claimed that rewriting these blocks were required to successfully display custom totals at the order stage, only to backpedal on this thanks to some helpful advice from Vinai Kopp! Since the display of invoice and credit memo totals works the same way as orders, this provides us an excellent opportunity to go back and explore the better alternative.

Each of these blocks calls “initTotals” on any of its child blocks that have such a method. If we define such child blocks for custom totals, these classes can, in turn, grab their parent and call addTotal or addTotalBefore to modify the totals array, thus eliminating the need for block rewrites. Here’s an example of adding a custom total block to each parent within layout XML:

<adminhtml_sales_order_view>
    <reference name="order_totals">
        <block type="mymodule/sales_order_totals_mytotal" name="total_mytotal" />
    </reference>
</adminhtml_sales_order_view>

<adminhtml_sales_order_invoice_new>
    <reference name="invoice_totals">
        <block type="mymodule/sales_order_totals_mytotal" name="total_mytotal" />
    </reference>
</adminhtml_sales_order_invoice_new>

<adminhtml_sales_order_creditmemo_new>
    <reference name="creditmemo_totals">
        <block type="mymodule/sales_order_totals_mytotal" name="total_custom_mytotal" />
    </reference>
</adminhtml_sales_order_creditmemo_new>

And the example code for the block itself:

class Me_MyModule_Block_Sales_Order_Totals_Mytotal 
    extends Mage_Core_Block_Abstract
{
    public function getSource()
    {
        return $this-&gt;getParentBlock()-&gt;getSource();
    }

    /**
     * Add this total to parent
     */
    public function initTotals()
    {
        if ((float)$this-&gt;getSource()-&gt;getMyTotalAmount() == 0) {
            return $this;
        }
        $total = new Varien_Object(array(
            'code'  =&gt; 'mytotal',
            'field' =&gt; 'mytotal_amount',
            'value' =&gt; $this-&gt;getSource()-&gt;getMyTotalAmount(),
            'label' =&gt; $this-&gt;__('My Total')
        ));
        $this-&gt;getParentBlock()-&gt;addTotalBefore($total, 'shipping');
        return $this;
    }
}

The only cumbersome part is making sure to apply this update to all of the appropriate layout handles. Here is a list of relevant layout update handles in the front-end:

  • sales_order_view
  • sales_order_invoice
  • sales_order_creditmemo
  • sales_order_print
  • sales_order_printinvoice
  • sales_order_printcreditmemo
  • sales_email_order_items
  • sales_email_order_invoice_items
  • sales_email_order_creditmemo_items
  • sales_guest_view
  • sales_guest_invoice
  • sales_guest_creditmemo
  • sales_guest_print
  • sales_guest_printinvoice
  • sales_guest_printcreditmemo

And here are the relevant handles in admin:

  • adminhtml_sales_order_view
  • adminhtml_sales_order_invoice_new
  • adminhtml_sales_order_invoice_updateqty
  • adminhtml_sales_order_invoice_view
  • adminhtml_sales_order_creditmemo_new
  • adminhtml_sales_order_creditmemo_updateqty
  • adminhtml_sales_order_creditmemo_vie

A complete explanation of customizing these areas will be included in the comprehensive example that concludes this series.

A Simple Start

Beyond the simple declaration of invoice and credit memo totals, the important question is how their calculations differ from those in a quote. If you have an eye for building your own custom total, that’s the crux of the matter you’ll have to decide for yourself: How do you want the amount to be calculated when only a portion of an order is invoiced or refunded? Is your custom total fully payable immediately on the first invoice (like the native shipping total)? Does your custom discount need to be re-calculated for the entire order if one item is refunded (and the amount applied to the refund adjusted accordingly)?

These are the tough questions to work through, but at the very least the native total collectors give some insight into what data is available for your calculations and how to get at it. To start with, let’s assume a custom total is tracked only on the entire order, rather than on individual items, and that we have no need to split it up between multiple invoices or refunds. The native “collect” methods on Mage_Sales_Model_Order_Invoice_Total_Shipping and Mage_Sales_Model_Order_Creditmemo_Total_Shipping provides us an equivalent example and demonstrates some of the things we’ll need to deal with.

  • “getOrder” can be called on both the invoice and credit memo models (passed as parameters to the respective “collect” methods) to retrieve totals or other information directly from the order.
  • An order’s “getInvoiceCollection” and “getCreditMemosCollection” methods will be important for retrieving any past invoices or credit memos that have been created (and thus any amounts already invoiced/refunded). When looping through invoices, the convenient method “isCanceled” can be used to exclude invoices that are no longer valid.
  • You’ll see frequent use of calls like “getShippingRefunded” on the order model. As handy as this is for finding out what amount of a total has already been invoiced/refunded, it relies not only on dedicated fields in the order table, but also the updates to these fields carried out in the invoice and credit memo models’ “register” methods that occur when these entities are finally saved. For custom totals, you can establish similar “total_name_invoiced”/”total_name_refunded” fields on the order table and use the “sales_order_invoice_register” event to update them, or simply fetch previous invoice or credit memo collections to obtain the data.
  • There is no “_addAmount” method here as in the quote totals. Instead, amounts need to be set directly on the invoice or credit memo – not just the specific total amount, but the adjusted grand total as well.

Here’s an incredibly stripped down version of an invoice “collect” method for a hypothetical custom total:

    public function collect(Mage_Sales_Model_Order_Invoice $invoice)
    {
        $invoice->setMyTotalAmount(0);
        $invoice->setBaseMyTotalAmount(0);

        foreach ($invoice->getOrder()->getInvoiceCollection() as $prevInvoice) {
            if ($prevInvoice->getMyTotalAmount() && !$prevInvoice->isCanceled()) {
                return $this;
            }
        }

        $myTotalAmt = $invoice->getOrder()->getMyTotalAmount();
        $baseMyTotalAmt = $invoice->getOrder()->getBaseMyTotalAmount();

        $invoice->setMyTotalAmount($myTotalAmt);
        $invoice->setBaseMyTotalAmount($baseMyTotalAmt);

        $invoice->setGrandTotal($invoice->getGrandTotal()+$myTotalAmt);
        $invoice->setBaseGrandTotal($invoice->getBaseGrandTotal()+$baseMyTotalAmt);

        return $this;
    }

Digging Deeper

If your custom total does need to be tracked per item, or if you need to divide it between multiple invoices and credit memos, you likely have some more complex calculations to do in your models. If we look to some of the other native totals logic (such as in Mage_Sales_Model_Order_Invoice_Total_Tax and Mage_Sales_Model_Order_Creditmemo_Total_Tax), here are some additional useful details we can glean:

  • Naturally, when looping through an invoice’s or credit memo’s items via the “getAllItems” method, the corresponding order item can be retrieved with “getOrderItem.”
  • And speaking of order items: These models contain the handy method “isDummy”, providing a useful and straightforward check for items that should be ignored in any calculations. (These come into play when a single order line item is represented by both a parent and child record for a composite product. A configurable product’s child items are considered “dummy” items, while in the case of bundled products, it’s the parent item that is.)
  • Both the invoice and credit memo models have an “isLast” method frequently used in totals collection. An invoice or credit memo is the “last” when the quantities being invoiced/refunded represent all of the remaining quantities on the order. Where an amount can be split over multiple invoices or refunds, it’s a best practice to use this check to calculate the last differently in order to safeguard against rounding errors or other unexpected calculation issues. (For example: “For any invoice that is not the last, invoice a percentage of the order total based on item quantities. For the last invoice, invoice remaining un-invoiced portion of the order total.)
  • Note that a credit memo can be created in the context of a specific invoice, or merely in the context of the entire order. If the details of the specific invoice being refunded factor into total collection, the appropriate model can be retrieved with “getInvoice” on the credit memo.

NOTE: Use the “isLast” check with caution, as it is apparently unreliable in the case of composite products being invoiced/refunded. In my testing, I observed this “isLast” method resulting in a false negative when configurable products were present in the invoice. “isLast” on an invoice or credit memo model runs an identically named method on the relevant items, which compares the current quantity with the quantity left to invoice/refund. However, the aforementioned “isDummy” condition is applied in one case and not the other, leading to an inaccurate comparison. Luckily, the logic performed here is not difficult to duplicate, and you can perform the same basic check in your own collection logic with this issue corrected. (Look for an example in the final article in this series.)

In summary, the collection process for invoices and credit memos doesn’t differ fundamentally from that of quotes (at least in terms of technical implementation). Whether the calculations involved are simple or headache-inducing, the exploration touched on here should illuminate how Magento treats these components of the order lifecycle.

The example that comprises the last article in this series (linked below) has been updated with the inclusion of these long overdue final missing pieces.

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