Category Archives: Shopping Cart Price Rules

Cloning a Magento Coupon

I’ve previously posted here about the complexities involved in Magento Salesrules and Coupon codes. While I don’t like the way Magento Rules are designed, I still have to work with them.

I put together a little module this morning because I needed to clone a coupon code, and I couldn’t find any native support to get this done.

For the lazy, I’ve put together the files required to get this functioning.
Download Mby_Rules.zip
Tested on EE 1.11.

For those of you interested in how this works, have a look at my run-down of conditions/actions first. Then, basically, I have extended the Mage_Salesrule_Model_Rule class and added some extra functions. Here’s the extended class, with minimal notes:

<?php
class Mby_Rules_Model_Salesrule extends Mage_SalesRule_Model_Rule
{

    /**
     * Makes a clone of a coupon code. Returns false if desired coupon code already exists.
     *
     * Usage:
        
        $salesrule = Mage::getModel("salesrule/rule")->load($ruleId);
        if ($clone = $salesrule->makeClone("NEWCOUPONCODE")) {    
            // Work with $clone
        } else {
            // Coupon code already exists.
        }
        
     * 
     * @param string $couponCode  the desired code for the cloned coupon.
     * return bool || Mby_Rules_Model_Salesrule  
     */
    public function makeClone($couponCode)
    {
        // Ensure no coupon code double up
        $rules = Mage::getModel("salesrule/rule")->getCollection()->addFieldToFilter('code', $couponCode);
        foreach ($rules as $rule) {
            return false;
        }
        
        $parentData = $this->getData();
        
        // Assign desired code
        $parentData['coupon_code'] = $couponCode;
        
        // Flatten conditions and actions
        $flatConditions = $this->_flatten('conditions', $parentData);
        $flatActions = $this->_flatten('actions', $parentData);
        
        // Unset unnecessary data
        unset($parentData['rule_id']);
        unset($parentData['conditions_serialized']);
        unset($parentData['actions_serialized']);
        
        // Init clone and assign data
        $clone = Mage::getModel("salesrule/rule")->setData($parentData)->save();
        $clone->setData('conditions', $flatConditions);
        $clone->setData('actions', $flatActions);
        
        // Setup data and finalise before save
        $cloneData = $clone->getData();
        if ($cloneData['from_date'] == "NULL") unset($cloneData['from_date']);
        if ($cloneData['to_date'] == "NULL") unset($cloneData['to_date']);
        $clone->loadPost($cloneData);
        $clone->save();
        
        return $clone;
    }
    
    /**
     * Sets up data and returns flattened data
     * @param string $type  Must be "actions" or "conditions"
     * @param array $parentData  Full data from the parent code.
     * return array
     */
    protected function _flatten($type, $parentData)
    {
        $unserialized = unserialize($parentData[$type.'_serialized']);
        $flatData = array();
        $this->_convertRecursiveToFlat("1", $unserialized, $flatData);
        ksort($flatData);
        return $flatData;
    }
    
    /**
     * Reverse of Mage_Rule_Model_Rule::_convertFlatToRecursive
     * Takes recursive Array and makes it flat again.
     * @param string $finalKey  the key of $conditions in the final flat array
     * @param array $conditions  a condition to insert
     * @param array &$finalContainer  the final array to end up flattened
     * return void
     */
    protected function _convertRecursiveToFlat($finalKey, array $conditions, array &$finalContainer = array())
    {
        foreach ($conditions['conditions'] as $key => $subconditions) {
            $this->_convertRecursiveToFlat($finalKey."--".($key+1), $subconditions, $finalContainer);
        }
        unset($conditions['is_value_processed']);
        unset($conditions['conditions']);
        $finalContainer[$finalKey] = $conditions;
        return;
    }

}

How to add Javascript to a Magento Admin page (AKA: How to Override Magento Blocks)

Recently I had to add some additional Javascript validation to a core part of the Magento Admin, and I thought I’d share the best way to do this without having to modify core files.

Because I often have to modify small parts of default Magento functionality, I have created a module that exists for the sole purpose of overriding models and blocks. In this post I’ll go through the steps required to override blocks properly, without modifying core code. The process for models is almost identical. I’m going to use as an example the block that I was required to override: Mage_Adminhtml_Block_Promo_Quote_Edit. All I want to do is add some additional Javascript to the page that hijacks the “Save” event and asks the user if they are sure.

First things first: build a custom module (or pick an existing one) to work from.

in {namespace}/{modulename}/etc/config.xml, add the following:

<config>
   ...
   <global>
      ...
      <blocks>
         <adminhtml>
            <rewrite>
               <promo_quote_edit>{namespace}_{modulename}_Block_Adminhtml_Promo_Quote_Edit</promo_quote_edit>
            </rewrite>
         </adminhtml>
      </blocks>
      ...
   </global>
   ...
</config>

What we’ve just done is tell Magento that when it looks for the block identified as “adminhtml/promo_quote_edit“, load this block class: {namespace}_{modulename}_Block_Adminhtml_Promo_Quote_Edit

We are therefore required to define this class. We do so in path that the class suggests. In this case, it’s: app/code/local/{namespace}/{modulename}/Block/Adminhtml/Promo/Quote/Edit.php. Create this file, and write the following:

<?php
    class {namespace}_{modulename}_Block_Adminhtml_Promo_Quote_Edit 
        extends Mage_Adminhtml_Block_Promo_Quote_Edit
    {

By using “extends” and giving the class name of the original block, our new class inherits all of the functions and variables of it’s parent class. If we left the file like this, Magento would act no differently from a user’s perspective.

To give that block new functionality, add as many new functions as you like. But to override a function, as I am doing here, redefine that function within your new class. One very important thing to remember, however, is to ensure that any overridden functions perform the same tasks as the parent, or you risk interfering unintentionally with Magento’s core functionality.

I want to add more Javascript, and I happen to know that Javascript is added into the $_formScripts variable that belongs to this block. I also know that Javascript tends to be added on __construct(). To ensure the class acts the same way as it always did, I define the __construct function like this:

<?php
    public function __construct()
    {
        parent::__construct();
        
        $this->_formScripts[] = "
            editForm.secondarySubmit = editForm.submit;
            editForm.submit = function(url) {
                if (document.getElementById('rule_coupon_type').value == 1) {
                    var answer = confirm('WARNING: This price rule has no Coupon Code.
                    Are you sure you want to do this?');
                } else {
                    var answer = true;
                }
                if (answer) {
                    editForm.secondarySubmit(url);
                } else {
                    return false;
                }
            }
        ";
    }
?>

Note the parent::__construct(); call, that will run the __construct() function of Mage_Adminhtml_Block_Promo_Quote_Edit

In this case, the Javascript I added uses the prototype library to redefine what happens when the form is saved.

Pretty simple really!

Tagged , , ,

Programmatically create Shopping Cart Price Rules with Conditions and Actions

Rather than going into detail of creating the Shopping Cart Price Rule from scratch, this post is specifically about conditions and actions.

If you have ever needed to programmatically create Magento Shopping Cart Price Rules, then you may have wondered how you attach conditions and actions.

Prior to saving your Mage_Salesrule_Model_Rule object, you will need to assign the following data:

<?php
    $rule->setData('actions',$actions);        
    $rule->setData('conditions',$conditions);
?>

Where $actions and $conditions are Array’s of a very specific format.

We then call Mage_Salesrule_Model_Rule::loadPost():

<?php
    $rule->loadPost($rule->getData());
?>

The format that is required for conditions and actions is significantly outside Magento’s EAV model, and is not documented as far as I can see. So to find it out we are required to trace it a little further.

Conditions and actions are processed within the Mage_Salesrule_Model_Rule::loadPost() function, and are converted to a recursive format that you find serialized in the database as conditions_serialized and actions_serialized table columns. This is done via Mage_Rule_Model_Rule::_convertFlatToRecursive() which is called within Mage_Salesrule_Model_Rule::loadPost().

But I digress, a little. What this means for us is that we need to put our data in a specific format prior to saving. I’ve established that format for you below. If you hijack Mage_Salesrule_Model_Rule::loadPost() and print_r() the content of a rule as it’s being saved, you’ll see the format:


    [conditions] => Array
        (
            [1] => Array
                (
                    [type] => salesrule/rule_condition_combine
                    [aggregator] => all
                    [value] => 1
                    [new_child] => 
                )
            [1--1] => Array
                (
                    [type] => salesrule/rule_condition_product_found
                    [value] => 1
                    [aggregator] => all
                    [new_child] => 
                )
            [1--1--1] => Array
                (
                    [type] => salesrule/rule_condition_product
                    [attribute] => sku
                    [operator] => ==
                    [value] => SKU123
                )
        )

    [actions] => Array
        (
            [1] => Array
                (
                    [type] => salesrule/rule_condition_product_combine
                    [aggregator] => all
                    [value] => 1
                    [new_child] => 
                )
            [1--1] => Array
                (
                    [type] => salesrule/rule_condition_product
                    [attribute] => sku
                    [operator] => ==
                    [value] => SKU123
                )
        )

In Layman’s terms, this is saying:


    [conditions][1]         
        "If ALL(aggregator) of these conditions are TRUE(value)"
    [conditions][1--1]      
        "If an item is FOUND(type, value) with ALL(aggregator) of these conditions true"
    [conditions][1--1--1]   
        "SKU(attribute) is(operator) SKU123(value)"
    
    [actions][1]            
        "if ALL(aggregator) of these conditions are TRUE(value)"
    [actions][1--1]         
        "SKU(attribute) is(operator) SKU123(value)"

So what are we really looking at? We’re looking at an array of arrays that uses the array keys to establish the heirarchy. The array keys are exploded by “--” in Mage_Rule_Model_Rule::_convertFlatToRecursive() and are converted to recursive arrays before being stored in the database.

This is how we can write it:

<?php
    $rule = Mage::getModel('salesrule/rule')->load($my_rule_id);
    $conditions = array(
        "1"         => array(
                "type"          => "salesrule/rule_condition_combine",
                "aggregator"    => "all",
                "value"         => "1",
                "new_child"     => null
            ),
        "1--1"      => array(
                "type"          => "salesrule/rule_condition_product_found",
                "aggregator"    => "all",
                "value"         => "1",
                "new_child"     => null
            ),
        "1--1--1"   => array(
                "type"          => "salesrule/rule_condition_product",
                "attribute"     => "sku",
                "operator"      => "==",
                "value"         => "SKU123"
            )
    );
    $actions = array(
        "1"         => array(
                "type"          => "salesrule/rule_condition_product",
                "aggregator"    => "all",
                "value"         => "1",
                "new_child"     => false
            ),
        "1--1"      => array(
                "type"          => "salesrule/rule_condition_product",
                "attribute"     => "sku",
                "operator"      => "==",
                "value"         => "SKU123"
            )
    );
    $rule->setData("conditions",$conditions);
    $rule->setData("actions",$actions);
    $rule->loadPost($rule->getData());
    $rule->save();
?>