Solving Magento

Solutions for Magento E-Commerce Platform

by Oleg Ishenko

Magento Grouped Product Type Tutorial

This post is a follow up to the discussion on the Grouped product type in Magento. In this tutorial I will develop a simple Magento module, which will change some front-end display features of grouped products while providing an insight into certain functionality aspects of the Grouped product type. The examples I am going to use are based on the sample Magento store data, which can be downloaded here.

The sample extension will perform the following tasks:

  • Customers must not be able to enter custom quantity for grouped set items into the add-to-cart form. The quantity to be added to cart will be the one provided in the back-end grouped product management as default_quantity.
  • If a grouped item has no or zero default_quantity it will be displayed as “out of stock”.
  • Customers can choose which items to add to the cart by selecting check-boxes next to the respective items in the add-to-cart form.

To implement these features we will have to override a block class responsible for the display of the grouped product options as well as extend the Grouped product type functionality to use default_quantity when adding products to cart.


As always, creating an extension means adding standard XML configuration files and creating a directory structure. The extension will be named GroupedQuantity and will be placed into the local pool under the Solvingmagento package. The module declaration file is this:

<config>
        <modules>
                <Solvingmagento_GroupedQuantity>
                        <active>true</active>
                        <codePool>local</codePool>
                </Solvingmagento_GroupedQuantity>
        </modules>
</config>

Listing 1. Extension declaration. /app/etc/modules/Solvingmagento_GroupedQuantity.xml

The directory structure we need is the following one:

/app
        code
                local
                        Solvingmagento
                                GroupedQuantity
                                        Block
                                                Product
                                                        View
                                                                Type
                                        Model
                                                Product
                                                        Type
                                        etc
Figure 1. Directory structure of the GroupedQuantity module.

To change the default appearance of grouped products we will have to customize the template file, which the system uses to output the grouped product options. The template file name and the block class reference can be found in the catalog layout XML file. Unless you are using a customized Magento template package, the layout file the system uses is the one in the base package’s default theme. The node containing the grouped product display configuration is PRODUCT_TYPE_grouped:

<PRODUCT_TYPE_grouped translate="label" module="catalog">
    <label>Catalog Product View (Grouped)</label>
    <reference name="product.info">
        <block type="catalog/product_view_type_grouped"
                            name="product.info.grouped" as="product_type_data"
                            template="catalog/product/view/type/grouped.phtml">
            <block type="core/text_list" name="product.info.grouped.extra"
                                    as="product_type_data_extra" translate="label">
                <label>Product Extra Info</label>
            </block>
        </block>
    </reference>
</PRODUCT_TYPE_grouped>

Listing 2. Grouped product type layout configuration, /app/design/frontend/base/default/layout/catalog.xml, line 288

The default template file catalog/product/view/type/grouped.phtml generates the following form in the grouped product details page:

default_grouped_look

Figure 2. Default look of a grouped product.

A customer must enter quantity for at least one item in order to add the grouped product to cart. We are going to change this by removing the text input fields and using check-boxes for item selection. To do that let’s create a template file under app/design/frontend/base/default/template/catalog/product/view/type/solvingmagento_grouped.phtml and copy the contents of the grouped.phtml template into it. The table that contains the grouped items is represented by the following code:

<table class="data-table grouped-items-table" id="super-product-table">
        <col />
        <col />
        <col width="1" />
        <thead>
                <tr>
                        <th><?php echo $this->__('Product Name') ?></th>
                        <?php if ($this->getCanShowProductPrice($_product)): ?>
                        <th class="a-right"><?php echo $this->__('Price') ?></th>
                        <?php endif; ?>
                        <?php if ($_product->isSaleable()): ?>
                        <th class="a-center"><?php echo $this->__('Qty') ?></th>
                        <?php endif; ?>
                </tr>
        </thead>
        <tbody>
        <?php if ($_hasAssociatedProducts): ?>
        <?php foreach ($_associatedProducts as $_item): ?>
                <?php $_finalPriceInclTax = $this->helper('tax')
                        ->getPrice($_item, $_item->getFinalPrice(), true) ?>
                <tr>
                        <td><?php echo $this->htmlEscape($_item->getName()) ?></td>
                        <?php if ($this->getCanShowProductPrice($_product)): ?>
                        <td class="a-right">
                                <?php if ($this->getCanShowProductPrice($_item)): ?>
                                <?php echo $this->getPriceHtml($_item, true) ?>
                                <?php echo $this->getTierPriceHtml($_item) ?>
                                <?php endif; ?>
                        </td>
                        <?php endif; ?>
                        <?php if ($_product->isSaleable()): ?>
                        <td class="a-center">
                        <?php if ($_item->isSaleable()) : ?>
                                <input type="text" name="super_group&#91;<?php echo $_item->getId() ?>]"
                                        maxlength="12" value="<?php echo $_item->getQty()*1 ?>"
                                        title="<?php echo $this->__('Qty') ?>" class="input-text qty" />
                        <?php else: ?>
                                <p class="availability out-of-stock">
                                        <span><?php echo $this->__('Out of stock') ?></span>
                                </p>
                        <?php endif; ?>
                        </td>
                        <?php endif; ?>
                </tr>
        <?php endforeach; ?>
        <?php else: ?>
           <tr>
                   <td colspan="<?php if ($_product->isSaleable()): ?>4<?php
                   else : ?>3<?php endif; ?>"><?php
                   echo $this->__('No options of this product are available.') ?></td>
           </tr>
        <?php endif; ?>
        </tbody>
</table>

Listing 3. Grouped options display as defined in the default template, /app/design/frontend/base/default/template/catalog/product/view/type/grouped.phtml, line 44.

I will add one more column (“Select”) to this table, which will contain check-boxes. The “Qty” column will display the default quantity value. If an item can’t be sold or has no value for default quantity, these cells will be merged and display a message “Out of stock”. This is how the table header is defined in my custom template file, solvingmagento_grouped.phtml:

        <thead>
                <tr>
                        <th><?php echo $this->__('Product Name') ?></th>
                        <?php if ($this->getCanShowProductPrice($_product)): ?>
                        <th class="a-right"><?php echo $this->__('Price') ?></th>
                        <?php endif; ?>
                        <?php if ($_product->isSaleable()): ?>
                        <th class="a-center"><?php echo $this->__('Qty') ?></th>
                        <th class="a-center"><?php echo $this->__('Select') ?></th>
                        <?php endif; ?>
                </tr>
        </thead>

Listing 4. Customized table header.

The loop that outputs “Qty” and “Select” cells for each item will look like this:

<?php foreach ($_associatedProducts as $_item): ?>
    <?php
    /** code omitted for brevity **/
        <?php if ($_product->isSaleable()): ?>
        <td class="a-center" <?php
        if ((!$_item->isSaleable()) || (!(float)$_item->getQty() > 0)) {
            echo ' colspan="2"';}?>>
        <?php
        if (($_item->isSaleable()) && ($_item->getQty() > 0)): ?>
            <?php
            echo $this->getItemQuantity($_item);
            ?>
            </td>
        <td class="a-center">
            <input type="checkbox" name="super_group_selection&#91;<?php echo $_item->getId() ?>]">
        </td>
        <?php else: ?>
            <p class="availability out-of-stock"><span><?php echo $this->__('Out of stock') ?></span></p>
        </td>
        <?php endif; ?>

        <?php endif; ?>
    </tr>
<?php endforeach; ?>

Listing 5. “Qty” and “Select” columns output.

The call to $_item->getQty() returns a default_quantity property value, which was set for the item in the back-end. If an item is not saleable or has no default quantity, the two cells are merged into one. Otherwise the “Qty” column gets the result of a call to $this->getItemQuantity($_item). Why not simply use the $_item->getQty() to display quantity? The problem is that some products can have decimal quantities and decimal separator can be different in different locales. For ‘en_US’ it is a period, while for ‘de_DE’ it is a comma. The method getItemQuantity is not a part of the standard grouped type block, that is why our module has to implement a rewrite of Mage_Catalog_Block_Product_View_Type_Grouped and implement it in its own block class.

The check-boxes are grouped under form input name super_group_selection, while the input group super_group of the original template is omitted. The super_group parameter, however, is vital for the processing the add-to-cart request and we will have to recreate is programmatically. For that our module will extend the Mage_Catalog_Model_Product_Type_Grouped class.

But before adding the model and block rewrites let’s create a layout update so that our custom template can be used in place of the original one. We must create a config.xml file in the module’s etc folder and add the following lines to it:

<config>
        <modules>
                <Solvingmagento_GroupedQuantity>
                        <version>0.1.0</version>
                </Solvingmagento_GroupedQuantity>
        </modules>
        <frontend>
                <layout>
                        <updates>
                                <solvingmagento_groupedquantity>
                                        <file>solvingmagento_groupedquantity.xml</file>
                                </solvingmagento_groupedquantity>
                        </updates>
                </layout>
        </frontend>
</config>

Listing 6. Declaring a layout update.

The layout update file is placed under /app/design/frontend/base/default/layout/ and must have the following content:

<layout version="0.1.0">
        <PRODUCT_TYPE_grouped>
                <reference name="product.info.grouped">
                        <action method="setTemplate">
                                <template>catalog/product/view/type/solvingmagento_grouped.phtml</template>
                        </action>
                </reference>
        </PRODUCT_TYPE_grouped>
</layout>

Listing 7. Layout update assigns a new template file to the grouped product type block.

With this update we reload the layout of the Grouped product type. By referring to the Grouped block by its declared name “product.info.grouped” we are able to call its setTemplate method, which accepts the file path specified in the <template> node as a parameter. Thus the block gets our custom template.

Next we have to rewrite the block class itself and add one more method to it. As usual, block class rewrite is defined in the module’s config.xml file:

<config>
        ...
        <global>
                <blocks>
                        <catalog>
                                <rewrite>
                                        <product_view_type_grouped>
                                                Solvingmagento_GroupedQuantity_Block_Product_View_Type_Grouped
                                        </product_view_type_grouped>
                                </rewrite>
                        </catalog>
                </blocks>
        </global>
        ...
</config>

Listing 8. Declaring a block class rewrite.

We have already set up a directory path for our block override class, now just add the Grouped.php file to the Block/Product/View/Type folder. The class will contain only one custom method, the rest is inherited:

class Solvingmagento_GroupedQuantity_Block_Product_View_Type_Grouped
        extends Mage_Catalog_Block_Product_View_Type_Grouped
{

        /**
         * Returns default quantity of a grouped item
         *
         * @param Mage_Catalog_Model_Product $item grouped item object
         *
         * @return string
         */
        public function getItemQuantity(Mage_Catalog_Model_Product $item)
        {
                $qty = ceil($item->getQty());
                if (($item->getTypeInstance(true, $item)->canUseQtyDecimals())
                        && ($item->getQty() != ceil($item->getQty()))
                ) {
                   $qty = Zend_Locale_Format::toNumber(
                                $item->getQty(),
                                array('locale' => Mage::app()->getLocale()->getLocale())
                   );
                }

                return (string) $qty;
        }
}

Listing 9. The Grouped type block class override.

The method getItemQuantity accepts an object of type Mage_Catalog_Model_Product. This object is an item of a grouped product and as such has a property qty, which is the default_quantity property I’ve been talking about. In this method we check, if the product’s type can use decimal values for quantity and also if the default_quantity value has a non-zero decimal part. If both conditions are true, we use the Zend_Locale_Format::toNumber method, which returns a decimal value with a decimal separator defined for the current shop’s locale. You can experiment later by setting decimal values to product items’ default quantities and changing shop locales via the back-end configuration.

Load a grouped product page and it should look like this:

customized_grouped_look

Figure 3. The new look of a grouped product.

Note that “Chair” is out of stock – I did not set a default quantity for this item. The “Couch” quantity is “1,50”, which is puzzling in real-life. For demonstration purposes I’ve taken a liberty of setting “1.5” to this item’s default quantity. I’ve also set ‘de_DE’ to the shop’s locale so that the decimal separator is displayed as comma, which it is in Germany. This is how this example’s associated products look like in the back-end:

grouped_products_back_end

Figure 4. The grouped product’s items in the back-end.

This concludes the front-end part of the tutorial. At the moment it is impossible to add a grouped product to cart. Every time you do it an error message appears: “Please specify the quantity of product(s).”. The reason is that the new template’s form does not have the super_group parameter, which is used to recognize which items and in which quantity are added. Instead the form posts the super_group_selection containing the selected products’ IDs only. So the next step is to modify the _prepareProduct method of the Grouped product type class to let the system use the default_quantity values.

Add the following lines to the config.xml file to declare a class rewrite for the Mage_Catalog_Model_Product_Type_Grouped class:

<config>
        ...
        <global>
                ...
                <models>
                        <catalog>
                                <rewrite>
                                        <product_type_grouped>
                                                Solvingmagento_GroupedQuantity_Model_Product_Type_Grouped
                                        </product_type_grouped>
                                </rewrite>
                        </catalog>
                </models>
        </global>
        ...
</config>

Listing 10. Rewriting the Grouped product type class.

The class Solvingmagento_GroupedQuantity_Model_Product_Type_Grouped will override the _prepareProduct method, which is called before the data posted by the add-to-cart form is converted to quote items. The modification required is just one line:

class Solvingmagento_GroupedQuantity_Model_Product_Type_Grouped
        extends Mage_Catalog_Model_Product_Type_Grouped
{
        protected function _prepareProduct(Varien_Object $buyRequest, $product, $processMode)
        {
                $product = $this->getProduct($product);
                $this->setSuperGroup($buyRequest, $product);
                $productsInfo = $buyRequest->getSuperGroup();
                /** code omitted for brevity **/
        }
}

Listing 11. Overriding the _prepareProduct method.

The line $this->setSuperGroup($buyRequest, $product); sets the property super_group to the $buyRequest variable. This variable represents the data posted by add-to-cart form. In the default implementation this property would’ve already been here, but since our custom template’s form does not have such input field, we must set it dynamically. Let’s add the method setSuperGroup to our custom block class:

        protected function setSuperGroup(Varien_Object $buyRequest, $product)
        {
                $selection = array_keys($buyRequest->getSuperGroupSelection());
                $associatedProducts = $this->getAssociatedProductCollection($product)
                        ->load();
                $superGroup = array();
                if (is_array($selection)) {
                        foreach($selection as $selected) {
                                $item = $associatedProducts->getItemById($selected);
                                if ($item && ($item->getQty() > 0)) {
                                        $superGroup[$selected] = $item->getQty();
                                }
                        }
                        $buyRequest->setSuperGroup($superGroup);
                }
        }

Listing 12. Dynamically setting the super_group parameter to the $buyRequest variable.

In the first line the $selection variable gets the data passed from the selected items’ check-boxes. It is an array of product IDs. In the next line we fetch a collection of products associated to the current grouped product $product. For every selected item we check if such product is available among the associated items and fetch this item’s default_quantity by calling $item->getQty(). This way we build an array of quantities whose keys are product IDs. This array, $superGroup, is then set to the $buyRequest object. That is it – $buyRequest is passed to this method by reference and the changes we’ve made to it are available further in the _prepareProduct method. The system now knows which quantities of which products must be added to the quote (shopping cart).

Conclusion

This tutorial’s purpose was to try in practice certain parts of functionality that is specific to the Grouped product type of Magento. We’ve tackled the front-end display of grouped product options by creating a customized template and extending the block class responsible for the Grouped type output. We’ve also looked into the _prepareProduct method of the Grouped type class and extended it to be compatible with the new form data posted by the customized template. The finished version of this tutorial’s extension is available here: Solvingmagento_GroupedQuantity.

5 thoughts on “Magento Grouped Product Type Tutorial

  1. Pingback: Magento Grouped Product Type | Solving Magento

  2. Hi,

    How i can add to cart to each individul product in group product, so that if we click on the add to cart button in product’s row, only that product must go to the shopping cart, thanks….

  3. hi,

    great post. but i need ” How to Add Aditional columns to a grouped products in magento other than product name, price, qty.”

    please tell me what i have to do. i am not expert in php.
    please tell in steps. i am really struggling a lot.

    thanks in advance.

  4. Thanks for this post, going to implement now. We’re on Mage 1.8 but I just cant figure out how to show the grouped product price. On the product page – Any ideas?

    Thanks
    Lee

  5. Hello, I was wondering if it was possible to have grouped options appear when you have a required option? There must be away of achieving this..

Leave a Reply

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

Theme: Esquire by Matthew Buchanan.