Posted on Leave a comment

Fixing Magento 1 newsletter queue bug due to missing encoding in grid renderer class

Magento Logo

In a recent Magento 1.9.3.2 project we experienced a strange behavior related to the built-in newsletter module in admin grid. When trying to add a newsletter template to the queue using the action dropdown in the admin grid the following JavaScript error showed up:

Uncaught SyntaxError: Unexpected end of JSON input
    at JSON.parse (<anonymous>)
    at String.parseJSON [as evalJSON] (prototype.js:720)
    at Object.execute (grid.js:717)
    at HTMLSelectElement.onchange (085f35f…:722)

A quick look at the option value for the admin grid’s row select input showed that the JSON was not properly escaped:

<select class="action-select" onchange="varienGridAction.execute(this);"><option value=""></option><option value="{" href":"https:\="" \="" www.someshop.com\="" index.php\="" __ma2ge_a5dm2in__\="" newsletter_queue\="" edit\="" template_id\="" 1\="" key\="" e5bdca9b9185cd175c6f9d297127d238\="" "}"="">Newsletter Warteschlange ...</option><option value="{" popup":true,"href":"https:\="" \="" www.someshop.com\="" index.php\="" __ma2ge_a5dm2in__\="" newsletter_template\="" preview\="" id\="" 1\="" key\="" b4b16e0fa2fb208b6191e6ddb3a6282c\="" ","onclick":"popwin(this.href,'_blank','width="800,height=700,resizable=1,scrollbars=1');return" false;"}"="">Vorschau</option></select>

As you can see the double quotes for the JSON option value was broken, thus resulting in a JavaScript exception when varienGridAction.execute(this) is triggered, e.g.:

<option value="{" href":"https:\="" \=""...

Since the built-in newsletter uses a custom row renderer for this action dropdown a check in Mage_Adminhtml_Block_Widget_Grid_Column_Renderer_Action was required, in particular _toOptionHtml:

  protected function _toOptionHtml($action, Varien_Object $row) {
        $actionAttributes = new Varien_Object();

        $actionCaption = '';
        $this->_transformActionData($action, $actionCaption, $row);
        $htmlAttibutes = array('value'=> $this->escapeHtml(Mage::helper('core')->jsonEncode($action)));

        $actionAttributes->setData($htmlAttibutes);
        return '<option ' . $actionAttributes->serialize() . '>' . $actionCaption . '</option>';
  }

The solution in this case is rather simple:

$htmlAttibutes = array('value'=> htmlentities($this->escapeHtml(Mage::helper('core')->jsonEncode($action))));

Further investigation is needed in this case as to why additional encoding is required. In the meantime the offending class was overwritten with the corresponding local version.

Posted on Leave a comment

Magento acquires RJMetrics to add Magento Analytics to portfolio

Magento Logo

Magento recently acquired RJMetrics, a powerful cloud-based analytics solution for e-commerce merchants. RJMetrics will be added to the Magento product suite as a brand new solution called Magento Analytics.

RJMetrics

The idea behind Magento Analytics according to Magento is that it will allow non-technical business users to quickly and easily integrate with enterprise-grade data sets across a broad array of applications in order to consolidate and analyze data for effective multi-brand, cross-channel reporting. Hereby, RJMetrics will be used to directly integrate an analytics solution to the core Magento framework to enable deep customer analysis for improve possible customization, personalization and merchandising, or as Magento puts it in fancy terms to continuously optimize digital shopping experiences and performance to increase sales and gross margin.

RJMetrics, or better put Magento Analytics will need to be put to a test to compare it to existing analytics and business intelligence solutions. After all, existing analytics tools such as Google Analytics or the open source alternative Piwik that although primarily focused on web tracking and web analytics with the correct setup can be used to effectively gather customer insights too without the need to incorporate additional tools in existing deployments.

RJMetrics key features

RJMetrics key features are Cohort Analysis to measure the effects of defined growth efforts on customer behavior, from conversion rate optimization to customer loyalty programs, Churn Analysis in order to adapt to customer feedback and identify causes that are making your customers churn, as always Marketing ROI to identify those channels and campaigns that are valuable for your business and be able to calculate ROI based on customer acquisition cost and customer lifetime value, Revenue Analytics based on your raw shop data, Email Segmentation to effectively target customers based on their behavior and finally Holiday Performance Enablers to optimize ad spendings on your key shopping days, ensure stocks are at hand and finally plan potential follow-up strategies to turn holiday buyers into loyal customers.

It’s good to know that Magento is expanding its horizon by acquiring RJMetrics and presenting a viable in-house analytics solution. For those of you who have tested or even implemented RJMetrics in your setups I’d be glad to hear some feedback. For more information about RJMetrics and what Magento Analytics can offer have a look at the RJMetrics website.
Posted on Leave a comment

Magento Newsletter Unsubscribe Form

Magento Logo

One of the most missed functions in Magento 1 is a newsletter unsubscribe form. Although, by default you can generate newsletter unsubscribe links to be used for instance in newsletters sent out to subscribers there’s no nice out-of-the-box way to integrate a newsletter unsubscribe form in Magento 1.

Which is why there already exist some extensions on the Magento Connect Marketplace that extend the existing newsletter functionality by e.g. adding a simple unsubscribe field and toggling the action submitted (“subscribe” vs. “unsubscribe”). Unfortunately, having a look at most of the existing extensions made clear that they either include too much functionality or simply are outdated.

Implementing the Magento Newsletter Unsubscribe Extension

Luckily, implementing the newsletter unsubscribe functionality in Magento is pretty straight forward. In order to share this information here’s the gist on how to create a Magento Newsletter unsubscribe form extension.

Basically, what we are trying to achieve here is an additional Magento page with a distinct URL that incorporates our newsletter unsubscribe form, i.e. “newsletter-unsubscribe”. This form handles submissions and checks if the e-mail submitted currently is subscribed to the Magento newsletter. If that’s the case it will unsubscribe this e-mail based on the built-in Magento functionality, i.e. it will automatically sent out the un-subscription confirmation mail and execute any other events that you might have integrated for newsletter related events. Last but not least, as always everything should be translatable.

Custom route instead of CMS page

Instead of creating an additional CMS page and loading our newsletter unsubscribe form into it (e.g. using layout updates) we are going to create a custom route for our extension. By default, this route will be newsletter-unsubscribe. The relevant entries in the config.xml are as follows:

<?xml version="1.0"?>
<config>
 
  <modules>
    <BothInteract_NewsletterUnsubscribe>
      <version>1.0.1</version>
    </BothInteract_NewsletterUnsubscribe>
  </modules> 
 
  <frontend>
    <routers>
      <bothinteract_newsletterunsubscribe>
        <use>standard</use>
        <args>
          <module>BothInteract_NewsletterUnsubscribe</module>
          <frontName>newsletter-unsubscribe</frontName>
        </args>
      </bothinteract_newsletterunsubscribe>
    </routers>
 
    <secure_url>
      <bothinteract_newsletterunsubscribe>/newsletter-unsubscribe/</bothinteract_newsletterunsubscribe>
    </secure_url>

    ...
</config>

As you can see an additional frontend route is added (newsletter-unsubscribe) that maps to our extensions’s controller.

Display newsletter unsubscribe form

Now that URLs starting with “newsletter-unsubscribe” are routed to our controller we need to make sure that our custom block is loaded and unsubscribe submissions are processed correctly.

As always, the default action is index and in our case it’s responsible for rendering or newsletter unsubscribe form:

public function indexAction() {

  $this->loadLayout();

  // load 1-column page layout
  $this->getLayout()
    ->getBlock('root')
    ->setTemplate('page/1column.phtml');

  // load breadcrumb bar
  $breadcrumbs = $this->getLayout()->getBlock('breadcrumbs');
  $helper = Mage::app()->getHelper('bothinteract_newsletterunsubscribe');

  if ($breadcrumbs) {
    $breadcrumbs->addCrumb('home', array(
      'label' => $helper->__('Home'),
      'title' => $helper->__('Go to Home Page'),
      'link' => Mage::getBaseUrl()));
    $breadcrumbs->addCrumb('newsletter-unsubscribe', array(
      'label' => $helper->__('Newsletter Unsubscribe'),
      'title' => $helper->__('Newsletter Unsubscribe'),
      'link' => Mage::getUrl('newsletter-unsubscribe')));
  }

  // render our custom block
  $block = $this->getLayout()
    ->createBlock('bothinteract_newsletterunsubscribe/customblock')
    ->setTemplate('bothinteract_newsletterunsubscribe/form.phtml');

  // append to CMS content block
  $this->getLayout()->getBlock('content')->append($block);

  $this->renderLayout();
}

There’s no magic here but let’s quickly dissect what is happening:

First, we are loading the layout and setting the 1-column page layout by default .

Hint: Using additional extension backend options we can make these settings available for users to edit directly without touching the code. This will be available in version 1.1.

Since we want our page to appear in the breadcrumbs too we manually add it. Finally, we create and append our custom block while setting the corresponding template and render the final layout.

Process newsletter unsubscribe form submissions

Now that we are able to display the newsletter unsubscribe form it’s time to process form submissions. This is done using the unsubscribe action:

public function unsubscribeAction() {
  if ($this->getRequest()->isPost() && $this->getRequest()->getPost('email')) {
    $session = Mage::getSingleton('core/session');
    $email = (string) $this->getRequest()->getPost('email');

  try {
    if (!Zend_Validate::is($email, 'EmailAddress')) {
      Mage::throwException($this->__('Please enter a valid email address.'));
    }

    $subscriber = Mage::getModel('newsletter/subscriber')
      ->loadByEmail($email);

    if ($subscriber && $subscriber->getId()) {
      // check already unsubscribed/inactivated before
      if ($subscriber->getStatus() == Mage_Newsletter_Model_Subscriber::STATUS_NOT_ACTIVE ||
        $subscriber->getStatus() == Mage_Newsletter_Model_Subscriber::STATUS_UNSUBSCRIBED) {
        $session->addSuccess($this->__('You are not registered with this e-mail.'));
      } else {
        $subscriber->unsubscribe();
        $session->addSuccess($this->__('You have been successfully unsubscribed.'));
      }
    } else {
      $session->addSuccess($this->__('You are not registered with this e-mail.'));
    }
   } catch (Mage_Core_Exception $e) {
     $session->addException($e, $this->__('There was a problem with the unsubscription: %s', $e->getMessage()));
   } catch (Exception $e) {
     $session->addException($e, $this->__('There was a problem with the unsubscription.'));
   }
  }
  $this->_redirectReferer();
}

Again, no magic is happening here. We are simply checking the submitted e-mail, try to load the corresponding newsletter subscriber object and check it’s status. Depending on the status we are able to unsubscribe this e-mail or display the appropriate message.

Our actual custom Magento block is very slim on purpose and only returns the form action URL of our custom extension:

class BothInteract_NewsletterUnsubscribe_Block_Customblock extends Mage_Core_Block_Template {

  public function getFormActionUrl() {
    return $this->getUrl('newsletter-unsubscribe/index/unsubscribe/', array('_secure' => true));
  }
}

Yes, that’s all there is to it. Sorry to disappoint you in case you are expecting magic stuff happening here. Thus, based on Magento’s stable architecture adding a newsletter unsubscribe form can be easily achieved.

Download Magento Newsletter Unsubscribe Extension

This extension has been released by Both Interact and is available for free on the Magento Connect marketplace as Both Interact Newsletter Unsubscribe extension. Feel free to download and rate the extension.

 

Posted on 4 Comments

Sample SEO Magento robots.txt file

Magento Logo

Since I get a lot of requests for a robots.txt file designed for Magento SEO here is a sample to get you started. This Magento robots.txt makes the following assumptions:

  • We don’t differentiate between search engines, hence User-agent: *
  • We allow assets to be crawled
    • i.e. images, CSS and JavaScript files
  • We only allow SEF URLs set in Magento
    • e.g. no direct access to the front controller index.php, view categories and products by ID, etc.
  • We don’t allow filter URLs
    • Please note: The list provided is not complete. In case you have custom extension that use filtering make sure to include these filter URLs and parameters in the filter URLs section.
  • We don’t allow session related URL segments
    • e.g. product comparison, customer, etc.
  • We don’t allow specific files to be crawled
    • e.g. READMEs, cron related files, etc.

Magento robots.txt

Enough of the talking, here comes your SEO Magento robots.txt:

# Crawlers Setup
User-agent: *

# Directories
Disallow: /app/
Disallow: /cgi-bin/
Disallow: /downloader/
Disallow: /includes/
Disallow: /lib/
Disallow: /pkginfo/
Disallow: /report/
Disallow: /shell/
Disallow: /var/

# Paths (clean URLs)
Disallow: /index.php/
Disallow: /catalog/product_compare/
Disallow: /catalog/category/view/
Disallow: /catalog/product/view/
Disallow: /catalogsearch/
#Disallow: /checkout/
Disallow: /control/
Disallow: /contacts/
Disallow: /customer/
Disallow: /customize/
Disallow: /newsletter/
Disallow: /poll/
Disallow: /review/
Disallow: /sendfriend/
Disallow: /tag/
Disallow: /wishlist/
Disallow: /catalog/product/gallery/

# Misc. files you don’t want search engines to crawl
Disallow: /cron.php
Disallow: /cron.sh
Disallow: /composer.json
Disallow: /LICENSE.html
Disallow: /LICENSE.txt
Disallow: /LICENSE_AFL.txt
Disallow: /STATUS.txt
Disallow: /mage
#Disallow: /modman
#Disallow: /n98-magerun.phar
Disallow: /scheduler_cron.sh
Disallow: /*.php$

# Disallow filter urls
Disallow: /*?min*
Disallow: /*?max*
Disallow: /*?q*
Disallow: /*?cat*
Disallow: /*?manufacturer_list*
Disallow: /*?tx_indexedsearch

Feel free to leave comments below for additional remarks and suggestions for improvement.

Posted on Leave a comment

Updating Magento tax rules causes code already exists error

Magento Logo

When trying to update tax rules in Magento that have been imported by extensions such as firegento magesetup you might run into problems related to duplicate tax rule codes, e.g. “20% VAT” for both customers and retailers.

By default, Magento happily accepts duplicate tax rule codes when importing them trough setup scripts. But when trying to update these tax rules through adminhtml it will fail due to model checks with a “code already exists” error, as shown below:

Magento tax rule code already exists

The easiest solution here is to manually set the tax rule codes in your table tax_calculation_rule. Simply edit the code column there and you are all set. Happy tax’ing!

Posted on Leave a comment

New table column not updating in Magento

Magento Logo

So you’ve added a custom column for a table, e.g. admin_user or through your custom extension’s model table. The only problem is your values are not being stored by your underlying model classes, i.e. abstractions of Mage_Core_Model_Abstract. But why you ask since Magento is supposed to automatically infer database table to model relations through Zend_Db and magic getters and setters (simply put).

Clear that cache

Well, in fact the solution is pretty simple:

Zend_Db caches the structure of the database tables.

Thus, if you add new columns make sure to refresh the cache. Zend will then update the cache accordingly.

Also note that even if you all your caches are disabled the database structure and other info may still be cached in var/cache, so:

Clear /var/cache.

to make sure that your table adaptions reflect properly.

Posted on 3 Comments

Customize surcharge price logic for Phoenix Cash on Delivery Magento extension

Magento Logo

A recent customer had the requirement to calculate cash on delivery surcharges in relation to the shipping costs. For instance, let’s say that shipping to Germany costs EUR 8 whereas customers from Austria are charged only EUR 5, unless the total order reaches a certain amount resulting in free shipping.

Now the requirement in this case was that shipping costs and cash on delivery surcharges should in total be EUR 12 for shipments to Germany and EUR 8 for shipments to Austria. So the cash on delivery surcharge has to be calculated in relation to the shipping costs selected to match the desired total cash on delivery surcharge + shipping costs.

Using Phoenix Cash on Delivery extension enables us to set fixed or percentage surcharge values depending in inland and foreign shipments. But these settings do not contain special logic for shipping methods and their respective costs. The extension by default only allows us to disable certain shipping methods when using cash on delivery and limit the list of allowed countries, which in general is sufficient enough but not in our particular case.

So, using the vanilla version of the Phoenix Cash on delivery extension would in the example described above result in duplicate costs (shipping + cash on delivery surcharge) for customers during the checkout process, i.e. EUR 8 for shipping + EUR 12 cash on delivery surcharge for German customers, where in fact we only want to charge German customers EUR 12 in total when cash on delivery is selected as payment method and in total EUR 8 for Austrian customers.

Customize Phoenix Cash on Delivery extension

Having a look at the Phoenix Cash on Delivery extension quickly reveals that for our price logic to work we need to customize the price calculcation in Phoenix_CashOnDelivery_Model_CashOnDelivery and especially the functions getInlandCosts and getForeignCountryCosts.

First, copy app/code/community/Phoenix/CashonDelivery/Model/CashOnDelivery.php to app/code/local/Phoenix/CashonDelivery/Model/CashOnDelivery.php to overwrite the model class in the local code pool.

Note that this should be done using a custom module. The example here only serves as demonstration of the required code pieces.

Next, add the function getConditionalCosts in your local CashOnDelivery.php file:

public function getConditionalCost($baseCost) {
  $quote = Mage::getModel('checkout/session')->getQuote();
  if ($quote) {
    if ($quote->getShippingAddress()) {
      // check customer shipping address country
      $shippingData = $quote->getShippingAddress()->getData();

      if (isset($shippingData['country_id'])) {
        if ($shippingData['country_id'] === 'DE') {
          $baseCost; // do some calculation here
        } else if ($shippingData['country_id'] === 'AT') {
          if (floatval($quote->getGrandTotal()) < 70) {
            $baseCost = // do some calculation here
          }
        }
      }
    }
  }

  return floatval($baseCost);
}

And to actually integrate the custom logic by adding the required calls to getConditionalCost in getInlandCosts and getForeignCountryCosts:

public function getInlandCosts($address = null) {

  $inlandCost = $this->getConfigData('inlandcosts');
  $minInlandCost = $this->getConfigData('minimum_inlandcosts');

  if (is_object($address) && Mage::getStoreConfigFlag(self::XML_CONFIG_PATH_CASHONDELIVERY_COST_TYPE)) {
    $calcBase = $this->getConfigData('cost_calc_base');
    $inlandCost = ($address->getData($calcBase) / 100) * $inlandCost;
    if ($inlandCost < $minInlandCost) {
      $inlandCost = $minInlandCost;
    }
  }

  return $this->getConditionalCost($inlandCost);

  //return floatval($inlandCost);
}

public function getForeignCountryCosts($address = null) {
  $foreignCost = $this->getConfigData('foreigncountrycosts');
  $minForeignCost = $this->getConfigData('minimum_foreigncountrycosts');

  if (is_object($address) && Mage::getStoreConfigFlag(self::XML_CONFIG_PATH_CASHONDELIVERY_COST_TYPE)) {
    $calcBase = $this->getConfigData('cost_calc_base');
    $foreignCost = ($address->getData($calcBase) / 100) * $foreignCost;
    if ($foreignCost < $minForeignCost) {
      $foreignCost = $minForeignCost;
    }
  }

  return $this->getConditionalCost($foreignCost);

  //return floatval($foreignCost);
}

Possible optimizations

Note that with a custom module can easily administrate the price values using by getConditionalCost through the admin backend. As always use system.xml to add your backend options. In addition, make sure to rewrite the model through config.xml. But I’ll leave that to you as an additional excercise 😉

 

Posted on Leave a comment

Fix “Unknown column SUM(IFNULL…” in Magento

Magento Logo

This is just a quick fix post in case you are experiencing the exception “Unknown column ‘SUM((IFNULL” when trying to log in to the Magento admin backend. Below you find the full exception stack trace for reference:

a:5:{i:0;s:1073:"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'SUM((IFNULL(main_table.base_total_invoiced, 0) - IFNULL(main_table.base_tax_invoiced, 0) - IFNULL(main_table.base_shipping_invoiced, 0) - (IFNULL(main_table.base_total_refunded, 0) - IFNULL(ma' in 'field list', query was: SELECT `SUM((IFNULL(main_table.base_total_invoiced, 0) - IFNULL(main_table.base_tax_invoiced, 0) - IFNULL(main_table.base_shipping_invoiced, 0) - (IFNULL(main_table.base_total_refunded, 0) - IFNULL(main_table.base_tax_refunded, 0) - IFNULL(main_table.base_shipping_refunded, 0))) * main_table`.`base_to_global_rate)` AS `lifetime`, `AVG((IFNULL(main_table.base_total_invoiced, 0) - IFNULL(main_table.base_tax_invoiced, 0) - IFNULL(main_table.base_shipping_invoiced, 0) - (IFNULL(main_table.base_total_refunded, 0) - IFNULL(main_table.base_tax_refunded, 0) - IFNULL(main_table.base_shipping_refunded, 0))) * main_table`.`base_to_global_rate)` AS `average` FROM `sales_flat_order` AS `main_table` WHERE (main_table.status NOT IN('canceled')) AND (main_table.state NOT IN('new', 'pending_payment'))";i:1;s:4503:"#0 lib/Varien/Db/Statement/Pdo/Mysql.php(110): Zend_Db_Statement_Pdo->_execute(Array)
#1 app/code/core/Zend/Db/Statement.php(291): Varien_Db_Statement_Pdo_Mysql->_execute(Array)
#2 lib/Zend/Db/Adapter/Abstract.php(480): Zend_Db_Statement->execute(Array)
#3 lib/Zend/Db/Adapter/Pdo/Abstract.php(238): Zend_Db_Adapter_Abstract->query('SELECT `SUM((IF...', Array)
#4 lib/Varien/Db/Adapter/Pdo/Mysql.php(504): Zend_Db_Adapter_Pdo_Abstract->query('SELECT `SUM((IF...', Array)
#5 lib/Zend/Db/Adapter/Abstract.php(737): Varien_Db_Adapter_Pdo_Mysql->query('SELECT `SUM((IF...', Array)
#6 lib/Varien/Data/Collection/Db.php(734): Zend_Db_Adapter_Abstract->fetchAll('SELECT `SUM((IF...', Array)
#7 app/code/core/Mage/Core/Model/Resource/Db/Collection/Abstract.php(521): Varien_Data_Collection_Db->_fetchAll('SELECT `SUM((IF...', Array)
#8 lib/Varien/Data/Collection/Db.php(566): Mage_Core_Model_Resource_Db_Collection_Abstract->getData()
#9 app/code/core/Mage/Adminhtml/Block/Dashboard/Sales.php(65): Varien_Data_Collection_Db->load()
#10 app/code/core/Mage/Core/Block/Abstract.php(293): Mage_Adminhtml_Block_Dashboard_Sales->_prepareLayout()
#11 app/code/core/Mage/Core/Model/Layout.php(456): Mage_Core_Block_Abstract->setLayout(Object(Mage_Core_Model_Layout))
#12 app/code/core/Mage/Adminhtml/Block/Dashboard.php(54): Mage_Core_Model_Layout->createBlock('adminhtml/dashb...')
#13 app/code/core/Mage/Core/Block/Abstract.php(293): Mage_Adminhtml_Block_Dashboard->_prepareLayout()
#14 app/code/core/Mage/Core/Model/Layout.php(456): Mage_Core_Block_Abstract->setLayout(Object(Mage_Core_Model_Layout))
#15 app/code/core/Mage/Core/Model/Layout.php(472): Mage_Core_Model_Layout->createBlock('adminhtml/dashb...', 'dashboard')
#16 app/code/core/Mage/Core/Model/Layout.php(239): Mage_Core_Model_Layout->addBlock('adminhtml/dashb...', 'dashboard')
#17 app/code/core/Mage/Core/Model/Layout.php(205): Mage_Core_Model_Layout->_generateBlock(Object(Mage_Core_Model_Layout_Element), Object(Mage_Core_Model_Layout_Element))
#18 app/code/core/Mage/Core/Model/Layout.php(210): Mage_Core_Model_Layout->generateBlocks(Object(Mage_Core_Model_Layout_Element))
#19 app/code/core/Mage/Core/Controller/Varien/Action.php(344): Mage_Core_Model_Layout->generateBlocks()
#20 app/code/core/Mage/Core/Controller/Varien/Action.php(269): Mage_Core_Controller_Varien_Action->generateLayoutBlocks()
#21 app/code/core/Mage/Adminhtml/Controller/Action.php(275): Mage_Core_Controller_Varien_Action->loadLayout(NULL, true, true)
#22 app/code/core/Mage/Adminhtml/controllers/DashboardController.php(40): Mage_Adminhtml_Controller_Action->loadLayout()
#23 app/code/core/Mage/Core/Controller/Varien/Action.php(418): Mage_Adminhtml_DashboardController->indexAction()
#24 app/code/core/Mage/Core/Controller/Varien/Router/Standard.php(250): Mage_Core_Controller_Varien_Action->dispatch('index')
#25 app/code/core/Mage/Core/Controller/Varien/Front.php(172): Mage_Core_Controller_Varien_Router_Standard->match(Object(Mage_Core_Controller_Request_Http))
#26 app/code/core/Mage/Core/Model/App.php(354): Mage_Core_Controller_Varien_Front->dispatch()
#27 app/Mage.php(684): Mage_Core_Model_App->run(Array)
#28 index.php(87): Mage::run('', 'store')
#29 {main}";s:3:"url";s:70:"/index.php/admin/dashboard/index/key/78e1f84097adc7e831f279f919f0d1a6/";s:11:"script_name";s:10:"/index.php";s:4:"skin";s:5:"admin";}

Check Zend Library version

The solution in this case is pretty easy. Make sure that your Zend library conforms to your Magento version. It’s easiest to just remove the lib/Zend folder and use a fresh Zend library version corresponding to your Magento version. As always, make sure to clear var/cache afterwards. This has been tested in Magento CE 1.9+.

Posted on 4 Comments

Fix Anaraky Google Dynamic Remarkting Tag extension

Magento Logo

By default, Anaraky Google Dynamic Remarkting Tag (“Anaraky GDRT”) does not set correct page type “other“, as described in Google’s Dynamic Remarketing guidelines. Instead, it uses “siteview“. To overcome this behavior there are only 2 simple modifications required.

Required modifications for Anaraky Gdrt extension

First, in

app/code/community/Anaraky/Gdrt/Block/Script.php on line 51 replace siteview by other:

$params = array('ecomm_pagetype' => 'siteview');

becomes

$params = array('ecomm_pagetype' => 'other');

Second, in the same file on line 104 replace

$params = array( 'ecomm_pagetype' => 'siteview' );

with

$params = array( 'ecomm_pagetype' => 'other' );

Hint on using Anaraky custom page setup

Furthermore, when using the custom page setup options make sure to omit the trailing slash when only specifying module/controller setup, e.g. checkout/cart as shown below:

Anaraky Gdrt Magento Extension

Anaraky Google Dynamic Remarkting Tag does string comparison check without the trailing slash in the observer class:

foreach ($gdrtPages as $k =&gt; $v) {
  $v = rtrim($v, '/');
  if ($mName . '/' . $cName . '/' . $aName == $v ||
    $mName . '/' . $cName == $v)
  {
    $pageType = $k;
  }
}

Handling special prices and grouped/bundle products for “ecomm_totalvalue”

In case you run into problem when displaying special prices or handling grouped or bundle products in the ecomm_totalvalue field here is a quick fix to add this check. Add the following function in Script.php:

private function _getProductPrice($product) {

 $totalvalue = 0;

 // check if we are handling grouped products
 if($product->getTypeId() == 'grouped') {
 $groupedSimpleProducts = $product->getTypeInstance(true)->getAssociatedProducts($product);

 $groupedPrices = array();

 foreach($groupedSimpleProducts as $gSimpleProduct) { 
 $groupedPrices[] = $this->_getProductPrice($gSimpleProduct);
 }

 $totalvalue = min($groupedPrices);

 } else { // handle other product types
 $_price = Mage::helper('tax')->getPrice($product, $product->getPrice(), $inclTax);
 $_specialPrice = Mage::helper('tax')->getPrice($product, $product->getSpecialPrice(), $inclTax);
 $_finalPrice = Mage::helper('tax')->getPrice($product, $product->getFinalPrice(), $inclTax);

 if($_price == $_finalPrice) { // no special price
 $totalvalue = (float)$_price;
 } else { // get special price
 if((float)$_finalPrice > 0 && (float)$_finalPrice <= (float)$_price) {
 $totalvalue = (float)$_finalPrice;
 } else {
 $totalvalue = (float)$_price;
 }
 }
 } 

 return $totalvalue;
 }

Second, change the “product” case like so:


case 'product':
$product = Mage::registry('current_product');

$totalvalue = $this->_getProductPrice($product);

$params = array(
'ecomm_prodid' => $this->getEcommProdid($product),
'ecomm_pagetype' => 'product',
'ecomm_totalvalue' => (float)number_format($totalvalue, '2', '.', '')
);
unset($product);
break;

Don’t forget to flush the cache afterwards.

Posted on 4 Comments

Set tier prices for product variants of configurable products in Magento

Magento Logo

So you want to set tier prices for product variants of configurable products in Magento. To achieve this you create a configurable product and associate your simple product variants based on your pre-defined attributes. At this point you start setting tier prices for your associated product variants, i.e. your simple products thinking that Magento will use these values on the frontend. Well, unfortunately in fact Magento by default only uses the tier prices set for the configurable product itself which obviously results in wrong prices in the frontend.

Surcharge to the rescue?

Ok, well let’s then use surcharges for product variants as basis for tier price calculations of product variants. Unfortunately, this also will not work since price calculations of tier prices for product variants only take the parent configurable product into consideration.

A concrete example

Let’s back up a little and have a look at the problem at hand with a concrete example. Imagine you want to sell t-shirts in different colors (green, blue, orange). For each color you want to set different base prices and in addition different tier prices. Have a look at the following table for clarification:

T-shirt 1 item 3 items 12 items 24 items
green 10 8 7 6
orange 11 10 9 8
blue 12 11 10 9

Prices are in EUR, e.g. 3 orange t-shirts cost 10 EUR each, whereas 3 blue t-shirts cost 11 EUR each. Again, by default Magento will use the tier price set for the product configurable product, thus ignoring tier prices set for product variants.

Magento extensions to use tier prices of product variants

This shortcoming of Magento’s default tier price handling for configurable products lead to the development of a handful of helpful extensions. One of the first ones on the market was Simple Configurable Producs (SCP), followed by the more advanced Better Configurable Products (BCP). Simple Configurable Products officially is not supported for the latest Magento 1.9.x series anymore. In fact, according to Magento Connect official support ended with Magento 1.5, although various people reported success with Magento 1.8 too.

Thus, you might want to use Better Configurable Products instead which is officially supported for the latest Magento EE and CE editions. Nevertheless, in case you want to save a little Configurable Products use Simple Price is cheaper but does the job just as well. No, I’m not affiliated with Configurable Products use Simple Price in any way. But after spending some time testing various extensions capable of using tier prices of product variants instead of the parent’s configurable product I’ve opted for Configurable Products use Simple Price for its price and functionality. Compared to Better Configurable Products Configurable Products use Simple Price is pretty simple with regards to the feature list.

To round up, in case you are just searching for a quick way to set tier prices for product variants of configurable products in Magento Configurable Products use Simple Price offers a quick and cost effective solution.

Grouped Products as alternative

Grouped products can also be used as alternative to the above mentioned extensions to correctly calculate and display tier prices in Magento. Of course, only if your product setup matches the required configuration for grouped products. For grouped products, tier prices are correctly calculated based on the values of the underlying simple products.

You can find a lot of information on the web concerning this issue in case you want to continue your search for alternative solutions. Hopefully, some day setting tier prices for configurable products correctly will become an integral part of Magento so we are not dependent on additional extensions for this (core) functionality.

Posted on 5 Comments

Solve “Unable to find a sitemap to generate” in Magento

Magento Logo

In case you receive the error message

Unable to find a sitemap to generate

when trying to generate a Google Sitemap for an existing entry in Magento make sure to check that the sitemap ID generated is set correctly, i.e. not equal to 0 (zero).

In this particular Magento 1.9.1.0 setup the ID for the first sitemap wrongly was set to 0 on generation via Catalog / Google Sitemap, thus breaking the sitemap generation process through the Generate option: magento-google-sitemap-id-0-unable-to-find-sitemap-to-generate

Correct Sitemap ID in Database

Manually setting the sitemap ID in the database to 1 for the first sitemap solved this problem:

Magento Google Sitemap ID 1 correct

 

Magento Google Sitemap generated successfully

 

 

Still, it’s strange that no error message was displayed when creating the first sitemap entry through Catalog / Google Sitemap.

Has anybody else experienced this behavior?

Posted on 9 Comments

Run Magento Data Flow Profile from shell

Magento Logo

Running Magento data flow profiles for importing/exporting data is quite resource intensive and can take a while to execute for larger junks of data. Although there exist alternative ways to import/export data in Magento sometimes existing data flow profiles can’t be replaced just yet. Below you find a shell script to run Magento data flow profiles from the shell based on the data flow profile id. You can execute this script using PHP or PHP-CI, thus being able to specify different resource limits for your PHP setups (e.g. time outs)

Create data flow profile

Go ahead and create a data flow profile as usual through Magento’s admin interface and take note of the profile id (13) in the URL: Magento data flow profile URL You will use this profile ID to run the data flow profile via the shell script provided below.

Run data flow profile via shell script

In order to execute the script specify the data flow profile id using the –profile switch, e.g. using PHP-CLI.


$ php5-5.4-cli ./run_data_flow_profile.php --profile 13

This will run the data flow profile with the ID 13.

Magento data flow profile shell script

Save the code below in a file called run_data_flow_profile.php in Magento’s shell folder. Use the command specified above to run the script from within your shell.

<?php

require_once 'abstract.php';

/**
 * @author Matthias Kerstner <matthias@kerstner.at>
 * @version 1.0.0
 */
class Mage_Shell_Run_Data_Flow_Profile extends Mage_Shell_Abstract {

  /** @var string this module's namespace */
  private static $_MODULE_NAMESPACE = 'kerstnerat_rundataflowprofileshell';

  /**
   * Logs $msg to logfile specified in configuration.
   * @param string $msg
   */
  private function logToFile($msg) {
    Mage::log($msg, null, self::$_MODULE_NAMESPACE . '.log');
  }

  /**
   * Run script based on shell arguments specified.
   */
  public function run() {
    try {
      if (!$this->getArg('profile')) {
        throw new Exception('Missing profile');
      }

      $profileId = (int) $this->getArg('profile');

      $this->logToFile('Profile started: ' . $profileId . ' at ' . date('Y-m-d H:i:s')
        . '...');

      $profile = Mage::getModel('dataflow/profile');
      $userModel = Mage::getModel('admin/user');
      $userModel->setUserId(0);

      Mage::getSingleton('admin/session')->setUser($userModel);
      $profile->load($profileId);

      if (!$profile->getId()) {
        $this->logToFile('error: ' . $profileId . ' - incorrect profile id');
        return;
      }

      Mage::register('current_convert_profile', $profile);
      $profile->run();

      $this->logToFile('Profile ended: ' . $profileId . ' at ' . date('Y-m-d H:i:s'));
    } catch (Exception $ex) {
      $this->logToFile($ex->getMessage());
      echo $this->usageHelp();
    }
  }

 /**
  * Retrieve Usage Help Message.
  */
 public function usageHelp() {
   return " 
Usage: php -f run_data_flow_profile.php --[options]
 
 --profile  Data Flow Profile ID
 help Show this help
 
  ID of Data Flow Profiles to run";
 }
}

$shell = new Mage_Shell_Run_Data_Flow_Profile();
$shell->run();

Compatibility

The script has been tested with Magento 1.8 and higher. If you happen to find a problem please leave a comment.

Posted on Leave a comment

Remote deployment script for Magento extensions using modman and rsync

Magento Logo

In order to have an efficient way of deploying Magento extensions to (remote) Magento setups I’ve created a simple deployment script based on modman and rsync.

How it works

This script copies your Magento extension to the .modman directory of your destination Magento project and deploys it there using modman. In addition, it exludes files and folders (such as .git) so that only relevant files are copied. This script uses rsync to copy files to your (remote) Magento project.

For example:

  1. Let’s say you develop a Magento extension in /home/my_user/workspace/MyExtension
  2. Your development Magento setup is located at /var/www/magento-dev1/
  3. This script copies your Magento extension from /home/my_user/workspace/MyExtension to /var/www/magento-dev1/.modman/ and automatically deploys it using modman.

Benefits

Although you could do the copying alone using the modman configuration file modman does not offer the possibility to exclude certain files for the deployment process. In addition, this script uses rsync, thus giving you the possibility to remotely deploy your Magento extension.

Download script

Feel free to grab the script from Github. Also, in case you have suggestions for improvements don’t hesitate to drop a comment below.

Posted on Leave a comment

Disable caching of API callbacks for Viveum Magento extension

Magento Logo

Using a caching server like Varnish is mandatory for running Magento (efficiently). When using extensions that make use of callback API calls make sure that you exclude them from your caching rules.

As we are using Viveum as payment gateway service for most eCommerce projects we generally deploy the official Viveum Magento extension. When using this extension in combination with Varnish make sure that at least exclude the following API calls from your caching list, e.g. through Turpentine URL Blacklist:

ops/payment/
ops/api/

We experienced problems especially related to Paypal transactions without excluding these callbacks, including wrong customer information on the Paypal checkout page.

Posted on 4 Comments

Mass import products to Magento multi-store setup

Magento Logo

So, you are about to import a large amount of products into Magento. Well, there are a couple of different approaches available. First, you could use the ancient Data Flow import based on specific profiles. Then, you could use the improved Data Import version, which still is (very) resource consuming. Lastly, there is magmi – a highly efficient tool to mass import data to Magento, which is the preferred way to go in almost all cases when dealing with larger sets of product data in Magento. This post shows how to mass import products to Magento multi-store setups using magmi.

Export products

First, you need to export existing products as a CSV file. This can be achieved (again) using Data Flow, Data Import or a more elaborate database dump. Let’s assume at this point that you have a CSV file ready (very convenient, I know). In case you don’t, simply run the built-in Data Export funtion from within the admin backend (specify only those columns you definitely need to keep the processing to a minimum) or use the default Export All Products Data Flow profile. Edit this CSV to your needs so that you have a working set of products to be imported afterwards. Make sure at this point to retain the required columns so that you don’t run into problems afterwards. The columns required for the import process depend on the scenario at hand but rest assured you need at least sku, store and any additional attribute such as name or description.

Export data manipulation tools

Make sure to edit your CSV file with a tool that is capable of handling UTF-8 correctly and also supports setting proper delimiters. LibreOffice is a very handy solution to do this. You can easily use the Save as functionality to set proper delimiters for magmi to import your edited CSV afterwards. Use comma (,) and as column separators, which is the default setting. Here is a working sample CSV extract with the respective required columns in the first row:

"sku","description","short_description","name","store"
"sample-1","My shiny product for Englisch store","Shiny product","Product A EN","en"
"sample-1","My shiny product for German store","Shiny product","Product A DE","de"

CSV import schema

In order to mass import products for particular store views in a multi store setup make sure that the store column is set correctly with the store view code. Have a look at core_store table or at the admin backend under System / Manage Stores to determine to correct store view code.

Note: The regular Data Import interface provided by Magento will activate the “Use default” setting on various attributes of the imported/updated products, thus potentially doing something you are not intending with importing/updating products! Instead, use magmi and the store column to explicitely set set which products to import into which store view.

Note that you can also specify a list of store codes for the store column. The fastest and (imho) safest way to mass import products to Magento is Magmi – the Magento Mass Importer. Next, we are going to setup magmi and set up a profile to mass import products.

Setup Magmi

First, download and install magmi and protect it from outside access (e.g. via .htaccess). Note that by default, the web interface will be accessible by everyone who knows the corresponding URL! Open the web interface: http://www.your-domain/magmi/web/magmi.php and

  1. Edit the global settings
  2. add and edit the import profile

Edit global configuration

Set your connection details and Magento version at hand: MAGMI Global configuration

Create an import profile

Next, setup the import profile based on the Default profile and specify CSV as data source: MAGMI Profile configuration

Make sure to enable the Magmi Optimizer as this will speed up the process significantly, especially when dealing with thousands of entries.

Run import

Finally, choose your CSV import file and set the import mode. You can choose between three different modes:

  1. Update existing items only, skip new ones
  2. Create new items, update existing ones
  3. create new items, skip existing ones

Again, for multi-store setups make sure that the store column contains the correct store code view code. The import process should run pretty fast using magmi.

Reindex data

Optionally, you can choose to kick off the reindexing process using magmi (enable the option Magento Magmi Reindexer), run the indexer with the shell script provided by Magento or use the corresponding admin backend option.