PageRenderTime 53ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 1ms

/app/code/core/Mage/ImportExport/Model/Import/Entity/Product.php

https://bitbucket.org/claudiu_marginean/magento-hg-mirror
PHP | 1472 lines | 1025 code | 104 blank | 343 comment | 187 complexity | ab7c1c659a000bea23ca4f2b94447a6e MD5 | raw file
Possible License(s): CC-BY-SA-3.0, LGPL-2.1, GPL-2.0, WTFPL
  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) 2010 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. * Import entity product model
  28. *
  29. * @category Mage
  30. * @package Mage_ImportExport
  31. * @author Magento Core Team <core@magentocommerce.com>
  32. */
  33. class Mage_ImportExport_Model_Import_Entity_Product extends Mage_ImportExport_Model_Import_Entity_Abstract
  34. {
  35. const CONFIG_KEY_PRODUCT_TYPES = 'global/importexport/import_product_types';
  36. /**
  37. * Size of bunch - part of products to save in one step.
  38. */
  39. const BUNCH_SIZE = 20;
  40. /**
  41. * Value that means all entities (e.g. websites, groups etc.)
  42. */
  43. const VALUE_ALL = 'all';
  44. /**
  45. * Data row scopes.
  46. */
  47. const SCOPE_DEFAULT = 1;
  48. const SCOPE_WEBSITE = 2;
  49. const SCOPE_STORE = 0;
  50. const SCOPE_NULL = -1;
  51. /**
  52. * Permanent column names.
  53. *
  54. * Names that begins with underscore is not an attribute. This name convention is for
  55. * to avoid interference with same attribute name.
  56. */
  57. const COL_STORE = '_store';
  58. const COL_ATTR_SET = '_attribute_set';
  59. const COL_TYPE = '_type';
  60. const COL_CATEGORY = '_category';
  61. const COL_SKU = 'sku';
  62. /**
  63. * Error codes.
  64. */
  65. const ERROR_INVALID_SCOPE = 'invalidScope';
  66. const ERROR_INVALID_WEBSITE = 'invalidWebsite';
  67. const ERROR_INVALID_STORE = 'invalidStore';
  68. const ERROR_INVALID_ATTR_SET = 'invalidAttrSet';
  69. const ERROR_INVALID_TYPE = 'invalidType';
  70. const ERROR_INVALID_CATEGORY = 'invalidCategory';
  71. const ERROR_VALUE_IS_REQUIRED = 'isRequired';
  72. const ERROR_TYPE_CHANGED = 'typeChanged';
  73. const ERROR_SKU_IS_EMPTY = 'skuEmpty';
  74. const ERROR_NO_DEFAULT_ROW = 'noDefaultRow';
  75. const ERROR_CHANGE_TYPE = 'changeProductType';
  76. const ERROR_DUPLICATE_SCOPE = 'duplicateScope';
  77. const ERROR_DUPLICATE_SKU = 'duplicateSKU';
  78. const ERROR_CHANGE_ATTR_SET = 'changeAttrSet';
  79. const ERROR_TYPE_UNSUPPORTED = 'productTypeUnsupported';
  80. const ERROR_ROW_IS_ORPHAN = 'rowIsOrphan';
  81. const ERROR_INVALID_TIER_PRICE_QTY = 'invalidTierPriceOrQty';
  82. const ERROR_INVALID_TIER_PRICE_SITE = 'tierPriceWebsiteInvalid';
  83. const ERROR_INVALID_TIER_PRICE_GROUP = 'tierPriceGroupInvalid';
  84. const ERROR_TIER_DATA_INCOMPLETE = 'tierPriceDataIsIncomplete';
  85. const ERROR_SKU_NOT_FOUND_FOR_DELETE = 'skuNotFoundToDelete';
  86. /**
  87. * Pairs of attribute set ID-to-name.
  88. *
  89. * @var array
  90. */
  91. protected $_attrSetIdToName = array();
  92. /**
  93. * Pairs of attribute set name-to-ID.
  94. *
  95. * @var array
  96. */
  97. protected $_attrSetNameToId = array();
  98. /**
  99. * Categories text-path to ID hash.
  100. *
  101. * @var array
  102. */
  103. protected $_categories = array();
  104. /**
  105. * Customer groups ID-to-name.
  106. *
  107. * @var array
  108. */
  109. protected $_customerGroups = array();
  110. /**
  111. * Attributes with index (not label) value.
  112. *
  113. * @var array
  114. */
  115. protected $_indexValueAttributes = array(
  116. 'status',
  117. 'tax_class_id',
  118. 'visibility',
  119. 'enable_googlecheckout',
  120. 'gift_message_available',
  121. 'custom_design'
  122. );
  123. /**
  124. * Links attribute name-to-link type ID.
  125. *
  126. * @var array
  127. */
  128. protected $_linkNameToId = array(
  129. '_links_related_' => Mage_Catalog_Model_Product_Link::LINK_TYPE_RELATED,
  130. '_links_crosssell_' => Mage_Catalog_Model_Product_Link::LINK_TYPE_CROSSSELL,
  131. '_links_upsell_' => Mage_Catalog_Model_Product_Link::LINK_TYPE_UPSELL
  132. );
  133. /**
  134. * Validation failure message template definitions
  135. *
  136. * @var array
  137. */
  138. protected $_messageTemplates = array(
  139. self::ERROR_INVALID_SCOPE => 'Invalid value in Scope column',
  140. self::ERROR_INVALID_WEBSITE => 'Invalid value in Website column (website does not exists?)',
  141. self::ERROR_INVALID_STORE => 'Invalid value in Store column (store does not exists?)',
  142. self::ERROR_INVALID_ATTR_SET => 'Invalid value for Attribute Set column (set does not exists?)',
  143. self::ERROR_INVALID_TYPE => 'Product Type is invalid or not supported',
  144. self::ERROR_INVALID_CATEGORY => 'Category does not exists',
  145. self::ERROR_VALUE_IS_REQUIRED => "Required attribute '%s' has an empty value",
  146. self::ERROR_TYPE_CHANGED => 'Trying to change type of existing products',
  147. self::ERROR_SKU_IS_EMPTY => 'SKU is empty',
  148. self::ERROR_NO_DEFAULT_ROW => 'Default values row does not exists',
  149. self::ERROR_CHANGE_TYPE => 'Product type change is not allowed',
  150. self::ERROR_DUPLICATE_SCOPE => 'Duplicate scope',
  151. self::ERROR_DUPLICATE_SKU => 'Duplicate SKU',
  152. self::ERROR_CHANGE_ATTR_SET => 'Product attribute set change is not allowed',
  153. self::ERROR_TYPE_UNSUPPORTED => 'Product type is not supported',
  154. self::ERROR_ROW_IS_ORPHAN => 'Orphan rows that will be skipped due default row errors',
  155. self::ERROR_INVALID_TIER_PRICE_QTY => 'Tier Price data price or quantity value is invalid',
  156. self::ERROR_INVALID_TIER_PRICE_SITE => 'Tier Price data website is invalid',
  157. self::ERROR_INVALID_TIER_PRICE_GROUP => 'Tier Price customer group ID is invalid',
  158. self::ERROR_TIER_DATA_INCOMPLETE => 'Tier Price data is incomplete',
  159. self::ERROR_SKU_NOT_FOUND_FOR_DELETE => 'Product with specified SKU not found'
  160. );
  161. /**
  162. * Dry-runned products information from import file.
  163. *
  164. * [SKU] => array(
  165. * 'type_id' => (string) product type
  166. * 'attr_set_id' => (int) product attribute set ID
  167. * 'entity_id' => (int) product ID (value for new products will be set after entity save)
  168. * 'attr_set_code' => (string) attribute set code
  169. * )
  170. *
  171. * @var array
  172. */
  173. protected $_newSku = array();
  174. /**
  175. * Existing products SKU-related information in form of array:
  176. *
  177. * [SKU] => array(
  178. * 'type_id' => (string) product type
  179. * 'attr_set_id' => (int) product attribute set ID
  180. * 'entity_id' => (int) product ID
  181. * 'supported_type' => (boolean) is product type supported by current version of import module
  182. * )
  183. *
  184. * @var array
  185. */
  186. protected $_oldSku = array();
  187. /**
  188. * Column names that holds values with particular meaning.
  189. *
  190. * @var array
  191. */
  192. protected $_particularAttributes = array(
  193. '_store', '_attribute_set', '_type', '_category', '_product_websites', '_tier_price_website',
  194. '_tier_price_customer_group', '_tier_price_qty', '_tier_price_price', '_links_related_sku',
  195. '_links_related_position', '_links_crosssell_sku', '_links_crosssell_position', '_links_upsell_sku',
  196. '_links_upsell_position', '_custom_option_store', '_custom_option_type', '_custom_option_title',
  197. '_custom_option_is_required', '_custom_option_price', '_custom_option_sku', '_custom_option_max_characters',
  198. '_custom_option_sort_order', '_custom_option_file_extension', '_custom_option_image_size_x',
  199. '_custom_option_image_size_y', '_custom_option_row_title', '_custom_option_row_price',
  200. '_custom_option_row_sku', '_custom_option_row_sort'
  201. );
  202. /**
  203. * Permanent entity columns.
  204. *
  205. * @var array
  206. */
  207. protected $_permanentAttributes = array(self::COL_SKU);
  208. /**
  209. * Array of supported product types as keys with appropriate model object as value.
  210. *
  211. * @var array
  212. */
  213. protected $_productTypeModels = array();
  214. /**
  215. * All stores code-ID pairs.
  216. *
  217. * @var array
  218. */
  219. protected $_storeCodeToId = array();
  220. /**
  221. * Store ID to its website stores IDs.
  222. *
  223. * @var array
  224. */
  225. protected $_storeIdToWebsiteStoreIds = array();
  226. /**
  227. * Website code-to-ID
  228. *
  229. * @var array
  230. */
  231. protected $_websiteCodeToId = array();
  232. /**
  233. * Website code to store code-to-ID pairs which it consists.
  234. *
  235. * @var array
  236. */
  237. protected $_websiteCodeToStoreIds = array();
  238. /**
  239. * Constructor.
  240. *
  241. * @return void
  242. */
  243. public function __construct()
  244. {
  245. parent::__construct();
  246. $this->_initWebsites()
  247. ->_initStores()
  248. ->_initAttributeSets()
  249. ->_initTypeModels()
  250. ->_initCategories()
  251. ->_initSkus()
  252. ->_initCustomerGroups();
  253. }
  254. /**
  255. * Delete products.
  256. *
  257. * @return Mage_ImportExport_Model_Import_Entity_Product
  258. */
  259. protected function _deleteProducts()
  260. {
  261. $productEntityTable = Mage::getModel('importexport/import_proxy_product_resource')->getEntityTable();
  262. while ($bunch = $this->_dataSourceModel->getNextBunch()) {
  263. $idToDelete = array();
  264. foreach ($bunch as $rowNum => $rowData) {
  265. if ($this->validateRow($rowData, $rowNum) && self::SCOPE_DEFAULT == $this->getRowScope($rowData)) {
  266. $idToDelete[] = $this->_oldSku[$rowData[self::COL_SKU]]['entity_id'];
  267. }
  268. }
  269. if ($idToDelete) {
  270. $this->_connection->query(
  271. $this->_connection->quoteInto(
  272. "DELETE FROM `{$productEntityTable}` WHERE `entity_id` IN (?)", $idToDelete
  273. )
  274. );
  275. }
  276. }
  277. return $this;
  278. }
  279. /**
  280. * Create Product entity from raw data.
  281. *
  282. * @throws Exception
  283. * @return bool Result of operation.
  284. */
  285. protected function _importData()
  286. {
  287. if (Mage_ImportExport_Model_Import::BEHAVIOR_DELETE == $this->getBehavior()) {
  288. $this->_deleteProducts();
  289. } else {
  290. $this->_saveProducts();
  291. $this->_saveStockItem();
  292. $this->_saveLinks();
  293. $this->_saveCustomOptions();
  294. foreach ($this->_productTypeModels as $productType => $productTypeModel) {
  295. $productTypeModel->saveData();
  296. }
  297. }
  298. return true;
  299. }
  300. /**
  301. * Initialize attribute sets code-to-id pairs.
  302. *
  303. * @return Mage_ImportExport_Model_Import_Entity_Product
  304. */
  305. protected function _initAttributeSets()
  306. {
  307. foreach (Mage::getResourceModel('eav/entity_attribute_set_collection')
  308. ->setEntityTypeFilter($this->_entityTypeId) as $attributeSet) {
  309. $this->_attrSetNameToId[$attributeSet->getAttributeSetName()] = $attributeSet->getId();
  310. $this->_attrSetIdToName[$attributeSet->getId()] = $attributeSet->getAttributeSetName();
  311. }
  312. return $this;
  313. }
  314. /**
  315. * Initialize categories text-path to ID hash.
  316. *
  317. * @return Mage_ImportExport_Model_Import_Entity_Product
  318. */
  319. protected function _initCategories()
  320. {
  321. $collection = Mage::getResourceModel('catalog/category_collection')->addNameToResult();
  322. /* @var $collection Mage_Catalog_Model_Resource_Eav_Mysql4_Category_Collection */
  323. foreach ($collection as $category) {
  324. $structure = explode('/', $category->getPath());
  325. $pathSize = count($structure);
  326. if ($pathSize > 2) {
  327. $path = array();
  328. for ($i = 2; $i < $pathSize; $i++) {
  329. $path[] = $collection->getItemById($structure[$i])->getName();
  330. }
  331. $this->_categories[implode('/', $path)] = $category->getId();
  332. }
  333. }
  334. return $this;
  335. }
  336. /**
  337. * Initialize customer groups.
  338. *
  339. * @return Mage_ImportExport_Model_Import_Entity_Product
  340. */
  341. protected function _initCustomerGroups()
  342. {
  343. foreach (Mage::getResourceModel('customer/group_collection') as $customerGroup) {
  344. $this->_customerGroups[$customerGroup->getId()] = true;
  345. }
  346. return $this;
  347. }
  348. /**
  349. * Initialize existent product SKUs.
  350. *
  351. * @return Mage_ImportExport_Model_Import_Entity_Product
  352. */
  353. protected function _initSkus()
  354. {
  355. foreach (Mage::getResourceModel('catalog/product_collection') as $product) {
  356. $typeId = $product->getTypeId();
  357. $sku = $product->getSku();
  358. $this->_oldSku[$sku] = array(
  359. 'type_id' => $typeId,
  360. 'attr_set_id' => $product->getAttributeSetId(),
  361. 'entity_id' => $product->getId(),
  362. 'supported_type' => isset($this->_productTypeModels[$typeId])
  363. );
  364. }
  365. return $this;
  366. }
  367. /**
  368. * Initialize stores hash.
  369. *
  370. * @return Mage_ImportExport_Model_Import_Entity_Product
  371. */
  372. protected function _initStores()
  373. {
  374. foreach (Mage::app()->getStores() as $store) {
  375. $this->_storeCodeToId[$store->getCode()] = $store->getId();
  376. $this->_storeIdToWebsiteStoreIds[$store->getId()] = $store->getWebsite()->getStoreIds();
  377. }
  378. return $this;
  379. }
  380. /**
  381. * Initialize product type models.
  382. *
  383. * @throws Exception
  384. * @return Mage_ImportExport_Model_Import_Entity_Product
  385. */
  386. protected function _initTypeModels()
  387. {
  388. $config = Mage::getConfig()->getNode(self::CONFIG_KEY_PRODUCT_TYPES)->asCanonicalArray();
  389. foreach ($config as $type => $typeModel) {
  390. if (!($model = Mage::getModel($typeModel, array($this, $type)))) {
  391. Mage::throwException("Entity type model '{$typeModel}' is not found");
  392. }
  393. if (! $model instanceof Mage_ImportExport_Model_Import_Entity_Product_Type_Abstract) {
  394. Mage::throwException(
  395. Mage::helper('importexport')->__('Entity type model must be an instance of Mage_ImportExport_Model_Import_Entity_Product_Type_Abstract')
  396. );
  397. }
  398. if ($model->isSuitable()) {
  399. $this->_productTypeModels[$type] = $model;
  400. }
  401. $this->_particularAttributes = array_merge(
  402. $this->_particularAttributes,
  403. $model->getParticularAttributes()
  404. );
  405. }
  406. // remove doubles
  407. $this->_particularAttributes = array_unique($this->_particularAttributes);
  408. return $this;
  409. }
  410. /**
  411. * Initialize website values.
  412. *
  413. * @return Mage_ImportExport_Model_Import_Entity_Product
  414. */
  415. protected function _initWebsites()
  416. {
  417. /** @var $website Mage_Core_Model_Website */
  418. foreach (Mage::app()->getWebsites() as $website) {
  419. $this->_websiteCodeToId[$website->getCode()] = $website->getId();
  420. $this->_websiteCodeToStoreIds[$website->getCode()] = array_flip($website->getStoreCodes());
  421. }
  422. return $this;
  423. }
  424. /**
  425. * Check product category validity.
  426. *
  427. * @param array $rowData
  428. * @param int $rowNum
  429. * @return bool
  430. */
  431. protected function _isProductCategoryValid(array $rowData, $rowNum)
  432. {
  433. if (!empty($rowData[self::COL_CATEGORY]) && !isset($this->_categories[$rowData[self::COL_CATEGORY]])) {
  434. $this->addRowError(self::ERROR_INVALID_CATEGORY, $rowNum);
  435. return false;
  436. }
  437. return true;
  438. }
  439. /**
  440. * Check product website belonging.
  441. *
  442. * @param array $rowData
  443. * @param int $rowNum
  444. * @return bool
  445. */
  446. protected function _isProductWebsiteValid(array $rowData, $rowNum)
  447. {
  448. if (!empty($rowData['_product_websites']) && !isset($this->_websiteCodeToId[$rowData['_product_websites']])) {
  449. $this->addRowError(self::ERROR_INVALID_WEBSITE, $rowNum);
  450. return false;
  451. }
  452. return true;
  453. }
  454. /**
  455. * Set valid attribute set and product type to rows with all scopes
  456. * to ensure that existing products doesn't changed.
  457. *
  458. * @param array $rowData
  459. * @return array
  460. */
  461. protected function _prepareRowForDb(array $rowData)
  462. {
  463. static $lastSku = null;
  464. if (Mage_ImportExport_Model_Import::BEHAVIOR_DELETE == $this->getBehavior()) {
  465. return $rowData;
  466. }
  467. if (self::SCOPE_DEFAULT == $this->getRowScope($rowData)) {
  468. $lastSku = $rowData[self::COL_SKU];
  469. }
  470. if (isset($this->_oldSku[$lastSku])) {
  471. $rowData[self::COL_ATTR_SET] = $this->_newSku[$lastSku]['attr_set_code'];
  472. $rowData[self::COL_TYPE] = $this->_newSku[$lastSku]['type_id'];
  473. }
  474. return $rowData;
  475. }
  476. /**
  477. * Check tier orice data validity.
  478. *
  479. * @param array $rowData
  480. * @param int $rowNum
  481. * @return bool
  482. */
  483. protected function _isTierPriceValid(array $rowData, $rowNum)
  484. {
  485. if ((isset($rowData['_tier_price_website']) && strlen($rowData['_tier_price_website']))
  486. || (isset($rowData['_tier_price_customer_group']) && strlen($rowData['_tier_price_customer_group']))
  487. || (isset($rowData['_tier_price_qty']) && strlen($rowData['_tier_price_qty']))
  488. || (isset($rowData['_tier_price_price']) && strlen($rowData['_tier_price_price']))
  489. ) {
  490. if (!isset($rowData['_tier_price_website']) || !isset($rowData['_tier_price_customer_group'])
  491. || !isset($rowData['_tier_price_qty']) || !isset($rowData['_tier_price_price'])
  492. || !strlen($rowData['_tier_price_website']) || !strlen($rowData['_tier_price_customer_group'])
  493. || !strlen($rowData['_tier_price_qty']) || !strlen($rowData['_tier_price_price'])
  494. ) {
  495. $this->addRowError(self::ERROR_TIER_DATA_INCOMPLETE, $rowNum);
  496. return false;
  497. } elseif ($rowData['_tier_price_website'] != self::VALUE_ALL
  498. && !isset($this->_websiteCodeToId[$rowData['_tier_price_website']])) {
  499. $this->addRowError(self::ERROR_INVALID_TIER_PRICE_SITE, $rowNum);
  500. return false;
  501. } elseif ($rowData['_tier_price_customer_group'] != self::VALUE_ALL
  502. && !isset($this->_customerGroups[$rowData['_tier_price_customer_group']])) {
  503. $this->addRowError(self::ERROR_INVALID_TIER_PRICE_GROUP, $rowNum);
  504. return false;
  505. } elseif ($rowData['_tier_price_qty'] <= 0 || $rowData['_tier_price_price'] <= 0) {
  506. $this->addRowError(self::ERROR_INVALID_TIER_PRICE_QTY, $rowNum);
  507. return false;
  508. }
  509. }
  510. return true;
  511. }
  512. /**
  513. * Custom options save.
  514. *
  515. * @return Mage_ImportExport_Model_Import_Entity_Product
  516. */
  517. protected function _saveCustomOptions()
  518. {
  519. $productTable = Mage::getSingleton('core/resource')->getTableName('catalog/product');
  520. $optionTable = Mage::getSingleton('core/resource')->getTableName('catalog/product_option');
  521. $priceTable = Mage::getSingleton('core/resource')->getTableName('catalog/product_option_price');
  522. $titleTable = Mage::getSingleton('core/resource')->getTableName('catalog/product_option_title');
  523. $typePriceTable = Mage::getSingleton('core/resource')->getTableName('catalog/product_option_type_price');
  524. $typeTitleTable = Mage::getSingleton('core/resource')->getTableName('catalog/product_option_type_title');
  525. $typeValueTable = Mage::getSingleton('core/resource')->getTableName('catalog/product_option_type_value');
  526. $nextOptionId = $this->getNextAutoincrement($optionTable);
  527. $nextValueId = $this->getNextAutoincrement($typeValueTable);
  528. $priceIsGlobal = Mage::helper('catalog')->isPriceGlobal();
  529. $type = null;
  530. $typeSpecific = array(
  531. 'date' => array('price', 'sku'),
  532. 'date_time' => array('price', 'sku'),
  533. 'time' => array('price', 'sku'),
  534. 'field' => array('price', 'sku', 'max_characters'),
  535. 'area' => array('price', 'sku', 'max_characters'),
  536. //'file' => array('price', 'sku', 'file_extension', 'image_size_x', 'image_size_y'),
  537. 'drop_down' => true,
  538. 'radio' => true,
  539. 'checkbox' => true,
  540. 'multiple' => true
  541. );
  542. while ($bunch = $this->_dataSourceModel->getNextBunch()) {
  543. $customOptions = array(
  544. 'product_id' => array(),
  545. $optionTable => array(),
  546. $priceTable => array(),
  547. $titleTable => array(),
  548. $typePriceTable => array(),
  549. $typeTitleTable => array(),
  550. $typeValueTable => array()
  551. );
  552. foreach ($bunch as $rowNum => $rowData) {
  553. if (!$this->isRowAllowedToImport($rowData, $rowNum)) {
  554. continue;
  555. }
  556. if (self::SCOPE_DEFAULT == $this->getRowScope($rowData)) {
  557. $productId = $this->_newSku[$rowData[self::COL_SKU]]['entity_id'];
  558. } elseif (!isset($productId)) {
  559. continue;
  560. }
  561. if (!empty($rowData['_custom_option_store'])) {
  562. if (!isset($this->_storeCodeToId[$rowData['_custom_option_store']])) {
  563. continue;
  564. }
  565. $storeId = $this->_storeCodeToId[$rowData['_custom_option_store']];
  566. } else {
  567. $storeId = Mage_Catalog_Model_Abstract::DEFAULT_STORE_ID;
  568. }
  569. if (!empty($rowData['_custom_option_type'])) { // get CO type if its specified
  570. if (!isset($typeSpecific[$rowData['_custom_option_type']])) {
  571. $type = null;
  572. continue;
  573. }
  574. $type = $rowData['_custom_option_type'];
  575. $rowIsMain = true;
  576. } else {
  577. if (null === $type) {
  578. continue;
  579. }
  580. $rowIsMain = false;
  581. }
  582. if (!isset($customOptions['product_id'][$productId])) { // for update product entity table
  583. $customOptions['product_id'][$productId] = array(
  584. 'entity_id' => $productId,
  585. 'has_options' => 0,
  586. 'required_options' => 0,
  587. 'updated_at' => now()
  588. );
  589. }
  590. if ($rowIsMain) {
  591. $solidParams = array(
  592. 'option_id' => $nextOptionId,
  593. 'sku' => '',
  594. 'max_characters' => 0,
  595. 'file_extension' => null,
  596. 'image_size_x' => 0,
  597. 'image_size_y' => 0,
  598. 'product_id' => $productId,
  599. 'type' => $type,
  600. 'is_require' => empty($rowData['_custom_option_is_required']) ? 0 : 1,
  601. 'sort_order' => empty($rowData['_custom_option_sort_order'])
  602. ? 0 : abs($rowData['_custom_option_sort_order'])
  603. );
  604. if (true !== $typeSpecific[$type]) { // simple option may have optional params
  605. $priceTableRow = array(
  606. 'option_id' => $nextOptionId,
  607. 'store_id' => Mage_Catalog_Model_Abstract::DEFAULT_STORE_ID,
  608. 'price' => 0,
  609. 'price_type' => 'fixed'
  610. );
  611. foreach ($typeSpecific[$type] as $paramSuffix) {
  612. if (isset($rowData['_custom_option_' . $paramSuffix])) {
  613. $data = $rowData['_custom_option_' . $paramSuffix];
  614. if (array_key_exists($paramSuffix, $solidParams)) {
  615. $solidParams[$paramSuffix] = $data;
  616. } elseif ('price' == $paramSuffix) {
  617. if ('%' == substr($data, -1)) {
  618. $priceTableRow['price_type'] = 'percent';
  619. }
  620. $priceTableRow['price'] = (float) rtrim($data, '%');
  621. }
  622. }
  623. }
  624. $customOptions[$priceTable][] = $priceTableRow;
  625. }
  626. $customOptions[$optionTable][] = $solidParams;
  627. $customOptions['product_id'][$productId]['has_options'] = 1;
  628. if (!empty($rowData['_custom_option_is_required'])) {
  629. $customOptions['product_id'][$productId]['required_options'] = 1;
  630. }
  631. $prevOptionId = $nextOptionId++; // increment option id, but preserve value for $typeValueTable
  632. }
  633. if ($typeSpecific[$type] === true && !empty($rowData['_custom_option_row_title'])
  634. && empty($rowData['_custom_option_store'])) {
  635. // complex CO option row
  636. $customOptions[$typeValueTable][$prevOptionId][] = array(
  637. 'option_type_id' => $nextValueId,
  638. 'sort_order' => empty($rowData['_custom_option_row_sort'])
  639. ? 0 : abs($rowData['_custom_option_row_sort']),
  640. 'sku' => !empty($rowData['_custom_option_row_sku'])
  641. ? $rowData['_custom_option_row_sku'] : ''
  642. );
  643. if (!isset($customOptions[$typeTitleTable][$nextValueId][0])) { // ensure default title is set
  644. $customOptions[$typeTitleTable][$nextValueId][0] = $rowData['_custom_option_row_title'];
  645. }
  646. $customOptions[$typeTitleTable][$nextValueId][$storeId] = $rowData['_custom_option_row_title'];
  647. if (!empty($rowData['_custom_option_row_price'])) {
  648. $typePriceRow = array(
  649. 'price' => (float) rtrim($rowData['_custom_option_row_price'], '%'),
  650. 'price_type' => 'fixed'
  651. );
  652. if ('%' == substr($rowData['_custom_option_row_price'], -1)) {
  653. $typePriceRow['price_type'] = 'percent';
  654. }
  655. if ($priceIsGlobal) {
  656. $customOptions[$typePriceTable][$nextValueId][0] = $typePriceRow;
  657. } else {
  658. // ensure default price is set
  659. if (!isset($customOptions[$typePriceTable][$nextValueId][0])) {
  660. $customOptions[$typePriceTable][$nextValueId][0] = $typePriceRow;
  661. }
  662. $customOptions[$typePriceTable][$nextValueId][$storeId] = $typePriceRow;
  663. }
  664. }
  665. $nextValueId++;
  666. }
  667. if (!empty($rowData['_custom_option_title'])) {
  668. if (!isset($customOptions[$titleTable][$prevOptionId][0])) { // ensure default title is set
  669. $customOptions[$titleTable][$prevOptionId][0] = $rowData['_custom_option_title'];
  670. }
  671. $customOptions[$titleTable][$prevOptionId][$storeId] = $rowData['_custom_option_title'];
  672. }
  673. }
  674. if ($this->getBehavior() != Mage_ImportExport_Model_Import::BEHAVIOR_APPEND) { // remove old data?
  675. $this->_connection->delete(
  676. $optionTable,
  677. $this->_connection->quoteInto('product_id IN (?)', array_keys($customOptions['product_id']))
  678. );
  679. }
  680. // if complex options does not contain values - ignore them
  681. foreach ($customOptions[$optionTable] as $key => $optionData) {
  682. if ($typeSpecific[$optionData['type']] === true
  683. && !isset($customOptions[$typeValueTable][$optionData['option_id']])) {
  684. unset($customOptions[$optionTable][$key], $customOptions[$titleTable][$optionData['option_id']]);
  685. }
  686. }
  687. if ($customOptions[$optionTable]) {
  688. $this->_connection->insertMultiple($optionTable, $customOptions[$optionTable]);
  689. } else {
  690. continue; // nothing to save
  691. }
  692. $titleRows = array();
  693. foreach ($customOptions[$titleTable] as $optionId => $storeInfo) {
  694. foreach ($storeInfo as $storeId => $title) {
  695. $titleRows[] = array('option_id' => $optionId, 'store_id' => $storeId, 'title' => $title);
  696. }
  697. }
  698. if ($titleRows) {
  699. $this->_connection->insertOnDuplicate($titleTable, $titleRows, array('title'));
  700. }
  701. if ($customOptions[$priceTable]) {
  702. $this->_connection->insertOnDuplicate(
  703. $priceTable,
  704. $customOptions[$priceTable],
  705. array('price', 'price_type')
  706. );
  707. }
  708. $typeValueRows = array();
  709. foreach ($customOptions[$typeValueTable] as $optionId => $optionInfo) {
  710. foreach ($optionInfo as $row) {
  711. $row['option_id'] = $optionId;
  712. $typeValueRows[] = $row;
  713. }
  714. }
  715. if ($typeValueRows) {
  716. $this->_connection->insertMultiple($typeValueTable, $typeValueRows);
  717. }
  718. $optionTypePriceRows = array();
  719. $optionTypeTitleRows = array();
  720. foreach ($customOptions[$typePriceTable] as $optionTypeId => $storesData) {
  721. foreach ($storesData as $storeId => $row) {
  722. $row['option_type_id'] = $optionTypeId;
  723. $row['store_id'] = $storeId;
  724. $optionTypePriceRows[] = $row;
  725. }
  726. }
  727. foreach ($customOptions[$typeTitleTable] as $optionTypeId => $storesData) {
  728. foreach ($storesData as $storeId => $title) {
  729. $optionTypeTitleRows[] = array(
  730. 'option_type_id' => $optionTypeId,
  731. 'store_id' => $storeId,
  732. 'title' => $title
  733. );
  734. }
  735. }
  736. if ($optionTypePriceRows) {
  737. $this->_connection->insertOnDuplicate(
  738. $typePriceTable,
  739. $optionTypePriceRows,
  740. array('price', 'price_type')
  741. );
  742. }
  743. if ($optionTypeTitleRows) {
  744. $this->_connection->insertOnDuplicate($typeTitleTable, $optionTypeTitleRows, array('title'));
  745. }
  746. if ($customOptions['product_id']) { // update product entity table to show that product has options
  747. $this->_connection->insertOnDuplicate(
  748. $productTable,
  749. $customOptions['product_id'],
  750. array('has_options', 'required_options', 'updated_at')
  751. );
  752. }
  753. }
  754. return $this;
  755. }
  756. /**
  757. * Gather and save information about product links.
  758. * Must be called after ALL products saving done.
  759. *
  760. * @return Mage_ImportExport_Model_Import_Entity_Product
  761. */
  762. protected function _saveLinks()
  763. {
  764. $resource = Mage::getResourceModel('catalog/product_link');
  765. $mainTable = $resource->getMainTable();
  766. $positionAttrId = array();
  767. $nextLinkId = $this->getNextAutoincrement($mainTable);
  768. // pre-load 'position' attributes ID for each link type once
  769. foreach ($this->_linkNameToId as $linkName => $linkId) {
  770. $select = $this->_connection->select()
  771. ->from(
  772. $resource->getTable('catalog/product_link_attribute'),
  773. array('id' => 'product_link_attribute_id')
  774. )
  775. ->where('link_type_id = ? AND product_link_attribute_code = "position"', $linkId);
  776. $positionAttrId[$linkId] = $this->_connection->fetchOne($select);
  777. }
  778. while ($bunch = $this->_dataSourceModel->getNextBunch()) {
  779. $productIds = array();
  780. $linkRows = array();
  781. $positionRows = array();
  782. foreach ($bunch as $rowNum => $rowData) {
  783. if (!$this->isRowAllowedToImport($rowData, $rowNum)) {
  784. continue;
  785. }
  786. if (self::SCOPE_DEFAULT == $this->getRowScope($rowData)) {
  787. $sku = $rowData[self::COL_SKU];
  788. }
  789. foreach ($this->_linkNameToId as $linkName => $linkId) {
  790. if (isset($rowData[$linkName . 'sku'])) {
  791. $productId = $this->_newSku[$sku]['entity_id'];
  792. $productIds[] = $productId;
  793. $linkedSku = $rowData[$linkName . 'sku'];
  794. if ((isset($this->_newSku[$linkedSku]) || isset($this->_oldSku[$linkedSku]))
  795. && $linkedSku != $sku) {
  796. if (isset($this->_newSku[$linkedSku])) {
  797. $linkedId = $this->_newSku[$linkedSku]['entity_id'];
  798. } else {
  799. $linkedId = $this->_oldSku[$linkedSku]['entity_id'];
  800. }
  801. $linkKey = "{$productId}-{$linkedId}-{$linkId}";
  802. if (!isset($linkRows[$linkKey])) {
  803. $linkRows[$linkKey] = array(
  804. 'link_id' => $nextLinkId,
  805. 'product_id' => $productId,
  806. 'linked_product_id' => $linkedId,
  807. 'link_type_id' => $linkId
  808. );
  809. if (!empty($rowData[$linkName . 'position'])) {
  810. $positionRows[] = array(
  811. 'link_id' => $nextLinkId,
  812. 'product_link_attribute_id' => $positionAttrId[$linkId],
  813. 'value' => $rowData[$linkName . 'position']
  814. );
  815. }
  816. $nextLinkId++;
  817. }
  818. }
  819. }
  820. }
  821. }
  822. if (Mage_ImportExport_Model_Import::BEHAVIOR_APPEND != $this->getBehavior() && $productIds) {
  823. $this->_connection->delete(
  824. $mainTable,
  825. $this->_connection->quoteInto('product_id IN (?)', array_keys($productIds))
  826. );
  827. }
  828. if ($linkRows) {
  829. $this->_connection->insertOnDuplicate(
  830. $mainTable,
  831. $linkRows,
  832. array('link_id')
  833. );
  834. }
  835. if ($positionRows) { // process linked product positions
  836. $this->_connection->insertOnDuplicate(
  837. $resource->getAttributeTypeTable('int'),
  838. $positionRows,
  839. array('value')
  840. );
  841. }
  842. }
  843. return $this;
  844. }
  845. /**
  846. * Save product attributes.
  847. *
  848. * @param array $attributesData
  849. * @return Mage_ImportExport_Model_Import_Entity_Product
  850. */
  851. protected function _saveProductAttributes(array $attributesData)
  852. {
  853. foreach ($attributesData as $tableName => $skuData) {
  854. $tableData = array();
  855. foreach ($skuData as $sku => $attributes) {
  856. $productId = $this->_newSku[$sku]['entity_id'];
  857. foreach ($attributes as $attributeId => $storeValues) {
  858. foreach ($storeValues as $storeId => $storeValue) {
  859. $tableData[] = array(
  860. 'entity_id' => $productId,
  861. 'entity_type_id' => $this->_entityTypeId,
  862. 'attribute_id' => $attributeId,
  863. 'store_id' => $storeId,
  864. 'value' => $storeValue
  865. );
  866. }
  867. }
  868. }
  869. $this->_connection->insertOnDuplicate($tableName, $tableData, array('value'));
  870. }
  871. return $this;
  872. }
  873. /**
  874. * Save product categories.
  875. *
  876. * @param array $categoriesData
  877. * @return Mage_ImportExport_Model_Import_Entity_Product
  878. */
  879. protected function _saveProductCategories(array $categoriesData)
  880. {
  881. static $tableName = null;
  882. if (!$tableName) {
  883. $tableName = Mage::getModel('importexport/import_proxy_product_resource')->getProductCategoryTable();
  884. }
  885. if ($categoriesData) {
  886. $categoriesIn = array();
  887. $delProductId = array();
  888. foreach ($categoriesData as $delSku => $categories) {
  889. $productId = $this->_newSku[$delSku]['entity_id'];
  890. $delProductId[] = $productId;
  891. foreach (array_keys($categories) as $categoryId) {
  892. $categoriesIn[] = array('product_id' => $productId, 'category_id' => $categoryId, 'position' => 1);
  893. }
  894. }
  895. if (Mage_ImportExport_Model_Import::BEHAVIOR_APPEND != $this->getBehavior()) {
  896. $this->_connection->delete(
  897. $tableName,
  898. $this->_connection->quoteInto('product_id IN (?)', $delProductId)
  899. );
  900. }
  901. if ($categoriesIn) {
  902. $this->_connection->insertOnDuplicate($tableName, $categoriesIn, array('position'));
  903. }
  904. }
  905. return $this;
  906. }
  907. /**
  908. * Update and insert data in entity table.
  909. *
  910. * @param array $entityRowsIn Row for insert
  911. * @param array $entityRowsUp Row for update
  912. * @return Mage_ImportExport_Model_Import_Entity_Product
  913. */
  914. protected function _saveProductEntity(array $entityRowsIn, array $entityRowsUp)
  915. {
  916. static $entityTable = null;
  917. if (!$entityTable) {
  918. $entityTable = Mage::getModel('importexport/import_proxy_product_resource')->getEntityTable();
  919. }
  920. if ($entityRowsUp) {
  921. $this->_connection->insertOnDuplicate(
  922. $entityTable,
  923. $entityRowsUp,
  924. array('updated_at')
  925. );
  926. }
  927. if ($entityRowsIn) {
  928. $this->_connection->insertMultiple($entityTable, $entityRowsIn);
  929. $newProducts = $this->_connection->fetchPairs($this->_connection->select()
  930. ->from($entityTable, array('sku', 'entity_id'))
  931. ->where('sku IN (?)', array_keys($entityRowsIn))
  932. );
  933. foreach ($newProducts as $sku => $newId) { // fill up entity_id for new products
  934. $this->_newSku[$sku]['entity_id'] = $newId;
  935. }
  936. }
  937. return $this;
  938. }
  939. /**
  940. * Gather and save information about product entities.
  941. *
  942. * @return Mage_ImportExport_Model_Import_Entity_Product
  943. */
  944. protected function _saveProducts()
  945. {
  946. /** @var $resource Mage_ImportExport_Model_Import_Proxy_Product_Resource */
  947. $resource = Mage::getModel('importexport/import_proxy_product_resource');
  948. $priceIsGlobal = Mage::helper('catalog')->isPriceGlobal();
  949. $strftimeFormat = Varien_Date::convertZendToStrftime(Varien_Date::DATETIME_INTERNAL_FORMAT, true, true);
  950. $productLimit = null;
  951. $productsQty = null;
  952. while ($bunch = $this->_dataSourceModel->getNextBunch()) {
  953. $entityRowsIn = array();
  954. $entityRowsUp = array();
  955. $attributes = array();
  956. $websites = array();
  957. $categories = array();
  958. $tierPrices = array();
  959. foreach ($bunch as $rowNum => $rowData) {
  960. if (!$this->validateRow($rowData, $rowNum)) {
  961. continue;
  962. }
  963. $rowScope = $this->getRowScope($rowData);
  964. if (self::SCOPE_DEFAULT == $rowScope) {
  965. $rowSku = $rowData[self::COL_SKU];
  966. // 1. Entity phase
  967. if (isset($this->_oldSku[$rowSku])) { // existing row
  968. $entityRowsUp[] = array(
  969. 'updated_at' => now(),
  970. 'entity_id' => $this->_oldSku[$rowSku]['entity_id']
  971. );
  972. } else { // new row
  973. if (!$productLimit || $productsQty < $productLimit) {
  974. $entityRowsIn[$rowSku] = array(
  975. 'entity_type_id' => $this->_entityTypeId,
  976. 'attribute_set_id' => $this->_newSku[$rowSku]['attr_set_id'],
  977. 'type_id' => $this->_newSku[$rowSku]['type_id'],
  978. 'sku' => $rowSku,
  979. 'created_at' => now(),
  980. 'updated_at' => now()
  981. );
  982. $productsQty++;
  983. } else {
  984. $rowSku = null; // sign for child rows to be skipped
  985. $this->_rowsToSkip[$rowNum] = true;
  986. continue;
  987. }
  988. }
  989. } elseif (null === $rowSku) {
  990. $this->_rowsToSkip[$rowNum] = true;
  991. continue; // skip rows when SKU is NULL
  992. } elseif (self::SCOPE_STORE == $rowScope) { // set necessary data from SCOPE_DEFAULT row
  993. $rowData[self::COL_TYPE] = $this->_newSku[$rowSku]['type_id'];
  994. $rowData['attribute_set_id'] = $this->_newSku[$rowSku]['attr_set_id'];
  995. $rowData[self::COL_ATTR_SET] = $this->_newSku[$rowSku]['attr_set_code'];
  996. }
  997. if (!empty($rowData['_product_websites'])) { // 2. Product-to-Website phase
  998. $websites[$rowSku][$this->_websiteCodeToId[$rowData['_product_websites']]] = true;
  999. }
  1000. if (!empty($rowData[self::COL_CATEGORY])) { // 3. Categories phase
  1001. $categories[$rowSku][$this->_categories[$rowData[self::COL_CATEGORY]]] = true;
  1002. }
  1003. if (!empty($rowData['_tier_price_website'])) { // 4. Tier prices phase
  1004. $tierPrices[$rowSku][] = array(
  1005. 'all_groups' => $rowData['_tier_price_customer_group'] == self::VALUE_ALL,
  1006. 'customer_group_id' => $rowData['_tier_price_customer_group'] == self::VALUE_ALL ?
  1007. 0 : $rowData['_tier_price_customer_group'],
  1008. 'qty' => $rowData['_tier_price_qty'],
  1009. 'value' => $rowData['_tier_price_price'],
  1010. 'website_id' => self::VALUE_ALL == $rowData['_tier_price_website'] || $priceIsGlobal ?
  1011. 0 : $this->_websiteCodeToId[$rowData['_tier_price_website']]
  1012. );
  1013. }
  1014. // 5. Attributes phase
  1015. if (self::SCOPE_NULL == $rowScope) {
  1016. continue; // skip attribute processing for SCOPE_NULL rows
  1017. }
  1018. $rowStore = self::SCOPE_STORE == $rowScope ? $this->_storeCodeToId[$rowData[self::COL_STORE]] : 0;
  1019. $rowData = $this->_productTypeModels[$rowData[self::COL_TYPE]]->prepareAttributesForSave($rowData);
  1020. $product = Mage::getModel('importexport/import_proxy_product', $rowData);
  1021. foreach ($rowData as $attrCode => $attrValue) {
  1022. $attribute = $resource->getAttribute($attrCode);
  1023. $attrId = $attribute->getId();
  1024. $backModel = $attribute->getBackendModel();
  1025. $attrTable = $attribute->getBackend()->getTable();
  1026. $storeIds = array(0);
  1027. if ('datetime' == $attribute->getBackendType()) {
  1028. $attrValue = gmstrftime($strftimeFormat, strtotime($attrValue));
  1029. } elseif ($backModel) {
  1030. $attribute->getBackend()->beforeSave($product);
  1031. $attrValue = $product->getData($attribute->getAttributeCode());
  1032. }
  1033. if (self::SCOPE_STORE == $rowScope) {
  1034. if (self::SCOPE_WEBSITE == $attribute->getIsGlobal()) {
  1035. // check website defaults already set
  1036. if (!isset($attributes[$attrTable][$rowSku][$attrId][$rowStore])) {
  1037. $storeIds = $this->_storeIdToWebsiteStoreIds[$rowStore];
  1038. }
  1039. } elseif (self::SCOPE_STORE == $attribute->getIsGlobal()) {
  1040. $storeIds = array($rowStore);
  1041. }
  1042. }
  1043. foreach ($storeIds as $storeId) {
  1044. $attributes[$attrTable][$rowSku][$attrId][$storeId] = $attrValue;
  1045. }
  1046. $attribute->setBackendModel($backModel); // restore 'backend_model' to avoid 'default' setting
  1047. }
  1048. }
  1049. $this->_saveProductEntity($entityRowsIn, $entityRowsUp)
  1050. ->_saveProductWebsites($websites)
  1051. ->_saveProductCategories($categories)
  1052. ->_saveProductTierPrices($tierPrices)
  1053. ->_saveProductAttributes($attributes);
  1054. }
  1055. return $this;
  1056. }
  1057. /**
  1058. * Save product tier prices.
  1059. *
  1060. * @param array $tierPriceData
  1061. * @return Mage_ImportExport_Model_Import_Entity_Product
  1062. */
  1063. protected function _saveProductTierPrices(array $tierPriceData)
  1064. {
  1065. static $tableName = null;
  1066. if (!$tableName) {
  1067. $tableName = Mage::getModel('importexport/import_proxy_product_resource')
  1068. ->getTable('catalog/product_attribute_tier_price');
  1069. }
  1070. if ($tierPriceData) {
  1071. $tierPriceIn = array();
  1072. $delProductId = array();
  1073. foreach ($tierPriceData as $delSku => $tierPriceRows) {
  1074. $productId = $this->_newSku[$delSku]['entity_id'];
  1075. $delProductId[] = $productId;
  1076. foreach ($tierPriceRows as $row) {
  1077. $row['entity_id'] = $productId;
  1078. $tierPriceIn[] = $row;
  1079. }
  1080. }
  1081. if (Mage_ImportExport_Model_Import::BEHAVIOR_APPEND != $this->getBehavior()) {
  1082. $this->_connection->delete(
  1083. $tableName,
  1084. $this->_connection->quoteInto('entity_id IN (?)', $delProductId)
  1085. );
  1086. }
  1087. if ($tierPriceIn) {
  1088. $this->_connection->insertOnDuplicate($tableName, $tierPriceIn, array('value'));
  1089. }
  1090. }
  1091. return $this;
  1092. }
  1093. /**
  1094. * Save product websites.
  1095. *
  1096. * @param array $websiteData
  1097. * @return Mage_ImportExport_Model_Import_Entity_Product
  1098. */
  1099. protected function _saveProductWebsites(array $websiteData)
  1100. {
  1101. static $tableName = null;
  1102. if (!$tableName) {
  1103. $tableName = Mage::getModel('importexport/import_proxy_product_resource')->getProductWebsiteTable();
  1104. }
  1105. if ($websiteData) {
  1106. $websitesData = array();
  1107. $delProductId = array();
  1108. foreach ($websiteData as $delSku => $websites) {
  1109. $productId = $this->_newSku[$delSku]['entity_id'];
  1110. $delProductId[] = $productId;
  1111. foreach (array_keys($websites) as $websiteId) {
  1112. $websitesData[] = array(
  1113. 'product_id' => $productId,
  1114. 'website_id' => $websiteId
  1115. );
  1116. }
  1117. }
  1118. if (Mage_ImportExport_Model_Import::BEHAVIOR_APPEND != $this->getBehavior()) {
  1119. $this->_connection->delete(
  1120. $tableName,
  1121. $this->_connection->quoteInto('product_id IN (?)', $delProductId)
  1122. );
  1123. }
  1124. if ($websitesData) {
  1125. $this->_connection->insertOnDuplicate($tableName, $websitesData);
  1126. }
  1127. }
  1128. return $this;
  1129. }
  1130. /**
  1131. * Stock item saving.
  1132. *
  1133. * @return Mage_ImportExport_Model_Import_Entity_Product
  1134. */
  1135. protected function _saveStockItem()
  1136. {
  1137. $defaultStockData = array(
  1138. 'manage_stock' => 1,
  1139. 'use_config_manage_stock' => 1,
  1140. 'qty' => 0,
  1141. 'min_qty' => 0,
  1142. 'use_config_min_qty' => 1,
  1143. 'min_sale_qty' => 1,
  1144. 'use_config_min_sale_qty' => 1,
  1145. 'max_sale_qty' => 10000,
  1146. 'use_config_max_sale_qty' => 1,
  1147. 'is_qty_decimal' => 0,
  1148. 'backorders' => 0,
  1149. 'use_config_backorders' => 1,
  1150. 'notify_stock_qty' => 1,
  1151. 'use_config_notify_stock_qty' => 1,
  1152. 'enable_qty_increments' => 0,
  1153. 'use_config_enable_qty_increments' => 1,
  1154. 'qty_increments' => 0,
  1155. 'use_config_qty_increments' => 1,
  1156. 'is_in_stock' => 0,
  1157. 'low_stock_date' => null,
  1158. 'stock_status_changed_automatically' => 0
  1159. );
  1160. $entityTable = Mage::getResourceModel('cataloginventory/stock_item')->getMainTable();
  1161. $helper = Mage::helper('catalogInventory');
  1162. while ($bunch = $this->_dataSourceModel->getNextBunch()) {
  1163. $stockData = array();
  1164. foreach ($bunch as $rowNum => $rowData) {
  1165. if (!$this->isRowAllowedToImport($rowData, $rowNum)) {
  1166. continue;
  1167. }
  1168. // only SCOPE_DEFAULT can contain stock data
  1169. if (self::SCOPE_DEFAULT == $this->getRowScope($rowData)) {
  1170. $row = array_merge(
  1171. $defaultStockData,
  1172. array_intersect_key($rowData, $defaultStockData)
  1173. );
  1174. $row['product_id'] = $this->_newSku[$rowData[self::COL_SKU]]['entity_id'];
  1175. $row['stock_id'] = 1;
  1176. /** @var $stockItem Mage_CatalogInventory_Model_Stock_Item */
  1177. $stockItem = Mage::getModel('cataloginventory/stock_item', $row);
  1178. if ($helper->isQty($this->_newSku[$rowData[self::COL_SKU]]['type_id'])) {
  1179. if ($stockItem->verifyNotification()) {
  1180. $stockItem->setLowStockDate(Mage::app()->getLocale()
  1181. ->date(null, null, null, false)
  1182. ->toString(Varien_Date::DATETIME_INTERNAL_FORMAT)
  1183. );
  1184. }
  1185. $stockItem->setStockStatusChangedAutomatically((int) !$stockItem->verifyStock());
  1186. } else {
  1187. $stockItem->setQty(0);
  1188. }
  1189. $stockData[] = $stockItem->getData();
  1190. }
  1191. }
  1192. if ($stockData) {
  1193. $this->_connection->insertOnDuplicate($entityTable, $stockData);
  1194. }
  1195. }
  1196. return $this;
  1197. }
  1198. /**
  1199. * Atttribute set ID-to-name pairs getter.
  1200. *
  1201. * @return array
  1202. */
  1203. public function getAttrSetIdToName()
  1204. {
  1205. return $this->_attrSetIdToName;
  1206. }
  1207. /**
  1208. * DB connection getter.
  1209. *
  1210. * @return Varien_Db_Adapter_Pdo_Mysql
  1211. */
  1212. public function getConnection()
  1213. {
  1214. return $this->_connection;
  1215. }
  1216. /**
  1217. * EAV entity type code getter.
  1218. *
  1219. * @abstract
  1220. * @return string
  1221. */
  1222. public function getEntityTypeCode()
  1223. {
  1224. return 'catalog_product';
  1225. }
  1226. /**
  1227. * New products SKU data.
  1228. *
  1229. * @return array
  1230. */
  1231. public function getNewSku()
  1232. {
  1233. return $this->_newSku;
  1234. }
  1235. /**
  1236. * Get next bunch of validatetd rows.
  1237. *
  1238. * @return array|null
  1239. */
  1240. public function getNextBunch()
  1241. {
  1242. return $this->_dataSourceModel->getNextBunch();
  1243. }
  1244. /**
  1245. * Existing products SKU getter.
  1246. *
  1247. * @return array
  1248. */
  1249. public function getOldSku()
  1250. {
  1251. return $this->_oldSku;
  1252. }
  1253. /**
  1254. * Obtain scope of the row from row data.
  1255. *
  1256. * @param array $rowData
  1257. * @return int
  1258. */
  1259. public function getRowScope(array $rowData)
  1260. {
  1261. if (strlen(trim($rowData[self::COL_SKU]))) {
  1262. return self::SCOPE_DEFAULT;
  1263. } elseif (empty($rowData[self::COL_STORE])) {
  1264. return self::SCOPE_NULL;
  1265. } else {
  1266. return self::SCOPE_STORE;
  1267. }
  1268. }
  1269. /**
  1270. * All website codes to ID getter.
  1271. *
  1272. * @return array
  1273. */
  1274. public function getWebsiteCodes()
  1275. {
  1276. return $this->_websiteCodeToId;
  1277. }
  1278. /**
  1279. * Validate data row.
  1280. *
  1281. * @param array $rowData
  1282. * @param int $rowNum
  1283. * @return boolean
  1284. */
  1285. public function validateRow(array $rowData, $rowNum)
  1286. {
  1287. static $sku = null; // SKU is remembered through all product rows
  1288. if (isset($this->_validatedRows[$rowNum])) { // check that row is already validated
  1289. return !isset($this->_invalidRows[$rowNum]);
  1290. }
  1291. $this->_validatedRows[$rowNum] = true;
  1292. if (isset($this->_newSku[$rowData[self::COL_SKU]])) {
  1293. $this->addRowError(self::ERROR_DUPLICATE_SKU, $rowNum);
  1294. return false;
  1295. }
  1296. $rowScope = $this->getRowScope($rowData);
  1297. // BEHAVIOR_DELETE use specific validation logic
  1298. if (Mage_ImportExport_Model_Import::BEHAVIOR_DELETE == $this->getBehavior()) {
  1299. if (self::SCOPE_DEFAULT == $rowScope && !isset($this->_oldSku[$rowData[self::COL_SKU]])) {
  1300. $this->addRowError(self::ERROR_SKU_NOT_FOUND_FOR_DELETE, $rowNum);
  1301. return false;
  1302. }
  1303. return true;
  1304. }
  1305. // common validation
  1306. $this->_isProductWebsiteValid($rowData, $rowNum);
  1307. $this->_isProductCategoryValid($rowData, $rowNum);
  1308. $this->_isTierPriceValid($rowData, $rowNum);
  1309. if (self::SCOPE_DEFAULT == $rowScope) { // SKU is specified, row is SCOPE_DEFAULT, new product block begins
  1310. $this->_processedEntitiesCount ++;
  1311. $sku = $rowData[self::COL_SKU];
  1312. if (isset($this->_oldSku[$sku])) { // can we get all necessary data from existant DB product?
  1313. // check for supported type of existing product
  1314. if (isset($this->_productTypeModels[$this->_oldSku[$sku]['type_id']])) {
  1315. $this->_newSku[$sku] = array(
  1316. 'entity_id' => $this->_oldSku[$sku]['entity_id'],
  1317. 'type_id' => $this->_oldSku[$sku]['type_id'],
  1318. 'attr_set_id' => $this->_oldSku[$sku]['attr_set_id'],
  1319. 'attr_set_code' => $this->_attrSetIdToName[$this->_oldSku[$sku]['attr_set_id']]
  1320. );
  1321. } else {
  1322. $this->addRowError(self::ERROR_TYPE_UNSUPPORTED, $rowNum);
  1323. $sku = false; // child rows of legacy products with unsupported types are orphans
  1324. }
  1325. } else { // validate new product type and attribute set
  1326. if (!isset($rowData[self::COL_TYPE])
  1327. || !isset($this->_productTypeModels[$rowData[self::COL_TYPE]])
  1328. ) {
  1329. $this->addRowError(self::ERROR_INVALID_TYPE, $rowNum);
  1330. } elseif (!isset($rowData[self::COL_ATTR_SET])
  1331. || !isset($this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]])
  1332. ) {
  1333. $this->addRowError(self::ERROR_INVALID_ATTR_SET, $rowNum);
  1334. } elseif (!isset($this->_newSku[$sku])) {
  1335. $this->_newSku[$sku] = array(
  1336. 'entity_id' => null,
  1337. 'type_id' => $rowData[self::COL_TYPE],
  1338. 'attr_set_id' => $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]],
  1339. 'attr_set_code' => $rowData[self::COL_ATTR_SET]
  1340. );
  1341. }
  1342. if (isset($this->_invalidRows[$rowNum])) {
  1343. // mark SCOPE_DEFAULT row as invalid for future child rows if product not in DB already
  1344. $sku = false;
  1345. }
  1346. }
  1347. } else {
  1348. if (null === $sku) {
  1349. $this->addRowError(self::ERROR_SKU_IS_EMPTY, $rowNum);
  1350. } elseif (false === $sku) {
  1351. $this->addRowError(self::ERROR_ROW_IS_ORPHAN, $rowNum);
  1352. } elseif (self::SCOPE_STORE == $rowScope && !isset($this->_storeCodeToId[$rowData[self::COL_STORE]])) {
  1353. $this->addRowError(self::ERROR_INVALID_STORE, $rowNum);
  1354. }
  1355. }
  1356. if (!isset($this->_invalidRows[$rowNum])) {
  1357. // set attribute set code into row data for followed attribute validation in type model
  1358. $rowData[self::COL_ATTR_SET] = $this->_newSku[$sku]['attr_set_code'];
  1359. $rowAttributesValid = $this->_productTypeModels[$this->_newSku[$sku]['type_id']]->isRowValid(
  1360. $rowData, $rowNum, !isset($this->_oldSku[$sku])
  1361. );
  1362. if (!$rowAttributesValid && self::SCOPE_DEFAULT == $rowScope && !isset($this->_oldSku[$sku])) {
  1363. $sku = false; // mark SCOPE_DEFAULT row as invalid for future child rows if product not in DB already
  1364. }
  1365. }
  1366. return !isset($this->_invalidRows[$rowNum]);
  1367. }
  1368. }