Overview

Packages

  • currencysymbol
  • MAbout
  • Mage
    • Admin
    • Adminhtml
    • AdminNotification
    • Api
    • Api2
    • Authorizenet
    • Backup
    • Bundle
    • Captcha
    • Catalog
    • CatalogIndex
    • CatalogInventory
    • CatalogRule
    • CatalogSearch
    • Centinel
    • Checkout
    • Cms
    • Compiler
    • Connect
    • Contacts
    • Core
    • Cron
    • CurrencySymbol
    • Customer
    • Dataflow
    • Directory
    • DirtectPost
    • Downloadable
    • Eav
    • GiftMessage
    • GoogleAnalytics
    • GoogleBase
    • GoogleCheckout
    • ImportExport
    • Index
    • Install
    • Log
    • Media
    • Newsletter
    • Oauth
    • Page
    • PageCache
    • Paygate
    • Payment
    • Paypal
    • PaypalUk
    • Persistent
    • Poll
    • ProductAlert
    • Rating
    • Reports
    • Review
    • Rss
    • Rule
    • Sales
    • SalesRule
    • Sedfriend
    • Sendfriend
    • Shipping
    • Sitemap
    • Tag
    • Tax
    • Usa
    • Weee
    • Widget
    • Wishlist
    • XmlConnect
  • None
  • Phoenix
    • Moneybookers
  • PHP
  • Zend
    • Date
    • Mime
    • XmlRpc

Classes

  • Mage_ImportExport_Adminhtml_ExportController
  • Mage_ImportExport_Adminhtml_ImportController
  • Mage_ImportExport_Block_Adminhtml_Export_Edit
  • Mage_ImportExport_Block_Adminhtml_Export_Edit_Form
  • Mage_ImportExport_Block_Adminhtml_Export_Filter
  • Mage_ImportExport_Block_Adminhtml_Import_Edit
  • Mage_ImportExport_Block_Adminhtml_Import_Edit_Form
  • Mage_ImportExport_Block_Adminhtml_Import_Frame_Result
  • Mage_ImportExport_Helper_Data
  • Mage_ImportExport_Model_Abstract
  • Mage_ImportExport_Model_Config
  • Mage_ImportExport_Model_Export
  • Mage_ImportExport_Model_Export_Adapter_Abstract
  • Mage_ImportExport_Model_Export_Adapter_Csv
  • Mage_ImportExport_Model_Export_Entity_Abstract
  • Mage_ImportExport_Model_Export_Entity_Customer
  • Mage_ImportExport_Model_Export_Entity_Product
  • Mage_ImportExport_Model_Export_Entity_Product_Type_Abstract
  • Mage_ImportExport_Model_Export_Entity_Product_Type_Configurable
  • Mage_ImportExport_Model_Export_Entity_Product_Type_Grouped
  • Mage_ImportExport_Model_Export_Entity_Product_Type_Simple
  • Mage_ImportExport_Model_Import
  • Mage_ImportExport_Model_Import_Adapter
  • Mage_ImportExport_Model_Import_Adapter_Abstract
  • Mage_ImportExport_Model_Import_Adapter_Csv
  • Mage_ImportExport_Model_Import_Entity_Abstract
  • Mage_ImportExport_Model_Import_Entity_Customer
  • Mage_ImportExport_Model_Import_Entity_Customer_Address
  • Mage_ImportExport_Model_Import_Entity_Product
  • Mage_ImportExport_Model_Import_Entity_Product_Type_Abstract
  • Mage_ImportExport_Model_Import_Entity_Product_Type_Configurable
  • Mage_ImportExport_Model_Import_Entity_Product_Type_Grouped
  • Mage_ImportExport_Model_Import_Entity_Product_Type_Simple
  • Mage_ImportExport_Model_Import_Proxy_Product
  • Mage_ImportExport_Model_Import_Proxy_Product_Resource
  • Mage_ImportExport_Model_Import_Uploader
  • Mage_ImportExport_Model_Mysql4_Import_Data
  • Mage_ImportExport_Model_Mysql4_Setup
  • Mage_ImportExport_Model_Resource_Import_Data
  • Mage_ImportExport_Model_Resource_Setup
  • Mage_ImportExport_Model_Source_Export_Entity
  • Mage_ImportExport_Model_Source_Export_Format
  • Mage_ImportExport_Model_Source_Import_Behavior
  • Mage_ImportExport_Model_Source_Import_Entity
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: /**
   3:  * Magento
   4:  *
   5:  * NOTICE OF LICENSE
   6:  *
   7:  * This source file is subject to the Open Software License (OSL 3.0)
   8:  * that is bundled with this package in the file LICENSE.txt.
   9:  * It is also available through the world-wide-web at this URL:
  10:  * http://opensource.org/licenses/osl-3.0.php
  11:  * If you did not receive a copy of the license and are unable to
  12:  * obtain it through the world-wide-web, please send an email
  13:  * to license@magentocommerce.com so we can send you a copy immediately.
  14:  *
  15:  * DISCLAIMER
  16:  *
  17:  * Do not edit or add to this file if you wish to upgrade Magento to newer
  18:  * versions in the future. If you wish to customize Magento for your
  19:  * needs please refer to http://www.magentocommerce.com for more information.
  20:  *
  21:  * @category    Mage
  22:  * @package     Mage_ImportExport
  23:  * @copyright   Copyright (c) 2012 Magento Inc. (http://www.magentocommerce.com)
  24:  * @license     http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
  25:  */
  26: 
  27: /**
  28:  * Import entity product model
  29:  *
  30:  * @category    Mage
  31:  * @package     Mage_ImportExport
  32:  * @author      Magento Core Team <core@magentocommerce.com>
  33:  */
  34: class Mage_ImportExport_Model_Import_Entity_Product extends Mage_ImportExport_Model_Import_Entity_Abstract
  35: {
  36:     const CONFIG_KEY_PRODUCT_TYPES = 'global/importexport/import_product_types';
  37: 
  38:     /**
  39:      * Size of bunch - part of products to save in one step.
  40:      */
  41:     const BUNCH_SIZE = 20;
  42: 
  43:     /**
  44:      * Value that means all entities (e.g. websites, groups etc.)
  45:      */
  46:     const VALUE_ALL = 'all';
  47: 
  48:     /**
  49:      * Data row scopes.
  50:      */
  51:     const SCOPE_DEFAULT = 1;
  52:     const SCOPE_WEBSITE = 2;
  53:     const SCOPE_STORE   = 0;
  54:     const SCOPE_NULL    = -1;
  55: 
  56:     /**
  57:      * Permanent column names.
  58:      *
  59:      * Names that begins with underscore is not an attribute. This name convention is for
  60:      * to avoid interference with same attribute name.
  61:      */
  62:     const COL_STORE    = '_store';
  63:     const COL_ATTR_SET = '_attribute_set';
  64:     const COL_TYPE     = '_type';
  65:     const COL_CATEGORY = '_category';
  66:     const COL_ROOT_CATEGORY = '_root_category';
  67:     const COL_SKU      = 'sku';
  68: 
  69:     /**
  70:      * Error codes.
  71:      */
  72:     const ERROR_INVALID_SCOPE                = 'invalidScope';
  73:     const ERROR_INVALID_WEBSITE              = 'invalidWebsite';
  74:     const ERROR_INVALID_STORE                = 'invalidStore';
  75:     const ERROR_INVALID_ATTR_SET             = 'invalidAttrSet';
  76:     const ERROR_INVALID_TYPE                 = 'invalidType';
  77:     const ERROR_INVALID_CATEGORY             = 'invalidCategory';
  78:     const ERROR_VALUE_IS_REQUIRED            = 'isRequired';
  79:     const ERROR_TYPE_CHANGED                 = 'typeChanged';
  80:     const ERROR_SKU_IS_EMPTY                 = 'skuEmpty';
  81:     const ERROR_NO_DEFAULT_ROW               = 'noDefaultRow';
  82:     const ERROR_CHANGE_TYPE                  = 'changeProductType';
  83:     const ERROR_DUPLICATE_SCOPE              = 'duplicateScope';
  84:     const ERROR_DUPLICATE_SKU                = 'duplicateSKU';
  85:     const ERROR_CHANGE_ATTR_SET              = 'changeAttrSet';
  86:     const ERROR_TYPE_UNSUPPORTED             = 'productTypeUnsupported';
  87:     const ERROR_ROW_IS_ORPHAN                = 'rowIsOrphan';
  88:     const ERROR_INVALID_TIER_PRICE_QTY       = 'invalidTierPriceOrQty';
  89:     const ERROR_INVALID_TIER_PRICE_SITE      = 'tierPriceWebsiteInvalid';
  90:     const ERROR_INVALID_TIER_PRICE_GROUP     = 'tierPriceGroupInvalid';
  91:     const ERROR_TIER_DATA_INCOMPLETE         = 'tierPriceDataIsIncomplete';
  92:     const ERROR_INVALID_GROUP_PRICE_SITE     = 'groupPriceWebsiteInvalid';
  93:     const ERROR_INVALID_GROUP_PRICE_GROUP    = 'groupPriceGroupInvalid';
  94:     const ERROR_GROUP_PRICE_DATA_INCOMPLETE  = 'groupPriceDataIsIncomplete';
  95:     const ERROR_SKU_NOT_FOUND_FOR_DELETE     = 'skuNotFoundToDelete';
  96:     const ERROR_SUPER_PRODUCTS_SKU_NOT_FOUND = 'superProductsSkuNotFound';
  97: 
  98:     /**
  99:      * Pairs of attribute set ID-to-name.
 100:      *
 101:      * @var array
 102:      */
 103:     protected $_attrSetIdToName = array();
 104: 
 105:     /**
 106:      * Pairs of attribute set name-to-ID.
 107:      *
 108:      * @var array
 109:      */
 110:     protected $_attrSetNameToId = array();
 111: 
 112:     /**
 113:      * Categories text-path to ID hash.
 114:      *
 115:      * @var array
 116:      */
 117:     protected $_categories = array();
 118: 
 119:     /**
 120:      * Categories text-path to ID hash with roots checking.
 121:      *
 122:      * @var array
 123:      */
 124:     protected $_categoriesWithRoots = array();
 125: 
 126:     /**
 127:      * Customer groups ID-to-name.
 128:      *
 129:      * @var array
 130:      */
 131:     protected $_customerGroups = array();
 132: 
 133:     /**
 134:      * Attributes with index (not label) value.
 135:      *
 136:      * @var array
 137:      */
 138:     protected $_indexValueAttributes = array(
 139:         'status',
 140:         'tax_class_id',
 141:         'visibility',
 142:         'enable_googlecheckout',
 143:         'gift_message_available',
 144:         'custom_design'
 145:     );
 146: 
 147:     /**
 148:      * Links attribute name-to-link type ID.
 149:      *
 150:      * @var array
 151:      */
 152:     protected $_linkNameToId = array(
 153:         '_links_related_'   => Mage_Catalog_Model_Product_Link::LINK_TYPE_RELATED,
 154:         '_links_crosssell_' => Mage_Catalog_Model_Product_Link::LINK_TYPE_CROSSSELL,
 155:         '_links_upsell_'    => Mage_Catalog_Model_Product_Link::LINK_TYPE_UPSELL
 156:     );
 157: 
 158:     /**
 159:      * Validation failure message template definitions
 160:      *
 161:      * @var array
 162:      */
 163:     protected $_messageTemplates = array(
 164:         self::ERROR_INVALID_SCOPE                => 'Invalid value in Scope column',
 165:         self::ERROR_INVALID_WEBSITE              => 'Invalid value in Website column (website does not exists?)',
 166:         self::ERROR_INVALID_STORE                => 'Invalid value in Store column (store does not exists?)',
 167:         self::ERROR_INVALID_ATTR_SET             => 'Invalid value for Attribute Set column (set does not exists?)',
 168:         self::ERROR_INVALID_TYPE                 => 'Product Type is invalid or not supported',
 169:         self::ERROR_INVALID_CATEGORY             => 'Category does not exists',
 170:         self::ERROR_VALUE_IS_REQUIRED            => "Required attribute '%s' has an empty value",
 171:         self::ERROR_TYPE_CHANGED                 => 'Trying to change type of existing products',
 172:         self::ERROR_SKU_IS_EMPTY                 => 'SKU is empty',
 173:         self::ERROR_NO_DEFAULT_ROW               => 'Default values row does not exists',
 174:         self::ERROR_CHANGE_TYPE                  => 'Product type change is not allowed',
 175:         self::ERROR_DUPLICATE_SCOPE              => 'Duplicate scope',
 176:         self::ERROR_DUPLICATE_SKU                => 'Duplicate SKU',
 177:         self::ERROR_CHANGE_ATTR_SET              => 'Product attribute set change is not allowed',
 178:         self::ERROR_TYPE_UNSUPPORTED             => 'Product type is not supported',
 179:         self::ERROR_ROW_IS_ORPHAN                => 'Orphan rows that will be skipped due default row errors',
 180:         self::ERROR_INVALID_TIER_PRICE_QTY       => 'Tier Price data price or quantity value is invalid',
 181:         self::ERROR_INVALID_TIER_PRICE_SITE      => 'Tier Price data website is invalid',
 182:         self::ERROR_INVALID_TIER_PRICE_GROUP     => 'Tier Price customer group ID is invalid',
 183:         self::ERROR_TIER_DATA_INCOMPLETE         => 'Tier Price data is incomplete',
 184:         self::ERROR_SKU_NOT_FOUND_FOR_DELETE     => 'Product with specified SKU not found',
 185:         self::ERROR_SUPER_PRODUCTS_SKU_NOT_FOUND => 'Product with specified super products SKU not found'
 186:     );
 187: 
 188:     /**
 189:      * Dry-runned products information from import file.
 190:      *
 191:      * [SKU] => array(
 192:      *     'type_id'        => (string) product type
 193:      *     'attr_set_id'    => (int) product attribute set ID
 194:      *     'entity_id'      => (int) product ID (value for new products will be set after entity save)
 195:      *     'attr_set_code'  => (string) attribute set code
 196:      * )
 197:      *
 198:      * @var array
 199:      */
 200:     protected $_newSku = array();
 201: 
 202:     /**
 203:      * Existing products SKU-related information in form of array:
 204:      *
 205:      * [SKU] => array(
 206:      *     'type_id'        => (string) product type
 207:      *     'attr_set_id'    => (int) product attribute set ID
 208:      *     'entity_id'      => (int) product ID
 209:      *     'supported_type' => (boolean) is product type supported by current version of import module
 210:      * )
 211:      *
 212:      * @var array
 213:      */
 214:     protected $_oldSku = array();
 215: 
 216:     /**
 217:      * Column names that holds values with particular meaning.
 218:      *
 219:      * @var array
 220:      */
 221:     protected $_particularAttributes = array(
 222:         '_store', '_attribute_set', '_type', self::COL_CATEGORY, self::COL_ROOT_CATEGORY, '_product_websites',
 223:         '_tier_price_website', '_tier_price_customer_group', '_tier_price_qty', '_tier_price_price',
 224:         '_links_related_sku', '_group_price_website', '_group_price_customer_group', '_group_price_price',
 225:         '_links_related_position', '_links_crosssell_sku', '_links_crosssell_position', '_links_upsell_sku',
 226:         '_links_upsell_position', '_custom_option_store', '_custom_option_type', '_custom_option_title',
 227:         '_custom_option_is_required', '_custom_option_price', '_custom_option_sku', '_custom_option_max_characters',
 228:         '_custom_option_sort_order', '_custom_option_file_extension', '_custom_option_image_size_x',
 229:         '_custom_option_image_size_y', '_custom_option_row_title', '_custom_option_row_price',
 230:         '_custom_option_row_sku', '_custom_option_row_sort', '_media_attribute_id', '_media_image', '_media_lable',
 231:         '_media_position', '_media_is_disabled'
 232:     );
 233: 
 234:     /**
 235:      * Column names that holds images files names
 236:      *
 237:      * @var array
 238:      */
 239:     protected $_imagesArrayKeys = array(
 240:         '_media_image', 'image', 'small_image', 'thumbnail'
 241:     );
 242: 
 243:     /**
 244:      * Permanent entity columns.
 245:      *
 246:      * @var array
 247:      */
 248:     protected $_permanentAttributes = array(self::COL_SKU);
 249: 
 250:     /**
 251:      * Array of supported product types as keys with appropriate model object as value.
 252:      *
 253:      * @var array
 254:      */
 255:     protected $_productTypeModels = array();
 256: 
 257:     /**
 258:      * All stores code-ID pairs.
 259:      *
 260:      * @var array
 261:      */
 262:     protected $_storeCodeToId = array();
 263: 
 264:     /**
 265:      * Store ID to its website stores IDs.
 266:      *
 267:      * @var array
 268:      */
 269:     protected $_storeIdToWebsiteStoreIds = array();
 270: 
 271:     /**
 272:      * Website code-to-ID
 273:      *
 274:      * @var array
 275:      */
 276:     protected $_websiteCodeToId = array();
 277: 
 278:     /**
 279:      * Website code to store code-to-ID pairs which it consists.
 280:      *
 281:      * @var array
 282:      */
 283:     protected $_websiteCodeToStoreIds = array();
 284: 
 285:     /**
 286:      * Media files uploader
 287:      *
 288:      * @var Mage_ImportExport_Model_Import_Uploader
 289:      */
 290:     protected $_fileUploader;
 291: 
 292:     /**
 293:      * Constructor.
 294:      *
 295:      */
 296:     public function __construct()
 297:     {
 298:         parent::__construct();
 299: 
 300:         $this->_initWebsites()
 301:             ->_initStores()
 302:             ->_initAttributeSets()
 303:             ->_initTypeModels()
 304:             ->_initCategories()
 305:             ->_initSkus()
 306:             ->_initCustomerGroups();
 307:     }
 308: 
 309:     /**
 310:      * Delete products.
 311:      *
 312:      * @return Mage_ImportExport_Model_Import_Entity_Product
 313:      */
 314:     protected function _deleteProducts()
 315:     {
 316:         $productEntityTable = Mage::getModel('importexport/import_proxy_product_resource')->getEntityTable();
 317: 
 318:         while ($bunch = $this->_dataSourceModel->getNextBunch()) {
 319:             $idToDelete = array();
 320: 
 321:             foreach ($bunch as $rowNum => $rowData) {
 322:                 if ($this->validateRow($rowData, $rowNum) && self::SCOPE_DEFAULT == $this->getRowScope($rowData)) {
 323:                     $idToDelete[] = $this->_oldSku[$rowData[self::COL_SKU]]['entity_id'];
 324:                 }
 325:             }
 326:             if ($idToDelete) {
 327:                 $this->_connection->query(
 328:                     $this->_connection->quoteInto(
 329:                         "DELETE FROM `{$productEntityTable}` WHERE `entity_id` IN (?)", $idToDelete
 330:                     )
 331:                 );
 332:             }
 333:         }
 334:         return $this;
 335:     }
 336: 
 337:     /**
 338:      * Create Product entity from raw data.
 339:      *
 340:      * @throws Exception
 341:      * @return bool Result of operation.
 342:      */
 343:     protected function _importData()
 344:     {
 345:         if (Mage_ImportExport_Model_Import::BEHAVIOR_DELETE == $this->getBehavior()) {
 346:             $this->_deleteProducts();
 347:         } else {
 348:             $this->_saveProducts();
 349:             $this->_saveStockItem();
 350:             $this->_saveLinks();
 351:             $this->_saveCustomOptions();
 352:             foreach ($this->_productTypeModels as $productType => $productTypeModel) {
 353:                 $productTypeModel->saveData();
 354:             }
 355:         }
 356:         Mage::dispatchEvent('catalog_product_import_finish_before', array('adapter'=>$this));
 357:         return true;
 358:     }
 359: 
 360:     /**
 361:      * Initialize attribute sets code-to-id pairs.
 362:      *
 363:      * @return Mage_ImportExport_Model_Import_Entity_Product
 364:      */
 365:     protected function _initAttributeSets()
 366:     {
 367:         foreach (Mage::getResourceModel('eav/entity_attribute_set_collection')
 368:                 ->setEntityTypeFilter($this->_entityTypeId) as $attributeSet) {
 369:             $this->_attrSetNameToId[$attributeSet->getAttributeSetName()] = $attributeSet->getId();
 370:             $this->_attrSetIdToName[$attributeSet->getId()] = $attributeSet->getAttributeSetName();
 371:         }
 372:         return $this;
 373:     }
 374: 
 375:     /**
 376:      * Initialize categories text-path to ID hash.
 377:      *
 378:      * @return Mage_ImportExport_Model_Import_Entity_Product
 379:      */
 380:     protected function _initCategories()
 381:     {
 382:         $collection = Mage::getResourceModel('catalog/category_collection')->addNameToResult();
 383:         /* @var $collection Mage_Catalog_Model_Resource_Eav_Mysql4_Category_Collection */
 384:         foreach ($collection as $category) {
 385:             $structure = explode('/', $category->getPath());
 386:             $pathSize  = count($structure);
 387:             if ($pathSize > 1) {
 388:                 $path = array();
 389:                 for ($i = 1; $i < $pathSize; $i++) {
 390:                     $path[] = $collection->getItemById($structure[$i])->getName();
 391:                 }
 392:                 $rootCategoryName = array_shift($path);
 393:                 if (!isset($this->_categoriesWithRoots[$rootCategoryName])) {
 394:                     $this->_categoriesWithRoots[$rootCategoryName] = array();
 395:                 }
 396:                 $index = implode('/', $path);
 397:                 $this->_categoriesWithRoots[$rootCategoryName][$index] = $category->getId();
 398:                 if ($pathSize > 2) {
 399:                     $this->_categories[$index] = $category->getId();
 400:                 }
 401:             }
 402:         }
 403:         return $this;
 404:     }
 405: 
 406:     /**
 407:      * Initialize customer groups.
 408:      *
 409:      * @return Mage_ImportExport_Model_Import_Entity_Product
 410:      */
 411:     protected function _initCustomerGroups()
 412:     {
 413:         foreach (Mage::getResourceModel('customer/group_collection') as $customerGroup) {
 414:             $this->_customerGroups[$customerGroup->getId()] = true;
 415:         }
 416:         return $this;
 417:     }
 418: 
 419:     /**
 420:      * Initialize existent product SKUs.
 421:      *
 422:      * @return Mage_ImportExport_Model_Import_Entity_Product
 423:      */
 424:     protected function _initSkus()
 425:     {
 426:         $columns = array('entity_id', 'type_id', 'attribute_set_id', 'sku');
 427:         foreach (Mage::getModel('catalog/product')->getProductEntitiesInfo($columns) as $info) {
 428:             $typeId = $info['type_id'];
 429:             $sku = $info['sku'];
 430:             $this->_oldSku[$sku] = array(
 431:                 'type_id'        => $typeId,
 432:                 'attr_set_id'    => $info['attribute_set_id'],
 433:                 'entity_id'      => $info['entity_id'],
 434:                 'supported_type' => isset($this->_productTypeModels[$typeId])
 435:             );
 436:         }
 437:         return $this;
 438:     }
 439: 
 440:     /**
 441:      * Initialize stores hash.
 442:      *
 443:      * @return Mage_ImportExport_Model_Import_Entity_Product
 444:      */
 445:     protected function _initStores()
 446:     {
 447:         foreach (Mage::app()->getStores() as $store) {
 448:             $this->_storeCodeToId[$store->getCode()] = $store->getId();
 449:             $this->_storeIdToWebsiteStoreIds[$store->getId()] = $store->getWebsite()->getStoreIds();
 450:         }
 451:         return $this;
 452:     }
 453: 
 454:     /**
 455:      * Initialize product type models.
 456:      *
 457:      * @throws Exception
 458:      * @return Mage_ImportExport_Model_Import_Entity_Product
 459:      */
 460:     protected function _initTypeModels()
 461:     {
 462:         $config = Mage::getConfig()->getNode(self::CONFIG_KEY_PRODUCT_TYPES)->asCanonicalArray();
 463:         foreach ($config as $type => $typeModel) {
 464:             if (!($model = Mage::getModel($typeModel, array($this, $type)))) {
 465:                 Mage::throwException("Entity type model '{$typeModel}' is not found");
 466:             }
 467:             if (! $model instanceof Mage_ImportExport_Model_Import_Entity_Product_Type_Abstract) {
 468:                 Mage::throwException(
 469:                     Mage::helper('importexport')->__('Entity type model must be an instance of Mage_ImportExport_Model_Import_Entity_Product_Type_Abstract')
 470:                 );
 471:             }
 472:             if ($model->isSuitable()) {
 473:                 $this->_productTypeModels[$type] = $model;
 474:             }
 475:             $this->_particularAttributes = array_merge(
 476:                 $this->_particularAttributes,
 477:                 $model->getParticularAttributes()
 478:             );
 479:         }
 480:         // remove doubles
 481:         $this->_particularAttributes = array_unique($this->_particularAttributes);
 482: 
 483:         return $this;
 484:     }
 485: 
 486:     /**
 487:      * Initialize website values.
 488:      *
 489:      * @return Mage_ImportExport_Model_Import_Entity_Product
 490:      */
 491:     protected function _initWebsites()
 492:     {
 493:         /** @var $website Mage_Core_Model_Website */
 494:         foreach (Mage::app()->getWebsites() as $website) {
 495:             $this->_websiteCodeToId[$website->getCode()] = $website->getId();
 496:             $this->_websiteCodeToStoreIds[$website->getCode()] = array_flip($website->getStoreCodes());
 497:         }
 498:         return $this;
 499:     }
 500: 
 501:     /**
 502:      * Check product category validity.
 503:      *
 504:      * @param array $rowData
 505:      * @param int $rowNum
 506:      * @return bool
 507:      */
 508:     protected function _isProductCategoryValid(array $rowData, $rowNum)
 509:     {
 510:         $emptyCategory = empty($rowData[self::COL_CATEGORY]);
 511:         $emptyRootCategory = empty($rowData[self::COL_ROOT_CATEGORY]);
 512:         $hasCategory = $emptyCategory ? false : isset($this->_categories[$rowData[self::COL_CATEGORY]]);
 513:         $category = $emptyRootCategory ? null : $this->_categoriesWithRoots[$rowData[self::COL_ROOT_CATEGORY]];
 514:         if (!$emptyCategory && !$hasCategory
 515:             || !$emptyRootCategory && !isset($category)
 516:             || !$emptyRootCategory && !$emptyCategory && !isset($category[$rowData[self::COL_CATEGORY]])
 517:         ) {
 518:             $this->addRowError(self::ERROR_INVALID_CATEGORY, $rowNum);
 519:             return false;
 520:         }
 521:         return true;
 522:     }
 523: 
 524:     /**
 525:      * Check product website belonging.
 526:      *
 527:      * @param array $rowData
 528:      * @param int $rowNum
 529:      * @return bool
 530:      */
 531:     protected function _isProductWebsiteValid(array $rowData, $rowNum)
 532:     {
 533:         if (!empty($rowData['_product_websites']) && !isset($this->_websiteCodeToId[$rowData['_product_websites']])) {
 534:             $this->addRowError(self::ERROR_INVALID_WEBSITE, $rowNum);
 535:             return false;
 536:         }
 537:         return true;
 538:     }
 539: 
 540:     /**
 541:      * Set valid attribute set and product type to rows with all scopes
 542:      * to ensure that existing products doesn't changed.
 543:      *
 544:      * @param array $rowData
 545:      * @return array
 546:      */
 547:     protected function _prepareRowForDb(array $rowData)
 548:     {
 549:         $rowData = parent::_prepareRowForDb($rowData);
 550: 
 551:         static $lastSku  = null;
 552: 
 553:         if (Mage_ImportExport_Model_Import::BEHAVIOR_DELETE == $this->getBehavior()) {
 554:             return $rowData;
 555:         }
 556:         if (self::SCOPE_DEFAULT == $this->getRowScope($rowData)) {
 557:             $lastSku = $rowData[self::COL_SKU];
 558:         }
 559:         if (isset($this->_oldSku[$lastSku])) {
 560:             $rowData[self::COL_ATTR_SET] = $this->_newSku[$lastSku]['attr_set_code'];
 561:             $rowData[self::COL_TYPE]     = $this->_newSku[$lastSku]['type_id'];
 562:         }
 563: 
 564:         return $rowData;
 565:     }
 566: 
 567:     /**
 568:      * Check tier price data validity.
 569:      *
 570:      * @param array $rowData
 571:      * @param int $rowNum
 572:      * @return bool
 573:      */
 574:     protected function _isTierPriceValid(array $rowData, $rowNum)
 575:     {
 576:         if ((isset($rowData['_tier_price_website']) && strlen($rowData['_tier_price_website']))
 577:                 || (isset($rowData['_tier_price_customer_group']) && strlen($rowData['_tier_price_customer_group']))
 578:                 || (isset($rowData['_tier_price_qty']) && strlen($rowData['_tier_price_qty']))
 579:                 || (isset($rowData['_tier_price_price']) && strlen($rowData['_tier_price_price']))
 580:         ) {
 581:             if (!isset($rowData['_tier_price_website']) || !isset($rowData['_tier_price_customer_group'])
 582:                     || !isset($rowData['_tier_price_qty']) || !isset($rowData['_tier_price_price'])
 583:                     || !strlen($rowData['_tier_price_website']) || !strlen($rowData['_tier_price_customer_group'])
 584:                     || !strlen($rowData['_tier_price_qty']) || !strlen($rowData['_tier_price_price'])
 585:             ) {
 586:                 $this->addRowError(self::ERROR_TIER_DATA_INCOMPLETE, $rowNum);
 587:                 return false;
 588:             } elseif ($rowData['_tier_price_website'] != self::VALUE_ALL
 589:                     && !isset($this->_websiteCodeToId[$rowData['_tier_price_website']])) {
 590:                 $this->addRowError(self::ERROR_INVALID_TIER_PRICE_SITE, $rowNum);
 591:                 return false;
 592:             } elseif ($rowData['_tier_price_customer_group'] != self::VALUE_ALL
 593:                     && !isset($this->_customerGroups[$rowData['_tier_price_customer_group']])) {
 594:                 $this->addRowError(self::ERROR_INVALID_TIER_PRICE_GROUP, $rowNum);
 595:                 return false;
 596:             } elseif ($rowData['_tier_price_qty'] <= 0 || $rowData['_tier_price_price'] <= 0) {
 597:                 $this->addRowError(self::ERROR_INVALID_TIER_PRICE_QTY, $rowNum);
 598:                 return false;
 599:             }
 600:         }
 601:         return true;
 602:     }
 603: 
 604:     /**
 605:      * Check group price data validity.
 606:      *
 607:      * @param array $rowData
 608:      * @param int $rowNum
 609:      * @return bool
 610:      */
 611:     protected function _isGroupPriceValid(array $rowData, $rowNum)
 612:     {
 613:         if ((isset($rowData['_group_price_website']) && strlen($rowData['_group_price_website']))
 614:             || (isset($rowData['_group_price_customer_group']) && strlen($rowData['_group_price_customer_group']))
 615:             || (isset($rowData['_group_price_price']) && strlen($rowData['_group_price_price']))
 616:         ) {
 617:             if (!isset($rowData['_group_price_website']) || !isset($rowData['_group_price_customer_group'])
 618:                 || !strlen($rowData['_group_price_website']) || !strlen($rowData['_group_price_customer_group'])
 619:                 || !strlen($rowData['_group_price_price'])
 620:             ) {
 621:                 $this->addRowError(self::ERROR_GROUP_PRICE_DATA_INCOMPLETE, $rowNum);
 622:                 return false;
 623:             } elseif ($rowData['_group_price_website'] != self::VALUE_ALL
 624:                 && !isset($this->_websiteCodeToId[$rowData['_group_price_website']])
 625:             ) {
 626:                 $this->addRowError(self::ERROR_INVALID_GROUP_PRICE_SITE, $rowNum);
 627:                 return false;
 628:             } elseif ($rowData['_group_price_customer_group'] != self::VALUE_ALL
 629:                 && !isset($this->_customerGroups[$rowData['_group_price_customer_group']])
 630:             ) {
 631:                 $this->addRowError(self::ERROR_INVALID_GROUP_PRICE_GROUP, $rowNum);
 632:                 return false;
 633:             }
 634:         }
 635:         return true;
 636:     }
 637: 
 638:     /**
 639:      * Check super products SKU
 640:      *
 641:      * @param array $rowData
 642:      * @param int $rowNum
 643:      * @return bool
 644:      */
 645:     protected function _isSuperProductsSkuValid($rowData, $rowNum)
 646:     {
 647:         if (!empty($rowData['_super_products_sku'])
 648:             && (!isset($this->_oldSku[$rowData['_super_products_sku']])
 649:                 && !isset($this->_newSku[$rowData['_super_products_sku']])
 650:             )
 651:         ) {
 652:             $this->addRowError(self::ERROR_SUPER_PRODUCTS_SKU_NOT_FOUND, $rowNum);
 653:             return false;
 654:         }
 655:         return true;
 656:     }
 657: 
 658:     /**
 659:      * Custom options save.
 660:      *
 661:      * @return Mage_ImportExport_Model_Import_Entity_Product
 662:      */
 663:     protected function _saveCustomOptions()
 664:     {
 665:         /** @var $coreResource Mage_Core_Model_Resource */
 666:         $coreResource   = Mage::getSingleton('core/resource');
 667:         $productTable   = $coreResource->getTableName('catalog/product');
 668:         $optionTable    = $coreResource->getTableName('catalog/product_option');
 669:         $priceTable     = $coreResource->getTableName('catalog/product_option_price');
 670:         $titleTable     = $coreResource->getTableName('catalog/product_option_title');
 671:         $typePriceTable = $coreResource->getTableName('catalog/product_option_type_price');
 672:         $typeTitleTable = $coreResource->getTableName('catalog/product_option_type_title');
 673:         $typeValueTable = $coreResource->getTableName('catalog/product_option_type_value');
 674:         $nextOptionId   = Mage::getResourceHelper('importexport')->getNextAutoincrement($optionTable);
 675:         $nextValueId    = Mage::getResourceHelper('importexport')->getNextAutoincrement($typeValueTable);
 676:         $priceIsGlobal  = Mage::helper('catalog')->isPriceGlobal();
 677:         $type           = null;
 678:         $typeSpecific   = array(
 679:             'date'      => array('price', 'sku'),
 680:             'date_time' => array('price', 'sku'),
 681:             'time'      => array('price', 'sku'),
 682:             'field'     => array('price', 'sku', 'max_characters'),
 683:             'area'      => array('price', 'sku', 'max_characters'),
 684:             //'file'      => array('price', 'sku', 'file_extension', 'image_size_x', 'image_size_y'),
 685:             'drop_down' => true,
 686:             'radio'     => true,
 687:             'checkbox'  => true,
 688:             'multiple'  => true
 689:         );
 690: 
 691:         while ($bunch = $this->_dataSourceModel->getNextBunch()) {
 692:             $customOptions = array(
 693:                 'product_id'    => array(),
 694:                 $optionTable    => array(),
 695:                 $priceTable     => array(),
 696:                 $titleTable     => array(),
 697:                 $typePriceTable => array(),
 698:                 $typeTitleTable => array(),
 699:                 $typeValueTable => array()
 700:             );
 701: 
 702:             foreach ($bunch as $rowNum => $rowData) {
 703:                 if (!$this->isRowAllowedToImport($rowData, $rowNum)) {
 704:                     continue;
 705:                 }
 706:                 if (self::SCOPE_DEFAULT == $this->getRowScope($rowData)) {
 707:                     $productId = $this->_newSku[$rowData[self::COL_SKU]]['entity_id'];
 708:                 } elseif (!isset($productId)) {
 709:                     continue;
 710:                 }
 711:                 if (!empty($rowData['_custom_option_store'])) {
 712:                     if (!isset($this->_storeCodeToId[$rowData['_custom_option_store']])) {
 713:                         continue;
 714:                     }
 715:                     $storeId = $this->_storeCodeToId[$rowData['_custom_option_store']];
 716:                 } else {
 717:                     $storeId = Mage_Catalog_Model_Abstract::DEFAULT_STORE_ID;
 718:                 }
 719:                 if (!empty($rowData['_custom_option_type'])) { // get CO type if its specified
 720:                     if (!isset($typeSpecific[$rowData['_custom_option_type']])) {
 721:                         $type = null;
 722:                         continue;
 723:                     }
 724:                     $type = $rowData['_custom_option_type'];
 725:                     $rowIsMain = true;
 726:                 } else {
 727:                     if (null === $type) {
 728:                         continue;
 729:                     }
 730:                     $rowIsMain = false;
 731:                 }
 732:                 if (!isset($customOptions['product_id'][$productId])) { // for update product entity table
 733:                     $customOptions['product_id'][$productId] = array(
 734:                         'entity_id'        => $productId,
 735:                         'has_options'      => 0,
 736:                         'required_options' => 0,
 737:                         'updated_at'       => now()
 738:                     );
 739:                 }
 740:                 if ($rowIsMain) {
 741:                     $solidParams = array(
 742:                         'option_id'      => $nextOptionId,
 743:                         'sku'            => '',
 744:                         'max_characters' => 0,
 745:                         'file_extension' => null,
 746:                         'image_size_x'   => 0,
 747:                         'image_size_y'   => 0,
 748:                         'product_id'     => $productId,
 749:                         'type'           => $type,
 750:                         'is_require'     => empty($rowData['_custom_option_is_required']) ? 0 : 1,
 751:                         'sort_order'     => empty($rowData['_custom_option_sort_order'])
 752:                                             ? 0 : abs($rowData['_custom_option_sort_order'])
 753:                     );
 754: 
 755:                     if (true !== $typeSpecific[$type]) { // simple option may have optional params
 756:                         $priceTableRow = array(
 757:                             'option_id'  => $nextOptionId,
 758:                             'store_id'   => Mage_Catalog_Model_Abstract::DEFAULT_STORE_ID,
 759:                             'price'      => 0,
 760:                             'price_type' => 'fixed'
 761:                         );
 762: 
 763:                         foreach ($typeSpecific[$type] as $paramSuffix) {
 764:                             if (isset($rowData['_custom_option_' . $paramSuffix])) {
 765:                                 $data = $rowData['_custom_option_' . $paramSuffix];
 766: 
 767:                                 if (array_key_exists($paramSuffix, $solidParams)) {
 768:                                     $solidParams[$paramSuffix] = $data;
 769:                                 } elseif ('price' == $paramSuffix) {
 770:                                     if ('%' == substr($data, -1)) {
 771:                                         $priceTableRow['price_type'] = 'percent';
 772:                                     }
 773:                                     $priceTableRow['price'] = (float) rtrim($data, '%');
 774:                                 }
 775:                             }
 776:                         }
 777:                         $customOptions[$priceTable][] = $priceTableRow;
 778:                     }
 779:                     $customOptions[$optionTable][] = $solidParams;
 780:                     $customOptions['product_id'][$productId]['has_options'] = 1;
 781: 
 782:                     if (!empty($rowData['_custom_option_is_required'])) {
 783:                         $customOptions['product_id'][$productId]['required_options'] = 1;
 784:                     }
 785:                     $prevOptionId = $nextOptionId++; // increment option id, but preserve value for $typeValueTable
 786:                 }
 787:                 if ($typeSpecific[$type] === true && !empty($rowData['_custom_option_row_title'])
 788:                         && empty($rowData['_custom_option_store'])) {
 789:                     // complex CO option row
 790:                     $customOptions[$typeValueTable][$prevOptionId][] = array(
 791:                         'option_type_id' => $nextValueId,
 792:                         'sort_order'     => empty($rowData['_custom_option_row_sort'])
 793:                                             ? 0 : abs($rowData['_custom_option_row_sort']),
 794:                         'sku'            => !empty($rowData['_custom_option_row_sku'])
 795:                                             ? $rowData['_custom_option_row_sku'] : ''
 796:                     );
 797:                     if (!isset($customOptions[$typeTitleTable][$nextValueId][0])) { // ensure default title is set
 798:                         $customOptions[$typeTitleTable][$nextValueId][0] = $rowData['_custom_option_row_title'];
 799:                     }
 800:                     $customOptions[$typeTitleTable][$nextValueId][$storeId] = $rowData['_custom_option_row_title'];
 801: 
 802:                     if (!empty($rowData['_custom_option_row_price'])) {
 803:                         $typePriceRow = array(
 804:                             'price'      => (float) rtrim($rowData['_custom_option_row_price'], '%'),
 805:                             'price_type' => 'fixed'
 806:                         );
 807:                         if ('%' == substr($rowData['_custom_option_row_price'], -1)) {
 808:                             $typePriceRow['price_type'] = 'percent';
 809:                         }
 810:                         if ($priceIsGlobal) {
 811:                             $customOptions[$typePriceTable][$nextValueId][0] = $typePriceRow;
 812:                         } else {
 813:                             // ensure default price is set
 814:                             if (!isset($customOptions[$typePriceTable][$nextValueId][0])) {
 815:                                 $customOptions[$typePriceTable][$nextValueId][0] = $typePriceRow;
 816:                             }
 817:                             $customOptions[$typePriceTable][$nextValueId][$storeId] = $typePriceRow;
 818:                         }
 819:                     }
 820:                     $nextValueId++;
 821:                 }
 822:                 if (!empty($rowData['_custom_option_title'])) {
 823:                     if (!isset($customOptions[$titleTable][$prevOptionId][0])) { // ensure default title is set
 824:                         $customOptions[$titleTable][$prevOptionId][0] = $rowData['_custom_option_title'];
 825:                     }
 826:                     $customOptions[$titleTable][$prevOptionId][$storeId] = $rowData['_custom_option_title'];
 827:                 }
 828:             }
 829:             if ($this->getBehavior() != Mage_ImportExport_Model_Import::BEHAVIOR_APPEND) { // remove old data?
 830:                 $this->_connection->delete(
 831:                     $optionTable,
 832:                     $this->_connection->quoteInto('product_id IN (?)', array_keys($customOptions['product_id']))
 833:                 );
 834:             }
 835:             // if complex options does not contain values - ignore them
 836:             foreach ($customOptions[$optionTable] as $key => $optionData) {
 837:                 if ($typeSpecific[$optionData['type']] === true
 838:                         && !isset($customOptions[$typeValueTable][$optionData['option_id']])
 839:                 ) {
 840:                     unset($customOptions[$optionTable][$key], $customOptions[$titleTable][$optionData['option_id']]);
 841:                 }
 842:             }
 843: 
 844:             if ($customOptions[$optionTable]) {
 845:                 $this->_connection->insertMultiple($optionTable, $customOptions[$optionTable]);
 846:             } else {
 847:                 continue; // nothing to save
 848:             }
 849:             $titleRows = array();
 850: 
 851:             foreach ($customOptions[$titleTable] as $optionId => $storeInfo) {
 852:                 foreach ($storeInfo as $storeId => $title) {
 853:                     $titleRows[] = array('option_id' => $optionId, 'store_id' => $storeId, 'title' => $title);
 854:                 }
 855:             }
 856:             if ($titleRows) {
 857:                 $this->_connection->insertOnDuplicate($titleTable, $titleRows, array('title'));
 858:             }
 859:             if ($customOptions[$priceTable]) {
 860:                 $this->_connection->insertOnDuplicate(
 861:                     $priceTable,
 862:                     $customOptions[$priceTable],
 863:                     array('price', 'price_type')
 864:                 );
 865:             }
 866:             $typeValueRows = array();
 867: 
 868:             foreach ($customOptions[$typeValueTable] as $optionId => $optionInfo) {
 869:                 foreach ($optionInfo as $row) {
 870:                     $row['option_id'] = $optionId;
 871:                     $typeValueRows[]  = $row;
 872:                 }
 873:             }
 874:             if ($typeValueRows) {
 875:                 $this->_connection->insertMultiple($typeValueTable, $typeValueRows);
 876:             }
 877:             $optionTypePriceRows = array();
 878:             $optionTypeTitleRows = array();
 879: 
 880:             foreach ($customOptions[$typePriceTable] as $optionTypeId => $storesData) {
 881:                 foreach ($storesData as $storeId => $row) {
 882:                     $row['option_type_id'] = $optionTypeId;
 883:                     $row['store_id']       = $storeId;
 884:                     $optionTypePriceRows[] = $row;
 885:                 }
 886:             }
 887:             foreach ($customOptions[$typeTitleTable] as $optionTypeId => $storesData) {
 888:                 foreach ($storesData as $storeId => $title) {
 889:                     $optionTypeTitleRows[] = array(
 890:                         'option_type_id' => $optionTypeId,
 891:                         'store_id'       => $storeId,
 892:                         'title'          => $title
 893:                     );
 894:                 }
 895:             }
 896:             if ($optionTypePriceRows) {
 897:                 $this->_connection->insertOnDuplicate(
 898:                     $typePriceTable,
 899:                     $optionTypePriceRows,
 900:                     array('price', 'price_type')
 901:                 );
 902:             }
 903:             if ($optionTypeTitleRows) {
 904:                 $this->_connection->insertOnDuplicate($typeTitleTable, $optionTypeTitleRows, array('title'));
 905:             }
 906:             if ($customOptions['product_id']) { // update product entity table to show that product has options
 907:                 $this->_connection->insertOnDuplicate(
 908:                     $productTable,
 909:                     $customOptions['product_id'],
 910:                     array('has_options', 'required_options', 'updated_at')
 911:                 );
 912:             }
 913:         }
 914:         return $this;
 915:     }
 916: 
 917:     /**
 918:      * Gather and save information about product links.
 919:      * Must be called after ALL products saving done.
 920:      *
 921:      * @return Mage_ImportExport_Model_Import_Entity_Product
 922:      */
 923:     protected function _saveLinks()
 924:     {
 925:         $resource       = Mage::getResourceModel('catalog/product_link');
 926:         $mainTable      = $resource->getMainTable();
 927:         $positionAttrId = array();
 928:         $nextLinkId     = Mage::getResourceHelper('importexport')->getNextAutoincrement($mainTable);
 929:         $adapter = $this->_connection;
 930: 
 931:         // pre-load 'position' attributes ID for each link type once
 932:         foreach ($this->_linkNameToId as $linkName => $linkId) {
 933:             $select = $adapter->select()
 934:                 ->from(
 935:                     $resource->getTable('catalog/product_link_attribute'),
 936:                     array('id' => 'product_link_attribute_id')
 937:                 )
 938:                 ->where('link_type_id = :link_id AND product_link_attribute_code = :position');
 939:             $bind = array(
 940:                 ':link_id' => $linkId,
 941:                 ':position' => 'position'
 942:             );
 943:             $positionAttrId[$linkId] = $adapter->fetchOne($select, $bind);
 944:         }
 945:         while ($bunch = $this->_dataSourceModel->getNextBunch()) {
 946:             $productIds   = array();
 947:             $linkRows     = array();
 948:             $positionRows = array();
 949: 
 950:             foreach ($bunch as $rowNum => $rowData) {
 951:                 if (!$this->isRowAllowedToImport($rowData, $rowNum)) {
 952:                     continue;
 953:                 }
 954:                 if (self::SCOPE_DEFAULT == $this->getRowScope($rowData)) {
 955:                     $sku = $rowData[self::COL_SKU];
 956:                 }
 957:                 foreach ($this->_linkNameToId as $linkName => $linkId) {
 958:                     if (isset($rowData[$linkName . 'sku'])) {
 959:                         $productId    = $this->_newSku[$sku]['entity_id'];
 960:                         $productIds[] = $productId;
 961:                         $linkedSku    = $rowData[$linkName . 'sku'];
 962: 
 963:                         if ((isset($this->_newSku[$linkedSku]) || isset($this->_oldSku[$linkedSku]))
 964:                                 && $linkedSku != $sku) {
 965:                             if (isset($this->_newSku[$linkedSku])) {
 966:                                 $linkedId = $this->_newSku[$linkedSku]['entity_id'];
 967:                             } else {
 968:                                 $linkedId = $this->_oldSku[$linkedSku]['entity_id'];
 969:                             }
 970:                             $linkKey = "{$productId}-{$linkedId}-{$linkId}";
 971: 
 972:                             if (!isset($linkRows[$linkKey])) {
 973:                                 $linkRows[$linkKey] = array(
 974:                                     'link_id'           => $nextLinkId,
 975:                                     'product_id'        => $productId,
 976:                                     'linked_product_id' => $linkedId,
 977:                                     'link_type_id'      => $linkId
 978:                                 );
 979:                                 if (!empty($rowData[$linkName . 'position'])) {
 980:                                     $positionRows[] = array(
 981:                                         'link_id'                   => $nextLinkId,
 982:                                         'product_link_attribute_id' => $positionAttrId[$linkId],
 983:                                         'value'                     => $rowData[$linkName . 'position']
 984:                                     );
 985:                                 }
 986:                                 $nextLinkId++;
 987:                             }
 988:                         }
 989:                     }
 990:                 }
 991:             }
 992:             if (Mage_ImportExport_Model_Import::BEHAVIOR_APPEND != $this->getBehavior() && $productIds) {
 993:                 $adapter->delete(
 994:                     $mainTable,
 995:                     $adapter->quoteInto('product_id IN (?)', array_keys($productIds))
 996:                 );
 997:             }
 998:             if ($linkRows) {
 999:                 $adapter->insertOnDuplicate(
1000:                     $mainTable,
1001:                     $linkRows,
1002:                     array('link_id')
1003:                 );
1004:             }
1005:             if ($positionRows) { // process linked product positions
1006:                 $adapter->insertOnDuplicate(
1007:                     $resource->getAttributeTypeTable('int'),
1008:                     $positionRows,
1009:                     array('value')
1010:                 );
1011:             }
1012:         }
1013:         return $this;
1014:     }
1015: 
1016:     /**
1017:      * Save product attributes.
1018:      *
1019:      * @param array $attributesData
1020:      * @return Mage_ImportExport_Model_Import_Entity_Product
1021:      */
1022:     protected function _saveProductAttributes(array $attributesData)
1023:     {
1024:         foreach ($attributesData as $tableName => $skuData) {
1025:             $tableData = array();
1026: 
1027:             foreach ($skuData as $sku => $attributes) {
1028:                 $productId = $this->_newSku[$sku]['entity_id'];
1029: 
1030:                 foreach ($attributes as $attributeId => $storeValues) {
1031:                     foreach ($storeValues as $storeId => $storeValue) {
1032:                         $tableData[] = array(
1033:                             'entity_id'      => $productId,
1034:                             'entity_type_id' => $this->_entityTypeId,
1035:                             'attribute_id'   => $attributeId,
1036:                             'store_id'       => $storeId,
1037:                             'value'          => $storeValue
1038:                         );
1039:                     }
1040:                 }
1041:             }
1042:             $this->_connection->insertOnDuplicate($tableName, $tableData, array('value'));
1043:         }
1044:         return $this;
1045:     }
1046: 
1047:     /**
1048:      * Save product categories.
1049:      *
1050:      * @param array $categoriesData
1051:      * @return Mage_ImportExport_Model_Import_Entity_Product
1052:      */
1053:     protected function _saveProductCategories(array $categoriesData)
1054:     {
1055:         static $tableName = null;
1056: 
1057:         if (!$tableName) {
1058:             $tableName = Mage::getModel('importexport/import_proxy_product_resource')->getProductCategoryTable();
1059:         }
1060:         if ($categoriesData) {
1061:             $categoriesIn = array();
1062:             $delProductId = array();
1063: 
1064:             foreach ($categoriesData as $delSku => $categories) {
1065:                 $productId      = $this->_newSku[$delSku]['entity_id'];
1066:                 $delProductId[] = $productId;
1067: 
1068:                 foreach (array_keys($categories) as $categoryId) {
1069:                     $categoriesIn[] = array('product_id' => $productId, 'category_id' => $categoryId, 'position' => 1);
1070:                 }
1071:             }
1072:             if (Mage_ImportExport_Model_Import::BEHAVIOR_APPEND != $this->getBehavior()) {
1073:                 $this->_connection->delete(
1074:                     $tableName,
1075:                     $this->_connection->quoteInto('product_id IN (?)', $delProductId)
1076:                 );
1077:             }
1078:             if ($categoriesIn) {
1079:                 $this->_connection->insertOnDuplicate($tableName, $categoriesIn, array('position'));
1080:             }
1081:         }
1082:         return $this;
1083:     }
1084: 
1085:     /**
1086:      * Update and insert data in entity table.
1087:      *
1088:      * @param array $entityRowsIn Row for insert
1089:      * @param array $entityRowsUp Row for update
1090:      * @return Mage_ImportExport_Model_Import_Entity_Product
1091:      */
1092:     protected function _saveProductEntity(array $entityRowsIn, array $entityRowsUp)
1093:     {
1094:         static $entityTable = null;
1095: 
1096:         if (!$entityTable) {
1097:             $entityTable = Mage::getModel('importexport/import_proxy_product_resource')->getEntityTable();
1098:         }
1099:         if ($entityRowsUp) {
1100:             $this->_connection->insertOnDuplicate(
1101:                 $entityTable,
1102:                 $entityRowsUp,
1103:                 array('updated_at')
1104:             );
1105:         }
1106:         if ($entityRowsIn) {
1107:             $this->_connection->insertMultiple($entityTable, $entityRowsIn);
1108: 
1109:             $newProducts = $this->_connection->fetchPairs($this->_connection->select()
1110:                 ->from($entityTable, array('sku', 'entity_id'))
1111:                 ->where('sku IN (?)', array_keys($entityRowsIn))
1112:             );
1113:             foreach ($newProducts as $sku => $newId) { // fill up entity_id for new products
1114:                 $this->_newSku[$sku]['entity_id'] = $newId;
1115:             }
1116:         }
1117:         return $this;
1118:     }
1119: 
1120:     /**
1121:      * Gather and save information about product entities.
1122:      *
1123:      * @return Mage_ImportExport_Model_Import_Entity_Product
1124:      */
1125:     protected function _saveProducts()
1126:     {
1127:         /** @var $resource Mage_ImportExport_Model_Import_Proxy_Product_Resource */
1128:         $resource       = Mage::getModel('importexport/import_proxy_product_resource');
1129:         $priceIsGlobal  = Mage::helper('catalog')->isPriceGlobal();
1130:         $strftimeFormat = Varien_Date::convertZendToStrftime(Varien_Date::DATETIME_INTERNAL_FORMAT, true, true);
1131:         $productLimit   = null;
1132:         $productsQty    = null;
1133: 
1134:         while ($bunch = $this->_dataSourceModel->getNextBunch()) {
1135:             $entityRowsIn = array();
1136:             $entityRowsUp = array();
1137:             $attributes   = array();
1138:             $websites     = array();
1139:             $categories   = array();
1140:             $tierPrices   = array();
1141:             $groupPrices  = array();
1142:             $mediaGallery = array();
1143:             $uploadedGalleryFiles = array();
1144:             $previousType = null;
1145:             $previousAttributeSet = null;
1146: 
1147:             foreach ($bunch as $rowNum => $rowData) {
1148:                 if (!$this->validateRow($rowData, $rowNum)) {
1149:                     continue;
1150:                 }
1151:                 $rowScope = $this->getRowScope($rowData);
1152: 
1153:                 if (self::SCOPE_DEFAULT == $rowScope) {
1154:                     $rowSku = $rowData[self::COL_SKU];
1155: 
1156:                     // 1. Entity phase
1157:                     if (isset($this->_oldSku[$rowSku])) { // existing row
1158:                         $entityRowsUp[] = array(
1159:                             'updated_at' => now(),
1160:                             'entity_id'  => $this->_oldSku[$rowSku]['entity_id']
1161:                         );
1162:                     } else { // new row
1163:                         if (!$productLimit || $productsQty < $productLimit) {
1164:                             $entityRowsIn[$rowSku] = array(
1165:                                 'entity_type_id'   => $this->_entityTypeId,
1166:                                 'attribute_set_id' => $this->_newSku[$rowSku]['attr_set_id'],
1167:                                 'type_id'          => $this->_newSku[$rowSku]['type_id'],
1168:                                 'sku'              => $rowSku,
1169:                                 'created_at'       => now(),
1170:                                 'updated_at'       => now()
1171:                             );
1172:                             $productsQty++;
1173:                         } else {
1174:                             $rowSku = null; // sign for child rows to be skipped
1175:                             $this->_rowsToSkip[$rowNum] = true;
1176:                             continue;
1177:                         }
1178:                     }
1179:                 } elseif (null === $rowSku) {
1180:                     $this->_rowsToSkip[$rowNum] = true;
1181:                     continue; // skip rows when SKU is NULL
1182:                 } elseif (self::SCOPE_STORE == $rowScope) { // set necessary data from SCOPE_DEFAULT row
1183:                     $rowData[self::COL_TYPE]     = $this->_newSku[$rowSku]['type_id'];
1184:                     $rowData['attribute_set_id'] = $this->_newSku[$rowSku]['attr_set_id'];
1185:                     $rowData[self::COL_ATTR_SET] = $this->_newSku[$rowSku]['attr_set_code'];
1186:                 }
1187:                 if (!empty($rowData['_product_websites'])) { // 2. Product-to-Website phase
1188:                     $websites[$rowSku][$this->_websiteCodeToId[$rowData['_product_websites']]] = true;
1189:                 }
1190: 
1191:                 // 3. Categories phase
1192:                 $categoryPath = empty($rowData[self::COL_CATEGORY]) ? '' : $rowData[self::COL_CATEGORY];
1193:                 if (!empty($rowData[self::COL_ROOT_CATEGORY])) {
1194:                     $categoryId = $this->_categoriesWithRoots[$rowData[self::COL_ROOT_CATEGORY]][$categoryPath];
1195:                     $categories[$rowSku][$categoryId] = true;
1196:                 } elseif (!empty($categoryPath)) {
1197:                     $categories[$rowSku][$this->_categories[$categoryPath]] = true;
1198:                 }
1199: 
1200:                 if (!empty($rowData['_tier_price_website'])) { // 4.1. Tier prices phase
1201:                     $tierPrices[$rowSku][] = array(
1202:                         'all_groups'        => $rowData['_tier_price_customer_group'] == self::VALUE_ALL,
1203:                         'customer_group_id' => ($rowData['_tier_price_customer_group'] == self::VALUE_ALL)
1204:                             ? 0 : $rowData['_tier_price_customer_group'],
1205:                         'qty'               => $rowData['_tier_price_qty'],
1206:                         'value'             => $rowData['_tier_price_price'],
1207:                         'website_id'        => (self::VALUE_ALL == $rowData['_tier_price_website'] || $priceIsGlobal)
1208:                             ? 0 : $this->_websiteCodeToId[$rowData['_tier_price_website']]
1209:                     );
1210:                 }
1211:                 if (!empty($rowData['_group_price_website'])) { // 4.2. Group prices phase
1212:                     $groupPrices[$rowSku][] = array(
1213:                         'all_groups'        => $rowData['_group_price_customer_group'] == self::VALUE_ALL,
1214:                         'customer_group_id' => ($rowData['_group_price_customer_group'] == self::VALUE_ALL)
1215:                             ? 0 : $rowData['_group_price_customer_group'],
1216:                         'value'             => $rowData['_group_price_price'],
1217:                         'website_id'        => (self::VALUE_ALL == $rowData['_group_price_website'] || $priceIsGlobal)
1218:                             ? 0 : $this->_websiteCodeToId[$rowData['_group_price_website']]
1219:                     );
1220:                 }
1221:                 foreach ($this->_imagesArrayKeys as $imageCol) {
1222:                     if (!empty($rowData[$imageCol])) { // 5. Media gallery phase
1223:                         if (!array_key_exists($rowData[$imageCol], $uploadedGalleryFiles)) {
1224:                             $uploadedGalleryFiles[$rowData[$imageCol]] = $this->_uploadMediaFiles($rowData[$imageCol]);
1225:                         }
1226:                         $rowData[$imageCol] = $uploadedGalleryFiles[$rowData[$imageCol]];
1227:                     }
1228:                 }
1229:                 if (!empty($rowData['_media_image'])) {
1230:                     $mediaGallery[$rowSku][] = array(
1231:                         'attribute_id'      => $rowData['_media_attribute_id'],
1232:                         'label'             => $rowData['_media_lable'],
1233:                         'position'          => $rowData['_media_position'],
1234:                         'disabled'          => $rowData['_media_is_disabled'],
1235:                         'value'             => $rowData['_media_image']
1236:                     );
1237:                 }
1238:                 // 6. Attributes phase
1239:                 $rowStore     = self::SCOPE_STORE == $rowScope ? $this->_storeCodeToId[$rowData[self::COL_STORE]] : 0;
1240:                 $productType  = $rowData[self::COL_TYPE];
1241:                 if(!is_null($rowData[self::COL_TYPE])) {
1242:                     $previousType = $rowData[self::COL_TYPE];
1243:                 }
1244:                 if(!is_null($rowData[self::COL_ATTR_SET])) {
1245:                     $previousAttributeSet = $rowData[Mage_ImportExport_Model_Import_Entity_Product::COL_ATTR_SET];
1246:                 }
1247:                 if (self::SCOPE_NULL == $rowScope) {
1248:                     // for multiselect attributes only
1249:                     if(!is_null($previousAttributeSet)) {
1250:                         $rowData[Mage_ImportExport_Model_Import_Entity_Product::COL_ATTR_SET] = $previousAttributeSet;
1251:                     }
1252:                     if(is_null($productType) && !is_null($previousType)) {
1253:                         $productType = $previousType;
1254:                     }
1255:                     if(is_null($productType)) {
1256:                         continue;
1257:                     }
1258:                 }
1259:                 $rowData      = $this->_productTypeModels[$productType]->prepareAttributesForSave($rowData);
1260:                 $product      = Mage::getModel('importexport/import_proxy_product', $rowData);
1261: 
1262:                 foreach ($rowData as $attrCode => $attrValue) {
1263:                     $attribute = $resource->getAttribute($attrCode);
1264:                     if('multiselect' != $attribute->getFrontendInput()
1265:                         && self::SCOPE_NULL == $rowScope) {
1266:                         continue; // skip attribute processing for SCOPE_NULL rows
1267:                     }
1268:                     $attrId    = $attribute->getId();
1269:                     $backModel = $attribute->getBackendModel();
1270:                     $attrTable = $attribute->getBackend()->getTable();
1271:                     $storeIds  = array(0);
1272: 
1273:                     if ('datetime' == $attribute->getBackendType() && strtotime($attrValue)) {
1274:                         $attrValue = gmstrftime($strftimeFormat, strtotime($attrValue));
1275:                     } elseif ($backModel) {
1276:                         $attribute->getBackend()->beforeSave($product);
1277:                         $attrValue = $product->getData($attribute->getAttributeCode());
1278:                     }
1279:                     if (self::SCOPE_STORE == $rowScope) {
1280:                         if (self::SCOPE_WEBSITE == $attribute->getIsGlobal()) {
1281:                             // check website defaults already set
1282:                             if (!isset($attributes[$attrTable][$rowSku][$attrId][$rowStore])) {
1283:                                 $storeIds = $this->_storeIdToWebsiteStoreIds[$rowStore];
1284:                             }
1285:                         } elseif (self::SCOPE_STORE == $attribute->getIsGlobal()) {
1286:                             $storeIds = array($rowStore);
1287:                         }
1288:                     }
1289:                     foreach ($storeIds as $storeId) {
1290:                         if('multiselect' == $attribute->getFrontendInput()) {
1291:                             if(!isset($attributes[$attrTable][$rowSku][$attrId][$storeId])) {
1292:                                 $attributes[$attrTable][$rowSku][$attrId][$storeId] = '';
1293:                             } else {
1294:                                 $attributes[$attrTable][$rowSku][$attrId][$storeId] .= ',';
1295:                             }
1296:                             $attributes[$attrTable][$rowSku][$attrId][$storeId] .= $attrValue;
1297:                         } else {
1298:                             $attributes[$attrTable][$rowSku][$attrId][$storeId] = $attrValue;
1299:                         }
1300:                     }
1301:                     $attribute->setBackendModel($backModel); // restore 'backend_model' to avoid 'default' setting
1302:                 }
1303:             }
1304:             $this->_saveProductEntity($entityRowsIn, $entityRowsUp)
1305:                 ->_saveProductWebsites($websites)
1306:                 ->_saveProductCategories($categories)
1307:                 ->_saveProductTierPrices($tierPrices)
1308:                 ->_saveProductGroupPrices($groupPrices)
1309:                 ->_saveMediaGallery($mediaGallery)
1310:                 ->_saveProductAttributes($attributes);
1311:         }
1312:         return $this;
1313:     }
1314: 
1315:     /**
1316:      * Save product tier prices.
1317:      *
1318:      * @param array $tierPriceData
1319:      * @return Mage_ImportExport_Model_Import_Entity_Product
1320:      */
1321:     protected function _saveProductTierPrices(array $tierPriceData)
1322:     {
1323:         static $tableName = null;
1324: 
1325:         if (!$tableName) {
1326:             $tableName = Mage::getModel('importexport/import_proxy_product_resource')
1327:                     ->getTable('catalog/product_attribute_tier_price');
1328:         }
1329:         if ($tierPriceData) {
1330:             $tierPriceIn  = array();
1331:             $delProductId = array();
1332: 
1333:             foreach ($tierPriceData as $delSku => $tierPriceRows) {
1334:                 $productId      = $this->_newSku[$delSku]['entity_id'];
1335:                 $delProductId[] = $productId;
1336: 
1337:                 foreach ($tierPriceRows as $row) {
1338:                     $row['entity_id'] = $productId;
1339:                     $tierPriceIn[]  = $row;
1340:                 }
1341:             }
1342:             if (Mage_ImportExport_Model_Import::BEHAVIOR_APPEND != $this->getBehavior()) {
1343:                 $this->_connection->delete(
1344:                     $tableName,
1345:                     $this->_connection->quoteInto('entity_id IN (?)', $delProductId)
1346:                 );
1347:             }
1348:             if ($tierPriceIn) {
1349:                 $this->_connection->insertOnDuplicate($tableName, $tierPriceIn, array('value'));
1350:             }
1351:         }
1352:         return $this;
1353:     }
1354: 
1355:     /**
1356:      * Save product group prices.
1357:      *
1358:      * @param array $groupPriceData
1359:      * @return Mage_ImportExport_Model_Import_Entity_Product
1360:      */
1361:     protected function _saveProductGroupPrices(array $groupPriceData)
1362:     {
1363:         static $tableName = null;
1364: 
1365:         if (!$tableName) {
1366:             $tableName = Mage::getModel('importexport/import_proxy_product_resource')
1367:                 ->getTable('catalog/product_attribute_group_price');
1368:         }
1369:         if ($groupPriceData) {
1370:             $groupPriceIn = array();
1371:             $delProductId = array();
1372: 
1373:             foreach ($groupPriceData as $delSku => $groupPriceRows) {
1374:                 $productId      = $this->_newSku[$delSku]['entity_id'];
1375:                 $delProductId[] = $productId;
1376: 
1377:                 foreach ($groupPriceRows as $row) {
1378:                     $row['entity_id'] = $productId;
1379:                     $groupPriceIn[]  = $row;
1380:                 }
1381:             }
1382:             if (Mage_ImportExport_Model_Import::BEHAVIOR_APPEND != $this->getBehavior()) {
1383:                 $this->_connection->delete(
1384:                     $tableName,
1385:                     $this->_connection->quoteInto('entity_id IN (?)', $delProductId)
1386:                 );
1387:             }
1388:             if ($groupPriceIn) {
1389:                 $this->_connection->insertOnDuplicate($tableName, $groupPriceIn, array('value'));
1390:             }
1391:         }
1392:         return $this;
1393:     }
1394: 
1395:     /**
1396:      * Returns an object for upload a media files
1397:      */
1398:     protected function _getUploader()
1399:     {
1400:         if (is_null($this->_fileUploader)) {
1401:             $this->_fileUploader    = new Mage_ImportExport_Model_Import_Uploader();
1402: 
1403:             $this->_fileUploader->init();
1404: 
1405:             $tmpDir     = Mage::getConfig()->getOptions()->getMediaDir() . '/import';
1406:             $destDir    = Mage::getConfig()->getOptions()->getMediaDir() . '/catalog/product';
1407:             if (!is_writable($destDir)) {
1408:                 @mkdir($destDir, 0777, true);
1409:             }
1410:             if (!$this->_fileUploader->setTmpDir($tmpDir)) {
1411:                 Mage::throwException("File directory '{$tmpDir}' is not readable.");
1412:             }
1413:             if (!$this->_fileUploader->setDestDir($destDir)) {
1414:                 Mage::throwException("File directory '{$destDir}' is not writable.");
1415:             }
1416:         }
1417:         return $this->_fileUploader;
1418:     }
1419: 
1420:     /**
1421:      * Uploading files into the "catalog/product" media folder.
1422:      * Return a new file name if the same file is already exists.
1423:      *
1424:      * @param string $fileName
1425:      * @return string
1426:      */
1427:     protected function _uploadMediaFiles($fileName)
1428:     {
1429:         try {
1430:             $res = $this->_getUploader()->move($fileName);
1431:             return $res['file'];
1432:         } catch (Exception $e) {
1433:             return '';
1434:         }
1435:     }
1436: 
1437:     /**
1438:      * Save product media gallery.
1439:      *
1440:      * @param array $mediaGalleryData
1441:      * @return Mage_ImportExport_Model_Import_Entity_Product
1442:      */
1443:     protected function _saveMediaGallery(array $mediaGalleryData)
1444:     {
1445:         if (empty($mediaGalleryData)) {
1446:             return $this;
1447:         }
1448: 
1449:         static $mediaGalleryTableName = null;
1450:         static $mediaValueTableName = null;
1451:         static $productId = null;
1452: 
1453:         if (!$mediaGalleryTableName) {
1454:             $mediaGalleryTableName = Mage::getModel('importexport/import_proxy_product_resource')
1455:                     ->getTable('catalog/product_attribute_media_gallery');
1456:         }
1457: 
1458:         if (!$mediaValueTableName) {
1459:             $mediaValueTableName = Mage::getModel('importexport/import_proxy_product_resource')
1460:                     ->getTable('catalog/product_attribute_media_gallery_value');
1461:         }
1462: 
1463:         foreach ($mediaGalleryData as $productSku => $mediaGalleryRows) {
1464:             $productId = $this->_newSku[$productSku]['entity_id'];
1465:             $insertedGalleryImgs = array();
1466: 
1467:             if (Mage_ImportExport_Model_Import::BEHAVIOR_APPEND != $this->getBehavior()) {
1468:                 $this->_connection->delete(
1469:                     $mediaGalleryTableName,
1470:                     $this->_connection->quoteInto('entity_id IN (?)', $productId)
1471:                 );
1472:             }
1473: 
1474:             foreach ($mediaGalleryRows as $insertValue) {
1475: 
1476:                 if (!in_array($insertValue['value'], $insertedGalleryImgs)) {
1477:                     $valueArr = array(
1478:                         'attribute_id' => $insertValue['attribute_id'],
1479:                         'entity_id'    => $productId,
1480:                         'value'        => $insertValue['value']
1481:                     );
1482: 
1483:                     $this->_connection
1484:                             ->insertOnDuplicate($mediaGalleryTableName, $valueArr, array('entity_id'));
1485: 
1486:                     $insertedGalleryImgs[] = $insertValue['value'];
1487:                 }
1488: 
1489:                 $newMediaValues = $this->_connection->fetchPairs($this->_connection->select()
1490:                                         ->from($mediaGalleryTableName, array('value', 'value_id'))
1491:                                         ->where('entity_id IN (?)', $productId)
1492:                 );
1493: 
1494:                 if (array_key_exists($insertValue['value'], $newMediaValues)) {
1495:                     $insertValue['value_id'] = $newMediaValues[$insertValue['value']];
1496:                 }
1497: 
1498:                 $valueArr = array(
1499:                     'value_id' => $insertValue['value_id'],
1500:                     'store_id' => Mage_Catalog_Model_Abstract::DEFAULT_STORE_ID,
1501:                     'label'    => $insertValue['label'],
1502:                     'position' => $insertValue['position'],
1503:                     'disabled' => $insertValue['disabled']
1504:                 );
1505: 
1506:                 try {
1507:                     $this->_connection
1508:                             ->insertOnDuplicate($mediaValueTableName, $valueArr, array('value_id'));
1509:                 } catch (Exception $e) {
1510:                     $this->_connection->delete(
1511:                             $mediaGalleryTableName, $this->_connection->quoteInto('value_id IN (?)', $newMediaValues)
1512:                     );
1513:                 }
1514:             }
1515:         }
1516: 
1517:         return $this;
1518:     }
1519: 
1520:     /**
1521:      * Save product websites.
1522:      *
1523:      * @param array $websiteData
1524:      * @return Mage_ImportExport_Model_Import_Entity_Product
1525:      */
1526:     protected function _saveProductWebsites(array $websiteData)
1527:     {
1528:         static $tableName = null;
1529: 
1530:         if (!$tableName) {
1531:             $tableName = Mage::getModel('importexport/import_proxy_product_resource')->getProductWebsiteTable();
1532:         }
1533:         if ($websiteData) {
1534:             $websitesData = array();
1535:             $delProductId = array();
1536: 
1537:             foreach ($websiteData as $delSku => $websites) {
1538:                 $productId      = $this->_newSku[$delSku]['entity_id'];
1539:                 $delProductId[] = $productId;
1540: 
1541:                 foreach (array_keys($websites) as $websiteId) {
1542:                     $websitesData[] = array(
1543:                         'product_id' => $productId,
1544:                         'website_id' => $websiteId
1545:                     );
1546:                 }
1547:             }
1548:             if (Mage_ImportExport_Model_Import::BEHAVIOR_APPEND != $this->getBehavior()) {
1549:                 $this->_connection->delete(
1550:                     $tableName,
1551:                     $this->_connection->quoteInto('product_id IN (?)', $delProductId)
1552:                 );
1553:             }
1554:             if ($websitesData) {
1555:                 $this->_connection->insertOnDuplicate($tableName, $websitesData);
1556:             }
1557:         }
1558:         return $this;
1559:     }
1560: 
1561:     /**
1562:      * Stock item saving.
1563:      *
1564:      * @return Mage_ImportExport_Model_Import_Entity_Product
1565:      */
1566:     protected function _saveStockItem()
1567:     {
1568:         $defaultStockData = array(
1569:             'manage_stock'                  => 1,
1570:             'use_config_manage_stock'       => 1,
1571:             'qty'                           => 0,
1572:             'min_qty'                       => 0,
1573:             'use_config_min_qty'            => 1,
1574:             'min_sale_qty'                  => 1,
1575:             'use_config_min_sale_qty'       => 1,
1576:             'max_sale_qty'                  => 10000,
1577:             'use_config_max_sale_qty'       => 1,
1578:             'is_qty_decimal'                => 0,
1579:             'backorders'                    => 0,
1580:             'use_config_backorders'         => 1,
1581:             'notify_stock_qty'              => 1,
1582:             'use_config_notify_stock_qty'   => 1,
1583:             'enable_qty_increments'         => 0,
1584:             'use_config_enable_qty_inc'     => 1,
1585:             'qty_increments'                => 0,
1586:             'use_config_qty_increments'     => 1,
1587:             'is_in_stock'                   => 0,
1588:             'low_stock_date'                => null,
1589:             'stock_status_changed_auto'     => 0,
1590:             'is_decimal_divided'            => 0
1591:         );
1592: 
1593:         $entityTable = Mage::getResourceModel('cataloginventory/stock_item')->getMainTable();
1594:         $helper      = Mage::helper('catalogInventory');
1595: 
1596:         while ($bunch = $this->_dataSourceModel->getNextBunch()) {
1597:             $stockData = array();
1598: 
1599:             // Format bunch to stock data rows
1600:             foreach ($bunch as $rowNum => $rowData) {
1601:                 if (!$this->isRowAllowedToImport($rowData, $rowNum)) {
1602:                     continue;
1603:                 }
1604:                 // only SCOPE_DEFAULT can contain stock data
1605:                 if (self::SCOPE_DEFAULT != $this->getRowScope($rowData)) {
1606:                     continue;
1607:                 }
1608: 
1609:                 $row['product_id'] = $this->_newSku[$rowData[self::COL_SKU]]['entity_id'];
1610:                 $row['stock_id'] = 1;
1611: 
1612:                 /** @var $stockItem Mage_CatalogInventory_Model_Stock_Item */
1613:                 $stockItem = Mage::getModel('cataloginventory/stock_item');
1614:                 $stockItem->loadByProduct($row['product_id']);
1615:                 $existStockData = $stockItem->getData();
1616: 
1617:                 $row = array_merge(
1618:                     $defaultStockData,
1619:                     array_intersect_key($existStockData, $defaultStockData),
1620:                     array_intersect_key($rowData, $defaultStockData),
1621:                     $row
1622:                 );
1623: 
1624:                 $stockItem->setData($row);
1625: 
1626:                 if ($helper->isQty($this->_newSku[$rowData[self::COL_SKU]]['type_id'])) {
1627:                     if ($stockItem->verifyNotification()) {
1628:                         $stockItem->setLowStockDate(Mage::app()->getLocale()
1629:                             ->date(null, null, null, false)
1630:                             ->toString(Varien_Date::DATETIME_INTERNAL_FORMAT)
1631:                         );
1632:                     }
1633:                     $stockItem->setStockStatusChangedAutomatically((int) !$stockItem->verifyStock());
1634:                 } else {
1635:                     $stockItem->setQty(0);
1636:                 }
1637:                 $stockData[] = $stockItem->unsetOldData()->getData();
1638:             }
1639: 
1640:             // Insert rows
1641:             if ($stockData) {
1642:                 $this->_connection->insertOnDuplicate($entityTable, $stockData);
1643:             }
1644:         }
1645:         return $this;
1646:     }
1647: 
1648:     /**
1649:      * Atttribute set ID-to-name pairs getter.
1650:      *
1651:      * @return array
1652:      */
1653:     public function getAttrSetIdToName()
1654:     {
1655:         return $this->_attrSetIdToName;
1656:     }
1657: 
1658:     /**
1659:      * DB connection getter.
1660:      *
1661:      * @return Varien_Db_Adapter_Pdo_Mysql
1662:      */
1663:     public function getConnection()
1664:     {
1665:         return $this->_connection;
1666:     }
1667: 
1668:     /**
1669:      * EAV entity type code getter.
1670:      *
1671:      * @abstract
1672:      * @return string
1673:      */
1674:     public function getEntityTypeCode()
1675:     {
1676:         return 'catalog_product';
1677:     }
1678: 
1679:     /**
1680:      * New products SKU data.
1681:      *
1682:      * @return array
1683:      */
1684:     public function getNewSku()
1685:     {
1686:         return $this->_newSku;
1687:     }
1688: 
1689:     /**
1690:      * Get next bunch of validatetd rows.
1691:      *
1692:      * @return array|null
1693:      */
1694:     public function getNextBunch()
1695:     {
1696:         return $this->_dataSourceModel->getNextBunch();
1697:     }
1698: 
1699:     /**
1700:      * Existing products SKU getter.
1701:      *
1702:      * @return array
1703:      */
1704:     public function getOldSku()
1705:     {
1706:         return $this->_oldSku;
1707:     }
1708: 
1709:     /**
1710:      * Obtain scope of the row from row data.
1711:      *
1712:      * @param array $rowData
1713:      * @return int
1714:      */
1715:     public function getRowScope(array $rowData)
1716:     {
1717:         if (strlen(trim($rowData[self::COL_SKU]))) {
1718:             return self::SCOPE_DEFAULT;
1719:         } elseif (empty($rowData[self::COL_STORE])) {
1720:             return self::SCOPE_NULL;
1721:         } else {
1722:             return self::SCOPE_STORE;
1723:         }
1724:     }
1725: 
1726:     /**
1727:      * All website codes to ID getter.
1728:      *
1729:      * @return array
1730:      */
1731:     public function getWebsiteCodes()
1732:     {
1733:         return $this->_websiteCodeToId;
1734:     }
1735: 
1736:     /**
1737:      * Validate data row.
1738:      *
1739:      * @param array $rowData
1740:      * @param int $rowNum
1741:      * @return boolean
1742:      */
1743:     public function validateRow(array $rowData, $rowNum)
1744:     {
1745:         static $sku = null; // SKU is remembered through all product rows
1746: 
1747:         if (isset($this->_validatedRows[$rowNum])) { // check that row is already validated
1748:             return !isset($this->_invalidRows[$rowNum]);
1749:         }
1750:         $this->_validatedRows[$rowNum] = true;
1751: 
1752:         if (isset($this->_newSku[$rowData[self::COL_SKU]])) {
1753:             $this->addRowError(self::ERROR_DUPLICATE_SKU, $rowNum);
1754:             return false;
1755:         }
1756:         $rowScope = $this->getRowScope($rowData);
1757: 
1758:         // BEHAVIOR_DELETE use specific validation logic
1759:         if (Mage_ImportExport_Model_Import::BEHAVIOR_DELETE == $this->getBehavior()) {
1760:             if (self::SCOPE_DEFAULT == $rowScope && !isset($this->_oldSku[$rowData[self::COL_SKU]])) {
1761:                 $this->addRowError(self::ERROR_SKU_NOT_FOUND_FOR_DELETE, $rowNum);
1762:                 return false;
1763:             }
1764:             return true;
1765:         }
1766:         // common validation
1767:         $this->_isProductWebsiteValid($rowData, $rowNum);
1768:         $this->_isProductCategoryValid($rowData, $rowNum);
1769:         $this->_isTierPriceValid($rowData, $rowNum);
1770:         $this->_isGroupPriceValid($rowData, $rowNum);
1771:         $this->_isSuperProductsSkuValid($rowData, $rowNum);
1772: 
1773:         if (self::SCOPE_DEFAULT == $rowScope) { // SKU is specified, row is SCOPE_DEFAULT, new product block begins
1774:             $this->_processedEntitiesCount ++;
1775: 
1776:             $sku = $rowData[self::COL_SKU];
1777: 
1778:             if (isset($this->_oldSku[$sku])) { // can we get all necessary data from existant DB product?
1779:                 // check for supported type of existing product
1780:                 if (isset($this->_productTypeModels[$this->_oldSku[$sku]['type_id']])) {
1781:                     $this->_newSku[$sku] = array(
1782:                         'entity_id'     => $this->_oldSku[$sku]['entity_id'],
1783:                         'type_id'       => $this->_oldSku[$sku]['type_id'],
1784:                         'attr_set_id'   => $this->_oldSku[$sku]['attr_set_id'],
1785:                         'attr_set_code' => $this->_attrSetIdToName[$this->_oldSku[$sku]['attr_set_id']]
1786:                     );
1787:                 } else {
1788:                     $this->addRowError(self::ERROR_TYPE_UNSUPPORTED, $rowNum);
1789:                     $sku = false; // child rows of legacy products with unsupported types are orphans
1790:                 }
1791:             } else { // validate new product type and attribute set
1792:                 if (!isset($rowData[self::COL_TYPE])
1793:                     || !isset($this->_productTypeModels[$rowData[self::COL_TYPE]])
1794:                 ) {
1795:                     $this->addRowError(self::ERROR_INVALID_TYPE, $rowNum);
1796:                 } elseif (!isset($rowData[self::COL_ATTR_SET])
1797:                           || !isset($this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]])
1798:                 ) {
1799:                     $this->addRowError(self::ERROR_INVALID_ATTR_SET, $rowNum);
1800:                 } elseif (!isset($this->_newSku[$sku])) {
1801:                     $this->_newSku[$sku] = array(
1802:                         'entity_id'     => null,
1803:                         'type_id'       => $rowData[self::COL_TYPE],
1804:                         'attr_set_id'   => $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]],
1805:                         'attr_set_code' => $rowData[self::COL_ATTR_SET]
1806:                     );
1807:                 }
1808:                 if (isset($this->_invalidRows[$rowNum])) {
1809:                     // mark SCOPE_DEFAULT row as invalid for future child rows if product not in DB already
1810:                     $sku = false;
1811:                 }
1812:             }
1813:         } else {
1814:             if (null === $sku) {
1815:                 $this->addRowError(self::ERROR_SKU_IS_EMPTY, $rowNum);
1816:             } elseif (false === $sku) {
1817:                 $this->addRowError(self::ERROR_ROW_IS_ORPHAN, $rowNum);
1818:             } elseif (self::SCOPE_STORE == $rowScope && !isset($this->_storeCodeToId[$rowData[self::COL_STORE]])) {
1819:                 $this->addRowError(self::ERROR_INVALID_STORE, $rowNum);
1820:             }
1821:         }
1822:         if (!isset($this->_invalidRows[$rowNum])) {
1823:             // set attribute set code into row data for followed attribute validation in type model
1824:             $rowData[self::COL_ATTR_SET] = $this->_newSku[$sku]['attr_set_code'];
1825: 
1826:             $rowAttributesValid = $this->_productTypeModels[$this->_newSku[$sku]['type_id']]->isRowValid(
1827:                 $rowData, $rowNum, !isset($this->_oldSku[$sku])
1828:             );
1829:             if (!$rowAttributesValid && self::SCOPE_DEFAULT == $rowScope && !isset($this->_oldSku[$sku])) {
1830:                 $sku = false; // mark SCOPE_DEFAULT row as invalid for future child rows if product not in DB already
1831:             }
1832:         }
1833:         return !isset($this->_invalidRows[$rowNum]);
1834:     }
1835: 
1836:     /**
1837:      * Get array of affected products
1838:      *
1839:      * @return array
1840:      */
1841:     public function getAffectedEntityIds()
1842:     {
1843:         $productIds = array();
1844:         while ($bunch = $this->_dataSourceModel->getNextBunch()) {
1845:             foreach ($bunch as $rowNum => $rowData) {
1846:                 if (!$this->isRowAllowedToImport($rowData, $rowNum)) {
1847:                     continue;
1848:                 }
1849:                 if (!isset($this->_newSku[$rowData[self::COL_SKU]]['entity_id'])) {
1850:                     continue;
1851:                 }
1852:                 $productIds[] = $this->_newSku[$rowData[self::COL_SKU]]['entity_id'];
1853:             }
1854:         }
1855:         return $productIds;
1856:     }
1857: }
1858: 
Magento 1.7.0.2 API documentation generated by ApiGen 2.8.0