Mini Tutorial: Adding a Shipping Date Selector

While working on a B2B site, a recent client requested that the customer have the ability to choose their own ship date for certain products. On a normal B2C site, this maybe audacious and an all around bad idea, but on a site that is selling to businesses, this is a common request. This mini tutorial will show how to add a shipping date selector feature in Magento.

The Situation

The exact request was for date selector in the checkout, in which the first available date to choose was a week from the current day. Once the order is placed, the date would then be viewable on the admin order screen.

Choose a suitable calendar API

My first step was to find a suitable calendar API that would allow me to set the earliest possible date. Although there are many good date selector APIs out there, I went with Zebra Datepicker. Once the datepicker has been downloaded, the appropriate files will need to be loaded on the checkout page.

app/design/frontend/enterprise/theme/layout/local.xml

    <checkout_onepage_index>
        <reference name="head">
            <action method="addItem">
                <type>skin_js</type>
                <name>js/zebra_datepicker.js</name>
            </action>
            <action method="addItem">
                <type>skin_css</type>
                <name>css/zebra/default.css</name>
            </action>
        </reference>
    </checkout_onepage_index>

Add the Datepicker and date field

Next, I added the datepicker and date field onto the shipping method section of the onepage checkout.

app/design/frontend/enterprise/theme/template/checkout/onepage/shipping_method/additional.phtml

	<div class="datepicker-wrapper">	
		<label class="datepicker-label" for="datepicker"><?php echo $this->__('Select date for products to be shipped by:') ?></label>
		<input type="text" class="datepicker" name="shipdate" />
	</div>

At the bottom of the file, I included the js code that inserts the datepicker. For this example, I’m going to use 5 days as the minimum amount of time from today that the customer can choose to have their order shipped.

	<script type="text/javascript">
		jQuery(document).ready(function(){			
			jQuery('input.datepicker').Zebra_DatePicker({
				direction: 5
			});
		});
	</script>

The part that says “direction:5” is what tells the datepicker that everything up until 5 days from today should be disabled and unable to choose.

Once this was done, I had to create modules that would create the ship date field in the database and save that value to the database. To do so, I created the following files:

  • app/code/local/Demac/Checkout/controllers/OnpageContoller.php
  • app/code/local/Demac/Checkout/Model/Observer.php
  • app/code/local/Demac/Checkout/sql/demac_checkout_setup/mysql4-install-0.1.0.php
  • app/code/local/Demac/Checkout/etc/config.xml
  • app/etc/modules/Demac_Checkout.xml

In the above module, I am overriding an action method in the Onepage controller, creating an observer that will fire on order save, and adding the shipment_date field to the database.

In the first file; the controller, I overrode the saveShippingMethodAction() in order to include the shipdate attribute in the quote.

	<?php

	require_once 'Mage/Checkout/controllers/OnepageController.php';
	class Demac_Checkout_OnepageController extends Mage_Checkout_OnepageController {

		public function saveShippingMethodAction()
		{
			if ($this->_expireAjax()) {
				return;
			}
			if ($this->getRequest()->isPost()) {
				$data = $this->getRequest()->getPost('shipping_method', '');
				$shipdate = $this->getRequest()->getPost('shipdate', '');
				$quote = $this->getOnepage()->getQuote();
				$quote->setShipmentDate($shipdate);
				$quote->save();
				$result = $this->getOnepage()->saveShippingMethod($data);
				// $result will contain error data if shipping method is empty
				if (!$result) {
					Mage::dispatchEvent(
						'checkout_controller_onepage_save_shipping_method',
						 array(
							  'request' => $this->getRequest(),
							  'quote'   => $this->getOnepage()->getQuote()));
					$this->getOnepage()->getQuote()->collectTotals();
					$this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result));

					$result['goto_section'] = 'payment';
					$result['update_section'] = array(
						'name' => 'payment-method',
						'html' => $this->_getPaymentMethodsHtml()
					);
				}
				$this->getOnepage()->getQuote()->collectTotals()->save();
				$this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result));
			}
		}
	}

After that, I used an observer to save the updated quote to the order.

	<?php

	class Demac_Checkout_Model_Observer extends Varien_Object
	{
		public function saveShipmentDate($observer)
		{
			$order = $observer->getEvent()->getOrder();
			$quote = $order->getQuote();

			$order->setShipmentDate($quote->getShipmentDate());
		}
	}

In order for any of that to work, we need to add the shipment_date field to the order and quote tables in the database. This could easily be done through phpmyadmin, or shell access to the DB, but this is poor practice. Its always a much better idea to use Magento’s built in functionality to edit tables, as you will not always have direct access to the DB for security reasons, and there are less chances of error occurring.

	<?php

	/** @var $installer Demac_Sales_Model_Resource_Setup */
	$installer = $this;

	$installer->startSetup();

	$orderItemTable = $installer->getTable('sales_flat_quote');

	$installer->getConnection()
		->addColumn($orderItemTable,'shipment_date',
			array(
				'type'      => Varien_Db_Ddl_Table::TYPE_DATE,
				'nullable'  => true,
				'comment'   => 'Shipment Date'
			)
		);

	$orderItemTable2 = $installer->getTable('sales_flat_order');

	$installer->getConnection()
		->addColumn($orderItemTable2,'shipment_date',
			array(
				'type'      => Varien_Db_Ddl_Table::TYPE_DATE,
				'nullable'  => true,
				'comment'   => 'Shipment Date'
			)
		);

	$installer->endSetup();

Then the config.xml and Demac_Checkout.xml which contain the standard module override and activation properties.

	<?xml version="1.0" ?>
	<config>
		<modules>
			<Demac_Checkout>
				<version>0.1.0</version>
			</Demac_Checkout>
		</modules>
		<frontend>
			<events>
				<checkout_type_onepage_save_order>
					<observers>
						<saveShipmentDate>
							<class>Demac_Checkout_Model_Observer</class>
							<method>saveShipmentDate</method>
						</saveShipmentDate>
					</observers>
				</checkout_type_onepage_save_order>
			</events>
			<routers>
				<checkout>
					<args>
						<modules>
							<Demac_Checkout before="Mage_Checkout">Demac_Checkout</Demac_Checkout>
						</modules>
					</args>
				</checkout>
			</routers>
		</frontend>
	</config>
	<?xml version="1.0" ?>
	<config>
		<modules>
			<Demac_Checkout>
				<version>0.1.0</version>
				<active>true</active>
				<codePool>local</codePool>
			</Demac_Checkout>
		</modules>
	</config>

Its important to note the inclusion of 0.1.0 in these files. This tells magento the version number of the module, and tells it whether it needs to run the mysql install or update file.

Display the Shipdate on the Order Edit Screen

The last step I needed to do was to show the shipdate on the Order edit screen on the backend. Since I was editing a template in the adminhtml section, Magento doesn’t allow us to overwrite adminhtml template files with the regular frontend files, I had to include the following module:

app/code/local/Demac/Adminhtml/etc/config.xml

	<?xml version='1.0' ?>
	<config>
		<stores>
			<admin>
				<design>
					<package>
						<name>default</name>
					</package>
					<theme>
						<default>[your_theme]</default>
					</theme>
				</design>
			</admin>
		</stores>
	</config>

Just remember to change [your_theme] with your actual theme name.

The file I had to override is located at:

app/design/adminhtml/default/theme/template/sales/order/view/tab/info/phtml

Below

	<?php else: ?>
		<?php echo $this->helper('sales')->__('No shipping information available'); ?>
	<?php endif; ?>

I added

	<strong><?php echo $this->__('Ship Items By ') ?></strong>
	<?php echo $_order->getShipmentDate() ?>

Once this was all done, I had a full functioning frontend datepicker that saved a date to the order, and was viewable on the backend per order by the admin.