Moving to Austin, TX

I have been married for 8 years now, and we have mostly lived up in the St. Paul / Minneapolis area of Minnesota. We’ve been talking about wanting to move down south for years, and we decided we just need to go for it. If we don’t just make the decision and do it, we may never end up doing it. Target time for move is June.

For those of you who might not know, it gets really cold here in the winter. We hate the cold. It just feels miserable. Average high in St. Paul in January is 27 degrees. Average high in Austin in January is 62 degrees. Yes, it gets hot in August in Austin, but I would take really hot over really cold any day. There’s also less daylight during the winter. In Austin, we’ll have nearly an hour and a half extra daylight at the peak of winter. Sunset in December here is at about 4:30pm. So, it’s dark when I get up, dark when I’m in the car going to work, and dark before I get in the car to go home. The only daylight is when I’m in an office building, and it’s too cold to want to go outside and enjoy any of the daylight.

The transition was made a lot easier with my current employer, August Ash, being willing to keep me on working remotely. So I will still have the same job. A huge plus of this setup is that I currently have to commute 45 minutes one-way. So I will gain 7 hours a week not having to drive a long way to work and back.

We have heard nothing but great things about the Austin area. If you have any suggestions about places to go, things to do, groups to get involved in, people to meet, etc., I would love to hear them.

Posted in Misc | Leave a comment

Magento Module: Hide Empty Categories

There are a number of ways that you can hide empty categories: You can disable them manually, you can modify the template output to check the product count and if zero don’t display it, etc. Another way is to write a module and intercept the catalog collection output before it’s sent to the templates.

I could easily package this up and put it up on Magento Connect, but I’m more into trying to help people understand Magento modules, so below is a walkthrough of how to create a module that will provide this functionality.

Note: Update – Fixed for Flat Categories too. This has only been tested on 1.6.x.

Step 1

Create the file ‘/app/etc/modules/Prattski_HideEmptyCategories.xml’. This file tells Magento that your module exists, if it’s enabled or not, and where to find it.

<?xml version="1.0"?>
<!--
/**
 * Hide Empty Categories
 *
 * @category    Prattski
 * @package     Prattski_HideEmptyCategories
 * @copyright   Copyright (c) 2011 Prattski (http://prattski.com/)
 * @author      Josh Pratt (Prattski)
 */
-->
<config>
    <modules>
        <Prattski_HideEmptyCategories>
            <active>true</active>
            <codePool>local</codePool>
        </Prattski_HideEmptyCategories>
    </modules>
</config>

Step 2

Now we create our module directory. Create ‘/app/code/local/Prattski/HideEmptyCategories/’. This is where our module will reside. Inside that directory, create this file: ‘etc/config.xml’. This file does a number of things: It tells Magento which version your module is, defines your model namespace and directory, and creates and event observer to observe every time a category collection is loaded.

<?xml version="1.0"?>
<!--
/**
 * Hide Empty Categories
 *
 * @category    Prattski
 * @package     Prattski_HideEmptyCategories
 * @copyright   Copyright (c) 2011 Prattski (http://prattski.com/)
 * @author      Josh Pratt (Prattski)
 */
-->
<config>
    <modules>
        <Prattski_HideEmptyCategories>
            <version>1.0.0</version>
        </Prattski_HideEmptyCategories>
    </modules>
    <global>
        <models>
            <hideemptycategories>
                <class>Prattski_HideEmptyCategories_Model</class>
            </hideemptycategories>
            <catalog_resource>
                <rewrite>
                    <category_flat>Prattski_HideEmptyCategories_Model_Catalog_Resource_Category_Flat</category_flat>
                </rewrite>
            </catalog_resource>
        </models>
    </global>
    <frontend>
        <events>
            <catalog_category_collection_load_after>
                <observers>
                    <hideemptycategories>
                        <type>singleton</type>
                        <class>hideemptycategories/observer</class>
                        <method>catalogCategoryCollectionLoadAfter</method>
                    </hideemptycategories>
                </observers>
            </catalog_category_collection_load_after>
        </events>
    </frontend>
</config>

Step 3

Let’s create our observer model. As you can see in the file above, we are going to have the ‘catalogCategoryCollectionLoadAfter’ method run in this file when it observes the ‘catalog_category_collection_load_after’ event. Create this file: Model/Observer.php

<?php
/**
 * Hide Empty Categories
 *
 * @category    Prattski
 * @package     Prattski_HideEmptyCategories
 * @copyright   Copyright (c) 2011 Prattski (http://prattski.com/)
 * @author      Josh Pratt (Prattski)
 */
 
/**
 * Event Observer
 *
 * @category    Prattski
 * @package     Prattski_HideEmptyCategories
 */
class Prattski_HideEmptyCategories_Model_Observer extends Mage_Core_Model_Abstract
{
    /**
     * Remove hidden caegories from the collection
     *
     * @param Varien_Event_Observer $observer
     */
    public function catalogCategoryCollectionLoadAfter($observer)
    {
        if ($this->_isApiRequest()) return;
        $collection = $observer->getEvent()->getCategoryCollection();
        $this->_removeHiddenCollectionItems($collection);
    }
 
    /**
     * Remove hidden items from a product or category collection
     * 
     * @param Mage_Eav_Model_Entity_Collection_Abstract|Mage_Core_Model_Mysql4_Collection_Abstract $collection
     */
    public function _removeHiddenCollectionItems($collection)
    {
        // Loop through each category or product
        foreach ($collection as $key => $item)
        {
            // If it is a category
            if ($item->getEntityTypeId() == 3) {
 
                if ($item->getProductCount() < 1) {
                    $collection->removeItemByKey($key);
                }
            }
        }
    }
 
    /**
     * Return true if the reqest is made via the api
     *
     * @return boolean
     */
    protected function _isApiRequest()
    {
        return Mage::app()->getRequest()->getModuleName() === 'api';
    }
}

Step 4

Magento has the ability to turn on Flat Categories to speed up the performance whenever categories are loaded. The event that we are observing above does not work when Flat Categories are enabled. So, thanks to Vinai’s help, we need to rewrite a method in a core file to make this work. Create this file: Model/Catalog/Resource/Category/Flat.php

<?php
/**
 * Hide Empty Categories
 *
 * @category    Prattski
 * @package     Prattski_HideEmptyCategories
 * @copyright   Copyright (c) 2011 Prattski (http://prattski.com/)
 * @author      Josh Pratt (Prattski)
 */
 
/**
 * Event Observer
 *
 * @category    Prattski
 * @package     Prattski_HideEmptyCategories
 */
class Prattski_HideEmptyCategories_Model_Catalog_Resource_Category_Flat
    extends Mage_Catalog_Model_Resource_Category_Flat
{
    /**
     * Load nodes by parent id
     *
     * @param unknown_type $parentNode
     * @param integer $recursionLevel
     * @param integer $storeId
     * @return Mage_Catalog_Model_Resource_Category_Flat
     */
    protected function _loadNodes($parentNode = null, $recursionLevel = 0, $storeId = 0)
    {
        $nodes = parent::_loadNodes($parentNode, $recursionLevel, $storeId);
 
        foreach ($nodes as $node) {
            if ($node->getProductCount() == 0) {
                unset($nodes[$node->getId()]);
            }
        }
        return $nodes;
    }
}

Step 5

To recap all the files we have made:

  • /app/etc/modules/Prattski_HideEmptyCategories.xml
  • /app/code/local/Prattski/HideEmptyCategories/etc/config.xml
  • /app/code/local/Prattski/HideEmptyCategories/Model/Observer.php
  • /app/code/local/Prattski/HideEmptyCategories/Model/Catalog/Resource/Category/Flat.php

Test it out! You’ll be able to see if Magento recognizes your module by going to System > Configuration > Advanced. If you see the module in the list, Magento knows it’s there. You’ll have to have some categories in your system, some with products in them, some without, to see if it’s working properly on the frontend.

I hope you enjoyed learning how to build this module. If you find any issues or bugs, please let me know.

Posted in Magento | 17 Comments

Looking For Small/Medium Magento Module Jobs

I am coming back to the freelance market for a bit, and I am looking for small/medium Magento module jobs. If you or anyone you know needs any work done in this area, please let me know!

Posted in Magento | 1 Comment

Magento: Modify Collection To Include Comma Separated Values From Another Table

It was rather difficult to come up with a title for this post, so I’m not sure that it is completely accurate. But, I have been working on a module, and I needed to modify the catalog/product collection to add a column that contains comma separated skus of the related products associated to each product.

First step is to get the product collection,

$collection = Mage::getModel('catalog/product')->getCollection();

Now we need to modify the collection to add a new select column. The mysql is also important here. You’ll see that I’m using GROUP_CONCAT and DISTINCT, and towards the bottom specifying to group by ‘e.entity_id’. I’m not going to dive into why this is necessary or how it all works. You’ll be better off visiting the mysql documentation on those.

$collection = Mage::getModel('catalog/product')->getCollection();
 
$collection->getSelect()
    ->columns('GROUP_CONCAT(DISTINCT (SELECT sku FROM catalog_product_entity WHERE entity_id = r.linked_product_id) SEPARATOR \', \') AS related_skus')
    ->joinLeft(array('r' => 'catalog_product_link'), 'r.product_id = e.entity_id AND r.link_type_id = 1')
    ->group('e.entity_id');

The result of this is an additional field in the collection called ‘related_skus’ that is a comma-space separated list of skus that are related products for each product in the collection.

If you want to log the actual query that is generated by this, simply add this line below the code above:

Mage::log($collection->getSelect()->__toString());

Hopefully this helps you in some way!

Posted in Magento, MySQL | Leave a comment

Magento: Adding/Updating Products with V2 API

Magento unfortunately has virtually no documentation on the V2 API. I needed to prove to someone today that using the V2 API, you can indeed create a product, assign it to 2 or more websites, and set the product price to different values for each website (as long as you’ve set the price scope to “Website” instead of the default “Global” which is in System > Configuration > Catalog > Price).

In this example, I set the initial product data to create, and run the API method ‘catalogProductCreate’. After the product has been created, I then set the product price for the wholesale website, and then run the ‘catalogProductUpdate’ method. The key here is passing in the correct store view code. Ultimately, the product is created with the price of $45. Then the wholesale website is updated to set a price of $20.

<?php
 
//error_reporting(E_ALL);
//ini_set('display_errors', '1');
 
/**
 * Get API Session
 */
$client = new SoapClient('http://core.local/api/v2_soap?wsdl=1');
$session = $client->login('augustash', 'password');
 
/**
 * Set Product Data
 */
$newProductData                     = new stdClass();
$newProductData->name               = 'Product Name';
$newProductData->description        = 'Description';
$newProductData->short_description  = 'Short Description';
$newProductData->websites	    = array(1,2);
$newProductData->categories         = array(7,15);
$newProductData->status             = 1;
$newProductData->price              = 45;
$newProductData->tax_class_id       = 2;
$newProductData->weight             = 1;
 
/**
 * Create Product Using V2 API
 */
$result = $client->catalogProductCreate(
    $session,           // Soap Session
    'simple',           // Product Type
    4,                  // Attribute Set Id (Default)
    'product-sku',      // Product Sku
    $newProductData     // Product Data
);
 
 
/**
 * Set Price for Wholesale Website
 */
$newProductData         = new stdClass();
$newProductData->price  = 20;
 
/**
 * Update Product with Wholesale Price using V2 API
 */
$result = $client->catalogProductUpdate(
    $session,           // Soap Session
    'product-sku',      // Product Sku
    $newProductData,    // Product Data
    'wholesale'         // Store View Code
);
 
 
/**
 * End Session
 */
$client->endSession($session);

If you are wondering where to look to find out what values you can pass in, and what parameters are needed when making V2 API calls, you’ll need to look at the wsdl. In app/code/core/Mage/Catalog/etc/wsdl.xml, you’ll find the product API info. I’ve extracted the product data fields that you can pass in when creating a product:

<complexType name="catalogProductCreateEntity">
    <all>
        <element name="categories" type="typens:ArrayOfString" minOccurs="0" />
        <element name="websites" type="typens:ArrayOfString" minOccurs="0" />
        <element name="name" type="xsd:string" minOccurs="0" />
        <element name="description" type="xsd:string" minOccurs="0" />
        <element name="short_description" type="xsd:string" minOccurs="0" />
        <element name="weight" type="xsd:string" minOccurs="0" />
        <element name="status" type="xsd:string" minOccurs="0" />
        <element name="url_key" type="xsd:string" minOccurs="0" />
        <element name="url_path" type="xsd:string" minOccurs="0" />
        <element name="visibility" type="xsd:string" minOccurs="0" />
        <element name="category_ids" type="typens:ArrayOfString" minOccurs="0" />
        <element name="website_ids" type="typens:ArrayOfString" minOccurs="0" />
        <element name="has_options" type="xsd:string" minOccurs="0" />
        <element name="gift_message_available" type="xsd:string" minOccurs="0" />
        <element name="price" type="xsd:string" minOccurs="0" />
        <element name="special_price" type="xsd:string" minOccurs="0" />
        <element name="special_from_date" type="xsd:string" minOccurs="0" />
        <element name="special_to_date" type="xsd:string" minOccurs="0" />
        <element name="tax_class_id" type="xsd:string" minOccurs="0" />
        <element name="tier_price" type="typens:catalogProductTierPriceEntityArray" minOccurs="0" />
        <element name="meta_title" type="xsd:string" minOccurs="0" />
        <element name="meta_keyword" type="xsd:string" minOccurs="0" />
        <element name="meta_description" type="xsd:string" minOccurs="0" />
        <element name="custom_design" type="xsd:string" minOccurs="0" />
        <element name="custom_layout_update" type="xsd:string" minOccurs="0" />
        <element name="options_container" type="xsd:string" minOccurs="0" />
        <element name="additional_attributes" type="typens:associativeArray" minOccurs="0" />
    </all>
</complexType>

Here are the parameters that can be passed in when using the create and update methods:

<message name="catalogProductCreateRequest">
    <part name="sessionId" type="xsd:string" />
    <part name="type" type="xsd:string" />
    <part name="set" type="xsd:string" />
    <part name="sku" type="xsd:string" />
    <part name="productData" type="typens:catalogProductCreateEntity" />
</message>
 
<message name="catalogProductUpdateRequest">
    <part name="sessionId" type="xsd:string" />
    <part name="product" type="xsd:string" />
    <part name="productData" type="typens:catalogProductCreateEntity" />
    <part name="storeView" type="xsd:string" />
    <part name="productIdentifierType" type="xsd:string" />
</message>
Posted in Magento | Leave a comment

Magento: Bug With Editing Date & Time, Time Custom Options

Using Magento 1.5.0.1, I noticed an interesting bug this afternoon. If you have setup any Date & Time or Time custom attributes, and have made them NOT required, here’s something you may notice:

When initially adding the product to the cart, and you leave the Date & Time or Time custom options NOT filled out, your product will be added to the cart just as you would expect. The untouched custom options are not applied to the product in your cart. However, in the rare case where a user is clicking the “Edit” link in the cart to edit it, and you still leave those custom options untouched, the validation will bark at you saying “Field is not complete” on those custom options. The only way to edit that product then is to put values in for those fields. Magento updates the product as it should, but you now have those other custom options in play, which could screw things up.

I have submitted the bug to Magento: http://www.magentocommerce.com/bug-tracking/issue?issue=12183

Posted in Magento | Leave a comment

Disappointed with Magento Module Sales/Distribution

Magento, from the beginning, has offered a system of packaging extensions, and installing those packages. It was a smart move, and one that I really appreciated. There are a lot of developers out there who properly package their extensions (everything that you can get directly off Magento Connect is packaged). However, there are still loads of developers who do not package their extensions. This is especially true with developers who sell their extensions.

One particular developer that I’d like to point out my frustration with is AITOC. Before I get started though, they ARE talented developers, and they make good functioning modules. However, I can’t stand their pricing and distributing philosophies.

Most of their modules are based off of how many enabled products you have in your catalog, how many stores/websites you have setup in Magento, or how many admin accounts you have setup. Let’s say you want to use one of their modules for 50 of your products, but you have 10,000 products in your catalog. Or, you want to use one of their modules for one of your websites, but you have 10 websites setup in Magento. With both of these scenarios, prepare to spend big bucks. You can easily go from a $99 module, to a $599 module. I hate that.

Also, let’s say you have 950 products in your catalog, and you buy a module from them and you get the license that allows up to 1000 products. When you hit 1001, the functionality of that module will disable itself.

Now to their distribution. When you buy one of their modules, prepare for a headache! They are loaded and bloated full of files. It’s a mess. You cannot install their module with a simple command line. Instead, you need to upload a ton of files, set special permissions, and then walk through a somewhat complicated installation process. There’s even a module in there just to install/manage the module you want to install, loading up your Magento install with over 100 additional files. They override a few Core Magento files and send out a web service request, checking your license to make sure it’s valid before it will fully install your module. Almost every single file in their module is wrapped in a PHP call that checks to make sure you have properly installed it, and that you are not exceeding the license rule (# of products, stores, admin accounts, etc.).

They make it a headache to install, a headache to maintain, and a nightmare to fully uninstall and remove all the files. If they properly packaged their extension, it would be cakewalk to install, update, and uninstall.

Why do they do it? It’s simple. As for their pricing, they want to make more money off you. They tease you with a price, but many people will not pay the base price for their modules. Why do they not package their extension? They want more control over you and your store, and more control ensuring people aren’t abusing their license (because of the dumb limitations they enforce) or re-using it over and over, or passing it along to other people to use.

They are smart in that regard, but when it become a total pain for the end-user, you should re-think the process. I think they need to ditch the limitations. And, when it comes to the modules, properly package them up so that they can be installed with one command or using Magneto Connect. There are other ways of protecting a module from being re-used or re-distributed like IonCube that are far less of a headache to deal with.

So, if you are a Magento developer distributing or selling modules, please, make them into a proper Magento extension. It doesn’t take long, it’s not hard, and it works way, way better. And, don’t put annoying limitations on them like AITOC does.

Posted in Magento | 36 Comments

Magento: Revised Multiple Image Import Module (1.5 +)

Back in 2009, I made this post about my first multiple image import module. It was quite popular, and worked all the way up through Magento 1.4.x. With the release of 1.5, it no longer worked due to core changes Magento made.

We finally ran into a client who needed this functionality on 1.5, so I have re-factored the module, and added a couple new features. Instead of just packaging up the module though, like last time, I will walk you through how to create it yourself (so that hopefully some of you will learn a little more about how to create modules).

Note: This will not work on Magento 1.4.x and lower. For a version that works on 1.4.x and lower (which is a little more basic too), please see this post.

What does this module do?

  • Imports multiple images (of course) in addition to the 3 main product images (image, small_image, thumbnail)
  • Allows you to enable or disable (in the system config) excluding any main product images (image, small_image, thumbnail) from showing up in the gallery (the exclude checkbox)
  • Allows you to specify the column name for your multiple images in the system config

How it works: When you do a product import, all you have to do is add a new column to your CSV with the header of your choice (which you can change in the system config under Catalog > Catalog > Import Image Options – By default it’s ‘additional_images’). In that column, you can have as many images as you’d like. Simply separate the image names with a semi-colon: ‘image1.jpg;image2.jpg;image3.jpg’. Just be sure you’ve put the images in /media/import/ like you would normally do.

Enough talk, let’s get to building the module. In my example, I’m using the ‘Prattski’ namespace for my module. You can certainly change that if you would like, just make sure to change it in ever place you see it.

Step 1

Create the file ‘/app/etc/modules/Prattski_ImageImport.xml’. This file tells Magento that your module exists, if it’s enabled or not, and where to find it.

<?xml version="1.0"?>
<config>
    <modules>
        <Prattski_ImageImport>
            <active>true</active>
            <codePool>local</codePool>
        </Prattski_ImageImport>
    </modules>
</config>

Step 2

Now we create our module directory. Create ‘/app/code/local/Prattski/ImageImport/’. This is where our module will reside. Inside that directory, create this file: ‘etc/config.xml’. This file does a number of things: It tells Magento which version your module is, defines your model namespace and directory, rewrites a core model with your own, defines your helper namespace and directory, and sets default values in the system config.

<?xml version="1.0"?>
<config>
    <modules>
        <Prattski_ImageImport>
            <version>0.1.0</version>
        </Prattski_ImageImport>
    </modules>
    <global>
        <models>
            <imageimport>
                <class>Prattski_ImageImport_Model</class>
            </imageimport>
            <catalog>
                <rewrite>
                    <convert_adapter_product>Prattski_ImageImport_Model_Catalog_Convert_Adapter_Product</convert_adapter_product>
                </rewrite>
            </catalog>
        </models>
        <helpers>
            <imageimport>
                <class>Prattski_ImageImport_Helper</class>
            </imageimport>
        </helpers>
    </global>
    <default>
        <catalog>
            <catalog>
                <image_exclude_enabled>1</image_exclude_enabled>
                <multiple_image_column_name>additional_images</multiple_image_column_name>
            </catalog>
        </catalog>
    </default>
</config>

Step 3

Let’s create our module’s helper. This isn’t necessary, but always a good practice to include it, as it is required for locale stuff, and it’s a ready utility if ever needed. In our case, we’ll just create a blank helper. Create this file in your module directory ‘Helper/Data.php’:

<?php
/**
 * Prattski Import Images Module
 *
 * @category    Prattski
 * @package     Prattski_ImageImport
 * @copyright   Copyright (c) 2011 Prattski (http://prattski.com)
 * @author      Prattski (Josh Pratt)
 */
 
/**
 * Helper
 *
 * @category    Prattski
 * @package     Prattski_ImageImport
 */
class Prattski_ImageImport_Helper_Data extends Mage_Core_Helper_Abstract
{
 
}

Step 4

Now we will create our system config. This will give us the ability to turn on or off excluding the main product images, and specify the column in our CSV for the additional images. Note: In our config.xml file that we created, you’ll see the ‘default’ tag near the bottom. There we pre-populate our system config fields that we are creating here with data so that they aren’t blank upon module installation. Create this file in your module directory: ‘etc/system.xml’:

<?xml version="1.0"?>
<config>
    <sections>
        <catalog>
            <groups>
                <catalog>
                    <label>Import Image Options</label>
                    <frontend_type>text</frontend_type>
                    <sort_order>999</sort_order>
                    <show_in_default>1</show_in_default>
                    <show_in_website>1</show_in_website>
                    <show_in_store>0</show_in_store>
                    <fields>
                        <image_exclude_enabled translate="label" module="imageimport">
                            <label>Enable Excluded Images</label>
                            <frontend_type>select</frontend_type>
                            <source_model>adminhtml/system_config_source_yesno</source_model>
                            <sort_order>10</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>0</show_in_store>
                            <comment>If the product image is a main product image (image, small_image, or thumbnail), automatically check the exlcude checkbox.</comment>
                        </image_exclude_enabled>
                        <multiple_image_column_name translate="label" module="imageimport">
                            <label>Multiple Image Column Name</label>
                            <frontend_type>text</frontend_type>
                            <sort_order>20</sort_order>
                            <show_in_default>1</show_in_default>
                            <show_in_website>1</show_in_website>
                            <show_in_store>0</show_in_store>
                            <comment>The header in your CSV where you will put your additional images to import. When importing multiple images, separate with a semi-colon: 'image1.jpg;image2.jpg;image3.jpg'</comment>
                        </multiple_image_column_name>
                    </fields>
                </catalog>
            </groups>
        </catalog>
    </sections>
</config>

Step 5

Finally, we’ll create the model that will rewrite the core module to add in our new functionality. You’ll see towards the bottom (at line 230) that I’ve added two code blocks. The first block will exclude (or not) based on the system configuration setting we setup. The second is what imports all the additional images. Create this file in your module directory ‘Model/Catalog/Convert/Adapter/Product.php’:

<?php
/**
 * Prattski Import Images Module
 *
 * @category    Prattski
 * @package     Prattski_ImageImport
 * @copyright   Copyright (c) 2011 Prattski (http://prattski.com)
 * @author      Prattski (Josh Pratt)
 */
 
/**
 * Product Import Adapter
 *
 * @category    Prattski
 * @package     Prattski_ImageImport
 */
class Prattski_ImageImport_Model_Catalog_Convert_Adapter_Product extends Mage_Catalog_Model_Convert_Adapter_Product
{
    /**
     * Save product (import)
     *
     * @param array $importData
     * @throws Mage_Core_Exception
     * @return bool
     */
    public function saveRow(array $importData)
    {
        $product = $this->getProductModel()
            ->reset();
 
        if (empty($importData['store'])) {
            if (!is_null($this->getBatchParams('store'))) {
                $store = $this->getStoreById($this->getBatchParams('store'));
            } else {
                $message = Mage::helper('catalog')->__(
                    'Skipping import row, required field "%s" is not defined.',
                    'store'
                );
                Mage::throwException($message);
            }
        }
        else {
            $store = $this->getStoreByCode($importData['store']);
        }
 
        if ($store === false) {
            $message = Mage::helper('catalog')->__(
                'Skipping import row, store "%s" field does not exist.',
                $importData['store']
            );
            Mage::throwException($message);
        }
 
        if (empty($importData['sku'])) {
            $message = Mage::helper('catalog')->__('Skipping import row, required field "%s" is not defined.', 'sku');
            Mage::throwException($message);
        }
        $product->setStoreId($store->getId());
        $productId = $product->getIdBySku($importData['sku']);
 
        if ($productId) {
            $product->load($productId);
        }
        else {
            $productTypes = $this->getProductTypes();
            $productAttributeSets = $this->getProductAttributeSets();
 
            /**
             * Check product define type
             */
            if (empty($importData['type']) || !isset($productTypes[strtolower($importData['type'])])) {
                $value = isset($importData['type']) ? $importData['type'] : '';
                $message = Mage::helper('catalog')->__(
                    'Skip import row, is not valid value "%s" for field "%s"',
                    $value,
                    'type'
                );
                Mage::throwException($message);
            }
            $product->setTypeId($productTypes[strtolower($importData['type'])]);
            /**
             * Check product define attribute set
             */
            if (empty($importData['attribute_set']) || !isset($productAttributeSets[$importData['attribute_set']])) {
                $value = isset($importData['attribute_set']) ? $importData['attribute_set'] : '';
                $message = Mage::helper('catalog')->__(
                    'Skip import row, the value "%s" is invalid for field "%s"',
                    $value,
                    'attribute_set'
                );
                Mage::throwException($message);
            }
            $product->setAttributeSetId($productAttributeSets[$importData['attribute_set']]);
 
            foreach ($this->_requiredFields as $field) {
                $attribute = $this->getAttribute($field);
                if (!isset($importData[$field]) && $attribute && $attribute->getIsRequired()) {
                    $message = Mage::helper('catalog')->__(
                        'Skipping import row, required field "%s" for new products is not defined.',
                        $field
                    );
                    Mage::throwException($message);
                }
            }
        }
 
        $this->setProductTypeInstance($product);
 
        if (isset($importData['category_ids'])) {
            $product->setCategoryIds($importData['category_ids']);
        }
 
        foreach ($this->_ignoreFields as $field) {
            if (isset($importData[$field])) {
                unset($importData[$field]);
            }
        }
 
        if ($store->getId() != 0) {
            $websiteIds = $product->getWebsiteIds();
            if (!is_array($websiteIds)) {
                $websiteIds = array();
            }
            if (!in_array($store->getWebsiteId(), $websiteIds)) {
                $websiteIds[] = $store->getWebsiteId();
            }
            $product->setWebsiteIds($websiteIds);
        }
 
        if (isset($importData['websites'])) {
            $websiteIds = $product->getWebsiteIds();
            if (!is_array($websiteIds) || !$store->getId()) {
                $websiteIds = array();
            }
            $websiteCodes = explode(',', $importData['websites']);
            foreach ($websiteCodes as $websiteCode) {
                try {
                    $website = Mage::app()->getWebsite(trim($websiteCode));
                    if (!in_array($website->getId(), $websiteIds)) {
                        $websiteIds[] = $website->getId();
                    }
                }
                catch (Exception $e) {}
            }
            $product->setWebsiteIds($websiteIds);
            unset($websiteIds);
        }
 
        foreach ($importData as $field => $value) {
            if (in_array($field, $this->_inventoryFields)) {
                continue;
            }
            if (is_null($value)) {
                continue;
            }
 
            $attribute = $this->getAttribute($field);
            if (!$attribute) {
                continue;
            }
 
            $isArray = false;
            $setValue = $value;
 
            if ($attribute->getFrontendInput() == 'multiselect') {
                $value = explode(self::MULTI_DELIMITER, $value);
                $isArray = true;
                $setValue = array();
            }
 
            if ($value && $attribute->getBackendType() == 'decimal') {
                $setValue = $this->getNumber($value);
            }
 
            if ($attribute->usesSource()) {
                $options = $attribute->getSource()->getAllOptions(false);
 
                if ($isArray) {
                    foreach ($options as $item) {
                        if (in_array($item['label'], $value)) {
                            $setValue[] = $item['value'];
                        }
                    }
                } else {
                    $setValue = false;
                    foreach ($options as $item) {
                        if ($item['label'] == $value) {
                            $setValue = $item['value'];
                        }
                    }
                }
            }
 
            $product->setData($field, $setValue);
        }
 
        if (!$product->getVisibility()) {
            $product->setVisibility(Mage_Catalog_Model_Product_Visibility::VISIBILITY_NOT_VISIBLE);
        }
 
        $stockData = array();
        $inventoryFields = isset($this->_inventoryFieldsProductTypes[$product->getTypeId()])
            ? $this->_inventoryFieldsProductTypes[$product->getTypeId()]
            : array();
        foreach ($inventoryFields as $field) {
            if (isset($importData[$field])) {
                if (in_array($field, $this->_toNumber)) {
                    $stockData[$field] = $this->getNumber($importData[$field]);
                }
                else {
                    $stockData[$field] = $importData[$field];
                }
            }
        }
        $product->setStockData($stockData);
 
        $mediaGalleryBackendModel = $this->getAttribute('media_gallery')->getBackend();
 
        $arrayToMassAdd = array();
 
        foreach ($product->getMediaAttributes() as $mediaAttributeCode => $mediaAttribute) {
                if (isset($importData[$mediaAttributeCode])) {
                $file = trim($importData[$mediaAttributeCode]);
                if (!empty($file) && !$mediaGalleryBackendModel->getImage($product, $file)) {
                    $arrayToMassAdd[] = array('file' => trim($file), 'mediaAttribute' => $mediaAttributeCode);
                }
            }
        }
 
        /**
         * Added code to enable/disable the exclude checkbox for main product images
         */
 
        $_exclude = (Mage::getStoreConfig('catalog/catalog/image_exclude_enabled')) ? true : false;
 
        $addedFilesCorrespondence = $mediaGalleryBackendModel->addImagesWithDifferentMediaAttributes(
            $product,
            $arrayToMassAdd, Mage::getBaseDir('media') . DS . 'import',
            false,
            $_exclude
        );
 
        /**
         * End new code
         */
 
        /**
         * Added code to allow the import of additional product images
         */
        $_column = Mage::getStoreConfig('catalog/catalog/multiple_image_column_name');
        if ($importData[$_column]) {
            $additionalImages = explode(';',$importData[$_column]);
            foreach ($additionalImages as $image) {
                if (!empty($image) && !$mediaGalleryBackendModel->getImage($product, $image)) {
                    $arrayToMassAdd2[] = array('file' => trim($image));
                }
            }
        }
 
        $addedFilesCorrespondence .= $mediaGalleryBackendModel->addImagesWithDifferentMediaAttributes(
            $product,
            $arrayToMassAdd2, Mage::getBaseDir('media') . DS . 'import',
            false,
            false
        );
        /**
         * End new code
         */
 
        foreach ($product->getMediaAttributes() as $mediaAttributeCode => $mediaAttribute) {
            $addedFile = '';
            if (isset($importData[$mediaAttributeCode . '_label'])) {
                $fileLabel = trim($importData[$mediaAttributeCode . '_label']);
                if (isset($importData[$mediaAttributeCode])) {
                    $keyInAddedFile = array_search($importData[$mediaAttributeCode],
                        $addedFilesCorrespondence['alreadyAddedFiles']);
                    if ($keyInAddedFile !== false) {
                        $addedFile = $addedFilesCorrespondence['alreadyAddedFilesNames'][$keyInAddedFile];
                    }
                }
 
                if (!$addedFile) {
                    $addedFile = $product->getData($mediaAttributeCode);
                }
                if ($fileLabel && $addedFile) {
                    $mediaGalleryBackendModel->updateImage($product, $addedFile, array('label' => $fileLabel));
                }
            }
        }
 
        $product->setIsMassupdate(true);
        $product->setExcludeUrlRewrite(true);
 
        $product->save();
 
        return true;
    }
}

Step 6

To recap all the files we have made:

  • /app/etc/modules/Prattski_ImageImport.xml
  • /app/code/local/Prattski/ImageImport/etc/config.xml
  • /app/code/local/Prattski/ImageImport/etc/system.xml
  • /app/code/local/Prattski/ImageImport/Helper/Data.php
  • /app/code/local/Prattski/ImageImport/Model/Catalog/Convert/Adapter/Product.php

Test it out! You’ll be able to see if Magento recognizes your module by going to System > Configuration > Advanced. If you see the module in the list, Magento knows it’s there. You can also go to System > Configuration > Catalog > Catalog > Import Image Options to see the configuration options we setup there.

If all looks good, then you are ready for a test. Setup your CSV import file, including a new column called ‘additional_images’ (or whatever you changed it to in the system config) and populate it with images to import. When ready, go to System > Import/Export > Dataflow – Profiles and select the Import All Products and give it a go!

I hope you enjoyed learning how to build this module. If you find any issues or bugs, please let me know. Remember, this is for Magento 1.5.x and probably will not work on 1.4.x and lower.

Posted in Magento | 22 Comments

Magento API: V2 API Does Not Return Custom Order Attributes

I am working with a 3rd party integrator to make an integration between Magento and Sage AccPac. We planned from the beginning to pass them custom order attributes and custom order-item attributes, which I verified come through the API, seeing both custom order attributes, and custom order-item attributes returned.

Now that they’ve got their stuff up and running, I just got an email telling me that they do not see any of these custom attributes coming through. At first I thought maybe they were just blind, but through investigating further, I found that they are using .NET and therefore have chosen to use the V2 API.

I then experimented with the same sales_order.info method with the V2 and discovered that they are right. No custom order or custom order-item attributes are returned!

If I figure out why, I will update this post. If you know anything about this, please comment!

Update:

Actually, I found out that the V2 returns WAY less information than the standard soap API. Why is this!?

Update 2:

I believe I’ve found out the ‘why’ as to why there are so many fewer attributes, and why no custom attributes come through the V2 methods: According to a .NET developer I talked with, languages like .NET and Java need to know the number of properties that are going to be returned. For example, if .NET expects to get 5 attributes, but because of a custom attribute we added it gets 6, bad things happen. So, the V2 of the api will always only ever return a very specific set of attributes that does not change.

As for where in the code it is doing this limitation, I am not quite sure yet. When/if I figure it out, and how to modify it, I will make a post on that.

Posted in Magento | 2 Comments

Magento API: v2_soap Does Not Use Call()

If you are utilizing Magento’s API and have checked out the documentation, you’ll see that they have included a V2 of their soap API saying:

As of v1.3 you may also use http://yourmagentohost/api/v2_soap?wsdl=1 which has been added to improve compatbility with Java and .NET.

If you find the need to utilize the v2_soap API, take note that you will need to call your methods differently than the documentation explains.

For example, if you would like to use the sales_order.info method, using the standard soap API, you would do the following to get all the order info for order number 1000000142:

$client = new SoapClient('http://yourwebsite.com/api/soap/?wsdl');
$session = $client->login('username', 'password');
$result = $client->call($session, 'sales_order.info', '1000000142');

If you need to use the V2 API, you need to so it slightly different. Instead of using the call() method, you call the specific API method instead:

$client = new SoapClient('http://yourwebsite.com/api/soap/?wsdl');
$session = $client->login('username', 'password');
$result = $client->salesOrderInfo($session, '1000000142');

So, for V2, you must convert the method names in the Magento documentation to a camel-case method name.

Posted in Magento | Leave a comment