Time Dependent Shopping Cart Rules in Magento [Freebie]

Time Dependent Shopping Cart Rules in Magento [Freebie]

Magento is probably the most powerful shopping cart currently available on the eCommerce market. In addition, it offers a free access to the core files and developers can add new features due to their needs. At the same time, this procedure requires the high level of technical skills. So, let’s take a deeper look at the process of adding a new feature to Magento. Below you will find out the path of creating a Time Dependent Shopping Cart Rules Extension for this shopping cart.

Magento Time Dependent Shopping Cart Rules Extension in Action

What Time Dependent Shopping cart Rules Mean?

When you start a promo campaign on your store, it is likely to get as much from the shopping cart as possible. Out-of-box, Magento provides store owners with a limited functionality regarding price rules. You can set only the Start and End Date, but have no permission to configure the exact time. So, Orange 35 Developers Team decided to change this situation. So, after a laborious inspection of Magento core files, the solution was found.

Today, it is available as a Free Magento Extension from Orange35 Store and Magento Connect. You can simply install it and benefit from new features. But, in this article, the development process is described step-by-step. So, keep reading to find out what was done before the release.

Step-By-Step Guide for Magento Extension Development

First of all, we are going to override some of the Magento core models and controllers. To do so, we will basically need to create an extension. So please see some basic steps of creating an extension.

\app\etc\modules\Orange35_SalesruleTime.xml
<?xml version="1.0"?>
<config>
    <modules>
        <Orange35_SalesruleTime>
            <active>true</active>
            <codePool>community</codePool>
            <depends>
                <Mage_Rule />
                <Mage_SalesRule />
            </depends>
        </Orange35_SalesruleTime>
    </modules>
</config>
\app\code\community\Orange35\SalesruleTime\etc\config.xml
<?xml version="1.0"?>
<config>
    <modules>
        <Orange35_SalesruleTime>
            <version>1.0.0</version>
        </Orange35_SalesruleTime>
    </modules>
    <global>
        <models>
            <orange35_salesruletime>
                <class>Orange35_SalesruleTime_Model</class>
                <resourceModel>orange35_salesruletime_resource</resourceModel>
            </orange35_salesruletime>
            <orange35_salesruletime_resource>
                <class>Orange35_SalesruleTime_Model_Resource</class>
            </orange35_salesruletime_resource>
        </models>
        <blocks>
            <orange35_salesruletime>
                <class>Orange35_SalesruleTime_Block</class>
            </orange35_salesruletime>
        </blocks>
        <resources>
            <orange35_salesruleTime_setup>
                <setup>
                    <module>Orange35_SalesruleTime</module>
                    <class>Mage_Catalog_Model_Resource_Setup</class>
                </setup>
            </orange35_salesruleTime_setup>
        </resources>
    </global>
</config>

Now we are going to go to the database and check where Magento saves shopping cart price rules. It is ‘salesrule’ table. And as you probably noticed, “from_date” and “to_date” columns have the “DATE” type.
We need to change it to “DATETIME”. Thus, you basically just need to execute this SQL script:

ALTER TABLE `salesrule` CHANGE `from_date` `from_date` DATETIME NULL DEFAULT NULL COMMENT 'From Date'
ALTER TABLE `salesrule` CHANGE `to_date` `to_date` DATETIME NULL DEFAULT NULL COMMENT 'To Date'

Although, executing it every time on every site could be a problem, so let’s just make our extension do it by itself on installation and save us much time.

\app\code\community\Orange35\SalesruleTime\sql\orange35_salesruleTime_setup\install-1.0.0.php
<?php
$installer = $this;
$installer->startSetup();

$adapter = $installer->getConnection();
$adapter->modifyColumn($installer->getTable('salesrule/rule'), "from_date", Varien_Db_Ddl_Table::TYPE_DATETIME);
$adapter->modifyColumn($installer->getTable('salesrule/rule'), "to_date", Varien_Db_Ddl_Table::TYPE_DATETIME);
$installer->endSetup();

If we check the JavaScript plugin Magento uses to pick the date, we will notice that it already has the time function and Magento itself already has the possibility to turn it on.
To do that we will need to override _prepareForm() function in the Block Mage_Adminhtml_Block_Promo_Quote_Edit_Tab_Main. We’re interested in these lines:


$dateFormatIso = Mage::app()->getLocale()->getDateFormat(Mage_Core_Model_Locale::FORMAT_TYPE_SHORT);
$fieldset->addField('from_date', 'date', array(
'name'   => 'from_date',
'label'  => Mage::helper('salesrule')->__('From Date'),
'title'  => Mage::helper('salesrule')->__('From Date'),
'image'  => $this->getSkinUrl('images/grid-cal.gif'),
'input_format' => Varien_Date::DATE_INTERNAL_FORMAT,
'format'       => $dateFormatIso
));
$fieldset->addField('to_date', 'date', array(
'name'   => 'to_date',
'label'  => Mage::helper('salesrule')->__('To Date'),
'title'  => Mage::helper('salesrule')->__('To Date'),
'image'  => $this->getSkinUrl('images/grid-cal.gif'),
'input_format' => Varien_Date::DATE_INTERNAL_FORMAT,
'format'       => $dateFormatIso
));

Here’s a list of changes that are required:

  • We need to modify $dateFormatIso this way:
    $dateFormatIso = Mage::app()->getLocale()->getDateTimeFormat(Mage_Core_Model_Locale::FORMAT_TYPE_SHORT);
  • change ‘input_format’ to Varien_Date::DATETIME_INTERNAL_FORMAT
  • add ‘time’ => true to each field to enable time inside javascript datepicker

Instead of overriding this block, we will use Magento event/observer feature and we need to put the observer on event named ‘adminhtml_promo_quote_edit_tab_main_prepare_form’. We need to add these lines to our config.xml file:

<adminhtml>
        <events>
            <adminhtml_promo_quote_edit_tab_main_prepare_form>
                <observers>
                    <orange35_salesruletime_quote_edit_tab_main_prepare_form>
                        <type>singleton</type>
                        <class>orange35_salesruletime/observer</class>
                        <method>adminhtmlPromoQuoteEditTabMainPrepareForm</method>
                    </orange35_salesruletime_quote_edit_tab_main_prepare_form>
                </observers>
            </adminhtml_promo_quote_edit_tab_main_prepare_form>
        </events>
    </adminhtml>

And create this file:

\app\code\community\Orange35\SalesruleTime\Model\Observer.php

Full code with changes listed above:

<?php
class Orange35_SalesruleTime_Model_Observer{
    public function adminhtmlPromoQuoteEditTabMainPrepareForm($observer){
        $form = $observer->getData("form");
        $fs = $form->getElement("base_fieldset");
        $dateFormatIso = Mage::app()->getLocale()->getDateTimeFormat(Mage_Core_Model_Locale::FORMAT_TYPE_SHORT);
        foreach($fs->getElements() as $element){
            if($element->getName() == "to_date" || $element->getName() == "from_date"){
                $element->setData("input_format", Varien_Date::DATETIME_INTERNAL_FORMAT);
                $element->setData("format", $dateFormatIso);
                $element->setData("time", true);
            }
        }
        $model = Mage::registry('current_promo_quote_rule');
        $form->setValues($model->getData());
    }
}

Highlighted are changes related to time format. But there is another code important to make everything work and keep the default functionality.
Getting the access to the form and fieldset:

$form = $observer->getData("form");
$fs = $form->getElement("base_fieldset");

Since we changed date field format we will need to fill them out with saved values again. We’ll just re-fill out the whole form since this is the easiest way to do it:

$model = Mage::registry('current_promo_quote_rule');
$form->setValues($model->getData());

Next, if we take a look at this function in this file:

\app\code\core\Mage\Rule\Model\Abstract.php
protected function _convertFlatToRecursive(array $data)
    {
        $arr = array();
        foreach ($data as $key => $value) {
            if (($key === 'conditions' || $key === 'actions') && is_array($value)) {
                foreach ($value as $id=>$data) {
                    $path = explode('--', $id);
                    $node =& $arr;
                    for ($i=0, $l=sizeof($path); $i<$l; $i++) {
                        if (!isset($node[$key][$path[$i]])) {
                            $node[$key][$path[$i]] = array();
                        }
                        $node =& $node[$key][$path[$i]];
                    }
                    foreach ($data as $k => $v) {
                        $node[$k] = $v;
                    }
                }
            } else {
                /**
                 * Convert dates into Zend_Date
                 */
                if (in_array($key, array('from_date', 'to_date')) && $value) {
                    $value = Mage::app()->getLocale()->date(
                        $value,
                        Varien_Date::DATE_INTERNAL_FORMAT,
                        null,
                        false
                    );
                }
                $this->setData($key, $value);
            }
        }

        return $arr;
    }

 

Our goal is to change this

Varien_Date::DATE_INTERNAL_FORMAT

to this:

Varien_Date::DATETIME_INTERNAL_FORMAT

But we can’t do this overriding a class Mage_Rule_Model_Abstract because the class is abstract. We need to override this function in all Model classes used in Sales Rules that extend this class. So here is the code that needs to be added to <models> tag inside config.xml:

            <salesrule>
                <rewrite>
                    <rule>Orange35_SalesruleTime_Model_SalesRule_Rule</rule>
                </rewrite>
            </salesrule>
            <rule>
                <rewrite>
                    <rule>Orange35_SalesruleTime_Model_Rule_Rule</rule>
                </rewrite>
            </rule>

 

And appropriate file for each class:

\app\code\community\Orange35\SalesruleTime\Model\SalesRule\Rule.php
<?php
class Orange35_SalesruleTime_Model_SalesRule_Rule extends Mage_SalesRule_Model_Rule {
    protected function _convertFlatToRecursive(array $data)
    {
        $arr = array();
        foreach ($data as $key => $value) {
            if (($key === 'conditions' || $key === 'actions') && is_array($value)) {
                foreach ($value as $id=>$data) {
                    $path = explode('--', $id);
                    $node =& $arr;
                    for ($i=0, $l=sizeof($path); $i<$l; $i++) {
                        if (!isset($node[$key][$path[$i]])) {
                            $node[$key][$path[$i]] = array();
                        }
                        $node =& $node[$key][$path[$i]];
                    }
                    foreach ($data as $k => $v) {
                        $node[$k] = $v;
                    }
                }
            } else {
                /**
                 * Convert dates into Zend_Date
                 */
                if (in_array($key, array('from_date', 'to_date')) && $value) {
                    $value = Mage::app()->getLocale()->date(
                        $value,
                        Varien_Date::DATETIME_INTERNAL_FORMAT,
                        null,
                        false
                    );
                }
                $this->setData($key, $value);
            }
        }

        return $arr;
    }
}
\app\code\community\Orange35\SalesruleTime\Model\Rule\Rule.php
<?php
class Orange35_SalesruleTime_Model_Rule_Rule extends Mage_Rule_Model_Rule {
    protected function _convertFlatToRecursive(array $data)
    {
        $arr = array();
        foreach ($data as $key => $value) {
            if (($key === 'conditions' || $key === 'actions') && is_array($value)) {
                foreach ($value as $id=>$data) {
                    $path = explode('--', $id);
                    $node =& $arr;
                    for ($i=0, $l=sizeof($path); $i<$l; $i++) {
                        if (!isset($node[$key][$path[$i]])) {
                            $node[$key][$path[$i]] = array();
                        }
                        $node =& $node[$key][$path[$i]];
                    }
                    foreach ($data as $k => $v) {
                        $node[$k] = $v;
                    }
                }
            } else {
                /**
                 * Convert dates into Zend_Date
                 */
                if (in_array($key, array('from_date', 'to_date')) && $value) {
                    $value = Mage::app()->getLocale()->date(
                        $value,
                        Varien_Date::DATETIME_INTERNAL_FORMAT,
                        null,
                        false
                    );
                }
                $this->setData($key, $value);
            }
        }

        return $arr;
    }
}

The next function we need to override is located here:
\app\code\core\Mage\SalesRule\Model\Resource\Rule\Collection.php

public function addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now = null)
    {
        if (!$this->getFlag('website_group_date_filter')) {
            if (is_null($now)) {
                $now = Mage::getModel('core/date')->date('Y-m-d');
            }

            $this->addWebsiteFilter($websiteId);

            $entityInfo = $this->_getAssociatedEntityInfo('customer_group');
            $connection = $this->getConnection();
            $this->getSelect()
                ->joinInner(
                    array('customer_group_ids' => $this->getTable($entityInfo['associations_table'])),
                    $connection->quoteInto(
                        'main_table.' . $entityInfo['rule_id_field']
                            . ' = customer_group_ids.' . $entityInfo['rule_id_field']
                            . ' AND customer_group_ids.' . $entityInfo['entity_id_field'] . ' = ?',
                        (int)$customerGroupId
                    ),
                    array()
                )
                ->where('from_date is null or from_date <= ?', $now)
                ->where('to_date is null or to_date >= ?', $now);

            $this->addIsActiveFilter();

            $this->setFlag('website_group_date_filter', true);
        }

        return $this;
    }

Our goal is to let Magento know the current time when it is validating the dates.
We need to change this:

$now = Mage::getModel('core/date')->date('Y-m-d');

to this:

$now = Mage::getModel('core/date')->date('Y-m-d H:i:s');

To override this function we will need to update our config.xml file with these lines under <models> tag.

            <salesrule_resource>
                <rewrite>
                    <rule_collection>Orange35_SalesruleTime_Model_Resource_SalesRule_Rule_Collection</rule_collection>
                </rewrite>
            </salesrule_resource>

Also you will need to create this file with following content:

\app\code\community\Orange35\SalesruleTime\Model\Resource\SalesRule\Rule\Collection.php
<?php
class Orange35_SalesruleTime_Model_Resource_SalesRule_Rule_Collection extends Mage_SalesRule_Model_Resource_Rule_Collection {
    public function addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now = null)
    {
        if (!$this->getFlag('website_group_date_filter')) {
            if (is_null($now)) {
                $now = Mage::getModel('core/date')->date('Y-m-d H:i:s');
            }

            $this->addWebsiteFilter($websiteId);

            $entityInfo = $this->_getAssociatedEntityInfo('customer_group');
            $connection = $this->getConnection();
            $this->getSelect()
                ->joinInner(
                    array('customer_group_ids' => $this->getTable($entityInfo['associations_table'])),
                    $connection->quoteInto(
                        'main_table.' . $entityInfo['rule_id_field']
                        . ' = customer_group_ids.' . $entityInfo['rule_id_field']
                        . ' AND customer_group_ids.' . $entityInfo['entity_id_field'] . ' = ?',
                        (int)$customerGroupId
                    ),
                    array()
                )
                ->where('from_date is null or from_date <= ?', $now)
                ->where('to_date is null or to_date >= ?', $now);

            $this->addIsActiveFilter();

            $this->setFlag('website_group_date_filter', true);
        }

        return $this;
    }
}

The last thing we need to do is to change types in:

\app\code\core\Mage\Adminhtml\controllers\Promo\QuoteController.php

All we need to do is to replace $this->_filterDates to $this->_filterDateTime.

So we need to override 2 functions in this controller:

  • saveAction()
  • generateAction()

To do so we need to add this strings to our config.xml file:

    <admin>
        <routers>
            <adminhtml>
                <args>
                    <modules>
                        <orange35_salesruletime before = "Mage_Adminhtml">Orange35_SalesruleTime_Adminhtml</orange35_salesruletime>
                    </modules>
                </args>
            </adminhtml>
        </routers>
    </admin>

And create this file:

\app\code\community\Orange35\SalesruleTime\controllers\Adminhtml\Promo\QuoteController.php
<?php
require_once Mage::getModuleDir('controllers', 'Mage_Adminhtml') . DS . 'Promo/QuoteController.php';
class Orange35_SalesruleTime_Adminhtml_Promo_QuoteController extends Mage_Adminhtml_Promo_QuoteController {

    public function generateAction()
    {
        if (!$this->getRequest()->isAjax()) {
            $this->_forward('noRoute');
            return;
        }
        $result = array();
        $this->_initRule();

        /** @var $rule Mage_SalesRule_Model_Rule */
        $rule = Mage::registry('current_promo_quote_rule');

        if (!$rule->getId()) {
            $result['error'] = Mage::helper('salesrule')->__('Rule is not defined');
        } else {
            try {
                $data = $this->getRequest()->getParams();
                if (!empty($data['to_date'])) {
                    $data = array_merge($data, $this->_filterDateTime($data, array('to_date')));
                }

                /** @var $generator Mage_SalesRule_Model_Coupon_Massgenerator */
                $generator = $rule->getCouponMassGenerator();
                if (!$generator->validateData($data)) {
                    $result['error'] = Mage::helper('salesrule')->__('Not valid data provided');
                } else {
                    $generator->setData($data);
                    $generator->generatePool();
                    $generated = $generator->getGeneratedCount();
                    $this->_getSession()->addSuccess(Mage::helper('salesrule')->__('%s Coupon(s) have been generated', $generated));
                    $this->_initLayoutMessages('adminhtml/session');
                    $result['messages']  = $this->getLayout()->getMessagesBlock()->getGroupedHtml();
                }
            } catch (Mage_Core_Exception $e) {
                $result['error'] = $e->getMessage();
            } catch (Exception $e) {
                $result['error'] = Mage::helper('salesrule')->__('An error occurred while generating coupons. Please review the log and try again.');
                Mage::logException($e);
            }
        }
        $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result));
    }

    public function saveAction()
    {
        if ($this->getRequest()->getPost()) {
            try {
                /** @var $model Mage_SalesRule_Model_Rule */
                $model = Mage::getModel('salesrule/rule');
                Mage::dispatchEvent(
                    'adminhtml_controller_salesrule_prepare_save',
                    array('request' => $this->getRequest()));
                $data = $this->getRequest()->getPost();
                $data = $this->_filterDateTime($data, array('from_date', 'to_date'));
                $id = $this->getRequest()->getParam('rule_id');
                if ($id) {
                    $model->load($id);
                    if ($id != $model->getId()) {
                        Mage::throwException(Mage::helper('salesrule')->__('Wrong rule specified.'));
                    }
                }

                $session = Mage::getSingleton('adminhtml/session');

                $validateResult = $model->validateData(new Varien_Object($data));
                if ($validateResult !== true) {
                    foreach($validateResult as $errorMessage) {
                        $session->addError($errorMessage);
                    }
                    $session->setPageData($data);
                    $this->_redirect('*/*/edit', array('id'=>$model->getId()));
                    return;
                }

                if (isset($data['simple_action']) && $data['simple_action'] == 'by_percent'
                    && isset($data['discount_amount'])) {
                    $data['discount_amount'] = min(100,$data['discount_amount']);
                }
                if (isset($data['rule']['conditions'])) {
                    $data['conditions'] = $data['rule']['conditions'];
                }
                if (isset($data['rule']['actions'])) {
                    $data['actions'] = $data['rule']['actions'];
                }
                unset($data['rule']);
                $model->loadPost($data);

                $useAutoGeneration = (int)!empty($data['use_auto_generation']);
                $model->setUseAutoGeneration($useAutoGeneration);

                $session->setPageData($model->getData());

                $model->save();
                $session->addSuccess(Mage::helper('salesrule')->__('The rule has been saved.'));
                $session->setPageData(false);
                if ($this->getRequest()->getParam('back')) {
                    $this->_redirect('*/*/edit', array('id' => $model->getId()));
                    return;
                }
                $this->_redirect('*/*/');
                return;
            } catch (Mage_Core_Exception $e) {
                $this->_getSession()->addError($e->getMessage());
                $id = (int)$this->getRequest()->getParam('rule_id');
                if (!empty($id)) {
                    $this->_redirect('*/*/edit', array('id' => $id));
                } else {
                    $this->_redirect('*/*/new');
                }
                return;

            } catch (Exception $e) {
                $this->_getSession()->addError(
                    Mage::helper('catalogrule')->__('An error occurred while saving the rule data. Please review the log and try again.'));
                Mage::logException($e);
                Mage::getSingleton('adminhtml/session')->setPageData($data);
                $this->_redirect('*/*/edit', array('id' => $this->getRequest()->getParam('rule_id')));
                return;
            }
        }
        $this->_redirect('*/*/');
    }
}

After that, you will be able to set an exact Start and End time for your Magento Shopping Cart Price Rules in the DateTime format.

Rememmber, that you can get this extension for Free from Magento Connect or Orange35 Extensions Store right now.

Hope this article was helpful, but if you wish to add something, leave it in the comments below.

5 thoughts on “Time Dependent Shopping Cart Rules in Magento [Freebie]

  1. one more thing, you need to update/change “$model->validateData”, because if we enter less time in to date for same date, it gets pass, but it shouldn’t.

  2. Hi Orange35 Team,

    nice extention and work perfect on cart rules….But I´m missing only the feature in the catalog price rules.
    It will be perfect, when this feature is in the next version….

    Thanks Markus Marcel

    • Unfortunately there’s no solution for catalog price rules yet as we didn’t have a chance to go over it.
      The catalog price rules system is actually a lot more complicated in terms of how items are being saved and updated. There’s a chance our team will investigate this in the future, but it is not scheduled at the moment.

Leave a Reply

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