Monday, November 11, 2013

Product relations importer

Check out the product relations import tool for Magento. This extension allows you to quickly update/replace related products, upsells and crossells for your catalog products. You can do that by manual input or file upload.

Thursday, October 31, 2013

Easylife Switcher extension

I've just released a new Magento extension called 'Easylife_Switcher'.
This is what it does:
  • Frontend:
    • it can change the configurable products dropdowns to text or image labels
      config
    • it allows you do display the out od stock combinations of configurable products with a "not available" overlay (see image above - medium size). The out of stock products may be selectable or not. Your choice.
    • it can change the main image in the product page when a combination is selected, if an image is available for the simple product. You can set the attributes that change the image.
    • it can change the full media block in the product page when a combination is selected, if at least the main image is available for the simple product. You can set the attributes that change the media section.
  • Backend:
    • it allows you to set a default configuration to be selected while accessing the configurable product page.
      backend
Code and more details on how to use can be found here: https://github.com/tzyganu/Switcher

Friday, April 19, 2013

Monday, April 8, 2013

Thursday, March 28, 2013

Get class rewrites

Here is a small script that lists all rewrites for models, blocks and helpers in Magento.
It also takes into account the disabled extensions but this should't be a big issue.
Create a new file on the root of your magento instance. Let's call it rewrites.php with this content.
<?php 
$folders = array('app/code/local/', 'app/code/community');//folders to parse
$configFiles = array();
foreach ($folders as $folder){
    $files = glob($folder.'*/*/etc/config.xml');//get all config.xml files in the specified folder
    if (is_array($files)){
        $configFiles = array_merge($configFiles, $files);//merge with the rest of the config files
    }
}
$rewrites = array();//list of all rewrites

foreach ($configFiles as $file){
    $dom = new DOMDocument;
    $dom->loadXML(file_get_contents($file));
    $xpath = new DOMXPath($dom);
        $path = '//rewrite/*';//search for tags named 'rewrite'
        $text = $xpath->query($path);
        foreach ($text as $rewriteElement){
            $type = $rewriteElement->parentNode->parentNode->parentNode->tagName;//what is overwritten (model, block, helper)
            $parent = $rewriteElement->parentNode->parentNode->tagName;//module identifier that is being rewritten (core, catalog, sales, ...)
            $name = $rewriteElement->tagName;//element that is rewritten (layout, product, category, order)
            foreach ($rewriteElement->childNodes as $element){
                $rewrites[$type][$parent.'/'.$name][] = $element->textContent;//class that rewrites it
            }
        }
}
echo "<pre>";print_r($rewrites);
When accessing mysite.com/rewrites.php you should be able to see all the model, block and helper rewrites. If there is one object rewritten by 2 or more classes it means you have an extension conflict.

Monday, March 25, 2013

Add custom page layout

Here is now you can add a custom page layout (similar to 1column, 2columns,....) without editing the core files.
Create a new module. Let's call it Easylife_Layout.
For this you need the following files:
app/etc/modules/Easylife_Layout.xml - the declaration file
<?xml version="1.0"?>
<config>
    <modules>
        <Easylife_Layout>
            <active>true</active>
            <codePool>local</codePool>
            <depends>
                <Mage_Page />
            </depends>
        </Easylife_Layout>
    </modules>
</config>
app/code/local/Easylife/Layout/etc/config.xml - the configuration file
<?xml version="1.0"?> 
<config>
    <modules>
        <Easylife_Layout>
            <version>0.0.1</version>
        </Easylife_Layout>
    </modules>
    <global>
        <page>
            <layouts> 
                <custom_layout module="page" translate="label">
                    <label>Custom layout</label>
                    <template>page/custom_layout.phtml</template>
                    <layout_handle>custom_layout</layout_handle>
                </custom_layout> 
            </layouts>
        </page>
    </global>
    <frontend>
        <layout>
            <updates>
                <easylife_layout>
                    <file>easylife_layout.xml</file>
                </easylife_layout>
            </updates>
        </layout>
    </frontend>
</config>
app/design/frontend/{interface}/{theme}/layout/easylife_layout.xml - the layout file (I recommend creating it inside the base/default theme so it will be available for all the custom themes)
<?xml version="1.0"?> 
<layout>
    <custom_layout translate="label">
        <label>Custom layout</label>
        <reference name="root">
            <action method="setTemplate"><template>page/custom_layout.phtml</template></action>
            <action method="setIsHandle"><applied>1</applied></action>
        </reference>
    </custom_layout> 
</layout>
app/design/frontend/{interface}/{theme}/template/page/custom_layout.phtml - the page html - feel free to customize it as you want. this is just a copy of 1column.phtml with some new added classes (I recommend creating it inside the base/default theme so it will be available for all the custom themes)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<?php echo $this->getLang() ?>" lang="<?php echo $this->getLang() ?>">
<head>
<?php echo $this->getChildHtml('head') ?>
</head>
<body<?php echo $this->getBodyClass()?' class="'.$this->getBodyClass().'"':'' ?>>
<?php echo $this->getChildHtml('after_body_start') ?>
<div class="wrapper custom-layout">
    <?php echo $this->getChildHtml('global_notices') ?>
    <div class="page">
        <?php echo $this->getChildHtml('header') ?>
        <div class="main-container custom-layout-container">
            <div class="main">
                <?php echo $this->getChildHtml('breadcrumbs') ?>
                <div class="col-main">
                    <?php echo $this->getChildHtml('global_messages') ?>
                    <?php echo $this->getChildHtml('content') ?>
                </div>
            </div>
        </div>
        <?php echo $this->getChildHtml('footer') ?>
        <?php echo $this->getChildHtml('before_body_end') ?>
    </div>
</div>
<?php echo $this->getAbsoluteFooter() ?>
</body>
</html>
That's it. If you want to load your layout in a custom layout xml file (let's say for your custom entities display) just add this inside the page handle
<update hande="custom_layout" />
The new layout should be available for all the CMS pages and category/product display settings.

Saturday, March 16, 2013

Monday, March 4, 2013

Tuesday, February 12, 2013

Create system config section with dynamic number of fields

Here is how you can create a section in System->Configuration with dynamic number of fields.
The following example will create a config section with yes/no dropdowns for each customer group.
For this you need to create a new module. Let's call it Easylife_Configsection. You can change the name to whatever you want but make sure you change the file names and references in the code.
The extension has the following files: app/etc/modules/Easylife_Configsection.xml - module declaration file
<?xml version="1.0"?>
<config>
    <modules>
        <Easylife_Configsection>
            <active>true</active>
            <codePool>local</codePool>
            <depends>
             <Mage_Adminhtml/>
            </depends>
        </Easylife_Configsection>
    </modules>
</config>
app/code/local/Easylife/Configsection/etc/config.xml - module config file
<?xml version="1.0"?>
<config>
 <modules>
  <Easylife_Configsection>
   <version>1.0.0</version>
  </Easylife_Configsection>
 </modules>
 <global>
  <blocks>
   <configsection>
    <class>Easylife_Configsection_Block</class>
   </configsection>
  </blocks>
  <helpers>
   <configsection>
    <class>Easylife_Configsection_Helper</class>
   </configsection>
  </helpers>
 </global>
</config>
app/code/local/Easylife/Configsection/Helper/Data.php - the module main helper
<?php 
class Easylife_Configsection_Helper_Data extends Mage_Core_Helper_Abstract{
 
}
app/code/local/Easylife/Configsection/etc/system.xml - this will add the section in the config area
<?xml version="1.0"?>
<config>
 <sections>
  <customer_groups_demo translate="label" module="configsection"><!-- this ads a new section in the config you can change the tab name to what you need-->
   <label>Customer groups demo</label>
            <tab>customer</tab><!-- the section is added in the customer tab. you can change to anything-->
            <frontend_type>text</frontend_type>
            <sort_order>200</sort_order>
            <show_in_default>1</show_in_default>
            <show_in_website>1</show_in_website>
            <show_in_store>1</show_in_store>
   <groups>
    <demo translate="label" module="configsection"><-- this add a group in the section you just created change the name to whatever you need -->
                    <label>Demo customer groups</label>
                    <frontend_type>text</frontend_type>
                    <frontend_model>configsection/adminhtml_system_config_form_fieldset_customer_groups</frontend_model><-- this is the model that renders all the fields -->
                    <sort_order>10</sort_order>
                    <show_in_default>1</show_in_default>
                    <show_in_website>1</show_in_website>
                    <show_in_store>1</show_in_store>
                </demo> 
   </groups>
  </customer_groups_demo>
 </sections>
</config>
app/code/local/Easylife/Configsection/etc/adminhtml.xml - this will add the acl for you section
<?xml version="1.0"?>
<config>
    <acl>
        <resources>
            <admin>
                <children>
                    <system>
                        <children>
                            <config>
                                <children>
                                    <customer_groups_demo translate="title" module="configsection"><-- this tag sould be named as the section you create -->
                                        <title>Customer groups demo</title>
                                    </customer_groups_demo>
                                </children>
                            </config>
                        </children>
                    </system>
                </children>
            </admin>
        </resources>
    </acl>
</config>
Now the fun part. app/code/local/Easylife/Configsection/Block/Adminhtml/System/Config/Form/Fieldset/Customer/Groups.php - this is the model that create the fields in the config section
<?php 
class Easylife_Configsection_Block_Adminhtml_System_Config_Form_Fieldset_Customer_Groups extends 
 Mage_Adminhtml_Block_System_Config_Form_Fieldset{
 protected $_dummyElement;
    protected $_fieldRenderer;
    protected $_values;

    public function render(Varien_Data_Form_Element_Abstract $element)
    {
        $html = $this->_getHeaderHtml($element);
 //here you cand loop through all the fields you want to add
 //for each element you neet to call $this->_getFieldHtml($element, $group);
        $groups = Mage::getModel('customer/group')->getCollection();

        foreach ($groups as $group) {
            $html.= $this->_getFieldHtml($element, $group);
        }
        $html .= $this->_getFooterHtml($element);

        return $html;
    }
    //this creates a dummy element so you can say if your config fields are available on default and website level - you can skip this and add the scope for each element in _getFieldHtml method
    protected function _getDummyElement()
    {
        if (empty($this->_dummyElement)) {
            $this->_dummyElement = new Varien_Object(array('show_in_default'=>1, 'show_in_website'=>1));
        }
        return $this->_dummyElement;
    }
    //this sets the fields renderer. If you have a custom renderer tou can change this. 
    protected function _getFieldRenderer()
    {
        if (empty($this->_fieldRenderer)) {
            $this->_fieldRenderer = Mage::getBlockSingleton('adminhtml/system_config_form_field');
        }
        return $this->_fieldRenderer;
    }
    //this is usefull in case you need to create a config field with type dropdown or multiselect. For text and texareaa you can skip it.
    protected function _getValues()
    {
        if (empty($this->_values)) {
            $this->_values = array(
                array('label'=>Mage::helper('adminhtml')->__('No'), 'value'=>0),
                array('label'=>Mage::helper('adminhtml')->__('Yes'), 'value'=>1),
            );
        }
        return $this->_values;
    }
    //this actually gets the html for a field
    protected function _getFieldHtml($fieldset, $group)
    {
        $configData = $this->getConfigData();
        $path = 'customer_groups_demo/demo/group_'.$group->getId();//this value is composed by the section name, group name and field name. The field name must not be numerical (that's why I added 'group_' in front of it)
        if (isset($configData[$path])) {
            $data = $configData[$path];
            $inherit = false;
        } else {
            $data = (int)(string)$this->getForm()->getConfigRoot()->descend($path);
            $inherit = true;
        }

        $e = $this->_getDummyElement();//get the dummy element

        $field = $fieldset->addField($group->getId(), 'select',//this is the type of the element (can be text, textarea, select, multiselect, ...)
            array(
                'name'          => 'groups[demo][fields][group_'.$group->getId().'][value]',//this is groups[group name][fields][field name][value]
                'label'         => $group->getCustomerGroupCode(),//this is the label of the element
                'value'         => $data,//this is the current value
                'values'        => $this->_getValues(),//this is necessary if the type is select or multiselect
                'inherit'       => $inherit,
                'can_use_default_value' => $this->getForm()->canUseDefaultValue($e),//sets if it can be changed on the default level
                'can_use_website_value' => $this->getForm()->canUseWebsiteValue($e),//sets if can be changed on website level
            ))->setRenderer($this->_getFieldRenderer());

        return $field->toHtml();
    }
}
That's about it. Clear the cache, logout of the admin and login again and you should be able to use this. You can get the value of a setting just like any other magento config setting:
$groupId = 1;
$val = Mage::getStoreConfig('customer_groups_demo/demo/group_'.$groupId);//section_name/group/field
Marius.

Tuesday, January 29, 2013

Share cart between websites

I saw that this is a general issue among Magneto users/developers.
Here is how I was able to keep the cart between websites.
The solution is not fully tested yet but for me it seams to work.
[Update]
Warning: I seams that this solution doesn't work with multiple currencies installed. See comments from MichaƂ Burda
[/Update]
If someone tries it please let me know the result and eventual bugs that appear.
Preconditions:
  • 'Use SID on Frontend' must be st to 'Yes' (System->Configuration->Web->Session Validation Settings)
Approach:
Magneto already allows you to keep the quote (cart) between store views from the same website.
I tried to manipulate this feature into letting me keep the cart between all websites (store views).
How is the quote kept in $_SESSION?
Magento keeps the quote id in the $_SESSION like this:
$_SESSION['quote_id_5'] = 34;
In the code above, 34 represents the quote id and 5 represents the website it (not store view id).
So it's basically like this:
$_SESSION['quote_id_{WEBSITE_ID}'] = {QUOTE_ID};
This means that the quote is different for each website.
Now for the actual code:
I've created a new extension called Easylife_Sales.
The extension has a fail-safe, in case it doesn't work as expected, it's behavior can be disabled from the configuration area. but more on this later.
Extension files:
app/etc/modules/Easylife_Sales.xml - declaration file
<?xml version="1.0"?>
<config>
    <modules>
        <Easylife_Sales>
            <active>true</active>
            <codePool>local</codePool>
            <depends>
             <Mage_Sales /><!-- so it's loaded after Mage_Sales -->
             <Mage_Checkout /><!-- so it's loaded after Mage_Checkout -->
            </depends>
        </Easylife_Sales>
    </modules>
</config>
app/code/local/Easylife/Sales/etc/config.xml - configuration file
<?xml version="1.0"?>
<config>
 <modules>
  <Easylife_Sales>
   <version>0.0.1</version>
  </Easylife_Sales>
 </modules>
 <global>
  <models>
   <sales>
    <rewrite>
     <observer>Easylife_Sales_Model_Observer</observer>
     <quote>Easylife_Sales_Model_Quote</quote>
    </rewrite>
   </sales>
   <checkout>
    <rewrite>
     <session>Easylife_Sales_Model_Checkout_Session</session>
    </rewrite>
   </checkout>
  </models>
  <helpers>
   <sales>
    <rewrite>
     <data>Easylife_Sales_Helper_Data</data>
    </rewrite>
   </sales>
  </helpers>
 </global>
 <frontend>
  <events><!-- this section is not mandatory, explanations later -->
   <sales_quote_collect_totals_before>
    <observers>
     <easylife_sales>
      <class>sales/observer</class>
      <method>checkProductAvailability</method>
     </easylife_sales>
    </observers>
   </sales_quote_collect_totals_before>
  </events>
 </frontend>
 <default>
  <checkout>
   <options>
    <persistent_quote>1</persistent_quote><!-- this is for the fail-safe -->
   </options>
  </checkout>
 </default>
</config>
app/code/local/Easylife/Sales/Helper/Data.php - override the default sales helper to add the fail-save method
<?php 
class Easylife_Sales_Helper_Data extends Mage_Sales_Helper_Data{
    public function getIsQuotePersistent(){
  return Mage::getStoreConfigFlag('checkout/options/persistent_quote');
 }
}
app/code/local/Easylife/Sales/Model/Checkout/Session.php - override the default checkout session in order to change the session key for the quote
<?php 
class Easylife_Sales_Model_Checkout_Session extends Mage_Checkout_Model_Session{
 protected function _getQuoteIdKey()
    {
     if (Mage::helper('sales')->getIsQuotePersistent()){//if behavior is not disabled
         return 'quote_id';
     }
     return parent::_getQuoteIdKey();
    }
}
app/code/local/Easylife/Sales/Model/Quote.php - override the quote model to share between all websites
<?php 
class Easylife_Sales_Model_Quote extends Mage_Sales_Model_Quote{
 public function getSharedStoreIds(){
  if (Mage::helper('sales')->getIsQuotePersistent()){//if behavior is not diasabled
   $ids = Mage::getModel('core/store')->getCollection()->getAllIds();
   unset($ids[0]);//remove admin just in case
   return $ids;
  }
  return parent::getSharedStoreIds();
 }
}
app/code/local/Easylife/Sales/etc/system.xml - this allows you do disable the functionality in case something is wrong.
<?xml version="1.0"?>
<config>
 <sections>
  <checkout>
   <groups>
    <options>
     <fields>
      <persistent_quote translate="label" module="sales">
       <label>Keep cart between websites</label>
       <frontend_type>select</frontend_type>
       <source_model>adminhtml/system_config_source_yesno</source_model>
       <sort_order>100</sort_order>
       <show_in_default>1</show_in_default>
       <show_in_website>0</show_in_website>
       <show_in_store>0</show_in_store>
      </persistent_quote>
     </fields>
    </options>
   </groups>
  </checkout>
 </sections>
</config>
We are almost done. At this point the following happens. If you have a product in cart, you change the website and the product is not available in the new site the product is still in the cart. If you want this behavior then there is no problem. All you need to do is to remove these lines from config.xml
<observer>Easylife_Sales_Model_Observer</observer>
and
<events><!-- this section is not mandatory, explanations later -->
 <sales_quote_collect_totals_before>
  <observers>
   <easylife_sales>
    <class>sales/observer</class>
    <method>checkProductAvailability</method>
   </easylife_sales>
  </observers>
 </sales_quote_collect_totals_before>
</events>
In my case I wanted to remove from cart (permanently) the products that are not available in that website. if you want this behavior add the following file: app/code/local/Easylife/Sales/Model/Observer.php - this will remove from cart the products that are not valid on the current store.
<?php 
class Easylife_Sales_Model_Observer extends Mage_Sales_Model_Observer{
        public function checkProductAvailability($observer){
  if (!Mage::helper('sales')->getIsQuotePersistent()){
   return $this;
  }
  $quote = $observer->getEvent()->getQuote();
  $currentId = Mage::app()->getWebsite()->getId();
  
  $messages = array();
  
  foreach ($quote->getAllItems() as $item){   
   $product = $item->getProduct();
   if (!in_array($currentId, $product->getWebsiteIds())){
    $quote->removeItem($item->getId());
    $messages[] = Mage::helper('catalog')->__('Product %s is not available on website %s', $item->getName(), Mage::app()->getWebsite()->getName());
   }
  }
  foreach ($messages as $message){
   Mage::getSingleton('checkout/session')->addError($message);
  }
  return $this;
 }
}
Well that's about it. Enjoy and let me know how it turns out.

Marius.