1<?php 2/** 3 * Copyright since 2007 PrestaShop SA and Contributors 4 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA 5 * 6 * NOTICE OF LICENSE 7 * 8 * This source file is subject to the Open Software License (OSL 3.0) 9 * that is bundled with this package in the file LICENSE.md. 10 * It is also available through the world-wide-web at this URL: 11 * https://opensource.org/licenses/OSL-3.0 12 * If you did not receive a copy of the license and are unable to 13 * obtain it through the world-wide-web, please send an email 14 * to license@prestashop.com so we can send you a copy immediately. 15 * 16 * DISCLAIMER 17 * 18 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer 19 * versions in the future. If you wish to customize PrestaShop for your 20 * needs please refer to https://devdocs.prestashop.com/ for more information. 21 * 22 * @author PrestaShop SA and Contributors <contact@prestashop.com> 23 * @copyright Since 2007 PrestaShop SA and Contributors 24 * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) 25 */ 26 27namespace PrestaShop\PrestaShop\Adapter\Product; 28 29use AdminProductsController; 30use Attachment; 31use Category; 32use Combination; 33use Configuration; 34use Context; 35use Customer; 36use Db; 37use Hook; 38use Image; 39use Language; 40use ObjectModel; 41use PrestaShop\PrestaShop\Adapter\Entity\Customization; 42use PrestaShop\PrestaShop\Core\Foundation\Database\EntityNotFoundException; 43use PrestaShop\PrestaShop\Core\Localization\Locale; 44use PrestaShopBundle\Form\Admin\Type\CustomMoneyType; 45use PrestaShopBundle\Utils\FloatParser; 46use Product; 47use ProductDownload; 48use Shop; 49use ShopUrl; 50use SpecificPrice; 51use SpecificPriceRule; 52use StockAvailable; 53use Symfony\Component\Translation\TranslatorInterface; 54use Tax; 55use Tools; 56use Validate; 57 58/** 59 * Admin controller wrapper for new Architecture, about Product admin controller. 60 */ 61class AdminProductWrapper 62{ 63 /** 64 * @var array 65 */ 66 private $errors = []; 67 68 /** 69 * @var Locale 70 */ 71 private $locale; 72 73 /** 74 * @var TranslatorInterface 75 */ 76 private $translator; 77 78 /** 79 * @var array 80 */ 81 private $employeeAssociatedShops; 82 83 /** 84 * @var FloatParser 85 */ 86 private $floatParser; 87 88 /** 89 * Constructor : Inject Symfony\Component\Translation Translator. 90 * 91 * @param object $translator 92 * @param array $employeeAssociatedShops 93 * @param Locale $locale 94 * @param FloatParser|null $floatParser 95 */ 96 public function __construct($translator, array $employeeAssociatedShops, Locale $locale, FloatParser $floatParser = null) 97 { 98 $this->translator = $translator; 99 $this->employeeAssociatedShops = $employeeAssociatedShops; 100 $this->locale = $locale; 101 $this->floatParser = $floatParser ?? new FloatParser(); 102 } 103 104 /** 105 * getInstance 106 * Get the legacy AdminProductsControllerCore instance. 107 * 108 * @return AdminProductsController instance 109 */ 110 public function getInstance() 111 { 112 return new AdminProductsController(); 113 } 114 115 /** 116 * processProductAttribute 117 * Update a combination. 118 * 119 * @param object $product 120 * @param array $combinationValues the posted values 121 * 122 * @return void 123 */ 124 public function processProductAttribute($product, $combinationValues) 125 { 126 $id_product_attribute = (int) $combinationValues['id_product_attribute']; 127 $images = []; 128 129 if (!Combination::isFeatureActive() || $id_product_attribute == 0) { 130 return; 131 } 132 133 if (!isset($combinationValues['attribute_wholesale_price'])) { 134 $combinationValues['attribute_wholesale_price'] = 0; 135 } 136 if (!isset($combinationValues['attribute_price_impact'])) { 137 $combinationValues['attribute_price_impact'] = 0; 138 } 139 if (!isset($combinationValues['attribute_weight_impact'])) { 140 $combinationValues['attribute_weight_impact'] = 0; 141 } 142 143 // This is VERY UGLY, but since ti ComputingPrecision can never return enough decimals for now we have no 144 // choice but to hard code this one to make sure enough precision is saved in the DB or it results in errors 145 // of 1 cent in the shop 146 $computingPrecision = CustomMoneyType::PRESTASHOP_DECIMALS; 147 if (!isset($combinationValues['attribute_ecotax']) || 0.0 === (float) $combinationValues['attribute_ecotax']) { 148 $combinationValues['attribute_ecotax'] = 0; 149 } else { 150 // Value is displayed tax included but must be saved tax excluded 151 $combinationValues['attribute_ecotax'] = Tools::ps_round( 152 $combinationValues['attribute_ecotax'] / (1 + Tax::getProductEcotaxRate() / 100), 153 $computingPrecision 154 ); 155 } 156 if ((isset($combinationValues['attribute_default']) && $combinationValues['attribute_default'] == 1)) { 157 $product->deleteDefaultAttributes(); 158 } 159 if (!empty($combinationValues['id_image_attr'])) { 160 $images = $combinationValues['id_image_attr']; 161 } else { 162 $combination = new Combination($id_product_attribute); 163 $combination->setImages([]); 164 } 165 if (!isset($combinationValues['attribute_low_stock_threshold'])) { 166 $combinationValues['attribute_low_stock_threshold'] = null; 167 } 168 if (!isset($combinationValues['attribute_low_stock_alert'])) { 169 $combinationValues['attribute_low_stock_alert'] = false; 170 } 171 172 $product->updateAttribute( 173 $id_product_attribute, 174 $combinationValues['attribute_wholesale_price'], 175 $combinationValues['attribute_price'] * $combinationValues['attribute_price_impact'], 176 $combinationValues['attribute_weight'] * $combinationValues['attribute_weight_impact'], 177 $combinationValues['attribute_unity'] * $combinationValues['attribute_unit_impact'], 178 $combinationValues['attribute_ecotax'], 179 $images, 180 $combinationValues['attribute_reference'], 181 $combinationValues['attribute_ean13'], 182 (isset($combinationValues['attribute_default']) && $combinationValues['attribute_default'] == 1), 183 isset($combinationValues['attribute_location']) ? $combinationValues['attribute_location'] : null, 184 $combinationValues['attribute_upc'], 185 $combinationValues['attribute_minimal_quantity'], 186 $combinationValues['available_date_attribute'], 187 false, 188 [], 189 $combinationValues['attribute_isbn'], 190 $combinationValues['attribute_low_stock_threshold'], 191 $combinationValues['attribute_low_stock_alert'], 192 $combinationValues['attribute_mpn'] 193 ); 194 195 StockAvailable::setProductDependsOnStock((int) $product->id, $product->depends_on_stock, null, $id_product_attribute); 196 StockAvailable::setProductOutOfStock((int) $product->id, $product->out_of_stock, null, $id_product_attribute); 197 StockAvailable::setLocation((int) $product->id, $combinationValues['attribute_location'], null, $id_product_attribute); 198 199 $product->checkDefaultAttributes(); 200 201 if ((isset($combinationValues['attribute_default']) && $combinationValues['attribute_default'] == 1)) { 202 Product::updateDefaultAttribute((int) $product->id); 203 $product->cache_default_attribute = (int) $id_product_attribute; 204 205 // We need to reload the product because some other calls have modified the database 206 // It's done just for the setAvailableDate to avoid side effects 207 Product::disableCache(); 208 $consistentProduct = new Product($product->id); 209 if ($available_date = $combinationValues['available_date_attribute']) { 210 $consistentProduct->setAvailableDate($available_date); 211 } else { 212 $consistentProduct->setAvailableDate(); 213 } 214 Product::enableCache(); 215 } 216 217 if (isset($combinationValues['attribute_quantity'])) { 218 $this->processQuantityUpdate($product, $combinationValues['attribute_quantity'], $id_product_attribute); 219 } 220 } 221 222 /** 223 * Update a quantity for a product or a combination. 224 * 225 * Does not work in Advanced stock management. 226 * 227 * @param Product $product 228 * @param int $quantity 229 * @param int $forAttributeId 230 */ 231 public function processQuantityUpdate(Product $product, $quantity, $forAttributeId = 0) 232 { 233 // Hook triggered by legacy code below: actionUpdateQuantity('id_product', 'id_product_attribute', 'quantity') 234 StockAvailable::setQuantity((int) $product->id, $forAttributeId, $quantity); 235 Hook::exec('actionProductUpdate', ['id_product' => (int) $product->id, 'product' => $product]); 236 } 237 238 /** 239 * Update the out of stock strategy. 240 * 241 * @param Product $product 242 * @param int $out_of_stock 243 */ 244 public function processProductOutOfStock(Product $product, $out_of_stock) 245 { 246 StockAvailable::setProductOutOfStock((int) $product->id, (int) $out_of_stock); 247 } 248 249 /** 250 * @param Product $product 251 * @param string $location 252 */ 253 public function processLocation(Product $product, $location) 254 { 255 StockAvailable::setLocation($product->id, $location); 256 } 257 258 /** 259 * Set if a product depends on stock (ASM). For a product or a combination. 260 * 261 * Does work only in Advanced stock management. 262 * 263 * @param Product $product 264 * @param bool $dependsOnStock 265 * @param int $forAttributeId 266 */ 267 public function processDependsOnStock(Product $product, $dependsOnStock, $forAttributeId = 0) 268 { 269 StockAvailable::setProductDependsOnStock((int) $product->id, $dependsOnStock, null, $forAttributeId); 270 } 271 272 /** 273 * Add/Update a SpecificPrice object. 274 * 275 * @param int $id_product 276 * @param array $specificPriceValues the posted values 277 * @param int|null $idSpecificPrice if this is an update of an existing specific price, null else 278 * 279 * @return AdminProductsController|array 280 */ 281 public function processProductSpecificPrice($id_product, $specificPriceValues, $idSpecificPrice = null) 282 { 283 // ---- data formatting ---- 284 $id_product_attribute = $specificPriceValues['sp_id_product_attribute'] ?? 0; 285 $id_shop = $specificPriceValues['sp_id_shop'] ? $specificPriceValues['sp_id_shop'] : 0; 286 $id_currency = $specificPriceValues['sp_id_currency'] ? $specificPriceValues['sp_id_currency'] : 0; 287 $id_country = $specificPriceValues['sp_id_country'] ? $specificPriceValues['sp_id_country'] : 0; 288 $id_group = $specificPriceValues['sp_id_group'] ? $specificPriceValues['sp_id_group'] : 0; 289 $id_customer = !empty($specificPriceValues['sp_id_customer']['data']) ? $specificPriceValues['sp_id_customer']['data'][0] : 0; 290 $price = isset($specificPriceValues['leave_bprice']) ? '-1' : $this->floatParser->fromString($specificPriceValues['sp_price']); 291 $from_quantity = $specificPriceValues['sp_from_quantity']; 292 $reduction = $this->floatParser->fromString($specificPriceValues['sp_reduction']); 293 $reduction_tax = $specificPriceValues['sp_reduction_tax']; 294 $reduction_type = !$reduction ? 'amount' : $specificPriceValues['sp_reduction_type']; 295 $reduction_type = $reduction_type == '-' ? 'amount' : $reduction_type; 296 $from = $specificPriceValues['sp_from']; 297 if (!$from) { 298 $from = '0000-00-00 00:00:00'; 299 } 300 $to = $specificPriceValues['sp_to']; 301 if (!$to) { 302 $to = '0000-00-00 00:00:00'; 303 } 304 $isThisAnUpdate = (null !== $idSpecificPrice); 305 306 // ---- validation ---- 307 if (($price == '-1') && ((float) $reduction == '0')) { 308 $this->errors[] = $this->translator->trans('No reduction value has been submitted', [], 'Admin.Catalog.Notification'); 309 } elseif ($to != '0000-00-00 00:00:00' && strtotime($to) < strtotime($from)) { 310 $this->errors[] = $this->translator->trans('Invalid date range', [], 'Admin.Catalog.Notification'); 311 } elseif ($reduction_type == 'percentage' && ((float) $reduction <= 0 || (float) $reduction > 100)) { 312 $this->errors[] = $this->translator->trans('Submitted reduction value (0-100) is out-of-range', [], 'Admin.Catalog.Notification'); 313 } 314 $validationResult = $this->validateSpecificPrice( 315 $id_product, 316 $id_shop, 317 $id_currency, 318 $id_country, 319 $id_group, 320 $id_customer, 321 $price, 322 $from_quantity, 323 $reduction, 324 $reduction_type, 325 $from, 326 $to, 327 $id_product_attribute, 328 $isThisAnUpdate 329 ); 330 331 if (false === $validationResult || count($this->errors)) { 332 return $this->errors; 333 } 334 335 // ---- data modification ---- 336 if ($isThisAnUpdate) { 337 $specificPrice = new SpecificPrice($idSpecificPrice); 338 } else { 339 $specificPrice = new SpecificPrice(); 340 } 341 342 $specificPrice->id_product = (int) $id_product; 343 $specificPrice->id_product_attribute = (int) $id_product_attribute; 344 $specificPrice->id_shop = (int) $id_shop; 345 $specificPrice->id_currency = (int) ($id_currency); 346 $specificPrice->id_country = (int) ($id_country); 347 $specificPrice->id_group = (int) ($id_group); 348 $specificPrice->id_customer = (int) $id_customer; 349 $specificPrice->price = (float) ($price); 350 $specificPrice->from_quantity = (int) ($from_quantity); 351 $specificPrice->reduction = (float) ($reduction_type == 'percentage' ? $reduction / 100 : $reduction); 352 $specificPrice->reduction_tax = $reduction_tax; 353 $specificPrice->reduction_type = $reduction_type; 354 $specificPrice->from = $from; 355 $specificPrice->to = $to; 356 357 if ($isThisAnUpdate) { 358 $dataSavingResult = $specificPrice->save(); 359 } else { 360 $dataSavingResult = $specificPrice->add(); 361 } 362 363 if (false === $dataSavingResult) { 364 $this->errors[] = $this->translator->trans('An error occurred while updating the specific price.', [], 'Admin.Catalog.Notification'); 365 } 366 367 return $this->errors; 368 } 369 370 /** 371 * Validate a specific price. 372 */ 373 private function validateSpecificPrice( 374 $id_product, 375 $id_shop, 376 $id_currency, 377 $id_country, 378 $id_group, 379 $id_customer, 380 $price, 381 $from_quantity, 382 $reduction, 383 $reduction_type, 384 $from, 385 $to, 386 $id_combination = 0, 387 $isThisAnUpdate = false 388 ) { 389 if (!Validate::isUnsignedId($id_shop) || !Validate::isUnsignedId($id_currency) || !Validate::isUnsignedId($id_country) || !Validate::isUnsignedId($id_group) || !Validate::isUnsignedId($id_customer)) { 390 $this->errors[] = 'Wrong IDs'; 391 } elseif ((!isset($price) && !isset($reduction)) || (isset($price) && !Validate::isNegativePrice($price)) || (isset($reduction) && !Validate::isPrice($reduction))) { 392 $this->errors[] = 'Invalid price/discount amount'; 393 } elseif (!Validate::isUnsignedInt($from_quantity)) { 394 $this->errors[] = 'Invalid quantity'; 395 } elseif ($reduction && !Validate::isReductionType($reduction_type)) { 396 $this->errors[] = 'Please select a discount type (amount or percentage).'; 397 } elseif ($from && $to && (!Validate::isDateFormat($from) || !Validate::isDateFormat($to))) { 398 $this->errors[] = 'The from/to date is invalid.'; 399 } elseif (!$isThisAnUpdate && SpecificPrice::exists((int) $id_product, $id_combination, $id_shop, $id_group, $id_country, $id_currency, $id_customer, $from_quantity, $from, $to, false)) { 400 $this->errors[] = 'A specific price already exists for these parameters.'; 401 } else { 402 return true; 403 } 404 405 return false; 406 } 407 408 /** 409 * Get specific prices list for a product. 410 * 411 * @param object $product 412 * @param object $defaultCurrency 413 * @param array $shops Available shops 414 * @param array $currencies Available currencies 415 * @param array $countries Available countries 416 * @param array $groups Available users groups 417 * 418 * @return array 419 */ 420 public function getSpecificPricesList($product, $defaultCurrency, $shops, $currencies, $countries, $groups) 421 { 422 $content = []; 423 $specific_prices = array_merge( 424 SpecificPrice::getByProductId((int) $product->id), 425 SpecificPrice::getByProductId(0) 426 ); 427 428 $tmp = []; 429 foreach ($shops as $shop) { 430 $tmp[$shop['id_shop']] = $shop; 431 } 432 $shops = $tmp; 433 $tmp = []; 434 foreach ($currencies as $currency) { 435 $tmp[$currency['id_currency']] = $currency; 436 } 437 $currencies = $tmp; 438 439 $tmp = []; 440 foreach ($countries as $country) { 441 $tmp[$country['id_country']] = $country; 442 } 443 $countries = $tmp; 444 445 $tmp = []; 446 foreach ($groups as $group) { 447 $tmp[$group['id_group']] = $group; 448 } 449 $groups = $tmp; 450 451 if (is_array($specific_prices) && count($specific_prices)) { 452 foreach ($specific_prices as $specific_price) { 453 $id_currency = $specific_price['id_currency'] ? $specific_price['id_currency'] : $defaultCurrency->id; 454 if (!isset($currencies[$id_currency])) { 455 continue; 456 } 457 458 $current_specific_currency = $currencies[$id_currency]; 459 if ($specific_price['reduction_type'] == 'percentage') { 460 $impact = '- ' . ($specific_price['reduction'] * 100) . ' %'; 461 } elseif ($specific_price['reduction'] > 0) { 462 $impact = '- ' . $this->locale->formatPrice($specific_price['reduction'], $current_specific_currency['iso_code']) . ' '; 463 if ($specific_price['reduction_tax']) { 464 $impact .= '(' . $this->translator->trans('Tax incl.', [], 'Admin.Global') . ')'; 465 } else { 466 $impact .= '(' . $this->translator->trans('Tax excl.', [], 'Admin.Global') . ')'; 467 } 468 } else { 469 $impact = '--'; 470 } 471 472 if ($specific_price['from'] == '0000-00-00 00:00:00' && $specific_price['to'] == '0000-00-00 00:00:00') { 473 $period = $this->translator->trans('Unlimited', [], 'Admin.Global'); 474 } else { 475 $period = $this->translator->trans('From', [], 'Admin.Global') . ' ' . ($specific_price['from'] != '0000-00-00 00:00:00' ? $specific_price['from'] : '0000-00-00 00:00:00') . '<br />' . $this->translator->trans('to', [], 'Admin.Global') . ' ' . ($specific_price['to'] != '0000-00-00 00:00:00' ? $specific_price['to'] : '0000-00-00 00:00:00'); 476 } 477 if ($specific_price['id_product_attribute']) { 478 $combination = new Combination((int) $specific_price['id_product_attribute']); 479 $attributes = $combination->getAttributesName(1); 480 $attributes_name = ''; 481 foreach ($attributes as $attribute) { 482 $attributes_name .= $attribute['name'] . ' - '; 483 } 484 $attributes_name = rtrim($attributes_name, ' - '); 485 } else { 486 $attributes_name = $this->translator->trans('All combinations', [], 'Admin.Catalog.Feature'); 487 } 488 489 $rule = new SpecificPriceRule((int) $specific_price['id_specific_price_rule']); 490 $rule_name = ($rule->id ? $rule->name : '--'); 491 492 if ($specific_price['id_customer']) { 493 $customer = new Customer((int) $specific_price['id_customer']); 494 if (Validate::isLoadedObject($customer)) { 495 $customer_full_name = $customer->firstname . ' ' . $customer->lastname; 496 } 497 unset($customer); 498 } 499 500 if (!$specific_price['id_shop'] || in_array($specific_price['id_shop'], Shop::getContextListShopID())) { 501 $can_delete_specific_prices = true; 502 if (Shop::isFeatureActive()) { 503 $can_delete_specific_prices = (count($this->employeeAssociatedShops) > 1 && !$specific_price['id_shop']) || $specific_price['id_shop']; 504 } 505 506 $price = Tools::ps_round($specific_price['price'], 2); 507 $fixed_price = (($price == Tools::ps_round($product->price, 2) && $current_specific_currency['id_currency'] == $defaultCurrency->id) || $specific_price['price'] == -1) ? '--' : $this->locale->formatPrice($price, $current_specific_currency['iso_code']); 508 509 $content[] = [ 510 'id_specific_price' => $specific_price['id_specific_price'], 511 'id_product' => $product->id, 512 'rule_name' => $rule_name, 513 'attributes_name' => $attributes_name, 514 'shop' => ($specific_price['id_shop'] ? $shops[$specific_price['id_shop']]['name'] : $this->translator->trans('All shops', [], 'Admin.Global')), 515 'currency' => ($specific_price['id_currency'] ? $currencies[$specific_price['id_currency']]['name'] : $this->translator->trans('All currencies', [], 'Admin.Global')), 516 'country' => ($specific_price['id_country'] ? $countries[$specific_price['id_country']]['name'] : $this->translator->trans('All countries', [], 'Admin.Global')), 517 'group' => ($specific_price['id_group'] ? $groups[$specific_price['id_group']]['name'] : $this->translator->trans('All groups', [], 'Admin.Global')), 518 'customer' => (isset($customer_full_name) ? $customer_full_name : $this->translator->trans('All customers', [], 'Admin.Global')), 519 'fixed_price' => $fixed_price, 520 'impact' => $impact, 521 'period' => $period, 522 'from_quantity' => $specific_price['from_quantity'], 523 'can_delete' => (!$rule->id && $can_delete_specific_prices) ? true : false, 524 'can_edit' => (!$rule->id && $can_delete_specific_prices) ? true : false, 525 ]; 526 527 unset($customer_full_name); 528 } 529 } 530 } 531 532 return $content; 533 } 534 535 /** 536 * @param int $id 537 * 538 * @return SpecificPrice 539 * 540 * @throws EntityNotFoundException 541 */ 542 public function getSpecificPriceDataById($id) 543 { 544 $price = new SpecificPrice($id); 545 if (null === $price->id) { 546 throw new EntityNotFoundException(sprintf('Cannot find specific price with id %d', $id)); 547 } 548 549 return $price; 550 } 551 552 /** 553 * Delete a specific price. 554 * 555 * @param int $id_specific_price 556 * 557 * @return array error & status 558 */ 559 public function deleteSpecificPrice($id_specific_price) 560 { 561 if (!$id_specific_price || !Validate::isUnsignedId($id_specific_price)) { 562 $error = $this->translator->trans('The specific price ID is invalid.', [], 'Admin.Catalog.Notification'); 563 } else { 564 $specificPrice = new SpecificPrice((int) $id_specific_price); 565 if (!$specificPrice->delete()) { 566 $error = $this->translator->trans('An error occurred while attempting to delete the specific price.', [], 'Admin.Catalog.Notification'); 567 } 568 } 569 570 if (isset($error)) { 571 return [ 572 'status' => 'error', 573 'message' => $error, 574 ]; 575 } 576 577 return [ 578 'status' => 'ok', 579 'message' => $this->translator->trans('Successful deletion', [], 'Admin.Notifications.Success'), 580 ]; 581 } 582 583 /** 584 * Get price priority. 585 * 586 * @param int|null $idProduct 587 * 588 * @return array 589 */ 590 public function getPricePriority($idProduct = null) 591 { 592 if (!$idProduct) { 593 return [ 594 0 => 'id_shop', 595 1 => 'id_currency', 596 2 => 'id_country', 597 3 => 'id_group', 598 ]; 599 } 600 601 $specific_price_priorities = SpecificPrice::getPriority((int) $idProduct); 602 603 // Not use id_customer 604 if ($specific_price_priorities[0] == 'id_customer') { 605 unset($specific_price_priorities[0]); 606 } 607 608 return array_values($specific_price_priorities); 609 } 610 611 /** 612 * Process customization collection. 613 * 614 * @param object $product 615 * @param array $data 616 * 617 * @return array<int, int> 618 */ 619 public function processProductCustomization($product, $data) 620 { 621 $customization_ids = []; 622 if ($data) { 623 foreach ($data as $customization) { 624 $customization_ids[] = (int) $customization['id_customization_field']; 625 } 626 } 627 628 $shopList = Shop::getContextListShopID(); 629 630 /* Update the customization fields to be deleted in the next step if not used */ 631 $product->softDeleteCustomizationFields($customization_ids); 632 633 $usedCustomizationIds = $product->getUsedCustomizationFieldsIds(); 634 $usedCustomizationIds = array_column($usedCustomizationIds, 'index'); 635 $usedCustomizationIds = array_map('intval', $usedCustomizationIds); 636 $usedCustomizationIds = array_unique(array_merge($usedCustomizationIds, $customization_ids), SORT_REGULAR); 637 638 //remove customization field langs for current context shops 639 $productCustomization = $product->getCustomizationFieldIds(); 640 $toDeleteCustomizationIds = []; 641 foreach ($productCustomization as $customizationFiled) { 642 if (!in_array((int) $customizationFiled['id_customization_field'], $usedCustomizationIds)) { 643 $toDeleteCustomizationIds[] = (int) $customizationFiled['id_customization_field']; 644 } 645 //if the customization_field is still in use, only delete the current context shops langs, 646 if (in_array((int) $customizationFiled['id_customization_field'], $customization_ids)) { 647 Customization::deleteCustomizationFieldLangByShop($customizationFiled['id_customization_field'], $shopList); 648 } 649 } 650 651 //remove unused customization for the product 652 $product->deleteUnusedCustomizationFields($toDeleteCustomizationIds); 653 654 //create new customizations 655 $countFieldText = 0; 656 $countFieldFile = 0; 657 $productCustomizableValue = 0; 658 $hasRequiredField = false; 659 660 $new_customization_fields_ids = []; 661 662 if ($data) { 663 foreach ($data as $key => $customization) { 664 if ($customization['require']) { 665 $hasRequiredField = true; 666 } 667 668 //create label 669 if (isset($customization['id_customization_field'])) { 670 $id_customization_field = (int) $customization['id_customization_field']; 671 Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'customization_field` 672 SET `required` = ' . ($customization['require'] ? 1 : 0) . ', `type` = ' . (int) $customization['type'] . ' 673 WHERE `id_customization_field` = ' . $id_customization_field); 674 } else { 675 Db::getInstance()->execute( 676 'INSERT INTO `' . _DB_PREFIX_ . 'customization_field` (`id_product`, `type`, `required`) 677 VALUES (' 678 . (int) $product->id . ', ' 679 . (int) $customization['type'] . ', ' 680 . ($customization['require'] ? 1 : 0) 681 . ')' 682 ); 683 $id_customization_field = (int) Db::getInstance()->Insert_ID(); 684 } 685 686 $new_customization_fields_ids[$key] = $id_customization_field; 687 688 // Create multilingual label name 689 $langValues = ''; 690 foreach (Language::getLanguages() as $language) { 691 $name = $customization['label'][$language['id_lang']]; 692 foreach ($shopList as $id_shop) { 693 $langValues .= '(' 694 . (int) $id_customization_field . ', ' 695 . (int) $language['id_lang'] . ', ' 696 . (int) $id_shop . ',\'' 697 . pSQL($name) 698 . '\'), '; 699 } 700 } 701 Db::getInstance()->execute( 702 'INSERT INTO `' . _DB_PREFIX_ . 'customization_field_lang` (`id_customization_field`, `id_lang`, `id_shop`, `name`) VALUES ' 703 . rtrim( 704 $langValues, 705 ', ' 706 ) 707 ); 708 709 if ($customization['type'] == Product::CUSTOMIZE_FILE) { 710 ++$countFieldFile; 711 } else { 712 ++$countFieldText; 713 } 714 } 715 716 $productCustomizableValue = $hasRequiredField ? 2 : 1; 717 } 718 719 //update product count fields labels 720 Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'product` SET `customizable` = ' . $productCustomizableValue . ', `uploadable_files` = ' . (int) $countFieldFile . ', `text_fields` = ' . (int) $countFieldText . ' WHERE `id_product` = ' . (int) $product->id); 721 722 //update product_shop count fields labels 723 ObjectModel::updateMultishopTable('product', [ 724 'customizable' => $productCustomizableValue, 725 'uploadable_files' => (int) $countFieldFile, 726 'text_fields' => (int) $countFieldText, 727 ], 'a.id_product = ' . (int) $product->id); 728 729 Configuration::updateGlobalValue('PS_CUSTOMIZATION_FEATURE_ACTIVE', '1'); 730 731 return $new_customization_fields_ids; 732 } 733 734 /** 735 * Update product download. 736 * 737 * @param object $product 738 * @param array $data 739 * 740 * @return ProductDownload 741 */ 742 public function updateDownloadProduct($product, $data) 743 { 744 $id_product_download = ProductDownload::getIdFromIdProduct((int) $product->id, false); 745 $download = new ProductDownload($id_product_download ? $id_product_download : null); 746 747 if ((int) $data['is_virtual_file'] == 1) { 748 $fileName = null; 749 $file = $data['file']; 750 751 if (!empty($file)) { 752 $fileName = ProductDownload::getNewFilename(); 753 $file->move(_PS_DOWNLOAD_DIR_, $fileName); 754 } 755 756 $product->setDefaultAttribute(0); //reset cache_default_attribute 757 758 $download->id_product = (int) $product->id; 759 $download->display_filename = $data['name']; 760 $download->filename = $fileName ? $fileName : $download->filename; 761 $download->date_add = date('Y-m-d H:i:s'); 762 $download->date_expiration = $data['expiration_date'] ? $data['expiration_date'] . ' 23:59:59' : ''; 763 $download->nb_days_accessible = (int) $data['nb_days']; 764 $download->nb_downloadable = (int) $data['nb_downloadable']; 765 $download->active = true; 766 $download->is_shareable = false; 767 768 if (!$id_product_download) { 769 $download->save(); 770 } else { 771 $download->update(); 772 } 773 } else { 774 if (!empty($id_product_download)) { 775 $download->date_expiration = date('Y-m-d H:i:s', time() - 1); 776 $download->active = false; 777 $download->update(); 778 } 779 } 780 781 return $download; 782 } 783 784 /** 785 * Delete file from a virtual product. 786 * 787 * @param object $product 788 */ 789 public function processDeleteVirtualProductFile($product) 790 { 791 $id_product_download = ProductDownload::getIdFromIdProduct((int) $product->id, false); 792 $download = new ProductDownload($id_product_download ? $id_product_download : null); 793 794 if (!empty($download->filename)) { 795 unlink(_PS_DOWNLOAD_DIR_ . $download->filename); 796 Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'product_download` SET filename = "" WHERE `id_product_download` = ' . (int) $download->id); 797 } 798 } 799 800 /** 801 * Delete a virtual product. 802 * 803 * @param object $product 804 */ 805 public function processDeleteVirtualProduct($product) 806 { 807 $id_product_download = ProductDownload::getIdFromIdProduct((int) $product->id, false); 808 $download = new ProductDownload($id_product_download ? $id_product_download : null); 809 if (Validate::isLoadedObject($download)) { 810 $download->delete(true); 811 } 812 } 813 814 /** 815 * Add attachement file. 816 * 817 * @param object $product 818 * @param array $data 819 * @param array $locales 820 * 821 * @return object|null Attachement 822 */ 823 public function processAddAttachment($product, $data, $locales) 824 { 825 $attachment = null; 826 $file = $data['file']; 827 if (!empty($file)) { 828 $fileName = sha1(microtime()); 829 $attachment = new Attachment(); 830 831 foreach ($locales as $locale) { 832 $attachment->name[(int) $locale['id_lang']] = $data['name']; 833 $attachment->description[(int) $locale['id_lang']] = $data['description']; 834 } 835 836 $attachment->file = $fileName; 837 $attachment->mime = $file->getMimeType(); 838 $attachment->file_name = $file->getClientOriginalName(); 839 840 $file->move(_PS_DOWNLOAD_DIR_, $fileName); 841 842 if ($attachment->add()) { 843 $attachment->attachProduct($product->id); 844 } 845 } 846 847 return $attachment; 848 } 849 850 /** 851 * Process product attachments. 852 * 853 * @param object $product 854 * @param array $data 855 */ 856 public function processAttachments($product, $data) 857 { 858 Attachment::attachToProduct($product->id, $data); 859 } 860 861 /** 862 * Update images positions. 863 * 864 * @param array $data Indexed array with id product/position 865 */ 866 public function ajaxProcessUpdateImagePosition($data) 867 { 868 foreach ($data as $id => $position) { 869 $img = new Image((int) $id); 870 $img->position = (int) $position; 871 $img->update(); 872 } 873 } 874 875 /** 876 * Update image legend and cover. 877 * 878 * @param int $idImage 879 * @param array $data 880 * 881 * @return object image 882 */ 883 public function ajaxProcessUpdateImage($idImage, $data) 884 { 885 $img = new Image((int) $idImage); 886 if ($data['cover']) { 887 Image::deleteCover((int) $img->id_product); 888 $img->cover = true; 889 } 890 $img->legend = $data['legend']; 891 $img->update(); 892 893 return $img; 894 } 895 896 /** 897 * Generate preview URL. 898 * 899 * @param object $product 900 * @param bool $preview 901 * 902 * @return string|bool Preview url 903 */ 904 public function getPreviewUrl($product, $preview = true) 905 { 906 $context = Context::getContext(); 907 $id_lang = Configuration::get('PS_LANG_DEFAULT', null, null, $context->shop->id); 908 909 if (!ShopUrl::getMainShopDomain()) { 910 return false; 911 } 912 913 $is_rewrite_active = (bool) Configuration::get('PS_REWRITING_SETTINGS'); 914 $preview_url = $context->link->getProductLink( 915 $product, 916 $product->link_rewrite[$context->language->id], 917 Category::getLinkRewrite($product->id_category_default, $context->language->id), 918 null, 919 $id_lang, 920 (int) $context->shop->id, 921 0, 922 $is_rewrite_active 923 ); 924 925 if (!$product->active && $preview) { 926 $preview_url = $this->getPreviewUrlDeactivate($preview_url); 927 } 928 929 return $preview_url; 930 } 931 932 /** 933 * Generate preview URL deactivate. 934 * 935 * @param string $preview_url 936 * 937 * @return string preview url deactivate 938 */ 939 public function getPreviewUrlDeactivate($preview_url) 940 { 941 $context = Context::getContext(); 942 $token = Tools::getAdminTokenLite('AdminProducts'); 943 944 $admin_dir = dirname($_SERVER['PHP_SELF']); 945 $admin_dir = substr($admin_dir, strrpos($admin_dir, '/') + 1); 946 $preview_url_deactivate = $preview_url . ((strpos($preview_url, '?') === false) ? '?' : '&') . 'adtoken=' . $token . '&ad=' . $admin_dir . '&id_employee=' . (int) $context->employee->id . '&preview=1'; 947 948 return $preview_url_deactivate; 949 } 950 951 /** 952 * Generate preview URL. 953 * 954 * @param int $productId 955 * 956 * @return string preview url 957 */ 958 public function getPreviewUrlFromId($productId) 959 { 960 $product = new Product($productId, false); 961 962 return $this->getPreviewUrl($product); 963 } 964} 965