* @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ namespace PrestaShop\PrestaShop\Adapter\Import\Handler; use Address; use Category; use Doctrine\DBAL\Connection; use Feature; use FeatureValue; use Image; use Manufacturer; use Module; use PrestaShop\PrestaShop\Adapter\Configuration; use PrestaShop\PrestaShop\Adapter\Database; use PrestaShop\PrestaShop\Adapter\Import\ImageCopier; use PrestaShop\PrestaShop\Adapter\Import\ImportDataFormatter; use PrestaShop\PrestaShop\Adapter\Tools; use PrestaShop\PrestaShop\Adapter\Validate; use PrestaShop\PrestaShop\Core\Cache\Clearer\CacheClearerInterface; use PrestaShop\PrestaShop\Core\Import\Configuration\ImportConfigInterface; use PrestaShop\PrestaShop\Core\Import\Configuration\ImportRuntimeConfigInterface; use PrestaShop\PrestaShop\Core\Import\Entity; use PrestaShop\PrestaShop\Core\Import\File\DataRow\DataRowInterface; use Product; use ProductDownload; use ProductSupplier; use Psr\Log\LoggerInterface; use Shop; use SpecificPrice; use StockAvailable; use StockManagerFactory; use Supplier; use Symfony\Contracts\Translation\TranslatorInterface; use Tag; use TaxManagerFactory; use TaxRulesGroup; use Warehouse; use WarehouseProductLocation; /** * Class ProductImportHandler is responsible for product import. */ final class ProductImportHandler extends AbstractImportHandler { /** * @var Connection database connection */ private $connection; /** * @var string product database table name */ private $productTable; /** * @var string accessory database table name */ private $accessoryTable; /** * @var Address */ private $shopAddress; /** * @var Tools */ private $tools; /** * @var ImageCopier */ private $imageCopier; /** * @param ImportDataFormatter $dataFormatter * @param array $allShopIds * @param array $contextShopIds * @param int $currentContextShopId * @param bool $isMultistoreEnabled * @param int $contextLanguageId * @param TranslatorInterface $translator * @param LoggerInterface $logger * @param int $employeeId * @param Database $legacyDatabase * @param CacheClearerInterface $cacheClearer * @param Connection $connection * @param string $dbPrefix * @param Configuration $configuration * @param Address $shopAddress * @param Validate $validate * @param Tools $tools * @param ImageCopier $imageCopier */ public function __construct( ImportDataFormatter $dataFormatter, array $allShopIds, array $contextShopIds, $currentContextShopId, $isMultistoreEnabled, $contextLanguageId, TranslatorInterface $translator, LoggerInterface $logger, $employeeId, Database $legacyDatabase, CacheClearerInterface $cacheClearer, Connection $connection, $dbPrefix, Configuration $configuration, Address $shopAddress, Validate $validate, Tools $tools, ImageCopier $imageCopier ) { parent::__construct( $dataFormatter, $allShopIds, $contextShopIds, $currentContextShopId, $isMultistoreEnabled, $contextLanguageId, $translator, $logger, $employeeId, $legacyDatabase, $cacheClearer, $configuration, $validate ); $this->connection = $connection; $this->productTable = $dbPrefix . 'product'; $this->accessoryTable = $dbPrefix . 'accessory'; $this->defaultValues = [ 'id_category' => [$this->configuration->getInt('PS_HOME_CATEGORY')], 'id_category_default' => null, 'active' => '1', 'width' => 0.000000, 'height' => 0.000000, 'depth' => 0.000000, 'weight' => 0.000000, 'visibility' => 'both', 'additional_shipping_cost' => 0.00, 'unit_price' => 0, 'quantity' => 0, 'minimal_quantity' => 1, 'low_stock_threshold' => null, 'low_stock_alert' => false, 'price' => 0, 'id_tax_rules_group' => 0, 'description_short' => [$this->defaultLanguageId => ''], 'link_rewrite' => [$this->defaultLanguageId => ''], 'online_only' => 0, 'condition' => 'new', 'available_date' => date('Y-m-d'), 'date_add' => date('Y-m-d H:i:s'), 'date_upd' => date('Y-m-d H:i:s'), 'customizable' => 0, 'uploadable_files' => 0, 'text_fields' => 0, 'advanced_stock_management' => 0, 'depends_on_stock' => 0, 'is_virtual' => 0, ]; $this->shopAddress = $shopAddress; $this->tools = $tools; $this->imageCopier = $imageCopier; $this->importTypeLabel = $this->translator->trans('Products', [], 'Admin.Global'); } /** * {@inheritdoc} */ public function setUp(ImportConfigInterface $importConfig, ImportRuntimeConfigInterface $runtimeConfig) { parent::setUp($importConfig, $runtimeConfig); if (!defined('PS_MASS_PRODUCT_CREATION')) { define('PS_MASS_PRODUCT_CREATION', true); } if (!$runtimeConfig->shouldValidateData()) { Module::setBatchMode(true); } } /** * {@inheritdoc} */ public function importRow( ImportConfigInterface $importConfig, ImportRuntimeConfigInterface $runtimeConfig, DataRowInterface $dataRow ) { parent::importRow($importConfig, $runtimeConfig, $dataRow); $entityFields = $runtimeConfig->getEntityFields(); $productId = $this->fetchProductId( $dataRow, $runtimeConfig->getEntityFields(), $importConfig->matchReferences() ); $productName = $this->fetchDataValueByKey($dataRow, $entityFields, 'name'); $product = new Product($productId); $this->loadStock($product); $this->setDefaultValues($product); $this->fillEntityData($product, $entityFields, $dataRow, $this->languageId); $this->loadShops($product, $importConfig, $productName); $this->loadTaxes($product); $this->loadManufacturer($product, false); $this->loadSupplier($product, false); $this->loadPrice($product); $this->loadCategory($product, false); $this->loadMetaData($product, $importConfig); $this->fixFloatValues($product); $productExistsById = $this->entityExists($product, 'product'); $productExistsByReference = $importConfig->matchReferences() && $product->reference && $product->existsRefInDatabase($product->reference) ; if ($productExistsByReference || $productExistsById) { $product->date_upd = date('Y-m-d H:i:s'); } $unfriendlyError = $this->configuration->getBoolean('UNFRIENDLY_ERROR'); $fieldsError = $product->validateFields($unfriendlyError, true); $langFieldsError = $product->validateFieldsLang($unfriendlyError, true); $isValid = true === $fieldsError && true === $langFieldsError; if ($isValid) { $productSaved = $this->loadProductData( $product, $importConfig, $productExistsById, $productExistsByReference, $runtimeConfig->shouldValidateData(), $dataRow, $entityFields ); if (!$productSaved) { $productId = $this->fetchDataValueByKey($dataRow, $entityFields, 'id'); $this->error(sprintf( $this->translator->trans('%1$s (ID: %2$s) cannot be saved', [], 'Admin.Advparameters.Notification'), !empty($productName) ? $this->tools->sanitize($productName) : 'No Name', !empty($productId) ? $this->tools->sanitize($productId) : 'No ID' )); $this->error($fieldsError . $langFieldsError . $this->legacyDatabase->getErrorMessage()); } else { if (!$runtimeConfig->shouldValidateData()) { $this->saveProductSupplier($product); $this->saveProductTags($product, $importConfig, $productName); $this->saveProductImages($product, $importConfig); $this->saveFeatures($product, $importConfig); } $this->saveSpecificPrice( $product, $this->fetchDataValueByKey($dataRow, $entityFields, 'reduction_price'), $this->fetchDataValueByKey($dataRow, $entityFields, 'reduction_percent'), $this->fetchDataValueByKey($dataRow, $entityFields, 'reduction_from'), $this->fetchDataValueByKey($dataRow, $entityFields, 'reduction_to'), $runtimeConfig->shouldValidateData(), $productName ); $this->updateAdditionalData($product, $runtimeConfig->shouldValidateData()); $this->saveStock( $product, $runtimeConfig->shouldValidateData(), $productExistsById || $productExistsByReference ); $this->linkAccessories($product, $runtimeConfig); } } } /** * {@inheritdoc} */ public function tearDown(ImportConfigInterface $importConfig, ImportRuntimeConfigInterface $runtimeConfig) { parent::tearDown($importConfig, $runtimeConfig); if ($runtimeConfig->isFinished() && !$runtimeConfig->shouldValidateData()) { $this->importAccessories($runtimeConfig); } if (!$runtimeConfig->shouldValidateData()) { Module::processDeferedFuncCall(); Module::processDeferedClearCache(); Tag::updateTagCount(); } } /** * Legacy logic to create category. * This method is internally called by legacy Category::searchByPath(), so it has to be public. * * @param int $defaultLanguageId * @param string $categoryName * @param int|null $parentCategoryId */ public function createCategory($defaultLanguageId, $categoryName, $parentCategoryId = null) { $unfriendlyError = $this->configuration->getBoolean('UNFRIENDLY_ERROR'); $homeCategoryId = $this->configuration->getInt('PS_HOME_CATEGORY'); $category = new Category(); $category->id_shop_default = $this->isMultistoreEnabled ? (int) $this->currentContextShopId : 1; $category->name = $this->dataFormatter->createMultiLangField(trim($categoryName)); $category->active = true; $category->id_parent = (int) ($parentCategoryId ? $parentCategoryId : $homeCategoryId); $category->link_rewrite = $this->dataFormatter->createMultiLangField( $this->dataFormatter->createFriendlyUrl($category->name[$defaultLanguageId]) ); $fieldsError = $category->validateFields($unfriendlyError, true); $langFieldsError = $category->validateFieldsLang($unfriendlyError, true); $isValid = true === $fieldsError && true === $langFieldsError; if (!$isValid || !$category->add()) { $this->error(sprintf( $this->translator->trans( '%1$s (ID: %2$s) cannot be saved', [], 'Admin.Advparameters.Notification' ), $category->name[$defaultLanguageId], !empty($category->id) ? $category->id : 'null' )); if (!$isValid) { $error = true !== $fieldsError ? $fieldsError : ''; $error .= true !== $langFieldsError ? $langFieldsError : ''; $this->error($error . $this->legacyDatabase->getErrorMessage()); } } } /** * Fetch the product ID. * * @param DataRowInterface $dataRow * @param array $entityFields * @param bool $fetchByReference if true, will fallback to finding the product ID by reference * * @return int|null */ private function fetchProductId( DataRowInterface $dataRow, array $entityFields, $fetchByReference ) { $productId = $this->fetchDataValueByKey($dataRow, $entityFields, 'id'); if (!empty($productId)) { return (int) $productId; } if ($fetchByReference) { $productReference = $this->fetchDataValueByKey($dataRow, $entityFields, 'reference'); if ($productReference) { $statement = $this->connection->query( 'SELECT p.`id_product` FROM `' . $this->productTable . '` p ' . Shop::addSqlAssociation('product', 'p') . ' WHERE p.`reference` = "' . pSQL($productReference) . '"' ); $row = $statement->fetch(); return isset($row['id_product']) ? $row['id_product'] : null; } } return null; } /** * Load stock data for the product. * * @param Product $product */ private function loadStock(Product $product) { if (!Validate::isLoadedObject($product)) { return; } $product->loadStockData(); $category_data = Product::getProductCategories((int) $product->id); if (is_array($category_data)) { foreach ($category_data as $tmp) { if ($product->category && !is_array($product->category)) { continue; } $product->category[] = $tmp; } } } /** * Load shops data into the product object. * * @param Product $product * @param ImportConfigInterface $importConfig * @param string $productName used for error messages */ private function loadShops(Product $product, ImportConfigInterface $importConfig, $productName) { $defaultShopId = $this->configuration->getInt('PS_SHOP_DEFAULT'); if (!$this->isMultistoreEnabled) { $product->shop = $defaultShopId; $product->id_shop_default = $defaultShopId; } elseif (!isset($product->shop) || empty($product->shop)) { $product->shop = implode($importConfig->getMultipleValueSeparator(), $this->contextShopIds); $product->id_shop_default = $this->currentContextShopId; } // link product to shops $product->id_shop_list = []; $multipleValueSeparator = $importConfig->getMultipleValueSeparator(); if (empty($multipleValueSeparator)) { return; } $productShops = explode($multipleValueSeparator, $product->shop); if (is_array($productShops)) { foreach ($productShops as $shop) { if (!empty($shop)) { $shop = is_numeric($shop) ? $shop : Shop::getIdByName($shop); if (!in_array($shop, $this->allShopIds)) { $this->addEntityWarning( $this->translator->trans('Shop is not valid', [], 'Admin.Advparameters.Notification'), $productName, $product->id ); } else { $product->id_shop_list[] = $shop; } } } } } /** * Load taxes data into the product object. * * @param Product $product */ private function loadTaxes(Product $product) { if ($product->id_tax_rules_group) { if (Validate::isLoadedObject(new TaxRulesGroup($product->id_tax_rules_group))) { $taxManager = TaxManagerFactory::getManager($this->shopAddress, $product->id_tax_rules_group); $taxCalculator = $taxManager->getTaxCalculator(); $product->tax_rate = $taxCalculator->getTotalRate(); } else { $this->addEntityWarning( $this->translator->trans( 'Unknown tax rule group ID. You need to create a group with this ID first.', [], 'Admin.Advparameters.Notification' ), 'id_tax_rules_group', $product->id_tax_rules_group ); } } if (!$this->configuration->getBoolean('PS_USE_ECOTAX')) { $product->ecotax = 0; } } /** * Load manufacturer data into the product object. * * @param Product $product * @param bool $validateOnly if true, will not create new manufacturer if not exists */ private function loadManufacturer(Product $product, $validateOnly) { if (!isset($product->manufacturer)) { return; } if (is_numeric($product->manufacturer) && Manufacturer::manufacturerExists($product->manufacturer)) { $product->id_manufacturer = (int) $product->manufacturer; } elseif (is_string($product->manufacturer) && !empty($product->manufacturer)) { if ($manufacturer = Manufacturer::getIdByName($product->manufacturer)) { $product->id_manufacturer = (int) $manufacturer; } else { $unfriendlyError = $this->configuration->getBoolean('UNFRIENDLY_ERROR'); $manufacturer = new Manufacturer(); $manufacturer->name = $product->manufacturer; $manufacturer->active = true; $fieldsError = $manufacturer->validateFields($unfriendlyError, true); $langFieldsError = $manufacturer->validateFieldsLang($unfriendlyError, true); $isValid = true === $fieldsError && true === $langFieldsError; // Creating the manufacturer if it's not validation step if ($isValid && !$validateOnly && $manufacturer->add()) { $product->id_manufacturer = (int) $manufacturer->id; $manufacturer->associateTo($product->id_shop_list); } else { if (!$validateOnly) { $this->error(sprintf( $this->translator->trans( '%1$s (ID: %2$s) cannot be saved', [], 'Admin.Advparameters.Notification' ), $manufacturer->name, !empty($manufacturer->id) ? $manufacturer->id : 'null' )); } if (!$isValid) { $error = true !== $fieldsError ? $fieldsError : ''; $error .= true !== $langFieldsError ? $langFieldsError : ''; $this->error($error . $this->legacyDatabase->getErrorMessage()); } } } } } /** * Load supplier data into the product object. * * @param Product $product * @param bool $validateOnly if true, will not create new supplier if not exists */ private function loadSupplier(Product $product, $validateOnly) { if (!isset($product->supplier)) { return; } if (is_numeric($product->supplier) && Supplier::supplierExists($product->supplier)) { $product->id_supplier = (int) $product->supplier; } elseif (is_string($product->supplier) && !empty($product->supplier)) { if ($supplier = Supplier::getIdByName($product->supplier)) { $product->id_supplier = (int) $supplier; } else { $unfriendlyError = $this->configuration->getBoolean('UNFRIENDLY_ERROR'); $supplier = new Supplier(); $supplier->name = $product->supplier; $supplier->active = true; $fieldsError = $supplier->validateFields($unfriendlyError, true); $langFieldsError = $supplier->validateFieldsLang($unfriendlyError, true); $isValid = true === $fieldsError && true === $langFieldsError; // Creating the supplier if it's not validation step if ($isValid && !$validateOnly && $supplier->add()) { $product->id_supplier = (int) $supplier->id; $supplier->associateTo($product->id_shop_list); } else { if (!$validateOnly) { $this->error(sprintf( $this->translator->trans( '%1$s (ID: %2$s) cannot be saved', [], 'Admin.Advparameters.Notification' ), $supplier->name, !empty($supplier->id) ? $supplier->id : 'null' )); } if (!$isValid) { $error = true !== $fieldsError ? $fieldsError : ''; $error .= true !== $langFieldsError ? $langFieldsError : ''; $this->error($error . $this->legacyDatabase->getErrorMessage()); } } } } } /** * Load prices into product object. * * @param Product $product */ private function loadPrice(Product $product) { if (isset($product->price_tex) && !isset($product->price_tin)) { $product->price = $product->price_tex; } elseif (isset($product->price_tin) && !isset($product->price_tex)) { $product->price = $product->price_tin; // If a tax is already included in price, withdraw it from price if ($product->tax_rate) { $product->price = (float) number_format($product->price / (1 + $product->tax_rate / 100), 6, '.', ''); } } elseif (isset($product->price_tin, $product->price_tex)) { $product->price = $product->price_tex; } } /** * Load category data into product object. * * @param Product $product * @param bool $validateOnly */ private function loadCategory(Product $product, $validateOnly) { if (is_array($product->category) && count($product->category)) { $unfriendlyError = $this->configuration->getBoolean('UNFRIENDLY_ERROR'); $defaultLanguageId = $this->configuration->getInt('PS_LANG_DEFAULT'); $homeCategoryId = $this->configuration->getInt('PS_HOME_CATEGORY'); $product->id_category = []; // Reset default values array foreach ($product->category as $value) { if (is_numeric($value)) { if (Category::categoryExists((int) $value)) { $product->id_category[] = (int) $value; } else { $category = new Category(); $category->id = (int) $value; $category->name = $this->dataFormatter->createMultiLangField($value); $category->active = true; $category->id_parent = $homeCategoryId; $category->link_rewrite = $this->dataFormatter->createMultiLangField( $this->dataFormatter->createFriendlyUrl($category->name[$defaultLanguageId]) ); $fieldsError = $category->validateFields($unfriendlyError, true); $langFieldsError = $category->validateFieldsLang($unfriendlyError, true); $isValid = true === $fieldsError && true === $langFieldsError; if ($isValid && !$validateOnly && $category->add()) { $product->id_category[] = (int) $category->id; } else { if (!$validateOnly) { $this->error(sprintf( $this->translator->trans( '%1$s (ID: %2$s) cannot be saved', [], 'Admin.Advparameters.Notification' ), $category->name[$defaultLanguageId], !empty($category->id) ? $category->id : 'null' )); } if (!$isValid) { $error = true !== $fieldsError ? $fieldsError : ''; $error .= true !== $langFieldsError ? $langFieldsError : ''; $this->error($error . $this->legacyDatabase->getErrorMessage()); } } } } elseif (!$validateOnly && is_string($value) && !empty($value)) { $category = Category::searchByPath( $defaultLanguageId, trim($value), $this, 'createCategory' ); if ($category['id_category']) { $product->id_category[] = (int) $category['id_category']; } else { $this->error( $this->translator->trans( '%data% cannot be saved', [ '%data%' => trim($value), ], 'Admin.Advparameters.Notification' ) ); } } } $product->id_category = array_values(array_unique($product->id_category)); } // Category default now takes the value of the first new category during import if (isset($product->id_category[0])) { $product->id_category_default = (int) $product->id_category[0]; } elseif (!empty($product->id_category_default)) { $defaultProductShop = new Shop($product->id_shop_default); $product->id_category_default = Category::getRootCategory( null, Validate::isLoadedObject($defaultProductShop) ? $defaultProductShop : null )->id; } } /** * Load meta data into the product object. * * @param Product $product * @param ImportConfigInterface $importConfig */ private function loadMetaData(Product $product, ImportConfigInterface $importConfig) { $linkRewrite = ''; $linkRewriteExists = is_array($product->link_rewrite) && isset($product->link_rewrite[$this->languageId]); if ($linkRewriteExists) { $linkRewrite = trim($product->link_rewrite[$this->languageId]); } $validLink = $this->validate->isLinkRewrite($linkRewrite); if (($linkRewriteExists && empty($product->link_rewrite[$this->languageId])) || !$validLink) { $linkRewrite = $this->dataFormatter->createFriendlyUrl($product->name[$this->languageId]); if ($linkRewrite == '') { $linkRewrite = 'friendly-url-autogeneration-failed'; } } if (!$validLink) { $this->notice($this->translator->trans( 'Rewrite link for %1$s (ID %2$s): re-written as %3$s.', [ '%1$s' => $product->name[$this->languageId], '%2$s' => 'null', '%3$s' => $linkRewrite, ], 'Admin.Advparameters.Notification' )); } if (!$validLink || !(is_array($product->link_rewrite) && count($product->link_rewrite))) { $product->link_rewrite = $this->dataFormatter->createMultiLangField($linkRewrite); } else { $product->link_rewrite[(int) $this->languageId] = $linkRewrite; } $multipleValueSeparator = $importConfig->getMultipleValueSeparator(); // replace the value of separator by coma if ($multipleValueSeparator != ',') { if (is_array($product->meta_keywords)) { foreach ($product->meta_keywords as &$metaKeyword) { if (!empty($metaKeyword)) { $metaKeyword = str_replace($multipleValueSeparator, ',', $metaKeyword); } } } } } /** * Fix float values. * * @param Product $product */ private function fixFloatValues(Product $product) { // Convert comma into dot for all floating values foreach (Product::$definition['fields'] as $key => $array) { if ($array['type'] == Product::TYPE_FLOAT) { $product->{$key} = str_replace(',', '.', $product->{$key}); } } } /** * Load other product data. * * @param Product $product * @param ImportConfigInterface $importConfig * @param bool $productExistsById * @param bool $productExistsByReference * @param bool $validateOnly * @param DataRowInterface $dataRow * @param array $entityFields * * @return bool */ private function loadProductData( Product $product, ImportConfigInterface $importConfig, $productExistsById, $productExistsByReference, $validateOnly, DataRowInterface $dataRow, array $entityFields ) { if (!$product->quantity) { $product->quantity = 0; } $product->force_id = (bool) $importConfig->forceIds(); $result = true; if ($productExistsById || $productExistsByReference) { $sqlPart = 'SELECT product_shop.`date_add`, p.`id_product` FROM `' . _DB_PREFIX_ . 'product` p ' . Shop::addSqlAssociation('product', 'p') . ' WHERE '; if ($productExistsByReference) { $sqlPart .= 'p.`reference` = "' . pSQL($product->reference) . '"'; } else { $sqlPart .= 'p.`id_product` = ' . (int) $product->id; } $statement = $this->connection->query($sqlPart); $row = $statement->fetch(); if ($productExistsByReference) { $product->id = (int) $row['id_product']; } $product->date_add = $row['date_add']; if (!$validateOnly) { $result = $product->update(); } } else { $result = $product->add($product->date_add == ''); } if (!$validateOnly) { if ($product->getType() == Product::PTYPE_VIRTUAL) { StockAvailable::setProductOutOfStock((int) $product->id, 1); } else { StockAvailable::setProductOutOfStock((int) $product->id, (int) $product->out_of_stock); } if ($productDownload_id = ProductDownload::getIdFromIdProduct((int) $product->id)) { $productDownload = new ProductDownload($productDownload_id); $productDownload->delete(true); } if ($product->getType() == Product::PTYPE_VIRTUAL) { $downloadDir = $this->configuration->get('_PS_DOWNLOAD_DIR_'); $productDownload = new ProductDownload(); $productDownload->filename = ProductDownload::getNewFilename(); $virtualProductFileUrl = $this->fetchDataValueByKey( $dataRow, $entityFields, 'file_url' ); $this->tools->copy($virtualProductFileUrl, $downloadDir . $productDownload->filename); $productDownload->id_product = (int) $product->id; $productDownload->nb_downloadable = (int) $this->fetchDataValueByKey( $dataRow, $entityFields, 'nb_downloadable' ); $productDownload->date_expiration = $this->fetchDataValueByKey( $dataRow, $entityFields, 'date_expiration' ); $productDownload->nb_days_accessible = (int) $this->fetchDataValueByKey( $dataRow, $entityFields, 'nb_days_accessible' ); $productDownload->display_filename = basename($virtualProductFileUrl); $productDownload->add(); } } return $result; } /** * Save product supplier data. * * @param Product $product */ private function saveProductSupplier(Product $product) { if ($product->id && property_exists($product, 'supplier_reference')) { $productSupplierId = (int) ProductSupplier::getIdByProductAndSupplier( (int) $product->id, 0, (int) $product->id_supplier ); $productSupplier = new ProductSupplier($productSupplierId); $productSupplier->id_product = (int) $product->id; $productSupplier->id_product_attribute = 0; $productSupplier->id_supplier = (int) $product->id_supplier; $productSupplier->product_supplier_price_te = $product->wholesale_price; $productSupplier->product_supplier_reference = $product->supplier_reference; $productSupplier->save(); } } /** * Save specific price for a product. * * @param Product $product * @param string $reductionPrice * @param string $reductionPercent * @param string $reductionFrom * @param string $reductionTo * @param bool $validateOnly * @param string $productName */ private function saveSpecificPrice( Product $product, $reductionPrice, $reductionPercent, $reductionFrom, $reductionTo, $validateOnly, $productName ) { $reductionPercent = (float) $reductionPercent; $reductionPrice = (float) $reductionPrice; if (!$reductionPrice <= 0 && $reductionPercent <= 0) { return; } foreach ($product->id_shop_list as $shopId) { $specificPrice = SpecificPrice::getSpecificPrice($product->id, $shopId, 0, 0, 0, 1, 0, 0, 0, 0); if (is_array($specificPrice) && isset($specificPrice['id_specific_price'])) { $specificPrice = new SpecificPrice((int) $specificPrice['id_specific_price']); } else { $specificPrice = new SpecificPrice(); } $specificPrice->id_product = (int) $product->id; $specificPrice->id_specific_price_rule = 0; $specificPrice->id_shop = $shopId; $specificPrice->id_currency = 0; $specificPrice->id_country = 0; $specificPrice->id_group = 0; $specificPrice->price = -1; $specificPrice->id_customer = 0; $specificPrice->from_quantity = 1; $specificPrice->reduction = $reductionPrice ? $reductionPrice : $reductionPercent / 100; $specificPrice->reduction_type = $reductionPrice ? 'amount' : 'percentage'; $specificPrice->from = Validate::isDate($reductionFrom) ? $reductionFrom : '0000-00-00 00:00:00'; $specificPrice->to = Validate::isDate($reductionTo) ? $reductionTo : '0000-00-00 00:00:00'; if (!$validateOnly && !$specificPrice->save()) { $this->addEntityWarning( $this->translator->trans('Discount is invalid', [], 'Admin.Advparameters.Notification'), $this->tools->sanitize($productName), $product->id ); } } } /** * Save product tags data. * * @param Product $product * @param ImportConfigInterface $importConfig * @param string $productName product name, used for error messages */ private function saveProductTags(Product $product, ImportConfigInterface $importConfig, $productName) { if (empty($product->tags)) { return; } $multipleValueSeparator = $importConfig->getMultipleValueSeparator(); if (isset($product->id) && $product->id) { $tags = Tag::getProductTags($product->id); if (is_array($tags) && count($tags)) { if (is_string($product->tags) && !empty($multipleValueSeparator)) { $product->tags = explode($multipleValueSeparator, $product->tags); } if (is_array($product->tags)) { foreach ($product->tags as $key => $tag) { if (!empty($tag)) { $product->tags[$key] = trim($tag); } } $tags[$this->languageId] = $product->tags; $product->tags = $tags; } } } // Delete tags for this id product, for no duplicating error Tag::deleteTagsForProduct($product->id); if (!is_array($product->tags) && !empty($product->tags)) { $product->tags = $this->dataFormatter->createMultiLangField($product->tags); foreach ($product->tags as $key => $tags) { $isTagAdded = Tag::addTags($key, $product->id, $tags, $multipleValueSeparator); if (!$isTagAdded) { $this->addEntityWarning( $this->translator->trans('Tags list is invalid', [], 'Admin.Advparameters.Notification'), $this->tools->sanitize($productName), $product->id ); break; } } } else { foreach ($product->tags as $key => $tags) { $str = ''; foreach ($tags as $one_tag) { $str .= $one_tag . $multipleValueSeparator; } $str = rtrim($str, $multipleValueSeparator); $isTagAdded = Tag::addTags($key, $product->id, $str, $multipleValueSeparator); if (!$isTagAdded) { $this->addEntityWarning( $this->translator->trans( 'Invalid tag(s) (%s)', [ $str, ], 'Admin.Notifications.Error' ), $this->tools->sanitize($productName), (int) $product->id ); break; } } } } /** * Save product images. * * @param Product $product * @param ImportConfigInterface $importConfig */ private function saveProductImages(Product $product, ImportConfigInterface $importConfig) { //delete existing images if "delete_existing_images" is set to 1 if (isset($product->delete_existing_images)) { if ((bool) $product->delete_existing_images) { $product->deleteImages(); } } if (isset($product->image) && is_array($product->image) && count($product->image)) { $unfriendlyError = $this->configuration->getBoolean('UNFRIENDLY_ERROR'); $product_has_images = (bool) Image::getImages($this->languageId, (int) $product->id); foreach ($product->image as $key => $url) { $url = trim($url); $error = false; if (!empty($url)) { $url = str_replace(' ', '%20', $url); $image = new Image(); $image->id_product = (int) $product->id; $image->position = Image::getHighestPosition($product->id) + 1; $image->cover = (!$key && !$product_has_images) ? true : false; $alt = $product->image_alt[$key]; if (strlen($alt) > 0) { $image->legend = $this->dataFormatter->createMultiLangField($alt); } $fieldsError = $image->validateFields($unfriendlyError, true); $langFieldsError = $image->validateFieldsLang($unfriendlyError, true); $isValid = true === $fieldsError && true === $langFieldsError; if ($isValid && $image->add()) { // associate image to selected shops $image->associateTo($product->id_shop_list); $copySucceeded = $this->imageCopier->copyImg( $product->id, $image->id, $url, 'products', !$importConfig->skipThumbnailRegeneration() ); if (!$copySucceeded) { $image->delete(); $this->warning( $this->translator->trans( 'Error copying image: %url%', [ '%url%' => $url, ], 'Admin.Advparameters.Notification' ) ); } } else { $error = true; } } else { $error = true; } if ($error) { $this->warning( $this->translator->trans( 'Product #%id%: the picture (%url%) cannot be saved.', [ '%id%' => isset($image) ? $image->id_product : '', '%url%' => $url, ], 'Admin.Advparameters.Notification' ) ); } } } } /** * Update additional product data. * * @param Product $product * @param bool $validateOnly */ private function updateAdditionalData(Product $product, $validateOnly) { if (!$validateOnly && isset($product->id_category) && is_array($product->id_category)) { $product->updateCategories(array_map('intval', $product->id_category)); } $product->checkDefaultAttributes(); if (!$validateOnly && !$product->cache_default_attribute) { Product::updateDefaultAttribute($product->id); } } /** * Save product features. * * @param Product $product * @param ImportConfigInterface $importConfig */ private function saveFeatures(Product $product, ImportConfigInterface $importConfig) { // Features import $features = get_object_vars($product); $multipleValueSeparator = $importConfig->getMultipleValueSeparator(); if (empty($features['features']) || empty($multipleValueSeparator)) { return; } foreach (explode($multipleValueSeparator, $features['features']) as $singleFeature) { if (empty($singleFeature)) { continue; } $feature = explode(':', $singleFeature); $featureName = isset($feature[0]) ? trim($feature[0]) : ''; $featureValue = isset($feature[1]) ? trim($feature[1]) : ''; $position = isset($feature[2]) ? (int) $feature[2] - 1 : false; $custom = isset($feature[3]) ? (int) $feature[3] : false; if (!empty($featureName) && !empty($featureValue)) { $featureId = (int) Feature::addFeatureImport($featureName, $position); $productId = null; if ($importConfig->forceIds() || $importConfig->matchReferences()) { $productId = (int) $product->id; } $featureValueId = (int) FeatureValue::addFeatureValueImport( $featureId, $featureValue, $productId, $this->languageId, $custom ); Product::addFeatureProductImport($product->id, $featureId, $featureValueId); } } // clean feature positions to avoid conflict Feature::cleanPositions(); } /** * Save stock data for the product. * * @param Product $product * @param bool $validateOnly * @param bool $productExists */ private function saveStock(Product $product, $validateOnly, $productExists) { $asmEnabled = $this->configuration->getBoolean('PS_ADVANCED_STOCK_MANAGEMENT'); // set advanced stock managment if (!$validateOnly) { /* @phpstan-ignore-next-line Data of the property `advanced_stock_management` comes from database */ if ($product->advanced_stock_management != 1 && $product->advanced_stock_management != 0) { $this->warning( $this->translator->trans( 'Advanced stock management has incorrect value. Not set for product %name%', ['%name%' => $product->name[$this->languageId]], 'Admin.Advparameters.Notification' ) ); } elseif (!$asmEnabled && $product->advanced_stock_management == 1) { $this->warning( $this->translator->trans( 'Advanced stock management is not enabled, cannot enable on product %name%', ['%name%' => $product->name[$this->languageId]], 'Admin.Advparameters.Notification' ) ); } elseif ($productExists) { $product->setAdvancedStockManagement($product->advanced_stock_management); } // automaticly disable depends on stock, if a_s_m set to disabled if (StockAvailable::dependsOnStock($product->id) == 1 && $product->advanced_stock_management == 0) { StockAvailable::setProductDependsOnStock($product->id, false); } } // Check if warehouse exists if (isset($product->warehouse) && $product->warehouse) { if (!$asmEnabled) { $this->warning( $this->translator->trans( 'Advanced stock management is not enabled, warehouse not set on product %name%', ['%name%' => $product->name[$this->languageId]], 'Admin.Advparameters.Notification' ) ); } elseif (!$validateOnly) { if (Warehouse::exists($product->warehouse)) { // Get already associated warehouses $associatedWarehousesCollection = WarehouseProductLocation::getCollection($product->id); // Delete any entry in warehouse for this product foreach ($associatedWarehousesCollection as $awc) { $awc->delete(); } $warehouseLocationEntity = new WarehouseProductLocation(); $warehouseLocationEntity->id_product = $product->id; $warehouseLocationEntity->id_product_attribute = 0; $warehouseLocationEntity->id_warehouse = $product->warehouse; $warehouseLocationEntity->save(); StockAvailable::synchronize($product->id); } else { $this->warning( $this->translator->trans( 'Warehouse did not exist, cannot set on product %name%', ['%name%' => $product->name[$this->languageId]], 'Admin.Advparameters.Notification' ) ); } } } if ($this->isMultistoreEnabled) { $shopIds = $product->id_shop_list; } else { $shopIds = [ $this->currentContextShopId, ]; } // stock available if (isset($product->depends_on_stock)) { /* @phpstan-ignore-next-line Data of the property `depends_on_stock` comes from database */ if ($product->depends_on_stock != 0 && $product->depends_on_stock != 1) { $this->warning( $this->translator->trans( 'Incorrect value for "Depends on stock" for product %name%', ['%name%' => $product->name[$this->languageId]], 'Admin.Advparameters.Notification' ) ); /* @phpstan-ignore-next-line Data of properties `advanced_stock_management` & `depends_on_stock` comes from database */ } elseif ((!$product->advanced_stock_management || $product->advanced_stock_management == 0) && $product->depends_on_stock == 1) { $this->warning( $this->translator->trans( 'Advanced stock management is not enabled, cannot set "Depends on stock" for product %name%', ['%name%' => $product->name[$this->languageId]], 'Admin.Advparameters.Notification' ) ); } elseif (!$validateOnly) { StockAvailable::setProductDependsOnStock($product->id, $product->depends_on_stock); } // This code allows us to set qty and disable depends on stock if (!$validateOnly) { // if depends on stock and quantity, add quantity to stock if ($product->depends_on_stock == 1) { $stockManager = StockManagerFactory::getManager(); $price = str_replace(',', '.', (string) $product->wholesale_price); if ($price == 0) { $price = 0.000001; } $price = round(floatval($price), 6); $warehouse = new Warehouse($product->warehouse); $productAdded = $stockManager->addProduct( (int) $product->id, 0, $warehouse, (int) $product->quantity, 1, $price, true ); if ($productAdded) { StockAvailable::synchronize((int) $product->id); } } else { foreach ($shopIds as $shop) { StockAvailable::setQuantity((int) $product->id, 0, (int) $product->quantity, (int) $shop); } } } } elseif (!$validateOnly) { // if not depends_on_stock set, use normal qty foreach ($shopIds as $shop) { StockAvailable::setQuantity((int) $product->id, 0, (int) $product->quantity, (int) $shop); } } } /** * Link product accessories. * * @param Product $product * @param ImportRuntimeConfigInterface $runtimeConfig */ private function linkAccessories(Product $product, ImportRuntimeConfigInterface $runtimeConfig) { // Accessories linkage if ($runtimeConfig->shouldValidateData()) { return; } $hasAccessories = isset($product->accessories) && is_array($product->accessories) && count($product->accessories) ; if ($hasAccessories) { $sharedData = $runtimeConfig->getSharedData(); $accessories = isset($sharedData['accessories']) ? $sharedData['accessories'] : []; $accessories[$product->id] = $product->accessories; $runtimeConfig->addSharedDataItem('accessories', $accessories); } } /** * Import accessories. * * @param ImportRuntimeConfigInterface $runtimeConfig */ private function importAccessories(ImportRuntimeConfigInterface $runtimeConfig) { $sharedData = $runtimeConfig->getSharedData(); if (!isset($sharedData['accessories'])) { return; } foreach ($sharedData['accessories'] as $productId => $links) { if (count($links) > 0) { // We delete and relink only if there is accessories to link... // Bulk jobs: for performances, we need to do a minimum amount of SQL queries. No product inflation. $uniqueIds = Product::getExistingIdsFromIdsOrRefs($links); $this->connection->delete( $this->accessoryTable, [ 'id_product_1' => (int) $productId, ] ); Product::changeAccessoriesForProduct($uniqueIds, $productId); } } } /** * {@inheritdoc} */ public function supports($importEntityType) { return $importEntityType === Entity::TYPE_PRODUCTS; } }