The Ultimate Guide to Creating Custom Order Grids in Magento 2 Admin

Today, we will walk through the steps to create a custom order grid in your Magento 2 admin panel, allowing you to efficiently manage your orders with ease.

Step 1: Create a New Menu Item

To begin, let's create a new menu item in the Magento 2 admin panel. This menu item will serve as the entry point for accessing your custom order grid. You can do this by navigating to the appropriate menu configuration file and adding a new menu item definition.

The menu.xml file is located inside the etc/adminhtml directory of the module.

xml
<config>
    <menu>
      <add id="MageMastery_Sales::sales_intorder"
title="International Orders"
translate="title"
module="MageMastery_Sales"
sortOrder="15"
parent="Magento_Sales::sales_operation"
action="sales/intorder"
resource="MageMastery_Sales::sales_intorder"/>
    </menu>
</config>

Step 2: Define Layout File

Next, create a layout file named sales_intorder_index.xml for the route corresponding to your custom order grid. In this layout file, we'll call the ui_component directive to load the grid component onto the page.

xml
<page>
    <body>
        <referenceContainer name="content">
            <uiComponent name="sales_intorder_grid"/>
        </referenceContainer>
    </body>
</page>

Step 3: Configure UI Component

Now, it's time to define the structure and behavior of your custom order grid by creating a UI component file. In the sales_intorder_grid.xml, define the grid layout, specify the data source, configure actions, and define columns to display relevant order information.

xml
<listing>
    <argument name="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="provider" xsi:type="string">sales_intorder_grid.sales_intorder_grid_data_source</item>
        </item>
    </argument>
    <settings>
        <spinner>sales_order_columns</spinner>
        <deps>
            <dep>sales_intorder_grid.sales_intorder_grid_data_source</dep>
        </deps>
    </settings>
    <dataSource name="sales_intorder_grid_data_source" component="Magento_Ui/js/grid/provider">
        <settings>
            <updateUrl path="mui/index/render"/>
        </settings>
        <aclResource>Magento_Sales::sales_order</aclResource>
        <dataProvider class="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider" name="sales_intorder_grid_data_source">
            <settings>
                <requestFieldName>id</requestFieldName>
                <primaryFieldName>main_table.entity_id</primaryFieldName>
            </settings>
        </dataProvider>
    </dataSource>
    <listingToolbar name="listing_top">
        <settings>
            <sticky>true</sticky>
        </settings>
        <bookmark name="bookmarks"/>
        <columnsControls name="columns_controls"/>
        <exportButton name="export_button"/>
        <filterSearch name="fulltext"/>
        <filters name="listing_filters">
            <filterSelect name="store_id" provider="${ $.parentName }">
                <settings>
                    <options class="Magento\Store\Ui\Component\Listing\Column\Store\Options"/>
                    <caption translate="true">All Store Views</caption>
                    <label translate="true">Purchase Point</label>
                    <dataScope>store_id</dataScope>
                    <imports>
                        <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link>
                    </imports>
                </settings>
            </filterSelect>
        </filters>
        <paging name="listing_paging"/>
    </listingToolbar>
    <columns name="sales_order_columns">
        <settings>
            <childDefaults>
                <param name="fieldAction" xsi:type="array">
                    <item name="provider" xsi:type="string">sales_intorder_grid.sales_intorder_grid.sales_order_columns.actions</item>
                    <item name="target" xsi:type="string">applyAction</item>
                    <item name="params" xsi:type="array">
                        <item name="0" xsi:type="string">view</item>
                        <item name="1" xsi:type="string">${ $.$data.rowIndex }</item>
                    </item>
                </param>
            </childDefaults>
        </settings>
        <selectionsColumn name="ids">
            <settings>
                <indexField>entity_id</indexField>
            </settings>
        </selectionsColumn>
        <column name="increment_id">
            <settings>
                <filter>text</filter>
                <label translate="true">ID</label>
            </settings>
        </column>
        <column name="store_id" class="Magento\Store\Ui\Component\Listing\Column\Store">
            <settings>
                <label translate="true">Purchase Point</label>
                <bodyTmpl>ui/grid/cells/html</bodyTmpl>
                <sortable>false</sortable>
            </settings>
        </column>
        <column name="created_at" class="Magento\Ui\Component\Listing\Columns\Date" component="Magento_Ui/js/grid/columns/date">
            <settings>
                <filter>dateRange</filter>
                <dataType>date</dataType>
                <label translate="true">Purchase Date</label>
                <sorting>desc</sorting>
            </settings>
        </column>
        <column name="billing_name">
            <settings>
                <filter>text</filter>
                <label translate="true">Bill-to Name</label>
            </settings>
        </column>
        <column name="shipping_name">
            <settings>
                <filter>text</filter>
                <label translate="true">Ship-to Name</label>
            </settings>
        </column>
        <column name="base_grand_total" class="Magento\Sales\Ui\Component\Listing\Column\Price">
            <settings>
                <filter>textRange</filter>
                <label translate="true">Grand Total (Base)</label>
            </settings>
        </column>
        <column name="grand_total" class="Magento\Sales\Ui\Component\Listing\Column\PurchasedPrice">
            <settings>
                <filter>textRange</filter>
                <label translate="true">Grand Total (Purchased)</label>
            </settings>
        </column>
        <column name="status" component="Magento_Ui/js/grid/columns/select">
            <settings>
                <filter>select</filter>
                <options class="Magento\Sales\Ui\Component\Listing\Column\Status\Options"/>
                <dataType>select</dataType>
                <label translate="true">Status</label>
            </settings>
        </column>
        <column name="billing_address" class="Magento\Sales\Ui\Component\Listing\Column\Address">
            <settings>
                <filter>text</filter>
                <label translate="true">Billing Address</label>
                <bodyTmpl>ui/grid/cells/html</bodyTmpl>
                <visible>false</visible>
            </settings>
        </column>
        <column name="shipping_address" class="Magento\Sales\Ui\Component\Listing\Column\Address">
            <settings>
                <filter>text</filter>
                <label translate="true">Shipping Address</label>
                <bodyTmpl>ui/grid/cells/html</bodyTmpl>
                <visible>false</visible>
            </settings>
        </column>
        <column name="shipping_information">
            <settings>
                <filter>text</filter>
                <label translate="true">Shipping Information</label>
                <visible>false</visible>
            </settings>
        </column>
        <column name="customer_email">
            <settings>
                <filter>text</filter>
                <label translate="true">Customer Email</label>
                <visible>false</visible>
            </settings>
        </column>
        <column name="customer_group" component="Magento_Ui/js/grid/columns/select">
            <settings>
                <filter>select</filter>
                <options class="Magento\Customer\Ui\Component\Listing\Column\Group\Options"/>
                <dataType>select</dataType>
                <label translate="true">Customer Group</label>
                <visible>false</visible>
            </settings>
        </column>
        <column name="subtotal" class="Magento\Sales\Ui\Component\Listing\Column\Price">
            <settings>
                <filter>textRange</filter>
                <label translate="true">Subtotal</label>
                <visible>false</visible>
            </settings>
        </column>
        <column name="shipping_and_handling" class="Magento\Sales\Ui\Component\Listing\Column\Price">
            <settings>
                <filter>textRange</filter>
                <label translate="true">Shipping and Handling</label>
                <visible>true</visible>
            </settings>
        </column>
        <column name="customer_name">
            <settings>
                <filter>text</filter>
                <label translate="true">Customer Name</label>
                <visible>false</visible>
            </settings>
        </column>
        <column name="payment_method" component="Magento_Ui/js/grid/columns/select">
            <settings>
                <filter>select</filter>
                <options class="Magento\Payment\Ui\Component\Listing\Column\Method\Options"/>
                <dataType>select</dataType>
                <label translate="true">Payment Method</label>
                <visible>false</visible>
            </settings>
        </column>
        <column name="total_refunded" class="Magento\Sales\Ui\Component\Listing\Column\PurchasedPrice">
            <settings>
                <filter>textRange</filter>
                <label translate="true">Total Refunded</label>
                <visible>false</visible>
            </settings>
        </column>
    </columns>
</listing>

Here's a breakdown of what you'll need to include in the UI component file:

  • Define the grid layout, including pagination, sorting, and filtering options.
  • Specify the data source to fetch order data from the database.
  • Configure actions such as view, edit, delete, or any custom actions you require.
  • Define columns to display key order information such as order number, customer name, order total, status, and any other relevant details.
  • With these steps completed, you'll have successfully created a custom order grid in your Magento 2 admin panel. This grid will provide you with a user-friendly interface for efficiently managing your orders, allowing you to stay organized and focused on growing your business.

Above we have used the data source for ui_component and to define it we will go to di.xml.

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory">
        <arguments>
            <argument name="collections" xsi:type="array">
              <item name="sales_intorder_grid_data_source" xsi:type="string">MageMastery\Sales\Model\ResourceModel\Order\Grid\Collection</item>
            </argument>
        </arguments>
    </type>
</config>

And, in the data source file, we can add our code to write the collection to render data over the grid. The example I created is to create a custom order grid listing orders from some specific country. So I coded accordingly.

declare(strict_types=1);

namespace MageMastery\Sales\Model\ResourceModel\Order\Grid;

use Magento\Framework\App\ObjectManager;
use Magento\Framework\Data\Collection\Db\FetchStrategyInterface as FetchStrategy;
use Magento\Framework\Data\Collection\EntityFactoryInterface as EntityFactory;
use Magento\Framework\Event\ManagerInterface as EventManager;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult;
use Magento\Sales\Model\ResourceModel\Order;
use MageMastery\Sales\Service\GetCountriesService;
use Psr\Log\LoggerInterface as Logger;

/**
 * International Order grid collection
 */
class Collection extends SearchResult
{
    /**
     * @var TimezoneInterface
     */
    private $timeZone;

    /**
     * @var GetCountriesService
     */
    private GetCountriesService $getCountriesService;

    /**
     * @param GetCountriesService $getCountriesService
     * @param EntityFactory $entityFactory
     * @param Logger $logger
     * @param FetchStrategy $fetchStrategy
     * @param EventManager $eventManager
     * @param string $mainTable
     * @param string $resourceModel
     * @param TimezoneInterface|null $timeZone
     * @throws LocalizedException
     */
    public function __construct(
        GetCountriesService $getCountriesService,
        EntityFactory $entityFactory,
        Logger $logger,
        FetchStrategy $fetchStrategy,
        EventManager $eventManager,
        $mainTable = 'sales_order_grid',
        $resourceModel = Order::class,
        TimezoneInterface $timeZone = null
    ) {
        $this->getCountriesService = $getCountriesService;
        parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $mainTable, $resourceModel);
        $this->timeZone = $timeZone ?: ObjectManager::getInstance()
            ->get(TimezoneInterface::class);
    }

    /**
     * @inheritdoc
     * @throws LocalizedException
     */
    protected function _initSelect()
    {
        parent::_initSelect();

        if ($this->getMainTable() === $this->getResource()->getTable('sales_order_grid')) {
            $orderAddressTableName = $this->getResource()->getTable('sales_order_address');
            $this->getSelect()->joinLeft(
                ['soa' => $orderAddressTableName],
                'soa.parent_id = main_table.entity_id AND soa.address_type = \'shipping\'',
                ['country_id']
            );
            $this->addFieldToFilter(
                'country_id',
                ['nin' => $this->getCountriesService->getListOfExcludedCountries()]
            );
        }

        $tableDescription = $this->getConnection()->describeTable($this->getMainTable());
        foreach ($tableDescription as $columnInfo) {
            $this->addFilterToMap($columnInfo['COLUMN_NAME'], 'main_table.' . $columnInfo['COLUMN_NAME']);
        }

        return $this;
    }

    /**
     * @inheritDoc
     * @throws LocalizedException
     */
    public function addFieldToFilter($field, $condition = null)
    {
        if ($field === 'created_at') {
            if (is_array($condition)) {
                foreach ($condition as $key => $value) {
                    $condition[$key] = $this->timeZone->convertConfigTimeToUtc($value);
                }
            }
        }

        return parent::addFieldToFilter($field, $condition);
    }
}

This way we can have our custom order grid.

By leveraging the power and flexibility of Magento 2's admin panel customization capabilities, you can tailor your order management processes to suit your specific business requirements, ultimately improving productivity and enhancing the overall user experience.