More Flexible Approach to Listing Sale Products in Magento

onsale_featured

Some clients have a need to show product on sale on homepage, or, sometimes, to filter products on sale in each category individually. In this post i’ll show you two ways of listing products on sale:

  • On CMS page as a list
  • By using a filter in category view

Let’s get to it!

Static CMS page

This is just a quick way of showing products on sale on one page, using admin and uploading one phtml file. Going down this path, you won’t be able to use pagination nor layered navigation. In case you would like those features, I’d recommend following the second part (Filter in Category) of this post.

Create a new CMS page with content set to

{{block type="catalog/product_list" template="inchoo/onsale/sale.phtml"}}

and a template for listing your products.

app/design/frontend/default/YOUR_THEME/template/inchoo/onsale/sale.phtml

<?php
$_productCollection = Mage::getModel('catalog/product')->getCollection();
$_productCollection->addAttributeToSelect(array(
                                   'image',
                                   'name',
                                   'short_description'
                   ))
                   ->addFieldToFilter('visibility', array(
                               Mage_Catalog_Model_Product_Visibility::VISIBILITY_BOTH,
                               Mage_Catalog_Model_Product_Visibility::VISIBILITY_IN_CATALOG
                   )) //showing just products visible in catalog or both search and catalog
                   ->addFinalPrice()
//                        ->addAttributeToSort('price', 'asc') //in case we would like to sort products by price
                   ->getSelect()
                   ->where('price_index.final_price < price_index.price')
//                        ->limit(30) //we can specify how many products we want to show on this page
//                        ->order(new Zend_Db_Expr('RAND()')) //in case we would like to sort products randomly
                   ;
Mage::getModel('review/review')->appendSummary($_productCollection);
$_helper = $this->helper('catalog/output');
?>
//
<?php if(!$_productCollection->count()): ?>
    <p class="note-msg"><?php echo $this->__('There are no products matching the selection.') ?></p>
<?php else: ?>
    <div class="category-products">
        <?php // List mode ?>
            <?php $_iterator = 0; ?>
            <ol class="products-list" id="products-list">
                <?php foreach ($_productCollection as $_product): ?>
                    <li class="item<?php if( ++$_iterator == sizeof($_productCollection) ): ?> last<?php endif; ?>">
                        <?php // Product Image ?>
                        <a href="<?php echo $_product->getProductUrl() ?>" title="<?php echo $this->stripTags($this->getImageLabel($_product, 'small_image'), null, true) ?>" class="product-image"><img src="<?php echo $_product->getImageUrl(); ?>" width="135" height="135" alt="<?php echo $this->stripTags($this->getImageLabel($_product, 'small_image'), null, true) ?>" /></a>
                        <?php // Product description ?>
                        <div class="product-shop">
                            <div class="f-fix">
                                <?php $_productNameStripped = $this->stripTags($_product->getName(), null, true); ?>
                                <h2 class="product-name"><a href="<?php echo $_product->getProductUrl() ?>" title="<?php echo $_productNameStripped; ?>"><?php echo $_helper->productAttribute($_product, $_product->getName() , 'name'); ?></a></h2>
                                <?php if($_product->getRatingSummary()): ?>
                                    <?php echo $this->getReviewsSummaryHtml($_product) ?>
                                <?php endif; ?>
                                <?php echo $this->getPriceHtml($_product, true) ?>
                                <?php if($_product->isSaleable()): ?>
                                    <p><button type="button" title="<?php echo $this->__('Add to Cart') ?>" class="button btn-cart" onclick="setLocation('<?php echo $this->getAddToCartUrl($_product) ?>')"><span><span><?php echo $this->__('Add to Cart') ?></span></span></button></p>
                                <?php else: ?>
                                    <p class="availability out-of-stock"><span><?php echo $this->__('Out of stock') ?></span></p>
                                <?php endif; ?>
                                <div class="desc std">
                                    <?php echo $_helper->productAttribute($_product, $_product->getShortDescription(), 'short_description') ?>
                                    <a href="<?php echo $_product->getProductUrl() ?>" title="<?php echo $_productNameStripped ?>" class="link-learn"><?php echo $this->__('Learn More') ?></a>
                                </div>
                                <ul class="add-to-links" style="margin:0; padding-left:0; list-style: none;">
                                    <?php if ($this->helper('wishlist')->isAllow()) : ?>
                                        <li><a href="<?php echo $this->helper('wishlist')->getAddUrl($_product) ?>" class="link-wishlist"><?php echo $this->__('Add to Wishlist') ?></a></li>
                                    <?php endif; ?>
                                    <?php if($_compareUrl=$this->getAddToCompareUrl($_product)): ?>
                                        <li><span class="separator">|</span> <a href="<?php echo $_compareUrl ?>" class="link-compare"><?php echo $this->__('Add to Compare') ?></a></li>
                                    <?php endif; ?>
                                </ul>
                            </div>
                        </div>
                    </li>
                <?php endforeach; ?>
            </ol>
            <script type="text/javascript">decorateList('products-list', 'none-recursive')</script>
    </div>
<?php endif; ?>

That’s it! Try and visit your CMS page. You’ll see that the products with special price are listed in a ‘list view’, something like this:

onsale1

With small modifications, you could display products in ‘grid view’ as well.

Filter in Category View

Magento applies filtering and loads collection to layered navigation before that same collection gets loaded in the template. That’s why we need to overwrite _getProductCollection method, so that we can apply custom filtering before the collection gets loaded. Let’s do that by extending Mage_Catalog_Product_List class and rendering our block instead of the default one on category view.

Let’s register out little module. For our purposes, let’s name it ‘Inchoo/Onsale’.

app/etc/modules/Inchoo_Onsale.xml

<?xml version="1.0"?>
<config>
    <modules>
        <Inchoo_Onsale>
            <active>true</active>
            <codePool>local</codePool>
        </Inchoo_Onsale>
    </modules>
</config>

Let’s also configure our module. We’ll use a block, helper, and modify the layout of our template a little bit.

app/code/local/Inchoo/Onsale/etc/config.xml

<?xml version="1.0"?>
<config>
    <modules>
        <Inchoo_Onsale>
            <version>1.0.0.3</version>
        </Inchoo_Onsale>
    </modules>
    <global>
        <blocks>
            <inchoo_onsale>
                <class>Inchoo_Onsale_Block</class>
            </inchoo_onsale>
        </blocks>
        <helpers>
            <inchoo_onsale>
                <class>Inchoo_Onsale_Helper</class>
            </inchoo_onsale>
        </helpers>
    </global>
    <frontend>
        <layout>
            <updates>
                <onsale>
                    <file>inchoo/onsale.xml</file>
                </onsale>
            </updates>
        </layout>
    </frontend>
</config>

Now, register a helper that will have two methods: getOnSaleUrl and getNotOnSaleUrl. They’ll be used by our checkbox, to fetch URL that will be loaded upon click. We’ll also add a new variable to URL – ‘sale‘. When sale equals 1, we’ll show products on sale, if it is not set, we’ll show all products in category.

app/code/local/Inchoo/Onsale/Helper/Data.php

<?php
class Inchoo_Onsale_Helper_Data extends Mage_Core_Helper_Abstract
{
    public function getOnSaleUrl()
    {
        $url = Mage::getUrl('', array(
            '_current' => true,
            '_use_rewrite' => true,
            '_query' => array(
                'sale' => 1,
                'p' => NULL
            )
        ));
        return $url;
    }
    public function getNotOnSaleUrl()
    {
        $url = Mage::getUrl('', array(
            '_current' => true,
            '_use_rewrite' => true,
            '_query' => array(
                'sale' => NULL,
                'p' => NULL
            )
        ));
        return $url;
    }
}

Now, lets create our block. We’ll extend our class from Mage_Catalog_Block_Product_List, and write our own _getProductCollection method.

app/code/local/Inchoo/Onsale/Block/Catalog/Product/List.php

<?php
class Inchoo_Onsale_Block_Catalog_Product_List extends Mage_Catalog_Block_Product_List
{
    protected function _getProductCollection()
    {
        if (is_null($this->_productCollection)) {
            $layer = $this->getLayer();
            /* @var $layer Mage_Catalog_Model_Layer */
            if ($this->getShowRootCategory()) {
                $this->setCategoryId(Mage::app()->getStore()->getRootCategoryId());
            }
            // if this is a product view page
            if (Mage::registry('product')) {
                // get collection of categories this product is associated with
                $categories = Mage::registry('product')->getCategoryCollection()
                                  ->setPage(1, 1)
                                  ->load();
                // if the product is associated with any category
                if ($categories->count()) {
                    // show products from this category
                    $this->setCategoryId(current($categories->getIterator()));
                }
            }
            $origCategory = null;
            if ($this->getCategoryId()) {
                $category = Mage::getModel('catalog/category')->load($this->getCategoryId());
                if ($category->getId()) {
                    $origCategory = $layer->getCurrentCategory();
                    $layer->setCurrentCategory($category);
                    $this->addModelTags($category);
                }
            }
            $this->_productCollection = $layer->getProductCollection();
//                inchoo start
            $param = Mage::app()->getRequest()->getParam('sale');
            if(isset($param) && $param==='1'){
                $this->_productCollection
                    ->addFinalPrice()
                    ->getSelect()
                    ->where('price_index.final_price < price_index.price');
            }
//                inchoo end
            $this->prepareSortableFieldsByCategory($layer->getCurrentCategory());
            if ($origCategory) {
                $layer->setCurrentCategory($origCategory);
            }
        }
        return $this->_productCollection;
//            return parent::_getProductCollection();
    }
}

We’ll also add a checkbox to our toolbar, where our customer will be able to filter products that are on sale. Open your toolbar.phtml file.

app/design/frontend/default/YOUR_THEME/template/inchoo/onsale/toolbar.phtml

<!--inchoo start-->
<?php $helper = Mage::helper('inchoo_onsale'); ?>
<!--inchoo end-->
<?php if($this->getCollection()->getSize()): ?>
    <div class="toolbar">
        <div class="pager">
            <p class="amount">
                <?php if($this->getLastPageNum()>1): ?>
                    <?php echo $this->__('Items %s to %s of %s total', $this->getFirstNum(), $this->getLastNum(), $this->getTotalNum()) ?>
                <?php else: ?>
                    <strong><?php echo $this->__('%s Item(s)', $this->getTotalNum()) ?></strong>
                <?php endif; ?>
            </p>
            <div class="limiter">
                <label><?php echo $this->__('Show') ?></label>
                <select onchange="setLocation(this.value)">
                    <?php foreach ($this->getAvailableLimit() as  $_key=>$_limit): ?>
                        <option value="<?php echo $this->getLimitUrl($_key) ?>"<?php if($this->isLimitCurrent($_key)): ?> selected="selected"<?php endif ?>>
                            <?php echo $_limit ?>
                        </option>
                    <?php endforeach; ?>
                </select> <?php echo $this->__('per page') ?>
            </div>
            <?php echo $this->getPagerHtml() ?>
        </div>
        <?php if( $this->isExpanded() ): ?>
            <div class="sorter">
                <?php if( $this->isEnabledViewSwitcher() ): ?>
                    <p class="view-mode">
                        <?php $_modes = $this->getModes(); ?>
                        <?php if($_modes && count($_modes)>1): ?>
                            <label><?php echo $this->__('View as') ?>:</label>
                            <?php foreach ($this->getModes() as $_code=>$_label): ?>
                                <?php if($this->isModeActive($_code)): ?>
                                    <strong title="<?php echo $_label ?>" class="<?php echo strtolower($_code); ?>"><?php echo $_label ?></strong>&nbsp;
                                <?php else: ?>
                                    <a href="<?php echo $this->getModeUrl($_code) ?>" title="<?php echo $_label ?>" class="<?php echo strtolower($_code); ?>"><?php echo $_label ?></a>&nbsp;
                                <?php endif; ?>
                            <?php endforeach; ?>
                        <?php endif; ?>
                    </p>
                <?php endif; ?>
                <div class="sort-by">
                    <label><?php echo $this->__('Sort By') ?></label>
                    <select onchange="setLocation(this.value)">
                        <?php foreach($this->getAvailableOrders() as $_key=>$_order): ?>
                            <option value="<?php echo $this->getOrderUrl($_key, 'asc') ?>"<?php if($this->isOrderCurrent($_key)): ?> selected="selected"<?php endif; ?>>
                                <?php echo $this->__($_order) ?>
                            </option>
                        <?php endforeach; ?>
                    </select>
                    <?php if($this->getCurrentDirection() == 'desc'): ?>
                        <a href="<?php echo $this->getOrderUrl(null, 'asc') ?>" title="<?php echo $this->__('Set Ascending Direction') ?>"><img src="<?php echo $this->getSkinUrl('images/i_desc_arrow.gif') ?>" alt="<?php echo $this->__('Set Ascending Direction') ?>" class="v-middle" /></a>
                    <?php else: ?>
                        <a href="<?php echo $this->getOrderUrl(null, 'desc') ?>" title="<?php echo $this->__('Set Descending Direction') ?>"><img src="<?php echo $this->getSkinUrl('images/i_asc_arrow.gif') ?>" alt="<?php echo $this->__('Set Descending Direction') ?>" class="v-middle" /></a>
                <!--inchoo start-->
                        <label for="sale"><?php echo $this->__('On Sale') ?></label>
                        <input name="sale" id="sale" type="checkbox"
                            value="<?php
                                $currentUrl = Mage::helper('core/url')->getCurrentUrl();
                                if($currentUrl===$helper->getOnSaleUrl()):
                                    echo $helper->getNotOnSaleUrl();
                                    echo "\"checked=\"checked";
                                else:
                                    echo $helper->getOnSaleUrl();
                                endif;
                            ?>" autocomplete="off" onchange="setLocation(this.value)"/>
                <!--inchoo end-->
                    <?php endif; ?>
                </div>
            </div>
        <?php endif; ?>
    </div>
<?php endif ?>

And, lastly, create our layout file to make Magento use our toolbar.phtml template instead of the default one.

app/design/frontend/default/YOUR_THEME/layout/inchoo/onsale.xml

<?xml version="1.0"?>
<layout version="0.1.0">
    <catalog_category_default>
        <reference name="category.products">
                <block type="inchoo_onsale/catalog_product_list" name="product_list" template="catalog/product/list.phtml">
                    <block type="catalog/product_list_toolbar" name="product_list_toolbar" template="inchoo/onsale/toolbar.phtml">
                        <block type="page/html_pager" name="product_list_toolbar_pager"/>
                        <!-- The following code shows how to set your own pager increments -->
                        <!--
                            <action method="setDefaultListPerPage"><limit>4</limit></action>
                            <action method="setDefaultGridPerPage"><limit>9</limit></action>
                            <action method="addPagerLimit"><mode>list</mode><limit>2</limit></action>
                            <action method="addPagerLimit"><mode>list</mode><limit>4</limit></action>
                            <action method="addPagerLimit"><mode>list</mode><limit>6</limit></action>
                            <action method="addPagerLimit"><mode>list</mode><limit>8</limit></action>
                            <action method="addPagerLimit" translate="label"><mode>list</mode><limit>all</limit><label>All</label></action>
                        -->
                    </block>
                    <action method="addColumnCountLayoutDepend"><layout>empty</layout><count>6</count></action>
                    <action method="addColumnCountLayoutDepend"><layout>one_column</layout><count>5</count></action>
                    <action method="addColumnCountLayoutDepend"><layout>two_columns_left</layout><count>4</count></action>
                    <action method="addColumnCountLayoutDepend"><layout>two_columns_right</layout><count>4</count></action>
                    <action method="addColumnCountLayoutDepend"><layout>three_columns</layout><count>3</count></action>
                    <action method="setToolbarBlockName"><name>product_list_toolbar</name></action>
                </block>
        </reference>
    </catalog_category_default>
    <catalog_category_layered>
        <reference name="category.products">
            <block type="inchoo_onsale/catalog_product_list" name="product_list" template="catalog/product/list.phtml">
                <block type="catalog/product_list_toolbar" name="product_list_toolbar" template="inchoo/onsale/toolbar.phtml">
                    <block type="page/html_pager" name="product_list_toolbar_pager"/>
                    <!-- The following code shows how to set your own pager increments -->
                    <!--
                        <action method="setDefaultListPerPage"><limit>4</limit></action>
                        <action method="setDefaultGridPerPage"><limit>9</limit></action>
                        <action method="addPagerLimit"><mode>list</mode><limit>2</limit></action>
                        <action method="addPagerLimit"><mode>list</mode><limit>4</limit></action>
                        <action method="addPagerLimit"><mode>list</mode><limit>6</limit></action>
                        <action method="addPagerLimit"><mode>list</mode><limit>8</limit></action>
                        <action method="addPagerLimit" translate="label"><mode>list</mode><limit>all</limit><label>All</label></action>
                    -->
                </block>
                <action method="addColumnCountLayoutDepend"><layout>empty</layout><count>6</count></action>
                <action method="addColumnCountLayoutDepend"><layout>one_column</layout><count>5</count></action>
                <action method="addColumnCountLayoutDepend"><layout>two_columns_left</layout><count>4</count></action>
                <action method="addColumnCountLayoutDepend"><layout>two_columns_right</layout><count>4</count></action>
                <action method="addColumnCountLayoutDepend"><layout>three_columns</layout><count>3</count></action>
                <action method="setToolbarBlockName"><name>product_list_toolbar</name></action>
            </block>
        </reference>
    </catalog_category_layered>
</layout>

See it in action:
onsale2

Note that layered navigation works, as well as pagination. Just what we wanted.

P.S. You can always view this module on GitHub or download zipped file and install it.

8
Top

Care to rate this post?

Author

Petar Sambolek

Backend Developer

Petar is a Backend Developer who is, alongside programming, also interested in business side of online stores. He’s bribable with sweets.

Other posts from this author

Discussion 8 Comments

Add Comment
  1. hotmonitor

    Hello
    List mode in CMS works
    How to put Grid mode in CMS?

  2. Hi, hotmonitor!
    Thanks for the comment :)

    For listing products in grid mode on a CMS page, place the following code in your app/design/frontend/default/YOUR_THEME/template/inchoo/onsale/sale.phtml
    instead of the one written above.

    <?php
        $_productCollection = Mage::getModel('catalog/product')->getCollection();
        $_productCollection->addAttributeToSelect(array(
                                       'image',
                                       'name',
                                       'short_description'
                                                  ))
                            ->addFieldToFilter('visibility', array(
                                        Mage_Catalog_Model_Product_Visibility::VISIBILITY_BOTH,
                                        Mage_Catalog_Model_Product_Visibility::VISIBILITY_IN_CATALOG
                                )) //showing just products visible in catalog or both search and catalog
                            ->addFinalPrice()
    //                        ->addAttributeToSort('price', 'asc') //in case we would like to sort products by price
                            ->getSelect()
                            ->where('price_index.final_price < price_index.price')
    //                        ->limit(30) //we specify how many products we want to show on this page
    //                        ->order(new Zend_Db_Expr('RAND()')) //in case we would like to sort products randomly
                            ;
        Mage::getModel('review/review')->appendSummary($_productCollection);
        $_helper = $this->helper('catalog/output');
    ?>
    <?php if(!$_productCollection->count()): ?>
        <p class="note-msg"><?php echo $this->__('There are no products matching the selection.') ?></p>
    <?php else: ?>
        <div class="category-products">
            <?php // Grid Mode ?>
            <?php $_collectionSize = $_productCollection->count() ?>
            <?php $_columnCount = $this->getColumnCount(); ?>
            <?php $i=0; foreach ($_productCollection as $_product): ?>
            <?php if ($i++%$_columnCount==0): ?>
                <ul class="products-grid" style="padding:0; list-style: none">
            <?php endif ?>
                <li class="item<?php if(($i-1)%$_columnCount==0): ?> first<?php elseif($i%$_columnCount==0): ?> last<?php endif; ?>">
                    <a href="<?php echo $_product->getProductUrl() ?>" title="<?php echo $this->stripTags($this->getImageLabel($_product, 'small_image'), null, true) ?>" class="product-image"><img src="<?php echo $_product->getImageUrl(); ?>" width="135" height="135" alt="<?php echo $this->stripTags($this->getImageLabel($_product, 'small_image'), null, true) ?>" /></a>
                    <h2 class="product-name"><a href="<?php echo $_product->getProductUrl() ?>" title="<?php echo $this->stripTags($_product->getName(), null, true) ?>"><?php echo $_helper->productAttribute($_product, $_product->getName(), 'name') ?></a></h2>
                    <?php if($_product->getRatingSummary()): ?>
                        <?php echo $this->getReviewsSummaryHtml($_product, 'short') ?>
                    <?php endif; ?>
                    <?php echo $this->getPriceHtml($_product, true) ?>
                    <div class="actions">
                        <?php if($_product->isSaleable()): ?>
                            <button type="button" title="<?php echo $this->__('Add to Cart') ?>" class="button btn-cart" onclick="setLocation('<?php echo $this->getAddToCartUrl($_product) ?>')"><span><span><?php echo $this->__('Add to Cart') ?></span></span></button>
                        <?php else: ?>
                            <p class="availability out-of-stock"><span><?php echo $this->__('Out of stock') ?></span></p>
                        <?php endif; ?>
                        <ul class="add-to-links">
                            <?php if ($this->helper('wishlist')->isAllow()) : ?>
                                <li><a href="<?php echo $this->helper('wishlist')->getAddUrl($_product) ?>" class="link-wishlist"><?php echo $this->__('Add to Wishlist') ?></a></li>
                            <?php endif; ?>
                            <?php if($_compareUrl=$this->getAddToCompareUrl($_product)): ?>
                                <li><span class="separator">|</span> <a href="<?php echo $_compareUrl ?>" class="link-compare"><?php echo $this->__('Add to Compare') ?></a></li>
                            <?php endif; ?>
                        </ul>
                    </div>
                </li>
                <?php if ($i%$_columnCount==0 || $i==$_collectionSize): ?>
                </ul>
            <?php endif ?>
            <?php endforeach ?>
            <script type="text/javascript">decorateGeneric($$('ul.products-grid'), ['odd','even','first','last'])</script>
        </div>
    <?php endif; ?>
    
  3. Jay

    I tried implementing your code and couldn’t get it to work with my custom theme. I have since removed the code, however, now I am still left with the option to filter by “On Sale”. Double checked that all files have been removed. Any tips on how I can remove this so that the option is longer available?

  4. Hi, Jay.

    Which approach of these two have you used?
    Have you cleared your cache when you changed layout xml configuration (onsale.xml)?

  5. vishnu

    Hi this is really nice work

    but i have a query . i show only 4 product on home page. and after that i want to add a link find out more . but i am unable to give this links how can i show all products using this link

  6. Travis

    Oh Man! I just had to solve exactly the same problem earlier this week. Unfortunately I hadn’t found this post til today after I’d finished.

    I was completely unaware of the addFinalPrice() collection method and of the price_index table alias used in the where clause. I solved it by creating an is_on_sale product attribute and then running a nightly ‘indexer’ to populate the attribute for all products. Same effect, but it would have been great to know Magento had the features I needed right there the whole time.

    Thanks for the research.

  7. Hans Kuijpers

    thank you for the great blog! Especially the second solution is great. I used to have sales items on a CMS Page but was missing the Layered Navigation.

    How can I implement the second solution without the toolbar? I want a catalog view with only products with special price AND layered navigation on the side.

  8. Hi, Hans.
    I’m glad to hear this article helped you :)

    As for your question, the simplest solution would be to comment out all code in the toolbar.phtml or add a conditional in toolbar.phtml file.
    However, this would remove toolbar from other category pages as well.

    If you need to do this for only one CMS page (without pagination), the most appropriate method would be following the second part of this article (Filter in Category View), but without doing layout updates (onsale.xml). Then, create a CMS page with the content set to

    {{block type="inchoo_onsale/product_list" template="inchoo/onsale/sale.phtml"}}

    and commenting out lines 36 and 41 in List.php

    But, this is just an idea. Hope this helped you :)

Add Your Comment

Please wrap all source codes with [code][/code] tags.
Top