Custom Product Option and its use-case in Magento

This post is actually short tutorial on how to create custom option for product and how to extend Magento Sale Order grid to show custom option as “Delivery Date”. This is beginner guide tutorial so if you’re new to Magento you’ll find it useful. At the end it will be fully functional Magento Module.

Imagine that you’ve gotten requirement that goes something like: “We would like to create custom option for each product in our Magento store, “Delivery Date“, so that our customers can set desired delivery date on every order. We are selling one product per order so setting custom option on each product would be enough for us. Further, our Operations Team needs additional information inside the Magento Sales grid. Requirement is to add a special sales grid within Magento back-end. New grid should be same as default Magento Sale grid with additional column “Delivery Date” that will be used by our Operations Team”.

This requirement could be one of your next tasks, feel free to modify it or even use it as-is (whole module).

If you’re not familiar with Magento’s product custom option please refer to next link.

Lets start with clean Magento with sample data. Open product Ottoman (ID could be 51) in Magento back-end (administration): Catalog / Manage Products. Click on “Custom Option” tab. Now add “Delivery Date” custom option, similar as on image bellow:

custom_product_backend

Now open product Ottoman under “Furniture” / “Living Room” in Magento front-end. You should see something like:

custom_product_frontend

And you’re all set. Simple as that – now you have for specified product custom product option that can be used by customers on your store.

Now lets see how we can create grid for our custom option, similar to Magento’s Sales Order grid with our new data, Delivery Date. So lets start with creating our module.

  1. Tell Magento about our module
  2. Create configuration file for our module
  3. Create menu item in Magento back-end with ACL permissions
  4. Create controller file for back-end
  5. Create Helper file
  6. Create Block for back-end
    • Create initial Block
    • Create grid Block
    • Create Renderer file for grid Block
  7. Add blocks to Magento layout structure

1. Tell Magento about our module

Create file Inchoo_DateOrder.xml under app/etc/modules/ with the following content:

<?xml version="1.0"?>
<config>
    <modules>
        <Inchoo_DateOrder>
            <active>true</active>
            <codePool>local</codePool>
            <depends>
                <Mage_Adminhtml />
            </depends>
        </Inchoo_DateOrder>
    </modules>
</config>

2. Create configuration file for our module

Create file config.xml under app/code/local/Inchoo/DateOrder/etc/ with the following content:

<?xml version="1.0"?>
<config>
    <modules>
        <Inchoo_DateOrder>
            <version>1.0.0.0</version>
        </Inchoo_DateOrder>
    </modules>
    <global>
        <blocks>
            <operations_adminhtml>
                <class>Inchoo_DateOrder_Block_Adminhtml</class>
            </operations_adminhtml>
        </blocks>
        <helpers>
            <operations>
                <class>Inchoo_DateOrder_Helper</class>
            </operations>
        </helpers>
    </global>
    <admin>
        <routers>
            <adminhtml>
                <args>
                    <modules>
                        <inchoo_dateorder after="Mage_Adminhtml">Inchoo_DateOrder_Adminhtml</inchoo_dateorder>
                    </modules>
                </args>
            </adminhtml>
        </routers>
    </admin>
    <adminhtml>
<!--         <translate> -->
<!--             <modules> -->
<!--                 <Inchoo_DateOrder> -->
<!--                     <files> -->
<!--                         <default>Inchoo_DateOrder.csv</default> -->
<!--                     </files> -->
<!--                 </Inchoo_DateOrder> -->
<!--             </modules> -->
<!--         </translate> -->
        <layout>
            <updates>
                <operations>
                    <file>operations.xml</file>
                </operations>
            </updates>
        </layout>
    </adminhtml>
</config>

Note that I’ve commented “translate” node under “adminhtml” XML node. If you have some sentences that needs to be translated on other languages feel free to un-comment those lines and add missing sentences.

3. Create menu item in Magento back-end with ACL permissions

Create file adminhtml.xml under app/code/local/Inchoo/DateOrder/etc/ with the following content:

<?xml version="1.0"?>
<config>
    <menu>
        <sales>
            <children>
                <operations translate="title" module="operations">
                    <title>Operations</title>
                    <sort_order>1</sort_order>
                    <action>adminhtml/operations_order</action>
                </operations>
            </children>
        </sales>
    </menu>
    <acl>
        <resources>
            <admin>
                <children>
                    <sales>
                        <children>
                            <operations translate="title">
                                <title>Operations</title>
                                <sort_order>1</sort_order>
                            </operations>
                        </children>
                    </sales>
                </children>
            </admin>
        </resources>
    </acl>
</config>

4. Create controller file for back-end

Create file app/code/local/Inchoo/DateOrder/controllers/Adminhtml/Operations/OrderController.php with the following content:

<?php
 
/**
 * Adminhtml sales operations controller
 *
 * @category    Inchoo
 * @package     Inchoo_DateOrder
 */
class Inchoo_DateOrder_Adminhtml_Operations_OrderController extends Mage_Adminhtml_Controller_Action
{
 
    /**
     * Additional initialization
     *
     */
    protected function _construct()
    {
        $this->setUsedModuleName('Inchoo_DateOrder');
    }
 
    /**
     * Init layout, menu and breadcrumb
     *
     * @return Inchoo_DateOrder_Adminhtml_Operations_OrderController
     */
    protected function _initAction()
    {
        $this->loadLayout()
            ->_setActiveMenu('sales/order')
            ->_addBreadcrumb($this->__('Sales'), $this->__('Sales'))
            ->_addBreadcrumb($this->__('Operations'), $this->__('Operations'));
        return $this;
    }
 
    /**
     * Orders grid
     */
    public function indexAction()
    {
        $this->_title($this->__('Sales'))->_title($this->__('Operations'));
 
        $this->_initAction()
            ->renderLayout();
    }
 
    /**
     * Order grid
     */
    public function gridAction()
    {
        $this->loadLayout(false);
        $this->renderLayout();
    }
 
    /**
     * Export order grid to CSV format
     */
    public function exportCsvAction()
    {
        $fileName   = 'operations.csv';
        $grid       = $this->getLayout()->createBlock('operations_adminhtml/operations_order_grid');
        $this->_prepareDownloadResponse($fileName, $grid->getCsvFile());
    }
 
    /**
     *  Export order grid to Excel XML format
     */
    public function exportExcelAction()
    {
        $fileName   = 'operations.xml';
        $grid       = $this->getLayout()->createBlock('operations_adminhtml/operations_order_grid');
        $this->_prepareDownloadResponse($fileName, $grid->getExcelFile($fileName));
    }
 
}

5. Create Helper file

Create file app/code/local/Inchoo/DateOrder/Helper/Data.php with the following content:

<?php
 
/**
 * Operations data helper
 *
 * @category   Inchoo
 * @package    Inchoo_DateOrder
 */
class Inchoo_DateOrder_Helper_Data extends Mage_Core_Helper_Abstract
{
 
}

6.1. Create initial Block

Create file app/code/local/Inchoo/DateOrder/Block/Adminhtml/Operations/Order.php with the following content:

<?php
 
/**
 * Adminhtml operations orders block
 *
 * @category   Inchoo
 * @package    Inchoo_DateOrder
 */
class Inchoo_DateOrder_Block_Adminhtml_Operations_Order extends Mage_Adminhtml_Block_Widget_Grid_Container
{
 
    public function __construct()
    {
        $this->_controller = 'operations_order';
        $this->_blockGroup = 'operations_adminhtml';
        $this->_headerText = Mage::helper('operations')->__('Operations');
        $this->_addButtonLabel = Mage::helper('sales')->__('Create New Order');
        parent::__construct();
        if (!Mage::getSingleton('admin/session')->isAllowed('sales/order/actions/create')) {
            $this->_removeButton('add');
        }
    }
 
}

6.2. Create grid Block

Create file app/code/local/Inchoo/DateOrder/Block/Adminhtml/Operations/Order/Grid.php with the following content:

<?php
 
/**
 * Adminhtml operations orders grid
 *
 * @category   Inchoo
 * @package    Inchoo_DateOrder
 */
class Inchoo_DateOrder_Block_Adminhtml_Operations_Order_Grid extends Mage_Adminhtml_Block_Widget_Grid
{
    /**
     * Note that I could extend Mage_Adminhtml_Block_Sales_Order_Grid instead of Mage_Adminhtml_Block_Widget_Grid
     * and overwrite only methods __construct() & _prepareColumns()
     */
 
    public function __construct()
    {
        parent::__construct();
        $this->setId('operations_order_grid');
        $this->setUseAjax(true);
        $this->setDefaultSort('created_at');
        $this->setDefaultDir('DESC');
        $this->setSaveParametersInSession(true);
    }
 
    /**
     * Retrieve collection class
     *
     * @return string
     */
    protected function _getCollectionClass()
    {
        return 'sales/order_grid_collection';
    }
 
    protected function _prepareCollection()
    {
        $collection = Mage::getResourceModel($this->_getCollectionClass());
        $this->setCollection($collection);
        return parent::_prepareCollection();
    }
 
    protected function _prepareColumns()
    {
 
        $this->addColumn('real_order_id', array(
            'header'=> Mage::helper('sales')->__('Order #'),
            'width' => '80px',
            'type'  => 'text',
            'index' => 'increment_id',
        ));
 
        if (!Mage::app()->isSingleStoreMode()) {
            $this->addColumn('store_id', array(
                'header'    => Mage::helper('sales')->__('Purchased From (Store)'),
                'index'     => 'store_id',
                'type'      => 'store',
                'store_view'=> true,
                'display_deleted' => true,
            ));
        }
 
        $this->addColumn('created_at', array(
            'header' => Mage::helper('sales')->__('Purchased On'),
            'index' => 'created_at',
            'type' => 'datetime',
            'width' => '100px',
        ));
 
        $this->addColumn('billing_name', array(
            'header' => Mage::helper('sales')->__('Bill to Name'),
            'index' => 'billing_name',
        ));
 
        $this->addColumn('shipping_name', array(
            'header' => Mage::helper('sales')->__('Ship to Name'),
            'index' => 'shipping_name',
        ));
 
        $this->addColumn('delivery_date', array(
                'header' => Mage::helper('operations')->__('Delivery Date'),
                'index' => 'entity_id',
                'sortable'  => false,
                'filter'    => false,
                'renderer'  => 'operations_adminhtml/operations_order_renderer_delivery',
        ));
 
        $this->addColumn('base_grand_total', array(
            'header' => Mage::helper('sales')->__('G.T. (Base)'),
            'index' => 'base_grand_total',
            'type'  => 'currency',
            'currency' => 'base_currency_code',
        ));
 
        $this->addColumn('grand_total', array(
            'header' => Mage::helper('sales')->__('G.T. (Purchased)'),
            'index' => 'grand_total',
            'type'  => 'currency',
            'currency' => 'order_currency_code',
        ));
 
        $this->addColumn('status', array(
            'header' => Mage::helper('sales')->__('Status'),
            'index' => 'status',
            'type'  => 'options',
            'width' => '70px',
            'options' => Mage::getSingleton('sales/order_config')->getStatuses(),
        ));
 
        if (Mage::getSingleton('admin/session')->isAllowed('sales/order/actions/view')) {
            $this->addColumn('action',
                array(
                    'header'    => Mage::helper('sales')->__('Action'),
                    'width'     => '50px',
                    'type'      => 'action',
                    'getter'     => 'getId',
                    'actions'   => array(
                        array(
                            'caption' => Mage::helper('sales')->__('View'),
                            'url'     => array('base'=>'*/sales_order/view'),
                            'field'   => 'order_id'
                        )
                    ),
                    'filter'    => false,
                    'sortable'  => false,
                    'index'     => 'stores',
                    'is_system' => true,
            ));
        }
        $this->addRssList('rss/order/new', Mage::helper('sales')->__('New Order RSS'));
 
        $this->addExportType('*/*/exportCsv', Mage::helper('sales')->__('CSV'));
        $this->addExportType('*/*/exportExcel', Mage::helper('sales')->__('Excel XML'));
 
        return parent::_prepareColumns();
    }
 
    protected function _prepareMassaction()
    {
        $this->setMassactionIdField('entity_id');
        $this->getMassactionBlock()->setFormFieldName('order_ids');
        $this->getMassactionBlock()->setUseSelectAll(false);
 
        if (Mage::getSingleton('admin/session')->isAllowed('sales/order/actions/cancel')) {
            $this->getMassactionBlock()->addItem('cancel_order', array(
                 'label'=> Mage::helper('sales')->__('Cancel'),
                 'url'  => $this->getUrl('*/sales_order/massCancel'),
            ));
        }
 
        if (Mage::getSingleton('admin/session')->isAllowed('sales/order/actions/hold')) {
            $this->getMassactionBlock()->addItem('hold_order', array(
                 'label'=> Mage::helper('sales')->__('Hold'),
                 'url'  => $this->getUrl('*/sales_order/massHold'),
            ));
        }
 
        if (Mage::getSingleton('admin/session')->isAllowed('sales/order/actions/unhold')) {
            $this->getMassactionBlock()->addItem('unhold_order', array(
                 'label'=> Mage::helper('sales')->__('Unhold'),
                 'url'  => $this->getUrl('*/sales_order/massUnhold'),
            ));
        }
 
        $this->getMassactionBlock()->addItem('pdfinvoices_order', array(
             'label'=> Mage::helper('sales')->__('Print Invoices'),
             'url'  => $this->getUrl('*/sales_order/pdfinvoices'),
        ));
 
        $this->getMassactionBlock()->addItem('pdfshipments_order', array(
             'label'=> Mage::helper('sales')->__('Print Packingslips'),
             'url'  => $this->getUrl('*/sales_order/pdfshipments'),
        ));
 
        $this->getMassactionBlock()->addItem('pdfcreditmemos_order', array(
             'label'=> Mage::helper('sales')->__('Print Credit Memos'),
             'url'  => $this->getUrl('*/sales_order/pdfcreditmemos'),
        ));
 
        $this->getMassactionBlock()->addItem('pdfdocs_order', array(
             'label'=> Mage::helper('sales')->__('Print All'),
             'url'  => $this->getUrl('*/sales_order/pdfdocs'),
        ));
 
        $this->getMassactionBlock()->addItem('print_shipping_label', array(
             'label'=> Mage::helper('sales')->__('Print Shipping Labels'),
             'url'  => $this->getUrl('*/sales_order_shipment/massPrintShippingLabel'),
        ));
 
        return $this;
    }
 
    public function getRowUrl($row)
    {
        if (Mage::getSingleton('admin/session')->isAllowed('sales/order/actions/view')) {
            return $this->getUrl('*/sales_order/view', array('order_id' => $row->getId()));
        }
        return false;
    }
 
    public function getGridUrl()
    {
        return $this->getUrl('*/*/grid', array('_current'=>true));
    }
 
}

6.3. Create Renderer file for grid Block

Create file app/code/local/Inchoo/DateOrder/Block/Adminhtml/Operations/Order/Renderer/Delivery.php with the following content:

<?php
 
/**
 * Adminhtml operations orders grid renderer
 *
 * @category   Inchoo
 * @package    Inchoo_DateOrder
 */
class Inchoo_DateOrder_Block_Adminhtml_Operations_Order_Renderer_Delivery
    extends Mage_Adminhtml_Block_Widget_Grid_Column_Renderer_Abstract
{
    public function render(Varien_Object $row)
    {
        //load first item of the order
        $orderItem = Mage::getResourceModel('sales/order_item_collection')
                        ->addFieldToFilter('order_id', $row->getId())
                        ->getFirstItem()
                        ;
 
        $orderItemOptions = $orderItem->getProductOptions();
 
        //if product doesn't have options stop with rendering
        if (!array_key_exists('options', $orderItemOptions)) {
            return;
        }
 
        $orderItemOptions = $orderItemOptions['options'];
 
        //if product options isn't array stop with rendering
        if (!is_array($orderItemOptions)) {
            return;
        }
 
        foreach ( $orderItemOptions as $orderItemOption) {
 
            if ($orderItemOption['label'] === 'Delivery Date') {
                if (array_key_exists('value', $orderItemOption)) {
                    return $orderItemOption['value'];
                }
            }
 
        }
 
        //if product options doesn't have Delivery Date custom option return void
        return;
    }
}

7. Add blocks to Magento layout structure

Create file app/design/adminhtml/default/default/layout/operations.xml with the following content:

<?xml version="1.0"?>
<layout>
    <adminhtml_operations_order_grid>
        <update handle="formkey"/>
        <block type="operations_adminhtml/operations_order_grid" name="operations_order.grid" output="toHtml"></block>
    </adminhtml_operations_order_grid>
 
    <adminhtml_operations_order_index>
        <reference name="content">
            <block type="operations_adminhtml/operations_order" name="operations_order.grid.container"></block>
        </reference>
    </adminhtml_operations_order_index>
</layout>

Now all you need to do is to add Operations menu item to your administrator role (sign out/sign in – if needed) and you can use your new grid in Magento back-end.

Note that when customers add some custom options for product, data will be saved in table sales_flat_order_item, in column product_options. Additionally, as data in table sales_flat_order_item is serialized its a bit complicated for this tutorial to show you how you can filter & sort by “Delivery Date” and because of that sorting and filtering by it has been disabled.

For the end, this is how our grid looks like in Magento back-end:

custom_product_grid


23 comments

  1. After added this module on my store display Error
    Fatal error: Class ‘Inchoo_DateOrder_Helper_Data’ not found in D:\xampp\htdocs\magento\app\Mage.php on line 547
    Please help resolve this issue as soon as possible

  2. Hi, that’s nice but what if I need a link inside my custom attributes? I tried to add a checkbox with a link inside its label but the label shows the link as plain text instead of showing the formatted link. How can I have links in custom attributes?

  3. hi i have added the option of delivery date in the checkout process of my magento store. but i want to this date on the order page under sales option of backend of magento. so can you provide me the any help for that

  4. hi,
    this is a great tutorial to add a custom column to admin sales table, now how can we make this column filterable ??? can any help.. It is sortable by commenting ‘sortable’ => false, of custom column i.e
    $this->addColumn(‘delivery_date’, array(
    ‘header’ => Mage::helper(‘operations’)->__(‘Delivery Date’),
    ‘index’ => ‘entity_id’,
    // ‘sortable’ => false,
    ‘filter’ => false,
    ‘renderer’ => ‘operations_adminhtml/operations_order_renderer_delivery’,
    ));

  5. I would also like to have this delivery date in the subject of the new order email sent to customers for any new purchase. How can we achieve this?

  6. Hi, I know my question is not exactly related to this subject but there is any extension to use (or ever been build) in order to filter after product custom options (not attribute) or somebody tried this? I have over 20k simple products in one of my websites…and using configurable products is not a option (I have products that have more then 10 options each – meaning 10 simple products). Thanks!

  7. Will this work if there are multiple products on the same order?? I need to generate reports for custom options that are selected, and there will most likely be more than one custom option per order (Or more than one product per order, custom options are mandatory)

    Looks like a great tutorial, but I don’t want to follow it through if it’s not going to do what I need it to. Thanks!

  8. First, thanks for your tutorial…and i got this problem when a tried to see operations “controller action: noroute”….the only diference that i made was the namespace and th module name…the rest is all the same!!!! Could anyone help me?….. 🙂

  9. i am developing Shirt selling website and i need the custom button for the size and colour to be displayed in below the name of the product and about the qty. please guide me .

  10. Thank you a lot, this code is working fine. I have a question,is there any way to edit date field from admin panel?

  11. Hi Ivan,

    Sorry for the late reply.. The code is working fine 🙂 I made mistake in naming the Block directory.. My mistake 😉 Thanks for your replies 🙂

  12. Hi bvkiran,

    Did you resolve your issue? If there’s anything that should be noted here?. Other people might have similar issue so it would be great to leave some note.

    Thank you,
    Ivan

  13. Hi bvkiran,

    I just installed new Magento v1702 with sample data and I followed all steps and I can see grid.

    Try to set in index.php ini_set(“display_errors”, true) & error_reporting(E_ALL ~ E_NOTICE).
    Also you can enable developer mode so you can see errors on screen.

    Everything should work correctly. Please re-check all files and see if you have any typo or similar (you might created some file with different capitalization and/or wrong filename).

    Waiting for your word,
    Regards,
    Ivan

  14. Hi Ivan,

    I have set System –> Permission –> Roles

    Administrators –> Resource Access –> All

    And I don’t see any error messages..
    Under reports –> sales there is no operations only orders and others… I’m able to view those reports..

    Everything is actually fine but when I click on sales –> Operations I don’t get to see the grid, I get menus and all with a blank area..

  15. Hi bvkiran,

    Can you please see under reports what is the error message? Did you set proper file permissions?

  16. Hi Ivan,

    I am able to see the Operations menu under Sales.. But when I click on that I’m getting a blank screen.. I’m not able to see the grid.. I tried log out/log in but it’s still not working..
    I am using magento 1.7.0.2

  17. Hi bvkiran,

    try to log out/log in and you need to set ACL role to see it under menu item. I’ve checked module in various Magento installation and everywhere is working fine…

    Thanks on the comment 😉

  18. I followed everything, but it’s not working for me.. Nothing is being displayed when I click Sales –> Operations..

  19. Hello,

    Good article.

    If order contain more than products then its create problem in Sales department to understand , why not display in Order View Page per Item Wise.

    Thanks,
    Sunil

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.