45

The following link will describe

http://excellencemagentoblog.com/blog/2012/01/27/magento-add-fee-discount-order-total/

to add fee to order totals in Magento 1.

Now this functionality is moved to Quote module in Magento 2 .

I think still same concept like collect and fetch methods. Has anyone tried this in Magento 2?

8

6 Answers 6

129
+50

follow below steps it will help you, in my module, I just added fee column
this will add one row in cart total called fee and also side bar in the checkout page
and also it added fee amount to total amount (fee static value I kept as 100) once the order is placed total will be with fee and if you are logged in fronted in order view you can see fee's new row in the total block but admin side not yet implemented if someone implements, you can post that answer

create sales.xml in your module etc folder

<?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="fee" instance="Sugarcode\Test\Model\Total\Fee" sort_order="150"/>
          
        </group>  
    </section>
</config>

app\code\Sugarcode\Test\view\frontend\web\js\view\checkout\cart\totals\fee.js

/**
 * Copyright © 2015 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
define(
    [
        'Sugarcode_Test/js/view/checkout/summary/fee'
    ],
    function (Component) {
        'use strict';

        return Component.extend({

            /**
             * @override
             */
            isDisplayed: function () {
                return true;
            }
        });
    }
);

app\code\Sugarcode\Test\view\frontend\web\js\view\checkout\summary\fee.js

/**
 * Copyright © 2015 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
/*jshint browser:true jquery:true*/
/*global alert*/
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: 'Sugarcode_Test/checkout/summary/fee'
            },
            totals: quote.getTotals(),
            isTaxDisplayedInGrandTotal: window.checkoutConfig.includeTaxInGrandTotal || false,
            isDisplayed: function() {
                return this.isFullMode();
            },
            getValue: function() {
                var price = 0;
                if (this.totals()) {
                    price = totals.getSegment('fee').value;
                }
                return this.getFormattedPrice(price);
            },
            getBaseValue: function() {
                var price = 0;
                if (this.totals()) {
                    price = this.totals().base_fee;
                }
                return priceUtils.formatPrice(price, quote.getBasePriceFormat());
            }
        });
    }
);

app\code\Sugarcode\Test\view\frontend\web\template\checkout\summary\fee.html

<!--
/**
 * Copyright © 2015 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<!-- ko -->

  <tr class="totals fee 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 -->

app\code\Sugarcode\Test\view\frontend\web\template\checkout\cart\totals\fee.html

<!--
/**
 * Copyright © 2015 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<!-- ko -->
<tr class="totals fee 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\Sugarcode\Test\Model\Total\Fee.php

<?php
/**
 * Copyright © 2015 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Sugarcode\Test\Model\Total;


class Fee extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal
{
   /**
     * Collect grand total address amount
     *
     * @param \Magento\Quote\Model\Quote $quote
     * @param \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment
     * @param \Magento\Quote\Model\Quote\Address\Total $total
     * @return $this
     */
    protected $quoteValidator = null; 
    
    public function __construct(\Magento\Quote\Model\QuoteValidator $quoteValidator)
    {
        $this->quoteValidator = $quoteValidator;
    }
  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);
      
      
        $exist_amount = 0; //$quote->getFee(); 
        $fee = 100; //Excellence_Fee_Model_Fee::getFee();
        $balance = $fee - $exist_amount;
        
        $total->setTotalAmount('fee', $balance);
        $total->setBaseTotalAmount('fee', $balance);

        $total->setFee($balance);
        $total->setBaseFee($balance);
        
        $total->setGrandTotal($total->getGrandTotal() + $balance);
        $total->setBaseGrandTotal($total->getBaseGrandTotal() + $balance);


        return $this;
    } 
    
    protected function clearValues(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 Address\Total $total
     * @return array|null
     */
    /**
     * Assign subtotal amount and label to address object
     *
     * @param \Magento\Quote\Model\Quote $quote
     * @param Address\Total $total
     * @return array
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function fetch(\Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Quote\Address\Total $total)
    {
        return [
            'code' => 'fee',
            'title' => 'Fee',
            'value' => 100
        ];
    }

    /**
     * Get Subtotal label
     *
     * @return \Magento\Framework\Phrase
     */
    public function getLabel()
    {
        return __('Fee');
    }
}

app\code\Sugarcode\Test\etc\module.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Module/etc/module.xsd">
    <module name="Sugarcode_Test" setup_version="2.0.6" schema_version="2.0.6">
        <sequence>
            <module name="Magento_Sales"/>
            <module name="Magento_Quote"/>
            <module name="Magento_Checkout"/>
        </sequence>
    </module>
</config>

app\code\Sugarcode\Test\view\frontend\layout\checkout_cart_index.xml

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2015 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<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="fee" xsi:type="array">
                                    <item name="component"  xsi:type="string">Sugarcode_Test/js/view/checkout/cart/totals/fee</item>
                                    <item name="sortOrder" xsi:type="string">20</item>
                                    <item name="config" xsi:type="array">
                                         <item name="template" xsi:type="string">Sugarcode_Test/checkout/cart/totals/fee</item>
                                        <item name="title" xsi:type="string" translate="true">Fee</item>
                                    </item>
                                </item>
                                    
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

app\code\Sugarcode\Test\view\frontend\layout\checkout_index_index.xml

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2015 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<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="fee" xsi:type="array">
                                                            <item name="component"  xsi:type="string">Sugarcode_Test/js/view/checkout/cart/totals/fee</item>
                                                            <item name="sortOrder" xsi:type="string">20</item>
                                                            <item name="config" xsi:type="array">
                                                                 <item name="template" xsi:type="string">Sugarcode_Test/checkout/cart/totals/fee</item>
                                                                <item name="title" xsi:type="string" translate="true">Fee</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>

app\code\Sugarcode\Test\view\frontend\layout\sales_order_view.xml

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2015 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
  
    <body>        
        <referenceContainer name="order_totals">
            <block class="Sugarcode\Test\Block\Sales\Order\Fee" name="fee"/>
        </referenceContainer>
    </body>
</page>

app\code\Sugarcode\Test\Block\Sales\Order\Fee.php

<?php
/**
 * Copyright © 2015 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */

/**
 * Tax totals modification block. Can be used just as subblock of \Magento\Sales\Block\Order\Totals
 */
namespace Sugarcode\Test\Block\Sales\Order;



class Fee extends \Magento\Framework\View\Element\Template
{
    /**
     * Tax configuration model
     *
     * @var \Magento\Tax\Model\Config
     */
    protected $_config;

    /**
     * @var Order
     */
    protected $_order;

    /**
     * @var \Magento\Framework\DataObject
     */
    protected $_source;

    /**
     * @param \Magento\Framework\View\Element\Template\Context $context
     * @param \Magento\Tax\Model\Config $taxConfig
     * @param array $data
     */
    public function __construct(
        \Magento\Framework\View\Element\Template\Context $context,
        \Magento\Tax\Model\Config $taxConfig,
        array $data = []
    ) {
        $this->_config = $taxConfig;
        parent::__construct($context, $data);
    }

    /**
     * Check if we nedd display full tax total info
     *
     * @return bool
     */
    public function displayFullSummary()
    {
        return true;
    }

    /**
     * Get data (totals) source model
     *
     * @return \Magento\Framework\DataObject
     */
    public function getSource()
    {
        return $this->_source;
    } 
    public function getStore()
    {
        return $this->_order->getStore();
    }

      /**
     * @return Order
     */
    public function getOrder()
    {
        return $this->_order;
    }

    /**
     * @return array
     */
    public function getLabelProperties()
    {
        return $this->getParentBlock()->getLabelProperties();
    }

    /**
     * @return array
     */
    public function getValueProperties()
    {
        return $this->getParentBlock()->getValueProperties();
    }
    
    /**
     * Initialize all order totals relates with tax
     *
     * @return \Magento\Tax\Block\Sales\Order\Tax
     */
     public function initTotals()
    {
        
        $parent = $this->getParentBlock();
        $this->_order = $parent->getOrder();
        $this->_source = $parent->getSource();

        $store = $this->getStore();
        
        $fee = new \Magento\Framework\DataObject(
                [
                    'code' => 'fee',
                    'strong' => false,
                    'value' => 100,
                    //'value' => $this->_source->getFee(),
                    'label' => __('Fee'),
                ]
            );
     
            $parent->addTotal($fee, 'fee');

            return $this;
    }

}

once the above steps are done run below command this is important else your js & Html files will be missing from pub/static folder. So run below command which will create js and html file in pub/static folder

bin\magento setup:static-content:deploy  

if works accept my answer which helps others

38
  • 26
    you wrote out the module... impressive! +1 for that Dec 10, 2015 at 11:55
  • 6
    well done praseep
    – Amit Bera
    Dec 14, 2015 at 17:24
  • 4
    hello Pradeep Kumar, great article, but there is one problem with that code, fee add two times into grand total, is there any solution for this? Feb 25, 2016 at 15:04
  • 4
    Has anyone fixed the two time fee applied bug in above code? Aug 3, 2016 at 12:24
  • 4
    I am sorry, I have to apologize. The "Two Times Fee"-Bug is most probably due to the fact that app\code\Sugarcode\Test\Model\Total\Fee.php extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal. As you normally have two Addresses in Checkout (Billing and Shipping) the Quote save gets called twice. There was a similar Behaviour in M1, unfortunately the M1-Fix is not applicable here... Aug 11, 2016 at 12:33
5

Based on above answer i developed custom fee extension.

https://github.com/sivajik34/Custom-Fee-Magento2

3
  • How to add a fee from text box prnt.sc/hfsni5 Nov 27, 2017 at 10:23
  • how to add textbox for adding fee in magento2
    – user83013
    Dec 11, 2019 at 5:34
  • @NagendraKodi I need same extra fee as you mentioned in the comment (prnt.sc/hfsni5) How you have achieved that ? Oct 9, 2020 at 7:04
5

please comment

        $total->setGrandTotal($total->getGrandTotal() + $balance);

form app\code\Sugarcode\Test\Model\Total\Fee.php for double custom fee issue

Hope it will help you!!

4

The answer of Pradeep is very helpful, but misses an important point.

The function Sugarcode\Test\Model\Total::collect() is called twice by Magento's Magento\Quote\Model\QuoteTotalsCollector::collect(), once for each address. At that point it creates a combined total that is stored in the quote table. It does not show up in the order, nor on the website in the checkout.

For this reason it is important to collect the fee in only one of times that collect() is called. This can be done by checking if there are any shipped items available:

    $items = $shippingAssignment->getItems();
    if (!count($items)) {
        return $this;
    }

Add this code at the beginning of your variant of Sugarcode\Test\Model\Total::collect()

3
  • 2
    still adding twice Dec 2, 2017 at 9:06
  • Any update on this? It is still adding the fee twice. Can't get it to work unfortunately.
    – hallleron
    Jun 26, 2019 at 13:31
  • @Patrick van Bergen good spot this solved the problem with dynamic fee not updated to grand total on frondend Feb 6, 2020 at 9:43
4

I have used the above code for adding the custom fee. Custom fee is dynamic for each order. So i have save the value in the database.

    <?xml version="1.0" encoding="UTF-8"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
        <table name="quote" resource="default" engine="innodb">
            <column xsi:type="decimal" name="fee" nullable="true" comment="Custom Tax from webservice"/>
        </table>
        <table name="sales_order" resource="sales" engine="innodb" comment="Sales Order">
            <column xsi:type="decimal" name="fee" nullable="true" comment="Custom Tax from webservice"/>
        </table>
</schema>

Convert the quote to sales using fieldset.xml

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:DataObject/etc/fieldset.xsd">
    <scope id="global">
        <!-- Copy quote to sale order fields -->
        <fieldset id="sales_convert_quote">
            <field name="fee">
                <aspect name="to_order" />
            </field>
        </fieldset>
    </scope>
</config>

Observer to convert quote to sales

    <?php

namespace BA\Vertex\Observer\Frontend\Sales;

class QuoteSubmitBefore implements \Magento\Framework\Event\ObserverInterface
{

    /**
     * Execute observer
     *
     * @param \Magento\Framework\Event\Observer $observer
     * @return void
     */
    public function execute(
        \Magento\Framework\Event\Observer $observer
    ) {
        try {
           
            $quote = $observer->getQuote();
            $order = $observer->getOrder();
            $order->setData('fee', $quote->getData('fee'));

        } catch (\Exception $e) {
            $this->messageManager->addErrorMessage($e->getMessage());
        }
    }
}

events.xml

 <?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="sales_model_service_quote_submit_before">
        <observer instance="BA\Vertex\Observer\Frontend\Sales\QuoteSubmitBefore" name="ba_vertex_quote_submit_before"/>
    </event>
</config>

For displaying custom fee in magento admin backend

view/adminhtml/layout/sales_order_view.xml

    <?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>        
        <referenceContainer name="order_totals">
            <block class="BA\Vertex\Block\Sales\Order\Fee" name="fee"/>
        </referenceContainer>
    </body>
</page>

Block class

    <?php
namespace BA\Vertex\Block\Sales\Order;

class Fee extends \Magento\Framework\View\Element\Template
{
    /**
     * @var Order
     */
    protected $_order;
    /**
     * @return Order
     */
    public function getOrder()
    {
        return $this->_order;
    }
    /**
     * Initialize all order totals relates with tax
     *
     * @return \Magento\Tax\Block\Sales\Order\Tax
     */
    public function initTotals()
    {
        $parent = $this->getParentBlock();
        $this->_order = $parent->getOrder();
        $fee = new \Magento\Framework\DataObject(
            [
                'code' => 'fee',
                'strong' => false,
                'value' => $this->_order->getFee(),
                'label' => __('Fee'),
            ]
        );

        $parent->addTotal($fee, 'fee');
        return $this;
    }
}
1
  • The custom fee set in quote area is missing. Could you please share where to set custom fee in quotes. Jun 22, 2022 at 8:10
2

Everyone who is struggling with 2 times fees issue, try the following code to apply fees in the collect method.

$fee = 10;           
$total->addTotalAmount('fee', $fee);
$total->addBaseTotalAmount('fee', $fee);
$total->setBaseGrandTotal($total->getBaseGrandTotal());

Your collect method should look something like 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);
        if (!count($shippingAssignment->getItems())) {
            return $this;
        }

            $fee = 10;           
            $total->addTotalAmount('fee', $fee);
            $total->addBaseTotalAmount('fee', $fee);
            $total->setBaseGrandTotal($total->getBaseGrandTotal());
            $quote->setFee($fee);          



        return $this;
    }

1
  • Thank you so much, this saved my time a lot. May 28 at 18:33

Not the answer you're looking for? Browse other questions tagged or ask your own question.