Custom totals help you modify the calculation process in the shopping cart. They can be used to add an extra tax or discount to Magento Checkout or modify the existing ones and they are later applied to the products in the shopping cart. BTW, this topic is included into the Magento 2 professional developer certification exam.
Adding Custom Total
The first thing we need to do is create an app/code/Vendor/Module/etc/sales.xml file in our module. This file is used to register all the available totals in Magento 2.
app/code/Vendor/Module/etc/sales.xml:
1 2 3 4 5 6 7 8 |
<?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="custom_total" instance="Vendor\Module\Model\Totals\Custom" sort_order="500"/> </group> </section> </config> |
Here we specified the \Vendor\Module\Model\Totals\Custom class as a model of our custom total. This class must inherit \Magento\Quote\Model\Quote\Address\Total\AbstractTotal and also implement the collect and fetch methods. The collect method is used to calculate the value of our total, and the fetch method returns this value, as well as the total’s code and its name.
The \Magento\Quote\Model\Quote\Address\Total $total parameter also allows you to affect the result of the other total classes. But depending on the task, it may make more sense to use plugins to modify their values.
app/code/Vendor/Module/Model/Total/Custom.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
<?php Namespace Vendor\Module\Model\Total; class Custom extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal { /** * Custom constructor. */ public function __construct() { $this->setCode('custom_total'); } /** * @param \Magento\Quote\Model\Quote $quote * @param \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment * @param \Magento\Quote\Model\Quote\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); $items = $shippingAssignment->getItems(); if (!count($items)) { return $this; } //we will add an additional amount of 150 to the order as an example $amount = 150; $total->setTotalAmount('custom_total', $amount); $total->setBaseTotalAmount('custom_total', $amount); $total->setCustomAmount($amount); $total->setBaseCustomAmount($amount); $total->setGrandTotal($total->getGrandTotal() + $amount); $total->setBaseGrandTotal($total->getBaseGrandTotal() + $amount); return $this; } /** * @param \Magento\Quote\Model\Quote\Address\Total $total */ protected function clearValues(\Magento\Quote\Model\Quote\Address\Total $total) { $total->setTotalAmount('subtotal', 0); $total->setBaseTotalAmount('subtotal', 0); $total->setTotalAmount('tax', 0); $total->setBaseTotalAmount('tax', 0); $total->setTotalAmount('discount_tax_compensation', 0); $total->setBaseTotalAmount('discount_tax_compensation', 0); $total->setTotalAmount('shipping_discount_tax_compensation', 0); $total->setBaseTotalAmount('shipping_discount_tax_compensation', 0); $total->setSubtotalInclTax(0); $total->setBaseSubtotalInclTax(0); } /** * @param \Magento\Quote\Model\Quote $quote * @param \Magento\Quote\Model\Quote\Address\Total $total * @return array */ public function fetch( \Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Quote\Address\Total $total ) { return [ 'code' => $this->getCode(), 'title' => 'Custom Total’, 'value' => 150 ]; } /** * @return \Magento\Framework\Phrase */ public function getLabel() { return __('Custom Total’); } } |
Displaying Custom Total
Cart and checkout pages
Knockout.js is used for displaying totals. In order for our total to appear on the cart and checkout pages, we need to add a new js component to checkout_cart_index.xml and checkout_index_index.xml layouts.
app/code/Vendor/Module/view/frontend/layout/checkout_cart_index.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?xml version="1.0"?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="checkout.cart.totals"> <arguments> <argument name="jsLayout" xsi:type="array"> <item name="components" xsi:type="array"> <item name="block-totals" xsi:type="array"> <item name="children" xsi:type="array"> <item name="custom_total" xsi:type="array"> <item name="component" xsi:type="string">Vendor_Module/js/view/checkout/cart/totals/custom_total</item> <item name="sortOrder" xsi:type="string">20</item> <item name="config" xsi:type="array"> <item name="template" xsi:type="string">Vendor_Module/checkout/cart/totals/custom_total</item> <item name="title" xsi:type="string">Custom Total</item> </item> </item> </item> </item> </item> </argument> </arguments> </referenceBlock> </body> </page> |
app/code/Vendor/Module/view/frontend/layout/checkout_index_index.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
<?xml version="1.0"?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="checkout.root"> <arguments> <argument name="jsLayout" xsi:type="array"> <item name="components" xsi:type="array"> <item name="checkout" xsi:type="array"> <item name="children" xsi:type="array"> <item name="sidebar" xsi:type="array"> <item name="children" xsi:type="array"> <item name="summary" xsi:type="array"> <item name="children" xsi:type="array"> <item name="totals" xsi:type="array"> <item name="children" xsi:type="array"> <item name="custom_total" xsi:type="array"> <item name="component" xsi:type="string">Vendor_Module/js/view/checkout/cart/totals/custom_total</item> <item name="sortOrder" xsi:type="string">20</item> <item name="config" xsi:type="array"> <item name="template" xsi:type="string">Vendor_Module/checkout/cart/totals/custom_total</item> <item name="title" xsi:type="string">Custom Total</item> </item> </item> </item> </item> <item name="cart_items" xsi:type="array"> <item name="children" xsi:type="array"> <item name="details" xsi:type="array"> <item name="children" xsi:type="array"> <item name="subtotal" xsi:type="array"> <item name="component" xsi:type="string">Magento_Tax/js/view/checkout/summary/item/details/subtotal</item> </item> </item> </item> </item> </item> </item> </item> </item> </item> </item> </item> </item> </argument> </arguments> </referenceBlock> </body> </page> |
And also write the components and HTML templates:
app/code/Vendor/Module/view/frontend/web/js/view/checkout/cart/totals/custom_total.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
define( [ 'Vendor_Module/js/view/checkout/summary/custom_total' ], function (Component) { 'use strict'; return Component.extend({ /** * @override */ isDisplayed: function () { return this.getPureValue() !== 0; } }); } ); |
app/code/Vendor/Module/view/frontend/web/template/checkout/cart/totals/custom_total.html
1 2 3 4 5 6 7 8 9 |
<!-- ko if: isDisplayed() --> <tr class="totals custom_total excl"> <th class="mark" colspan="1" scope="row" data-bind="text: title"></th> <td class="amount"> <span class="price" data-bind="text: getValue()"></span> </td> </tr> <!-- /ko → |
app/code/Vendor/Module/view/frontend/web/js/view/checkout/summary/custom_total.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
define( [ 'Magento_Checkout/js/view/summary/abstract-total', 'Magento_Checkout/js/model/quote', 'Magento_Catalog/js/price-utils', 'Magento_Checkout/js/model/totals' ], function (Component, quote, priceUtils, totals) { "use strict"; return Component.extend({ defaults: { isFullTaxSummaryDisplayed: window.checkoutConfig.isFullTaxSummaryDisplayed || false, template: 'Vendor_Module/checkout/summary/custom_total' }, totals: quote.getTotals(), isTaxDisplayedInGrandTotal: window.checkoutConfig.includeTaxInGrandTotal || false, isDisplayed: function() { return this.isFullMode() && this.getPureValue() !== 0; }, getValue: function() { var price = 0; if (this.totals()) { price = totals.getSegment('custom_total').value; } return this.getFormattedPrice(price); }, getPureValue: function() { var price = 0; if (this.totals()) { price = totals.getSegment('custom_total').value; } return price; } }); } ); |
The getValue and getPureValue methods return the value of our custom total, but the getValue method formats this value by adding two decimal digits and a symbol of the current currency.
app/code/Vendor_Module/view/frontend/web/template/checkout/summary/custom_total.html
1 2 3 4 5 6 7 8 9 10 11 |
<!-- ko if: isDisplayed() --> <tr class="totals custom_total excl"> <th class="mark" scope="row"> <span class="label" data-bind="text: title"></span> <span class="value" data-bind="text: getValue()"></span> </th> <td class="amount"> <span class="price" data-bind="text: getValue(), attr: {'data-th': title}"></span> </td> </tr> <!-- /ko --> |
Order Emails
To make a new total display in the order email you need to add a new block in the sales_email_order_items layout.
app/code/Vendor/Module/view/frontend/layout/sales_email_order_items.xml
1 2 3 4 5 6 7 8 |
<?xml version="1.0" encoding="UTF-8"?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="order_totals"> <block class="Vendor\Module\Block\Order\CustomTotal" name="order.totals.custom" /> </referenceBlock> </body> </page> |
app/code/Vendor/Module/Block/Order/CustomTotal.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php namespace Vendor\Module\Block\Order; class CustomTotal extends \Magento\Framework\View\Element\AbstractBlock { public function initTotals() { $orderTotalsBlock = $this->getParentBlock(); $order = $orderTotalsBlock->getOrder(); if ($order->getCustomAmount() > 0) { $orderTotalsBlock->addTotal(new \Magento\Framework\DataObject([ 'code' => 'custom_total', 'label' => __('Custom Total'), 'value' => $order->getCustomAmount(), 'base_value' => $order->getCustomBaseAmount(), ]), 'subtotal'); } } } |
That is it, thank you for reading. Leave your comments and questions below and good luck at the exam!
Check out our great Ajax Cart extension to improve the quality of customer experience.
Hi, Nicolas. Thanks for the comment.
There are a lot of things that can be the reason it doesn’t work. The easiest way to diagnose them is by taking the standard Total field. For example Discount. Then, look at how it was added in the current version of Magento and compare with what you have.
So I’ve tried out your example in here, but I figured out two things; The first one is the typo with Total and Totals in the Custom.php. And second, it compiles through and stuff, but it doesn’t do anything sadly. And I don’t really get why :( No exceptions, etc. Just nothing displayed.
Hi Parth
Thanks for your question!
Usually the custom total is copied from quote to order page.
Hi, how can we display the custom total on view order page ? Will this total be copied from quote to order or do we have to write fieldset for copying it to the order ? Thanks in advance.
Hi, Geofil
Thank you for your question. Unfortunately, it’s not possible to provide a qualitative answer in the comment.
You can check this article, it dealt with your question and can be useful
https://belvg.com/blog/database-in-magento-2-models-resource-models-and-collections.html
Hi, can I ask a sample codes on how to create a table/page in frontend custom module using Model, Resource Model in Magento 2 with Insert, Delete, Edit functions. Thank you very much Sir!