Magento2 | PWA | GraphQL

Add Order note textarea field to Magento one step checkout and Add/edit note from admin with AJAX


In this post we wiil check how to add order note (textarea field) to Magento one step checkout and display it on Admin sales order view page.

We will provide Add/Edit functionality for this order note information at admin with AJAX.

We will also check how to add custom validation for this order note and provide some admin configuration for this field.

Let's start by creating custom module.

You can find complete module on Github at Magelearn_OrderNote







Create folder inside app/code/Magelearn/OrderNote

Add registration.php file in it:
<?php
/**
 * Copyright © Scriptlodge. All rights reserved.
 * See LICENSE.txt for license details.
 */
 \Magento\Framework\Component\ComponentRegistrar::register(
     \Magento\Framework\Component\ComponentRegistrar::MODULE,
     'Magelearn_OrderNote',
     __DIR__
 );
Add composer.json file in it:
{
    "name": "magelearn/order-note",
    "description": "Add Internal order note for Magento 2",
    "type": "magento2-module",
    "license": "proprietary",
    "authors": [
        {
            "name": "vijay rami",
            "email": "vijaymrami@gmail.com"
        }
    ],
    "minimum-stability": "dev",
    "require": {},
    "autoload": {
        "files": [ "registration.php" ],
        "psr-4": {
            "Magelearn\\OrderNote\\": ""
        }
    }
}
Add etc/module.xml file in it:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Magelearn_OrderNote" setup_version="2.3.3">
        <sequence>
            <module name="Magento_Sales"/>
            <module name="Magento_Quote"/>
            <module name="Magento_Store"/>
            <module name="Magento_Backend"/>
        </sequence>
    </module>
</config>
Now, we will add database setup script.
Create db_schema.xml file in etc folder.
here we will define property of all database fields and indexes. 
<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
    <table name="sales_order" resource="sales" engine="innodb" comment="Sales Order">
        <column xsi:type="text" name="order_note" nullable="true"  comment="order note"/>
    </table>
    <table name="quote" resource="checkout" engine="innodb" comment="Sales quote">
        <column xsi:type="text" name="order_note" nullable="true"  comment="order note"/>
    </table>
    <table name="quote_item" resource="checkout" engine="innodb" comment="Sales quote Item ">
        <column xsi:type="smallint" name="internal_order_note" padding="5" default="0" unsigned="true"  comment="internal order note"/>
    </table>
</schema>

To validate this db_schema.xml file we will first run below command:

php bin/magento setup:db-declaration:generate-whitelist --module-name=Magelearn_OrderNote

This command will automatically generate db_schema_whitelist.json file inside etc folder.

Now we will define the Magento WebAPI.
Add app/code/Magelearn/OrderNote/etc/webapi.xml file.
<?xml version="1.0"?>
<!--
/**
 * Copyright © Scriptlodge. All rights reserved.
 * See LICENSE.txt for license details.
 */
-->
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
    <!-- Managing checkout order note -->
    <route url="/V1/guest-carts/:cartId/setordernote" method="PUT">
        <service class="Magelearn\OrderNote\Api\GuestOrderNoteManagementInterface"
                 method="saveOrderNote"/>
        <resources>
            <resource ref="anonymous" />
        </resources>
    </route>
    <!-- Managing checkout order note -->
    <route url="/V1/carts/mine/setordernote" method="PUT">
        <service class="Magelearn\OrderNote\Api\OrderNoteManagementInterface"
                 method="saveOrderNote"/>
        <resources>
            <resource ref="self" />
        </resources>
        <data>
            <parameter name="cartId" force="true">%cart_id%</parameter>
        </data>
    </route>

    <route url="/V1/sales/note/setordernote" method="PUT">
        <service class="Magelearn\OrderNote\Api\OrderNoteManagementInterface"
                 method="saveOrderNoteAdmin"/>
        <resources>
            <resource ref="self" />
        </resources>
    </route>
</routes>

Now, create di.xml file inside etc folder. 

This file will define necessary nodes to save this order_note field in admin.

Create app/code/Magelearn/OrderNote/etc/di.xml file.

<?xml version="1.0"?>
<!--
/**
 * Copyright © Scriptlodge. All rights reserved.
 * See LICENSE.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Magelearn\OrderNote\Api\Data\OrderNoteInterface"
                type="Magelearn\OrderNote\Model\Data\OrderNote" />
    <preference for="Magelearn\OrderNote\Api\OrderNoteManagementInterface"
                type="Magelearn\OrderNote\Model\OrderNoteManagement" />
    <preference for="Magelearn\OrderNote\Api\GuestOrderNoteManagementInterface"
                type="Magelearn\OrderNote\Model\GuestOrderNoteManagement" />
</config>

Now, as per defined in di.xml file,

Create app/code/Magelearn/OrderNote/Api/Data/OrderNoteInterface.php file.

<?php

namespace Magelearn\OrderNote\Api\Data;

/**
 * Interface OrderNoteInterface
 * @api
 */
interface OrderNoteInterface
{
    /**
     * @return string|null
     */
    public function getNote();

    /**
     * @param string $note
     * @return null
     */
    public function setNote($note);
}

Create app/code/Magelearn/OrderNote/Api/OrderNoteManagementInterface.php file.

<?php
/**
 * Copyright © Scriptlodge. All rights reserved.
 * See LICENSE.txt for license details.
 */
namespace Magelearn\OrderNote\Api;

use Magelearn\OrderNote\Api\Data\OrderNoteInterface;

/**
 * Interface for saving the checkout order Note
 * to the quote for logged in users
 * @api
 */
interface OrderNoteManagementInterface
{
    /**
     * @param int $cartId
     * @param OrderNoteInterface $orderNote
     * @return string
     */
    public function saveOrderNote(
        $cartId,
        OrderNoteInterface $orderNote
    );

    /**
     * @param int $orderId
     * @param OrderNoteInterface $orderNote
     * @return string
     */
    public function saveOrderNoteAdmin(
        $orderId,
        OrderNoteInterface $orderNote
    );
}

Create app/code/Magelearn/OrderNote/Api/GuestOrderNoteManagementInterface.php file.

<?php
/**
 * Copyright © Scriptlodge. All rights reserved.
 * See LICENSE.txt for license details.
 */
namespace Magelearn\OrderNote\Api;

use Magelearn\OrderNote\Api\Data\OrderNoteInterface;

/**
 * Interface for saving the checkout order note
 * to the quote for guest users
 * @api
 */
interface GuestOrderNoteManagementInterface
{
    /**
     * @param string $cartId
     * @param OrderNoteInterface $orderNote
     * @return \Magento\Checkout\Api\Data\PaymentDetailsInterface
     */
    public function saveOrderNote(
        $cartId,
        OrderNoteInterface $orderNote
    );
}
Create app/code/Magelearn/OrderNote/Model/Data/OrderNote.php file.
<?php

namespace Magelearn\OrderNote\Model\Data;

use Magelearn\OrderNote\Api\Data\OrderNoteInterface;
use Magento\Framework\Api\AbstractSimpleObject;

class OrderNote extends AbstractSimpleObject implements OrderNoteInterface
{
    const FIELD_NAME = 'order_note';

    /**
     * @return string|null
     */
    public function getNote()
    {
        return $this->_get(static::FIELD_NAME);
    }

    /**
     * @param string $note
     * @return $this
     */
    public function setNote($note)
    {
        return $this->setData(static::FIELD_NAME, $note);
    }

}

Create app/code/Magelearn/OrderNote/Model/OrderNoteManagement.php file.

<?php

namespace Magelearn\OrderNote\Model;

use Magelearn\OrderNote\Api\OrderNoteManagementInterface;
use Magelearn\OrderNote\Model\Data\OrderNote;
use Magelearn\OrderNote\Model\OrderNoteConfig;
use Magelearn\OrderNote\Api\Data\OrderNoteInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Exception\ValidatorException;
use Magento\Store\Model\ScopeInterface;

class OrderNoteManagement implements OrderNoteManagementInterface
{
    /**
     * @var CartRepositoryInterface
     */
    protected $quoteRepository;

    /**
     * @var ScopeConfigInterface
     */
    protected $scopeConfig;

    /**
     * @param CartRepositoryInterface $quoteRepository
     * @param ScopeConfigInterface $scopeConfig
     */
    public function __construct(
        CartRepositoryInterface $quoteRepository,
        ScopeConfigInterface $scopeConfig
    )
    {
        $this->quoteRepository = $quoteRepository;
        $this->scopeConfig = $scopeConfig;
    }

    /**
     * @param string $ordernote
     * @throws ValidatorException
     */
    protected function validateNote($ordernote)
    {
        $maxLength = $this->scopeConfig->getValue(
            OrderNoteConfig::XML_PATH_GENERAL_MAX_LENGTH,
            ScopeInterface::SCOPE_STORE
        );

        if ($maxLength && (mb_strlen($ordernote) > $maxLength)) {
            throw new ValidatorException(
                __('The order note entered exceeded the limit')
            );
        }
    }

    /**
     * @param int $cartId
     * @param OrderNoteInterface $orderNote
     * @return mixed
     */
    public function saveOrderNote($cartId, OrderNoteInterface $orderNote)
    {
        $quote = $this->quoteRepository->getActive($cartId);

        if (!$quote->getItemsCount()) {
            throw new NoSuchEntityException(
                __('Cart %1 doesn\'t contain products', $cartId)
            );
        }

        $note = $orderNote->getNote();

        $this->validateNote($note);

        try {
            $quote->setData(OrderNote::FIELD_NAME, strip_tags($note));

            $this->quoteRepository->save($quote);
        } catch (\Exception $e) {
            throw new CouldNotSaveException(
                __('The order comment could not be saved')
            );
        }

        return $note;
    }

    /**
     * @param int $orderId
     * @param OrderNoteInterface $orderNote
     * @return mixed
     */
    public function saveOrderNoteAdmin($orderId, OrderNoteInterface $orderNote)
    {
        $quote = $this->quoteRepository->getActive($cartId);

        if (!$quote->getItemsCount()) {
            throw new NoSuchEntityException(
                __('Cart %1 doesn\'t contain products', $cartId)
            );
        }

        $note = $orderNote->getNote();

        $this->validateNote($note);

        try {
            $quote->setData(OrderNote::FIELD_NAME, strip_tags($note));

            $this->quoteRepository->save($quote);
        } catch (\Exception $e) {
            throw new CouldNotSaveException(
                __('The order comment could not be saved')
            );
        }

        return $note;
    }
}

Create app/code/Magelearn/OrderNote/Model/GuestOrderNoteManagement.php file.

<?php

namespace Magelearn\OrderNote\Model;

use Magelearn\OrderNote\Api\Data\OrderNoteInterface;
use Magelearn\OrderNote\Api\GuestOrderNoteManagementInterface;
use Magento\Quote\Model\QuoteIdMaskFactory;
use Magelearn\OrderNote\Api\OrderNoteManagementInterface;

class GuestOrderNoteManagement implements GuestOrderNoteManagementInterface
{

    /**
     * @var QuoteIdMaskFactory
     */
    protected $quoteIdMaskFactory;

    /**
     * @var OrderNoteManagementInterface
     */
    protected $orderNoteManagement;

    /**
     * GuestOrderNoteManagement constructor.
     * @param QuoteIdMaskFactory $quoteIdMaskFactory
     * @param OrderNoteManagementInterface $orderNoteManagement
     */
    public function __construct(
        QuoteIdMaskFactory $quoteIdMaskFactory,
        OrderNoteManagementInterface $orderNoteManagement
    )
    {
        $this->quoteIdMaskFactory = $quoteIdMaskFactory;
        $this->orderNoteManagement = $orderNoteManagement;
    }

    /**
     * @param string $cartId
     * @param OrderNoteInterface $orderNote
     * @return mixed
     */
    public function saveOrderNote($cartId, OrderNoteInterface $orderNote)
    {
        $quoteIdMask = $this->quoteIdMaskFactory->create()
            ->load($cartId, 'masked_id');

        return $this->orderNoteManagement->saveOrderNote(
            $quoteIdMask->getQuoteId(),
            $orderNote
        );
    }
}

Now we will check how to copy this custom field `order_note` data from a quote object to an order object.

Create app/code/Magelearn/OrderNote/etc/extension_attributes.xml file.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
    <extension_attributes for="Magento\Quote\Api\Data\CartInterface">
        <attribute code="order_note" type="string" />
    </extension_attributes>
    <extension_attributes for="Magento\Sales\Api\Data\OrderInterface">
        <attribute code="order_note" type="string" />
    </extension_attributes>
</config>

Create app/code/Magelearn/OrderNote/etc/fieldset.xml file.

<?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">
        <fieldset id="sales_convert_quote">
            <field name="order_note">
                <aspect name="to_order"/>
            </field>
        </fieldset>
    </scope>
</config>

Create app/code/Magelearn/OrderNote/etc/events.xml file.

<?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 name="magelearn_ordernote_sales_model_service_quote_submit_before" instance="Magelearn\OrderNote\Observer\SaveOrderBeforeSalesModelQuoteObserver" />
    </event>
</config>

Create app/code/Magelearn/OrderNote/Observer/SaveOrderBeforeSalesModelQuoteObserver.php file.

<?php

namespace Magelearn\OrderNote\Observer;

use Magento\Framework\Event\ObserverInterface;

class SaveOrderBeforeSalesModelQuoteObserver implements ObserverInterface
{
    /**
     * @var \Magento\Framework\DataObject\Copy
     */
    protected $objectCopyService;


    /**
     * @param \Magento\Framework\DataObject\Copy $objectCopyService
     */
    public function __construct(
        \Magento\Framework\DataObject\Copy $objectCopyService
    )
    {
        $this->objectCopyService = $objectCopyService;
    }

    /**
     * @param \Magento\Framework\Event\Observer $observer
     */
    public function execute(\Magento\Framework\Event\Observer $observer)
    {
        /* @var \Magento\Sales\Model\Order $order */
        $order = $observer->getEvent()->getData('order');
        /* @var \Magento\Quote\Model\Quote $quote */
        $quote = $observer->getEvent()->getData('quote');
       /* print_r($quote->getEntityId());
        print_r($observer->getEvent()->getData());
        exit('kk');*/
        $order->setOrderNote($quote->getOrderNote());

        $this->objectCopyService->copyFieldsetToTarget('sales_convert_quote', 'to_order', $quote, $order);

        return $this;
    }
}

Now we will provide some system configuration for this order note module.
Create etc/adminhtml/system.xml file.

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <section id="order_note" translate="label" type="text" sortOrder="1001" showInDefault="1" showInWebsite="1" showInStore="1">
            <class>separator-top</class>
            <label>Order note</label>
            <tab>sales</tab>
            <resource>Magelearn_OrderNote::config_ordernote</resource>
            <group id="general" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0">
                <label>Order Note: General</label>
                <field id="enabled" translate="label" type="select" sortOrder="0" showInDefault="1">
                    <label>Enabled</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="is_show_in_myaccount" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Show In Customer Account?</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                   <comment><![CDATA[If <strong>Yes</strong> the order note will show on customer frontend account,
					in <strong>"My Account -> My Orders -> View Order"</strong>]]></comment>
                </field>
                <field id="max_length" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Maximum Characters Length (limit)</label>
					<validate>validate-number</validate>
                    <comment><![CDATA[Enter the maximum characters allowed for the order comment at the checkout page.
						Eg: <strong>200</strong>. Leave empty for no characters limit]]></comment>
                </field>
            </group>
        </section>
    </system>
</config>

We will also provide some default configuration values.
For that, add etc/config.xml file.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <magelearn_ordernote>
            <general>
            	<enabled>1</enabled>
                <is_show_in_myaccount>1</is_show_in_myaccount>
                <max_length></max_length>
            </general>
        </magelearn_ordernote>
    </default>
</config>

Now, to display this order_note text area field on checkout page, we will modify checkout_index_index.xml file

Add app/code/Magelearn/OrderNote/view/frontend/layout/checkout_index_index.xml file.

Here we have defined custom validation rule and front-end block file to display this order_note field.

<?xml version="1.0"?>
<!--
/**
* Copyright © Scriptlodge. All rights reserved.
* See LICENSE.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.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="steps" xsi:type="array">
									<item name="children" xsi:type="array">
										<item name="billing-step" xsi:type="array">
											<item name="children" xsi:type="array">
												<item name="payment" xsi:type="array">
													<item name="children" xsi:type="array">
														<item name="additional-payment-validators" xsi:type="array">
															<item name="children" xsi:type="array">
																<item name="order-note-validator" xsi:type="array">
																	<item name="component" xsi:type="string">Magelearn_OrderNote/js/view/checkout/order-note-validator</item>
																</item>
															</item>
														</item>
														<item name="payments-list" xsi:type="array">
															<item name="children" xsi:type="array">
																<item name="before-place-order" xsi:type="array">
																	<item name="children" xsi:type="array">
																		<item name="comment" xsi:type="array">
																			<item name="component" xsi:type="string">Magelearn_OrderNote/js/view/checkout/order-note-block</item>
																		</item>
																	</item>
																</item>
															</item>
														</item>
													</item>
												</item>
											</item>
										</item>
									</item>
								</item>
							</item>
						</item>
					</item>
				</argument>
			</arguments>
		</referenceBlock>
	</body>
</page>

Before proceed further with this, we will add some additional variables in window checkout configuration on checkout page.
We will set some values for this additional variables and then use it in JS configuration.

Add app/code/Magelearn/OrderNote/etc/frontend/di.xml file.

<?xml version="1.0"?>
<!--
/**
 * Copyright © Scriptlodge. All rights reserved.
 * See LICENSE.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Checkout\Model\CompositeConfigProvider">
        <arguments>
            <argument name="configProviders" xsi:type="array">
                <item name="internal_order_note_config_provider" xsi:type="object">Magelearn\OrderNote\Model\OrderNoteConfig</item>
            </argument>
        </arguments>
    </type>
</config>

After that, we will create app/code/Magelearn/OrderNote/Model/OrderNoteConfig.php file.

<?php

namespace Magelearn\OrderNote\Model;

use Magento\Checkout\Model\ConfigProviderInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;
use Magento\Authorization\Model\UserContextInterface;


class OrderNoteConfig implements ConfigProviderInterface
{
    /**
     *  Config Paths
     */
    const XML_PATH_GENERAL_ENABLED = 'order_note/general/enabled';
    const XML_PATH_GENERAL_IS_SHOW_IN_MYACCOUNT = 'order_note/general/is_show_in_myaccount';
    const XML_PATH_GENERAL_MAX_LENGTH = 'order_note/general/max_length';


    /**
     * @var \Magento\Authorization\Model\UserContextInterface
     */
    private $userContext;


    /**
     * @var \Magento\Quote\Api\CartRepositoryInterface
     */
    private $quoteRepository;


    /**
     * @var \Magento\Framework\App\Action\Context
     */
    private $context;

    /**
     * @var \Magento\Quote\Api\Data\CartInterface
     */
    private $quote;

    /**
     * @var ScopeConfigInterface
     */
    private $scopeConfig;

    /**
     * @param ScopeConfigInterface $scopeConfig
     */
    public function __construct(
        UserContextInterface $userContext,
        \Magento\Quote\Api\CartRepositoryInterface $quoteRepository,
        \Magento\Checkout\Model\Session $session,
        ScopeConfigInterface $scopeConfig
    )
    {
        $this->userContext = $userContext;
        $this->quoteRepository = $quoteRepository;
        $this->scopeConfig = $scopeConfig;
        $this->session = $session;
    }

    public function isEnabled()
    {
        return $this->scopeConfig->getValue(
            self::XML_PATH_GENERAL_ENABLED,
            ScopeInterface::SCOPE_STORE
        );
    }

    /**
     * Check if show order note to customer account
     *
     * @return bool
     */
    public function isShowNoteInAccount()
    {
        return $this->scopeConfig->getValue(
            self::XML_PATH_GENERAL_IS_SHOW_IN_MYACCOUNT,
            ScopeInterface::SCOPE_STORE
        );
    }
    /**
     * Get order note max length
     *
     * @return int
     */
    public function getConfig()
    {
        $show_order_note = $this->getItemInternalOrderNote();
        return [
            'max_length' => (int)$this->scopeConfig->getValue(self::XML_PATH_GENERAL_MAX_LENGTH, ScopeInterface::SCOPE_STORE),
            'show_order_note' => $show_order_note
        ];
    }

    public function getItemInternalOrderNote()
    {
        $showOrderNote = false;
        if (!$this->isEnabled()) {
            return $showOrderNote;
        } else {
        	$showOrderNote = true;
        }

        return $showOrderNote;
    }


    /**
     * Get quote.
     *
     * @return \Magento\Quote\Api\Data\CartInterface
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    public function getQuote()
    {
        if ($this->quote === null) {
            try {
                if ($this->userContext->getUserId()) {
                    $this->quote = $this->quoteRepository->getActiveForCustomer($this->userContext->getUserId());
                } else {
                    $this->quote = $this->session->getQuote();
                }

            } catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
                // $this->quote = $this->quoteRepository->get($id);
            }
        }

        return $this->quote;
    }
}

After adding above code, you can check 'window.checkoutConfig.max_length' and 'window.checkoutConfig.show_order_note' are available in your js on the checkout page.

Now, as per defined in checkout_index_index.xml file, we will add our JS component.
Add app/code/Magelearn/OrderNote/view/frontend/web/js/view/checkout/order-note-block.js file.

define(
    [
        'jquery',
        'uiComponent',
		'knockout'
    ],
    function ($, Component, ko) {
        'use strict';

		/**
		 * @param {Function} target
		 * @param {String} maxLength
		 * @return {*}
		 */
        ko.extenders.maxNoteLength = function (target, maxLength) {
            var timer;

            var result = ko.computed({
                read: target,
                write: function (val) {
                    if (maxLength > 0) {
                        clearTimeout(timer);
                        if (val.length > maxLength) {
                            var limitedVal = val.substring(0, maxLength);
                            if (target() === limitedVal) {
                                target.notifySubscribers();
                            } else {
                                target(limitedVal);
                            }
                            result.css("_error");
                            timer = setTimeout(function () { result.css(""); }, 800);
                        } else {
                            target(val);
                            result.css("");
                        }
                    } else {
                        target(val);
                    }
                }
            }).extend({ notify: 'always' });

            result.css = ko.observable();
            result(target());

            return result;
        };


        return Component.extend({
            defaults: {
                template: 'Magelearn_OrderNote/checkout/order-note-block'
            },

            initialize: function() {
                this._super();
                var self = this;
				this.note = ko.observable("").extend(
					{
						maxNoteLength: this.getMaxNoteLength()
					}
				);
                this.remainingCharacters = ko.computed(function(){
                    return self.getMaxNoteLength() - self.note().length;
                });
            },

            /**
             * Is order note has max length
             *
             * @return {Boolean}
             */
            hasMaxNoteLength: function() {
               return window.checkoutConfig.max_length > 0;
            },

            /**
             * Retrieve order note length limit
             *
             * @return {String}
             */
            getMaxNoteLength: function () {
              return window.checkoutConfig.max_length;
            },

            /**
             * Show/hide Order note Text Field based on condition
             *
             * @return {String}
             */
            isShowOrderNoteTextField: function () {
                return window.checkoutConfig.show_order_note > 0;
            }
        });
    }
);

Now, add app/code/Magelearn/OrderNote/view/frontend/web/template/checkout/order-note-block.html file.

<div data-bind=" if: isShowOrderNoteTextField()">
    <div class="payment-option _collapsible opc-payment-additional internal_order_note last"
         data-bind="mageInit: {'collapsible':{'openedState': '_active'}}">
        <div class="payment-option-title field choice" data-role="title">
        <span class="action action-toggle" role="heading" aria-level="2">
            <!-- ko i18n: 'Leave A Order Note'--><!-- /ko -->
        </span>
        </div>
        <div class="payment-option-content" data-role="content">
            <form class="form form-discount order-note-form">
                <div class="payment-option-inner">
                    <div class="field" data-bind="css: note.css()">
                        <label class="label">
                            <span data-bind="i18n: 'Enter Order Note'"></span>
                        </label>
                        <div class="control">
						<textarea class="order-note input-text order-note internal-order-note"
                                  name="internal-order-note" rows="4"
                                  data-bind="value: note,valueUpdate: 'afterkeydown',attr:{placeholder: $t('Enter your note...')} "></textarea>
                            <p data-bind="if: hasMaxNoteLength()">
							<span class="remaining-chars">
								<span class="chars-value" data-bind="text: remainingCharacters"></span>
								<span class="chars-label">
								<!-- ko i18n: 'remaining characters'--><!-- /ko -->
								</span>
							</span>
                            </p>
                        </div>
                    </div>
                </div>
            </form>
        </div>
    </div>
</div>

We will also add our validation files.
Add app/code/Magelearn/OrderNote/view/frontend/web/js/view/checkout/order-note-validator.js file.

define(
    [
        'uiComponent',
        'Magento_Checkout/js/model/payment/additional-validators',
        'Magelearn_OrderNote/js/model/checkout/order-note-validator'
    ],
    function (Component, additionalValidators, ordernoteValidator) {
        'use strict';

        additionalValidators.registerValidator(ordernoteValidator);

        return Component.extend({});
    }
);

Add app/code/Magelearn/OrderNote/view/frontend/web/js/model/checkout/order-note-validator.js file.

define(
    [
        'jquery',
        'Magento_Customer/js/model/customer',
        'Magento_Checkout/js/model/quote',
        'Magento_Checkout/js/model/url-builder',
        'mage/url',
        'Magento_Checkout/js/model/error-processor',
        'Magento_Ui/js/model/messageList',
        'mage/translate'
    ],
	function ($, customer, quote, urlBuilder, urlFormatter, errorProcessor, messageContainer, __) {
        'use strict';

        return {

            /**
             * Make an ajax PUT request to save internal order note in the quote.
             *
             * @returns {Boolean}
             */
            validate: function () {
                if(this.isOrderNoteEnable()==0){
                    return true;
                }
                var isCustomer = customer.isLoggedIn();
                var form = $('.payment-method input[name="payment[method]"]:checked').parents('.payment-method').find('form.order-note-form');

                var quoteId = quote.getQuoteId();
                var url;

                // validate max length
                var order_note = $('.input-text.internal-order-note').val();
                console.log(order_note);
                if (this.hasMaxOrderNoteLength() && order_note.length > this.getMaxOrderNoteLength()) {
                    messageContainer.addErrorMessage({ message: __("The internal order note entered exceeded the limit") });
                    return false;
                }

                if (isCustomer) {
                    url = urlBuilder.createUrl('/carts/mine/setordernote', {})
                } else {
                    url = urlBuilder.createUrl('/guest-carts/:cartId/setordernote', {cartId: quoteId});
                }

                var payload = {
                    cartId: quoteId,
                    orderNote: {
                        note: order_note
                    }
                };

                if (!payload.orderNote.note) {
                    return true;
                }

                var result = true;

                $.ajax({
                    url: urlFormatter.build(url),
                    data: JSON.stringify(payload),
                    global: false,
                    contentType: 'application/json',
                    type: 'PUT',
                    async: false
                }).done(
                    function (response) {
                        result = true;
                    }
                ).fail(
                    function (response) {
                        result = false;
                        errorProcessor.process(response);
                    }
                );

                return result;
            },

            /**
             * Is order note has max length
             *
             * @return {Boolean}
             */
            hasMaxOrderNoteLength: function() {
                 return window.checkoutConfig.max_length > 0;
            },

            /**
             * Retrieve order note length limit
             *
             * @return {String}
             */
            getMaxOrderNoteLength: function () {
                 return window.checkoutConfig.max_length;
            },
            /**
             * Show/hide Order note Text Field based on condition
             *
             * @return {String}
             */
            isOrderNoteEnable: function () {
                return window.checkoutConfig.show_order_note > 0;
            }
        };
    }
);

Also add css _module.less file at app/code/Magelearn/OrderNote/view/frontend/web/css/source/_module.less file.

Now, to display this order_note field at admin and provide Add/Edit functionality for this order note,
we will add app/code/Magelearn/OrderNote/view/adminhtml/layout/sales_order_view.xml file. 

<?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="order_additional_info">
            <block class="Magento\Backend\Block\Template" name="ml_order_notes" template="Magelearn_OrderNote::order/view/notes.phtml"/>
        </referenceBlock>
    </body>
</page>

Now we will add etc/adminhtml/di.xml file and app our plugin for Magento\Sales\Block\Adminhtml\Order\View\Info file.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Sales\Block\Adminhtml\Order\View\Info">
        <plugin name="magelearn_ordernote_show_note"
		        type="Magelearn\OrderNote\Plugin\Block\Adminhtml\SalesOrderViewInfo"
				sortOrder="8888" />
    </type>
</config>

Now, add app/code/Magelearn/OrderNote/Plugin/Block/Adminhtml/SalesOrderViewInfo.php file.

<?php

namespace Magelearn\OrderNote\Plugin\Block\Adminhtml;

use Magento\Backend\Block\Template\Context;
use Magelearn\OrderNote\Model\Data\OrderNote;
use Magento\Sales\Block\Adminhtml\Order\View\Info as ViewInfo;
use Magelearn\OrderNote\Model\OrderNoteConfig;

class SalesOrderViewInfo {
	
	/**
     * @var OrderNoteConfig
     */
    protected $orderNoteConfig;
	
	public function __construct(
		Context $context,
		OrderNoteConfig $orderNoteConfig
	) {
		$this ->_urlBuilder = $context->getUrlBuilder();
		$this->orderNoteConfig = $orderNoteConfig;
	}

	/**
	 * @param ViewInfo $subject
	 * @param string $result
	 * @return string
	 * @throws \Magento\Framework\Exception\LocalizedException
	 */
	public function afterToHtml(ViewInfo $subject, $result) {
		$noteBlock = $subject->getLayout()->getBlock('ml_order_notes');
		$orderId = $subject->getOrder()->getData('entity_id');
		if ($noteBlock !== false) {
			$editUrl = $this->getOrderAttributeEditUrl($orderId);
			$isAllowedToAddNote = $this->getItemInternalOrderNote($subject->getOrder());
			$noteBlock-> setOrderNote($subject->getOrder()->getData(OrderNote::FIELD_NAME));
			$noteBlock->setOrderId($subject->getOrder()->getData('entity_id'));
			$noteBlock->setNoteUrl($editUrl);
			$noteBlock->setIsAllowedToAddNote($isAllowedToAddNote);
			// $result = $result . $noteBlock->toHtml();
		}

		return $result;
	}

	/**
	 * @return string
	 */
	protected function getOrderAttributeEditUrl($orderId) {
		return $this->getUrl('ordernote/note/edit', ['order_id' => $orderId]);
	}

	/**
	 * Generate url by route and parameters
	 *
	 * @param string $route
	 * @param array $params
	 * @return  string
	 */
	public function getUrl($route = '', $params = []) {
		return $this->_urlBuilder->getUrl($route, $params);
	}

	public function getItemInternalOrderNote($order) {
		$showOrderNote = false;
		if(!$this->isEnabled()) {
			return $showOrderNote;
		} else {
			return $showOrderNote = true;
		}
		
		return $showOrderNote;
	}
	
	/**
     * Check if extension is enabled or not
     *
     * @return bool
     */
    public function isEnabled()
    {
        return $this->orderNoteConfig->isEnabled();
    }
}

Now, we will add app/code/Magelearn/OrderNote/view/adminhtml/templates/order/view/notes.phtml file.

<?php
$orderId = $block->getOrderId();
$note = trim($block->getOrderNote());
$isAllowedToAddNote = $block->getIsAllowedToAddNote();
$noteUrl = $block->getNoteUrl();
$label = __('Add');
if ($note) {
    $label = __('Edit');
}
//$link = sprintf('<a href="%s">%s</a>', $noteUrl, $label);
?>
<?php if ($isAllowedToAddNote) { ?>

    <section class="admin__page-section ordernote">
        <div class="admin__page-section-title">
            <span class="title"><?= $block->escapeHtml(__('Order Note')) ?></span>
            <div class="actions note-link" style="padding: 5px;"><a href="javascript:;"><?= $label; ?></a></div>
        </div>

        <div class="admin__page-section-content admin__field-control input" id="input-box" style="display: none;">
                <textarea id="order_note" name="order_note" rows="3" cols="5" class="admin__control-textarea" style="width: 50%"></textarea>
            <div class="admin__page-section-content actions" style="padding: 10px 0;">
                <button id="note_submit" class="action-default scalable action-save action-secondary"
                        type="button"><?= $block->escapeHtml(__('Save note')) ?></button>
            </div>
        </div>

        <div class="admin__page-section-content text-note" id="text-note">
            <?php
            if ($note) {
                echo $note;
                } else {
                echo $block->escapeHtml(__('No note for this order'));
                }
            ?>
        </div>


        <script>
        require([
            'jquery',
            'jquery/ui',
            //'Magento_Checkout/js/model/url-builder',
            'mage/url'
        ], function ($, ui, urlFormatter) {
            'use strict';
            var ajaxRequest;


            $(document).on('click', '#note_submit', function () {
                var orderId =<?php echo $orderId; ?>;
                var order_note = $('#order_note').val();

                ajaxRequest = $.ajax({
                    url: "<?php echo $noteUrl; ?>",
                    type: 'post',
                    data: {order_id: orderId, note: order_note, form_key: FORM_KEY},
                    dataType: 'json',
                    beforeSend: function () {
                        $('body').trigger('processStart');
                    }
                });

                //Show successfully for submit message
                ajaxRequest.done(function (response, textStatus, jqXHR) {
                    if (response.error == false) {
                        $('#text-note').html(order_note);
                        $('#input-box').slideToggle(200);
                    } else {
                        alert(response.message);
                    }
                    $('body').trigger('processStop');
                });
                ajaxRequest.fail(function () {
                    alert('Oops, An error occured, please try again later!');
                });
            });

            $(".actions.note-link").on("click", function (event) {
                $('#input-box').slideToggle(200);
            });


        });
        </script>
    </section>
<?php } ?>

Now, as per the highlighted code above, to give Add/Edit url for order note,

First we will add app/code/Magelearn/OrderNote/etc/adminhtml/routes.xml file.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="admin">
        <route id="order_note" frontName="ordernote">
            <module name="Magelearn_OrderNote"/>
        </route>
    </router>
</config>

Now, we will create our controller file at app/code/Magelearn/OrderNote/Controller/Adminhtml/Note/Edit.php

<?php

namespace Magelearn\OrderNote\Controller\Adminhtml\Note;

use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Backend\App\Action\Context;
use Magento\Sales\Model\OrderFactory;


class Edit extends \Magento\Backend\App\Action implements HttpPostActionInterface
{
    protected $resultPageFactory;
    protected $resultJsonFactory;
    /**
     * Authorization level of a basic admin session
     *
     * @see _isAllowed()
     */
    const ADMIN_RESOURCE = 'Magento_Sales::comment';


    public function __construct(
        \Magento\Backend\App\Action\Context $context,
        \Magento\Framework\View\Result\PageFactory $resultPageFactory,
        \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory,
        OrderFactory $orderFactory

    )
    {
        parent::__construct($context);
        $this->resultPageFactory = $resultPageFactory;
        $this->resultJsonFactory = $resultJsonFactory;
        $this->orderFactory = $orderFactory;
    }

    public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException
    {
        return null;
    }

    public function validateForCsrf(RequestInterface $request): ?bool
    {
        return true;
    }

    public function execute()
    {
        try {
            $params = $this->getRequest()->getParams();
            if (isset($params['order_id'])) {
                $note = trim($params['note']);
                $orderModel = $this->orderFactory->create();
                $orderModel->load($params['order_id']);
                $orderModel->setOrderNote($note)->save();

                $resultJson = $this->resultJsonFactory->create();
                $response = ['error' => false, 'message' => __('Sucessfully added order note.')];
                $resultJson->setData($response);
                return $resultJson;
            } else {
                throw new \Magento\Framework\Exception\LocalizedException(
                    __('The order id is missing. Refresh try again.')
                );
            }


        } catch (\Magento\Framework\Exception\LocalizedException $e) {
            $response = ['error' => true, 'message' => $e->getMessage()];
        } catch (\Exception $e) {

            $response = ['error' => true, 'message' => __('We cannot add order note.')];
        }
        if (is_array($response)) {
            $resultJson = $this->resultJsonFactory->create();
            $resultJson->setData($response);
            return $resultJson;
        }
        return $this->resultRedirectFactory->create()->setPath('sales/*/');

    }

    protected function _isAllowed()
    {
        return true;
    }
}

Now, to display this order_note in Sales >> Order admin grid,

We will add app/code/Magelearn/OrderNote/view/adminhtml/ui_component/sales_order_grid.xml file.

<?xml version="1.0" encoding="UTF-8"?>

<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <columns name="sales_order_columns">
       <column name="order_note" class="\Magelearn\OrderNote\Ui\Component\Listing\Column\OrderNote">
            <settings>
                <filter>text</filter>
                <label translate="true">Order Note</label>
                <sortable>false</sortable>
            </settings>
        </column>
    </columns>
</listing>

Now, we will add app/code/Magelearn/OrderNote/Ui/Component/Listing/Column/OrderNote.php file.

<?php
namespace Magelearn\OrderNote\Ui\Component\Listing\Column;
 
use \Magento\Sales\Api\OrderRepositoryInterface;
use \Magento\Framework\View\Element\UiComponent\ContextInterface;
use \Magento\Framework\View\Element\UiComponentFactory;
use \Magento\Ui\Component\Listing\Columns\Column;
use \Magento\Framework\Api\SearchCriteriaBuilder;
use \Magento\Store\Model\StoreManagerInterface;
 
class OrderNote extends Column
{
    /**
     * @var OrderRepositoryInterface
     */
    protected $_orderRepository;
    /**
     * @var SearchCriteriaBuilder
     */
    protected $_searchCriteria;
    /**
     * @var \Magento\Framework\View\Asset\Repository
     */
    protected $_assetRepository;
    /**
     * @var \Magento\Framework\App\RequestInterface
     */
    protected $_requestInterfase;
    /**
     * @var \Magento\Sales\Model\OrderFactory
     */
    protected $_orderFactory;
     
    /**
     * Products constructor.
     * @param ContextInterface $context
     * @param UiComponentFactory $uiComponentFactory
     * @param OrderRepositoryInterface $orderRepository
     * @param \Magento\Framework\View\Asset\Repository $assetRepository
     * @param \Magento\Framework\App\RequestInterface $requestInterface
     * @param SearchCriteriaBuilder $criteria
     * @param \Magento\Sales\Model\OrderFactory $orderFactory
     * @param array $components
     * @param array $data
     */
    public function __construct(
        ContextInterface $context,
        UiComponentFactory $uiComponentFactory,
        OrderRepositoryInterface $orderRepository,
        \Magento\Framework\View\Asset\Repository $assetRepository,
        \Magento\Framework\App\RequestInterface $requestInterface,
        SearchCriteriaBuilder $criteria,
        \Magento\Sales\Model\OrderFactory $orderFactory,
        array $components = [],
        array $data = []
    ) {
     
        $this->_orderRepository = $orderRepository;
        $this->_searchCriteria  = $criteria;
        $this->_assetRepository = $assetRepository;
        $this->_requestInterfase= $requestInterface;
        $this->_orderFactory    = $orderFactory;
        parent::__construct($context, $uiComponentFactory, $components, $data);
    }
 
    public function prepareDataSource(array $dataSource)
    {
        //echo "<pre>";print_r($dataSource);exit;     
        if (isset($dataSource['data']['items'])) {
            foreach ($dataSource['data']['items'] as & $item) {
                $order  = $this->_orderRepository->get($item["entity_id"]);
                $order_note = $order->getData('order_note');
                $item['order_note'] = $order_note;
            }
        }
        return $dataSource;
    }
}

Now, to display this order_note at front-end we will add app/code/Magelearn/OrderNote/view/frontend/layout/sales_order_view.xml file.

<?xml version="1.0"?>
<!--
/**
 * Copyright © Scriptlodge. All rights reserved.
 * See LICENSE.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="content">
           <block class="Magelearn\OrderNote\Block\Order\Note" as="ordernote" name="sales.order.note" after="sales.order.info"/>
        </referenceContainer>
    </body>
</page>

Now, we will add app/code/Magelearn/OrderNote/Block/Order/Note.php file.

<?php
namespace Magelearn\OrderNote\Block\Order;

use Magento\Framework\View\Element\Template;
use Magento\Framework\View\Element\Template\Context;
use Magelearn\OrderNote\Model\Data\OrderNote;
use Magento\Framework\Registry;
use Magento\Sales\Model\Order;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magelearn\OrderNote\Model\OrderNoteConfig;

class Note extends Template
{
    /**
     * @var OrderNoteConfig
     */
    protected $orderNoteConfig;

    /**
     * @var Registry
     */
    protected $coreRegistry = null;

    /**
     * @param    Context $context
     * @param    Registry $registry
     * @param    OrderNoteConfig $orderNoteConfig
     * @param   array $data
     */
    public function __construct(
        Context $context,
        Registry $registry,
        OrderNoteConfig $orderNoteConfig,
        array $data = []
    ) {
        $this->coreRegistry = $registry;
        $this->orderNoteConfig = $orderNoteConfig;
        $this->_isScopePrivate = true;
        $this->_template = 'order/view/note.phtml';
        parent::__construct($context, $data);

    }

    /**
     * Check if show order note to customer account
     *
     * @return bool
     */
    public function isShowNoteInAccount()
    {
        return $this->orderNoteConfig->isShowNoteInAccount();
    }

    /**
     * Get Order
     *
     * @return array|null
     */
    public function getOrder()
    {
        return $this->coreRegistry->registry('current_order');
    }

    /**
     * Get Order Note
     *
     * @return string
     */
    public function getOrderNote()
    {
        return trim($this->getOrder()->getData(OrderNote::FIELD_NAME));
    }

    /**
     * Retrieve html Note
     *
     * @return string
     */
    public function getOrderNoteHtml()
    {
        return nl2br($this->escapeHtml($this->getOrderNote()));
    }

    /**
     * Check if has order Note
     *
     * @return bool
     */
    public function hasOrderNote()
    {
        return strlen($this->getOrderNote()) > 0;
    }
}

Now, we will add app/code/Magelearn/OrderNote/view/frontend/templates/order/view/note.phtml file.

<?php
/** @var \Magelearn\OrderNote\Block\Order\Note $block */

?>
<?php if ($block->isShowNoteInAccount()): ?>
    <?php if ($note = $block->getOrderNote()): ?>
    <div class="box box-order-note">
            <div class="box-title">
                <strong><?= $block->escapeHtml(__('Order note')) ?></strong>
            </div>
            <div class="box-content">
                <?= $block->getOrderNoteHtml(); ?>
            </div>
        </div>
    <?php endif; ?>
<?php endif; ?>

We will also add our translation file in i18n directory at app/code/Magelearn/OrderNote/i18n/en_US.csv

0 Comments On "Add Order note textarea field to Magento one step checkout and Add/edit note from admin with AJAX"

Back To Top