Cloning a Magento Coupon

I’ve previously posted here about the complexities involved in Magento Salesrules and Coupon codes. While I don’t like the way Magento Rules are designed, I still have to work with them.

I put together a little module this morning because I needed to clone a coupon code, and I couldn’t find any native support to get this done.

For the lazy, I’ve put together the files required to get this functioning.
Download Mby_Rules.zip
Tested on EE 1.11.

For those of you interested in how this works, have a look at my run-down of conditions/actions first. Then, basically, I have extended the Mage_Salesrule_Model_Rule class and added some extra functions. Here’s the extended class, with minimal notes:

<?php
class Mby_Rules_Model_Salesrule extends Mage_SalesRule_Model_Rule
{

    /**
     * Makes a clone of a coupon code. Returns false if desired coupon code already exists.
     *
     * Usage:
        
        $salesrule = Mage::getModel("salesrule/rule")->load($ruleId);
        if ($clone = $salesrule->makeClone("NEWCOUPONCODE")) {    
            // Work with $clone
        } else {
            // Coupon code already exists.
        }
        
     * 
     * @param string $couponCode  the desired code for the cloned coupon.
     * return bool || Mby_Rules_Model_Salesrule  
     */
    public function makeClone($couponCode)
    {
        // Ensure no coupon code double up
        $rules = Mage::getModel("salesrule/rule")->getCollection()->addFieldToFilter('code', $couponCode);
        foreach ($rules as $rule) {
            return false;
        }
        
        $parentData = $this->getData();
        
        // Assign desired code
        $parentData['coupon_code'] = $couponCode;
        
        // Flatten conditions and actions
        $flatConditions = $this->_flatten('conditions', $parentData);
        $flatActions = $this->_flatten('actions', $parentData);
        
        // Unset unnecessary data
        unset($parentData['rule_id']);
        unset($parentData['conditions_serialized']);
        unset($parentData['actions_serialized']);
        
        // Init clone and assign data
        $clone = Mage::getModel("salesrule/rule")->setData($parentData)->save();
        $clone->setData('conditions', $flatConditions);
        $clone->setData('actions', $flatActions);
        
        // Setup data and finalise before save
        $cloneData = $clone->getData();
        if ($cloneData['from_date'] == "NULL") unset($cloneData['from_date']);
        if ($cloneData['to_date'] == "NULL") unset($cloneData['to_date']);
        $clone->loadPost($cloneData);
        $clone->save();
        
        return $clone;
    }
    
    /**
     * Sets up data and returns flattened data
     * @param string $type  Must be "actions" or "conditions"
     * @param array $parentData  Full data from the parent code.
     * return array
     */
    protected function _flatten($type, $parentData)
    {
        $unserialized = unserialize($parentData[$type.'_serialized']);
        $flatData = array();
        $this->_convertRecursiveToFlat("1", $unserialized, $flatData);
        ksort($flatData);
        return $flatData;
    }
    
    /**
     * Reverse of Mage_Rule_Model_Rule::_convertFlatToRecursive
     * Takes recursive Array and makes it flat again.
     * @param string $finalKey  the key of $conditions in the final flat array
     * @param array $conditions  a condition to insert
     * @param array &$finalContainer  the final array to end up flattened
     * return void
     */
    protected function _convertRecursiveToFlat($finalKey, array $conditions, array &$finalContainer = array())
    {
        foreach ($conditions['conditions'] as $key => $subconditions) {
            $this->_convertRecursiveToFlat($finalKey."--".($key+1), $subconditions, $finalContainer);
        }
        unset($conditions['is_value_processed']);
        unset($conditions['conditions']);
        $finalContainer[$finalKey] = $conditions;
        return;
    }

}

php_browscap iOS 6/Mobile Safari Browser Detection

I recently updated my iPhone to iOS 6 and realised that my websites weren’t recognising my Safari User Agent any more.

I use the php_browscap.ini file provided by the Browser Capabilities Project, from the August 31 2012 update. Since I needed a fix quickly, I modified the file myself. I also noticed there were no other resources on this problem out there, so I thought I’d post up my solution for others to use.

Add the following after the section named “Mobile Safari 5.1”.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Mobile Safari 6.0

[Mobile Safari 6.0]
Parent=DefaultProperties
Comment="Mobile Safari 6.0"
Browser="Safari"
Version=6.0
MajorVer=6
MinorVer=0
Platform="iOS"
Frames=true
IFrames=true
Tables=true
Cookies=true
JavaScript=true
isMobileDevice=true
CssVersion=3

[Mozilla/5.0 (iPad*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*]
Parent=Mobile Safari 6.0
Version=0
MajorVer=0
Platform_Version=6.0

[Mozilla/5.0 (iPad*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*Mobile/*]
Parent=Mobile Safari 6.0
Platform_Version=6.0

[Mozilla/5.0 (iPad*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*Version/5.0.*Mobile/*Safari/*]
Parent=Mobile Safari 6.0
Platform_Version=6.0

[Mozilla/5.0 (iPad*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*]
Parent=Mobile Safari 6.0
Version=0
MajorVer=0
Platform_Version=6.0

[Mozilla/5.0 (iPad*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*Mobile/*]
Parent=Mobile Safari 6.0
Platform_Version=6.0

[Mozilla/5.0 (iPad*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*Version/5.0.*Mobile/*Safari/*]
Parent=Mobile Safari 6.0
Platform_Version=6.0

[Mozilla/5.0 (iPhone*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*]
Parent=Mobile Safari 6.0
Version=0
MajorVer=0
Platform_Version=6.0

[Mozilla/5.0 (iPhone*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*Mobile/*]
Parent=Mobile Safari 6.0
Platform_Version=6.0

[Mozilla/5.0 (iPhone*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*Version/5.0*Mobile/*Safari/*]
Parent=Mobile Safari 6.0
Platform_Version=6.0

[Mozilla/5.0 (iPhone*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/(*KHTML, like Gecko*)*]
Parent=Mobile Safari 6.0
Version=0
MajorVer=0
Platform_Version=6.0

[Mozilla/5.0 (iPhone*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*Mobile/*]
Parent=Mobile Safari 6.0
Platform_Version=6.0

[Mozilla/5.0 (iPhone*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*Version/5.0*Mobile/*Safari/*]
Parent=Mobile Safari 6.0
Platform_Version=6.0

[Mozilla/5.0 (iPod*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*]
Parent=Mobile Safari 6.0
Version=0
MajorVer=0
Platform_Version=6.0

[Mozilla/5.0 (iPod*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*Mobile/*]
Parent=Mobile Safari 6.0
Platform_Version=6.0

[Mozilla/5.0 (iPod*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*Version/5.0*Mobile/*Safari/*]
Parent=Mobile Safari 6.0
Platform_Version=6.0

[Mozilla/5.0 (iPod*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*]
Parent=Mobile Safari 6.0
Version=0
MajorVer=0
Platform_Version=6.0

[Mozilla/5.0 (iPod*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*Mobile/*]
Parent=Mobile Safari 6.0
Platform_Version=6.0

[Mozilla/5.0 (iPod*CPU*OS 6_0* like Mac OS X*)*AppleWebKit/*(*KHTML, like Gecko*)*Version/5.0*Mobile/*Safari/*]
Parent=Mobile Safari 6.0
Platform_Version=6.0

Magento Event Observer List

I’m putting this here mostly so I know where to find it when I need to look next.

Event Observers defined in config.xml

Event Area Module
admin_system_config_changed_section_cataloginventory global CatalogInventory
adminhtml_catalog_product_edit_element_types adminhtml Weee
adminhtml_catalog_product_edit_prepare_form adminhtml Weee
adminhtml_catalog_product_form_prepare_excluded_field_list adminhtml Weee
adminhtml_controller_action_predispatch_start global Adminhtml
adminhtml_product_attribute_types adminhtml Weee
adminhtml_sales_order_create_create_order adminhtml GiftMessage
bundle_product_view_config global Weee
catalog_category_delete_after adminhtml GoogleOptimizer
catalog_category_load_after adminhtml, frontend GoogleOptimizer
catalog_category_prepare_save adminhtml GoogleOptimizer
catalog_category_save_after adminhtml GoogleOptimizer
catalog_controller_product_delete adminhtml Tag
catalog_controller_product_save_visibility_changed adminhtml Tag
catalog_controller_product_view frontend Reports, Sendfriend
catalog_entity_attribute_save_before global Weee
catalog_helper_output_construct frontend GoogleOptimizer
catalog_model_product_duplicate adminhtml, global Bundle, CatalogInventory
catalog_product_collection_load_after frontend, global Bundle, CatalogInventory, GiftMessage, Tax
catalog_product_compare_add_product frontend Reports
catalog_product_compare_item_collection_clear frontend Reports
catalog_product_compare_remove_product frontend Reports
catalog_product_delete_after adminhtml GoogleOptimizer
catalog_product_delete_after_done adminhtml Rating, Review
catalog_product_delete_before adminhtml GoogleBase, Sales, Tag
catalog_product_edit_action adminhtml Bundle
catalog_product_get_final_price adminhtml, crontab, frontend CatalogRule
catalog_product_import_after adminhtml CatalogRule
catalog_product_load_after adminhtml, frontend, global CatalogInventory, GoogleOptimizer
catalog_product_new_action adminhtml Bundle
catalog_product_prepare_index_select global CatalogInventory
catalog_product_prepare_save adminhtml Bundle, Downloadable, GoogleOptimizer
catalog_product_save_after adminhtml, global CatalogInventory, CatalogRule, GoogleBase, GoogleOptimizer, Sales
catalog_product_status_update adminhtml Sales
catalog_product_upsell frontend Bundle
catalog_product_view_config global Weee
catalogindex_plain_reindex_after global Bundle
catalogindex_prepare_price_select global Tax, Weee
catalogrule_after_apply adminhtml, global Sales, Weee
catalogrule_before_apply adminhtml Sales
checkout_allow_guest frontend Downloadable
checkout_cart_add_product_complete global Wishlist
checkout_cart_update_items_before global Wishlist
checkout_controller_multishipping_shipping_post frontend GiftMessage
checkout_controller_onepage_save_shipping_method frontend GiftMessage
checkout_multishipping_controller_success_action frontend GoogleAnalytics
checkout_onepage_controller_success_action frontend GoogleAnalytics
checkout_quote_destroy frontend Log
checkout_type_multishipping_create_orders_single frontend Downloadable
checkout_type_onepage_save_order_after frontend Downloadable
cms_page_delete_after adminhtml GoogleOptimizer
cms_page_load_after adminhtml, global GoogleOptimizer
cms_page_prepare_save adminhtml GoogleOptimizer
cms_page_save_after adminhtml GoogleOptimizer
cms_wysiwyg_config_prepare adminhtml Core, Widget
controller_action_layout_load_before frontend Customer
controller_action_nocookies frontend Cms
controller_action_noroute frontend Cms
controller_action_postdispatch frontend Log
controller_action_predispatch adminhtml, frontend, install AdminNotification, Adminhtml, Install, Log
controller_front_init_routers global Cms
core_config_data_save_commit_after global Index
core_locale_set_locale adminhtml, install Adminhtml, Install
customer_delete_after adminhtml, frontend Newsletter
customer_login frontend Catalog, Checkout, Log, Reports, Wishlist
customer_logout frontend Catalog, Checkout, Log, Reports, Wishlist
customer_save_after adminhtml, frontend Newsletter
default crontab Cron
log_log_clean_after global Catalog, Reports
mage_run_exception install Install
payment_form_block_to_html_before frontend Centinel
payment_info_block_prepare_specific_information adminhtml Centinel
prepare_catalog_product_index_select global CatalogInventory
prepare_catalog_product_price_index_table global CatalogRule
review_save_after frontend Review
sales_convert_order_item_to_quote_item adminhtml GiftMessage
sales_convert_order_to_quote adminhtml, frontend GiftMessage
sales_convert_quote_address_to_order adminhtml, frontend, global GiftMessage, Tax
sales_convert_quote_item_to_order_item adminhtml, frontend Bundle, Catalog, GiftMessage
sales_convert_quote_to_order adminhtml, frontend, global Centinel, GiftMessage
sales_creditmemo_item_save_after global CatalogInventory
sales_order_item_cancel global CatalogInventory
sales_order_item_save_after adminhtml, frontend Downloadable, Rss
sales_order_item_save_before global CatalogInventory
sales_order_place_after global SalesRule
sales_order_save_after adminhtml, frontend, global Downloadable, Tax
sales_order_save_before global CatalogInventory, Payment
sales_order_shipment_save_after adminhtml GoogleCheckout
sales_order_shipment_track_save_after adminhtml GoogleCheckout
sales_quote_item_collection_products_after_load global CatalogInventory
sales_quote_item_qty_set_after global CatalogInventory
sales_quote_item_save_before frontend Reports
sales_quote_save_after frontend Checkout, Log
sendfriend_product frontend Reports
store_delete_commit_after global Index
store_group_delete_commit_after global Index
store_group_save_commit_after global Index
store_save_commit_after global Index
tag_tag_product_collection_load_after frontend Review
website_delete_commit_after global Index
website_save_commit_after global Index
wishlist_add_product frontend Reports
wishlist_share frontend Reports

Javascript Base 64 Encoder/Decoder Bookmarklet

It’s not pretty, and it’s not Magento related (really) but here’s a little bookmarklet I wrote to encode/decode base64.

It only supports Webkit and Gecko based browsers, but it just offers you a way to encode or decode base 64 very quickly.

copy and paste this code into a bookmark URL:

javascript:function appendAll(a){for(var b=0;b<a.length;b++){for(var c=0;c<a[b].c.length;c++){a[b].p.appendChild(a[b].c[c])}}}function getDocDims(){return[Math.max(Math.max(document.body.scrollWidth,document.documentElement.scrollWidth),Math.max(document.body.offsetWidth,document.documentElement.offsetWidth),Math.max(document.body.clientWidth,document.documentElement.clientWidth)),Math.max(Math.max(document.body.scrollHeight,document.documentElement.scrollHeight),Math.max(document.body.offsetHeight,document.documentElement.offsetHeight),Math.max(document.body.clientHeight,document.documentElement.clientHeight))]}if(document.getElementById("base64container")!=null)document.body.removeChild(document.getElementById("base64container"));var dims=getDocDims();var div=document.createElement("div");div.id="base64container";div.setAttribute("style","width:"+dims[0]+"px;height:"+dims[1]+"px;top:0;left:0;position:absolute;");var basebg=document.createElement("div");basebg.id="base64blackout";basebg.setAttribute("style","width:"+dims[0]+"px;height:"+dims[1]+"px;position:absolute;z-index:100000000;top:0;left:0;background:#000;opacity:0.8;filter:alpha(opacity=80);-moz-opacity:0.8;-khtml-opacity:0.8;");var wrap=document.createElement("div");wrap.id="base64wrap";wrap.setAttribute("style","position:fixed;top:0;left:0;width:100%;height:100%;z-index:200000000;");wrap.onclick=function(a){if(a.target==this){document.getElementById("base64container").parentNode.removeChild(document.getElementById("base64container"))}};var inner=document.createElement("div");inner.id="base64inner";inner.setAttribute("style","position:relative;width:500px;height:200px;z-index:200000000;margin:10px auto;background:white;border-radius:5px;box-shadow:0px 0px 20px -5px #000");var textbit=document.createElement("div");textbit.id="base64textbit";textbit.setAttribute("style","position:relative;width:500px;height:auto;z-index:200000000;margin:10px auto 10px auto;border-radius:5px;text-align:center;font-family:monospace;color:#fff;font-size:25px;text-shadow: 0px 0px 10px black;");textbit.innerHTML="encoder < &nbsp; > decoder";var leftArea=document.createElement("div");leftArea.id="base64leftarea";leftArea.setAttribute("style","float:left;width:235px;height:180px;border-radius:3px;margin:10px 0px 10px 10px;");var rightArea=document.createElement("div");rightArea.id="base64rightarea";rightArea.setAttribute("style","float:left;width:235px;height:180px;border-radius:3px;margin:10px;");var rightTextArea=document.createElement("textarea");rightTextArea.id="base64righttextarea";rightTextArea.setAttribute("style","width:233px;height:100%;border-radius:3px;border-color:#888;padding:0;box-shadow:inset 1px 1px 5px #aaa;");rightTextArea.onkeyup=function(){leftTextArea.value=atob(this.value)};var leftTextArea=document.createElement("textarea");leftTextArea.id="base64lefttextarea";leftTextArea.setAttribute("style","width:233px;height:100%;border-radius:3px;border-color:#888;padding:0;box-shadow:inset 1px 1px 5px #aaa;");leftTextArea.onkeyup=function(){rightTextArea.value=btoa(this.value)};appendAll([{p:leftArea,c:[leftTextArea]},{p:rightArea,c:[rightTextArea]},{p:inner,c:[leftArea,rightArea]},{p:wrap,c:[inner,textbit]},{p:div,c:[basebg,wrap]},{p:document.body,c:[div]}]);

Tagged , ,

Add Javascript/CSS to page Head from within a Magento Block

This is a pretty easy one, but it’s something that can come in very handy at times.

Say you want to load a particular script on the page, but only if a particular block loads on that page. As it stands, Magento’s core/template blocks don’t support this.

To do it, include this function from within your custom module’s Block file in app/code/local/Namespace/Modulename/Block

public function addItem($type, $path)
{
    $head = $this->getLayout()->getBlock('head');
    return $type == 'css' ? $head->addCss($path) : $type == 'javascript' ? $head->addJs($path) : $this ;
}

Then to use it, just include a call to this function from within your block declaration in the layout:

<block type="modulename/blockname" name="modulename.blockname" as="blockname" template="path/to/template/file.phtml">
    <action method="addItem"><type>css</type><path>css/path/to/file.css</path></action>
    <action method="addItem"><type>javascript</type><path>path/to/file.js</path></action>
</block>

When that block is constructed, it will run the function and your custom scripts will load in the head.

Tagged , , , , ,

EAV and Flat Table Models in the Same Module

If you ever want to develop a module that has both EAV Models and Flat Table Models, you can use the info here.

As usual, there’s a lot of assumed knowledge regarding module development here, both flat and EAV. If you haven’t already, first go through all of Alan Storm’s tutorials here:

http://alanstorm.com/category/magento

Specifically Magento Models and ORM Basics, Magento Setup Resources and Magento ORM: Entity Attribute Value; Part 1.

Once you’ve done those, here’s how you combine them.

Your modules config.xml needs to look like this. The different parts being four nodes under models (two for EAV, two for Flat), and new nodes for read, write and setup in the resources node.

<config>
    ...
    <global>
        ...
        <models>    
            <modulename>
                <class>Mby_Modulename_Model</class>
                <resourceModel>modulename_resource_eav_mysql4</resourceModel>
            </modulename>
            <modulename_resource_eav_mysql4>
                <class>Mby_Modulename_Model_Resource_Eav_Mysql4</class>               
                <entities>
                    <thing>
                        <table>modulename_things</table>
                    </thing>
                </entities>
            </modulename_resource_eav_mysql4>
            <modulename_flat>
                <class>Mby_Modulename_Model_Flat</class>
                <resourceModel>modulename_flat_mysql4</resourceModel>
            </modulename_flat>
            <modulename_flat_mysql4>
                <class>Mby_Modulename_Model_Flat_Mysql4</class>
                <entities>
                    <item>
                        <table>modulename_items</table>
                    </item>
                </entities>
            </modulename_flat_mysql4>
        </models>
        <resources>
            <modulename_write>
                <connection>
                    <use>core_write</use>
                </connection>
            </modulename_write>
            <modulename_read>
                <connection>
                    <use>core_read</use>
                </connection>
            </modulename_read>
            <modulename_setup>
                <setup>
                    <module>Mby_Modulename</module>
                    <class>Mby_Modulename_Entity_Setup</class>
                </setup>
                <connection>
                    <use>core_setup</use>
                </connection>
            </modulename_setup>
            <modulename_flat_write>
                <connection>
                    <use>core_write</use>
                </connection>
            </modulename_flat_write>
            <modulename_flat_read>
                <connection>
                    <use>core_read</use>
                </connection>
            </modulename_flat_read>
            <modulename_flat_setup>
                <setup>
                    <module>Mby_Modulename</module>
                    <class>Mby_Modulename_Entity_Mysql4_Setup</class>
                </setup>
                <connection>
                    <use>core_setup</use>
                </connection>
            </modulename_flat_setup>
        </resources>
        ...
    </global>
    ....
</config>

Ensure your folder structure looks like this:

/app/code/local/Mby/Modulename
/app/code/local/Mby/Modulename/Block
/app/code/local/Mby/Modulename/controllers
/app/code/local/Mby/Modulename/Entity
/app/code/local/Mby/Modulename/Entity/Mysql4
/app/code/local/Mby/Modulename/etc
/app/code/local/Mby/Modulename/Model
/app/code/local/Mby/Modulename/Model/Flat
/app/code/local/Mby/Modulename/Model/Flat/Mysql4
/app/code/local/Mby/Modulename/Model/Resource
/app/code/local/Mby/Modulename/Model/Resource/Eav
/app/code/local/Mby/Modulename/Model/Resource/Eav/Mysql4
/app/code/local/Mby/Modulename/sql
/app/code/local/Mby/Modulename/sql/modulename_setup
/app/code/local/Mby/Modulename/sql/modulename_flat_setup

From here, put your install/upgrade scripts in their necessary folders in /sql.

Then create these two Setup classes in /Entity (EAV) and /Entity/Mysql4 (Flat):

<?php
class Mby_Modulename_Entity_Setup extends Mage_Eav_Model_Entity_Setup
{
}
<?php
class Mby_Modulename_Entity_Mysql4_Setup extends Mage_Core_Model_Resource_Setup
{
}

Define your models’ class names as per usual (Mby_Modulename_Model_Thing and Mby_Modulename_Model_Flat_Item).
When you’re initialising your models, use these:

Mage::getModel("modulename/thing");
Mage::getModel("modulename_flat/item");

And there you have it.

Fixing Magento URL Rewrite Indexing Error

Recently, I was trying to rebuild Magento indexes when I was presented with this extremely informative error message:


An error occurred while saving the URL rewrite

…Seriously?

Magento’s error reporting can be very good, or very bad. Unfortunately in this case it’s bad.
No amount of index rebuilding would make this go away, and the red “Rebuild Required” flag next to the URL Rewrites index was doing nothing but filling my heart and body with an insurmountable rage.
A Google search for a fix produced some results that included 777ing the media folder, and other such “solutions”, but nothing of value. There seemed to be a lot of people left in the lurch, unable to fix the issue. I hope they all made it out alive. Some SQLing & log checking later, I found the problem and was able to fix it.

I had recently upgraded Magento from Enterprise 1.9.1.1 to 1.11.2.0, which included a number of minor and major updates. Somewhere in-between versions, the URL Rewrites data was modified to replace NULL values in the product_id and category_id fields with “0”. When indexing the later version of Magento, these zeroes are unable to be indexed as there is no product/category with ID “0”. The indexing process therefore fails, and leaves you hanging. To check if you have the same issue as me, run this command on SQL:


SELECT * FROM core_url_rewrite WHERE product_id = 0 OR category_id = 0;

If that statement yields any results, the solution is an easy one. Run this SQL on your database:


UPDATE core_url_rewrite SET core_url_rewrite.product_id = NULL WHERE core_url_rewrite.product_id = 0;

Rebuild your indexes, and if your problem was the same as mine, your issue should be fixed.

Tagged , , , , ,

Magento Multiple Group-by’s on a collection

This might never be useful for anyone but me, but this is an obscure little function I needed to write recently.

If you need to return a collection that groups by different columns depending on condition, this function will help you out.
Please excuse me if this is a little hard to follow. I’ll do my best to explain the requirement. Here’s a simplified example:

Think of a table that records access to a particular page called “page_views”. We’ll call each record a “view”.
Each view has these fields:

    page_id
    customer_id
    ip_address
    datetime

Say we want to get all unique views from this table for the page with page_id “10”. What constitutes a “unique” view?

Well, let’s say that we group the table by customer_id, and there you have it. But wait, what about if the customer isn’t logged in, and the user_id field is blank? Grouping by customer_id will lump all of those ‘Guest’ views together. So let’s group by ip_address instead. But wait, then we come across multiple views from the same customer_id because the customer has viewed the same content from different connections.

So we want to group by customer_id if the customer_id is set, and ip_address if the customer is browsing as a guest. using “GROUP BY customer_id, ip_address” Doesn’t work either, as it groups by the combination of the two.
What we need to use is a UNION between two selects. This is straightforward if you’re writing direct SQL:

    SELECT t.* FROM page_views WHERE page_id = '10' AND customer_id IS NOT NULL GROUP BY customer_id
    UNION SELECT t.* FROM page_views WHERE page_id = '10' AND customer_id IS NULL GROUP BY ip_address;

How would we do this with Magento?

Magento implements the Zend_Db_Select class to handle the SELECT statements for its models and collections. This allows us to do some pretty cool things with collections. To access the Zend_Db_Select class instance for a collection, you just have to call $collection->getSelect().

Assuming we’re working with a module called “pages”, and our view model is called “page_view”, the following function from within the collection model will generate the above select statement.

<?php
public function getUniques()
    {        
        $select_a = Mage::getModel('pages/page_view')->getCollection()
            ->getSelect()
            ->where("customer_id IS NOT NULL")
            ->group(array("customer_id"));
        $select_b = Mage::getModel('pages/page_view')->getCollection()
            ->getSelect()
            ->where("customer_id IS NULL")
            ->group(array("ip_address"));
        
        $this->getSelect()->reset()->union(array(
            $select_a,
            $select_b
        ));
        
        return $this;
    }
?>

Then you just call $collection->getUniques() to modify your collection.

What’s happening here? If it’s not immediately obvious, what we’re doing here:

$this->getSelect()->reset()->union(array(
         $select_a,
         $select_b
     ));
?>

is making sure we’re modifying the Zend_Db_Select instance of the collection in question. That way, the defined collection returns data based on the modifications we’ve made. Calling “->reset()” does exactly that. It basically nullifies the select statement so that we can start from scratch. Try the same thing without the reset command and call $collection->getSelect()->__toString() on the returned data. It won’t look pretty.

We then use Zend_Db_Select’s “union” function, which takes an array of Zend_Db_Select instances as a parameter.

As usual when working with the select class of a collection, don’t go saving the models returned. This is only recommended for data output, or for indexing in a separate flat table.

Happy Magento-ing!

Tagged , ,

Copy Product data from one Magento store view to another

I found recently that adding a new store view to Magento was a pretty straightforward task, however I needed all products on the new store view to be the same as another. All new store-view catalog data inherits from the default store scope instead of a sibling store-view’s data. Copying this content from one store view to another through the Magento admin area is a very cumbersome task, especially if you have a large catalog of products with store-view specific data. I also didn’t think it was worth running code through a controller, so I decided to dust off my old SQL boots and take the fight to the database. This could easily be modified to take a product_id parameter as well, so get your teeth in.

Ultimately this wasn’t overly difficult, and it’s probably a useful exercise for someone trying to learn EAV. Since EAV bloats out tables quite significantly, there’s a number of tables to consider. I think I’ve covered them all; at least I have for my purposes. Let me know if there’s something you encounter that isn’t covered here and I’ll be sure to add it.

Also, as usual, always try this on a development server with a separate database.

All you have to do is change the ID’s defined at the start.

/*********************************************************
 *              Magento Product Data Copier              *
 *              mikebywaters.wordpress.com               *
 *********************************************************/

# DEFINE
SET @to_store := 6;      # the store_id of the recipient store
SET @from_store := 2;    # the store_id of the donor store

/*********************************************************
 * catalog_product_entity                                *
 *********************************************************/

# PROCESS datetime VALUES
DELETE FROM catalog_product_entity_datetime
WHERE store_id = @to_store;
INSERT INTO catalog_product_entity_datetime (
    store_id,
    entity_type_id,
    attribute_id,
    entity_id,
    value
) SELECT
    @to_store,
    entity_type_id,
    attribute_id,
    entity_id,
    value
FROM catalog_product_entity_datetime
WHERE store_id = @from_store;


# PROCESS decimal VALUES
DELETE FROM catalog_product_entity_decimal
WHERE store_id = @to_store;
INSERT INTO catalog_product_entity_decimal (
    store_id,
    entity_type_id,
    attribute_id,
    entity_id,
    value
) SELECT
    @to_store,
    entity_type_id,
    attribute_id,
    entity_id,
    value
FROM catalog_product_entity_decimal
WHERE store_id = @from_store;


# PROCESS gallery VALUES
DELETE FROM catalog_product_entity_gallery
WHERE store_id = @to_store;
INSERT INTO catalog_product_entity_gallery (
    store_id,
    entity_type_id,
    attribute_id,
    entity_id,
    position,
    value
) SELECT
    @to_store,
    entity_type_id,
    attribute_id,
    entity_id,
    position,
    value
FROM catalog_product_entity_gallery
WHERE store_id = @from_store;

# PROCESS int VALUES
DELETE FROM catalog_product_entity_int
WHERE store_id = @to_store;
INSERT INTO catalog_product_entity_int (
    store_id,
    entity_type_id,
    attribute_id,
    entity_id,
    value
) SELECT
    @to_store,
    entity_type_id,
    attribute_id,
    entity_id,
    value
FROM catalog_product_entity_int
WHERE store_id = @from_store;

# PROCESS text VALUES
DELETE FROM catalog_product_entity_text
WHERE store_id = @to_store;
INSERT INTO catalog_product_entity_text (
    store_id,
    entity_type_id,
    attribute_id,
    entity_id,
    value
) SELECT
    @to_store,
    entity_type_id,
    attribute_id,
    entity_id,
    value
FROM catalog_product_entity_text
WHERE store_id = @from_store;

# PROCESS varchar VALUES
DELETE FROM catalog_product_entity_varchar
WHERE store_id = @to_store;
INSERT INTO catalog_product_entity_varchar (
    store_id,
    entity_type_id,
    attribute_id,
    entity_id,
    value
) SELECT
    @to_store,
    entity_type_id,
    attribute_id,
    entity_id,
    value
FROM catalog_product_entity_varchar
WHERE store_id = @from_store;


/*********************************************************
 * catalog_product_entity_media_gallery                  *
 *********************************************************/

# PROCESS value VALUES
DELETE FROM catalog_product_entity_media_gallery_value
WHERE store_id = @to_store;
INSERT INTO catalog_product_entity_media_gallery_value (
    store_id,
    value_id,
    label,
    position,
    disabled
) SELECT
    @to_store,
    value_id,
    label,
    position,
    disabled
FROM catalog_product_entity_media_gallery_value
WHERE store_id = @from_store;

# IM UP TO HERE

/*********************************************************
 * catalog_product_option                                *
 *********************************************************/

# PROCESS price VALUES
DELETE FROM catalog_product_option_price
WHERE store_id = @to_store;
INSERT INTO catalog_product_option_price (
    store_id,
    option_id,
    price,
    price_type
) SELECT
    @to_store,
    option_id,
    price,
    price_type
FROM catalog_product_option_price
WHERE store_id = @from_store;

# PROCESS title VALUES
DELETE FROM catalog_product_option_title
WHERE store_id = @to_store;
INSERT INTO catalog_product_option_title (
    store_id,
    option_id,
    title
) SELECT
    @to_store,
    option_id,
    title
FROM catalog_product_option_title
WHERE store_id = @from_store;

# PROCESS type_price VALUES
DELETE FROM catalog_product_option_type_price
WHERE store_id = @to_store;
INSERT INTO catalog_product_option_type_price (
    store_id,
    option_type_id,
    price,
    price_type
) SELECT
    @to_store,
    option_type_id,
    price,
    price_type
FROM catalog_product_option_type_price
WHERE store_id = @from_store;

# PROCESS type_title VALUES
DELETE FROM catalog_product_option_type_title
WHERE store_id = @to_store;
INSERT INTO catalog_product_option_type_title (
    store_id,
    option_type_id,
    title
) SELECT
    @to_store,
    option_type_id,
    title
FROM catalog_product_option_type_title
WHERE store_id = @from_store;

/*********************************************************
 * catalog_product_super_attribute                       *
 *********************************************************/

# PROCESS label VALUES
DELETE FROM catalog_product_super_attribute_label
WHERE store_id = @to_store;
INSERT INTO catalog_product_super_attribute_label (
    store_id,
    product_super_attribute_id,
    use_default,
    value
) SELECT
    @to_store,
    product_super_attribute_id,
    use_default,
    value
FROM catalog_product_super_attribute_label
WHERE store_id = @from_store;

/*********************************************************
 * catalog_product_bundle_option                         *
 *********************************************************/

# PROCESS value VALUES
DELETE FROM catalog_product_bundle_option_value
WHERE store_id = @to_store;
INSERT INTO catalog_product_bundle_option_value (
    store_id,
    option_id,
    title
) SELECT
    @to_store,
    option_id,
    title
FROM catalog_product_bundle_option_value
WHERE store_id = @from_store;

Add an Item to the Cart with a Custom Price in Magento

Magento doesn’t offer the ability to add custom prices when adding items to your cart. This is a solution I’ve used on occasion.

You can use an observer class to listen to checkout_cart_product_add_after, and use a product’s “Super Mode” to set custom prices against the quote item.

In your /app/code/local/{namespace}/{yourmodule}/etc/config.xml:

    <config>
        ...
        <frontend>
            ...
            <events>
                <checkout_cart_product_add_after>
                    <observers>
                        <unique_event_name>
                            <class>{{modulename}}/observer</class>
                            <method>modifyPrice</method>
                        </unique_event_name>
                    </observers>
                </checkout_cart_product_add_after>
            </events>
            ...
        </frontend>
        ...
    </config>

And then create an Observer class at /app/code/local/{namespace}/{yourmodule}/Model/Observer.php

<?php
    class <namespace>_<modulename>_Model_Observer
    {
        public function modifyPrice(Varien_Event_Observer $obs)
        {
            // Get the quote item
            $item = $obs->getQuoteItem();
            // Ensure we have the parent item, if it has one
            $item = ( $item->getParentItem() ? $item->getParentItem() : $item );
            // Load the custom price
            $price = $this->_getPriceByItem($item);
            // Set the custom price
            $item->setCustomPrice($price);
            $item->setOriginalCustomPrice($price);
            // Enable super mode on the product.
            $item->getProduct()->setIsSuperMode(true);
        }
        
        protected function _getPriceByItem(Mage_Sales_Model_Quote_Item $item)
        {
            $price;
            
            //use $item to determine your custom price.
            
            return $price;
        }
        
    }