How to detect if a price rule gets applied to a product

We were asked to write a simple module that makes it possible to display a rule-specific teaser on product pages when they are affected by catalog price rules.

So, our first question was: how can we even detect if a price rule is applied to a product?

The easiest Solutions

Model Mage_CatalogRule_Model_Rule offers two methods that seem quite interesting: loadProductRules and calcProductPriceRule.

loadProductRules will save any rule ids that are attached to the given product in the product’s collection:

// $_product is an instance of Mage_Catalog_Model_Product
$catalogRule = Mage::getModel('catalogrule/rule')->loadProductRules($_product);

$catalogRule will now contain an array: Array( [rule_id] => [irrelevant_number] ). If you want to know why we get an irrelevant number here, you can read up below.

While this is the fastest and easiest way to get any rules applied to our product, the most important thing to note is that it literally gives us *every* rule that is somehow connected. There are no filters applied which means we also get future and past rules and rules that are meant for any customer group or website. Depending on what you want to do this can be totally fine, of course.

Lets take a look at calcProductPriceRule. This handy method is returning our product’s price with any catalog rule applied to it. If there is no rule applied it will return NULL. This sounds great because we can use it do exactly what we need – detect whether a price rule gets applied to our product or not:

if( Mage::getModel('catalogrule/rule')->calcProductPriceRule($_product,$_product->getPrice()) ){
	echo 'Catalog price rule applied';
} else {
	echo 'No catalog price rule applied';
}

And yes, this one filters our rules by date, customer groups and website. The only downside of it is that it does more than we need it to do – which means it takes more resources than necessary.

So what are the alternatives?

An alternative way and also the way that we ended up going is reproducing what calcProductPriceRule does, but without actually calculating any prices. Essentially, it makes use of Mage_CatalogRule_Model_Resource_Rule::getRulesFromProduct and so will we:

Step 1: Set up your Model

class My_Module_Model_Catalogrule extends Mage_Core_Model_Abstract {
  /*
  * @param Mage_Catalog_Model_Product $product
  * @return array
  */
  public function getRulesFromProduct(Mage_Catalog_Model_Product $product)
  {
	  $productId  = $product->getId();
	  $storeId    = $product->getStoreId();
	  $websiteId  = Mage::app()->getStore($storeId)->getWebsiteId();
	  if ($product->hasCustomerGroupId()) {
		  $customerGroupId = $product->getCustomerGroupId();
	  } else {
		  $customerGroupId = Mage::getSingleton('customer/session')->getCustomerGroupId();
	  }
	  $dateTs     = Mage::app()->getLocale()->date()->getTimestamp();
  
	  return Mage::getResourceModel('catalogrule/rule')->getRulesFromProduct($dateTs, $websiteId, $customerGroupId, $productId);	  
  }	
}

Our method returns an empty array if the product does not have any price rules applied.

Step 2: Set up your Block

class My_Module_Block_Catalog_Product_Catalogrule extends Mage_Catalog_Block_Product_Abstract {
	
	protected $_rules;
	public function getRules(){
		if( is_null($this->_rules) ){
			$catalogrule = Mage::getModel('my_module/catalogrule');
			if(($product = $this->getProduct()) && ($rules = $catalogrule->getRulesFromProduct($product))){
				$this->_rules = $rules;
			} else {
				$this->_rules = false;	
			}
		}
		return $this->_rules;
	}
	
	public function hasRuleApplied(){
		if( $this->getRules() ){
			return true;
		}
		return false;
	}	
}

Step 3: Set up your template

Now we can do stuff like this in our block’s template:

<?php if($this->hasRuleApplied()): ?>
    <?php foreach($this->getRules() as $rule): ?>
    <div class="pricerule-teaser">
		<strong><?php 
        switch($rule['action_operator']){
            case 'by_percent':
                echo $this->__('%s%% Discount', round($rule['action_amount']));
            break;
            case 'by_fixed':
                echo $this->__('%s Discount', Mage::helper('core')->currency($rule['action_amount'], true, false));
            break;	
            case 'to_percent':
                echo $this->__('Only %s%% of the original price', round($rule['action_amount']));
            break;	
            case 'to_fixed':
                echo $this->__('Only %s', Mage::helper('core')->currency($rule['action_amount'], true, false));
            break;	
        } ?></strong>
    	<?php echo $this->__('until the %s !', date('dS M', $rule['to_time'])) ?>
    </div>
    <?php endforeach; ?>
<?php endif; ?>

Read up below if you want a list of all the values our $rule array stores.

Well and that’s it! Let us know if you have questions or ideas for improvement.

Continue reading How to detect if a price rule gets applied to a product

Extra Renderer for Dynamic Thumbnails in Cart

(Magento 1.9.2.2 on Debian Jessie, running PHP 5.6.30 with Imagick 3.1.2)

In some cases it becomes necessary to show Dynamic Thumbnails, like SVG or canvas-gernerated images, in your cart rather than static product photos. In this article we address a possible solution for this.

Situation

We’ve developed a module that lets end customers put together their desired color-combination of a product by assigning patterns to a SVG via JavaScript. They can choose between over 50 colors for 4 separate product parts, therefore the number of possible color-combinations is quite huge.

Each color-combination is represented by a simple unqiue code (e.g. A: 22 B:09, C: 01, D: 11) that is stored into a hidden custom option input when added to cart.

Problem

Our item’s cart thumbnail needs to represent the actual color-combination (preferrably SVG into JPG). However, Magento doesn’t make Dynamic Thumbnails possible.

Our Solution

We add a custom cart-renderer that will overwrite Mage_Checkout_Block_Cart_Item_Renderer_Configurable::getProductThumbnail().

Alternatively, you could add the new renderer and assign it to a new template file that will for instance call a new method like getDynamicThumbnail(), without overwriting anything. However, in our case we had to work with the already existing renderer templates.

Step 1

We took a very similar approach like they did in this article. However, we made our module listen to a different Event, sales_quote_item_set_product:

<?xml version="1.0" encoding="utf-8"?>
<config>
	<!-- ... -->
	<frontend>
		<!-- ... -->
		<events>
		<!-- ... -->
			<controller_action_layout_load_before>
				<observers>
					<sales_quote_item_set_product>
				<observers>
					<my_cart_observer>
						<class>my_module/observer</class>
						<method>salesQuoteItemSetProduct</method>
					</my_cart_observer>
				</observers>
			</sales_quote_item_set_product>
		</events>
	</frontend>
	<!-- ... -->
</config>

Step 2

In our Observer method we first check if set quote item is the one attached to our SVG module. In our case we’re storing this product ID in our module’s settings so you might want to adjust this code to your business logic:

class My_Module_Model_Observer
{	 
	public function salesQuoteItemSetProduct(Varien_Event_Observer $observer){
		
		$product = $observer->getEvent()->getProduct();
		
		if($product->getId() != Mage::getStoreConfig('model_config/general/product_id'))
		{
			return $this;
		}
		
		$quoteItem = $observer->getEvent()->getQuoteItem();
		if(!($option = $quoteItem->getOptionByCode('product_type')) 
				|| $option->getValue() != 'customized_product')
		{
			$option = new Varien_Object();
			$option->setData(array(
				'product' => $product,
				'code' => 'product_type',
				'value' => 'customized_product'
			));		
			$quoteItem->addOption($option);
		}
		return $this;
	}	
}

So, what are we doing there? Well, what we don’t do is adding an actual new product type here. No, we simply assign a new option with the code “product_type” and the value “customized_product” (the name of the pseudo product-type; you can choose any name that isn’t already taken by actual product types). Why does this even work? Read up in the above linked article.

Step 3

Now we have to assign our custom Item Renderer, that will generate and add our Dynamic Thumbnails, to our new “pseudo product type”. We do this in a layout file – in our case our module’s own layout file. This is an example:

<?xml version="1.0" encoding="UTF-8" ?>
<layout version="0.1.0">
	<default>
		<reference name="cart_topcart">
        	<action method="addItemRender"><type>customized_product</type><block>my_module/cart_renderer</block><template>your/sidecart/renderer/layout.phtml</template></action>
		</reference>
	</default>	
	<checkout_cart_index>
		<reference name="checkout.cart">
        	<action method="addItemRender"><type>customized_product</type><block>my_module/cart_renderer</block><template>your/cart/renderer/layout.phtml</template></action>
		</reference>
	</checkout_cart_index>
	<checkout_onepage_index>
		<reference name="cart_sidebar">
        	<action method="addItemRender"><type>customized_product</type><block>my_module/cart_renderer</block><template>your/checkout/renderer/layout.phtml</template></action>
		</reference>
	</checkout_onepage_index>
	<checkout_onepage_review>
		<reference name="root">
        	<action method="addItemRender"><type>customized_product</type><block>my_module/cart_renderer</block><template>your/review/renderer/layout.phtml</template></action>
		</reference>
	</checkout_onepage_review>
</layout>

You’d have to adjust this to the renderer-layout files and references you are using. Also, keep in mind that depending on whether you already have other Modules addressing the Checkout/Cart process in place, you want to add them to the <depends> tags in your new module’s my_module.xml.

Step 4

Because our customizable product is a simple product belonging to a configurable product, we let our Renderer extend Magento’s Configurable Renderer:

class My_Module_Block_Cart_Item_Renderer extends Mage_Checkout_Block_Cart_Item_Renderer_Configurable
{	
    /**
     * @return Mage_Catalog_Model_Product_Image
     */
    public function getProductThumbnail()
    {
        $isCustomized	= false;
		$code 			= [];
		$product 		= $this->getChildProduct();
		
		if(($options = $this->getProductOptions())){
			/** 	
			 *	Get and form your necessary code.
			 *	Set $isCustomized to true during this process. 
			**/
		} 
		if( $isCustomized ){
			
			$helper = Mage::helper('my_module');
			/**
			 * We form following variables here:
			 * 
			 * $baseDir - the base path to the directory we'll store the connverted file
			 * $path - the absolute path to the directory we'll store the converted SVG files
			 * $filename - the name of the converted file. In our case it's the code combination.
			 * $svg -	the content of our svg with set color patterns. if you have trouble saving
			 * 			your SVG with patterns, write them directly (base64_encode) into your SVG
			 * 			instead of linking the image file.
			 * 
			**/
			$filepath = $path . DS . $filename;
			if(!is_file($filepath)){
				clearstatcache();
				
				$image = new Imagick();
				$image->readImageBlob($svg);
				
				$image->setImageFormat('jpeg');
      			$image->setImageCompressionQuality(70);
				$image->adaptiveResizeImage(100, 100);
				$image->writeImage($path . DS . $filename);
				
			}	
			return Mage::helper('my_module/image')->init($product, $baseDir . DS . $filename);
			
		} else {
			return parent::getProductThumbnail();
		}
	}
}

Step 5

Normally, getProductThumbnail() is returning Mage_Catalog_Model_Product_Image, called by $this->helper(‘catalog/image’)->init($product, ‘thumbnail’). This module does only access images within {ROOT}/media/catalog/product, however, we store our converted image files in {ROOT}/media/my/module. Therefore we make a custom helper “jump in”:

class My_Module_Helper_Image extends Mage_Core_Helper_Abstract
{
	protected $_product;
    public function init($product, $imageFile)
    {
        $this->setImageFile($imageFile);
        return $this;
    }
    public function resize($width, $height = null)
    {
        return $this;
    }	
    protected function setWatermark($watermark)
    {
        return $this;
    }
    protected function setImageFile($file)
    {
        $this->_imageFile = $file;
        return $this;
    }
    protected function getImageFile()
    {
        return $this->_imageFile;
    }
    public function __toString()
    {
       return Mage::getBaseUrl('media') . $this->getImageFile();
    }
}

In our case this helper doesn’t need to do more at the moment. But we could extend it anytime, if necessary.

This is pretty much it.

Let us know if you have ideas on improvements or some related questions!

Adding conditional Links to the top navigation

One of my today’s tasks was to add conditional links to our shop’s top navigation. Let me show you how we achieved that – it’s easy!

The very Basics

Adding new links is archieved quite easily by inserting following markup to your local.xml (app/design/frontend/{yourpackage}/{yourtheme}/layout/local.xml) or custom module template file, as we will do later on:

<?xml version="1.0"?>
<layout version="0.1.0">
<!-- ... -->
    <default>
        <reference name="top.links">
            <action method="addLink" translate="label title">
                <label>My link</label>
                <url>fancy/url</url>
                <title>My link</title>
                <prepare />
                <urlParams />
                <position>100</position>
                <aParams>
                    <class>top-link-mylink</class>
                </aParams>
            </action>
        </reference>
    </default>
<!-- ... -->
</layout>

As Magento comes with <customer_logged_in> and <customer_logged_out> handles, we can easily add links that are supposed to only show up for logged in (or only logged out) customers:

<?xml version="1.0"?>
<layout version="0.1.0">
     <customer_logged_in>
        <reference name="top.links">
            <action method="addLink" translate="label title">
                <label>My new link</label>
                <url>path/to/wherever</url>
                <title>My new link</title>
                <prepare />
                <urlParams />
                <position>50</position>
                <aParams>
                    <class>top-link-mynewlink</class>
                </aParams>
            </action>
        </reference>
    </customer_logged_in>
</layout>

In order to make already existing links, like “My account”, only show up for logged in customers, we have to remove them first. Also, in case you’re new to this, pay attention to the preferred way of getting an internal URL: instead of writing <url>customer/account</url> we want the available helper to fetch the URL:

<?xml version="1.0"?>
<layout version="0.1.0">
    <default>
        <reference name="top.links">
            <action method="removeLinkByUrl"><url helper="customer/getAccountUrl" /></action>
        </reference>
    </default>
    <customer_logged_in>
        <reference name="top.links">
            <action method="addLink" translate="label title">
                <label>My Account</label>
                <url helper="customer/getAccountUrl"/>
                <title>My Account</title>
                <prepare />
                <urlParams />
                <position>50</position>
                <aParams>
                    <class>top-link-account</class>
                </aParams>
            </action>
        </reference>
    </customer_logged_in>
</layout>

Conditional Links

Lets assume we want certain Links only to be displayed when our customer belongs to a certain customer group like, say, “Wholesale”… or really, any condition which isn’t being covered by an already existing handle.

Some days ago I came across a stackexchange thread where the accepted reply suggested to manage this by letting a top.link’s custom child block inject links to its parent…  While this approach actually works, it’s not how it’s supposed to be done. I’d want a handle (like its name suggests) handling the layout – meaning: I want to create <customer_group_[group_name]> handles.

So what we do is letting an Observer add our custom handlers. Observers listen to a certain event that they’re attached to. So, we want our Observer to listen to an event that’s for one dispatched globally and, for two, linked to the layout (I found this overview to be helpful). The event we want to observe is called controller_action_layout_load_before.

While we didn’t necessarily need to create a module for our previous layout updates, we need one now in order to set up our Observer. I created a module which is handling all things concerning updates on our shop’s navigation layout.

1. Creating a module

(just skip this if you already know how to create modules)

A. Architecture

Our module needs following files:

  • app/etc/modules/Montareno_Navigation.xml
  • app/local/Montareno/Navigation/etc/config.xml
  • app/local/Montareno/Navigation/Model/Observer.xml
  • app/design/frontend/montareno/default/layout/montareno_navigation.xml

Montareno = a custom name for your own pack of modules. Make sure this name isn’t already taken by community or core modules.
Navigation = your module’s name
montareno = your theme’s package name
default = your theme’s name

Always pay attention to Magento’s case sensitivity. Uppercase the first letter of your pack and module, but only the first – I haven’t tried it myself but some folks reported problems with uppercase letters following the first letter. The templates’s xml filename is the only exception as its name will be set in the config.xml and can have any name. However, it’s good habit to always name it by its pack’s and module’s name, so you will always know where it belongs to.

B. Registration & Configuration

Any layout updates concerning the Navigation can now move into the module’s own layout file. However, at this point Magento will ignore this and any other file belonging to our module. First, we want to make Magento be aware of our module and tell what our module offers (Magento is kind of a diva  😉 ).

The first file on our architecture’s list is a “registry file” (well, that’s what I call it) – it tells Magento “Hey, look, here’s another module, you’ll find it at Montareno/Navigation in the ‘local’ code directory”:

<?xml version="1.0" encoding="utf-8"?>
<config>
    <modules>
        <Montareno_Navigation>
            <active>true</active>
            <codePool>local</codePool>
            <depends>
                <Mage_Checkout />
                <Mage_Customer />
                <Mage_Wishlist />
            </depends>
        </Montareno_Navigation>
    </modules>
</config>

Note: You can remove the depends-Tag. However, in case you’re planning to extent your Navigation module, I find them to be a good start.

If Magento can locate the module we advertised, the first thing it does is looking for the modules configuration files located within etc. In our case it’ll only be one configuration file: config.xml. This file tells Magento where else to look, so obviously, this is the place where you tell Magento that your module comes with its own layout file – you write following lines into your config.xml:

<?xml version="1.0"?>
<config>
    <modules>
        <Montareno_Navigation>
            <version>0.1.0</version>
        </Montareno_Navigation>
    </modules>
    <frontend>
        <layout>
            <updates>
                <Montareno_Navigation>
                    <file>montareno_navigation.xml</file>
                </Montareno_Navigation>
            </updates>
        </layout>
    </frontend>
</config>

Remember to adjust “Montareno_Navigation” to the pack & module name you’ve chosen (see above). Remember: case sensitivity!

2. Creating the observer

Now its time to open up the Observer.php and write our class Montareno_Navigation_Model_Observer:

class Montareno_Navigation_Model_Observer
{
/**
     * Adds layout handles customer_group_<group-name> 
     *
     * Event: controller_action_layout_load_before
     *
     * @param Varien_Event_Observer $observer
     */
    public function addCustomerGroupHandle(Varien_Event_Observer $observer){

        $customer = Mage::getSingleton('customer/session');
        if ($customer->isLoggedIn()) {
         
            $groupId = $customer->getCustomerGroupId();
            $groupName = preg_replace('#[^a-z0-9_]#', '', 
                str_replace(' ', '_',
                    strtolower(
                    Mage::getModel('customer/group')->load($groupId)->getCustomerGroupCode() 
                    ) 
                ) 
            );
            
            /* @var $update Mage_Core_Model_Layout_Update */
            $update = $observer->getEvent()->getLayout()->getUpdate();
            $update->addHandle('customer_group_' . $groupName);
            
        }
        
        return $this;
    }
}

I don’t think I have to explain what’s happening here and I also think you know what we have to do next: attach our Oberserver class to the event controller_action_layout_load_before

3. Attaching our observer to the event

Of course we do this in our module’s config.xml, too – simply add following lines after the closing modules tag:

   <!-- ... </modules> -->
    <global>
        <events>
            <controller_action_layout_load_before>
                <observers>
                    <customer_group_handle>
                        <class>Montareno_Navigation_Model_Observer</class>
                        <method>addCustomerGroupHandle</method>
                    </customer_group_handle>
                </observers>
            </controller_action_layout_load_before>
        </events>
    </global>
    <!-- <frontend> ... -->

These lines of xml are pretty straight forward. I chose to name my oberserver <customer_group_handle> but you can name it whatever you like – however, once again, I suggest you choose one that makes sense and, most importantly, isn’t already taken by other modules.

4. Yippie yay!

Yes, that’s it! Just clear the cache and there it is, our custom Navigation~