Magento Multishipping Checkout

As well as the standard Onepage checkout, the Magento Multishipping checkout is used when customers need to ship their items to multiple addresses.

  • Multishipping checkout is not available for guests. The registered customers must also have at least one address saved.
  • Multishipping checkout will be disabled if the shopping cart contains virtual items only.

The initial structure of the Multishipping is not too different from the Onepage checkout. The checkout is wrapped in a state.phtml template file that has a corresponding State.php block.

Within the State.php block, a getSteps() method returns the checkout steps.

<?php
class Mage_Checkout_Block_Multishipping_State extends Mage_Core_Block_Template
{
    public function getSteps()
    {
        return Mage::getSingleton('checkout/type_multishipping_state')->getSteps();
    }
}

The steps are set within the __construct() method of the Mage_Checkout_Model_Type_Multishipping_State class.

public function __construct()
{
    parent::__construct();
    $this->_steps = array(
        self::STEP_SELECT_ADDRESSES => new Varien_Object(array(
            'label' => Mage::helper('checkout')->__('Select Addresses')
        )),
        self::STEP_SHIPPING => new Varien_Object(array(
            'label' => Mage::helper('checkout')->__('Shipping Information')
        )),
        self::STEP_BILLING => new Varien_Object(array(
            'label' => Mage::helper('checkout')->__('Billing Information')
        )),
        self::STEP_OVERVIEW => new Varien_Object(array(
            'label' => Mage::helper('checkout')->__('Place Order')
        )),
        self::STEP_SUCCESS => new Varien_Object(array(
            'label' => Mage::helper('checkout')->__('Order Success')
        )),
    );

    foreach ($this->_steps as $step) {
        $step->setIsComplete(false);
    }

    $this->_checkout = Mage::getSingleton('checkout/type_multishipping');
    $this->_steps[$this->getActiveStep()]->setIsActive(true);
}

The steps also have their own templates and corresponding blocks.

  • Select Addresses – template/checkout/multishipping/addresses.phtmlMage/Checkout/Block/Multishipping/Addresses.php
  • Shipping Information – template/checkout/multishipping/shipping.phtmlMage/Checkout/Block/Multishipping/Shipping.php
  • Billing Information – template/checkout/multishipping/billing.phtmlMage/Checkout/Block/Multishipping/Billing.php
  • Place Order – template/checkout/multishipping/overview.phtmlMage/Checkout/Block/Multishipping/Overview.php
  • Order Success – template/checkout/multishipping/success.phtmlMage/Checkout/Block/Multishipping/Success.php

The layouts for Multishipping checkout steps and related views are defined the checkout.xml layout folder.

Most of the layouts include an update handle directive <update handle="checkout_multishipping"/> that sets up a basic structure for the checkout page by assigning the one column template, removing the right and left sidebars and adding a child block checkout/multishipping_state to display the checkout progress.

Unlike the Onepage checkout, the Multishipping checkout does not rely heavily on the use of JavaScript and keep the user on the same page. Therefore moving between MultiShipping steps is a lot easier to follow.

The main controller class involved with the Multishipping checkout is the Mage_Checkout_Multishipping_AddressController class.

Within the indexAction() we can see an immediate redirect to the addressesAction() method.

public function indexAction()
{
    $this->_getCheckoutSession()->setCartWasUpdated(false);
    $this->_redirect('*/*/addresses');
}

Within the addressesAction() method, a check to see if the customer has a default shipping address saved is made. If they don’t, they are redirected to an AddressController.php file with a newShippingAction() method.

public function addressesAction()
{
    // If customer do not have addresses
    if (!$this->_getCheckout()->getCustomerDefaultShippingAddress()) {
        $this->_redirect('*/multishipping_address/newShipping');
        return;
    }

    $this->_getState()->unsCompleteStep(
        Mage_Checkout_Model_Type_Multishipping_State::STEP_SHIPPING
    );

    $this->_getState()->setActiveStep(
        Mage_Checkout_Model_Type_Multishipping_State::STEP_SELECT_ADDRESSES
    );
    if (!$this->_getCheckout()->validateMinimumAmount()) {
        $message = $this->_getCheckout()->getMinimumAmountDescription();
        $this->_getCheckout()->getCheckoutSession()->addNotice($message);
    }
    $this->loadLayout();
    $this->_initLayoutMessages('customer/session');
    $this->_initLayoutMessages('checkout/session');
    $this->renderLayout();
}

The newShippingAction() method simply gets the customer address edit block and uses the edit form for the custom to add an address.

public function overviewPostAction()
{
    if (!$this->_validateFormKey()) {
        $this->_forward('backToAddresses');
        return;
    }

    if (!$this->_validateMinimumAmount()) {
        return;
    }

    try {
        if ($requiredAgreements = Mage::helper('checkout')->getRequiredAgreementIds()) {
            $postedAgreements = array_keys($this->getRequest()->getPost('agreement', array()));
            if ($diff = array_diff($requiredAgreements, $postedAgreements)) {
                $this->_getCheckoutSession()->addError($this->__('Please agree to all Terms and Conditions before placing the order.'));
                $this->_redirect('*/*/billing');
                return;
            }
        }

        $payment = $this->getRequest()->getPost('payment');
        $paymentInstance = $this->_getCheckout()->getQuote()->getPayment();
        if (isset($payment['cc_number'])) {
            $paymentInstance->setCcNumber($payment['cc_number']);
        }
        if (isset($payment['cc_cid'])) {
            $paymentInstance->setCcCid($payment['cc_cid']);
        }
        $this->_getCheckout()->createOrders();
        $this->_getState()->setActiveStep(
            Mage_Checkout_Model_Type_Multishipping_State::STEP_SUCCESS
        );
        $this->_getState()->setCompleteStep(
            Mage_Checkout_Model_Type_Multishipping_State::STEP_OVERVIEW
        );
        $this->_getCheckout()->getCheckoutSession()->clear();
        $this->_getCheckout()->getCheckoutSession()->setDisplaySuccess(true);
        $this->_redirect('*/*/success');
     } catch (Mage_Payment_Model_Info_Exception $e) {
        ....
     }
     ....
}

On the overview page, totals are calculated per shipping address using the getShippingAddressTotals() method.

public function getShippingAddressTotals($address)
{
    $totals = $address->getTotals();
    foreach ($totals as $total) {
        if ($total->getCode()=='grand_total') {
            if ($address->getAddressType() == Mage_Sales_Model_Quote_Address::TYPE_BILLING) {
                $total->setTitle($this->__('Total'));
            }
            else {
                $total->setTitle($this->__('Total for this address'));
            }
        }
    }
    return $totals;
}

When the customer eventually places an order on the overview page, the overviewPostAction() method is called.

public function overviewPostAction()
{
    if (!$this->_validateFormKey()) {
        $this->_forward('backToAddresses');
        return;
    }

    if (!$this->_validateMinimumAmount()) {
        return;
    }

    try {
        if ($requiredAgreements = Mage::helper('checkout')->getRequiredAgreementIds()) {
            $postedAgreements = array_keys($this->getRequest()->getPost('agreement', array()));
            if ($diff = array_diff($requiredAgreements, $postedAgreements)) {
                $this->_getCheckoutSession()->addError($this->__('Please agree to all Terms and Conditions before placing the order.'));
                $this->_redirect('*/*/billing');
                return;
            }
        }

        $payment = $this->getRequest()->getPost('payment');
        $paymentInstance = $this->_getCheckout()->getQuote()->getPayment();
        if (isset($payment['cc_number'])) {
            $paymentInstance->setCcNumber($payment['cc_number']);
        }
        if (isset($payment['cc_cid'])) {
            $paymentInstance->setCcCid($payment['cc_cid']);
        }
        $this->_getCheckout()->createOrders();
        $this->_getState()->setActiveStep(
            Mage_Checkout_Model_Type_Multishipping_State::STEP_SUCCESS
        );
        $this->_getState()->setCompleteStep(
            Mage_Checkout_Model_Type_Multishipping_State::STEP_OVERVIEW
        );
        $this->_getCheckout()->getCheckoutSession()->clear();
        $this->_getCheckout()->getCheckoutSession()->setDisplaySuccess(true);
        $this->_redirect('*/*/success');
    } catch (Mage_Payment_Model_Info_Exception $e) {
        ....
    } 
    ....
}

We can see that there is a createOrders() method.

public function createOrders()
{
    $orderIds = array();
    $this->_validate();
    $shippingAddresses = $this->getQuote()->getAllShippingAddresses();
    $orders = array();

    if ($this->getQuote()->hasVirtualItems()) {
        $shippingAddresses[] = $this->getQuote()->getBillingAddress();
    }

    try {
        foreach ($shippingAddresses as $address) {
            $order = $this->_prepareOrder($address);

            $orders[] = $order;
            Mage::dispatchEvent(
                'checkout_type_multishipping_create_orders_single',
                array('order'=>$order, 'address'=>$address)
            );
        }

        foreach ($orders as $order) {
            $order->place();
            $order->save();
            if ($order->getCanSendNewEmailFlag()){
                $order->queueNewOrderEmail();
            }
            $orderIds[$order->getId()] = $order->getIncrementId();
        }

        Mage::getSingleton('core/session')->setOrderIds($orderIds);
        Mage::getSingleton('checkout/session')->setLastQuoteId($this->getQuote()->getId());

        $this->getQuote()
            ->setIsActive(false)
            ->save();

        Mage::dispatchEvent('checkout_submit_all_after', array('orders' => $orders, 'quote' => $this->getQuote()));

        return $this;
    } catch (Exception $e) {
        Mage::dispatchEvent('checkout_multishipping_refund_all', array('orders' => $orders));
        throw $e;
    }
}

Within this method, we prepare the orders using the _prepareOrder() when foreaching around the number of addresses selected by the customer on the Multishipping checkout.

The orders are added to an $orders array, then we have another foreach that loops around the orders array and places the orders.

The checkout_submit_all_after event that gets dispatched subtracts and reindexes the quote inventory.

<?php
class Mage_CatalogInventory_Model_Observer
{
    ....
    public function checkoutAllSubmitAfter(Varien_Event_Observer $observer)
    {
        $quote = $observer->getEvent()->getQuote();
        if (!$quote->getInventoryProcessed()) {
            $this->subtractQuoteInventory($observer);
            $this->reindexQuoteInventory($observer);
        }
        return $this;
    }
    ....
}

Note: This article is based on Magento Community/Open Source version 1.9.