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.

2016/10/31

Secure your shop against brute force attacks!

Magento published a post which says that some URLs in your Magento installation may vulnerable to brute force attacks and they talk about an increasing number of attacks on Magento stores.

Secure /admin path

This URL leads you to your adminpanel with a default Magento installation. You should change that path URI to something cryptic to increase your security level.
You can do that in your Magento system configuration: Advanced > Admin.
Additionally you can secure it by denying all access by .htaccess except your IP:

order deny,allow
deny from all
allow from x.x.x.x

 

Secure /rss/catalog and /rss/order

This can be done by adding new rewrite conditions to your .htacces.
Add these lines to your /magento_root/.htaccess:

## block access to admin rss feed
    RewriteCond %{REQUEST_URI} ^.*/rss/catalog[OR,NC]
    RewriteCond %{REQUEST_URI} ^.*/rss/order[NC]
    RewriteRule ^(.*)$ https://%{HTTP_HOST}/ [R=302,L]

This will redirect the /rss/ feed directly to your homepage. If you have no SSL change the last line to

RewriteRule ^(.*)$ http://%{HTTP_HOST}/ [R=302,L]

But you should add SSL to your shop.

Secure /downloader 

This path is used to install or uninstall modules in your shop. To secure this place from unauthorized access we have to deny access with the .htaccess in the /downloader folder. Add these lines to your /downloader/.htaccess file:

order deny,allow
deny from all

If you want to use the downloader, you can temporary comment these lines or whitelist your IP, if you add this line:

allow from x.x.x.x

Check other vulnerabilities.

To be sure you secured all known vulnerabilities, this page is good to check states of all available patches: https://www.magereport.com/

2016/10/30

How to crop an image in Magento 1.x

We can easily render an image in Magento through the delivered Varien_Image library.

Here is an example for a class method (e.g. for a helper):

public function getCroppedImage($imageSource,
        $top,
        $left,
        $right,
        $bottom)
    {
        $mediaPathSegment = "yourfolder/cache/";
        if(!$imageSource) {
            return false;
        }
        $destinationFilepath = Mage::getBaseDir('base') . "/media/" . $mediaPathSegment . basename($imageSource);

        // only render, if source file exists and destination file does not (to prevent rendering on every page request
        if(file_exists($imageSource) && !file_exists($destinationFilepath)) {
            // render image
            $imageObj = new Varien_Image($imageSource);
            // crop image
            $imageObj->crop($top, $left, $right, $bottom);
            // save to file
            $imageObj->save( $destinationFilepath );

            // return image url
            if(file_exists($destinationFilepath)) {
                // build url path to get image link for frontend
                return Mage::getBaseUrl('media') .$mediaPathSegment . basename($imageSource);
            } else {
                // if image could not be rendered fallback and return original image
                return $imageSource;
            }
        }
        return false;
    }

The parameter $imageSource has to be an absolute filepath to the image you want to render.

How crop() is working

Crop has four parameters: top, left, right and bottom. To understand how the crop method is working you take an imaginary line which is parallel to the side you set. If you increase the value, the imaginary line is moving to the opposite side of the image. Everything of the image that is between the specific image side and the imaginary line will be cut.

This means that

$imageObj->crop(10,10,10,10);

will cut 10 pixel from image on every side. The image will be 20 pixel smaller for width and height.
This also means, that the cropped image will keep the original ratio.

Cropping an image to a square

As another example I created a method that will cut an image to a square. This is needed sometimes to fit an image to a layout or a design. Images that are not the same format look ugly and uneasy.

/**
 * cropping images to square and resize
 *
 * @param string $imageFilename
 * @param int $resizeWidth in pixel
 * @return mixed returns new resized image url or false
 */
public function getSquareImageResized($imageFilename, $resizeWidth = null)
{
    if(!$imageFilename) {
        return false;
    }

    $mediaPathSegment = "yourfolder" . DS . "cache" . DS . "square_resized" . DS;
    $destinationFilepath = Mage::getBaseDir('base') . DS . "media" . DS . $mediaPathSegment . basename($imageFilename);

    if(file_exists($imageFilename)) {
        if(!file_exists($destinationFilepath)) {
            // render image
            $imageObj = new Varien_Image($imageFilename);
            // crop image
            $imageObj->constrainOnly( true );
            $imageObj->keepAspectRatio( true );
            $imageObj->keepFrame( false );

            // crop image
            $width = $imageObj->getOriginalWidth();
            $height = $imageObj->getOriginalHeight();

            // cut image to square
            if ($width > $height) {
                $diff = ($width-$height) / 2;
                $imageObj->crop(0, $diff, $diff, 0);
            } else {
                $diff = ($height-$width) / 2;
                $imageObj->crop($diff, 0, 0, $diff);
            }

            // resize image if needed
            if($resizeWidth != null) {
                $imageObj->resize($resizeWidth, $resizeWidth);
            }
            $imageObj->save( $destinationFilepath );
        }
        // return image url
        if(file_exists($destinationFilepath)) {
            return Mage::getBaseUrl('media') . $mediaPathSegment . basename($imageFilename);
        }else{
            return $imageFilename;
        }
    }
    return false;
}

2016/10/29

How to pay with Paypal in Magento from the Admin Panel?

If you or your sales team creates orders for your customers over the Magento adminpanel sometimes, you might came to this issue: You cannot select Paypal in the Backend order.

This seems logical if you think about the Paypal payment process: after the checkout you are redirected to the Paypal website for the purchase. This is not possible in a situation, where you are creating an order for a customer, because you do not have his password (e.g. if you talk with the customer on the phone or in person in your local store). This is a general issue for all payment options like Paypal like Sofort.com, Klarna, Billsafe and so on.

But with a little workaround, you can easily let your customers pay with Paypal while you are ordering for him in the Magento Backend.

The solution

We are adding a Backend Payment method which is called "Paypal" with no further program logic. This ensures, the order and invoice information shows the correct payment method.

BackendPayment provides payment methods like cash, debit and Paypal
After we created the order, we go to our the official Paypal website and use the "Request money" function. This allows you to enter the Paypal Email address of your customer and the amount of the order to request the money over Paypal.

For the developers 

It is easy to provide a new payment method which is only available in the admin panel and not on the customers checkout. Just create a new module with the default folders and configuration for at least models, blocks and helpers.

The first thing you need is a model which looks like the following (you have to change class names to your correct namespace):

/**
 * Grafzahl_BackendPayments
 *
 * @category    Grafzahl
 * @package     Grafzahl_BackendPayments
 * @copyright   Copyright (c) 2016 Grafzahl (https://grafzahl.io)
 * @license     https://grafzahl.io/license
 */

class Grafzahl_BackendPayments_Model_PaypalPayment
    extends Mage_Payment_Model_Method_Abstract
{
    protected $_code = 'backendpayments_paypal';
    protected $_infoBlockType = 'backendpayments/info';
    protected $_canUseInternal = true;
    protected $_canUseCheckout = false;
    protected $_canUseForMultishipping  = false;

    public function __construct()
    {
        parent::__construct();
    }
}

The second thing is to create the related block from the model in the attribute $_infoBlockType. The block just needs to extend the block Mage_Payment_Block_Info. Now check the extending block and update the methods to your needs.

Complete ready to use solution

Such a payment method, mentioned above is already available. You can download BackendPayments from our grafzahl.io store and install it in your Magento store from version 1.7.x - 1.9.x with no pain.

This will provide you three new payment methods in your admin panel order form: Paypal, Debit and Cash. You can even use any other payment provider instead of Paypal. The title can be edited in the system configuration.

With this module you can also create order with the method Cash payment and Debit Payment. If you have a little local store which you manage with a Magento Shop-System, you will be able to create orders with cash payment from your admin panel.

You can also configure if these methods should automatically create the invoice for you. For further information, check our documentation source.


2016/10/27

Why should you make your Magento Product Filter URL look sexy

Fist off - it is every URL´s right to look sexy and Magento supports readable URLs well.

But some areas are still a little bit abstract and not optimized for humans or the search engines.

Search engines like Google have very complex algorithms to rate a website. They already guess very good which content a user is searching for. Because they are rating the websites on how good they are optimized for human!

This is the reason why Magento´s default layered filter URLs are bad for humans and for SEO:
http://magento.local/men/shirts.html?color=27
Nobody knows that color=27 is blue - neither does Google (although we think Google knows everything)!
So if someone is searching for a Shirt in blue, your products won´t be in the index, because your Shirt is 27, and therefor not indexed as blue shirts ;). This may be the case for the product detail pages, where the product has it´s attributes, but here we talk about the category pages where the product collections are filtered.

To solve this problem we have created the SEO Filter URL module. It is easy to install and it works with almost no configuration.
At the end you will get URLs for your Magento  1.7, 1.8, 1.8.1, 1.9, 1.9.1, 1.9.2, 1.9.3 like:
 http://magento.local/men/shirts/filter/color/blue.html
 Also the price filter is translated. If you click filter "$140.00 and above", you will get:
http://magento1-9-3.local/men/shirts/filter/color/blue/price/from-140-and-above.html
in your browser´s address line.

For the developers among us:
It seamlessly integrates in the routing with an observer on the event controller_front_init_before. The links of the layered are change by overwriting the navigation model Mage_Catalog_Model_Layer_Filter_Item.
Also the page links of the catalog toolbar are change to get a readable "page" instead of "p=" for links in the pager.