Automatic Cross Related Products – CSV Import

related_featured

If you have configurable products that have been associated with simple products, you can assign configurable products to related products. You probably know how this task can be boring. To avoid that (in most cases) boring task, I created this module so you don’t need to lose lots of your precious time.

You probably know how to associate related products in Magento, but let’s repeat for those who forgot or maybe don’t know.  Open Product_1 and assign Product_2, Product_3 and Product_4 as Related Products to Product_1. If you want to associate Product 2 to other (previously mentioned) Products 1, 3 and 4, you just follow the same procedure. If, perhaps, you don’t know what Related products, Up-sells or Cross-sells are,  check out related Inchoo and/or Magento post. Those posts will certainly get you on the right track.

To avoid this boring additional work, I’ve created Magento module that will automatically link all products in one step! Not only that, you can exclude some of the products from a list of products (if you wish to do that) in just one step, so that excluded product will not be anymore associated to other products from the list. One more thing, you can also upload CSV file with your product ID#, where each row represents collection of related products. Everything will be much easier to explain through examples, so let’s start!

First things first,  let’s explain configuration options.

Description options

1. Enabled

This configuration option will enable this enhancement to Magento Related Products functionality.

2. Remove un-assigned Products From a Collection

This is the interesting one… Imagine that you have products 1, 2, 3 and 4 and you want to associate them as Related Products (in a way that all of them are associated to each other – “cross related”). To avoid boring additional work, you’ve enabled this module. You would go in Magento administration to Catalog/Manage Products. Then select Product 1 and then click on Related Products tab.  After assigning products 2, 3 and 4, click on Save. And that’s it! If you select Product_2 you would see that products 1, 3 and 4 are all associated to Product_2. Similar is for products 3 and 4.

Now, for this configuration option, let’s say that you don’t want anymore Product_4 to be associated to collection of products 1, 2 and 3. Normally, in default Magento functionality you would need to select Product_1 and un-select Product_4. Then you would select Product_2 and un-select Product_4. Same for Product_3.  At the end, if you select Product_4 (which you want to exclude from your related collection of products 1, 2 and 3) you would see that Product_4 is connected to products 1, 2 and 3.

Let’s come back to our example – Cross Related enhancement. Firstly, we’ve associated products 2,3,4 to Product_1. Then we un-selected Product_4 from specified collection, so each products: 1, 2 and 3 would be associated with each other. In default Magento behaviour, Product_4 would be associated with products 1, 2 and 3. To remove, also in one step, Product_4 to be associated to products 1, 2 and 3 (we have had connected products 1, 2, 3, 4) you can enable this configuration option and system will automatically unassigned all “irrelevant” products from specified collection.
So basically, imaging that connecting on Product_1 products 2, 3, 4 is the same as you are sending one row in CSV file: “1”, “2”, “3”, “4”.
As the result, all of those products would be assigned to each other. Same, if you send (with this configuration option) again in next row “1”, “2”, “3” as a result you’ll have products 1, 2 and 3 associated to each other, BUT product 4 will be “alone” – more precisely, Product 4 will not have anymore associated products 1, 2 and 3.

3. Enable CSV Import

If this module is enabled and if this configuration option is enabled, you’ll see additional button in Magento AdministrationCatalog / Manage Products – “Cross Related CSV Import

Upload CSV file button

If you click on “Cross Related CSV Import” you’ll see next screen:

Import and check button

You can now Browse your csv file and click on “Check Data” button to upload and check your csv file, after which you’ll see “Import” button, if you have at least 1 row for insert. 🙂 Otherwise you’ll see next message without Import button: “File is totally invalid. Please fix errors and re-upload file“.
After you click on “Import”, you’ll see “Back” button so that you can re-upload some other file, if needed.

Note. “Check” button will upload your file (on success) in [MAGENTO_ROOT]/var/crossrelated/crossrelated.csv and “Import” button will connect those items and delete the file.

4. Exclude items from the cart

In Magento, by default, you don’t see related items on front-end in case if you have them in cart. If you don’t want to hide them from Related Products block on front-end you can enable this option. We needed this, so I hope it might help someone…

Code Snippets

Now let see some snippets of the code. For full module you’ll need to download archive file from here.

Model/Import.php

< ?php
 
/**
 * Import model
 *
 * @category    Inchoo
 * @package     Inchoo_CrossRelated
 * @author      Ivan Galamboš <ivan.galambos@inchoo.net>
 */
class Inchoo_CrossRelated_Model_Import extends Inchoo_CrossRelated_Model_Abstract
{
    /**
     * Form field names (and IDs)
     */
    const FIELD_NAME_SOURCE_FILE = 'import_file';
    const FIELD_NAME_IMG_ARCHIVE_FILE = 'import_image_archive';
 
    /**
     * Validates source file and returns validation result.
     *
     * @param string $sourceFile Full path to source file
     * @return bool
     */
    public function validateSource($sourceFile)
    {
        $result = $this->isDataValid($sourceFile);
 
        return $result;
    }
 
    /**
     * Move uploaded file
     *
     * @throws Mage_Core_Exception
     * @return string Source file path
     */
    public function uploadSource()
    {
        $uploader  = Mage::getModel('core/file_uploader', self::FIELD_NAME_SOURCE_FILE);
        $uploader->skipDbProcessing(true);
        $result    = $uploader->save(self::getWorkingDir());
        $extension = pathinfo($result['file'], PATHINFO_EXTENSION);
 
        $uploadedFile = $result['path'] . $result['file'];
 
        if (!$extension) {
            unlink($uploadedFile);
            Mage::throwException(Mage::helper('crossrelated')->__('Uploaded file has no extension'));
        }
 
        $sourceFile = self::getWorkingDir() . 'crossrelated';
        $sourceFile .= '.' . $extension;
 
        if(strtolower($uploadedFile) != strtolower($sourceFile)) {
 
            if (file_exists($sourceFile)) {
                unlink($sourceFile);
            }
 
            if (!@rename($uploadedFile, $sourceFile)) {
                Mage::throwException(Mage::helper('crossrelated')->__('Source file moving failed'));
            }
        }
 
        return $sourceFile;
    }
 
    /**
     * Import/Export working directory (source files, result files, lock files etc.).
     *
     * @return string
     */
    public static function getWorkingDir()
    {
        return Mage::getBaseDir('var') . DS . 'crossrelated' . DS;
    }
 
    /**
     * Import & unlink checked crossrelated csv file.
     *
     * @throws Mage_Core_Exception
     * @return void
     */
    public function importSource()
    {
        $sourceFile = self::getWorkingDir() . 'crossrelated';
        $sourceFile .= '.' . 'csv';
 
        if (file_exists($sourceFile)) {
 
            $io = new Varien_Io_File();
            $io->streamOpen($sourceFile, 'r');
 
            while ( $row = $io->streamReadCsv() ) {
 
                $checkThisRow = true;
 
                foreach ($row as $v) {
 
                    $v = trim($v);
                    if ( !ctype_digit($v) ) {
                        $checkThisRow = false;
                    }
 
                }
 
                if ( $checkThisRow ) {
                    $this->connectCsvDataToCrossRelated($row);
                }
            }
 
            unlink($sourceFile);
 
        } else {
            Mage::throwException(Mage::helper('crossrelated')->__('Source file does not exist'));
        }
 
    }
 
}

controllers/Adminhtml/Catalog/ProductController.php

< ?php
 
require_once 'Mage/Adminhtml/controllers/Catalog/ProductController.php';
 
/**
 * Catalog product controller
 *
 * @category    Inchoo
 * @package     Inchoo_CrossRelated
 * @author      Ivan Galamboš <ivan.galambos@inchoo.net>
 */
class Inchoo_CrossRelated_Adminhtml_Catalog_ProductController extends Mage_Adminhtml_Catalog_ProductController
{
    /**
     * Initialize layout.
     *
     * @return Inchoo_CrossRelated_Adminhtml_Catalog_ProductController
     */
    protected function _initAction()
    {
        $this->_title($this->__('Inchoo CrossRelated Products'))
            ->loadLayout()
        ;
 
        return $this;
    }
 
    /**
     * Import CrossRelated CSV data
     */
    public function csvimportAction()
    {
        $maxUploadSize = Mage::helper('crossrelated')->getMaxUploadSize();
        $this->_getSession()->addNotice(
            $this->__('Total size of uploadable files must not exceed %s', $maxUploadSize)
        );
        $this->_initAction()
            ->_title($this->__('CrossRelate CSV Import'))
            ->_addBreadcrumb($this->__('CrossRelate CSV Import'), $this->__('CrossRelate CSV Import'));
        $this->_setActiveMenu('catalog/products');
        $this->renderLayout();
    }
 
    /**
     * Start validation process action.
     *
     * @return void
     */
    public function validatecsvimportAction()
    {
        $data = $this->getRequest()->getPost();
 
        if ($data) {
 
            $this->loadLayout(false);
            /** @var $resultBlock Inchoo_CrossRelated_Block_Adminhtml_Import_Frame_Result */
            $resultBlock = $this->getLayout()->getBlock('import.frame.result.crossrelated');
 
            try {
                /** @var $import Inchoo_CrossRelated_Model_Import */
                $import = Mage::getModel('crossrelated/import');
 
                $validationResult = $import->validateSource($import->setData($data)->uploadSource());
 
                if (!$import->getProcessedRowsCount()) {
                    $resultBlock->addError($this->__('File does not contain data. Please upload another one'));
                } else {
 
                    if (!$validationResult) {
 
                        if ($import->getProcessedRowsCount() == $import->getInvalidRowsCount()) {
                            $resultBlock->addNotice(
                                $this->__('File is totally invalid. Please fix errors and re-upload file')
                            );
                        } else {
                                $resultBlock->addNotice(
                                    $this->__('Please fix errors and re-upload file or simply press "Import" button to skip rows with errors'),
                                    true
                                );
                        }
                        $error = $this->__('Invalid data (non-digits) in rows:') . ' ' . implode(', ', $import->getErrors());
                        $resultBlock->addError($error);
 
                    } else {
                            $resultBlock->addSuccess(
                                $this->__('File is valid! To start import process press "Import" button'), true
                            );
                    }
                    $resultBlock->addNotice($this->__('Checked rows: %d, invalid rows: %d', $import->getProcessedRowsCount(), $import->getInvalidRowsCount()));
                }
 
            } catch (Exception $e) {
                $resultBlock->addNotice($this->__('Please fix errors and re-upload file'))
                    ->addError($e->getMessage());
            }
            $this->renderLayout();
 
        } elseif ($this->getRequest()->isPost() && empty($_FILES)) {
 
            $this->loadLayout(false);
            $resultBlock = $this->getLayout()->getBlock('import.frame.result.crossrelated');
            $resultBlock->addError($this->__('File was not uploaded'));
            $this->renderLayout();
 
        } else {
 
            $this->_getSession()->addError($this->__('Data is invalid or file is not uploaded'));
            $this->_redirect('*/*/csvimport');
            return;
 
        }
 
        return;
    }
 
    /**
     * Start import process action.
     *
     * @return void
     */
    public function startcrossrelatedimportAction()
    {
        try {
 
            $this->loadLayout(false);
 
            /** @var $resultBlock Inchoo_CrossRelated_Block_Adminhtml_Import_Frame_Result */
            $resultBlock = $this->getLayout()->getBlock('import.frame.result.crossrelated');
 
            /** @var $import Inchoo_CrossRelated_Model_Import */
            $import = Mage::getModel('crossrelated/import');
            $import->importSource();
 
            $resultBlock->addAction('show', 'import_validation_container')
                ->addAction('innerHTML', 'import_validation_container_header', $this->__('Status'));
 
        } catch (Exception $e) {
 
            $resultBlock->addError($e->getMessage());
            $this->renderLayout();
            return;
 
        }
 
        $resultBlock->addAction('hide', array('edit_form', 'upload_button', 'messages'));
        $resultBlock->addSuccessRefresh($this->__('Import successfully done.'), true);
 
        $this->renderLayout();
 
        return;
    }
 
}

Note. For full module you’ll need to download archive file from here.

Feel free to create your own csv file with IDs of your desired products and let me know what do you think.

If you have any questions… we are here for you!


4 comments

  1. when i upload csv this types of error “Invalid data (non-digits) in rows: 1, 2” what can i do…

  2. thank you for the great article!
    i’ve uploaded all the module but nothing change in the admin section. what’s wrong?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.