How to produce PDFs on Magento orders using a grid bulk action

We recently worked with a client who wanted to be able to print a pick list for orders placed in their Magento Store. Magento has a number of features that enable quick, printable versions of order information to be saved, or printed. However, it appeared that all of the options required the order to reach a certain point prior to being able to use them; for example, “Printing Packing Slips” or “Print Shipping Labels”. Both require a shipment to be raised, and as the client wanted to use this as a picking slip, we didn’t want this to have to happen.

Whilst these solutions didn’t offer exactly what we wanted, they did provide a template and an idea of the functionality we required. We wanted PDF downloads and we wanted to be able to select multiple orders at once and not have to click into each separate order to create the PDF.

The only change required to the invoice template was that we weren’t interested about billing information, and we required comments written by the customer whilst going through the checkout process.


So, we faced two challenges. Firstly, we needed to add a mass action to the orders grid. Secondly, we needed to create a multi-page PDF – depending on how many orders had been selected on the orders grid.

As mentioned, Magento already has a series of mass actions that can be applied to orders from the orders grid page.

As with all plugins, we need to start by creating a new folder in app/code/local/

We tend to keep all of our custom extensions together and use the Branded3 namespace, so we start with /app/code/local/Branded3/

Next, we’re going to add the folder for our extension. Let’s call it PrintPicking, and place it in the Branded3 namespace at /app/code/local/Branded3/PrintPicking/

Suffice to say, you can call it whatever you want.

We’ll split things down a little from now on for better understanding…

Adding a new mass action

There are two ways to add a new mass action to the orders grid. The first way is to extend the Sales Order Grid (Mage_Adminhtml_Block_Sales_Order_Grid). We’d do this by doing something like the following: /app/code/local/Branded3/PrintPicking/etc/config.xml

 
                <sales_order_grid>Branded3_PrintPicking_Block_Sales_Order_Grid
class Branded3_PrintPicking_Block_Sales_Order_Grid extends Mage_Adminhtml_Block_Sales_Order_Grid
{
 
protected function _prepareMassaction()
    {
        parent::_prepareMassaction();
 
        // Append new mass action option
        $this->getMassactionBlock()->addItem(
            'mymodule',
            array('label' => $this->__('Print Picking Slips'),
                  'url'   => $this->getUrl('mymodule/controller/action')
            )
        );
    }
}

Extending the Sales Order Grid is quick and easy; it’s also pretty dirty. The reason being – other plugins could already be extending the Sales Order Grid and adding new features. Features you might want to be using. So, you might need to extend their class and this then leads to dependency in your plugin which isn’t really something we want.

As with many programming solutions; there is always more than one way, and this solution will also leave other plugins to take control/add to the Sales Order Grid. We achieve this by using a Magento event instead. Specifically, core_block_abstract_to_html_before.

It’s worth mentioning that it’s always best practice to use event/observers whenever you can because it’s future proof. Whilst a core class such as Mage_Adminhtml_Block_Sales_Order_Grid is probably unlikely to change, the core event won’t, and we can keep adding events on top of events.

So let’s go ahead and look at the code that we need to control this. As suggested above, we’re looking at adding an event hook and pointing to our own observer function. Firstly, the config.xml for our extension – /app/code/local/Branded3/PrintPicking/etc/config.xml

 
<config>    
    <modules>
        <Branded3_Printpicking>
            <version>1.0</version>
        </Branded3_Printpicking>
    </modules>
 
    <admin>
        <routers>
            <printpicking>
                <use>admin</use>
                <args>
                    <module>Branded3_Printpicking</module>
                    <frontName>printpicking</frontName>
                </args>
            </printpicking>
        </routers>
    </admin>
 
    <global>
        <events>
            <!-- Event to attach observer to -->
            <core_block_abstract_to_html_before>
                <observers>
                    <gridexample>
                        <type>singleton</type>
                        <!-- Observer location (e.g. SW/GridExample/Model) -->
                        <class>Branded3_Printpicking_Model_Observer</class>
                        <!-- Function to call -->
                        <method>addActions</method>
                    </gridexample>
                </observers>
            </core_block_abstract_to_html_before>
        </events>
 
        <models>
            <branded3_printpicking>
                <class>Branded3_Printpicking_Model</class>
            </branded3_printpicking>
        </models>
 
    </global>
 
 
 
</config>

As you can see, we tell Magento that we want to bind to an event. Specifically the core_block_abstract_to_html_before event. We enter the observers block, and define a new class – Branded3_Printpicking_Model_Observer, and hook up the method addActions. We also define that the model exists so that we can add an Observer (referenced in the event observer).

This means we can go ahead and make our observer next /app/code/local/Branded3/PrintPicking/Model/Observer.php

class Branded3_Printpicking_Model_Observer
{
    public function addActions($observer)
    {
        $block = $observer->getEvent()->getBlock();
        // Check if this block is a MassAction block
        if ($block instanceof Mage_Adminhtml_Block_Widget_Grid_Massaction) {
            // Check if we're dealing with the Orders grid
            if ($block->getParentBlock() instanceof Mage_Adminhtml_Block_Sales_Order_Grid) {
                // The first parameter has to be unique, or you'll overwrite the old action.
                $block->addItem('demo', array(
                        'label' => Mage::helper('sales')->__('Print Predator Packing Labels'),
                        'url' => $block->getUrl('printpicking/adminhtml_index/getPacking'),
                    )
                );
            }
        }
    }
}

Here, we define our new Observer Class and our method addAction. We get the Observer Event Block and check for the MassAction widget, and check that it is the MassAction for the Sales Order Grid. If it is, we add a new item to it – our new mass action. As we’re adding a block, it’s worth using a unqiue name (first parameter of the $block->addItem call).

This gives us a new item in our drop down – “Print Picking Labels” – and sets its URL to route through to our eventual new controller. The controller is going to provide the action – i.e. creating a PDF for us to view / print / download.

Hooking up our mass action to actually do something is the next step. We’ve already defined a path, so let’s put the correct files in place.

Creating a PDF action

We need to create our new controller and action.

At /app/code/local/Branded3/PrintPicking/controllers/adminhtml/ lets create an IndexController.php file.

Here, we create a new class that extends Mage_Adminhtml_Controller_Action. We call this Branded3_Printpicking_Adminhtml_IndexController and we can define a new action. Based on our route, this needs to be getPickingAction().

Since we have the ability to select multiple orders, the first thing we need to do is get all the order ID’s we’ve passed through to this function and find the relevant orders.

$orderIds = $this->getRequest()->getParam('order_ids');
$collection = Mage::getModel('sales/order')->getCollection()
      ->addFieldToSelect('*')
      ->addFieldToFilter('entity_id',array('in'=>$orderIds));

From here, we pass off the order information to a PDF model. As we’re not dealing with Shipments, some information isn’t available if we used Mage_Sales_Model_Order_Pdf_Abstract & Mage_Sales_Model_Order_Pdf_Items_Abstract – as we’d need to be looking for the order/invoice that the shipment had been created on, so we’ll go ahead and override these with our own methods.

Create the following two files: /app/code/local/Branded3/Printpicking/Model/Pdf.php and /app/code/local/Branded3/Printpicking/Model/Pdfitem.php

Here we can go ahead and override the two classes mentioned, so we end up with class
class Branded3_Printpaicking_Model_Pdf extends Mage_Sales_Model_Order_Pdf_Abstract
class Branded3_Printpicking_Model_Pdfitem extends Mage_Sales_Model_Order_Pdf_Items_Abstract

We can then call these from our controller.

if (!isset($pdf)){
    $pdf = Mage::getModel('branded3_printpicking/pdf')->getPdf($orders);
} else {
    $pages = Mage::getModel('branded3_printpicking/pdf')->getPdf($orders);
    $pdf->pages = array_merge ($pdf->pages, $pages->pages);
}

This means that we can override the _drawHeader method (for example if we want to add or remove any information) and we can also override the getPdf() method in order to change complete layout.

We need to also tell the PDF class to use the Pdf class as its Renderer.

protected function _getRenderer($type)
{
    $renderer = "branded3_printpicking/pdfitem";
    $this->_renderers[$type]['renderer'] = Mage::getSingleton($renderer);
    return $this->_renderers[$type]['renderer'];
}

As mentioned above, there are some issues around how we can get order item information from an order. To do this, we can override the getSku($item) function found within Pdfitem. It should look like:

/**
 * Return item Sku
 *
 * @param  $item
 * @return mixed
 */
 public function getSku($item)
 {
    return $item->getSku();
 }

As stated in my introduction, we needed to add customer comments to the PDF. This added an additional issue, as we noticed that the site had the One Step Checkout extension running. Luckily, One Step Checkout makes this easy. You can load up an order object and call the function getOnestepcheckoutCustomercomment().

Now you should hopefully have all the component parts to put together your new module. Moving on from here, you can now look at changing Branded3_Printpaicking_Model_Pdf & Branded3_Printpicking_Model_Pdfitem to update the information required in your PDF.

If you find yourself up against any problems implementing any of the above, simply leave me a comment below and I’ll get back to you.

By Douglas Radburn. at 1:24PM on Wednesday, 31 Jul 2013

Doug is our Senior Open Source Web Developer. Having gained some informative insight and technical experience at two major digital agencies after graduating; Doug brought his knowledge and skills to Branded3 in 2009, and has been solving our development dilemmas ever since. Follow Douglas Radburn on Twitter.

comments

  • minguy

    This looks like what I’m looking for. However, I’m having difficulty following your directions.

    Can you show exactly what the extension files should contain? For example,
    the code you show for the config.xml file looks completely different
    from the typical config.xml file in other extensions.

    Or perhaps you have a zip archive of sample files I can use as a template?

    Thanks.

  • Bloop Pop

    Do you hace an example of the pdf created by this? Seems to be what i need but not sure :(

  • Andy

    Hi Douglas, this has been a great help so far but I’m having trouble calling the controller from the observer when selecting print picking lists from the action menu.

    It’s trying to navigate to a url like this: /magento/index.php/printpicking/adminhtml_index/getPacking/key/2afaa2b691f5b0ee94287b6d4a62e699/

    Any ideas?

  • Douglas Radburn

    Hi All, I’m going to add to this post to give some more indepth code samples, and some example output. Cheers, Douglas.

  • Douglas Radburn

    Hi Doug,

    Thanks for pointing that out. It seems that WordPress got a little too clever when it formatted the post. Should be rectified now. I’ve added in the config.xml sample too. Let me know if you still need anything, or hit me up on Twitter @douglasradburn.