As well as the standard Onepage checkout, the Magento Multishipping checkout is used when customers need to ship their items to multiple addresses.
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.
template/checkout/multishipping/addresses.phtml
– Mage/Checkout/Block/Multishipping/Addresses.php
template/checkout/multishipping/shipping.phtml
– Mage/Checkout/Block/Multishipping/Shipping.php
template/checkout/multishipping/billing.phtml
– Mage/Checkout/Block/Multishipping/Billing.php
template/checkout/multishipping/overview.phtml
– Mage/Checkout/Block/Multishipping/Overview.php
template/checkout/multishipping/success.phtml
– Mage/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.