Some businesses (or nice merchands) may want to let their customers cancel their orders directly from their account area. But this features always come with limitations and rules (leadtime since order date, some shipments that happened...). Here is a tutorial (with its sample module) that shows how to do such a thing.


Idea and feature

Giving the ability to customers to cancel their orders by themselves means that conditions and limitations must be taken into account. And those may depend on the store view. It is then required to prepare a system configuration that answers these needs. In order to do that we will add several options in Magento system configuration, in the "Sales > Sales" area:

  • Enable or disable order cancelation by customers in their account area
  • Let the site administrator set a limit beyond which orders cannot be canceled anymore by the customer
  • Let the site administrator enable or disable order cancelation for partially and/or fully shipped orders

We will also add an option that enables an email to be sent to the customer once he/she has canceled an order.

In this example, order cancelation will only be available to customers in the order view page because this is the only consistent place where it is possible to add this new content without modifiying Magento base templates. But you are still free to use inspiration from this current tutorial to also update base templates.

Making things happen

As a prerequisit, a module must exist (in our example the module is Herve_CustomerOrderCancel) with:

  • Its declaration file (etc/modules/Herve_CustomerOrderCancel.xml)
  • Its configuration file (app/code/community/Herve/Herve_CustomerOrderCancel/etc/config.xml)
  • Its system configuration file (app/code/community/Herve/Herve_CustomerOrderCancel/etc/system.xml)
  • Its controllers directory (app/code/community/Herve/controllers)
  • A Helper to manage translations (app/code/community/Herve/Herve_CustomerOrderCancel/Helper/Data.php).

We will speak about design files and folders later on in this post.

Here is the config.xml file:

<config>  
    <modules>
        <Herve_CustomerOrderCancel>
            <version>1.0.0.0</version>
        </Herve_CustomerOrderCancel>
    </modules>
    <global>
        <helpers>
            <customerordercancel>
                <class>Herve_CustomerOrderCancel_Helper</class>
            </customerordercancel>
        </helpers>
    </global>
    <adminhtml>
        <translate>
            <modules>
                <Herve_CustomerOrderCancel>
                    <files>
                        <default>Herve_CustomerOrderCancel.csv</default>
                    </files>
                </Herve_CustomerOrderCancel>
            </modules>
        </translate>
    </adminhtml>
    <frontend>
        <routers>
            <customerordercancel>
                <use>standard</use>
                <args>
                    <module>Herve_CustomerOrderCancel</module>
                    <frontName>customerordercancel</frontName>
                </args>
            </customerordercancel>
        </routers>
        <layout>
            <updates>
                <customerordercancel>
                    <file>customerordercancel.xml</file>
                </customerordercancel>
            </updates>
        </layout>
        <translate>
            <modules>
                <Herve_CustomerOrderCancel>
                    <files>
                        <default>Herve_CustomerOrderCancel.csv</default>
                    </files>
                </Herve_CustomerOrderCancel>
            </modules>
        </translate>
    </frontend>
    <default>
        <sales>
            <cancel>
                <enabled>0</enabled>
                <leadtime></leadtime>
                <cancel_partially_shipped>0</cancel_partially_shipped>
                <cancel_fully_shipped>0</cancel_fully_shipped>
                <send_email>1</send_email>
            </cancel>
        </sales>
    </default>
</config>  

Please note the default node in which sets default values for system configuration:

  • enabled to 0: order cancelation by customers is disabled by default.
  • leadtime empty: leadtime prior to disabling order cancelation by customers is empty in order to disable this limit.
  • cancel_partially_shipped to 0: order cancelation for partially shipped orders is not possible by default
  • cancel_fully_shipped to 0: order cancelation for fully shipped orders is not possible by default
  • send_email to 1: a transactional email is sent to the customer once he/she has canceled an order

And here is the system.xml file that manage system configuration and mirrors the default node we just spoke about:

<config>  
    <sections>
        <sales>
            <groups>
                <cancel translate="label" module="customerordercancel">
                    <label>Customer Cancelation</label>
                    <frontend_type>text</frontend_type>
                    <sort_order>1000</sort_order>
                    <show_in_default>1</show_in_default>
                    <show_in_website>1</show_in_website>
                    <show_in_store>1</show_in_store>
                    <fields>
                        <enabled translate="label comment">
                            <label>Enable cancelation by customer</label>
                            <frontend_type>select</frontend_type>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                            <sort_order>10</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                            <comment>Allows customers to cancel their orders from their account</comment>
                        </enabled>
                        <leadtime translate="label comment" module="customerordercancel">
                            <label>Allow cancelation of orders younger than...</label>
                            <frontend_type>text</frontend_type>
                            <sort_order>20</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                            <comment>In days. Leave empty for no limit.</comment>
                            <validate>validate-digits</validate>
                            <depends><enabled>1</enabled></depends>
                        </leadtime>
                        <cancel_partially_shipped translate="label">
                            <label>Allow cancelation of partially shipped orders</label>
                            <frontend_type>select</frontend_type>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                            <sort_order>30</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                            <depends><enabled>1</enabled></depends>
                        </cancel_partially_shipped>
                        <cancel_fully_shipped translate="label">
                            <label>Allow cancelation of fully shipped orders</label>
                            <frontend_type>select</frontend_type>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                            <sort_order>40</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                            <depends><enabled>1</enabled></depends>
                        </cancel_fully_shipped>
                        <send_email translate="label comment">
                            <label>Send order update email</label>
                            <frontend_type>select</frontend_type>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                            <sort_order>50</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>1</show_in_store>
                            <comment>Email template is the one configured for Order Comments.</comment>
                            <depends><enabled>1</enabled></depends>
                        </send_email>
                    </fields>
                </cancel>
            </groups>
        </sales>
    </sections>
</config>  

Please note several things:

  • leadtime, cancel_partially_shipped, cancel_fully_shipped and send_email nodes embed another depends node with enabled set to 1. This means that the value for enabled must be set to 1 in order for these fields to be displayed. In other words, they are displayed only when the administrator sets the "Enable cancelation by customer" select to "Yes".
  • There is a form validation with validate-digits for leadtime. This ensures that the value entered in the field really is an integer.

Designing the frontend (order detail in customer acccount)

As said above, the order detail page is the only consistent page where we can add our "Cancel order" link without modifying base templates files. This is a best practice when developing modules that are not embeded in a project - like this tutorial which is not part of a project. Then, the block that can host some childHtml is the one named sales.order.info.buttons in the sales_order_view layout handle. Let's add a child block that will be in charge of displaying a link that, when clicked, will cancel the order currently displayed in the customer's browser.

Here is the layout file - /app/design/frontend/base/default/layout/customerordercancel.xml:

<layout version="0.1.0">

    <sales_order_view>
        <reference name="sales.order.info.buttons">
            <block type="sales/order_info_buttons" name="customer.order.cancel.button" as="customer_order_cancel_button" template="customerordercancel/sales/order/info/buttons/cancel.phtml"></block>
        </reference>
    </sales_order_view>

</layout>  

This layout calls a block of type sales/order_info_buttons. This type of block has the information needed for our development: the Mage_Sales_Model_Order object relative to the currently displayed order in the customer's browser. We can grab this object by using $this->getOrder().

And here is the code of the template called in the layout - /app/design/frontend/base/default/template/customerordercancel/sales/order/info/buttons/cancel.phtml:

<?php $_order = $this->getOrder() ?>

    <?php if(Mage::helper('customerordercancel')->canCancel($_order)): ?>
        <span class="separator">|`
        <a href="<?php echo $this->getUrl('customerordercancel/order/cancel', array('order_id' => $_order->getId(), '_secure'=>true)) ?>" class="link-reorder"><?php echo $this->__('Cancel Order') ?></a>
    <?php endif ?>

Please note that :

  • Our helper (that we will get more depth afterwards) is called in order to check if the order can be canceled by the customer
  • The a tag calls a URL belonging to our controller (that we will get more depth afterwards) with order_id as argument which is the ID of the order currently displayed in the customer's browser

Check if the order can be canceled thanks to our helper

The helper file is /app/code/community/Herve/CustomerOrderCancel/Helper/Data.php and here is its commented code:

class Herve_CustomerOrderCancel_Helper_Data extends Mage_Core_Helper_Abstract  
{
    /**
     * Check if order can be canceled by customer
     *
     * @param Mage_Sales_Model_Order $order
     * @return bool
     */
    public function canCancel(Mage_Sales_Model_Order $order)
    {
        // If order cancelation is disabled in system configuration: return false
        if(!Mage::getStoreConfigFlag('sales/cancel/enabled')) {
            return false;
        }

        // If Magento decides that this order cannot be canceled
        if(!$order->canCancel()) {
            return false;
        }

        // If order has shipment(s) but can still be shipped, it means that is partially ship.
        // If order is partially shipped and that cancelation of partially shipped orders is disabled in system config: return false
        if($order->hasShipments() && $order->canShip() && !Mage::getStoreConfigFlag('sales/cancel/cancel_partially_shipped')) {
            return false;
        }

        // If order cannot be shipped is means that is has been fully shipped.
        // If order has been fully shipped and that cancelation of fully shipped order is disabled in system config: return false
        if(!$order->canShip() && !Mage::getStoreConfigFlag('sales/cancel/cancel_fully_shipped')) {
            return false;
        }

        // Calculate the number of days since the order's datetime
        $dateModel = Mage::getModel('core/date');
        $createdAt = $order->getCreatedAtStoreDate();
        $deltaDays = ($dateModel->gmtTimestamp() - $dateModel->gmtTimestamp($createdAt)) / 86400;

        // If the numebr of days since order's datetime is larger than the cancelation leadtime in system config: return false
        if(Mage::getStoreConfig('sales/cancel/leadtime') !== '' && $deltaDays > Mage::getStoreConfig('sales/cancel/leadtime')) {
            return false;
        }

        // Else... return true
        return true;
    }
}

Thanks to this logic, we can make sure that the order is elligible to cancelation by the customer depending on system configuration and on Magento internal rules.

Cancel the order with our controller

If an order can be canceled by the customer, a link "Cancel Order" will be available in the order detail view in the customer account. As said earlier, clicking on this link calls a URL (www.domain.com/customerordercancel/order/cancel/orderid/[ORDERID] that belongs to our controller.

Here is the commented code of the controller (/app/code/community/Herve/CustomerOrderCancel/controllers/OrderController.php) :

class Herve_CustomerOrderCancel_OrderController extends Mage_Core_Controller_Front_Action {

    /**
     * Cancel order on customer request
     */
    public function cancelAction()
    {

        // Retrieve order_id passed by clicking on "Cancel Order" in customer account
        $orderId = $this->getRequest()->getParam('order_id');

        // Load Mage_Sales_Model_Order object
        $order = Mage::getModel('sales/order')->load($orderId);

        // Retrieve catalog session.
        // We must use catalog session as customer session messages are not initiated for sales order view
        // and this is where we want to redirect at the end of this action
        // @see Mage_Sales_Controller_Abstract::_viewAction()
        $session = Mage::getSingleton('catalog/session');

        try {

            // Make sure that the order can still be canceled since customer clicked on "Cancel Order"
            if(!Mage::helper('customerordercancel')->canCancel($order)) {
                throw new Exception('Order cannot be canceled anymore.');
            }

            // Cancel and save the order
            $order->cancel();
            $order->save();

            // If sending transactionnal email is enabled in system configuration, we send the email
            if(Mage::getStoreConfigFlag('sales/cancel/send_email')) {
                $order->sendOrderUpdateEmail();
            }

            $session->addSuccess($this->__('The order has been canceled.'));
        }
        catch (Exception $e) {
            Mage::logException($e);
            $session->addError($this->__('The order cannot be canceled.'));
        }

        // Redirect to current sale order view
        $this->_redirect('sales/order/view', array('order_id' => $orderId));
    }
}

Result

System configuration (translated in French): System configuration

The order cancelation link (translated in French):

The success messags once the order has been canceled (still in French):

Example module

An example module is available here: https://github.com/herveguetin/Herve_CustomerOrderCancel