One of our clients needed to add a custom order status “in preparation” and to send an email along with changing to this status. Moreover, the client’s requirement included the ability to change the status of the order from the orders grid using the mass action.
To solve the problem you will need to:
- create and configure new order status;
- add email template and settings for it;
- add a mass action to the orders grid;
- register an order change event and send an email when the order status is changed.
Ok, let’s start.
Creating a new order status
Open the Magento 2 admin and go to Stores ➜ Settings ➜ Order Status. Click Create New Status.
Fill in the form. In our case, a preparation status is created with the “In preparation” label. Click Save Status.
After that click the Assign Status to State button and assign the “In preparation” status to the “Processing” state.
Step 1 is completed. Well done!
To perform the following steps, you need to create a module:
The symbol “$>” will indicate the actions performed in the bash console.
$> mkdir -p app/code/Belvg/OrderCustomStatus/etc
$> cd app/code/Belvg/OrderCustomStatus
Next come all the paths relative to the root of the module pp/code/Belvg/OrderCustomStatus. The content of the file follows Immediately after the “touch” command.
$> touch etc/module.xml
1 2 3 4 |
<?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="Belvg_OrderCustomStatus" setup_version="0.0.1" /> </config> |
$> touch registration.php
1 2 3 4 5 6 |
<?php \Magento\Framework\Component\ComponentRegistrar::register( \Magento\Framework\Component\ComponentRegistrar::MODULE, 'Belvg_OrderCustomStatus', __DIR__ ); |
These are the basic files of the empty module.
Next, you need to store the “Status Code” of the created status somewhere, so as to use a constant in the code, and not a string.
$> mkdir Model
$> touch Model/Order.php
1 2 3 4 5 6 |
<?php namespace Belvg\OrderCustomStatus\Model; class Order extends \Magento\Sales\Model\Order { const STATUS_PREPARATION = "preparation"; } |
Adding an email template and its setting
The email template itself is declared in the email_templates.xml file.
$> touch etc/email_templates.xml
1 2 3 4 |
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Email:etc/email_templates.xsd"> <template id="sales_email_order_template_preparation" label="Preparation Shipping Order" file="order_preparation.html" type="html" module="Belvg_OrderCustomStatus" area="frontend"/> </config> |
We announced a new email template with an identifier “sales_email_order_template_preparation” – it will be used in the code when generating an email, label is displayed in the email templates list in Marketing ➜ Communication ➜ Email templates;
file=”order_preparation.html”, module=”Belvg_OrderCustomStatus”, area=”frontend”, module=”Belvg_OrderCustomStatus” indicate that the file with the content of a template is located in the folder of the “Belvg_OrderCustomStatus” module (this is our module) in the view/frontend/email folder. From here Magento will try to download a template when creating a template in Marketing ➜ Communication ➜ Email templates. This is defined in xml. since the place can be in adminhtml as well. Everything depends on the specified area attribute.
Let’s create this file:
$> mkdir -p view/frontend/email/
$> touch view/frontend/email/order_preparation.html
1 2 3 4 5 6 7 |
<!--@subject {{trans "Your %store_name order has shipped" store_name=$store.getFrontendName()}} @--> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <body> {{trans "Your order is being prepared by one of our flower enthusiast and will be on the way soon." }} </body> </html> |
Here you must specify “@subject”. From here Magento will try to get the subject of the email by default.
Create a setting in Stores ➜ Settings ➜ Configuration ➜ Sales ➜ Sales Emails ➜ Order ➜ Order Preparation Template, so that you can override the email template for different stores.
$> mkdir -p etc/adminhtml
$> touch etc/adminhtml/system.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?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="sales_email" translate="label" type="text" sortOrder="301" showInDefault="1" showInWebsite="1" showInStore="1"> <group id="order" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1"> <field id="template_preparation" translate="label comment" type="select" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Order Preparation Template</label> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> </field> </group> </section> </system> </config> |
Add a mass action to the orders grid
The view/adminhtml/ui_component/sales_order_grid.xml file is responsible for adding a mass action.
Let’s create it:
$> mkdir -p view/adminhtml/ui_component/
$> touch view/adminhtml/ui_component/sales_order_grid.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?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"> <listingToolbar name="listing_top"> <massaction name="listing_massaction" component="Magento_Ui/js/grid/tree-massactions"> <action name="order_preparation"> <settings> <url path="ordercustomstatus/order/preparation"/> <type>order_preparation</type> <label translate="true">Orders processed</label> </settings> </action> </massaction> </listingToolbar> </listing> |
An action is created here named “Orders processed”. If it is selected, all the marked orders will be sent to the URL <admin_backend>/ordercustomstatus/order/preparation.
First of all, in order to declare the base URL of our module, Magento will send all requests of the <admin_backend>/ordercustomstatus/* form to our module $> touch etc/adminhtml/routes.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:framework:App/etc/routes.xsd"> <router id="admin"> <route id="ordercustomstatus" frontName="ordercustomstatus"> <module name="Belvg_OrderCustomStatus" before="Magento_Backend" /> </route> </router> </config> |
Now we create a controller, which will process the query from the mass action:
$> mkdir -p Controller/Adminhtml/Order
$> touch Preparation.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 77 78 |
<?php namespace Belvg\OrderCustomStatus\Controller\Adminhtml\Order; use Magento\Ui\Component\MassAction\Filter; use Magento\Backend\App\Action\Context; use Magento\Framework\Controller\ResultFactory; class Preparation extends \Magento\Backend\App\Action { /** * Authorization level of a basic admin session */ const ADMIN_RESOURCE = 'Magento_Sales::order_statuses'; /** * @var \Magento\Sales\Model\ResourceModel\Order\CollectionFactory */ protected $collectionFactory; /** * @var Filter */ protected $filter; /** * @var \Magento\Sales\Api\OrderRepositoryInterface */ protected $orderRepository; protected $redirectUrl = '*/*/'; /** * Preparation constructor. * @param Context $context * @param Filter $filter * @param \Magento\Sales\Model\ResourceModel\Order\CollectionFactory $orderCollectionFactory * @param \Magento\Sales\Api\OrderRepositoryInterface $orderRepository */ public function __construct( Context $context, Filter $filter, \Magento\Sales\Model\ResourceModel\Order\CollectionFactory $orderCollectionFactory, \Magento\Sales\Api\OrderRepositoryInterface $orderRepository ) { parent::__construct($context); $this->collectionFactory = $orderCollectionFactory; $this->orderRepository = $orderRepository; $this->filter = $filter; } public function execute() { try { $collection = $this->filter->getCollection($this->collectionFactory->create()); //mass action $countPreparationOrder = 0; /** @var \Magento\Sales\Model\Order $order */ foreach ($collection->getItems() as $order) { if ($order->getState() != \Magento\Sales\Model\Order::STATE_NEW) { continue; } $order->setStatus(\Belvg\OrderCustomStatus\Model\Order::STATUS_PREPARATION); $this->orderRepository->save($order); $order->getBillingAddress()->getEmail(); $countPreparationOrder++; } $countNonPreparationOrder = $collection->count() - $countPreparationOrder; if ($countNonPreparationOrder && $countPreparationOrder) { $this->messageManager->addErrorMessage(__('%1 order(s) cannot be preparation.', $countNonPreparationOrder)); } elseif ($countNonPreparationOrder) { $this->messageManager->addErrorMessage(__('You cannot preparation the order(s).')); } if ($countPreparationOrder) { $this->messageManager->addSuccessMessage(__('We send preparation email for %1 order(s).', $countPreparationOrder)); } $resultRedirect = $this->resultRedirectFactory->create(); $resultRedirect->setPath($this->filter->getComponentRefererUrl() ?: 'sales/*/'); return $resultRedirect; } catch (\Exception $e) { $this->messageManager->addErrorMessage($e->getMessage()); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); return $resultRedirect->setPath($this->redirectUrl); } } } |
In this controller, the control is transferred to the execute method, in which orders from the collection are selected one by one. It is also checked that the state of the order is STATE_NEW, its status is changed and gets written to the database. It is important to use \Magento\Sales\Api\OrderRepositoryInterface, otherwise the order change event will not work.
Registering an order change event and sending an email when the order status is changed
Create an events.xml file:
$> touch etc/events.xml
1 2 3 4 5 6 |
<?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_order_save_after"> <observer name="ordercustomstatus_sales_order_save_after" instance="Belvg\OrderCustomStatus\Observer\OrderStatusPreparationObserver" /> </event> </config> |
and its observer:
$> mkdir Observer
$> touch OrderStatusPreparationObserver.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 |
<?php namespace Belvg\OrderCustomStatus\Observer; use Magento\Framework\Event\ObserverInterface; class OrderStatusPreparationObserver implements ObserverInterface { /** * @var \Magento\Framework\Mail\Template\TransportBuilder */ protected $transportBuilder; /** * @var \Magento\Framework\App\Config\ScopeConfigInterface */ protected $scopeConfig; /** * @var \Magento\Framework\Mail\Template\SenderResolverInterface */ protected $senderResolver; public function __construct( \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Framework\Mail\Template\SenderResolverInterface $senderResolver ) { $this->transportBuilder = $transportBuilder; $this->scopeConfig = $scopeConfig; $this->senderResolver = $senderResolver; } public function execute(\Magento\Framework\Event\Observer $observer) { /** @var \Magento\Sales\Model\Order $order */ $order = $observer->getOrder(); if ( $order->getOrigData("status") != $order->getStatus() && $order->getStatus() === \Belvg\OrderCustomStatus\Model\Order::STATUS_PREPARATION ) { $senderIdentity = $this->scopeConfig->getValue( 'sales_email/order/identity', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $order->getStore()->getId() ); $sender = $this->senderResolver->resolve($senderIdentity, $order->getStore()->getId()); $transport = $this->transportBuilder ->setTemplateIdentifier('sales_email_order_template_preparation') ->setTemplateOptions( [ 'area' => \Magento\Framework\App\Area::AREA_FRONTEND, 'store' => $order->getStore()->getId(), ] ) ->setFrom($sender) ->setTemplateVars([ 'order' => $order, 'billing' => $order->getBillingAddress(), 'store' => $order->getStore(), ]) ->addTo($order->getBillingAddress()->getEmail(), $order->getBillingAddress()->getName()) ->getTransport(); $transport->sendMessage(); } } } |
When saving the order, if the model has been changed, Magento will call the event handlers of the sales_order_save_after event (in fact, there will be much more handlers but for now we are only interested in this one) and will transfer control to the execute method of our observer. In the method, it is checked if the order status has become STATUS_PREPARATION. After that, an email is generated, the storeId received from the order is indicated.
Done! Now, when the status of the order is changed to preparation, the buyer will be notified about it by email. And it doesn’t matter where the change was made: in the list of orders (orders grid) or in the order editing page.
It remains to enable our module with the following command:
$> php bin/magento setup:upgrade
That is how you add a custom mass action in order grid in Magento 2, I hope it was clear and helpful.
Tnq so much this is very helpful to add a custom column in mass update.