2016/11/20

Fatal Error in Magento: Class Mage_YourModule_Helper_Data not found in app/Mage.php on line 547

This is an error, which I faced some times after late nights in code.

It is a very bad error message, because it does not explain what´s really going wrong. This is also problem, because you do not know where to search for the error. Especially if you done some more changes in your code and you need to go through every single change with your GIT diff.

The reason of this error is, that you or another module of your Magento installations accesses a configuration from the XML files which is not valid. In most cases it is a typing error in a recent added or changed config section.

The solution


To solve this error: go carefully through your XML configuration in config.xml, adminhtml.xml, system.xml or any layout XML file and find the mistyping node.
It could be a close-Tag which is typed different from the open-Tag or vise versa. Or it could be a missing close-Tag.

Sometimes some of this mistakes could also lead to strange behaviour like your custom module is not activated anymore and as a result controllers actions are not working which will cause 404 on routes that were working before this mistakes.

I hope this little hint will save somebody ours of debugging!

2016/11/07

How to display M2E order data or a product attribute in the Magento order grid

Lets assume you wish to extend your Magento order grid in the admin panel with a new column that displays an attribute of the ordered products to get a better overview for your shipping processes.

The most small or midsize online shops use the order grid to manage their shipments with export bulk methods or directly through the grid table.

Therefor it is comfortable to have all information needed in this grid. In this little how-to we will extend our grid to display order information of the m2e module and another example with a product attribute. You can customize the code to get any other attribute.

Create a local module to extend the order grid

We assume that you know how to create a new local module.
Create a proper config/etc.xml.

First create the Grid.php block which will override the core block in the default sales module. Copy your orignal Mage_Adminhtml_Block_Sales_Order_Grid an paste it into your new file. Then it might look like the following class.
Yourname_Yourmodule_Block_Adminhtml_Sales_Order_Grid:

class Yourname_Yourmodule_Block_Adminhtml_Sales_Order_Grid
    extends Mage_Adminhtml_Block_Widget_Grid
{

    public function __construct()
    {
        parent::__construct();
        $this->setId('sales_order_grid');
        $this->setUseAjax(true);
        $this->setDefaultSort('created_at');
        $this->setDefaultDir('DESC');
        $this->setSaveParametersInSession(true);
    }

    /**
     * Retrieve collection class
     *
     * @return string
     */
    protected function _getCollectionClass()
    {
        return 'sales/order_grid_collection';
    }

    protected function _prepareCollection()
    {
        $collection = Mage::getResourceModel($this->_getCollectionClass());

        $this->setCollection($collection);
        return parent::_prepareCollection();
    }

    protected function _prepareColumns()
    {
        $this->addColumn('real_order_id', array(
            'header'=> Mage::helper('sales')->__('Order #'),
            'width' => '80px',
            'type'  => 'text',
            'index' => 'increment_id',
        ));

        if (!Mage::app()->isSingleStoreMode()) {
            $this->addColumn('store_id', array(
                'header'    => Mage::helper('sales')->__('Purchased From (Store)'),
                'index'     => 'store_id',
                'type'      => 'store',
                'store_view'=> true,
                'display_deleted' => true,
            ));
        }

        $this->addColumn('created_at', array(
            'header' => Mage::helper('sales')->__('Purchased On'),
            'index' => 'created_at',
            'type' => 'datetime',
            'width' => '100px',
        ));

        $this->addColumn('billing_name', array(
            'header' => Mage::helper('sales')->__('Bill to Name'),
            'index' => 'billing_name',
        ));

        $this->addColumn('shipping_name', array(
            'header' => Mage::helper('sales')->__('Ship to Name'),
            'index' => 'shipping_name',
        ));

        $this->addColumn('base_grand_total', array(
            'header' => Mage::helper('sales')->__('G.T. (Base)'),
            'index' => 'base_grand_total',
            'type'  => 'currency',
            'currency' => 'base_currency_code',
        ));

        $this->addColumn('grand_total', array(
            'header' => Mage::helper('sales')->__('G.T. (Purchased)'),
            'index' => 'grand_total',
            'type'  => 'currency',
            'currency' => 'order_currency_code',
        ));

        $this->addColumn('order_type', array(
            'header' => Mage::helper('sales')->__('Order Type'),
            'width' => '100px',
            'align' => 'left',
            'index' => 'order_type',
            'renderer' => 'yourmodule/adminhtml_sales_grid_renderer_m2eAttribute',
            'filter_condition_callback' => array($this, '_filterM2eConditionCallback')
        ));

        $this->addColumn('status', array(
            'header' => Mage::helper('sales')->__('Status'),
            'index' => 'status',
            'type'  => 'options',
            'width' => '70px',
            'options' => Mage::getSingleton('sales/order_config')->getStatuses(),
        ));

        if (Mage::getSingleton('admin/session')->isAllowed('sales/order/actions/view')) {
            $this->addColumn('action',
                array(
                    'header'    => Mage::helper('sales')->__('Action'),
                    'width'     => '50px',
                    'type'      => 'action',
                    'getter'     => 'getId',
                    'actions'   => array(
                        array(
                            'caption' => Mage::helper('sales')->__('View'),
                            'url'     => array('base'=>'*/sales_order/view'),
                            'field'   => 'order_id'
                        )
                    ),
                    'filter'    => false,
                    'sortable'  => false,
                    'index'     => 'stores',
                    'is_system' => true,
            ));
        }

        $this->addRssList('rss/order/new', Mage::helper('sales')->__('New Order RSS'));

        $this->addExportType('*/*/exportCsv', Mage::helper('sales')->__('CSV'));
        $this->addExportType('*/*/exportExcel', Mage::helper('sales')->__('Excel XML'));

        return parent::_prepareColumns();
    }

    protected function _prepareMassaction()
    {
        $this->setMassactionIdField('entity_id');
        $this->getMassactionBlock()->setFormFieldName('order_ids');
        $this->getMassactionBlock()->setUseSelectAll(false);

        if (Mage::getSingleton('admin/session')->isAllowed('sales/order/actions/cancel')) {
            $this->getMassactionBlock()->addItem('cancel_order', array(
                 'label'=> Mage::helper('sales')->__('Cancel'),
                 'url'  => $this->getUrl('*/sales_order/massCancel'),
            ));
        }

        if (Mage::getSingleton('admin/session')->isAllowed('sales/order/actions/hold')) {
            $this->getMassactionBlock()->addItem('hold_order', array(
                 'label'=> Mage::helper('sales')->__('Hold'),
                 'url'  => $this->getUrl('*/sales_order/massHold'),
            ));
        }

        if (Mage::getSingleton('admin/session')->isAllowed('sales/order/actions/unhold')) {
            $this->getMassactionBlock()->addItem('unhold_order', array(
                 'label'=> Mage::helper('sales')->__('Unhold'),
                 'url'  => $this->getUrl('*/sales_order/massUnhold'),
            ));
        }

        $this->getMassactionBlock()->addItem('pdfinvoices_order', array(
             'label'=> Mage::helper('sales')->__('Print Invoices'),
             'url'  => $this->getUrl('*/sales_order/pdfinvoices'),
        ));

        $this->getMassactionBlock()->addItem('pdfshipments_order', array(
             'label'=> Mage::helper('sales')->__('Print Packingslips'),
             'url'  => $this->getUrl('*/sales_order/pdfshipments'),
        ));

        $this->getMassactionBlock()->addItem('pdfcreditmemos_order', array(
             'label'=> Mage::helper('sales')->__('Print Credit Memos'),
             'url'  => $this->getUrl('*/sales_order/pdfcreditmemos'),
        ));

        $this->getMassactionBlock()->addItem('pdfdocs_order', array(
             'label'=> Mage::helper('sales')->__('Print All'),
             'url'  => $this->getUrl('*/sales_order/pdfdocs'),
        ));

        $this->getMassactionBlock()->addItem('print_shipping_label', array(
             'label'=> Mage::helper('sales')->__('Print Shipping Labels'),
             'url'  => $this->getUrl('*/sales_order_shipment/massPrintShippingLabel'),
        ));

        return $this;
    }

    public function getRowUrl($row)
    {
        if (Mage::getSingleton('admin/session')->isAllowed('sales/order/actions/view')) {
            return $this->getUrl('*/sales_order/view', array('order_id' => $row->getId()));
        }
        return false;
    }

    public function getGridUrl()
    {
        return $this->getUrl('*/*/grid', array('_current'=>true));
    }

    /**
     * filter callback to find the order_type
     * of orders through m2e (amazon, ebay, ...)
     *
     * @param object $collection
     * @param object $column
     * @return Yourname_Yourmodule_Block_Adminhtml_Sales_Order_Grid
     */
    public function _filterM2eConditionCallback($collection, $column) {
        if (!$value = $column->getFilter()->getValue()) {
            return $this;
        }
        if (!empty($value) && strtolower($value) != 'magento') {
            $this->getCollection()->getSelect()
                ->join(
                    'm2epro_order',
                    'main_table.entity_id=m2epro_order.magento_order_id',
                    array('component_mode')
                    )
                ->where(
                 'm2epro_order.component_mode = "' . strtolower($value) . '"');
        } elseif(strtolower($value) == 'magento') {
            $this->getCollection()->getSelect()
                ->join(
                    'm2epro_order',
                    'main_table.entity_id=m2epro_order.magento_order_id',
                    array('component_mode')
                    )
                ->where(
                 'm2epro_order.component_mode = NULL');
        }

        return $this;
    }
}

What has changed to the original class, is the following snippet to add a new column and the relating callback method named _filterM2eConditionCallback.


$this->addColumn('order_type', array(
    'header' => Mage::helper('sales')->__('Order Type'),
    'width' => '100px',
    'align' => 'left',
    'index' => 'order_type',
    'renderer' => 'yourmodule/adminhtml_sales_grid_renderer_m2eAttribute',
    'filter_condition_callback' => array($this, '_filterM2eConditionCallback')
));

As you can see, the column has a filter_condition_callback which manage the select to filter through the right tables. The renderer block is the other thing that is special about our column. The renderer will take care of the value that is displayed in the column. Lets take a look at the renderer:
Yourname_Yourmodule_Block_Adminhtml_Sales_Grid_Renderer_M2eAttribute


class Yourname_Yourmodule_Block_Adminhtml_Sales_Grid_Renderer_M2eAttribute
    extends Mage_Adminhtml_Block_Widget_Grid_Column_Renderer_Abstract
{
    public function render(Varien_Object $row)
    {
        // do whatever you need, to display your data
        // get the id of the row order data
        $orderId = $row->getEntityId();
        // get the related m2e order data
        $orders = Mage::getModel('M2ePro/Order')
            ->getCollection()
            ->addFieldToSelect('component_mode')
            ->addFieldToFilter('magento_order_id', $orderId);

        if($orders) {
            $data = $orders->getFirstItem()->getData();
            if(isset($data['component_mode'])) {
                return ucfirst($data['component_mode']);
            }
        }
        // return the string "magento" if there is no m2e relation
        return 'Magento';
    }
}

Now we look at the filter callback:


/**
 * filter callback to find the order_type
 * of orders through m2e (amazon, ebay, ...)
 *
 * @param object $collection
 * @param object $column
 * @return Yourname_Yourmodule_Block_Adminhtml_Sales_Order_Grid
 */
public function _filterM2eConditionCallback($collection, $column) {
    if (!$value = $column->getFilter()->getValue()) {
        return $this;
    }
    if (!empty($value) && strtolower($value) != 'magento') {
        $this->getCollection()->getSelect()
            // join to the m2mepro order table and select component_mode
            ->join(
                'm2epro_order',
                'main_table.entity_id=m2epro_order.magento_order_id',
                array('component_mode')
                )
            ->where(
             'm2epro_order.component_mode = "' . strtolower($value) . '"');
    } elseif(strtolower($value) == 'magento') {
        $this->getCollection()->getSelect()
            ->join(
                'm2epro_order',
                'main_table.entity_id=m2epro_order.magento_order_id',
                array('component_mode')
                )
            ->where(
             'm2epro_order.component_mode = NULL');
    }

    return $this;
}


Another example to select a product attribute in the order grid

This is an example of a filter callback to get data of a specific product attribute:


public function _filterShippingCostConditionCallback($collection, $column) {
    if (!$value = $column->getFilter()->getValue()) {
        return $this;
    }
    if (!empty($value)) {
        $this->getCollection()->getSelect()
            // join to to the flat order item table
            ->join(
                'sales_flat_order_item',
                'main_table.entity_id=sales_flat_order_item.order_id',
                array('product_id')
                )
            // join to the value table for the products
            ->join(
                'catalog_product_entity_decimal',
                'sales_flat_order_item.product_id=catalog_product_entity_decimal.entity_id',
                array('value')
            )
            // where condition to select the value
            ->where(
                // attribute_id relates to your attribute
             'catalog_product_entity_decimal.value = "' . ($value) . '" AND catalog_product_entity_decimal.attribute_id=247');
    }
    return $this;
}


The first join will get the table of the order items, to get the relation between the order and the ordered items. From there we join the second table from the product id of the previously selected items. Here you have to check which of the eav table has your attribute value. If you have an attribute with a text field. catalog_product_entity_varchar will be the right one.

In the condition you have to adapt the attribute_id to the one you are trying to select.

Now go to your adminpanel and check the result.

2016/11/02

6 steps to create a custom admin controller with a entity grid in Magento

If you don´t do this all day, you might need some tiny reminder on how to create a customer resource entity in Magento. This post will give you a list of the things you need to do  to list your entity data in the admin panel.

Here it is:

1. Setting up the config.xml

Create the common config nodes like helpers, blocks, models. In the following is only written what is relevant for your entities (this belongs to your global node):

<models>
 <modulename>
   <class>Vendor_ModuleName_Model</class>
   <resourceModel>modulename_resource</resourceModel>
 </modulename>

 <modulename_resource>
   <class>Vendor_ModuleName_Model_Resource</class>
   <entities>
     <dataset>
       <table>modulename_dataset</table>
     </dataset>
   </entities>
 </modulename_resource>
</models>
<resources>
 <modulename_setup>
   <setup>
     <module>Vendor_ModuleName</module>
   </setup>
 </modulename_setup>
</resources>

modulename is the key for our model name. It is derived from our module name: ModuleName. Our entity will be called dataset and is located in the database within the table modulename_dataset.
The node is defining what resource model to use under the key modulename_resource which you can use in Mage methods like 

Mage::getResourceModel('modulename_dataset')


This is related to the resource definition (under node ) in this config with the classname and its entities (in this case dataset).

The node in the last lines are preparing our setup class where you define the SQL to create the tables for our entity.

2. Setting up the SQL Setup

Create a new file in your modules folder:  sql/modulename_setup/install-0.1.0.php (the last folder name is defined by our previous set node. You can do a lot of magic in this setup file but for simplicity we just will create a new table.


/* @var $installer Mage_Core_Model_Resource_Setup */
$installer = $this;
$installer->startSetup();
$installer->run("

DROP TABLE IF EXISTS {$this->getTable('modulename_dataset')};
CREATE TABLE {$this->getTable('modulename_dataset')} (
  `id` int(255) NOT NULL auto_increment,
  `anyfield` int(255) NOT NULL,
  `text` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Your own table';

$installer->endSetup();


The relation for $this->getTable() we defined in our config above in the node
.
This file is executed when you access your adminpanel the first time after you created the module and the module you created has the exact version number as in the filename.

3. Create the models and resource models

Create the dataset model in app/code/community/Vendor/ModuleName/Model/Dataset.php

class Vendor_ModuleName_Model_Dataset
    extends Mage_Core_Model_Abstract
{
    const CACHE_TAG             = 'modulename_dataset';
    protected $_cacheTag        = 'modulename_dataset';
    protected $_append_class    = '';

    protected function _construct()
    {
        $this->_init('modulename/dataset');
    }
}

The string in the _init method is related to resouce model key and resource name.

Info: It has to be exactly the same as your classname of the model - case sensitive! If you name it DataSet.php (classname = Vendor_ModuleName_Model_DataSet) then you have to give _init this string: 'modulename/dataSet'. And you have to update your part in the config.xml!

Create the dataset resource model in app/code/community/Vendor/ModuleName/Model/Resource/Dataset.php

class Vendor_ModuleName_Model_Resource_Dataset
    extends Mage_Core_Model_Resource_Db_Abstract
{
    /**
     * Initialize resource
     *
     */
    protected function _construct()
    {
        $this->_init('modulename/dataset', 'id');
    }
}

The same here. The second parameter in _init belongs to the id field in your table.

Create the dataset resource collection in app/code/community/Vendor/ModuleName/Model/Resource/Dataset/Collection.php

class Vendor_ModuleName_Model_Resource_Dataset_Collection
    extends Mage_Core_Model_Resource_Db_Collection_Abstract
{
    /**
     * construct
     */
    public function _construct()
    {
        $this->_init('modulename/dataset');
    }
}


4. Create the adminhtml controller


class Vendor_ModuleName_Adminhtml_ModuleName_DatasetController
    extends Mage_Adminhtml_Controller_Action
{
    protected function _construct()
    {
        // Define module dependent translate
        $this->setUsedModuleName('Vendor_ModuleName');
    }
    /**
     * Init actions
     *
     * @return Vendor_ModuleName_Adminhtml_ModuleName_DatasetController
     */
    protected function _initAction()
    {
        return $this;
    }

    /**
     * configure action
     * includes the listing of configuration
     * items
     */
    public function indexAction()
    {
        $this->_title($this->__('ModuleName'));
        $this->loadLayout();

        $this->_addContent($this->getLayout()->createBlock('modulename/adminhtml_dataset'));

        $this->renderLayout();
    }
}


The block we create in the index action, we will define in the next step. This for sure requires the node in you configuration, which we did not wrote here.

5. Create the blocks

class Vendor_ModuleName_Block_Adminhtml_Dataset
    extends Mage_Adminhtml_Block_Widget_Container
{
    public function __construct()
    {
        parent::__construct();
        $this->setTemplate('modulename/dataset.phtml');
    }

    /**
     * Prepare button and grid
     *
     * @return Vendor_ModuleName_Block_Adminhtml_Dataset
     */
    protected function _prepareLayout()
    {
        $this->setChild('grid', $this->getLayout()->createBlock('modulename/adminhtml_dataset_grid', 'adminhtml_modulename_dataset.grid'));
        return parent::_prepareLayout();
    }

    /**
     * Render grid
     *
     * @return string
     */
    public function getGridHtml()
    {
        return $this->getChildHtml('grid');
    }
}


Create the grid block, which we created in this block.

class Vendor_ModuleName_Block_Adminhtml_Dataset_Grid
    extends Mage_Adminhtml_Block_Widget_Grid
{
    /**
     * construct
     */
    public function __construct()
    {
        parent::__construct();
        $this->setId('moduleNameGrid');
        $this->setDefaultSort('id');
        $this->setSaveParametersInSession(true);
        $this->setDefaultDir('DESC');
    }

    /**
     * prepare collection
     *
     * @return Vendor_ModuleName_Block_Adminhtml_Dataset_Grid
     */
    protected function _prepareCollection()
    {
        $collection = Mage::getModel('modulename/dataset')->getCollection();
        $this->setCollection($collection);
        parent::_prepareCollection();

        return $this;
    }

    /**
     * prepare Columns
     */
    public function _prepareColumns()
    {
        $baseUrl = $this->getUrl();

        $this->addColumn('id',array(
            'header'        => Mage::helper('modulename')->__('Id'),
            'align'         => 'center',
            'index'         => 'id',
            'width'         => '1%'
        ));
        $this->addColumn('text',array(
            'header'        => Mage::helper('modulename')->__('Text'),
            'align'         => 'left',
            'index'         => 'type',
            'width'         => '29%',
            'filter_index'  => 'type'
        ));
        return parent::_prepareColumns();
    }

    public function getRowUrl($row) {
        // only works if you create that controller action
        return $this->getUrl('*/*/edit',array('id' => $row->getEntityId()));
    }
}


This creates the grid for you. You can define which fields are displayed in the grid.

6. Create the adminhtml template


<div class="content-header">
<table cellspacing="0">
<tr>
<td style="width:50%;"><h3 class="icon-head head-dataset"><?php echo />Mage::helper('modulename')->__('See data') ?></h3></td>
<td class="a-right">
</td>
</tr>
</table>
</div>
<div>
<?php echo $this->getGridHtml() ?>
</div>

This was a summery of what to do to make the grid work. Some steps, like setting up the locale files or helper are not explained in this steps, but as a professional developer you know how to do this.

2016/11/01

How a german online-shop reduced customer questions by 80%

A customer of our new module Advanced Category Navigation has a shop layout, that is nearly the same as the default Magento template.

The products of this store need a little more information, because all products are used as boxes for specific tires and rims with several measurements.

Before this shop was using the Advanced Category Navigation they were receiving many phone calls from customers every day who needed help to find the right product.
After the module was installed in his shop the the shop owner was exited, because there were almost no more phone calls from his customers.

Our customer is a great example for how to use the Advanced Category Navigation to get a great overview with brief descriptions for your products.


The Magento module helped his customers to find the correct product by adding descriptions to the categories, using categories as groups and by displaying the products right next to the categories. This will give a better summary to the user. This let the user decide much faster, which of the products are best for him.

You will find our module Grafzahl AdvancedCategoryNavigation in our shop or on magentocommerce.com/connect.

We also provide a detailed documentation for an easy installation and fast setup of the module.

Purchase AdvancedCategoryNavigation in our Store.