collectTotals in Magento 2

If we were to have a custom quote object which we were adding items to, we can re-calculate the totals in the cart using the following method.

$shippingAddress = $preorderQuote->getShippingAddress();
$shippingAddress->unsetData('cached_items_all');            
$quote->setTotalsCollectedFlag(false)->collectTotals();
$this->quoteRepository->save($quote);

We unset the cached item on the quote address object to force Magento to re-load the items for the address. This is necessary as Magento will sometimes not update this automatically, leading to an incorrect zero value quote and quote items.

Magento Tax Snippets

Getting the Customer’s Address Country For Tax Calculations

The following will perform the following fallbacks:

-The quote’s shipping address
-The customer’s shipping address
-The customer’s billing address
-The default system configuration country

class Namespace_Module_Helper_Data extends Mage_Core_Helper_Abstract {
    const COUNTRY_CODE_GB = "GB";

    public function isDeliveryAddressUk(){
        /** @var Mage_Sales_Model_Quote $quote */
        $quote = Mage::getSingleton('checkout/session')->getQuote();
        $address = $quote->getShippingAddress();

        if($address && $address->getCountryId()){
            return $address->getCountryId() == self::COUNTRY_CODE_GB;
        }

        $cSession = Mage::getSingleton('customer/session');

        if($cSession->isLoggedIn()){
            $customer = $cSession->getCustomer();
            $address = $customer->getDefaultShippingAddress();

            if($address && $address->getCountryId()){
                return $address->getCountryId() == self::COUNTRY_CODE_GB;
            }

            $customer->getDefaultBillingAddress();

            if($address && $address->getCountryId()){
                return $address->getCountryId() == self::COUNTRY_CODE_GB;
            }
        }
        
        return Mage::getStoreConfig(Mage_Tax_Model_Config::CONFIG_XML_PATH_DEFAULT_COUNTRY) == self::COUNTRY_CODE_GB;
    }
}

Checking the Tax Display Type

// Including Tax
if(Mage::helper('tax')->getPriceDisplayType() == Mage_Tax_Model_Config::DISPLAY_TYPE_INCLUDING_TAX){
    ...
}

// Excluding Tax
if(Mage::helper('tax')->getPriceDisplayType() == Mage_Tax_Model_Config::DISPLAY_TYPE_EXCLUDING_TAX){
    ...
}

Basket Sales Row Totals

Sales Row Totals are defined in config.xml in the nodes:

<sales>
    <quote>
        <totals>
            <shipping>
                <class>sales/quote_address_total_shipping</class>
                <after>subtotal,freeshipping,tax_subtotal</after>
                <before>grand_total</before>
            </shipping>
        </totals>
    </quote>
</sales>

The class node is instantiated by each total type, and then collect is called on each. The fetch method actually adds the total type to the address — up until this point the config.xml definition only declares the total types which should be queried for addition. Logic could exist in the fetch method to check whether the total type should be added at all. Additionally, the fetch method could add more than one type of total, as the Mage_Tax_Model_Sales_Total_Quote_Tax does.

<?php
  public function fetch(Mage_Sales_Model_Quote_Address $address)
    {
        $amount = $address->getShippingAmount();
       if ($amount != 0 || $address->getShippingDescription()) {
            $title = Mage::helper('sales')->__('Shipping & Handling');
            if ($address->getShippingDescription()) {
                $title .= ' (' . $address->getShippingDescription() . ')';
            }
            $address->addTotal(array(
                'code' => $this->getCode(),
                'title' => $title,
                'value' => $address->getShippingAmount()
            ));
        }
        return $this;
    }

These are rendered by the Mage_Checkout_Block_Cart_Totals‘ renderTotal method. A total can also have a block total renderer assigned to it instead of having to rely on the default output in the cart rows. The block is instantiated using the node names of the total config definition. The total node name is used (in the node) with _total_renderer appended to it. This is done in the Cart Totals’ _getTotalRenderer() method. In the example above, the renderer would be called *shipping_total_renderer*.

Fishpigs Total Renderer Example

Fish pigs add a cart total renderer to the cart. To achieve this, they add a shipping_total_renderer in their layout XML:

<layout>
	
	<checkout_cart_index>
		<reference name="checkout.cart.totals">
			<block type="basketshipping/total_shipping" name="shipping_total_renderer" />
		</reference>
	</checkout_cart_index>
	
</layout>

– This block will now be rendered for the shipping_total. Due to the way the fetch method works however, the row will only be added to the cart totals is not equal to zero or the address does not has a shipping description. This can be changed by overwriting the Mage_Sales_Model_Quote_Address_Total_Shipping model.

Notes on Magento’s Tax Module

Magento’s tax modules adds renderers to many of the sales/quote/total rows. This may often be overlooked when looking at the config file from Magento’s Sales module. This changes the renderers of the subtotal, shipping, discount, and grand total to renderer blocks within the tax module:

 <subtotal>
    <renderer>tax/checkout_subtotal</renderer>
    <admin_renderer>adminhtml/sales_order_create_totals_subtotal</admin_renderer>
</subtotal>
<shipping>
    <renderer>tax/checkout_shipping</renderer>
    <admin_renderer>adminhtml/sales_order_create_totals_shipping</admin_renderer>
</shipping>
<discount>
    <renderer>tax/checkout_discount</renderer>
    <admin_renderer>adminhtml/sales_order_create_totals_discount</admin_renderer>
</discount>
<grand_total>
    <renderer>tax/checkout_grandtotal</renderer>
    <admin_renderer>adminhtml/sales_order_create_totals_grandtotal</admin_renderer>
</grand_total>

Basket Row Total Areas

There are three main areas in the shipping row totals: footer, taxes, and null. Null is the default area any is output by default, other renderers require explicit output in a template, such as the taxes area. This area area is only created if tax/cart_display/grandtotal is set to Yes.

The position of taxes in Magento will render based on the tax/cart_display/grandtotal setting. If this setting is set to Yes, then the tax will not be output as part of the regular totals, and assigned to the taxes area, which by default is output as part of the grand total template (tax/checkout/grandtotal.phtml):

<?php echo $this->renderTotals('taxes', $this->getColspan()); ?>

The area is set in the fetch method of Mage_Tax_Model_Sales_Total_Quote_Tax:

$area = null;
if ($this->_config->displayCartTaxWithGrandTotal($store) && $address->getGrandTotal()) {
    $area = 'taxes';
}

if (($amount != 0) || ($this->_config->displayCartZeroTax($store))) {
    $address->addTotal(array(
        'code' => $this->getCode(),
        'title' => Mage::helper('tax')->__('Tax'),
        'full_info' => $applied ? $applied : array(),
        'value' => $amount,
        'area' => $area
    ));
}

If the tax/cart_display/grandtotal is set to “No”, then area doesn’t get assigned, and defaults to null, so the total will be output alongside all of the other totals.

Copying Product Attributes to Quote & Order Items

Add this to your XML;

<global>
    <sales>
        <quote>
            <item>
                <product_attributes>
                    <location/>
                </product_attributes>
            </item>
        </quote>
    </sales>
</global>

  • This makes the product attributes accessible to the Mage_Sales_Model_Quote_Config class’ getProductAttributes() method. This reads in the sales/quote/item/product_attributes node.

  • This is called by the _assignProducts method of the Mage_Sales_Model_Resource_Quote_Item_Collection‘s _assignProducts() method, where it adds all of the attributes to the product collection of the quote item collection.

Add the columns to the quote and order item tables

  • Create an installer with the following:

    $installer = new Mage_Sales_Model_Resource_Setup('core_setup');
    
    $entities = array(
        'quote_item',
        'order_item'
    );
    
    $options = array(
        // For some reason, VARCHAR works here, whereas elsewhere it must be TYPE_TEXT with a length of 255.
        'type'     => Varien_Db_Ddl_Table::TYPE_VARCHAR,
        'visible'  => true,
        'required' => false
    );
    
    foreach ($entities as $entity) {
        $installer->addAttribute($entity, 'location', $options);
    }
    

  • The addAttribute() method of the Mage_Sales_Model_Resource_Setup creates the columns on the quote_item and order_item tables.

Copy the attributes to the quote item

Unlike copying from quote items to order items, there isn’t an XML method to do this – it has to be accomplished through an observer. Add the following to the config (with the observer / method substituted):

<frontend>
    <events>
        <sales_quote_item_set_product>
            <observers>
                <observer_name>
                    <class>model/observer</class>
                    <method>addAttributesToQuoteItem</method>
                </observer_name>
            </observers>
        </sales_quote_item_set_product>
    </events>
</frontend>

Then create the following method for the observer above:

    public function addAttributesToQuoteItem($observer){
        $quoteItem = $observer->getQuoteItem();
        $product = $observer->getProduct();
        $quoteItem->setLocation($product->getLocation());
    }

Note: Values Don’t have to come from the product item

Any arbitrary information could be set on the quote or quote item; it does not have to come from the product itself. E.g. A different type of ship note could be set by a different observer. The associated columns just need to exist on the quote or quote item tables.

Copying quote item attributes to the order item

  • This can be achieved with a small amount of XML:
  <global>
    <fieldsets>
        <sales_convert_quote_item>
            <location>
                <to_order_item>*</to_order_item>
            </location>
        </sales_convert_quote_item>
    </fieldsets>
</global>

Note: This will not copy the data to the sales_flat_shipment_item table.

Converting from an order item to other types of item

The following will copy the object’s attributes when converting the quote item back to a quote item, an invoice item, and a credit memo item

<global>
    <fieldsets>
        <sales_convert_order_item>
            <location>
                <to_quote_item>*</to_quote_item>
                <to_invoice_item>*</to_invoice_item>
                <to_cm_item>*</to_cm_item>
            </location>
        </sales_convert_order_item>
    </fieldsets>
</global>

These are used in the Mage_Sales_Model_Convert_Order class’ itemToQuoteItem, itemToInvoiceItem, and itemToCreditmemoItem methods. Note: In default Magento, it does not appear that the itemToQuoteItem is used, so this should be included for third party extensions which may rely on this method.