Browser language auto-detection in Magento

By default, Magento shows the store that has been configured «by default». Adding browser language auto-detection is pretty easy editing the Magento index.php file. We just need to check what’s the user browser main language and then try to load the store that uses that code.

It’s important to note that maybe the user has already selected a preferred language before. In that case, we just check that the variable $_SERVER['MAGE_RUN_CODE'] is set.

Make a copy of your old-fashioned index.php file a create a new one with the following code.

  • Note the variable $default_language_code, which sets the default language code when the detected user browser language code doesn’t match with any of our store codes)
  • Note also that this code is compatible with Magento 1.6+ and 1.11+ (you can try using it for older versions, it will probably work).
  • Last but not least, I’ve taken the main getLanguageCode() from the Magento Wiki. I just modified it in order to play with codes instead of stores.
<?php
/**
 * Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is bundled with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://opensource.org/licenses/osl-3.0.php
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@magentocommerce.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Magento to newer
 * versions in the future. If you wish to customize Magento for your
 * needs please refer to http://www.magentocommerce.com for more information.
 *
 * @category   Mage
 * @package    Mage
 * @copyright  Copyright (c) 2008 Irubin Consulting Inc. DBA Varien (http://www.varien.com)
 * @license    http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 */

if (version_compare(phpversion(), '5.2.0', '<')===true) {
    echo  '<div style="font:12px/1.35em arial, helvetica, sans-serif;"><div style="margin:0 0 25px 0; border-bottom:1px solid #ccc;"><h3 style="margin:0; font-size:1.7em; font-weight:normal; text-transform:none; text-align:left; color:#2f2f2f;">Whoops, it looks like you have an invalid PHP version.</h3></div><p>Magento supports PHP 5.2.0 or newer. <a href="http://www.magentocommerce.com/install" target="">Find out</a> how to install</a> Magento using PHP-CGI as a work-around.</p></div>';
    exit;
}

/**
 * Error reporting
 */
error_reporting(E_ALL | E_STRICT);

/**
 * Compilation includes configuration file
 */
$compilerConfig = 'includes/config.php';
if (file_exists($compilerConfig)) {
    include $compilerConfig;
}

$mageFilename = 'app/Mage.php';
$maintenanceFile = 'maintenance.flag';

if (!file_exists($mageFilename)) {
    if (is_dir('downloader')) {
        header("Location: downloader");
    } else {
        echo $mageFilename." was not found";
    }
    exit;
}

if (file_exists($maintenanceFile)) {
    include_once dirname(__FILE__) . '/errors/503.php';
    exit;
}

require_once $mageFilename;

#Varien_Profiler::enable();

if (isset($_SERVER['MAGE_IS_DEVELOPER_MODE'])) {
    Mage::setIsDeveloperMode(true);
}

#ini_set('display_errors', 1);

umask(0);

/* Language detection */
$default_language_code = 'en';
function getLanguageCode()
{
    if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
        foreach (explode(",", strtolower($_SERVER['HTTP_ACCEPT_LANGUAGE'])) as $accept) {
            if (preg_match("!([a-z-]+)(;q=([0-9.]+))?!", trim($accept), $found)) {
                $langs[] = $found[1];
                $quality[] = (isset($found[3]) ? (float) $found[3] : 1.0);
            }
        }
        // Order the codes by quality
        array_multisort($quality, SORT_NUMERIC, SORT_DESC, $langs);
        // get list of stores and use the store code for the key
        $stores = Mage::app()->getStores(false, true);
        // iterate through languages found in the accept-language header
        foreach ($langs as $lang) {
            $lang = substr($lang,0,2);
            if (isset($stores[$lang]) && $stores[$lang]->getIsActive()) 
                return $lang;
        }
    }
    return $default_language_code;
}

/* Store or website code */
if(isset($_SERVER['MAGE_RUN_CODE']))
    $mageRunCode = $_SERVER['MAGE_RUN_CODE'];
else
    $mageRunCode = getLanguageCode();

/* Run store or run website */
$mageRunType = isset($_SERVER['MAGE_RUN_TYPE']) ? $_SERVER['MAGE_RUN_TYPE'] : 'store';

Mage::run($mageRunCode, $mageRunType);

Solr: 413 Status: FULL head and Magento (hits but no results)

A few days ago we tried to install and set Solr as a Magento default search system. We run into a weird problem: once we checked that Solr was working we enabled it in Magento. Everything seemed to work well: when reindexing products the log would show the changes and the button for testing whether Solr was connected with Magento said that it all was OK.

But when searching for products in Magento there were no results. The point was that what we got was a message saying that there were no results, but just above, there was a pagination and the number of results, that actually were greater than 0.

The Problem

After debugging a while, we found that there was an exception thrown by Solr: 413 Status: FULL head

Once we found out what the exception was, it was very easy to know what to do: we needed to increment the headerBufferSize parameter value in the jetty.xml file. Given that that paramenter wasn’t on our etc/jetty.xml file, we had to add it:

<!-- This connector is currently being used for Solr because it
showed better performance than nio.SelectChannelConnector
for typical Solr requests.  -->
<Call name="addConnector">
 <Arg>
  <New>
   <Set name="host"><SystemProperty name="jetty.host" /></Set>
   <Set name="port"><SystemProperty name="jetty.port" default="8983"/></Set>
   <Set name="maxIdleTime">50000</Set>
   <Set name="lowResourceMaxIdleTime">1500</Set>
   <Set name="statsOn">false</Set>
   <Set name="headerBufferSize">36384</Set>
  </New>
 </Arg>
</Call>

After changing this we started getting results in Magento. It’s important to note that when using the solr interface (from http://localhost:8983/solr/admin) it always worked perfectly.

Magento Professional Edition no longer exists

February 1st: Magento announces that Magento Professional edition will disappear (they’ll phase it out, they said). This means that people who have recently bought the professional edition have been left hanging.

In our case, we have 3 clients that have recently paid for a Magento PE license and now we have to explain to them that they won’t get any updates unless they upgrade to Magento Enterprise edition. For most of them that won’t be an option: more than 10.000€/year is too much money compared to the 3.000 – 4000€ that the Professional Edition would cost.

I think that that’s been a really bad decision. Some clients look for professional support and they don’t mind paying some money for it. The thing is that they don’t want something called «community», even if the differences with the professional edition are minimal.

So What happens now?

Now you have to decide whether not to pay and use the community edition or pay a huge amount of money for the enterprise edition (and there is an even much more expensive edition…). Small companies that can’t afford that amount of money are going to use the «community edition», but they’re going to use it without being sure if it’s the best option for their e-shops. The truth is that «free stuff» means «buggy or non reliable stuff», even though that’s not true at all. On the other hand, the Magento Professional edition had no real improvements compared to the Community one. Furthermore, the Enterprise Edition has a lot of interesting features, but I’m not sure it’s worth it given that it’s possible to implement all those features using some [paid] extensions.

In our particular case, we’ll probably lose one of those 3 clients as a consequence of this policy change. It’s a very «special» client and it’s not a serious thing, but now we have to ask the other clients whether they want to pay more than 10K€ or not pay anything and use the non reliable «community edition» (non reliable for them, of course).

In conclusion, Magento’s decision is going to affect our commercial way of selling Magento. Maybe this is going to help making the «community» edition a more trustworthy edition.

Programmatically update product price per website in Magento

If you want to programmatically update product prices per website (this means that you have many websites and have set a website scope for price in the configuration), then you can use this script:

public function updateProductPrices ($sku, $newPrice) {
    Mage::app()->setCurrentStore(Mage_Core_Model_App::ADMIN_STORE_ID);

    $websites = Mage::app()->getWebsites();

    $product = Mage::getModel('catalog/product')
    $productId = $product->getIdBySku($sku);

    foreach ($websites as $_eachWebsite) {
        $_websiteId = $_eachWebsite->getWebsiteId();

        $websiteObj = new Mage_Core_Model_Website();
        $websiteObj->load($_websiteId);

        $storeIds = $websiteObj->getStoreIds();

        if (count($storeIds)) {
            foreach ($storeIds as $_eachStoreId) {
                $product->setStoreId($_eachStoreId)
                        ->load($productId);

                $oldPrice = $product->getPrice();

                if ($oldPrice != $newPrice) {
                    $product->setPrice($newPrice);
                    $product->save();
                }
            }
        }

        unset($storeIds, $websiteObj, $_websiteId);
    }

    unset($product);
}

Important note: I haven’t developed this, but I think it’s interesting to note it down. Thanks to Knowledge Craving from Stackoverflow.

Adding configurable product options to category list in Magento

At this moment we’re working on 3 projects, all of them require a category list with products that are only configurable and the customer has to be able to choose the options from the configurable and add it directly to the cart without going to the product page.

Step by Step

How can we add all this to the product category list? Sadly it’s not that easy.

  • The configurable.js only works with one product at a time, so it won’t work
  • The configurable options block can’t be called either: we need a specific block for each product on the category list

Using the previous work from the Inchoo blog I want to detail the procedure.

To solve our problem we need to do the following steps. Note that Inchoo people shared with the world their specific configurable js file for this case.

Step 1

Create a template called configurable_category.phtml and put it on the template/catalog/product/view/type folder of your theme (create the folder if it doesn’t exist). Put the following code in that file:

<?php
$_product    = $this->getProduct();
$_attributes = Mage::helper('core')->decorateArray($this->getAllowAttributes());
?>

<?php if ($_product->isSaleable() && count($_attributes)):?>
<dl>
    <?php foreach($_attributes as $_attribute): ?>
    <dt><label><em>*</em><?php echo $_attribute->getLabel() ?></label></dt>
    <dd<?php if ($_attribute->decoratedIsLast){?><?php }?>>
        <div>
        <select name="super_attribute[<?php echo $_attribute->getAttributeId() ?>]" id="attribute<?php echo $_attribute->getAttributeId() ?>" class="required-entry super-attribute-select_<?php echo $_product->getId()?>">
        <option><?php echo $this->__('Choose an Option...') ?></option>
        </select>
        </div>
    </dd>
    <?php endforeach; ?>
</dl>

<script type="text/javascript">
    var spConfig_<?php echo $_product->getId()?> = new Inchoo_Product.Config(<?php echo $this->getJsonConfig() ?>);
</script>
<?php endif;?>

The differences with the original configurable.phtml file and this one are that now we create a different spConfig object for each product. This object contains all the product configurable options. We also add the class super-attribute-select with the product id at the end.

Step 2

Now create the configurable_list.js file that is going to handle and print the options on the select. Create this file in the js directory (we’ll place it directly in /js):

if(typeof Inchoo_Product =='undefined') {
    var Inchoo_Product  = {};
}

/**************************** CONFIGURABLE PRODUCT **************************/
Inchoo_Product.Config = Class.create();
Inchoo_Product.Config.prototype = {
    initialize: function(config){
        this.config     = config;
        this.taxConfig  = this.config.taxConfig;
        var settingsClassToSelect = '.super-attribute-select_'+this.config.productId;
        this.settings   = $$(settingsClassToSelect);
        this.state      = new Hash();
        this.priceTemplate = new Template(this.config.template);
        this.prices     = config.prices;

        this.settings.each(function(element){
            Event.observe(element, 'change', this.configure.bind(this))
        }.bind(this));

        // fill state
        this.settings.each(function(element){
            var attributeId = element.id.replace(/[a-z]*/, '');
            attributeId = attributeId.replace(/_.*/, '');
            if(attributeId && this.config.attributes[attributeId]) {
                element.config = this.config.attributes[attributeId];
                element.attributeId = attributeId;
                this.state[attributeId] = false;
            }
        }.bind(this))

        // Init settings dropdown
        var childSettings = [];
        for(var i=this.settings.length-1;i>=0;i--){
            var prevSetting = this.settings[i-1] ? this.settings[i-1] : false;
            var nextSetting = this.settings[i+1] ? this.settings[i+1] : false;
            if(i==0){
                this.fillSelect(this.settings[i])
            }
            else {
                this.settings[i].disabled=true;
            }
            $(this.settings[i]).childSettings = childSettings.clone();
            $(this.settings[i]).prevSetting   = prevSetting;
            $(this.settings[i]).nextSetting   = nextSetting;
            childSettings.push(this.settings[i]);
        }

        // Set default values - from config and overwrite them by url values
        if (config.defaultValues) {
            this.values = config.defaultValues;
        }

        var separatorIndex = window.location.href.indexOf('#');
        if (separatorIndex != -1) {
            var paramsStr = window.location.href.substr(separatorIndex+1);
            var urlValues = paramsStr.toQueryParams();
            if (!this.values) {
                this.values = {};
            }
            for (var i in urlValues) {
                this.values[i] = urlValues[i];
            }
        }

        this.configureForValues();
        document.observe("dom:loaded", this.configureForValues.bind(this));
    },

    configureForValues: function () {
        if (this.values) {
            this.settings.each(function(element){
                var attributeId = element.attributeId;
                element.value = (typeof(this.values[attributeId]) == 'undefined')? '' : this.values[attributeId];
                this.configureElement(element);
            }.bind(this));
        }
    },

    configure: function(event){
        var element = Event.element(event);
        this.configureElement(element);
    },

    configureElement : function(element) {
        this.reloadOptionLabels(element);
        if(element.value){
            this.state[element.config.id] = element.value;
            if(element.nextSetting){
                element.nextSetting.disabled = false;
                this.fillSelect(element.nextSetting);
                this.resetChildren(element.nextSetting);
            }
        }
        else {
            this.resetChildren(element);
        }
        //this.reloadPrice();
//      Calculator.updatePrice();
    },

    reloadOptionLabels: function(element){
        var selectedPrice;
        if(element.options[element.selectedIndex].config){
            selectedPrice = parseFloat(element.options[element.selectedIndex].config.price)
        }
        else{
            selectedPrice = 0;
        }
        for(var i=0;i<element.options.length;i++){
            if(element.options[i].config){
                element.options[i].text = this.getOptionLabel(element.options[i].config, element.options[i].config.price-selectedPrice);
            }
        }
    },

    resetChildren : function(element){
        if(element.childSettings) {
            for(var i=0;i<element.childSettings.length;i++){
                element.childSettings[i].selectedIndex = 0;
                element.childSettings[i].disabled = true;
                if(element.config){
                    this.state[element.config.id] = false;
                }
            }
        }
    },

    fillSelect: function(element){
        var attributeId = element.id.replace(/[a-z]*/, '');
        attributeId = attributeId.replace(/_.*/, '');
        var options = this.getAttributeOptions(attributeId);
        this.clearSelect(element);
        element.options[0] = new Option(this.config.chooseText, '');

        var prevConfig = false;
        if(element.prevSetting){
            prevConfig = element.prevSetting.options[element.prevSetting.selectedIndex];
        }

        if(options) {
            var index = 1;
            for(var i=0;i<options.length;i++){
                var allowedProducts = [];
                if(prevConfig) {
                    for(var j=0;j<options[i].products.length;j++){
                        if(prevConfig.config.allowedProducts
                            && prevConfig.config.allowedProducts.indexOf(options[i].products[j])>-1){
                            allowedProducts.push(options[i].products[j]);
                        }
                    }
                } else {
                    allowedProducts = options[i].products.clone();
                }

                if(allowedProducts.size()>0){
                    options[i].allowedProducts = allowedProducts;
                    element.options[index] = new Option(this.getOptionLabel(options[i], options[i].price), options[i].id);
                    element.options[index].config = options[i];
                    index++;
                }
            }
        }
    },

    getOptionLabel: function(option, price){
        var price = parseFloat(price);
        if (this.taxConfig.includeTax) {
            var tax = price / (100 + this.taxConfig.defaultTax) * this.taxConfig.defaultTax;
            var excl = price - tax;
            var incl = excl*(1+(this.taxConfig.currentTax/100));
        } else {
            var tax = price * (this.taxConfig.currentTax / 100);
            var excl = price;
            var incl = excl + tax;
        }

        if (this.taxConfig.showIncludeTax || this.taxConfig.showBothPrices) {
            price = incl;
        } else {
            price = excl;
        }

        var str = option.label;
        if(price){
            if (this.taxConfig.showBothPrices) {
                str+= ' ' + this.formatPrice(excl, true) + ' (' + this.formatPrice(price, true) + ' ' + this.taxConfig.inclTaxTitle + ')';
            } else {
                str+= ' ' + this.formatPrice(price, true);
            }
        }
        return str;
    },

    formatPrice: function(price, showSign){
        var str = '';
        price = parseFloat(price);
        if(showSign){
            if(price<0){
                str+= '-';
                price = -price;
            }
            else{
                str+= '+';
            }
        }

        var roundedPrice = (Math.round(price*100)/100).toString();

        if (this.prices && this.prices[roundedPrice]) {
            str+= this.prices[roundedPrice];
        }
        else {
            str+= this.priceTemplate.evaluate({price:price.toFixed(2)});
        }
        return str;
    },

    clearSelect: function(element){
        for(var i=element.options.length-1;i>=0;i--){
            element.remove(i);
        }
    },

    getAttributeOptions: function(attributeId){
        if(this.config.attributes[attributeId]){
            return this.config.attributes[attributeId].options;
        }
    },

    reloadPrice: function(){
        var price    = 0;
        var oldPrice = 0;
        for(var i=this.settings.length-1;i>=0;i--){
            var selected = this.settings[i].options[this.settings[i].selectedIndex];
            if(selected.config){
                price    += parseFloat(selected.config.price);
                oldPrice += parseFloat(selected.config.oldPrice);
            }
        }

        optionsPrice.changePrice('config', {'price': price, 'oldPrice': oldPrice});
        optionsPrice.reload();

        return price;

        if($('product-price-'+this.config.productId)){
            $('product-price-'+this.config.productId).innerHTML = price;
        }
        this.reloadOldPrice();
    },

    reloadOldPrice: function(){
        if ($('old-price-'+this.config.productId)) {

            var price = parseFloat(this.config.oldPrice);
            for(var i=this.settings.length-1;i>=0;i--){
                var selected = this.settings[i].options[this.settings[i].selectedIndex];
                if(selected.config){
                    price+= parseFloat(selected.config.price);
                }
            }
            if (price < 0)
                price = 0;
            price = this.formatPrice(price);

            if($('old-price-'+this.config.productId)){
                $('old-price-'+this.config.productId).innerHTML = price;
            }

        }
    }
}

Step 3

In order to load the configurable_list.js file when browsing a category we need to add it into the head block in the catalog.xml file. So now open the catalog.xml of your theme and look for the tag. There we now add the js file. Simply add the following lines inside it:

<reference name="head">
      <action method="addJs"><script>configurable_list.js</script></action>
</reference>

Step 4

At this point we have prepared our layout to handle the configurable options block. The only thing that lasts is… that configurable block. We cannot add it to the catalog.xml file, because each configurable block will be different than the previous one (and adding it through the xml layout will show always the same block with the same options). To solve this, we need to dynamically create a block in the phtml file. Simply add these lines before the add to cart button in the catalog/product/list of your theme:

<?php Mage::unregister('product') ?>
<?php Mage::register('product', $_product); ?>
<?php if ( $_product->getTypeId() == 'configurable'): ?>
    <?php echo $this->getLayout()->createBlock('catalog/product_view_type_configurable', '', array('template'=> 'catalog/product/view/type/configurable_category.phtml'))->toHtml(); ?>
<?php endif; ?>

It’s very important to «register» and «unregister» the product global object, because the configurable template gets the product from there. If we don’t do this, then the configurable block won’t be able to load the product.

Step 5

At this point, if you load your category product list, you should be the configurable product options and also select them. But the add to cart button won’t use them when adding the product to the cart. By default there is no form tag, but we need to add it to the list.phtml file, inside the foreach loop, concretly inside the div class=”actions”:

<div class="actions">
    <?php if($_product->isSaleable()): ?>
    <form action="<?php echo $this->helper('checkout/cart')->getAddUrl($_product) ?>" method="post" id="product_addtocart_form" <?php if($_product->getOptions()): ?> enctype="multipart/form-data"<?php endif; ?>>
    <?php Mage::unregister('product') ?>
    <?php Mage::register('product', $_product); ?>
     <?php if ( $_product->getTypeId() == 'configurable'): ?>

            <?php echo $this->getLayout()->createBlock('catalog/product_view_type_configurable', '', array('template'=> 'catalog/category/configurable_category.phtml'))->toHtml(); ?>
        <?php endif; ?>
        <button type="submit" title="<?php echo $this->__('Add to Cart') ?>" class="button btn-cart"><span><span><?php echo $this->__('Add to Cart') ?></span></span></button>
    <?php else: ?>
        <?php if ($_product->getIsSalable()): ?>
            <p class="availability in-stock"><span><?php echo $this->__('In stock') ?></span></p>
        <?php else: ?>
            <p class="availability out-of-stock"><span><?php echo $this->__('Out of stock') ?></span></p>
        <?php endif; ?>
    <?php endif; ?>
        <ul class="add-to-links">
            <?php if ($this->helper('wishlist')->isAllow()) : ?>
                <li><a href="<?php echo $this->helper('wishlist')->getAddUrl($_product) ?>" class="link-wishlist"><?php echo $this->__('Add to Wishlist') ?></a></li>
            <?php endif; ?>
            <?php if($_compareUrl=$this->getAddToCompareUrl($_product)): ?>
                <li><a href="<?php echo $_compareUrl ?>" class="link-compare"><?php echo $this->__('Add to Compare') ?></a></li>
            <?php endif; ?>
        </ul>
    </form>
</div>

Here the result:

Category list with configurable product options

Category list with configurable product options

Programmatically create order statuses in Magento

If you want to create new order statuses (not states) and assign them to a particular state you need to use the sales/order_status model. It’s pretty easy:

$status = Mage::getModel('sales/order_status');
$status->setStatus('status_code');
$status->setLabel('status_label');

Then you can assign the state with the assignState method:

$status->assignState('processing');
$status->save();

What’s new in Magento 1.7 alpha?

People from bloggento have written a great article about what’s new in Magento 1.7 alpha. Let’s take a look!

  • Forms Captcha

Captcha is a security system for forms that basically prevents spam. The «one» that is filling the form has to prove that he or she is a human. Captcha helps with that. Now we’ll find this option inside: System » Configuration » Client » Configuration client

Captcha options in the backoffice

Captcha options in the backoffice

  • Price per client group

From now on it’s not going to be necessary to use the tier prices feature to set prices per client group. Magento 1.7 will bring this particular option that we’ll let us easily set different prices for different client groups.

Client group price option

Client group price option

I’m not completely sure whether this is really useful. It’s clear that this is a «must», but tier prices was clear enough to set prices per client group. Maybe now is more clear this way, but it adds complexity to the admin interface.

  • Coupon generation

Magento 1.7 will bring (at last!) an automatic coupon generator. This alpha version has the option, but it seems that it’s not working properly (it’s an alpha version, so that’s normal).

  • Complete Magento Backup

Up to Magento 1.6, the only option we had in order to backup our system was to backup the database. Magento 1.7 will bring the possibility to backup all our magento data and save it in the var/backup directory:

  • Magento files
  • Database
  • Media content
New backup tool

New backup tool

  • Returns management

Now clients will be able to manage order returns in a very easy way. This is not always a seller preference, but makes this operation easier most of the times it’s needed.

Returns dialog

Returns dialog

  • Index management system has been redesigned

This is one of the most important changes, above all because index management is one of the points that provokes more headaches among programmers. Now we’ll now whether we need to update or reindex and it seems that we’ll be able to solve those primary key problems (or not, we’ll see):

New index dialog

New index dialog

  • More feautures

There are other new features:

  • Paypal: new advanced payment methods have been added (also features related to particular countries like Japan)
  • Mobile theme: has been redesigned, now it’s cooler ;)
  • DHL Europe shipping method integration
  • Layered navigation: new options for the price step filter

Conclusions

It seems that Magento 1.7 will bring a lot of new things and we can be sure that the community version is still alive. You can download it from the main website:

Magento 1.7 Alpha 1 download

How to set tier prices programmatically in Magento

Working with tier prices programmatically in Magento is somewhat tricky. I found a couple of annoying bugs or behaviors that really got me stuck. I’ll speak about that at the end of this post.

Update: Our colleague Josh found how to solve this issue by rewriting a core module. Take a look at that solution in StackOverflow. The solution below is still valid and you can use it anyway!

Setting tier prices programmatically

In order to set product tier prices in Magento we need to create an array of arrays that contain each new tier price rule. The process consists in: load the product , define the tier prices array (… matrix), set it and finally save the product:

/* First we tell magento to use the admin store in order to let
 * us to save products. This is needed for the Enterprise version,
 * but not for the community one
*/
Mage::app()->setCurrentStore(Mage_Core_Model_App::ADMIN_STORE_ID);
$id_product = 80880; //Select your product id
$product = Mage::getModel('catalog/product')->load($id_product);

//First defined tier price...
$tierPrices[] = array(
                  'website_id'  => 0,
                  'cust_group'  => 1,
                  'price_qty'   => 2,
                  'price'       => 5
                 );
//Second defined tier price
$tierPrices[] = array(
                  'website_id'  => 0,
                  'cust_group'  => 2,
                  'price_qty'   => 3,
                  'price'       => 10
                 );

//Now we set the tier price and save the product
$product->setTierPrice($tierPrices);

$product->save();

It seems to be easy, but it’s important to note that:

  • There is an annoying bug that deletes all tier prices when saving a product programatically, if you don’t set a tierprice, then it deletes all tier prices.

This bug can be solved adding a simple tier price array. How is it? Something like this:

$tierPrices = array(
                  'website_id'  => 0,
                  'cust_group'  => 2,
                  'price_qty'   => 3,
                  'price'       => 10
                 );
//Now we set the tier price and save the product
//No tier price will be added or deleted in this case
$product->setTierPrice($tierPrices);

Exactly, a variable without “[ ]“. Magento won’t add this tier price, but at least it will keep the old ones that have been already defined.

  • There is another bug, or not, that: if you try to save a tierprice that already exists, then Magento returns a constraint violation (I love those errors).

I have no a clear idea on how to solve this. In my case, I always receive all tier prices every time I update the product, so what I do first is to delete all product tier prices directly from the database and then add the tier prices again (because in my script I always receive from the xml file all the product tier prices). Then, in my case:

//Workaround to restart tier prices for the product.
//First delete tier prices related
$dbc = Mage::getSingleton('core/resource')->getConnection('core_write');
$resource = Mage::getSingleton('core/resource');
$table = $resource->getTableName('catalog/product').'_tier_price';
$dbc->query("DELETE FROM $table WHERE entity_id = $id_product");

//Now we can set the product tier prices
$tierPrices[] = array(
                  'website_id'  => 0,
                  'cust_group'  => 1,
                  'price_qty'   => 2,
                  'price'       => 5
                 );
//Second defined tier price
$tierPrices[] = array(
                  'website_id'  => 0,
                  'cust_group'  => 2,
                  'price_qty'   => 3,
                  'price'       => 10
                 );

//Now we set the tier price and save the product
$product->setTierPrice($tierPrices);

$product->save();

Get customer product alert subscriptions list in Magento

Some things are easy, some things are not. This one is easy-peasy.

If you want to get the product list that a customer has been subscribed to (price or stock alert), you need to use the productalert model.

  • Product stock alert list
$customer_id = Mage::getSingleton('customer/session')->getCustomer()->getId();
$customer_stock_alerts = Mage::getModel('productalert/stock')
         ->getCollection()
         ->addFieldToFilter('customer_id', $customer_id);
foreach ( $customer_stock_alerts as $alert )
{
 echo $alert->getProductId();
}
  • Product price alert list
$customer_id = Mage::getSingleton('customer/session')->getCustomer()->getId();
$customer_price_alerts = Mage::getModel('productalert/price')
         ->getCollection()
         ->addFieldToFilter('customer_id', $customer_id);
foreach ( $customer_price_alerts as $alert )
{
 echo $alert->getProductId();
}

What you need if you want to develop with magento

Here are a few tips to take into account if you want to start developing with Magento.

On the backend

Go to System » Configuration and click on the Advanced » Configuration tab. There you should enable the profiler (debug tab) and also the log (log settings tab). Remember that the /var directory has to have writing permissions.

Developer Mode

Enable the developer mode in htaccess (or your vhost configuration). If you want to develop on Magento you have to enable the developer mode. Just add the following line to your .htaccess file (or vhost configuration)

#Magento Developer Mode
SetEnv MAGE_IS_DEVELOPER_MODE &quot;true&quot;
  • Edit the index.php file

Next step is to edit the index.php file in order to enable the Varien Profiler and also display errors. Your index.php file should look like this:

Varien_Profiler::enable();
if (isset($_SERVER['MAGE_IS_DEVELOPER_MODE'])) {
Mage::setIsDeveloperMode(true);
}

ini_set('display_errors', 1);

MySQL performance

When it comes to performance we always need to know which queries in Magento are taking more time to execute. It is possible to log all the queries and also show (along with the profiler) how long it took to load the page, how long was the longest query (in  time) and the longest query itself. In order to enable this, do the following:

  • Modify: lib/Varien/Db/Adapter/Pdo/Mysql.php and set to true: $_debug, $_logCallStack and set to “0″ the $_logQueryTime value.
  • Create a writable directory in var called debug (that is, var/debug) in your magento directory.

Once you do this. At the bottom of each loaded page in Magento you’ll see the profiler and also the mysql data. You can also find a file called pdo_mysql.log with all the queries that have been executed during the page loading. You can also watch this file and see in live mode which queries are being executed and see the time each query needs to do its work.

Useful extensions

Install a couple of useful extensions. There are many extensions to help developers. Not all of them work in all Magento versions. Up to now I’ve found two extensions that are free and compatible up to Magento CE 1.6 and EE 1.11.

  • Wee_developertoolbar | It adds a tool button at the top-right corner of our website showing some basic information. Clicking there we’ll be able to see information about blocks, mysql queries, clearing the cache, enable translation, block hints, etc. It’s the greatest extension for developers I’ve found up to this moment

  • Layout analyzer | This extensions allows us to see what’s the final layout that has been loaded on our page. The most interesting thing is that we can take a look at the final xml layout that has been used to load the page.

Conclusion

With all this you should have all the tools you need to start developing on Magento and be able to debug properly. Magento is somewhat hard sometimes, so it’s important to have all the available debugging and developing tools with us.