We continue to set extension attributes in Magento 2. Last time we were talking about the adding a custom attribute to the customer’s address. But what if we need to set these attributes for the total model? Today we’ll consider how we can do that step by step.

Total Model

Total model is a dynamic system used for collecting the shopping cart and the order total. It usually contains the elements which take part in calculating the total cost: final products total, discount, tax, shipping fee, and some other parameters.

How It Works

Imagine the online store. You buy some products there and add them to the shopping cart. When you view shopping cart page, you can see something like this:

Subtotal – X.XX$ final cost of products (the product’s cost multiplied by the number of products)

Tax  – X.XX$ (optionally)

Shipping  – X.XX$ (optionally)

Discount (optionally)

Total – X.XX $ (the final cost of the order)

Let’s take a look at VENROY online store (it’s Magento based) to see how it can work for you:

Magento based online store

The whole system shown in this example is the Total model. Let’s move on.

What We Need to Do

First of all, we need to add the row before subtotal in the total model. The row will show the customer’s profit. The profit is a marketing element that shows how much money the customer saves using the offer. This profit should not be included in the final cost of the shopping cart and the order totals. It is calculated on the base of subtotal.

So, we can separate our task in two main problems:

  • add the row before Subtotal and don’t include its results to the total cost.
  • make the extension and Magento 2 work right together.

These both conditions must work correctly with each other.

At the end, we should get the result of this kind:

You save: (Our additional row)

Subtotal – final cost of products (the product cost * the number of products)

Tax (optionally)

Shipping (optionally)

Discount (optionally)

Total – the final cost of the order

Let’s go on to code!

Preconditions

There is a Magento 2 based online store. It is oriented on the retail and wholesale customers. Due to this fact, most of the products have the tier prices, as well as the retail prices. The types of store products are the simple products and configurable products. As we said before, we need to output the additional row before Subtotal on the shopping cart and the order pages. This row must show the total profit for the wholesale customer. While calculating the profit, we don’t include the extra-charge for product options. This row mustn’t influence the order final cost, as well as it mustn’t be copied to the order.

Solution

Setting Magento 2 Extension Attributes

Last time, we added the custom attribute to the customer address. Let’s follow the same way. We’ll add the style_discount field with decimal(10,4) type to the address extension attributes table from the last example. Also, we’ll need to add the attribute to the extenson_attributes.xml file of our module. We should add it twice for two different interfaces.

...
<extension_attributes for="MagentoQuoteApiDataAddressInterface">
        <attribute code="type" type="int">
            <join reference_table="web4pro_quote_address" join_on_field="address_id" reference_field="address_id">
                <field column="type">type</field>
            </join>
        </attribute>
        <attribute code="style_discount" type="float">
            <join reference_table="web4pro_quote_address" join_on_field="address_id" reference_field="address_id">
                <field column="style_discount">style_discount</field>
            </join>
        </attribute>
    </extension_attributes>
    <extension_attributes for="MagentoQuoteApiDataTotalsInterface">
        <attribute code="style_discount" type="float"/>
    </extension_attributes>
…

In the first case, we should specify the table which will store this attribute. In the second case, there is no necessity to do this, because the entity implemented with MagentoQuoteApiDataTotalsInterface is always formed from the address data along the way. Saving of this attribute will be performed in sales_quote_address_save_after event processor, which we described in the previous example.

Furthermore, we need to describe the total model. Let’s create etc/sales.xml file in the module for these purposes. It looks like the following:

<?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="style_discount" instance="Web4proAjaxcartModelQuoteAddressStyle" sort_order="110"/>
        </group>
    </section>
</config>

We choose the sort_order parameter keeping in mind that calculations were performed after Subtotal had been calculated. In this case, the Product entity has already been initialized for all shopping cart elements.

Implementing Total Model in Magento 2

The total model must implement two methods: collect() method used for the calculation and fetch() method used for the output on request. You can implement the total by following this example:

namespace Web4proAjaxcartModelQuoteAddress;
use MagentoQuoteModelQuoteAddressItem as AddressItem;

class Style extends MagentoQuoteModelQuoteAddressTotalAbstractTotal {

    protected $_objectManager;

    public function __construct(MagentoFrameworkObjectManagerInterface $objectManagerInterface){
        $this->_objectManager = $objectManagerInterface;
    }

    public function collect(
        MagentoQuoteModelQuote $quote,
        MagentoQuoteApiDataShippingAssignmentInterface $shippingAssignment,
        MagentoQuoteModelQuoteAddressTotal $total
    ) {
        parent::collect($quote,$shippingAssignment,$total);
        $items = $shippingAssignment->getItems();
        $address = $shippingAssignment->getShipping()->getAddress();
        $amount = 0;
        foreach($items as $item){
            if ($item->getParentItem()) {
                continue;
            }
            if ($item instanceof AddressItem) {
                $quoteItem = $item->getAddress()->getQuote()->getItemById($item
->getQuoteItemId());
            } else {
                $quoteItem = $item;
            }
            $product = $quoteItem->getData('product');
            $childProduct = $product;
            if($product->getTypeId()=='configurable'){
                $childProduct = $product->getCustomOption('simple_product')->getProduct();
            }
            $qty = $quoteItem->getQty();
            $amount+=(float)($qty*($childProduct->getPriceModel()->getFinalPrice($qty,$childProduct)-$childProduct->getPriceModel()->getBasePrice($childProduct,1)));
        }
        $total->addTotalAmount($this->getCode(),$amount);
        $extensionAttr = $address->getExtensionAttributes();
        if(!$extensionAttr){
            $extensionAttr = $this->_objectManager
->create('MagentoQuoteApiDataAddressExtension');
        }
        $extensionAttr->setData($this->getCode(),$total->getTotalAmount($this->getCode()));
        $address->setExtensionAttributes($extensionAttr);
        return $this;
    }
    public function fetch(MagentoQuoteModelQuote $quote, MagentoQuoteModelQuoteAddressTotal $total)
    {
        $result = null;
        $amount = $total->getTotalAmount($this->getCode());

        if ($amount != 0) {
            $result = [
                'code' => $this->getCode(),
                'title' => __('Style Discount'),
                'value' => $amount
            ];
        }
        return $result;
    }
}

Copying Data to Total Model in Magento 2

We need to provide copying the data from the address to the total model. It’s possible with the plugin from the previous example to MagentoFrameworkApiDataObjectHelper class. But note that we should work separately with address attributes and total model attributes. We copy the first ones to the order address and the second ones – to the total model. When trying to copy some data to the receiver which can’t receive the attribute, the exception will be thrown out. The method looks like this one:

public function beforePopulateWithArray($helper,$dataObject, array $data, $interfaceName){
        switch($interfaceName){
            case 'MagentoSalesApiDataOrderAddressInterface':
                if($data['extension_attributes'] instanceof MagentoQuoteApiDataAddressExtensionInterface){
                    $data['extension_attributes'] = $data['extension_attributes']->__toArray();
                    if(isset($data['extension_attributes']['style_discount'])){
                        unset($data['extension_attributes']['style_discount']);
                    }
                }
            break;
            case 'MagentoCustomerApiDataAddressInterface':
                if(isset($data['extension_attributes'])&&($data['extension_attributes'] instanceof MagentoQuoteApiDataAddressExtensionInterface)){
                    $data['extension_attributes'] = $data['extension_attributes']->__toArray();
                    if(isset($data['extension_attributes']['type'])){
                        $data['type'] = $data['extension_attributes']['type'];
                    }
                }
                break;
            case 'MagentoQuoteApiDataTotalsInterface':
                if($data['extension_attributes'] instanceof MagentoFrameworkApiAbstractSimpleObject){
                    $data['extension_attributes'] = $data['extension_attributes']->__toArray();
                    if(isset($data['extension_attributes']['type'])){
                        unset($data['extension_attributes']['type']);
                    }
                }
                break;
        }
        return array($dataObject,$data,$interfaceName);
    }

Outputting Total on Shopping Cart Page

Now we are going to output a new Total on the shopping cart page. It requires us to describe and implement Magento 2 knockout-component. It’s described in Layout file the next way:

<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="style_discount" xsi:type="array">
                                    <item name="component"  xsi:type="string">Web4pro_Ajaxcart/js/style</item>
                                    <item name="sortOrder" xsi:type="string">1</item>
                                    <item name="config" xsi:type="array">
                                        <item name="template" xsi:type="string">Web4pro_Ajaxcart/checkout/cart/totals/style</item>
                                        <item name="title" xsi:type="string" translate="true"><![CDATA[Style Quantity Discount]]></item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>

We choose sort_order taking into consideration that the row is outputted before Subtotal. As you can see, the component consists of a template and a javascript file. The template is loaded asynchronously, and it’s filled with the content with JavaScript. This is how the template supposed to look like:

<tr class="totals">
    <th class="mark" scope="row" data-bind="text: title"></th>
    <td data-bind="attr: {'data-th': title}" class="amount">
        <span class="price" data-bind="text: getValue()"></span>
    </td>
</tr>

JavaScript code:

define(
    [
        'Magento_Checkout/js/view/summary/abstract-total',
        'Magento_Checkout/js/model/quote'
    ],
    function (Component, quote) {
        "use strict";
        return Component.extend({

            isDisplayed: function() {
                return this.getPureValue()!=0;
            },
            getPureValue: function() {
                var totals = quote.getTotals()();
                if (totals) {
                    if(typeof totals.style_discount=='undefined'){
                        quote.setTotals(window.checkoutConfig.totalsData);
                        totals = quote.getTotals()();
                    }
                    return totals.style_discount;
                }
                return quote.style_discount;
            },
            getValue: function() {
                    return this.getFormattedPrice(this.getPureValue());
            }
         });
    }
);

Pay attention to the if condition (type of totals.style_discount==’undefined’). It’s necessary because Magento 2 extension attributes (we checked  2.1.2 version) are written to Total JavaScript object if only the asynchronous loading takes place (_SetTotals() method). It is not being written when the page is loaded from window.checkoutConfig.totalsData.

We can output this component to the order page by following the same routine.

Pitfalls

One of the moments we should keep in mind is that Total content is being added while calculating the Grand Total. It’s not good for our task. The reason lies in implementation features of Grand Total calculation in MagentoQuoteModelQuoteAddressTotalGrand class. Take a look:

public function collect(
        MagentoQuoteModelQuote $quote,
        MagentoQuoteApiDataShippingAssignmentInterface $shippingAssignment,
        MagentoQuoteModelQuoteAddressTotal $total
    ) {
        $totals = array_sum($total->getAllTotalAmounts());
        $baseTotals = array_sum($total->getAllBaseTotalAmounts());

        $total->setGrandTotal($totals);
        $total->setBaseGrandTotal($baseTotals);
        return $this;
    }

We can fix this out using a plugin which excludes our total from the sum.

<type name="MagentoQuoteModelQuoteAddressTotal">
        <plugin name="web4pro-remove-external-totals" type="Web4proAjaxcartModelPlugin" sortOrder="20"/>
    </type>

public function afterGetAllTotalAmounts($total,$result){
        if(isset($result['style_discount'])){
            unset($result['style_discount']);
        }
        return $result;
    }

Conclusion

Finally, we have coped with our task. Let’s summarise what we needed to do:

  • add the style_discount field with decimal(10,4) type to the address extension attributes table;
  • add the attribute to the extenson_attributes.xml file of our module (twice for two different interfaces);
  • create etc/sales.xml file in the module to describe the total model;
  • implement the total model;
  • provide copying the data from the address to the total model with the plugin (work separately with address attributes and total model attributes);
  • fix the adding of Total content while calculating the Grand Total with a special plugin;
  • output a new Total on the shopping cart and the order pages.

This is how it works. We hope that our article answers your question about set extension attributes for the total model. However, if you have some issues regarding extensions at your store and you need an assistance, we can provide you with Magento 2 Extension Development.

Wish you good luck with Total Models in Magento 2!