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 27// Deprecated since 1.5.0.1 use Product::CUSTOMIZE_FILE 28define('_CUSTOMIZE_FILE_', 0); 29 30// Deprecated since 1.5.0.1 use Product::CUSTOMIZE_TEXTFIELD 31define('_CUSTOMIZE_TEXTFIELD_', 1); 32 33use PrestaShop\Decimal\DecimalNumber; 34use PrestaShop\PrestaShop\Adapter\ServiceLocator; 35use PrestaShop\PrestaShop\Core\Domain\Product\ProductSettings; 36use PrestaShop\PrestaShop\Core\Domain\Product\Stock\ValueObject\OutOfStockType; 37use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\Ean13; 38use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\Isbn; 39use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductType; 40use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\RedirectType; 41use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\Reference; 42use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\Upc; 43use PrestaShop\PrestaShop\Core\Product\ProductInterface; 44use PrestaShop\PrestaShop\Core\Util\DateTime\DateTime as DateTimeUtil; 45 46class ProductCore extends ObjectModel 47{ 48 /** 49 * @var string Tax name 50 * 51 * @deprecated Since 1.4 52 */ 53 public $tax_name; 54 55 /** @var float Tax rate */ 56 public $tax_rate; 57 58 /** @var int Manufacturer identifier */ 59 public $id_manufacturer; 60 61 /** @var int Supplier identifier */ 62 public $id_supplier; 63 64 /** @var int default Category identifier */ 65 public $id_category_default; 66 67 /** @var int default Shop identifier */ 68 public $id_shop_default; 69 70 /** @var string Manufacturer name */ 71 public $manufacturer_name; 72 73 /** @var string Supplier name */ 74 public $supplier_name; 75 76 /** @var string|array Name or array of names by id_lang */ 77 public $name; 78 79 /** @var string|array Long description or array of long description by id_lang */ 80 public $description; 81 82 /** @var string|array Short description or array of short description by id_lang */ 83 public $description_short; 84 85 /** 86 * @deprecated since 1.7.8 87 * @see StockAvailable::$quantity instead 88 * 89 * @var int Quantity available 90 */ 91 public $quantity = 0; 92 93 /** @var int Minimal quantity for add to cart */ 94 public $minimal_quantity = 1; 95 96 /** @var int|null Low stock for mail alert */ 97 public $low_stock_threshold = null; 98 99 /** @var bool Low stock mail alert activated */ 100 public $low_stock_alert = false; 101 102 /** @var string|array Text when in stock or array of text by id_lang */ 103 public $available_now; 104 105 /** @var string|array Text when not in stock but available to order or array of text by id_lang */ 106 public $available_later; 107 108 /** @var float Price */ 109 public $price = 0; 110 111 /** @var array|int|null Will be filled by reference by priceCalculation() */ 112 public $specificPrice = 0; 113 114 /** @var string Additional shipping cost */ 115 public $additional_shipping_cost = 0; 116 117 /** @var string Wholesale Price in euros */ 118 public $wholesale_price = 0; 119 120 /** @var bool on_sale */ 121 public $on_sale = false; 122 123 /** @var bool online_only */ 124 public $online_only = false; 125 126 /** @var string unity */ 127 public $unity = null; 128 129 /** @var float price for product's unity */ 130 public $unit_price = 0; 131 132 /** @var float price for product's unity ratio */ 133 public $unit_price_ratio = 0; 134 135 /** @var float Ecotax */ 136 public $ecotax = 0; 137 138 /** @var string Reference */ 139 public $reference; 140 141 /** 142 * @var string Supplier Reference 143 * 144 * @deprecated since 1.7.7.0 145 */ 146 public $supplier_reference; 147 148 /** 149 * @deprecated since 1.7.8 150 * @see StockAvailable::$location instead 151 * 152 * @var string Location 153 */ 154 public $location = ''; 155 156 /** @var string|float Width in default width unit */ 157 public $width = 0; 158 159 /** @var string|float Height in default height unit */ 160 public $height = 0; 161 162 /** @var string|float Depth in default depth unit */ 163 public $depth = 0; 164 165 /** @var string|float Weight in default weight unit */ 166 public $weight = 0; 167 168 /** @var string Ean-13 barcode */ 169 public $ean13; 170 171 /** @var string ISBN */ 172 public $isbn; 173 174 /** @var string Upc barcode */ 175 public $upc; 176 177 /** @var string MPN */ 178 public $mpn; 179 180 /** @var string|array Friendly URL or array of friendly URL by id_lang */ 181 public $link_rewrite; 182 183 /** @var string|array Meta description or array of meta description by id_lang */ 184 public $meta_description; 185 186 /** 187 * @deprecated 188 */ 189 public $meta_keywords; 190 191 /** @var string|array Meta title or array of meta title by id_lang */ 192 public $meta_title; 193 194 /** 195 * @var mixed 196 * 197 * @deprecated Unused 198 */ 199 public $quantity_discount = 0; 200 201 /** @var bool|int Product customization */ 202 public $customizable; 203 204 /** @var bool|null Product is new */ 205 public $new = null; 206 207 /** @var int Number of uploadable files (concerning customizable products) */ 208 public $uploadable_files; 209 210 /** @var int Number of text fields */ 211 public $text_fields; 212 213 /** @var bool Product status */ 214 public $active = true; 215 216 /** 217 * @var string Redirection type 218 * 219 * @see RedirectType 220 */ 221 public $redirect_type = RedirectType::TYPE_NOT_FOUND; 222 223 /** 224 * @var int Product identifier or Category identifier depends on redirect_type 225 */ 226 public $id_type_redirected = 0; 227 228 /** @var bool Product available for order */ 229 public $available_for_order = true; 230 231 /** @var string Available for order date in mysql format Y-m-d */ 232 public $available_date = DateTimeUtil::NULL_DATE; 233 234 /** @var bool Will the condition select should be visible for this product ? */ 235 public $show_condition = false; 236 237 /** @var string Enumerated (enum) product condition (new, used, refurbished) */ 238 public $condition; 239 240 /** @var bool Show price of Product */ 241 public $show_price = true; 242 243 /** @var bool is the product indexed in the search index? */ 244 public $indexed = 0; 245 246 /** @var string ENUM('both', 'catalog', 'search', 'none') front office visibility */ 247 public $visibility; 248 249 /** @var string Object creation date in mysql format Y-m-d H:i:s */ 250 public $date_add; 251 252 /** @var string Object last modification date in mysql format Y-m-d H:i:s */ 253 public $date_upd; 254 255 /** @var array Tags data */ 256 public $tags; 257 258 /** @var int temporary or saved object */ 259 public $state = self::STATE_SAVED; 260 261 /** 262 * @var float Base price of the product 263 * 264 * @deprecated 1.6.0.13 265 */ 266 public $base_price; 267 268 /** 269 * @var int TaxRulesGroup identifier 270 */ 271 public $id_tax_rules_group; 272 273 /** 274 * @var int 275 * We keep this variable for retrocompatibility for themes 276 * 277 * @deprecated 1.5.0 278 */ 279 public $id_color_default = 0; 280 281 /** 282 * @deprecated since 1.7.8 283 * The advanced stock management feature is not maintained anymore 284 * 285 * @var bool Tells if the product uses the advanced stock management 286 */ 287 public $advanced_stock_management = 0; 288 289 /** 290 * @deprecated since 1.7.8 291 * @see StockAvailable::$out_of_stock instead 292 * 293 * @var int 294 * - O Deny orders 295 * - 1 Allow orders 296 * - 2 Use global setting 297 */ 298 public $out_of_stock = OutOfStockType::OUT_OF_STOCK_DEFAULT; 299 300 /** 301 * @deprecated since 1.7.8 302 * This property was only relevant to advanced stock management and that feature is not maintained anymore 303 * 304 * @var bool 305 */ 306 public $depends_on_stock; 307 308 /** 309 * @var bool 310 */ 311 public $isFullyLoaded = false; 312 313 /** 314 * @var bool 315 */ 316 public $cache_is_pack; 317 318 /** 319 * @var bool 320 */ 321 public $cache_has_attachments; 322 323 /** 324 * @var bool 325 */ 326 public $is_virtual; 327 328 /** 329 * @var int 330 */ 331 public $id_pack_product_attribute; 332 333 /** 334 * @var int 335 */ 336 public $cache_default_attribute; 337 338 /** 339 * @var string|string[] If product is populated, this property contain the rewrite link of the default category 340 */ 341 public $category; 342 343 /** 344 * @var int tell the type of stock management to apply on the pack 345 */ 346 public $pack_stock_type = Pack::STOCK_TYPE_DEFAULT; 347 348 /** 349 * Type of delivery time. 350 * 351 * Choose which parameters use for give information delivery. 352 * 0 - none 353 * 1 - use default information 354 * 2 - use product information 355 * 356 * @var int 357 */ 358 public $additional_delivery_times = 1; 359 360 /** 361 * Delivery in-stock information. 362 * 363 * Long description for delivery in-stock product information. 364 * 365 * @var string[] 366 */ 367 public $delivery_in_stock; 368 369 /** 370 * Delivery out-stock information. 371 * 372 * Long description for delivery out-stock product information. 373 * 374 * @var string[] 375 */ 376 public $delivery_out_stock; 377 378 /** 379 * For now default value remains undefined, to keep compatibility with page v1 and former products. 380 * But once the v2 is merged the default value should be ProductType::TYPE_STANDARD 381 * 382 * @var string 383 */ 384 public $product_type = ProductType::TYPE_UNDEFINED; 385 386 /** 387 * @var int|null 388 */ 389 public static $_taxCalculationMethod = null; 390 391 /** @var array Price cache */ 392 protected static $_prices = []; 393 394 /** @var array */ 395 protected static $_pricesLevel2 = []; 396 397 /** @var array */ 398 protected static $_incat = []; 399 400 /** @var array */ 401 protected static $_combinations = []; 402 403 /** 404 * @deprecated Since 1.5.6.1 405 * 406 * @var array 407 */ 408 protected static $_cart_quantity = []; 409 410 /** 411 * @deprecated Since 1.5.0.9 412 * 413 * @var array 414 */ 415 protected static $_tax_rules_group = []; 416 417 /** @var array */ 418 protected static $_cacheFeatures = []; 419 420 /** @var array */ 421 protected static $_frontFeaturesCache = []; 422 423 /** @var array */ 424 protected static $productPropertiesCache = []; 425 426 /** 427 * @deprecated Since 1.5.0.1 Unused 428 * 429 * @var array cache stock data in getStock() method 430 */ 431 protected static $cacheStock = []; 432 433 /** 434 * Product can be temporary saved in database 435 */ 436 const STATE_TEMP = 0; 437 const STATE_SAVED = 1; 438 439 /** 440 * @var array Contains object definition 441 * 442 * @see ObjectModel::$definition 443 */ 444 public static $definition = [ 445 'table' => 'product', 446 'primary' => 'id_product', 447 'multilang' => true, 448 'multilang_shop' => true, 449 'fields' => [ 450 /* Classic fields */ 451 'id_shop_default' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 452 'id_manufacturer' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 453 'id_supplier' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 454 'reference' => ['type' => self::TYPE_STRING, 'validate' => 'isReference', 'size' => Reference::MAX_LENGTH], 455 'supplier_reference' => ['type' => self::TYPE_STRING, 'validate' => 'isReference', 'size' => 64], 456 'location' => ['type' => self::TYPE_STRING, 'validate' => 'isString', 'size' => 255], 457 'width' => ['type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'], 458 'height' => ['type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'], 459 'depth' => ['type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'], 460 'weight' => ['type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'], 461 'quantity_discount' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 462 'ean13' => ['type' => self::TYPE_STRING, 'validate' => 'isEan13', 'size' => Ean13::MAX_LENGTH], 463 'isbn' => ['type' => self::TYPE_STRING, 'validate' => 'isIsbn', 'size' => Isbn::MAX_LENGTH], 464 'upc' => ['type' => self::TYPE_STRING, 'validate' => 'isUpc', 'size' => Upc::MAX_LENGTH], 465 'mpn' => ['type' => self::TYPE_STRING, 'validate' => 'isMpn', 'size' => ProductSettings::MAX_MPN_LENGTH], 466 'cache_is_pack' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 467 'cache_has_attachments' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 468 'is_virtual' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 469 'state' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 470 'additional_delivery_times' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 471 'delivery_in_stock' => [ 472 'type' => self::TYPE_STRING, 473 'lang' => true, 474 'validate' => 'isGenericName', 475 'size' => 255, 476 ], 477 'delivery_out_stock' => [ 478 'type' => self::TYPE_STRING, 479 'lang' => true, 480 'validate' => 'isGenericName', 481 'size' => 255, 482 ], 483 'product_type' => [ 484 'type' => self::TYPE_STRING, 485 'validate' => 'isGenericName', 486 // For now undefined value is still allowed, in 179 we should use ProductType::AVAILABLE_TYPES here 487 'values' => [ 488 ProductType::TYPE_STANDARD, 489 ProductType::TYPE_PACK, 490 ProductType::TYPE_VIRTUAL, 491 ProductType::TYPE_COMBINATIONS, 492 ProductType::TYPE_UNDEFINED, 493 ], 494 // This default value should be replaced with ProductType::TYPE_STANDARD in 179 when the v2 page is fully migrated 495 'default' => ProductType::TYPE_UNDEFINED, 496 ], 497 498 /* Shop fields */ 499 'id_category_default' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'], 500 'id_tax_rules_group' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'], 501 'on_sale' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'], 502 'online_only' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'], 503 'ecotax' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice'], 504 'minimal_quantity' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'], 505 'low_stock_threshold' => ['type' => self::TYPE_INT, 'shop' => true, 'allow_null' => true, 'validate' => 'isInt'], 506 'low_stock_alert' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'], 507 'price' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice', 'required' => true], 508 'wholesale_price' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice'], 509 'unity' => ['type' => self::TYPE_STRING, 'shop' => true, 'validate' => 'isString'], 510 'unit_price_ratio' => ['type' => self::TYPE_FLOAT, 'shop' => true], 511 'additional_shipping_cost' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice'], 512 'customizable' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'], 513 'text_fields' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'], 514 'uploadable_files' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'], 515 'active' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'], 516 'redirect_type' => ['type' => self::TYPE_STRING, 'shop' => true, 'validate' => 'isString'], 517 'id_type_redirected' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'], 518 'available_for_order' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'], 519 'available_date' => ['type' => self::TYPE_DATE, 'shop' => true, 'validate' => 'isDateFormat'], 520 'show_condition' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'], 521 'condition' => ['type' => self::TYPE_STRING, 'shop' => true, 'validate' => 'isGenericName', 'values' => ['new', 'used', 'refurbished'], 'default' => 'new'], 522 'show_price' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'], 523 'indexed' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'], 524 'visibility' => ['type' => self::TYPE_STRING, 'shop' => true, 'validate' => 'isProductVisibility', 'values' => ['both', 'catalog', 'search', 'none'], 'default' => 'both'], 525 'cache_default_attribute' => ['type' => self::TYPE_INT, 'shop' => true], 526 'advanced_stock_management' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'], 527 'date_add' => ['type' => self::TYPE_DATE, 'shop' => true, 'validate' => 'isDate'], 528 'date_upd' => ['type' => self::TYPE_DATE, 'shop' => true, 'validate' => 'isDate'], 529 'pack_stock_type' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'], 530 531 /* Lang fields */ 532 'meta_description' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 512], 533 'meta_keywords' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255], 534 'meta_title' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255], 535 'link_rewrite' => [ 536 'type' => self::TYPE_STRING, 537 'lang' => true, 538 'validate' => 'isLinkRewrite', 539 'required' => false, 540 'size' => 128, 541 'ws_modifier' => [ 542 'http_method' => WebserviceRequest::HTTP_POST, 543 'modifier' => 'modifierWsLinkRewrite', 544 ], 545 ], 546 'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isCatalogName', 'required' => false, 'size' => ProductSettings::MAX_NAME_LENGTH], 547 'description' => ['type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml'], 548 'description_short' => ['type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml'], 549 'available_now' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255], 550 'available_later' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'IsGenericName', 'size' => 255], 551 ], 552 'associations' => [ 553 'manufacturer' => ['type' => self::HAS_ONE], 554 'supplier' => ['type' => self::HAS_ONE], 555 'default_category' => ['type' => self::HAS_ONE, 'field' => 'id_category_default', 'object' => 'Category'], 556 'tax_rules_group' => ['type' => self::HAS_ONE], 557 'categories' => ['type' => self::HAS_MANY, 'field' => 'id_category', 'object' => 'Category', 'association' => 'category_product'], 558 'stock_availables' => ['type' => self::HAS_MANY, 'field' => 'id_stock_available', 'object' => 'StockAvailable', 'association' => 'stock_availables'], 559 'attachments' => ['type' => self::HAS_MANY, 'field' => 'id_attachment', 'object' => 'Attachment', 'association' => 'product_attachment'], 560 ], 561 ]; 562 563 /** @var array */ 564 protected $webserviceParameters = [ 565 'objectMethods' => [ 566 'add' => 'addWs', 567 'update' => 'updateWs', 568 ], 569 'objectNodeNames' => 'products', 570 'fields' => [ 571 'id_manufacturer' => [ 572 'xlink_resource' => 'manufacturers', 573 ], 574 'id_supplier' => [ 575 'xlink_resource' => 'suppliers', 576 ], 577 'id_category_default' => [ 578 'xlink_resource' => 'categories', 579 ], 580 'new' => [], 581 'cache_default_attribute' => [], 582 'id_default_image' => [ 583 'getter' => 'getCoverWs', 584 'setter' => 'setCoverWs', 585 'xlink_resource' => [ 586 'resourceName' => 'images', 587 'subResourceName' => 'products', 588 ], 589 ], 590 'id_default_combination' => [ 591 'getter' => 'getWsDefaultCombination', 592 'setter' => 'setWsDefaultCombination', 593 'xlink_resource' => [ 594 'resourceName' => 'combinations', 595 ], 596 ], 597 'id_tax_rules_group' => [ 598 'xlink_resource' => [ 599 'resourceName' => 'tax_rule_groups', 600 ], 601 ], 602 'position_in_category' => [ 603 'getter' => 'getWsPositionInCategory', 604 'setter' => 'setWsPositionInCategory', 605 ], 606 'manufacturer_name' => [ 607 'getter' => 'getWsManufacturerName', 608 'setter' => false, 609 ], 610 'quantity' => [ 611 'getter' => false, 612 'setter' => false, 613 ], 614 'type' => [ 615 'getter' => 'getWsType', 616 'setter' => 'setWsType', 617 ], 618 ], 619 'associations' => [ 620 'categories' => [ 621 'resource' => 'category', 622 'fields' => [ 623 'id' => ['required' => true], 624 ], 625 ], 626 'images' => [ 627 'resource' => 'image', 628 'fields' => ['id' => []], 629 ], 630 'combinations' => [ 631 'resource' => 'combination', 632 'fields' => [ 633 'id' => ['required' => true], 634 ], 635 ], 636 'product_option_values' => [ 637 'resource' => 'product_option_value', 638 'fields' => [ 639 'id' => ['required' => true], 640 ], 641 ], 642 'product_features' => [ 643 'resource' => 'product_feature', 644 'fields' => [ 645 'id' => ['required' => true], 646 'id_feature_value' => [ 647 'required' => true, 648 'xlink_resource' => 'product_feature_values', 649 ], 650 ], 651 ], 652 'tags' => ['resource' => 'tag', 653 'fields' => [ 654 'id' => ['required' => true], 655 ], ], 656 'stock_availables' => ['resource' => 'stock_available', 657 'fields' => [ 658 'id' => ['required' => true], 659 'id_product_attribute' => ['required' => true], 660 ], 661 'setter' => false, 662 ], 663 'attachments' => [ 664 'resource' => 'attachment', 665 'api' => 'attachments', 666 'fields' => [ 667 'id' => ['required' => true], 668 ], 669 ], 670 'accessories' => [ 671 'resource' => 'product', 672 'api' => 'products', 673 'fields' => [ 674 'id' => [ 675 'required' => true, 676 'xlink_resource' => 'products', ], 677 ], 678 ], 679 'product_bundle' => [ 680 'resource' => 'product', 681 'api' => 'products', 682 'fields' => [ 683 'id' => ['required' => true], 684 'id_product_attribute' => [], 685 'quantity' => [], 686 ], 687 ], 688 ], 689 ]; 690 691 const CUSTOMIZE_FILE = 0; 692 const CUSTOMIZE_TEXTFIELD = 1; 693 694 /** 695 * Note: prefix is "PTYPE" because TYPE_ is used in ObjectModel (definition). 696 */ 697 const PTYPE_SIMPLE = 0; 698 const PTYPE_PACK = 1; 699 const PTYPE_VIRTUAL = 2; 700 701 /** 702 * @param int|null $id_product Product identifier 703 * @param bool $full Load with price, tax rate, manufacturer name, supplier name, tags, stocks... 704 * @param int|null $id_lang Language identifier 705 * @param int|null $id_shop Shop identifier 706 * @param Context|null $context Context to use for retrieve cart 707 */ 708 public function __construct($id_product = null, $full = false, $id_lang = null, $id_shop = null, Context $context = null) 709 { 710 parent::__construct($id_product, $id_lang, $id_shop); 711 712 $unitPriceRatio = new DecimalNumber((string) ($this->unit_price_ratio ?? 0)); 713 $price = new DecimalNumber((string) ($this->price ?? 0)); 714 715 if ($unitPriceRatio->isGreaterThanZero()) { 716 $this->unit_price = (float) (string) $price->dividedBy($unitPriceRatio); 717 } 718 719 if ($full && $this->id) { 720 if (!$context) { 721 $context = Context::getContext(); 722 } 723 724 $this->isFullyLoaded = $full; 725 $this->tax_name = 'deprecated'; // The applicable tax may be BOTH the product one AND the state one (moreover this variable is some deadcode) 726 $this->manufacturer_name = Manufacturer::getNameById((int) $this->id_manufacturer); 727 $this->supplier_name = Supplier::getNameById((int) $this->id_supplier); 728 $address = null; 729 if (is_object($context->cart) && $context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')} != null) { 730 $address = $context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}; 731 } 732 733 $this->tax_rate = $this->getTaxesRate(new Address($address)); 734 735 $this->new = $this->isNew(); 736 737 // Keep base price 738 $this->base_price = $this->price; 739 740 $this->price = Product::getPriceStatic((int) $this->id, false, null, 6, null, false, true, 1, false, null, null, null, $this->specificPrice); 741 $this->unit_price = ($this->unit_price_ratio != 0 ? $this->price / $this->unit_price_ratio : 0); 742 $this->tags = Tag::getProductTags((int) $this->id); 743 744 $this->loadStockData(); 745 } 746 747 if ($this->id_category_default) { 748 $this->category = Category::getLinkRewrite((int) $this->id_category_default, (int) $id_lang); 749 } 750 } 751 752 /** 753 * @see ObjectModel::getFieldsShop() 754 * 755 * @return array 756 */ 757 public function getFieldsShop() 758 { 759 $fields = parent::getFieldsShop(); 760 if (null === $this->update_fields || (!empty($this->update_fields['price']) && !empty($this->update_fields['unit_price']))) { 761 if ($this->unit_price !== null) { 762 $fields['unit_price_ratio'] = (float) $this->unit_price > 0 ? $this->price / $this->unit_price : 0; 763 } 764 } 765 $fields['unity'] = pSQL($this->unity); 766 767 return $fields; 768 } 769 770 /** 771 * {@inheritdoc} 772 */ 773 public function add($autodate = true, $null_values = false) 774 { 775 if ($this->is_virtual) { 776 $this->product_type = ProductType::TYPE_VIRTUAL; 777 } 778 779 if (!parent::add($autodate, $null_values)) { 780 return false; 781 } 782 783 $id_shop_list = Shop::getContextListShopID(); 784 if ($this->getType() == Product::PTYPE_VIRTUAL) { 785 foreach ($id_shop_list as $value) { 786 StockAvailable::setProductOutOfStock((int) $this->id, OutOfStockType::OUT_OF_STOCK_AVAILABLE, $value); 787 } 788 789 if ($this->active && !Configuration::get('PS_VIRTUAL_PROD_FEATURE_ACTIVE')) { 790 Configuration::updateGlobalValue('PS_VIRTUAL_PROD_FEATURE_ACTIVE', '1'); 791 } 792 } else { 793 foreach ($id_shop_list as $value) { 794 StockAvailable::setProductOutOfStock((int) $this->id, OutOfStockType::OUT_OF_STOCK_DEFAULT, $value); 795 } 796 } 797 798 $this->setGroupReduction(); 799 Hook::exec('actionProductSave', ['id_product' => (int) $this->id, 'product' => $this]); 800 801 return true; 802 } 803 804 /** 805 * {@inheritdoc} 806 */ 807 public function update($null_values = false) 808 { 809 if ($this->is_virtual) { 810 $this->product_type = ProductType::TYPE_VIRTUAL; 811 } 812 813 $return = parent::update($null_values); 814 $this->setGroupReduction(); 815 816 // Sync stock Reference, EAN13, MPN and UPC 817 if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && StockAvailable::dependsOnStock($this->id, Context::getContext()->shop->id)) { 818 Db::getInstance()->update('stock', [ 819 'reference' => pSQL($this->reference), 820 'ean13' => pSQL($this->ean13), 821 'isbn' => pSQL($this->isbn), 822 'upc' => pSQL($this->upc), 823 'mpn' => pSQL($this->mpn), 824 ], 'id_product = ' . (int) $this->id . ' AND id_product_attribute = 0'); 825 } 826 827 Hook::exec('actionProductSave', ['id_product' => (int) $this->id, 'product' => $this]); 828 Hook::exec('actionProductUpdate', ['id_product' => (int) $this->id, 'product' => $this]); 829 if ($this->getType() == Product::PTYPE_VIRTUAL && $this->active && !Configuration::get('PS_VIRTUAL_PROD_FEATURE_ACTIVE')) { 830 Configuration::updateGlobalValue('PS_VIRTUAL_PROD_FEATURE_ACTIVE', '1'); 831 } 832 833 return $return; 834 } 835 836 /** 837 * Init computation of price display method (i.e. price should be including tax or not) for a customer. 838 * If customer Id passed as null then this compute price display method with according of current group. 839 * Otherwise a price display method will compute with according of a customer address (i.e. country). 840 * 841 * @see Group::getPriceDisplayMethod() 842 * 843 * @param int|null $id_customer Customer identifier 844 */ 845 public static function initPricesComputation($id_customer = null) 846 { 847 if ((int) $id_customer > 0) { 848 $customer = new Customer((int) $id_customer); 849 if (!Validate::isLoadedObject($customer)) { 850 die(Tools::displayError()); 851 } 852 self::$_taxCalculationMethod = Group::getPriceDisplayMethod((int) $customer->id_default_group); 853 $cur_cart = Context::getContext()->cart; 854 $id_address = 0; 855 if (Validate::isLoadedObject($cur_cart)) { 856 $id_address = (int) $cur_cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}; 857 } 858 $address_infos = Address::getCountryAndState($id_address); 859 860 if (self::$_taxCalculationMethod != PS_TAX_EXC 861 && !empty($address_infos['vat_number']) 862 && $address_infos['id_country'] != Configuration::get('VATNUMBER_COUNTRY') 863 && Configuration::get('VATNUMBER_MANAGEMENT')) { 864 self::$_taxCalculationMethod = PS_TAX_EXC; 865 } 866 } else { 867 self::$_taxCalculationMethod = Group::getPriceDisplayMethod(Group::getCurrent()->id); 868 } 869 } 870 871 /** 872 * Returns price display method for a customer (i.e. price should be including tax or not). 873 * 874 * @see initPricesComputation() 875 * 876 * @param int|null $id_customer Customer identifier 877 * 878 * @return int Returns 0 (PS_TAX_INC) if tax should be included, otherwise 1 (PS_TAX_EXC) - tax should be excluded 879 */ 880 public static function getTaxCalculationMethod($id_customer = null) 881 { 882 if (self::$_taxCalculationMethod === null || $id_customer !== null) { 883 Product::initPricesComputation($id_customer); 884 } 885 886 return (int) self::$_taxCalculationMethod; 887 } 888 889 /** 890 * Move a product inside its category. 891 * 892 * @param bool $way Up (1) or Down (0) 893 * @param int $position 894 * 895 * @return bool Update result 896 */ 897 public function updatePosition($way, $position) 898 { 899 if (!$res = Db::getInstance()->executeS(' 900 SELECT cp.`id_product`, cp.`position`, cp.`id_category` 901 FROM `' . _DB_PREFIX_ . 'category_product` cp 902 WHERE cp.`id_category` = ' . (int) Tools::getValue('id_category', 1) . ' 903 ORDER BY cp.`position` ASC') 904 ) { 905 return false; 906 } 907 908 foreach ($res as $product) { 909 if ((int) $product['id_product'] == (int) $this->id) { 910 $moved_product = $product; 911 } 912 } 913 914 if (!isset($moved_product) || !isset($position)) { 915 return false; 916 } 917 918 // < and > statements rather than BETWEEN operator 919 // since BETWEEN is treated differently according to databases 920 $result = ( 921 Db::getInstance()->execute(' 922 UPDATE `' . _DB_PREFIX_ . 'category_product` cp 923 INNER JOIN `' . _DB_PREFIX_ . 'product` p ON (p.`id_product` = cp.`id_product`) 924 ' . Shop::addSqlAssociation('product', 'p') . ' 925 SET cp.`position`= `position` ' . ($way ? '- 1' : '+ 1') . ', 926 p.`date_upd` = "' . date('Y-m-d H:i:s') . '", product_shop.`date_upd` = "' . date('Y-m-d H:i:s') . '" 927 WHERE cp.`position` 928 ' . ($way 929 ? '> ' . (int) $moved_product['position'] . ' AND `position` <= ' . (int) $position 930 : '< ' . (int) $moved_product['position'] . ' AND `position` >= ' . (int) $position) . ' 931 AND `id_category`=' . (int) $moved_product['id_category']) 932 && Db::getInstance()->execute(' 933 UPDATE `' . _DB_PREFIX_ . 'category_product` cp 934 INNER JOIN `' . _DB_PREFIX_ . 'product` p ON (p.`id_product` = cp.`id_product`) 935 ' . Shop::addSqlAssociation('product', 'p') . ' 936 SET cp.`position` = ' . (int) $position . ', 937 p.`date_upd` = "' . date('Y-m-d H:i:s') . '", product_shop.`date_upd` = "' . date('Y-m-d H:i:s') . '" 938 WHERE cp.`id_product` = ' . (int) $moved_product['id_product'] . ' 939 AND cp.`id_category`=' . (int) $moved_product['id_category']) 940 ); 941 Hook::exec('actionProductUpdate', ['id_product' => (int) $this->id, 'product' => $this]); 942 943 return $result; 944 } 945 946 /** 947 * Reorder product position in category $id_category. 948 * Call it after deleting a product from a category. 949 * 950 * @param int $id_category Category identifier 951 * @param int $position 952 * 953 * @return bool 954 */ 955 public static function cleanPositions($id_category, $position = 0) 956 { 957 $return = true; 958 959 if (!(int) $position) { 960 $result = Db::getInstance()->executeS(' 961 SELECT `id_product` 962 FROM `' . _DB_PREFIX_ . 'category_product` 963 WHERE `id_category` = ' . (int) $id_category . ' 964 ORDER BY `position` 965 '); 966 $total = count($result); 967 968 for ($i = 0; $i < $total; ++$i) { 969 $return &= Db::getInstance()->update( 970 'category_product', 971 ['position' => $i], 972 '`id_category` = ' . (int) $id_category . ' AND `id_product` = ' . (int) $result[$i]['id_product'] 973 ); 974 $return &= Db::getInstance()->execute( 975 'UPDATE `' . _DB_PREFIX_ . 'product` p' . Shop::addSqlAssociation('product', 'p') . ' 976 SET p.`date_upd` = "' . date('Y-m-d H:i:s') . '", product_shop.`date_upd` = "' . date('Y-m-d H:i:s') . '" 977 WHERE p.`id_product` = ' . (int) $result[$i]['id_product'] 978 ); 979 } 980 } else { 981 $result = Db::getInstance()->executeS(' 982 SELECT `id_product` 983 FROM `' . _DB_PREFIX_ . 'category_product` 984 WHERE `id_category` = ' . (int) $id_category . ' AND `position` > ' . (int) $position . ' 985 ORDER BY `position` 986 '); 987 $total = count($result); 988 $return &= Db::getInstance()->update( 989 'category_product', 990 ['position' => ['type' => 'sql', 'value' => '`position`-1']], 991 '`id_category` = ' . (int) $id_category . ' AND `position` > ' . (int) $position 992 ); 993 994 for ($i = 0; $i < $total; ++$i) { 995 $return &= Db::getInstance()->execute( 996 'UPDATE `' . _DB_PREFIX_ . 'product` p' . Shop::addSqlAssociation('product', 'p') . ' 997 SET p.`date_upd` = "' . date('Y-m-d H:i:s') . '", product_shop.`date_upd` = "' . date('Y-m-d H:i:s') . '" 998 WHERE p.`id_product` = ' . (int) $result[$i]['id_product'] 999 ); 1000 } 1001 } 1002 1003 return $return; 1004 } 1005 1006 /** 1007 * Get the default attribute for a product. 1008 * 1009 * @param int $id_product Product identifier 1010 * @param int $minimum_quantity 1011 * @param bool $reset 1012 * 1013 * @return int Attributes list 1014 */ 1015 public static function getDefaultAttribute($id_product, $minimum_quantity = 0, $reset = false) 1016 { 1017 if (!Combination::isFeatureActive()) { 1018 return 0; 1019 } 1020 1021 if ($reset && isset(static::$_combinations[$id_product])) { 1022 unset(static::$_combinations[$id_product]); 1023 } 1024 1025 if (!isset(static::$_combinations[$id_product])) { 1026 static::$_combinations[$id_product] = []; 1027 } 1028 if (isset(static::$_combinations[$id_product][$minimum_quantity])) { 1029 return static::$_combinations[$id_product][$minimum_quantity]; 1030 } 1031 1032 $sql = 'SELECT product_attribute_shop.id_product_attribute 1033 FROM ' . _DB_PREFIX_ . 'product_attribute pa 1034 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 1035 WHERE pa.id_product = ' . (int) $id_product; 1036 1037 $result_no_filter = Db::getInstance()->getValue($sql); 1038 if (!$result_no_filter) { 1039 static::$_combinations[$id_product][$minimum_quantity] = 0; 1040 1041 return 0; 1042 } 1043 1044 $sql = 'SELECT product_attribute_shop.id_product_attribute 1045 FROM ' . _DB_PREFIX_ . 'product_attribute pa 1046 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 1047 ' . ($minimum_quantity > 0 ? Product::sqlStock('pa', 'pa') : '') . 1048 ' WHERE product_attribute_shop.default_on = 1 ' 1049 . ($minimum_quantity > 0 ? ' AND IFNULL(stock.quantity, 0) >= ' . (int) $minimum_quantity : '') . 1050 ' AND pa.id_product = ' . (int) $id_product; 1051 $result = Db::getInstance()->getValue($sql); 1052 1053 if (!$result) { 1054 $sql = 'SELECT product_attribute_shop.id_product_attribute 1055 FROM ' . _DB_PREFIX_ . 'product_attribute pa 1056 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 1057 ' . ($minimum_quantity > 0 ? Product::sqlStock('pa', 'pa') : '') . 1058 ' WHERE pa.id_product = ' . (int) $id_product 1059 . ($minimum_quantity > 0 ? ' AND IFNULL(stock.quantity, 0) >= ' . (int) $minimum_quantity : ''); 1060 1061 $result = Db::getInstance()->getValue($sql); 1062 } 1063 1064 if (!$result) { 1065 $sql = 'SELECT product_attribute_shop.id_product_attribute 1066 FROM ' . _DB_PREFIX_ . 'product_attribute pa 1067 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 1068 WHERE product_attribute_shop.`default_on` = 1 1069 AND pa.id_product = ' . (int) $id_product; 1070 1071 $result = Db::getInstance()->getValue($sql); 1072 } 1073 1074 if (!$result) { 1075 $result = $result_no_filter; 1076 } 1077 1078 static::$_combinations[$id_product][$minimum_quantity] = $result; 1079 1080 return $result; 1081 } 1082 1083 /** 1084 * @param string $available_date Date in mysql format Y-m-d 1085 * 1086 * @return bool 1087 */ 1088 public function setAvailableDate($available_date = '0000-00-00') 1089 { 1090 if (Validate::isDateFormat($available_date) && $this->available_date != $available_date) { 1091 $this->available_date = $available_date; 1092 1093 return $this->update(); 1094 } 1095 1096 return false; 1097 } 1098 1099 /** 1100 * For a given id_product and id_product_attribute, return available date. 1101 * 1102 * @param int $id_product Product identifier 1103 * @param int|null $id_product_attribute Attribute identifier 1104 * 1105 * @return string|null 1106 */ 1107 public static function getAvailableDate($id_product, $id_product_attribute = null) 1108 { 1109 $sql = 'SELECT'; 1110 1111 if ($id_product_attribute === null) { 1112 $sql .= ' p.`available_date`'; 1113 } else { 1114 $sql .= ' pa.`available_date`'; 1115 } 1116 1117 $sql .= ' FROM `' . _DB_PREFIX_ . 'product` p'; 1118 1119 if ($id_product_attribute !== null) { 1120 $sql .= ' LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON (pa.`id_product` = p.`id_product`)'; 1121 } 1122 1123 $sql .= Shop::addSqlAssociation('product', 'p'); 1124 1125 if ($id_product_attribute !== null) { 1126 $sql .= Shop::addSqlAssociation('product_attribute', 'pa'); 1127 } 1128 1129 $sql .= ' WHERE p.`id_product` = ' . (int) $id_product; 1130 1131 if ($id_product_attribute !== null) { 1132 $sql .= ' AND pa.`id_product` = ' . (int) $id_product . ' AND pa.`id_product_attribute` = ' . (int) $id_product_attribute; 1133 } 1134 1135 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql); 1136 1137 if ($result == '0000-00-00') { 1138 $result = null; 1139 } 1140 1141 return $result; 1142 } 1143 1144 /** 1145 * @param int $id_product Product identifier 1146 * @param bool $is_virtual 1147 */ 1148 public static function updateIsVirtual($id_product, $is_virtual = true) 1149 { 1150 $isVirtual = (bool) $is_virtual; 1151 $updateData = [ 1152 'is_virtual' => $isVirtual, 1153 ]; 1154 1155 // We only update the type if we are sure it is virtual 1156 if ($isVirtual) { 1157 $updateData['product_type'] = ProductType::TYPE_VIRTUAL; 1158 } 1159 1160 Db::getInstance()->update('product', $updateData, 'id_product = ' . (int) $id_product); 1161 } 1162 1163 /** 1164 * @see ObjectModel::resetStaticCache() 1165 * 1166 * reset static cache (eg unit testing purpose). 1167 */ 1168 public static function resetStaticCache() 1169 { 1170 static::$loaded_classes = []; 1171 static::$productPropertiesCache = []; 1172 static::$_cacheFeatures = []; 1173 static::$_frontFeaturesCache = []; 1174 static::$_prices = []; 1175 static::$_pricesLevel2 = []; 1176 static::$_incat = []; 1177 static::$_combinations = []; 1178 } 1179 1180 /** 1181 * {@inheritdoc} 1182 */ 1183 public function validateField($field, $value, $id_lang = null, $skip = [], $human_errors = false) 1184 { 1185 if ($field == 'description_short') { 1186 // The legacy validation is basic, so the idea here is to adapt the allowed limit so that it takes into 1187 // account the difference between the raw text and the html text (since actually the limit is only about 1188 // the raw text) This is a bit ugly the real validation should only be performed by TinyMceMaxLengthValidator 1189 // but we have to deal with this for now. 1190 $limit = (int) Configuration::get('PS_PRODUCT_SHORT_DESC_LIMIT'); 1191 if ($limit <= 0) { 1192 $limit = 800; 1193 } 1194 1195 $replaceArray = [ 1196 "\n", 1197 "\r", 1198 "\n\r", 1199 "\r\n", 1200 ]; 1201 $str = str_replace($replaceArray, [''], strip_tags($value)); 1202 $size_without_html = iconv_strlen($str); 1203 $size_with_html = Tools::strlen($value); 1204 $adaptedLimit = $limit + $size_with_html - $size_without_html; 1205 $this->def['fields']['description_short']['size'] = $adaptedLimit; 1206 } 1207 1208 return parent::validateField($field, $value, $id_lang, $skip, $human_errors); 1209 } 1210 1211 /** 1212 * {@inheritdoc} 1213 */ 1214 public function delete() 1215 { 1216 /* 1217 * @since 1.5.0 1218 * It is NOT possible to delete a product if there are currently: 1219 * - physical stock for this product 1220 * - supply order(s) for this product 1221 */ 1222 if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && $this->advanced_stock_management) { 1223 $stock_manager = StockManagerFactory::getManager(); 1224 $physical_quantity = $stock_manager->getProductPhysicalQuantities($this->id, 0); 1225 $real_quantity = $stock_manager->getProductRealQuantities($this->id, 0); 1226 if ($physical_quantity > 0) { 1227 return false; 1228 } 1229 if ($real_quantity > $physical_quantity) { 1230 return false; 1231 } 1232 1233 $warehouse_product_locations = ServiceLocator::get('\\PrestaShop\\PrestaShop\\Core\\Foundation\\Database\\EntityManager')->getRepository('WarehouseProductLocation')->findByIdProduct($this->id); 1234 foreach ($warehouse_product_locations as $warehouse_product_location) { 1235 $warehouse_product_location->delete(); 1236 } 1237 1238 $stocks = ServiceLocator::get('\\PrestaShop\\PrestaShop\\Core\\Foundation\\Database\\EntityManager')->getRepository('Stock')->findByIdProduct($this->id); 1239 foreach ($stocks as $stock) { 1240 $stock->delete(); 1241 } 1242 } 1243 $result = parent::delete(); 1244 1245 // Removes the product from StockAvailable, for the current shop 1246 StockAvailable::removeProductFromStockAvailable($this->id); 1247 $result &= ($this->deleteProductAttributes() && $this->deleteImages()); 1248 // If there are still entries in product_shop, don't remove completely the product 1249 if ($this->hasMultishopEntries()) { 1250 return true; 1251 } 1252 1253 Hook::exec('actionProductDelete', ['id_product' => (int) $this->id, 'product' => $this]); 1254 if (!$result || 1255 !GroupReduction::deleteProductReduction($this->id) || 1256 !$this->deleteCategories(true) || 1257 !$this->deleteProductFeatures() || 1258 !$this->deleteTags() || 1259 !$this->deleteCartProducts() || 1260 !$this->deleteAttributesImpacts() || 1261 !$this->deleteAttachments(false) || 1262 !$this->deleteCustomization() || 1263 !SpecificPrice::deleteByProductId((int) $this->id) || 1264 !$this->deletePack() || 1265 !$this->deleteProductSale() || 1266 !$this->deleteSearchIndexes() || 1267 !$this->deleteAccessories() || 1268 !$this->deleteFromAccessories() || 1269 !$this->deleteFromSupplier() || 1270 !$this->deleteDownload() || 1271 !$this->deleteFromCartRules()) { 1272 return false; 1273 } 1274 1275 return true; 1276 } 1277 1278 /** 1279 * @param array $products Product identifiers 1280 * 1281 * @return bool|int 1282 */ 1283 public function deleteSelection($products) 1284 { 1285 $return = 1; 1286 if (is_array($products) && ($count = count($products))) { 1287 // Deleting products can be quite long on a cheap server. Let's say 1.5 seconds by product (I've seen it!). 1288 if ((int) (ini_get('max_execution_time')) < round($count * 1.5)) { 1289 ini_set('max_execution_time', round($count * 1.5)); 1290 } 1291 1292 foreach ($products as $id_product) { 1293 $product = new Product((int) $id_product); 1294 $return &= $product->delete(); 1295 } 1296 } 1297 1298 return $return; 1299 } 1300 1301 /** 1302 * @return bool 1303 */ 1304 public function deleteFromCartRules() 1305 { 1306 CartRule::cleanProductRuleIntegrity('products', $this->id); 1307 1308 return true; 1309 } 1310 1311 /** 1312 * @return bool 1313 */ 1314 public function deleteFromSupplier() 1315 { 1316 return Db::getInstance()->delete('product_supplier', 'id_product = ' . (int) $this->id); 1317 } 1318 1319 /** 1320 * addToCategories add this product to the category/ies if not exists. 1321 * 1322 * @param int|int[] $categories id_category or array of id_category 1323 * 1324 * @return bool true if succeed 1325 */ 1326 public function addToCategories($categories = []) 1327 { 1328 if (empty($categories)) { 1329 return false; 1330 } 1331 1332 if (!is_array($categories)) { 1333 $categories = [$categories]; 1334 } 1335 1336 if (!count($categories)) { 1337 return false; 1338 } 1339 1340 $categories = array_map('intval', $categories); 1341 1342 $current_categories = $this->getCategories(); 1343 $current_categories = array_map('intval', $current_categories); 1344 1345 // for new categ, put product at last position 1346 $res_categ_new_pos = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' 1347 SELECT id_category, MAX(position)+1 newPos 1348 FROM `' . _DB_PREFIX_ . 'category_product` 1349 WHERE `id_category` IN(' . implode(',', $categories) . ') 1350 GROUP BY id_category'); 1351 foreach ($res_categ_new_pos as $array) { 1352 $new_categories[(int) $array['id_category']] = (int) $array['newPos']; 1353 } 1354 1355 $new_categ_pos = []; 1356 // The first position must be 1 instead of 0 1357 foreach ($categories as $id_category) { 1358 $new_categ_pos[$id_category] = isset($new_categories[$id_category]) ? $new_categories[$id_category] : 1; 1359 } 1360 1361 $product_cats = []; 1362 1363 foreach ($categories as $new_id_categ) { 1364 if (!in_array($new_id_categ, $current_categories)) { 1365 $product_cats[] = [ 1366 'id_category' => (int) $new_id_categ, 1367 'id_product' => (int) $this->id, 1368 'position' => (int) $new_categ_pos[$new_id_categ], 1369 ]; 1370 } 1371 } 1372 1373 Db::getInstance()->insert('category_product', $product_cats); 1374 1375 Cache::clean('Product::getProductCategories_' . (int) $this->id); 1376 1377 return true; 1378 } 1379 1380 /** 1381 * Update categories to index product into. 1382 * 1383 * @param int[] $categories Categories list to index product into 1384 * @param bool $keeping_current_pos (deprecated, no more used) 1385 * 1386 * @return bool Update/insertion result 1387 */ 1388 public function updateCategories($categories, $keeping_current_pos = false) 1389 { 1390 if (empty($categories)) { 1391 return false; 1392 } 1393 1394 $result = Db::getInstance()->executeS( 1395 ' 1396 SELECT c.`id_category` 1397 FROM `' . _DB_PREFIX_ . 'category_product` cp 1398 LEFT JOIN `' . _DB_PREFIX_ . 'category` c ON (c.`id_category` = cp.`id_category`) 1399 ' . Shop::addSqlAssociation('category', 'c', true, null, true) . ' 1400 WHERE cp.`id_category` NOT IN (' . implode(',', array_map('intval', $categories)) . ') 1401 AND cp.id_product = ' . (int) $this->id 1402 ); 1403 1404 // if none are found, it's an error 1405 if (!is_array($result)) { 1406 return false; 1407 } 1408 1409 foreach ($result as $categ_to_delete) { 1410 $this->deleteCategory($categ_to_delete['id_category']); 1411 } 1412 1413 if (!$this->addToCategories($categories)) { 1414 return false; 1415 } 1416 1417 SpecificPriceRule::applyAllRules([(int) $this->id]); 1418 1419 Cache::clean('Product::getProductCategories_' . (int) $this->id); 1420 1421 return true; 1422 } 1423 1424 /** 1425 * deleteCategory delete this product from the category $id_category. 1426 * 1427 * @param int $id_category Category identifier 1428 * @param bool $clean_positions 1429 * 1430 * @return bool 1431 */ 1432 public function deleteCategory($id_category, $clean_positions = true) 1433 { 1434 $result = Db::getInstance()->executeS( 1435 'SELECT `id_category`, `position` 1436 FROM `' . _DB_PREFIX_ . 'category_product` 1437 WHERE `id_product` = ' . (int) $this->id . ' 1438 AND id_category = ' . (int) $id_category 1439 ); 1440 1441 $return = Db::getInstance()->delete('category_product', 'id_product = ' . (int) $this->id . ' AND id_category = ' . (int) $id_category); 1442 if ($clean_positions === true) { 1443 foreach ($result as $row) { 1444 static::cleanPositions((int) $row['id_category'], (int) $row['position']); 1445 } 1446 } 1447 1448 SpecificPriceRule::applyAllRules([(int) $this->id]); 1449 1450 Cache::clean('Product::getProductCategories_' . (int) $this->id); 1451 1452 return $return; 1453 } 1454 1455 /** 1456 * Delete all association to category where product is indexed. 1457 * 1458 * @param bool $clean_positions clean category positions after deletion 1459 * 1460 * @return bool Deletion result 1461 */ 1462 public function deleteCategories($clean_positions = false) 1463 { 1464 if ($clean_positions === true) { 1465 $result = Db::getInstance()->executeS( 1466 'SELECT `id_category`, `position` 1467 FROM `' . _DB_PREFIX_ . 'category_product` 1468 WHERE `id_product` = ' . (int) $this->id 1469 ); 1470 } 1471 1472 $return = Db::getInstance()->delete('category_product', 'id_product = ' . (int) $this->id); 1473 if ($clean_positions === true && is_array($result)) { 1474 foreach ($result as $row) { 1475 $return &= static::cleanPositions((int) $row['id_category'], (int) $row['position']); 1476 } 1477 } 1478 1479 Cache::clean('Product::getProductCategories_' . (int) $this->id); 1480 1481 return $return; 1482 } 1483 1484 /** 1485 * Delete products tags entries. 1486 * 1487 * @return bool Deletion result 1488 */ 1489 public function deleteTags() 1490 { 1491 return Tag::deleteTagsForProduct((int) $this->id); 1492 } 1493 1494 /** 1495 * Delete product from cart. 1496 * 1497 * @return bool Deletion result 1498 */ 1499 public function deleteCartProducts() 1500 { 1501 return Db::getInstance()->delete('cart_product', 'id_product = ' . (int) $this->id); 1502 } 1503 1504 /** 1505 * Delete product images from database. 1506 * 1507 * @return bool success 1508 */ 1509 public function deleteImages() 1510 { 1511 $result = Db::getInstance()->executeS( 1512 ' 1513 SELECT `id_image` 1514 FROM `' . _DB_PREFIX_ . 'image` 1515 WHERE `id_product` = ' . (int) $this->id 1516 ); 1517 1518 $status = true; 1519 if ($result) { 1520 foreach ($result as $row) { 1521 $image = new Image($row['id_image']); 1522 $status &= $image->delete(); 1523 } 1524 } 1525 1526 return $status; 1527 } 1528 1529 /** 1530 * Get all available products. 1531 * 1532 * @param int $id_lang Language identifier 1533 * @param int $start Start number 1534 * @param int $limit Number of products to return 1535 * @param string $order_by Field for ordering 1536 * @param string $order_way Way for ordering (ASC or DESC) 1537 * @param int|false $id_category Category identifier 1538 * @param bool $only_active 1539 * @param Context|null $context 1540 * 1541 * @return array Products details 1542 */ 1543 public static function getProducts( 1544 $id_lang, 1545 $start, 1546 $limit, 1547 $order_by, 1548 $order_way, 1549 $id_category = false, 1550 $only_active = false, 1551 Context $context = null 1552 ) { 1553 if (!$context) { 1554 $context = Context::getContext(); 1555 } 1556 1557 $front = true; 1558 if (!in_array($context->controller->controller_type, ['front', 'modulefront'])) { 1559 $front = false; 1560 } 1561 1562 if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay($order_way)) { 1563 die(Tools::displayError()); 1564 } 1565 if ($order_by == 'id_product' || $order_by == 'price' || $order_by == 'date_add' || $order_by == 'date_upd') { 1566 $order_by_prefix = 'p'; 1567 } elseif ($order_by == 'name') { 1568 $order_by_prefix = 'pl'; 1569 } elseif ($order_by == 'position') { 1570 $order_by_prefix = 'c'; 1571 } 1572 1573 if (strpos($order_by, '.') > 0) { 1574 $order_by = explode('.', $order_by); 1575 $order_by_prefix = $order_by[0]; 1576 $order_by = $order_by[1]; 1577 } 1578 $sql = 'SELECT p.*, product_shop.*, pl.* , m.`name` AS manufacturer_name, s.`name` AS supplier_name 1579 FROM `' . _DB_PREFIX_ . 'product` p 1580 ' . Shop::addSqlAssociation('product', 'p') . ' 1581 LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (p.`id_product` = pl.`id_product` ' . Shop::addSqlRestrictionOnLang('pl') . ') 1582 LEFT JOIN `' . _DB_PREFIX_ . 'manufacturer` m ON (m.`id_manufacturer` = p.`id_manufacturer`) 1583 LEFT JOIN `' . _DB_PREFIX_ . 'supplier` s ON (s.`id_supplier` = p.`id_supplier`)' . 1584 ($id_category ? 'LEFT JOIN `' . _DB_PREFIX_ . 'category_product` c ON (c.`id_product` = p.`id_product`)' : '') . ' 1585 WHERE pl.`id_lang` = ' . (int) $id_lang . 1586 ($id_category ? ' AND c.`id_category` = ' . (int) $id_category : '') . 1587 ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . 1588 ($only_active ? ' AND product_shop.`active` = 1' : '') . ' 1589 ORDER BY ' . (isset($order_by_prefix) ? pSQL($order_by_prefix) . '.' : '') . '`' . pSQL($order_by) . '` ' . pSQL($order_way) . 1590 ($limit > 0 ? ' LIMIT ' . (int) $start . ',' . (int) $limit : ''); 1591 $rq = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); 1592 if ($order_by == 'price') { 1593 Tools::orderbyPrice($rq, $order_way); 1594 } 1595 1596 foreach ($rq as &$row) { 1597 $row = Product::getTaxesInformations($row); 1598 } 1599 1600 return $rq; 1601 } 1602 1603 /** 1604 * @param int $id_lang Language identifier 1605 * @param Context|null $context 1606 * 1607 * @return array 1608 */ 1609 public static function getSimpleProducts($id_lang, Context $context = null) 1610 { 1611 if (!$context) { 1612 $context = Context::getContext(); 1613 } 1614 1615 $front = true; 1616 if (!in_array($context->controller->controller_type, ['front', 'modulefront'])) { 1617 $front = false; 1618 } 1619 1620 $sql = 'SELECT p.`id_product`, pl.`name` 1621 FROM `' . _DB_PREFIX_ . 'product` p 1622 ' . Shop::addSqlAssociation('product', 'p') . ' 1623 LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (p.`id_product` = pl.`id_product` ' . Shop::addSqlRestrictionOnLang('pl') . ') 1624 WHERE pl.`id_lang` = ' . (int) $id_lang . ' 1625 ' . ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . ' 1626 ORDER BY pl.`name`'; 1627 1628 return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); 1629 } 1630 1631 /** 1632 * @return bool 1633 */ 1634 public function isNew() 1635 { 1636 $nbDaysNewProduct = Configuration::get('PS_NB_DAYS_NEW_PRODUCT'); 1637 if (!Validate::isUnsignedInt($nbDaysNewProduct)) { 1638 $nbDaysNewProduct = 20; 1639 } 1640 1641 $query = 'SELECT COUNT(p.id_product) 1642 FROM `' . _DB_PREFIX_ . 'product` p 1643 ' . Shop::addSqlAssociation('product', 'p') . ' 1644 WHERE p.id_product = ' . (int) $this->id . ' 1645 AND DATEDIFF("' . date('Y-m-d') . ' 00:00:00", product_shop.`date_add`) < ' . $nbDaysNewProduct; 1646 1647 return (bool) Db::getInstance()->getValue($query, false); 1648 } 1649 1650 /** 1651 * @param int[] $attributes_list Attribute identifier(s) 1652 * @param int|false $current_product_attribute Attribute identifier 1653 * @param Context|null $context 1654 * @param bool $all_shops 1655 * @param bool $return_id 1656 * 1657 * @return bool|int|string Attribute exist or Attribute identifier if return_id = true 1658 */ 1659 public function productAttributeExists($attributes_list, $current_product_attribute = false, Context $context = null, $all_shops = false, $return_id = false) 1660 { 1661 if (!Combination::isFeatureActive()) { 1662 return false; 1663 } 1664 if ($context === null) { 1665 $context = Context::getContext(); 1666 } 1667 $result = Db::getInstance()->executeS( 1668 'SELECT pac.`id_attribute`, pac.`id_product_attribute` 1669 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 1670 JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` pas ON (pas.id_product_attribute = pa.id_product_attribute) 1671 LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON (pac.`id_product_attribute` = pa.`id_product_attribute`) 1672 WHERE 1 ' . (!$all_shops ? ' AND pas.id_shop =' . (int) $context->shop->id : '') . ' AND pa.`id_product` = ' . (int) $this->id . 1673 ($all_shops ? ' GROUP BY pac.id_attribute, pac.id_product_attribute ' : '') 1674 ); 1675 1676 /* If something's wrong */ 1677 if (!$result || empty($result)) { 1678 return false; 1679 } 1680 /* Product attributes simulation */ 1681 $product_attributes = []; 1682 foreach ($result as $product_attribute) { 1683 $product_attributes[$product_attribute['id_product_attribute']][] = $product_attribute['id_attribute']; 1684 } 1685 /* Checking product's attribute existence */ 1686 foreach ($product_attributes as $key => $product_attribute) { 1687 if (count($product_attribute) == count($attributes_list)) { 1688 $diff = false; 1689 for ($i = 0; $diff == false && isset($product_attribute[$i]); ++$i) { 1690 if (!in_array($product_attribute[$i], $attributes_list) || $key == $current_product_attribute) { 1691 $diff = true; 1692 } 1693 } 1694 if (!$diff) { 1695 if ($return_id) { 1696 return $key; 1697 } 1698 1699 return true; 1700 } 1701 } 1702 } 1703 1704 return false; 1705 } 1706 1707 /** 1708 * addProductAttribute is deprecated. 1709 * 1710 * The quantity params now set StockAvailable for the current shop with the specified quantity 1711 * The supplier_reference params now set the supplier reference of the default supplier of the product if possible 1712 * 1713 * @deprecated since 1.5.0 1714 * @see StockManager if you want to manage real stock 1715 * @see StockAvailable if you want to manage available quantities for sale on your shop(s) 1716 * @see ProductSupplier for manage supplier reference(s) 1717 * 1718 * @param float $price Additional price 1719 * @param float $weight Additional weight 1720 * @param float $unit_impact 1721 * @param float $ecotax Additional ecotax 1722 * @param int $quantity 1723 * @param int[] $id_images Image ids 1724 * @param string $reference Reference 1725 * @param int $id_supplier Supplier identifier 1726 * @param string $ean13 1727 * @param bool $default Is default attribute for product 1728 * @param string $location 1729 * @param string $upc 1730 * @param int $minimal_quantity 1731 * @param string $isbn 1732 * @param int|null $low_stock_threshold Low stock for mail alert 1733 * @param bool $low_stock_alert Low stock mail alert activated 1734 * @param string|null $mpn 1735 * 1736 * @return int|false Attribute identifier if success, false if it fail 1737 */ 1738 public function addProductAttribute( 1739 $price, 1740 $weight, 1741 $unit_impact, 1742 $ecotax, 1743 $quantity, 1744 $id_images, 1745 $reference, 1746 $id_supplier, 1747 $ean13, 1748 $default, 1749 $location, 1750 $upc, 1751 $minimal_quantity, 1752 $isbn, 1753 $low_stock_threshold = null, 1754 $low_stock_alert = false, 1755 $mpn = null 1756 ) { 1757 Tools::displayAsDeprecated(); 1758 1759 $id_product_attribute = $this->addAttribute( 1760 $price, 1761 $weight, 1762 $unit_impact, 1763 $ecotax, 1764 $id_images, 1765 $reference, 1766 $ean13, 1767 $default, 1768 $location, 1769 $upc, 1770 $minimal_quantity, 1771 [], 1772 null, 1773 0, 1774 $isbn, 1775 $low_stock_threshold, 1776 $low_stock_alert, 1777 $mpn 1778 ); 1779 1780 if (!$id_product_attribute) { 1781 return false; 1782 } 1783 1784 StockAvailable::setQuantity($this->id, $id_product_attribute, $quantity); 1785 //Try to set the default supplier reference 1786 $this->addSupplierReference($id_supplier, $id_product_attribute); 1787 1788 return $id_product_attribute; 1789 } 1790 1791 /** 1792 * @param array $combinations 1793 * @param array $attributes 1794 * @param bool $resetExistingCombination 1795 * 1796 * @return bool 1797 */ 1798 public function generateMultipleCombinations($combinations, $attributes, $resetExistingCombination = true) 1799 { 1800 $res = true; 1801 foreach ($combinations as $key => $combination) { 1802 $id_combination = (int) $this->productAttributeExists($attributes[$key], false, null, true, true); 1803 if ($id_combination && !$resetExistingCombination) { 1804 continue; 1805 } 1806 1807 $obj = new Combination($id_combination); 1808 1809 if ($id_combination) { 1810 $obj->minimal_quantity = 1; 1811 $obj->available_date = '0000-00-00'; 1812 } 1813 1814 foreach ($combination as $field => $value) { 1815 $obj->$field = $value; 1816 } 1817 1818 $obj->default_on = 0; 1819 $this->setAvailableDate(); 1820 1821 $obj->save(); 1822 1823 if (!$id_combination) { 1824 $attribute_list = []; 1825 foreach ($attributes[$key] as $id_attribute) { 1826 $attribute_list[] = [ 1827 'id_product_attribute' => (int) $obj->id, 1828 'id_attribute' => (int) $id_attribute, 1829 ]; 1830 } 1831 $res &= Db::getInstance()->insert('product_attribute_combination', $attribute_list); 1832 } 1833 } 1834 1835 return $res; 1836 } 1837 1838 /** 1839 * @param int[] $combinations 1840 * @param $langId 1841 * 1842 * @return array 1843 */ 1844 public function sortCombinationByAttributePosition($combinations, $langId) 1845 { 1846 $attributes = []; 1847 foreach ($combinations as $combinationId) { 1848 $attributeCombination = $this->getAttributeCombinationsById($combinationId, $langId); 1849 $attributes[$attributeCombination[0]['position']][$combinationId] = $attributeCombination[0]; 1850 } 1851 1852 ksort($attributes); 1853 1854 return $attributes; 1855 } 1856 1857 /** 1858 * @param float $wholesale_price 1859 * @param float $price Additional price 1860 * @param float $weight Additional weight 1861 * @param float $unit_impact 1862 * @param float $ecotax Additional ecotax 1863 * @param int $quantity deprecated 1864 * @param int[] $id_images Image ids 1865 * @param string $reference Reference 1866 * @param int $id_supplier Supplier identifier 1867 * @param string $ean13 1868 * @param bool $default Is default attribute for product 1869 * @param string|null $location 1870 * @param string|null $upc 1871 * @param int $minimal_quantity 1872 * @param array $id_shop_list 1873 * @param string|null $available_date Date in mysql format Y-m-d 1874 * @param string $isbn 1875 * @param int|null $low_stock_threshold Low stock for mail alert 1876 * @param bool $low_stock_alert Low stock mail alert activated 1877 * @param string|null $mpn 1878 * 1879 * @return int|false Attribute identifier if success, false if it fail 1880 */ 1881 public function addCombinationEntity( 1882 $wholesale_price, 1883 $price, 1884 $weight, 1885 $unit_impact, 1886 $ecotax, 1887 $quantity, 1888 $id_images, 1889 $reference, 1890 $id_supplier, 1891 $ean13, 1892 $default, 1893 $location = null, 1894 $upc = null, 1895 $minimal_quantity = 1, 1896 array $id_shop_list = [], 1897 $available_date = null, 1898 $isbn = '', 1899 $low_stock_threshold = null, 1900 $low_stock_alert = false, 1901 $mpn = null 1902 ) { 1903 $id_product_attribute = $this->addAttribute( 1904 $price, 1905 $weight, 1906 $unit_impact, 1907 $ecotax, 1908 $id_images, 1909 $reference, 1910 $ean13, 1911 $default, 1912 $location, 1913 $upc, 1914 $minimal_quantity, 1915 $id_shop_list, 1916 $available_date, 1917 0, 1918 $isbn, 1919 $low_stock_threshold, 1920 $low_stock_alert, 1921 $mpn 1922 ); 1923 $this->addSupplierReference($id_supplier, $id_product_attribute); 1924 $result = ObjectModel::updateMultishopTable('Combination', [ 1925 'wholesale_price' => (float) $wholesale_price, 1926 ], 'a.id_product_attribute = ' . (int) $id_product_attribute); 1927 1928 if (!$id_product_attribute || !$result) { 1929 return false; 1930 } 1931 1932 return $id_product_attribute; 1933 } 1934 1935 /** 1936 * @deprecated 1.5.5.0 1937 * 1938 * @param array $attributes 1939 * @param bool $set_default 1940 * 1941 * @return array 1942 */ 1943 public function addProductAttributeMultiple($attributes, $set_default = true) 1944 { 1945 Tools::displayAsDeprecated(); 1946 $return = []; 1947 $default_value = 1; 1948 foreach ($attributes as $attribute) { 1949 $obj = new Combination(); 1950 foreach ($attribute as $key => $value) { 1951 $obj->$key = $value; 1952 } 1953 1954 if ($set_default) { 1955 $obj->default_on = $default_value; 1956 $default_value = 0; 1957 // if we add a combination for this shop and this product does not use the combination feature in other shop, 1958 // we clone the default combination in every shop linked to this product 1959 if (!$this->hasAttributesInOtherShops()) { 1960 $id_shop_list_array = Product::getShopsByProduct($this->id); 1961 $id_shop_list = []; 1962 foreach ($id_shop_list_array as $array_shop) { 1963 $id_shop_list[] = $array_shop['id_shop']; 1964 } 1965 $obj->id_shop_list = $id_shop_list; 1966 } 1967 } 1968 $obj->add(); 1969 $return[] = $obj->id; 1970 } 1971 1972 return $return; 1973 } 1974 1975 /** 1976 * Delete all default attributes for product. 1977 * 1978 * @return bool 1979 */ 1980 public function deleteDefaultAttributes() 1981 { 1982 return ObjectModel::updateMultishopTable('Combination', [ 1983 'default_on' => null, 1984 ], 'a.`id_product` = ' . (int) $this->id); 1985 } 1986 1987 /** 1988 * @param int $id_product_attribute Attribute identifier 1989 * 1990 * @return bool 1991 */ 1992 public function setDefaultAttribute($id_product_attribute) 1993 { 1994 // We only update the type when we know it has combinations 1995 if (!empty($id_product_attribute)) { 1996 $this->product_type = ProductType::TYPE_COMBINATIONS; 1997 } 1998 1999 $result = ObjectModel::updateMultishopTable('Combination', [ 2000 'default_on' => 1, 2001 ], 'a.`id_product` = ' . (int) $this->id . ' AND a.`id_product_attribute` = ' . (int) $id_product_attribute); 2002 2003 $result &= ObjectModel::updateMultishopTable('product', [ 2004 'cache_default_attribute' => (int) $id_product_attribute, 2005 'product_type' => $this->product_type, 2006 ], 'a.`id_product` = ' . (int) $this->id); 2007 $this->cache_default_attribute = (int) $id_product_attribute; 2008 2009 return $result; 2010 } 2011 2012 /** 2013 * @param int $id_product Product identifier 2014 * 2015 * @return int|false Default Attribute identifier if success, false if it false 2016 */ 2017 public static function updateDefaultAttribute($id_product) 2018 { 2019 $id_default_attribute = (int) Product::getDefaultAttribute($id_product, 0, true); 2020 2021 $result = Db::getInstance()->update('product_shop', [ 2022 'cache_default_attribute' => $id_default_attribute, 2023 ], 'id_product = ' . (int) $id_product . Shop::addSqlRestriction()); 2024 2025 // We only update the type when we know it has combinations 2026 $updateData = [ 2027 'cache_default_attribute' => $id_default_attribute, 2028 ]; 2029 if (!empty($id_default_attribute)) { 2030 $updateData['product_type'] = ProductType::TYPE_COMBINATIONS; 2031 } 2032 $result &= Db::getInstance()->update('product', $updateData, 'id_product = ' . (int) $id_product); 2033 2034 if ($result && $id_default_attribute) { 2035 return $id_default_attribute; 2036 } else { 2037 return $result; 2038 } 2039 } 2040 2041 /** 2042 * Update a product attribute. 2043 * 2044 * @deprecated since 1.5 2045 * @see updateAttribute() to use instead 2046 * @see ProductSupplier for manage supplier reference(s) 2047 * 2048 * @param int $id_product_attribute Attribute identifier 2049 * @param float $wholesale_price 2050 * @param float $price Additional price 2051 * @param float $weight Additional weight 2052 * @param float $unit 2053 * @param float $ecotax Additional ecotax 2054 * @param int[] $id_images Image ids 2055 * @param string $reference 2056 * @param int $id_supplier Supplier identifier 2057 * @param string $ean13 2058 * @param bool $default Is default attribute for product 2059 * @param string $location 2060 * @param string $upc 2061 * @param int $minimal_quantity 2062 * @param string $available_date Date in mysql format Y-m-d 2063 * @param string $isbn 2064 * @param int|null $low_stock_threshold Low stock for mail alert 2065 * @param bool $low_stock_alert Low stock mail alert activated 2066 * @param string|null $mpn 2067 * 2068 * @return array 2069 */ 2070 public function updateProductAttribute( 2071 $id_product_attribute, 2072 $wholesale_price, 2073 $price, 2074 $weight, 2075 $unit, 2076 $ecotax, 2077 $id_images, 2078 $reference, 2079 $id_supplier, 2080 $ean13, 2081 $default, 2082 $location, 2083 $upc, 2084 $minimal_quantity, 2085 $available_date, 2086 $isbn = '', 2087 $low_stock_threshold = null, 2088 $low_stock_alert = false, 2089 $mpn = null 2090 ) { 2091 Tools::displayAsDeprecated('Use updateAttribute() instead'); 2092 2093 $return = $this->updateAttribute( 2094 $id_product_attribute, 2095 $wholesale_price, 2096 $price, 2097 $weight, 2098 $unit, 2099 $ecotax, 2100 $id_images, 2101 $reference, 2102 $ean13, 2103 $default, 2104 $location = null, 2105 $upc = null, 2106 $minimal_quantity, 2107 $available_date, 2108 true, 2109 [], 2110 $isbn, 2111 $low_stock_threshold, 2112 $low_stock_alert, 2113 $mpn = null 2114 ); 2115 $this->addSupplierReference($id_supplier, $id_product_attribute); 2116 2117 return $return; 2118 } 2119 2120 /** 2121 * Sets or updates Supplier Reference. 2122 * 2123 * @param int $id_supplier Supplier identifier 2124 * @param int $id_product_attribute Attribute identifier 2125 * @param string|null $supplier_reference 2126 * @param float|null $price 2127 * @param int|null $id_currency Currency identifier 2128 */ 2129 public function addSupplierReference($id_supplier, $id_product_attribute, $supplier_reference = null, $price = null, $id_currency = null) 2130 { 2131 //in some case we need to add price without supplier reference 2132 if ($supplier_reference === null) { 2133 $supplier_reference = ''; 2134 } 2135 2136 //Try to set the default supplier reference 2137 if (($id_supplier > 0) && ($this->id > 0)) { 2138 $id_product_supplier = (int) ProductSupplier::getIdByProductAndSupplier($this->id, $id_product_attribute, $id_supplier); 2139 2140 $product_supplier = new ProductSupplier($id_product_supplier); 2141 2142 if (!$id_product_supplier) { 2143 $product_supplier->id_product = (int) $this->id; 2144 $product_supplier->id_product_attribute = (int) $id_product_attribute; 2145 $product_supplier->id_supplier = (int) $id_supplier; 2146 } 2147 2148 $product_supplier->product_supplier_reference = pSQL($supplier_reference); 2149 $product_supplier->product_supplier_price_te = null !== $price ? (float) $price : (float) $product_supplier->product_supplier_price_te; 2150 $product_supplier->id_currency = null !== $id_currency ? (int) $id_currency : (int) $product_supplier->id_currency; 2151 $product_supplier->save(); 2152 } 2153 } 2154 2155 /** 2156 * Update a product attribute. 2157 * 2158 * @param int $id_product_attribute Product attribute id 2159 * @param float $wholesale_price Wholesale price 2160 * @param float $price Additional price 2161 * @param float $weight Additional weight 2162 * @param float $unit Additional unit price 2163 * @param float $ecotax Additional ecotax 2164 * @param int[] $id_images Image identifiers 2165 * @param string $reference Reference 2166 * @param string $ean13 Ean-13 barcode 2167 * @param bool $default Is default attribute for product 2168 * @param string|null $location 2169 * @param string $upc Upc barcode 2170 * @param int|null $minimal_quantity Minimal quantity 2171 * @param string|null $available_date Date in mysql format Y-m-d 2172 * @param bool $update_all_fields 2173 * @param int[] $id_shop_list 2174 * @param string $isbn ISBN reference 2175 * @param int|null $low_stock_threshold Low stock for mail alert 2176 * @param bool $low_stock_alert Low stock mail alert activated 2177 * @param string $mpn MPN 2178 * 2179 * @return bool Update result 2180 */ 2181 public function updateAttribute( 2182 $id_product_attribute, 2183 $wholesale_price, 2184 $price, 2185 $weight, 2186 $unit, 2187 $ecotax, 2188 $id_images, 2189 $reference, 2190 $ean13, 2191 $default, 2192 $location = null, 2193 $upc = null, 2194 $minimal_quantity = null, 2195 $available_date = null, 2196 $update_all_fields = true, 2197 array $id_shop_list = [], 2198 $isbn = '', 2199 $low_stock_threshold = null, 2200 $low_stock_alert = false, 2201 $mpn = null 2202 ) { 2203 $combination = new Combination($id_product_attribute); 2204 2205 if (!$update_all_fields) { 2206 $combination->setFieldsToUpdate([ 2207 'price' => null !== $price, 2208 'wholesale_price' => null !== $wholesale_price, 2209 'ecotax' => null !== $ecotax, 2210 'weight' => null !== $weight, 2211 'unit_price_impact' => null !== $unit, 2212 'default_on' => null !== $default, 2213 'minimal_quantity' => null !== $minimal_quantity, 2214 'available_date' => null !== $available_date, 2215 ]); 2216 } 2217 2218 $price = str_replace(',', '.', $price); 2219 $weight = str_replace(',', '.', $weight); 2220 2221 $combination->price = (float) $price; 2222 $combination->wholesale_price = (float) $wholesale_price; 2223 $combination->ecotax = (float) $ecotax; 2224 $combination->weight = (float) $weight; 2225 $combination->unit_price_impact = (float) $unit; 2226 $combination->reference = pSQL($reference); 2227 $combination->location = pSQL($location); 2228 $combination->ean13 = pSQL($ean13); 2229 $combination->isbn = pSQL($isbn); 2230 $combination->upc = pSQL($upc); 2231 $combination->mpn = pSQL($mpn); 2232 $combination->default_on = (int) $default; 2233 $combination->minimal_quantity = (int) $minimal_quantity; 2234 $combination->low_stock_threshold = empty($low_stock_threshold) && '0' != $low_stock_threshold ? null : (int) $low_stock_threshold; 2235 $combination->low_stock_alert = !empty($low_stock_alert); 2236 $combination->available_date = $available_date ? pSQL($available_date) : '0000-00-00'; 2237 2238 if (count($id_shop_list)) { 2239 $combination->id_shop_list = $id_shop_list; 2240 } 2241 2242 $combination->save(); 2243 2244 if (is_array($id_images) && count($id_images)) { 2245 $combination->setImages($id_images); 2246 } 2247 2248 $id_default_attribute = (int) Product::updateDefaultAttribute($this->id); 2249 if ($id_default_attribute) { 2250 $this->cache_default_attribute = $id_default_attribute; 2251 } 2252 2253 // Sync stock Reference, EAN13, ISBN, MPN and UPC for this attribute 2254 if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && StockAvailable::dependsOnStock($this->id, Context::getContext()->shop->id)) { 2255 Db::getInstance()->update('stock', [ 2256 'reference' => pSQL($reference), 2257 'ean13' => pSQL($ean13), 2258 'isbn' => pSQL($isbn), 2259 'upc' => pSQL($upc), 2260 'mpn' => pSQL($mpn), 2261 ], 'id_product = ' . $this->id . ' AND id_product_attribute = ' . (int) $id_product_attribute); 2262 } 2263 2264 Hook::exec('actionProductAttributeUpdate', ['id_product_attribute' => (int) $id_product_attribute]); 2265 Tools::clearColorListCache($this->id); 2266 2267 return true; 2268 } 2269 2270 /** 2271 * Add a product attribute. 2272 * 2273 * @since 1.5.0.1 2274 * 2275 * @param float $price Additional price 2276 * @param float $weight Additional weight 2277 * @param float $unit_impact Additional unit price 2278 * @param float $ecotax Additional ecotax 2279 * @param int[] $id_images Image ids 2280 * @param string $reference Reference 2281 * @param string $ean13 Ean-13 barcode 2282 * @param bool $default Is default attribute for product 2283 * @param string $location Location 2284 * @param string|null $upc 2285 * @param int $minimal_quantity Minimal quantity to add to cart 2286 * @param int[] $id_shop_list 2287 * @param string|null $available_date Date in mysql format Y-m-d 2288 * @param int $quantity 2289 * @param string $isbn ISBN reference 2290 * @param int|null $low_stock_threshold Low stock for mail alert 2291 * @param bool $low_stock_alert Low stock mail alert activated 2292 * @param string|null $mpn 2293 * 2294 * @return int|false|void Attribute identifier if success, false if failed to add Combination, void if Product identifier not set 2295 */ 2296 public function addAttribute( 2297 $price, 2298 $weight, 2299 $unit_impact, 2300 $ecotax, 2301 $id_images, 2302 $reference, 2303 $ean13, 2304 $default, 2305 $location = null, 2306 $upc = null, 2307 $minimal_quantity = 1, 2308 array $id_shop_list = [], 2309 $available_date = null, 2310 $quantity = 0, 2311 $isbn = '', 2312 $low_stock_threshold = null, 2313 $low_stock_alert = false, 2314 $mpn = null 2315 ) { 2316 if (!$this->id) { 2317 return; 2318 } 2319 2320 $price = str_replace(',', '.', $price); 2321 $weight = str_replace(',', '.', $weight); 2322 2323 $combination = new Combination(); 2324 $combination->id_product = (int) $this->id; 2325 $combination->price = (float) $price; 2326 $combination->ecotax = (float) $ecotax; 2327 $combination->quantity = (int) $quantity; 2328 $combination->weight = (float) $weight; 2329 $combination->unit_price_impact = (float) $unit_impact; 2330 $combination->reference = pSQL($reference); 2331 $combination->location = pSQL($location); 2332 $combination->ean13 = pSQL($ean13); 2333 $combination->isbn = pSQL($isbn); 2334 $combination->upc = pSQL($upc); 2335 $combination->mpn = pSQL($mpn); 2336 $combination->default_on = (int) $default; 2337 $combination->minimal_quantity = (int) $minimal_quantity; 2338 $combination->low_stock_threshold = empty($low_stock_threshold) && '0' != $low_stock_threshold ? null : (int) $low_stock_threshold; 2339 $combination->low_stock_alert = !empty($low_stock_alert); 2340 $combination->available_date = $available_date; 2341 2342 if (count($id_shop_list)) { 2343 $combination->id_shop_list = array_unique($id_shop_list); 2344 } 2345 2346 $combination->add(); 2347 2348 if (!$combination->id) { 2349 return false; 2350 } 2351 2352 $total_quantity = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 2353 ' 2354 SELECT SUM(quantity) as quantity 2355 FROM ' . _DB_PREFIX_ . 'stock_available 2356 WHERE id_product = ' . (int) $this->id . ' 2357 AND id_product_attribute <> 0 ' 2358 ); 2359 2360 if (!$total_quantity) { 2361 Db::getInstance()->update('stock_available', ['quantity' => 0], '`id_product` = ' . $this->id); 2362 } 2363 2364 $id_default_attribute = Product::updateDefaultAttribute($this->id); 2365 2366 if ($id_default_attribute) { 2367 $this->cache_default_attribute = $id_default_attribute; 2368 if (!$combination->available_date) { 2369 $this->setAvailableDate(); 2370 } 2371 } 2372 $this->product_type = ProductType::TYPE_COMBINATIONS; 2373 2374 if (!empty($id_images)) { 2375 $combination->setImages($id_images); 2376 } 2377 2378 Tools::clearColorListCache($this->id); 2379 2380 if (Configuration::get('PS_DEFAULT_WAREHOUSE_NEW_PRODUCT') != 0 && Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')) { 2381 $warehouse_location_entity = new WarehouseProductLocation(); 2382 $warehouse_location_entity->id_product = $this->id; 2383 $warehouse_location_entity->id_product_attribute = (int) $combination->id; 2384 $warehouse_location_entity->id_warehouse = Configuration::get('PS_DEFAULT_WAREHOUSE_NEW_PRODUCT'); 2385 $warehouse_location_entity->location = pSQL(''); 2386 $warehouse_location_entity->save(); 2387 } 2388 2389 return (int) $combination->id; 2390 } 2391 2392 /** 2393 * @deprecated since 1.5.0 2394 * 2395 * @return bool 2396 */ 2397 public function updateQuantityProductWithAttributeQuantity() 2398 { 2399 Tools::displayAsDeprecated(); 2400 2401 return Db::getInstance()->execute(' 2402 UPDATE `' . _DB_PREFIX_ . 'product` 2403 SET `quantity` = IFNULL( 2404 ( 2405 SELECT SUM(`quantity`) 2406 FROM `' . _DB_PREFIX_ . 'product_attribute` 2407 WHERE `id_product` = ' . (int) $this->id . ' 2408 ), \'0\') 2409 WHERE `id_product` = ' . (int) $this->id); 2410 } 2411 2412 /** 2413 * Delete product attributes. 2414 * 2415 * @return bool Deletion result 2416 */ 2417 public function deleteProductAttributes() 2418 { 2419 Hook::exec('actionProductAttributeDelete', ['id_product_attribute' => 0, 'id_product' => (int) $this->id, 'deleteAllAttributes' => true]); 2420 2421 $result = true; 2422 $combinations = new PrestaShopCollection('Combination'); 2423 $combinations->where('id_product', '=', $this->id); 2424 foreach ($combinations as $combination) { 2425 $result &= $combination->delete(); 2426 } 2427 SpecificPriceRule::applyAllRules([(int) $this->id]); 2428 Tools::clearColorListCache($this->id); 2429 2430 return $result; 2431 } 2432 2433 /** 2434 * Delete product attributes impacts. 2435 * 2436 * @return bool 2437 */ 2438 public function deleteAttributesImpacts() 2439 { 2440 return Db::getInstance()->execute( 2441 'DELETE FROM `' . _DB_PREFIX_ . 'attribute_impact` 2442 WHERE `id_product` = ' . (int) $this->id 2443 ); 2444 } 2445 2446 /** 2447 * Delete product features. 2448 * 2449 * @return bool Deletion result 2450 */ 2451 public function deleteProductFeatures() 2452 { 2453 SpecificPriceRule::applyAllRules([(int) $this->id]); 2454 2455 return $this->deleteFeatures(); 2456 } 2457 2458 /** 2459 * @param int $id_product Product identifier 2460 * 2461 * @return bool 2462 */ 2463 public static function updateCacheAttachment($id_product) 2464 { 2465 $value = (bool) Db::getInstance()->getValue(' 2466 SELECT id_attachment 2467 FROM ' . _DB_PREFIX_ . 'product_attachment 2468 WHERE id_product=' . (int) $id_product); 2469 2470 return Db::getInstance()->update( 2471 'product', 2472 ['cache_has_attachments' => (int) $value], 2473 'id_product = ' . (int) $id_product 2474 ); 2475 } 2476 2477 /** 2478 * Delete product attachments. 2479 * 2480 * @param bool $update_attachment_cache If set to true attachment cache will be updated 2481 * 2482 * @return bool Deletion result 2483 */ 2484 public function deleteAttachments($update_attachment_cache = true) 2485 { 2486 $res = Db::getInstance()->execute( 2487 ' 2488 DELETE FROM `' . _DB_PREFIX_ . 'product_attachment` 2489 WHERE `id_product` = ' . (int) $this->id 2490 ); 2491 2492 if (isset($update_attachment_cache) && (bool) $update_attachment_cache === true) { 2493 Product::updateCacheAttachment((int) $this->id); 2494 } 2495 2496 return $res; 2497 } 2498 2499 /** 2500 * Delete product customizations. 2501 * 2502 * @return bool Deletion result 2503 */ 2504 public function deleteCustomization() 2505 { 2506 return 2507 Db::getInstance()->execute( 2508 'DELETE FROM `' . _DB_PREFIX_ . 'customization_field` 2509 WHERE `id_product` = ' . (int) $this->id 2510 ) 2511 && 2512 Db::getInstance()->execute( 2513 'DELETE `' . _DB_PREFIX_ . 'customization_field_lang` FROM `' . _DB_PREFIX_ . 'customization_field_lang` LEFT JOIN `' . _DB_PREFIX_ . 'customization_field` 2514 ON (' . _DB_PREFIX_ . 'customization_field.id_customization_field = ' . _DB_PREFIX_ . 'customization_field_lang.id_customization_field) 2515 WHERE ' . _DB_PREFIX_ . 'customization_field.id_customization_field IS NULL' 2516 ); 2517 } 2518 2519 /** 2520 * Delete product pack details. 2521 * 2522 * @return bool Deletion result 2523 */ 2524 public function deletePack() 2525 { 2526 return Db::getInstance()->execute( 2527 'DELETE FROM `' . _DB_PREFIX_ . 'pack` 2528 WHERE `id_product_pack` = ' . (int) $this->id . ' 2529 OR `id_product_item` = ' . (int) $this->id 2530 ); 2531 } 2532 2533 /** 2534 * Delete product sales. 2535 * 2536 * @return bool Deletion result 2537 */ 2538 public function deleteProductSale() 2539 { 2540 return Db::getInstance()->execute( 2541 'DELETE FROM `' . _DB_PREFIX_ . 'product_sale` 2542 WHERE `id_product` = ' . (int) $this->id 2543 ); 2544 } 2545 2546 /** 2547 * Delete product indexed words. 2548 * 2549 * @return bool Deletion result 2550 */ 2551 public function deleteSearchIndexes() 2552 { 2553 return 2554 Db::getInstance()->execute( 2555 'DELETE FROM `' . _DB_PREFIX_ . 'search_index` 2556 WHERE `id_product` = ' . (int) $this->id 2557 ) && 2558 Db::getInstance()->execute( 2559 'DELETE sw FROM `' . _DB_PREFIX_ . 'search_word` sw 2560 LEFT JOIN `' . _DB_PREFIX_ . 'search_index` si ON (sw.id_word=si.id_word) 2561 WHERE si.id_word IS NULL;' 2562 ); 2563 } 2564 2565 /** 2566 * Add a product attributes combinaison. 2567 * 2568 * @deprecated since 1.5.0.7 2569 * 2570 * @param int $id_product_attribute Attribute identifier 2571 * @param array $attributes Attributes to forge combinaison 2572 * 2573 * @return bool Insertion result 2574 */ 2575 public function addAttributeCombinaison($id_product_attribute, $attributes) 2576 { 2577 Tools::displayAsDeprecated(); 2578 if (!is_array($attributes)) { 2579 die(Tools::displayError()); 2580 } 2581 if (!count($attributes)) { 2582 return false; 2583 } 2584 2585 $combination = new Combination((int) $id_product_attribute); 2586 2587 return $combination->setAttributes($attributes); 2588 } 2589 2590 /** 2591 * @deprecated 1.5.5.0 2592 * 2593 * @param array $id_attributes 2594 * @param array $combinations 2595 * 2596 * @return bool 2597 * 2598 * @throws PrestaShopDatabaseException 2599 */ 2600 public function addAttributeCombinationMultiple($id_attributes, $combinations) 2601 { 2602 Tools::displayAsDeprecated(); 2603 $attributes_list = []; 2604 foreach ($id_attributes as $nb => $id_product_attribute) { 2605 if (isset($combinations[$nb])) { 2606 foreach ($combinations[$nb] as $id_attribute) { 2607 $attributes_list[] = [ 2608 'id_product_attribute' => (int) $id_product_attribute, 2609 'id_attribute' => (int) $id_attribute, 2610 ]; 2611 } 2612 } 2613 } 2614 2615 return Db::getInstance()->insert('product_attribute_combination', $attributes_list); 2616 } 2617 2618 /** 2619 * Delete a product attributes combination. 2620 * 2621 * @param int $id_product_attribute Attribute identifier 2622 * 2623 * @return bool Deletion result 2624 */ 2625 public function deleteAttributeCombination($id_product_attribute) 2626 { 2627 if (!$this->id || !$id_product_attribute || !is_numeric($id_product_attribute)) { 2628 return false; 2629 } 2630 2631 Hook::exec( 2632 'deleteProductAttribute', 2633 [ 2634 'id_product_attribute' => $id_product_attribute, 2635 'id_product' => $this->id, 2636 'deleteAllAttributes' => false, 2637 ] 2638 ); 2639 2640 $combination = new Combination($id_product_attribute); 2641 $res = $combination->delete(); 2642 SpecificPriceRule::applyAllRules([(int) $this->id]); 2643 2644 return $res; 2645 } 2646 2647 /** 2648 * Delete features. 2649 * 2650 * @return bool 2651 */ 2652 public function deleteFeatures() 2653 { 2654 $all_shops = Context::getContext()->shop->getContext() == Shop::CONTEXT_ALL ? true : false; 2655 2656 // List products features 2657 $features = Db::getInstance()->executeS( 2658 ' 2659 SELECT p.*, f.* 2660 FROM `' . _DB_PREFIX_ . 'feature_product` as p 2661 LEFT JOIN `' . _DB_PREFIX_ . 'feature_value` as f ON (f.`id_feature_value` = p.`id_feature_value`) 2662 ' . (!$all_shops ? 'LEFT JOIN `' . _DB_PREFIX_ . 'feature_shop` fs ON (f.`id_feature` = fs.`id_feature`)' : null) . ' 2663 WHERE `id_product` = ' . (int) $this->id 2664 . (!$all_shops ? ' AND fs.`id_shop` = ' . (int) Context::getContext()->shop->id : '') 2665 ); 2666 2667 foreach ($features as $tab) { 2668 // Delete product custom features 2669 if ($tab['custom']) { 2670 Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'feature_value` WHERE `id_feature_value` = ' . (int) $tab['id_feature_value']); 2671 Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'feature_value_lang` WHERE `id_feature_value` = ' . (int) $tab['id_feature_value']); 2672 } 2673 } 2674 // Delete product features 2675 $result = Db::getInstance()->execute(' 2676 DELETE `' . _DB_PREFIX_ . 'feature_product` FROM `' . _DB_PREFIX_ . 'feature_product` 2677 WHERE `id_product` = ' . (int) $this->id . (!$all_shops ? ' 2678 AND `id_feature` IN ( 2679 SELECT `id_feature` 2680 FROM `' . _DB_PREFIX_ . 'feature_shop` 2681 WHERE `id_shop` = ' . (int) Context::getContext()->shop->id . ' 2682 )' : '')); 2683 2684 SpecificPriceRule::applyAllRules([(int) $this->id]); 2685 2686 return $result; 2687 } 2688 2689 /** 2690 * Get all available product attributes resume. 2691 * 2692 * @param int $id_lang Language identifier 2693 * @param string $attribute_value_separator 2694 * @param string $attribute_separator 2695 * 2696 * @return array Product attributes combinations 2697 */ 2698 public function getAttributesResume($id_lang, $attribute_value_separator = ' - ', $attribute_separator = ', ') 2699 { 2700 if (!Combination::isFeatureActive()) { 2701 return []; 2702 } 2703 2704 $combinations = Db::getInstance()->executeS('SELECT pa.*, product_attribute_shop.* 2705 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 2706 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 2707 WHERE pa.`id_product` = ' . (int) $this->id . ' 2708 GROUP BY pa.`id_product_attribute` 2709 ORDER BY pa.`id_product_attribute`'); 2710 2711 if (!$combinations) { 2712 return false; 2713 } 2714 2715 $product_attributes = []; 2716 foreach ($combinations as $combination) { 2717 $product_attributes[] = (int) $combination['id_product_attribute']; 2718 } 2719 2720 $lang = Db::getInstance()->executeS('SELECT pac.id_product_attribute, GROUP_CONCAT(agl.`name`, \'' . pSQL($attribute_value_separator) . '\',al.`name` ORDER BY agl.`id_attribute_group` SEPARATOR \'' . pSQL($attribute_separator) . '\') as attribute_designation 2721 FROM `' . _DB_PREFIX_ . 'product_attribute_combination` pac 2722 LEFT JOIN `' . _DB_PREFIX_ . 'attribute` a ON a.`id_attribute` = pac.`id_attribute` 2723 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group` ag ON ag.`id_attribute_group` = a.`id_attribute_group` 2724 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) $id_lang . ') 2725 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) $id_lang . ') 2726 WHERE pac.id_product_attribute IN (' . implode(',', $product_attributes) . ') 2727 GROUP BY pac.id_product_attribute 2728 ORDER BY pac.id_product_attribute'); 2729 2730 foreach ($lang as $k => $row) { 2731 $combinations[$k]['attribute_designation'] = $row['attribute_designation']; 2732 } 2733 2734 $computingPrecision = Context::getContext()->getComputingPrecision(); 2735 //Get quantity of each variations 2736 foreach ($combinations as $key => $row) { 2737 $cache_key = $row['id_product'] . '_' . $row['id_product_attribute'] . '_quantity'; 2738 2739 if (!Cache::isStored($cache_key)) { 2740 $result = StockAvailable::getQuantityAvailableByProduct($row['id_product'], $row['id_product_attribute']); 2741 Cache::store( 2742 $cache_key, 2743 $result 2744 ); 2745 $combinations[$key]['quantity'] = $result; 2746 } else { 2747 $combinations[$key]['quantity'] = Cache::retrieve($cache_key); 2748 } 2749 2750 $ecotax = (float) $combinations[$key]['ecotax'] ?? 0; 2751 $combinations[$key]['ecotax_tax_excluded'] = $ecotax; 2752 $combinations[$key]['ecotax_tax_included'] = Tools::ps_round($ecotax * (1 + Tax::getProductEcotaxRate() / 100), $computingPrecision); 2753 } 2754 2755 return $combinations; 2756 } 2757 2758 /** 2759 * Get all available product attributes combinations. 2760 * 2761 * @param int|null $id_lang Language identifier 2762 * @param bool $groupByIdAttributeGroup 2763 * 2764 * @return array Product attributes combinations 2765 */ 2766 public function getAttributeCombinations($id_lang = null, $groupByIdAttributeGroup = true) 2767 { 2768 if (!Combination::isFeatureActive()) { 2769 return []; 2770 } 2771 if (null === $id_lang) { 2772 $id_lang = Context::getContext()->language->id; 2773 } 2774 2775 $sql = 'SELECT pa.*, product_attribute_shop.*, ag.`id_attribute_group`, ag.`is_color_group`, agl.`name` AS group_name, al.`name` AS attribute_name, 2776 a.`id_attribute` 2777 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 2778 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 2779 LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON pac.`id_product_attribute` = pa.`id_product_attribute` 2780 LEFT JOIN `' . _DB_PREFIX_ . 'attribute` a ON a.`id_attribute` = pac.`id_attribute` 2781 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group` ag ON ag.`id_attribute_group` = a.`id_attribute_group` 2782 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) $id_lang . ') 2783 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) $id_lang . ') 2784 WHERE pa.`id_product` = ' . (int) $this->id . ' 2785 GROUP BY pa.`id_product_attribute`' . ($groupByIdAttributeGroup ? ',ag.`id_attribute_group`' : '') . ' 2786 ORDER BY pa.`id_product_attribute`'; 2787 2788 $res = Db::getInstance()->executeS($sql); 2789 2790 //Get quantity of each variations 2791 foreach ($res as $key => $row) { 2792 $cache_key = $row['id_product'] . '_' . $row['id_product_attribute'] . '_quantity'; 2793 2794 if (!Cache::isStored($cache_key)) { 2795 Cache::store( 2796 $cache_key, 2797 StockAvailable::getQuantityAvailableByProduct($row['id_product'], $row['id_product_attribute']) 2798 ); 2799 } 2800 2801 $res[$key]['quantity'] = Cache::retrieve($cache_key); 2802 } 2803 2804 return $res; 2805 } 2806 2807 /** 2808 * Get product attribute combination by id_product_attribute. 2809 * 2810 * @param int $id_product_attribute Attribute identifier 2811 * @param int $id_lang Language identifier 2812 * @param bool $groupByIdAttributeGroup 2813 * 2814 * @return array Product attribute combination by id_product_attribute 2815 */ 2816 public function getAttributeCombinationsById($id_product_attribute, $id_lang, $groupByIdAttributeGroup = true) 2817 { 2818 if (!Combination::isFeatureActive()) { 2819 return []; 2820 } 2821 $sql = 'SELECT pa.*, product_attribute_shop.*, ag.`id_attribute_group`, ag.`is_color_group`, agl.`name` AS group_name, al.`name` AS attribute_name, 2822 a.`id_attribute`, a.`position` 2823 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 2824 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 2825 LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON pac.`id_product_attribute` = pa.`id_product_attribute` 2826 LEFT JOIN `' . _DB_PREFIX_ . 'attribute` a ON a.`id_attribute` = pac.`id_attribute` 2827 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group` ag ON ag.`id_attribute_group` = a.`id_attribute_group` 2828 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) $id_lang . ') 2829 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) $id_lang . ') 2830 WHERE pa.`id_product` = ' . (int) $this->id . ' 2831 AND pa.`id_product_attribute` = ' . (int) $id_product_attribute . ' 2832 GROUP BY pa.`id_product_attribute`' . ($groupByIdAttributeGroup ? ',ag.`id_attribute_group`' : '') . ' 2833 ORDER BY pa.`id_product_attribute`'; 2834 2835 $res = Db::getInstance()->executeS($sql); 2836 2837 $computingPrecision = Context::getContext()->getComputingPrecision(); 2838 //Get quantity of each variations 2839 foreach ($res as $key => $row) { 2840 $cache_key = $row['id_product'] . '_' . $row['id_product_attribute'] . '_quantity'; 2841 2842 if (!Cache::isStored($cache_key)) { 2843 $result = StockAvailable::getQuantityAvailableByProduct($row['id_product'], $row['id_product_attribute']); 2844 Cache::store( 2845 $cache_key, 2846 $result 2847 ); 2848 $res[$key]['quantity'] = $result; 2849 } else { 2850 $res[$key]['quantity'] = Cache::retrieve($cache_key); 2851 } 2852 2853 $ecotax = (float) $res[$key]['ecotax'] ?? 0; 2854 $res[$key]['ecotax_tax_excluded'] = $ecotax; 2855 $res[$key]['ecotax_tax_included'] = Tools::ps_round($ecotax * (1 + Tax::getProductEcotaxRate() / 100), $computingPrecision); 2856 } 2857 2858 return $res; 2859 } 2860 2861 /** 2862 * @param int $id_lang Language identifier 2863 * 2864 * @return array|false 2865 */ 2866 public function getCombinationImages($id_lang) 2867 { 2868 if (!Combination::isFeatureActive()) { 2869 return false; 2870 } 2871 2872 $product_attributes = Db::getInstance()->executeS( 2873 'SELECT `id_product_attribute` 2874 FROM `' . _DB_PREFIX_ . 'product_attribute` 2875 WHERE `id_product` = ' . (int) $this->id 2876 ); 2877 2878 if (!$product_attributes) { 2879 return false; 2880 } 2881 2882 $ids = []; 2883 2884 foreach ($product_attributes as $product_attribute) { 2885 $ids[] = (int) $product_attribute['id_product_attribute']; 2886 } 2887 2888 $result = Db::getInstance()->executeS( 2889 ' 2890 SELECT pai.`id_image`, pai.`id_product_attribute`, il.`legend` 2891 FROM `' . _DB_PREFIX_ . 'product_attribute_image` pai 2892 LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (il.`id_image` = pai.`id_image`) 2893 LEFT JOIN `' . _DB_PREFIX_ . 'image` i ON (i.`id_image` = pai.`id_image`) 2894 WHERE pai.`id_product_attribute` IN (' . implode(', ', $ids) . ') AND il.`id_lang` = ' . (int) $id_lang . ' ORDER by i.`position`' 2895 ); 2896 2897 if (!$result) { 2898 return false; 2899 } 2900 2901 $images = []; 2902 2903 foreach ($result as $row) { 2904 $images[$row['id_product_attribute']][] = $row; 2905 } 2906 2907 return $images; 2908 } 2909 2910 /** 2911 * @param int $id_product_attribute Attribute identifier 2912 * @param int $id_lang Language identifier 2913 * 2914 * @return array|false 2915 */ 2916 public static function getCombinationImageById($id_product_attribute, $id_lang) 2917 { 2918 if (!Combination::isFeatureActive() || !$id_product_attribute) { 2919 return false; 2920 } 2921 2922 $result = Db::getInstance()->executeS( 2923 ' 2924 SELECT pai.`id_image`, pai.`id_product_attribute`, il.`legend` 2925 FROM `' . _DB_PREFIX_ . 'product_attribute_image` pai 2926 LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (il.`id_image` = pai.`id_image`) 2927 LEFT JOIN `' . _DB_PREFIX_ . 'image` i ON (i.`id_image` = pai.`id_image`) 2928 WHERE pai.`id_product_attribute` = ' . (int) $id_product_attribute . ' AND il.`id_lang` = ' . (int) $id_lang . ' ORDER by i.`position` LIMIT 1' 2929 ); 2930 2931 if (!$result) { 2932 return false; 2933 } 2934 2935 return $result[0]; 2936 } 2937 2938 /** 2939 * Check if product has attributes combinations. 2940 * 2941 * @return int Attributes combinations number 2942 */ 2943 public function hasAttributes() 2944 { 2945 if (!Combination::isFeatureActive()) { 2946 return 0; 2947 } 2948 2949 return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 2950 ' 2951 SELECT COUNT(*) 2952 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 2953 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 2954 WHERE pa.`id_product` = ' . (int) $this->id 2955 ); 2956 } 2957 2958 /** 2959 * Get new products. 2960 * 2961 * @param int $id_lang Language identifier 2962 * @param int $page_number Start from 2963 * @param int $nb_products Number of products to return 2964 * @param bool $count 2965 * @param string|null $order_by 2966 * @param string|null $order_way 2967 * @param Context|null $context 2968 * 2969 * @return array|int|false New products, total of product if $count is true, false if it fail 2970 */ 2971 public static function getNewProducts($id_lang, $page_number = 0, $nb_products = 10, $count = false, $order_by = null, $order_way = null, Context $context = null) 2972 { 2973 $now = date('Y-m-d') . ' 00:00:00'; 2974 if (!$context) { 2975 $context = Context::getContext(); 2976 } 2977 2978 $front = true; 2979 if (!in_array($context->controller->controller_type, ['front', 'modulefront'])) { 2980 $front = false; 2981 } 2982 2983 if ($page_number < 1) { 2984 $page_number = 1; 2985 } 2986 if ($nb_products < 1) { 2987 $nb_products = 10; 2988 } 2989 if (empty($order_by) || $order_by == 'position') { 2990 $order_by = 'date_add'; 2991 } 2992 if (empty($order_way)) { 2993 $order_way = 'DESC'; 2994 } 2995 if ($order_by == 'id_product' || $order_by == 'price' || $order_by == 'date_add' || $order_by == 'date_upd') { 2996 $order_by_prefix = 'product_shop'; 2997 } elseif ($order_by == 'name') { 2998 $order_by_prefix = 'pl'; 2999 } 3000 if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay($order_way)) { 3001 die(Tools::displayError()); 3002 } 3003 3004 $sql_groups = ''; 3005 if (Group::isFeatureActive()) { 3006 $groups = FrontController::getCurrentCustomerGroups(); 3007 $sql_groups = ' AND EXISTS(SELECT 1 FROM `' . _DB_PREFIX_ . 'category_product` cp 3008 JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cp.id_category = cg.id_category AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id) . ') 3009 WHERE cp.`id_product` = p.`id_product`)'; 3010 } 3011 3012 if (strpos($order_by, '.') > 0) { 3013 $order_by = explode('.', $order_by); 3014 $order_by_prefix = $order_by[0]; 3015 $order_by = $order_by[1]; 3016 } 3017 3018 $nb_days_new_product = (int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT'); 3019 3020 if ($count) { 3021 $sql = 'SELECT COUNT(p.`id_product`) AS nb 3022 FROM `' . _DB_PREFIX_ . 'product` p 3023 ' . Shop::addSqlAssociation('product', 'p') . ' 3024 WHERE product_shop.`active` = 1 3025 AND product_shop.`date_add` > "' . date('Y-m-d', strtotime('-' . $nb_days_new_product . ' DAY')) . '" 3026 ' . ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . ' 3027 ' . $sql_groups; 3028 3029 return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql); 3030 } 3031 $sql = new DbQuery(); 3032 $sql->select( 3033 'p.*, product_shop.*, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`link_rewrite`, pl.`meta_description`, 3034 pl.`meta_keywords`, pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`, image_shop.`id_image` id_image, il.`legend`, m.`name` AS manufacturer_name, 3035 (DATEDIFF(product_shop.`date_add`, 3036 DATE_SUB( 3037 "' . $now . '", 3038 INTERVAL ' . $nb_days_new_product . ' DAY 3039 ) 3040 ) > 0) as new' 3041 ); 3042 3043 $sql->from('product', 'p'); 3044 $sql->join(Shop::addSqlAssociation('product', 'p')); 3045 $sql->leftJoin( 3046 'product_lang', 3047 'pl', 3048 ' 3049 p.`id_product` = pl.`id_product` 3050 AND pl.`id_lang` = ' . (int) $id_lang . Shop::addSqlRestrictionOnLang('pl') 3051 ); 3052 $sql->leftJoin('image_shop', 'image_shop', 'image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop=' . (int) $context->shop->id); 3053 $sql->leftJoin('image_lang', 'il', 'image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $id_lang); 3054 $sql->leftJoin('manufacturer', 'm', 'm.`id_manufacturer` = p.`id_manufacturer`'); 3055 3056 $sql->where('product_shop.`active` = 1'); 3057 if ($front) { 3058 $sql->where('product_shop.`visibility` IN ("both", "catalog")'); 3059 } 3060 $sql->where('product_shop.`date_add` > "' . date('Y-m-d', strtotime('-' . $nb_days_new_product . ' DAY')) . '"'); 3061 if (Group::isFeatureActive()) { 3062 $groups = FrontController::getCurrentCustomerGroups(); 3063 $sql->where('EXISTS(SELECT 1 FROM `' . _DB_PREFIX_ . 'category_product` cp 3064 JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cp.id_category = cg.id_category AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id) . ') 3065 WHERE cp.`id_product` = p.`id_product`)'); 3066 } 3067 3068 if ($order_by !== 'price') { 3069 $sql->orderBy((isset($order_by_prefix) ? pSQL($order_by_prefix) . '.' : '') . '`' . pSQL($order_by) . '` ' . pSQL($order_way)); 3070 $sql->limit($nb_products, (int) (($page_number - 1) * $nb_products)); 3071 } 3072 3073 if (Combination::isFeatureActive()) { 3074 $sql->select('product_attribute_shop.minimal_quantity AS product_attribute_minimal_quantity, IFNULL(product_attribute_shop.id_product_attribute,0) id_product_attribute'); 3075 $sql->leftJoin('product_attribute_shop', 'product_attribute_shop', 'p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shop.id_shop=' . (int) $context->shop->id); 3076 } 3077 $sql->join(Product::sqlStock('p', 0)); 3078 3079 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); 3080 3081 if (!$result) { 3082 return false; 3083 } 3084 3085 if ($order_by === 'price') { 3086 Tools::orderbyPrice($result, $order_way); 3087 $result = array_slice($result, (int) (($nb_products - 1) * $page_number), (int) $page_number); 3088 } 3089 $products_ids = []; 3090 foreach ($result as $row) { 3091 $products_ids[] = $row['id_product']; 3092 } 3093 // Thus you can avoid one query per product, because there will be only one query for all the products of the cart 3094 Product::cacheFrontFeatures($products_ids, $id_lang); 3095 3096 return Product::getProductsProperties((int) $id_lang, $result); 3097 } 3098 3099 /** 3100 * @param string $beginning Date in mysql format Y-m-d 3101 * @param string $ending Date in mysql format Y-m-d 3102 * @param Context|null $context 3103 * @param bool $with_combination 3104 * 3105 * @return array 3106 */ 3107 protected static function _getProductIdByDate($beginning, $ending, Context $context = null, $with_combination = false) 3108 { 3109 if (!$context) { 3110 $context = Context::getContext(); 3111 } 3112 3113 $id_address = $context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}; 3114 $ids = Address::getCountryAndState($id_address); 3115 $id_country = isset($ids['id_country']) ? (int) $ids['id_country'] : (int) Configuration::get('PS_COUNTRY_DEFAULT'); 3116 3117 return SpecificPrice::getProductIdByDate( 3118 $context->shop->id, 3119 $context->currency->id, 3120 $id_country, 3121 $context->customer->id_default_group, 3122 $beginning, 3123 $ending, 3124 0, 3125 $with_combination 3126 ); 3127 } 3128 3129 /** 3130 * Get a random special. 3131 * 3132 * @param int $id_lang Language identifier 3133 * @param string|false $beginning Date in mysql format Y-m-d 3134 * @param string|false $ending Date in mysql format Y-m-d 3135 * @param Context|null $context 3136 * 3137 * @return array|false Special 3138 */ 3139 public static function getRandomSpecial($id_lang, $beginning = false, $ending = false, Context $context = null) 3140 { 3141 if (!$context) { 3142 $context = Context::getContext(); 3143 } 3144 3145 $front = true; 3146 if (!in_array($context->controller->controller_type, ['front', 'modulefront'])) { 3147 $front = false; 3148 } 3149 3150 $current_date = date('Y-m-d H:i:00'); 3151 $product_reductions = Product::_getProductIdByDate((!$beginning ? $current_date : $beginning), (!$ending ? $current_date : $ending), $context, true); 3152 3153 if ($product_reductions) { 3154 $ids_products = ''; 3155 foreach ($product_reductions as $product_reduction) { 3156 $ids_products .= '(' . (int) $product_reduction['id_product'] . ',' . ($product_reduction['id_product_attribute'] ? (int) $product_reduction['id_product_attribute'] : '0') . '),'; 3157 } 3158 3159 $ids_products = rtrim($ids_products, ','); 3160 Db::getInstance()->execute('CREATE TEMPORARY TABLE `' . _DB_PREFIX_ . 'product_reductions` (id_product INT UNSIGNED NOT NULL DEFAULT 0, id_product_attribute INT UNSIGNED NOT NULL DEFAULT 0) ENGINE=MEMORY', false); 3161 if ($ids_products) { 3162 Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'product_reductions` VALUES ' . $ids_products, false); 3163 } 3164 3165 $groups = FrontController::getCurrentCustomerGroups(); 3166 $sql_groups = ' AND EXISTS(SELECT 1 FROM `' . _DB_PREFIX_ . 'category_product` cp 3167 JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cp.id_category = cg.id_category AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id) . ') 3168 WHERE cp.`id_product` = p.`id_product`)'; 3169 3170 // Please keep 2 distinct queries because RAND() is an awful way to achieve this result 3171 $sql = 'SELECT product_shop.id_product, IFNULL(product_attribute_shop.id_product_attribute,0) id_product_attribute 3172 FROM 3173 `' . _DB_PREFIX_ . 'product_reductions` pr, 3174 `' . _DB_PREFIX_ . 'product` p 3175 ' . Shop::addSqlAssociation('product', 'p') . ' 3176 LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` product_attribute_shop 3177 ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shop.id_shop=' . (int) $context->shop->id . ') 3178 WHERE p.id_product=pr.id_product AND (pr.id_product_attribute = 0 OR product_attribute_shop.id_product_attribute = pr.id_product_attribute) AND product_shop.`active` = 1 3179 ' . $sql_groups . ' 3180 ' . ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . ' 3181 ORDER BY RAND()'; 3182 3183 $result = Db::getInstance()->getRow($sql); 3184 3185 Db::getInstance()->execute('DROP TEMPORARY TABLE `' . _DB_PREFIX_ . 'product_reductions`', false); 3186 3187 if (!$id_product = $result['id_product']) { 3188 return false; 3189 } 3190 3191 // no group by needed : there's only one attribute with cover=1 for a given id_product + shop 3192 $sql = 'SELECT p.*, product_shop.*, stock.`out_of_stock` out_of_stock, pl.`description`, pl.`description_short`, 3193 pl.`link_rewrite`, pl.`meta_description`, pl.`meta_keywords`, pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`, 3194 p.`ean13`, p.`isbn`, p.`upc`, p.`mpn`, image_shop.`id_image` id_image, il.`legend`, 3195 DATEDIFF(product_shop.`date_add`, DATE_SUB("' . date('Y-m-d') . ' 00:00:00", 3196 INTERVAL ' . (Validate::isUnsignedInt(Configuration::get('PS_NB_DAYS_NEW_PRODUCT')) ? Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20) . ' 3197 DAY)) > 0 AS new 3198 FROM `' . _DB_PREFIX_ . 'product` p 3199 LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON ( 3200 p.`id_product` = pl.`id_product` 3201 AND pl.`id_lang` = ' . (int) $id_lang . Shop::addSqlRestrictionOnLang('pl') . ' 3202 ) 3203 ' . Shop::addSqlAssociation('product', 'p') . ' 3204 LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop 3205 ON (image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop=' . (int) $context->shop->id . ') 3206 LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $id_lang . ') 3207 ' . Product::sqlStock('p', 0) . ' 3208 WHERE p.id_product = ' . (int) $id_product; 3209 3210 $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql); 3211 if (!$row) { 3212 return false; 3213 } 3214 3215 $row['id_product_attribute'] = (int) $result['id_product_attribute']; 3216 3217 return Product::getProductProperties($id_lang, $row); 3218 } else { 3219 return false; 3220 } 3221 } 3222 3223 /** 3224 * Get prices drop. 3225 * 3226 * @param int $id_lang Language identifier 3227 * @param int $page_number Start from 3228 * @param int $nb_products Number of products to return 3229 * @param bool $count Only in order to get total number 3230 * @param string|null $order_by 3231 * @param string|null $order_way 3232 * @param string|false $beginning Date in mysql format Y-m-d 3233 * @param string|false $ending Date in mysql format Y-m-d 3234 * @param Context|null $context 3235 * 3236 * @return array|false 3237 */ 3238 public static function getPricesDrop( 3239 $id_lang, 3240 $page_number = 0, 3241 $nb_products = 10, 3242 $count = false, 3243 $order_by = null, 3244 $order_way = null, 3245 $beginning = false, 3246 $ending = false, 3247 Context $context = null 3248 ) { 3249 if (!Validate::isBool($count)) { 3250 die(Tools::displayError()); 3251 } 3252 3253 if (!$context) { 3254 $context = Context::getContext(); 3255 } 3256 if ($page_number < 1) { 3257 $page_number = 1; 3258 } 3259 if ($nb_products < 1) { 3260 $nb_products = 10; 3261 } 3262 if (empty($order_by) || $order_by == 'position') { 3263 $order_by = 'price'; 3264 } 3265 if (empty($order_way)) { 3266 $order_way = 'DESC'; 3267 } 3268 if ($order_by == 'id_product' || $order_by == 'price' || $order_by == 'date_add' || $order_by == 'date_upd') { 3269 $order_by_prefix = 'product_shop'; 3270 } elseif ($order_by == 'name') { 3271 $order_by_prefix = 'pl'; 3272 } 3273 if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay($order_way)) { 3274 die(Tools::displayError()); 3275 } 3276 $current_date = date('Y-m-d H:i:00'); 3277 $ids_product = Product::_getProductIdByDate((!$beginning ? $current_date : $beginning), (!$ending ? $current_date : $ending), $context); 3278 3279 $tab_id_product = []; 3280 foreach ($ids_product as $product) { 3281 if (is_array($product)) { 3282 $tab_id_product[] = (int) $product['id_product']; 3283 } else { 3284 $tab_id_product[] = (int) $product; 3285 } 3286 } 3287 3288 $front = true; 3289 if (!in_array($context->controller->controller_type, ['front', 'modulefront'])) { 3290 $front = false; 3291 } 3292 3293 $sql_groups = ''; 3294 if (Group::isFeatureActive()) { 3295 $groups = FrontController::getCurrentCustomerGroups(); 3296 $sql_groups = ' AND EXISTS(SELECT 1 FROM `' . _DB_PREFIX_ . 'category_product` cp 3297 JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cp.id_category = cg.id_category AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id) . ') 3298 WHERE cp.`id_product` = p.`id_product`)'; 3299 } 3300 3301 if ($count) { 3302 return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(' 3303 SELECT COUNT(DISTINCT p.`id_product`) 3304 FROM `' . _DB_PREFIX_ . 'product` p 3305 ' . Shop::addSqlAssociation('product', 'p') . ' 3306 WHERE product_shop.`active` = 1 3307 AND product_shop.`show_price` = 1 3308 ' . ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . ' 3309 ' . ((!$beginning && !$ending) ? 'AND p.`id_product` IN(' . ((is_array($tab_id_product) && count($tab_id_product)) ? implode(', ', $tab_id_product) : 0) . ')' : '') . ' 3310 ' . $sql_groups); 3311 } 3312 3313 if (strpos($order_by, '.') > 0) { 3314 $order_by = explode('.', $order_by); 3315 $order_by = pSQL($order_by[0]) . '.`' . pSQL($order_by[1]) . '`'; 3316 } 3317 3318 $sql = ' 3319 SELECT 3320 p.*, product_shop.*, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`available_now`, pl.`available_later`, 3321 IFNULL(product_attribute_shop.id_product_attribute, 0) id_product_attribute, 3322 pl.`link_rewrite`, pl.`meta_description`, pl.`meta_keywords`, pl.`meta_title`, 3323 pl.`name`, image_shop.`id_image` id_image, il.`legend`, m.`name` AS manufacturer_name, 3324 DATEDIFF( 3325 p.`date_add`, 3326 DATE_SUB( 3327 "' . date('Y-m-d') . ' 00:00:00", 3328 INTERVAL ' . (Validate::isUnsignedInt(Configuration::get('PS_NB_DAYS_NEW_PRODUCT')) ? Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20) . ' DAY 3329 ) 3330 ) > 0 AS new 3331 FROM `' . _DB_PREFIX_ . 'product` p 3332 ' . Shop::addSqlAssociation('product', 'p') . ' 3333 LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` product_attribute_shop 3334 ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shop.id_shop=' . (int) $context->shop->id . ') 3335 ' . Product::sqlStock('p', 0, false, $context->shop) . ' 3336 LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON ( 3337 p.`id_product` = pl.`id_product` 3338 AND pl.`id_lang` = ' . (int) $id_lang . Shop::addSqlRestrictionOnLang('pl') . ' 3339 ) 3340 LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop 3341 ON (image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop=' . (int) $context->shop->id . ') 3342 LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $id_lang . ') 3343 LEFT JOIN `' . _DB_PREFIX_ . 'manufacturer` m ON (m.`id_manufacturer` = p.`id_manufacturer`) 3344 WHERE product_shop.`active` = 1 3345 AND product_shop.`show_price` = 1 3346 ' . ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . ' 3347 ' . ((!$beginning && !$ending) ? ' AND p.`id_product` IN (' . ((is_array($tab_id_product) && count($tab_id_product)) ? implode(', ', $tab_id_product) : 0) . ')' : '') . ' 3348 ' . $sql_groups; 3349 3350 if ($order_by != 'price') { 3351 $sql .= ' 3352 ORDER BY ' . (isset($order_by_prefix) ? pSQL($order_by_prefix) . '.' : '') . pSQL($order_by) . ' ' . pSQL($order_way) . ' 3353 LIMIT ' . (int) (($page_number - 1) * $nb_products) . ', ' . (int) $nb_products; 3354 } 3355 3356 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); 3357 3358 if (!$result) { 3359 return false; 3360 } 3361 3362 if ($order_by === 'price') { 3363 Tools::orderbyPrice($result, $order_way); 3364 $result = array_slice($result, (int) (($page_number - 1) * $nb_products), (int) $nb_products); 3365 } 3366 3367 return Product::getProductsProperties($id_lang, $result); 3368 } 3369 3370 /** 3371 * getProductCategories return an array of categories which this product belongs to. 3372 * 3373 * @param int|string $id_product Product identifier 3374 * 3375 * @return array Category identifiers 3376 */ 3377 public static function getProductCategories($id_product = '') 3378 { 3379 $cache_id = 'Product::getProductCategories_' . (int) $id_product; 3380 if (!Cache::isStored($cache_id)) { 3381 $ret = []; 3382 3383 $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 3384 ' 3385 SELECT `id_category` FROM `' . _DB_PREFIX_ . 'category_product` 3386 WHERE `id_product` = ' . (int) $id_product 3387 ); 3388 3389 if ($row) { 3390 foreach ($row as $val) { 3391 $ret[] = $val['id_category']; 3392 } 3393 } 3394 Cache::store($cache_id, $ret); 3395 3396 return $ret; 3397 } 3398 3399 return Cache::retrieve($cache_id); 3400 } 3401 3402 /** 3403 * @param int|string $id_product Product identifier 3404 * @param int|null $id_lang Language identifier 3405 * 3406 * @return array 3407 */ 3408 public static function getProductCategoriesFull($id_product = '', $id_lang = null) 3409 { 3410 if (!$id_lang) { 3411 $id_lang = Context::getContext()->language->id; 3412 } 3413 3414 $ret = []; 3415 $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 3416 ' 3417 SELECT cp.`id_category`, cl.`name`, cl.`link_rewrite` FROM `' . _DB_PREFIX_ . 'category_product` cp 3418 LEFT JOIN `' . _DB_PREFIX_ . 'category` c ON (c.id_category = cp.id_category) 3419 LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl ON (cp.`id_category` = cl.`id_category`' . Shop::addSqlRestrictionOnLang('cl') . ') 3420 ' . Shop::addSqlAssociation('category', 'c') . ' 3421 WHERE cp.`id_product` = ' . (int) $id_product . ' 3422 AND cl.`id_lang` = ' . (int) $id_lang 3423 ); 3424 3425 foreach ($row as $val) { 3426 $ret[$val['id_category']] = $val; 3427 } 3428 3429 return $ret; 3430 } 3431 3432 /** 3433 * getCategories return an array of categories which this product belongs to. 3434 * 3435 * @return array of categories 3436 */ 3437 public function getCategories() 3438 { 3439 return Product::getProductCategories($this->id); 3440 } 3441 3442 /** 3443 * Gets carriers assigned to the product. 3444 * 3445 * @return array 3446 */ 3447 public function getCarriers() 3448 { 3449 return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' 3450 SELECT c.* 3451 FROM `' . _DB_PREFIX_ . 'product_carrier` pc 3452 INNER JOIN `' . _DB_PREFIX_ . 'carrier` c 3453 ON (c.`id_reference` = pc.`id_carrier_reference` AND c.`deleted` = 0) 3454 WHERE pc.`id_product` = ' . (int) $this->id . ' 3455 AND pc.`id_shop` = ' . (int) $this->id_shop); 3456 } 3457 3458 /** 3459 * Sets carriers assigned to the product. 3460 * 3461 * @param int[] $carrier_list 3462 */ 3463 public function setCarriers($carrier_list) 3464 { 3465 $data = []; 3466 3467 foreach ($carrier_list as $carrier) { 3468 $data[] = [ 3469 'id_product' => (int) $this->id, 3470 'id_carrier_reference' => (int) $carrier, 3471 'id_shop' => (int) $this->id_shop, 3472 ]; 3473 } 3474 Db::getInstance()->execute( 3475 'DELETE FROM `' . _DB_PREFIX_ . 'product_carrier` 3476 WHERE id_product = ' . (int) $this->id . ' 3477 AND id_shop = ' . (int) $this->id_shop 3478 ); 3479 3480 $unique_array = []; 3481 foreach ($data as $sub_array) { 3482 if (!in_array($sub_array, $unique_array)) { 3483 $unique_array[] = $sub_array; 3484 } 3485 } 3486 3487 if (count($unique_array)) { 3488 Db::getInstance()->insert('product_carrier', $unique_array, false, true, Db::INSERT_IGNORE); 3489 } 3490 } 3491 3492 /** 3493 * Get product images and legends. 3494 * 3495 * @param int $id_lang Language identifier 3496 * @param Context|null $context 3497 * 3498 * @return array Product images and legends 3499 */ 3500 public function getImages($id_lang, Context $context = null) 3501 { 3502 return Db::getInstance()->executeS( 3503 ' 3504 SELECT image_shop.`cover`, i.`id_image`, il.`legend`, i.`position` 3505 FROM `' . _DB_PREFIX_ . 'image` i 3506 ' . Shop::addSqlAssociation('image', 'i') . ' 3507 LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (i.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $id_lang . ') 3508 WHERE i.`id_product` = ' . (int) $this->id . ' 3509 ORDER BY `position`' 3510 ); 3511 } 3512 3513 /** 3514 * Get product cover image. 3515 * 3516 * @param int $id_product Product identifier 3517 * @param Context|null $context 3518 * 3519 * @return array Product cover image 3520 */ 3521 public static function getCover($id_product, Context $context = null) 3522 { 3523 if (!$context) { 3524 $context = Context::getContext(); 3525 } 3526 $cache_id = 'Product::getCover_' . (int) $id_product . '-' . (int) $context->shop->id; 3527 if (!Cache::isStored($cache_id)) { 3528 $sql = 'SELECT image_shop.`id_image` 3529 FROM `' . _DB_PREFIX_ . 'image` i 3530 ' . Shop::addSqlAssociation('image', 'i') . ' 3531 WHERE i.`id_product` = ' . (int) $id_product . ' 3532 AND image_shop.`cover` = 1'; 3533 $result = Db::getInstance()->getRow($sql); 3534 Cache::store($cache_id, $result); 3535 3536 return $result; 3537 } 3538 3539 return Cache::retrieve($cache_id); 3540 } 3541 3542 /** 3543 * Returns product price. 3544 * 3545 * @param int $id_product Product identifier 3546 * @param bool $usetax With taxes or not (optional) 3547 * @param int|null $id_product_attribute Attribute identifier (optional). 3548 * If set to false, do not apply the combination price impact. 3549 * NULL does apply the default combination price impact 3550 * @param int $decimals Number of decimals (optional) 3551 * @param int|null $divisor Useful when paying many time without fees (optional) 3552 * @param bool $only_reduc Returns only the reduction amount 3553 * @param bool $usereduc Set if the returned amount will include reduction 3554 * @param int $quantity Required for quantity discount application (default value: 1) 3555 * @param bool $force_associated_tax DEPRECATED - NOT USED Force to apply the associated tax. 3556 * Only works when the parameter $usetax is true 3557 * @param int|null $id_customer Customer identifier (for customer group reduction) 3558 * @param int|null $id_cart Cart identifier Required when the cookie is not accessible 3559 * (e.g., inside a payment module, a cron task...) 3560 * @param int|null $id_address Address identifier of Customer. Required for price (tax included) 3561 * calculation regarding the guest localization 3562 * @param array|null $specific_price_output If a specific price applies regarding the previous parameters, 3563 * this variable is filled with the corresponding SpecificPrice data 3564 * @param bool $with_ecotax insert ecotax in price output 3565 * @param bool $use_group_reduction 3566 * @param Context $context 3567 * @param bool $use_customer_price 3568 * @param int|null $id_customization Customization identifier 3569 * 3570 * @return float Product price 3571 */ 3572 public static function getPriceStatic( 3573 $id_product, 3574 $usetax = true, 3575 $id_product_attribute = null, 3576 $decimals = 6, 3577 $divisor = null, 3578 $only_reduc = false, 3579 $usereduc = true, 3580 $quantity = 1, 3581 $force_associated_tax = false, 3582 $id_customer = null, 3583 $id_cart = null, 3584 $id_address = null, 3585 &$specific_price_output = null, 3586 $with_ecotax = true, 3587 $use_group_reduction = true, 3588 Context $context = null, 3589 $use_customer_price = true, 3590 $id_customization = null 3591 ) { 3592 if (!$context) { 3593 $context = Context::getContext(); 3594 } 3595 3596 $cur_cart = $context->cart; 3597 3598 if ($divisor !== null) { 3599 Tools::displayParameterAsDeprecated('divisor'); 3600 } 3601 3602 if (!Validate::isBool($usetax) || !Validate::isUnsignedId($id_product)) { 3603 die(Tools::displayError()); 3604 } 3605 3606 // Initializations 3607 $id_group = null; 3608 if ($id_customer) { 3609 $id_group = Customer::getDefaultGroupId((int) $id_customer); 3610 } 3611 if (!$id_group) { 3612 $id_group = (int) Group::getCurrent()->id; 3613 } 3614 3615 // If there is cart in context or if the specified id_cart is different from the context cart id 3616 if (!is_object($cur_cart) || (Validate::isUnsignedInt($id_cart) && $id_cart && $cur_cart->id != $id_cart)) { 3617 /* 3618 * When a user (e.g., guest, customer, Google...) is on PrestaShop, he has already its cart as the global (see /init.php) 3619 * When a non-user calls directly this method (e.g., payment module...) is on PrestaShop, he does not have already it BUT knows the cart ID 3620 * When called from the back office, cart ID can be inexistant 3621 */ 3622 if (!$id_cart && !isset($context->employee)) { 3623 die(Tools::displayError()); 3624 } 3625 $cur_cart = new Cart($id_cart); 3626 // Store cart in context to avoid multiple instantiations in BO 3627 if (!Validate::isLoadedObject($context->cart)) { 3628 $context->cart = $cur_cart; 3629 } 3630 } 3631 3632 $cart_quantity = 0; 3633 if ((int) $id_cart) { 3634 $cache_id = 'Product::getPriceStatic_' . (int) $id_product . '-' . (int) $id_cart; 3635 if (!Cache::isStored($cache_id) || ($cart_quantity = Cache::retrieve($cache_id) != (int) $quantity)) { 3636 $sql = 'SELECT SUM(`quantity`) 3637 FROM `' . _DB_PREFIX_ . 'cart_product` 3638 WHERE `id_product` = ' . (int) $id_product . ' 3639 AND `id_cart` = ' . (int) $id_cart; 3640 $cart_quantity = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql); 3641 Cache::store($cache_id, $cart_quantity); 3642 } else { 3643 $cart_quantity = Cache::retrieve($cache_id); 3644 } 3645 } 3646 3647 $id_currency = Validate::isLoadedObject($context->currency) ? (int) $context->currency->id : (int) Configuration::get('PS_CURRENCY_DEFAULT'); 3648 3649 if (!$id_address && Validate::isLoadedObject($cur_cart)) { 3650 $id_address = $cur_cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}; 3651 } 3652 3653 // retrieve address informations 3654 $address = Address::initialize($id_address, true); 3655 $id_country = (int) $address->id_country; 3656 $id_state = (int) $address->id_state; 3657 $zipcode = $address->postcode; 3658 3659 if (Tax::excludeTaxeOption()) { 3660 $usetax = false; 3661 } 3662 3663 if ($usetax != false 3664 && !empty($address->vat_number) 3665 && $address->id_country != Configuration::get('VATNUMBER_COUNTRY') 3666 && Configuration::get('VATNUMBER_MANAGEMENT')) { 3667 $usetax = false; 3668 } 3669 3670 if (null === $id_customer && Validate::isLoadedObject($context->customer)) { 3671 $id_customer = $context->customer->id; 3672 } 3673 3674 $return = Product::priceCalculation( 3675 $context->shop->id, 3676 $id_product, 3677 $id_product_attribute, 3678 $id_country, 3679 $id_state, 3680 $zipcode, 3681 $id_currency, 3682 $id_group, 3683 $quantity, 3684 $usetax, 3685 $decimals, 3686 $only_reduc, 3687 $usereduc, 3688 $with_ecotax, 3689 $specific_price_output, 3690 $use_group_reduction, 3691 $id_customer, 3692 $use_customer_price, 3693 $id_cart, 3694 $cart_quantity, 3695 $id_customization 3696 ); 3697 3698 return $return; 3699 } 3700 3701 /** 3702 * Price calculation / Get product price. 3703 * 3704 * @param int $id_shop Shop identifier 3705 * @param int $id_product Product identifier 3706 * @param int $id_product_attribute Attribute identifier 3707 * @param int $id_country Country identifier 3708 * @param int $id_state State identifier 3709 * @param string $zipcode 3710 * @param int $id_currency Currency identifier 3711 * @param int $id_group Group identifier 3712 * @param int $quantity Quantity Required for Specific prices : quantity discount application 3713 * @param bool $use_tax with (1) or without (0) tax 3714 * @param int $decimals Number of decimals returned 3715 * @param bool $only_reduc Returns only the reduction amount 3716 * @param bool $use_reduc Set if the returned amount will include reduction 3717 * @param bool $with_ecotax insert ecotax in price output 3718 * @param array|null $specific_price If a specific price applies regarding the previous parameters, 3719 * this variable is filled with the corresponding SpecificPrice data 3720 * @param bool $use_group_reduction 3721 * @param int $id_customer Customer identifier 3722 * @param bool $use_customer_price 3723 * @param int $id_cart Cart identifier 3724 * @param int $real_quantity 3725 * @param int $id_customization Customization identifier 3726 * 3727 * @return float|void Product price, void if not found in cache $_pricesLevel2 3728 */ 3729 public static function priceCalculation( 3730 $id_shop, 3731 $id_product, 3732 $id_product_attribute, 3733 $id_country, 3734 $id_state, 3735 $zipcode, 3736 $id_currency, 3737 $id_group, 3738 $quantity, 3739 $use_tax, 3740 $decimals, 3741 $only_reduc, 3742 $use_reduc, 3743 $with_ecotax, 3744 &$specific_price, 3745 $use_group_reduction, 3746 $id_customer = 0, 3747 $use_customer_price = true, 3748 $id_cart = 0, 3749 $real_quantity = 0, 3750 $id_customization = 0 3751 ) { 3752 static $address = null; 3753 static $context = null; 3754 3755 if ($context == null) { 3756 $context = Context::getContext()->cloneContext(); 3757 } 3758 3759 if ($address === null) { 3760 if (is_object($context->cart) && $context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')} != null) { 3761 $id_address = $context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}; 3762 $address = new Address($id_address); 3763 } else { 3764 $address = new Address(); 3765 } 3766 } 3767 3768 if ($id_shop !== null && $context->shop->id != (int) $id_shop) { 3769 $context->shop = new Shop((int) $id_shop); 3770 } 3771 3772 if (!$use_customer_price) { 3773 $id_customer = 0; 3774 } 3775 3776 if ($id_product_attribute === null) { 3777 $id_product_attribute = Product::getDefaultAttribute($id_product); 3778 } 3779 3780 $cache_id = (int) $id_product . '-' . (int) $id_shop . '-' . (int) $id_currency . '-' . (int) $id_country . '-' . $id_state . '-' . $zipcode . '-' . (int) $id_group . 3781 '-' . (int) $quantity . '-' . (int) $id_product_attribute . '-' . (int) $id_customization . 3782 '-' . (int) $with_ecotax . '-' . (int) $id_customer . '-' . (int) $use_group_reduction . '-' . (int) $id_cart . '-' . (int) $real_quantity . 3783 '-' . ($only_reduc ? '1' : '0') . '-' . ($use_reduc ? '1' : '0') . '-' . ($use_tax ? '1' : '0') . '-' . (int) $decimals; 3784 3785 // reference parameter is filled before any returns 3786 $specific_price = SpecificPrice::getSpecificPrice( 3787 (int) $id_product, 3788 $id_shop, 3789 $id_currency, 3790 $id_country, 3791 $id_group, 3792 $quantity, 3793 $id_product_attribute, 3794 $id_customer, 3795 $id_cart, 3796 $real_quantity 3797 ); 3798 3799 if (isset(self::$_prices[$cache_id])) { 3800 return self::$_prices[$cache_id]; 3801 } 3802 3803 // fetch price & attribute price 3804 $cache_id_2 = $id_product . '-' . $id_shop; 3805 // We need to check the cache for this price AND attribute, if absent the whole product cache needs update 3806 // This can happen if the cache was filled before the combination was created for example 3807 if (!isset(self::$_pricesLevel2[$cache_id_2][(int) $id_product_attribute])) { 3808 $sql = new DbQuery(); 3809 $sql->select('product_shop.`price`, product_shop.`ecotax`'); 3810 $sql->from('product', 'p'); 3811 $sql->innerJoin('product_shop', 'product_shop', '(product_shop.id_product=p.id_product AND product_shop.id_shop = ' . (int) $id_shop . ')'); 3812 $sql->where('p.`id_product` = ' . (int) $id_product); 3813 if (Combination::isFeatureActive()) { 3814 $sql->select('IFNULL(product_attribute_shop.id_product_attribute,0) id_product_attribute, product_attribute_shop.`price` AS attribute_price, product_attribute_shop.default_on, product_attribute_shop.`ecotax` AS attribute_ecotax'); 3815 $sql->leftJoin('product_attribute_shop', 'product_attribute_shop', '(product_attribute_shop.id_product = p.id_product AND product_attribute_shop.id_shop = ' . (int) $id_shop . ')'); 3816 } else { 3817 $sql->select('0 as id_product_attribute'); 3818 } 3819 3820 $res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); 3821 3822 if (is_array($res) && count($res)) { 3823 foreach ($res as $row) { 3824 $array_tmp = [ 3825 'price' => $row['price'], 3826 'ecotax' => $row['ecotax'], 3827 'attribute_price' => $row['attribute_price'] ?? null, 3828 'attribute_ecotax' => $row['attribute_ecotax'] ?? null, 3829 ]; 3830 self::$_pricesLevel2[$cache_id_2][(int) $row['id_product_attribute']] = $array_tmp; 3831 3832 if (isset($row['default_on']) && $row['default_on'] == 1) { 3833 self::$_pricesLevel2[$cache_id_2][0] = $array_tmp; 3834 } 3835 } 3836 } 3837 } 3838 3839 if (!isset(self::$_pricesLevel2[$cache_id_2][(int) $id_product_attribute])) { 3840 return; 3841 } 3842 3843 $result = self::$_pricesLevel2[$cache_id_2][(int) $id_product_attribute]; 3844 3845 if (!$specific_price || $specific_price['price'] < 0) { 3846 $price = (float) $result['price']; 3847 } else { 3848 $price = (float) $specific_price['price']; 3849 } 3850 // convert only if the specific price is in the default currency (id_currency = 0) 3851 if ( 3852 !$specific_price || 3853 !( 3854 $specific_price['price'] >= 0 && 3855 $specific_price['id_currency'] && 3856 $id_currency !== $specific_price['id_currency'] 3857 ) 3858 ) { 3859 $price = Tools::convertPrice($price, $id_currency); 3860 3861 if (isset($specific_price['price']) && $specific_price['price'] >= 0) { 3862 $specific_price['price'] = $price; 3863 } 3864 } 3865 3866 // Attribute price 3867 if (is_array($result) && (!$specific_price || !$specific_price['id_product_attribute'] || $specific_price['price'] < 0)) { 3868 $attribute_price = Tools::convertPrice($result['attribute_price'] !== null ? (float) $result['attribute_price'] : 0, $id_currency); 3869 // If you want the default combination, please use NULL value instead 3870 if ($id_product_attribute !== false) { 3871 $price += $attribute_price; 3872 } 3873 } 3874 3875 // Customization price 3876 if ((int) $id_customization) { 3877 $price += Tools::convertPrice(Customization::getCustomizationPrice($id_customization), $id_currency); 3878 } 3879 3880 // Tax 3881 $address->id_country = $id_country; 3882 $address->id_state = $id_state; 3883 $address->postcode = $zipcode; 3884 3885 $tax_manager = TaxManagerFactory::getManager($address, Product::getIdTaxRulesGroupByIdProduct((int) $id_product, $context)); 3886 $product_tax_calculator = $tax_manager->getTaxCalculator(); 3887 3888 // Add Tax 3889 if ($use_tax) { 3890 $price = $product_tax_calculator->addTaxes($price); 3891 } 3892 3893 // Eco Tax 3894 if (($result['ecotax'] || isset($result['attribute_ecotax'])) && $with_ecotax) { 3895 $ecotax = $result['ecotax']; 3896 if (isset($result['attribute_ecotax']) && $result['attribute_ecotax'] > 0) { 3897 $ecotax = $result['attribute_ecotax']; 3898 } 3899 3900 if ($id_currency) { 3901 $ecotax = Tools::convertPrice($ecotax, $id_currency); 3902 } 3903 if ($use_tax) { 3904 static $psEcotaxTaxRulesGroupId = null; 3905 if ($psEcotaxTaxRulesGroupId === null) { 3906 $psEcotaxTaxRulesGroupId = (int) Configuration::get('PS_ECOTAX_TAX_RULES_GROUP_ID'); 3907 } 3908 // reinit the tax manager for ecotax handling 3909 $tax_manager = TaxManagerFactory::getManager( 3910 $address, 3911 $psEcotaxTaxRulesGroupId 3912 ); 3913 $ecotax_tax_calculator = $tax_manager->getTaxCalculator(); 3914 $price += $ecotax_tax_calculator->addTaxes($ecotax); 3915 } else { 3916 $price += $ecotax; 3917 } 3918 } 3919 3920 // Reduction 3921 $specific_price_reduction = 0; 3922 if (($only_reduc || $use_reduc) && $specific_price) { 3923 if ($specific_price['reduction_type'] == 'amount') { 3924 $reduction_amount = $specific_price['reduction']; 3925 3926 if (!$specific_price['id_currency']) { 3927 $reduction_amount = Tools::convertPrice($reduction_amount, $id_currency); 3928 } 3929 3930 $specific_price_reduction = $reduction_amount; 3931 3932 // Adjust taxes if required 3933 3934 if (!$use_tax && $specific_price['reduction_tax']) { 3935 $specific_price_reduction = $product_tax_calculator->removeTaxes($specific_price_reduction); 3936 } 3937 if ($use_tax && !$specific_price['reduction_tax']) { 3938 $specific_price_reduction = $product_tax_calculator->addTaxes($specific_price_reduction); 3939 } 3940 } else { 3941 $specific_price_reduction = $price * $specific_price['reduction']; 3942 } 3943 } 3944 3945 if ($use_reduc) { 3946 $price -= $specific_price_reduction; 3947 } 3948 3949 // Group reduction 3950 if ($use_group_reduction) { 3951 $reduction_from_category = GroupReduction::getValueForProduct($id_product, $id_group); 3952 if ($reduction_from_category !== false) { 3953 $group_reduction = $price * (float) $reduction_from_category; 3954 } else { // apply group reduction if there is no group reduction for this category 3955 $group_reduction = (($reduc = Group::getReductionByIdGroup($id_group)) != 0) ? ($price * $reduc / 100) : 0; 3956 } 3957 3958 $price -= $group_reduction; 3959 } 3960 3961 if ($only_reduc) { 3962 return Tools::ps_round($specific_price_reduction, $decimals); 3963 } 3964 3965 $price = Tools::ps_round($price, $decimals); 3966 3967 if ($price < 0) { 3968 $price = 0; 3969 } 3970 3971 self::$_prices[$cache_id] = $price; 3972 3973 return self::$_prices[$cache_id]; 3974 } 3975 3976 /** 3977 * @param int $orderId 3978 * @param int $productId 3979 * @param int $combinationId 3980 * @param bool $withTaxes 3981 * @param bool $useReduction 3982 * @param bool $withEcoTax 3983 * 3984 * @return float|null 3985 * 3986 * @throws PrestaShopDatabaseException 3987 */ 3988 public static function getPriceFromOrder( 3989 int $orderId, 3990 int $productId, 3991 int $combinationId, 3992 bool $withTaxes, 3993 bool $useReduction, 3994 bool $withEcoTax 3995 ): ?float { 3996 $sql = new DbQuery(); 3997 $sql->select('od.*, t.rate AS tax_rate'); 3998 $sql->from('order_detail', 'od'); 3999 $sql->where('od.`id_order` = ' . $orderId); 4000 $sql->where('od.`product_id` = ' . $productId); 4001 if (Combination::isFeatureActive()) { 4002 $sql->where('od.`product_attribute_id` = ' . $combinationId); 4003 } 4004 $sql->leftJoin('order_detail_tax', 'odt', 'odt.id_order_detail = od.id_order_detail'); 4005 $sql->leftJoin('tax', 't', 't.id_tax = odt.id_tax'); 4006 $res = Db::getInstance((bool) _PS_USE_SQL_SLAVE_)->executeS($sql); 4007 if (!is_array($res) || empty($res)) { 4008 return null; 4009 } 4010 4011 $orderDetail = $res[0]; 4012 if ($useReduction) { 4013 // If we want price with reduction it is already the one stored in OrderDetail 4014 $price = $withTaxes ? $orderDetail['unit_price_tax_incl'] : $orderDetail['unit_price_tax_excl']; 4015 } else { 4016 // Without reduction we use the original product price to compute the original price 4017 $tax_rate = $withTaxes ? (1 + ($orderDetail['tax_rate'] / 100)) : 1; 4018 $price = $orderDetail['original_product_price'] * $tax_rate; 4019 } 4020 if (!$withEcoTax) { 4021 // Remove the ecotax as the order detail contains already ecotax in the price 4022 $price -= ($withTaxes ? $orderDetail['ecotax'] * (1 + $orderDetail['ecotax_tax_rate']) : $orderDetail['ecotax']); 4023 } 4024 4025 return $price; 4026 } 4027 4028 /** 4029 * @param float $price 4030 * @param Currency|false $currency 4031 * @param Context|null $context 4032 * 4033 * @return string 4034 */ 4035 public static function convertAndFormatPrice($price, $currency = false, Context $context = null) 4036 { 4037 if (!$context) { 4038 $context = Context::getContext(); 4039 } 4040 if (!$currency) { 4041 $currency = $context->currency; 4042 } 4043 4044 return $context->getCurrentLocale()->formatPrice(Tools::convertPrice($price, $currency), $currency->iso_code); 4045 } 4046 4047 /** 4048 * @param int $id_product Product identifier 4049 * @param int $quantity 4050 * @param Context|null $context 4051 * 4052 * @return bool 4053 */ 4054 public static function isDiscounted($id_product, $quantity = 1, Context $context = null) 4055 { 4056 if (!$context) { 4057 $context = Context::getContext(); 4058 } 4059 4060 $id_group = $context->customer->id_default_group; 4061 $cart_quantity = !$context->cart ? 0 : Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 4062 ' 4063 SELECT SUM(`quantity`) 4064 FROM `' . _DB_PREFIX_ . 'cart_product` 4065 WHERE `id_product` = ' . (int) $id_product . ' AND `id_cart` = ' . (int) $context->cart->id 4066 ); 4067 $quantity = $cart_quantity ? $cart_quantity : $quantity; 4068 4069 $id_currency = (int) $context->currency->id; 4070 $ids = Address::getCountryAndState((int) $context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}); 4071 $id_country = $ids['id_country'] ? (int) $ids['id_country'] : (int) Configuration::get('PS_COUNTRY_DEFAULT'); 4072 4073 return (bool) SpecificPrice::getSpecificPrice((int) $id_product, $context->shop->id, $id_currency, $id_country, $id_group, $quantity, null, 0, 0, $quantity); 4074 } 4075 4076 /** 4077 * Get product price 4078 * Same as static function getPriceStatic, no need to specify product id. 4079 * 4080 * @param bool $tax With taxes or not (optional) 4081 * @param int|null $id_product_attribute Attribute identifier 4082 * @param int $decimals Number of decimals 4083 * @param int|null $divisor Util when paying many time without fees 4084 * @param bool $only_reduc 4085 * @param bool $usereduc 4086 * @param int $quantity 4087 * 4088 * @return float Product price in euros 4089 */ 4090 public function getPrice( 4091 $tax = true, 4092 $id_product_attribute = null, 4093 $decimals = 6, 4094 $divisor = null, 4095 $only_reduc = false, 4096 $usereduc = true, 4097 $quantity = 1 4098 ) { 4099 return Product::getPriceStatic((int) $this->id, $tax, $id_product_attribute, $decimals, $divisor, $only_reduc, $usereduc, $quantity); 4100 } 4101 4102 /** 4103 * @param bool $tax With taxes or not (optional) 4104 * @param int|null $id_product_attribute Attribute identifier 4105 * @param int $decimals Number of decimals 4106 * @param null $divisor Util when paying many time without fees 4107 * @param bool $only_reduc 4108 * @param bool $usereduc 4109 * @param int $quantity 4110 * 4111 * @return float 4112 */ 4113 public function getPublicPrice( 4114 $tax = true, 4115 $id_product_attribute = null, 4116 $decimals = 6, 4117 $divisor = null, 4118 $only_reduc = false, 4119 $usereduc = true, 4120 $quantity = 1 4121 ) { 4122 $specific_price_output = null; 4123 4124 return Product::getPriceStatic( 4125 (int) $this->id, 4126 $tax, 4127 $id_product_attribute, 4128 $decimals, 4129 $divisor, 4130 $only_reduc, 4131 $usereduc, 4132 $quantity, 4133 false, 4134 null, 4135 null, 4136 null, 4137 $specific_price_output, 4138 true, 4139 true, 4140 null, 4141 false 4142 ); 4143 } 4144 4145 /** 4146 * @return int 4147 */ 4148 public function getIdProductAttributeMostExpensive() 4149 { 4150 if (!Combination::isFeatureActive()) { 4151 return 0; 4152 } 4153 4154 return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(' 4155 SELECT pa.`id_product_attribute` 4156 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 4157 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 4158 WHERE pa.`id_product` = ' . (int) $this->id . ' 4159 ORDER BY product_attribute_shop.`price` DESC'); 4160 } 4161 4162 /** 4163 * @return int 4164 */ 4165 public function getDefaultIdProductAttribute() 4166 { 4167 if (!Combination::isFeatureActive()) { 4168 return 0; 4169 } 4170 4171 return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 4172 ' 4173 SELECT pa.`id_product_attribute` 4174 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 4175 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 4176 WHERE pa.`id_product` = ' . (int) $this->id . ' 4177 AND product_attribute_shop.default_on = 1' 4178 ); 4179 } 4180 4181 /** 4182 * @param bool $notax With taxes or not (optional) 4183 * @param int|null $id_product_attribute Attribute identifier 4184 * @param int $decimals Number of decimals 4185 * 4186 * @return float 4187 */ 4188 public function getPriceWithoutReduct($notax = false, $id_product_attribute = null, $decimals = 6) 4189 { 4190 return Product::getPriceStatic((int) $this->id, !$notax, $id_product_attribute, $decimals, null, false, false); 4191 } 4192 4193 /** 4194 * Display price with right format and currency. 4195 * 4196 * @param array $params Params 4197 * @param object $smarty Smarty object (DEPRECATED) 4198 * 4199 * @return string Price with right format and currency 4200 */ 4201 public static function convertPrice($params, &$smarty) 4202 { 4203 return Context::getContext()->getCurrentLocale()->formatPrice($params['price'], Context::getContext()->currency->iso_code); 4204 } 4205 4206 /** 4207 * Convert price with currency. 4208 * 4209 * @param array $params 4210 * @param object $smarty Smarty object (DEPRECATED) 4211 * 4212 * @return string Ambigous <string, mixed, Ambigous <number, string>> 4213 */ 4214 public static function convertPriceWithCurrency($params, &$smarty) 4215 { 4216 $currency = $params['currency']; 4217 $currency = is_object($currency) ? $currency->iso_code : Currency::getIsoCodeById((int) $currency); 4218 4219 return Tools::getContextLocale(Context::getContext())->formatPrice($params['price'], $currency); 4220 } 4221 4222 /** 4223 * @param array $params 4224 * @param object $smarty Smarty object (DEPRECATED) 4225 * 4226 * @return string 4227 */ 4228 public static function displayWtPrice($params, &$smarty) 4229 { 4230 return Tools::getContextLocale(Context::getContext())->formatPrice($params['p'], Context::getContext()->currency->iso_code); 4231 } 4232 4233 /** 4234 * Display WT price with currency. 4235 * 4236 * @param array $params 4237 * @param object $smarty Smarty object (DEPRECATED) 4238 * 4239 * @return string Ambigous <string, mixed, Ambigous <number, string>> 4240 */ 4241 public static function displayWtPriceWithCurrency($params, &$smarty) 4242 { 4243 $currency = $params['currency']; 4244 $currency = is_object($currency) ? $currency->iso_code : Currency::getIsoCodeById((int) $currency); 4245 4246 return !is_null($params['price']) ? Tools::getContextLocale(Context::getContext())->formatPrice($params['price'], $currency) : null; 4247 } 4248 4249 /** 4250 * Get available product quantities (this method already have decreased products in cart). 4251 * 4252 * @param int $idProduct Product identifier 4253 * @param int|null $idProductAttribute Product attribute id (optional) 4254 * @param bool|null $cacheIsPack 4255 * @param Cart|null $cart 4256 * @param int|null $idCustomization Product customization id (optional) 4257 * 4258 * @return int Available quantities 4259 */ 4260 public static function getQuantity( 4261 $idProduct, 4262 $idProductAttribute = null, 4263 $cacheIsPack = null, 4264 Cart $cart = null, 4265 $idCustomization = null 4266 ) { 4267 // pack usecase: Pack::getQuantity() returns the pack quantity after cart quantities have been removed from stock 4268 if (Pack::isPack((int) $idProduct)) { 4269 return Pack::getQuantity($idProduct, $idProductAttribute, $cacheIsPack, $cart, $idCustomization); 4270 } 4271 $availableQuantity = StockAvailable::getQuantityAvailableByProduct($idProduct, $idProductAttribute); 4272 $nbProductInCart = 0; 4273 4274 // we don't substract products in cart if the cart is already attached to an order, since stock quantity 4275 // has already been updated, this is only useful when the order has not yet been created 4276 if (!empty($cart) && empty(Order::getByCartId($cart->id))) { 4277 $cartProduct = $cart->getProductQuantity($idProduct, $idProductAttribute, $idCustomization); 4278 4279 if (!empty($cartProduct['deep_quantity'])) { 4280 $nbProductInCart = $cartProduct['deep_quantity']; 4281 } 4282 } 4283 4284 // @since 1.5.0 4285 return $availableQuantity - $nbProductInCart; 4286 } 4287 4288 /** 4289 * Create JOIN query with 'stock_available' table. 4290 * 4291 * @param string $product_alias Alias of product table 4292 * @param string|int|null $product_attribute If string : alias of PA table ; if int : value of PA ; if null : nothing about PA 4293 * @param bool $inner_join LEFT JOIN or INNER JOIN 4294 * @param Shop|null $shop 4295 * 4296 * @return string 4297 */ 4298 public static function sqlStock($product_alias, $product_attribute = null, $inner_join = false, Shop $shop = null) 4299 { 4300 $id_shop = ($shop !== null ? (int) $shop->id : null); 4301 $sql = (($inner_join) ? ' INNER ' : ' LEFT ') 4302 . 'JOIN ' . _DB_PREFIX_ . 'stock_available stock 4303 ON (stock.id_product = `' . bqSQL($product_alias) . '`.id_product'; 4304 4305 if (null !== $product_attribute) { 4306 if (!Combination::isFeatureActive()) { 4307 $sql .= ' AND stock.id_product_attribute = 0'; 4308 } elseif (is_numeric($product_attribute)) { 4309 $sql .= ' AND stock.id_product_attribute = ' . $product_attribute; 4310 } elseif (is_string($product_attribute)) { 4311 $sql .= ' AND stock.id_product_attribute = IFNULL(`' . bqSQL($product_attribute) . '`.id_product_attribute, 0)'; 4312 } 4313 } 4314 4315 $sql .= StockAvailable::addSqlShopRestriction(null, $id_shop, 'stock') . ' )'; 4316 4317 return $sql; 4318 } 4319 4320 /** 4321 * @deprecated since 1.5.0 4322 * 4323 * It's not possible to use this method with new stockManager and stockAvailable features 4324 * Now this method do nothing 4325 * @see StockManager if you want to manage real stock 4326 * @see StockAvailable if you want to manage available quantities for sale on your shop(s) 4327 * @deprecated 1.5.3.0 4328 * 4329 * @return false 4330 */ 4331 public static function updateQuantity() 4332 { 4333 Tools::displayAsDeprecated(); 4334 4335 return false; 4336 } 4337 4338 /** 4339 * @deprecated since 1.5.0 4340 * 4341 * It's not possible to use this method with new stockManager and stockAvailable features 4342 * Now this method do nothing 4343 * @deprecated 1.5.3.0 4344 * @see StockManager if you want to manage real stock 4345 * @see StockAvailable if you want to manage available quantities for sale on your shop(s) 4346 * 4347 * @return false 4348 */ 4349 public static function reinjectQuantities() 4350 { 4351 Tools::displayAsDeprecated(); 4352 4353 return false; 4354 } 4355 4356 /** 4357 * @param int $out_of_stock 4358 * - O Deny orders 4359 * - 1 Allow orders 4360 * - 2 Use global setting 4361 * 4362 * @return bool|int Returns false is Stock Management is disabled, or the (int) configuration if it's enabled 4363 */ 4364 public static function isAvailableWhenOutOfStock($out_of_stock) 4365 { 4366 /** @TODO 1.5.0 Update of STOCK_MANAGEMENT & ORDER_OUT_OF_STOCK */ 4367 $ps_stock_management = Configuration::get('PS_STOCK_MANAGEMENT'); 4368 4369 if (!$ps_stock_management) { 4370 return true; 4371 } 4372 4373 $ps_order_out_of_stock = Configuration::get('PS_ORDER_OUT_OF_STOCK'); 4374 4375 return (int) $out_of_stock === OutOfStockType::OUT_OF_STOCK_DEFAULT ? (int) $ps_order_out_of_stock : (int) $out_of_stock; 4376 } 4377 4378 /** 4379 * Check product availability. 4380 * 4381 * @param int $qty Quantity desired 4382 * 4383 * @return bool True if product is available with this quantity, false otherwise 4384 */ 4385 public function checkQty($qty) 4386 { 4387 if ($this->isAvailableWhenOutOfStock(StockAvailable::outOfStock($this->id))) { 4388 return true; 4389 } 4390 $id_product_attribute = isset($this->id_product_attribute) ? $this->id_product_attribute : null; 4391 $availableQuantity = StockAvailable::getQuantityAvailableByProduct($this->id, $id_product_attribute); 4392 4393 return $qty <= $availableQuantity; 4394 } 4395 4396 /** 4397 * Check if there is no default attribute and create it if not. 4398 * 4399 * @return bool 4400 */ 4401 public function checkDefaultAttributes() 4402 { 4403 if (!$this->id) { 4404 return false; 4405 } 4406 4407 if (Db::getInstance()->getValue('SELECT COUNT(*) 4408 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 4409 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 4410 WHERE product_attribute_shop.`default_on` = 1 4411 AND pa.`id_product` = ' . (int) $this->id) > Shop::getTotalShops(true)) { 4412 Db::getInstance()->execute('UPDATE ' . _DB_PREFIX_ . 'product_attribute_shop product_attribute_shop, ' . _DB_PREFIX_ . 'product_attribute pa 4413 SET product_attribute_shop.default_on=NULL, pa.default_on = NULL 4414 WHERE product_attribute_shop.id_product_attribute=pa.id_product_attribute AND pa.id_product=' . (int) $this->id 4415 . Shop::addSqlRestriction(false, 'product_attribute_shop')); 4416 } 4417 4418 $row = Db::getInstance()->getRow( 4419 ' 4420 SELECT pa.id_product 4421 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 4422 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 4423 WHERE product_attribute_shop.`default_on` = 1 4424 AND pa.`id_product` = ' . (int) $this->id 4425 ); 4426 if ($row) { 4427 return true; 4428 } 4429 4430 $mini = Db::getInstance()->getRow( 4431 ' 4432 SELECT MIN(pa.id_product_attribute) as `id_attr` 4433 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 4434 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 4435 WHERE pa.`id_product` = ' . (int) $this->id 4436 ); 4437 if (!$mini) { 4438 return false; 4439 } 4440 4441 if (!ObjectModel::updateMultishopTable('Combination', ['default_on' => 1], 'a.id_product_attribute = ' . (int) $mini['id_attr'])) { 4442 return false; 4443 } 4444 4445 return true; 4446 } 4447 4448 /** 4449 * @param array $products 4450 * @param bool $have_stock DEPRECATED 4451 * 4452 * @return array|false 4453 */ 4454 public static function getAttributesColorList(array $products, $have_stock = true) 4455 { 4456 if ($have_stock !== true) { 4457 Tools::displayParameterAsDeprecated('have_stock'); 4458 } 4459 4460 if (!count($products)) { 4461 return []; 4462 } 4463 4464 $id_lang = Context::getContext()->language->id; 4465 4466 $check_stock = !Configuration::get('PS_DISP_UNAVAILABLE_ATTR'); 4467 if (!$res = Db::getInstance()->executeS( 4468 ' 4469 SELECT pa.`id_product`, a.`color`, pac.`id_product_attribute`, ' . ($check_stock ? 'SUM(IF(stock.`quantity` > 0, 1, 0))' : '0') . ' qty, a.`id_attribute`, al.`name`, IF(color = "", a.id_attribute, color) group_by 4470 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 4471 ' . Shop::addSqlAssociation('product_attribute', 'pa') . 4472 ($check_stock ? Product::sqlStock('pa', 'pa') : '') . ' 4473 JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON (pac.`id_product_attribute` = product_attribute_shop.`id_product_attribute`) 4474 JOIN `' . _DB_PREFIX_ . 'attribute` a ON (a.`id_attribute` = pac.`id_attribute`) 4475 JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) $id_lang . ') 4476 JOIN `' . _DB_PREFIX_ . 'attribute_group` ag ON (a.id_attribute_group = ag.`id_attribute_group`) 4477 WHERE pa.`id_product` IN (' . implode(',', array_map('intval', $products)) . ') AND ag.`is_color_group` = 1 4478 GROUP BY pa.`id_product`, a.`id_attribute`, `group_by` 4479 ' . ($check_stock ? 'HAVING qty > 0' : '') . ' 4480 ORDER BY a.`position` ASC;' 4481 ) 4482 ) { 4483 return false; 4484 } 4485 4486 $colors = []; 4487 foreach ($res as $row) { 4488 $row['texture'] = ''; 4489 4490 if (@filemtime(_PS_COL_IMG_DIR_ . $row['id_attribute'] . '.jpg')) { 4491 $row['texture'] = _THEME_COL_DIR_ . $row['id_attribute'] . '.jpg'; 4492 } elseif (Tools::isEmpty($row['color'])) { 4493 continue; 4494 } 4495 4496 $colors[(int) $row['id_product']][] = ['id_product_attribute' => (int) $row['id_product_attribute'], 'color' => $row['color'], 'texture' => $row['texture'], 'id_product' => $row['id_product'], 'name' => $row['name'], 'id_attribute' => $row['id_attribute']]; 4497 } 4498 4499 return $colors; 4500 } 4501 4502 /** 4503 * Get all available attribute groups. 4504 * 4505 * @param int $id_lang Language identifier 4506 * @param int $id_product_attribute Combination id to get the groups for 4507 * 4508 * @return array Attribute groups 4509 */ 4510 public function getAttributesGroups($id_lang, $id_product_attribute = null) 4511 { 4512 if (!Combination::isFeatureActive()) { 4513 return []; 4514 } 4515 $sql = 'SELECT ag.`id_attribute_group`, ag.`is_color_group`, agl.`name` AS group_name, agl.`public_name` AS public_group_name, 4516 a.`id_attribute`, al.`name` AS attribute_name, a.`color` AS attribute_color, product_attribute_shop.`id_product_attribute`, 4517 IFNULL(stock.quantity, 0) as quantity, product_attribute_shop.`price`, product_attribute_shop.`ecotax`, product_attribute_shop.`weight`, 4518 product_attribute_shop.`default_on`, pa.`reference`, pa.`ean13`, pa.`mpn`, pa.`upc`, pa.`isbn`, product_attribute_shop.`unit_price_impact`, 4519 product_attribute_shop.`minimal_quantity`, product_attribute_shop.`available_date`, ag.`group_type` 4520 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 4521 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 4522 ' . Product::sqlStock('pa', 'pa') . ' 4523 LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON (pac.`id_product_attribute` = pa.`id_product_attribute`) 4524 LEFT JOIN `' . _DB_PREFIX_ . 'attribute` a ON (a.`id_attribute` = pac.`id_attribute`) 4525 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group` ag ON (ag.`id_attribute_group` = a.`id_attribute_group`) 4526 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ON (a.`id_attribute` = al.`id_attribute`) 4527 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl ON (ag.`id_attribute_group` = agl.`id_attribute_group`) 4528 ' . Shop::addSqlAssociation('attribute', 'a') . ' 4529 WHERE pa.`id_product` = ' . (int) $this->id . ' 4530 AND al.`id_lang` = ' . (int) $id_lang . ' 4531 AND agl.`id_lang` = ' . (int) $id_lang . ' 4532 '; 4533 4534 if ($id_product_attribute !== null) { 4535 $sql .= ' AND product_attribute_shop.`id_product_attribute` = ' . (int) $id_product_attribute . ' '; 4536 } 4537 4538 $sql .= 'GROUP BY id_attribute_group, id_product_attribute 4539 ORDER BY ag.`position` ASC, a.`position` ASC, agl.`name` ASC'; 4540 4541 return Db::getInstance()->executeS($sql); 4542 } 4543 4544 /** 4545 * Delete product accessories. 4546 * Wrapper to static method deleteAccessories($product_id). 4547 * 4548 * @return bool Deletion result 4549 */ 4550 public function deleteAccessories() 4551 { 4552 return Db::getInstance()->delete('accessory', 'id_product_1 = ' . (int) $this->id); 4553 } 4554 4555 /** 4556 * Delete product from other products accessories. 4557 * 4558 * @return bool Deletion result 4559 */ 4560 public function deleteFromAccessories() 4561 { 4562 return Db::getInstance()->delete('accessory', 'id_product_2 = ' . (int) $this->id); 4563 } 4564 4565 /** 4566 * Get product accessories (only names). 4567 * 4568 * @param int $id_lang Language identifier 4569 * @param int $id_product Product identifier 4570 * 4571 * @return array Product accessories 4572 */ 4573 public static function getAccessoriesLight($id_lang, $id_product) 4574 { 4575 return Db::getInstance()->executeS( 4576 ' 4577 SELECT p.`id_product`, p.`reference`, pl.`name` 4578 FROM `' . _DB_PREFIX_ . 'accessory` 4579 LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON (p.`id_product`= `id_product_2`) 4580 ' . Shop::addSqlAssociation('product', 'p') . ' 4581 LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON ( 4582 p.`id_product` = pl.`id_product` 4583 AND pl.`id_lang` = ' . (int) $id_lang . Shop::addSqlRestrictionOnLang('pl') . ' 4584 ) 4585 WHERE `id_product_1` = ' . (int) $id_product 4586 ); 4587 } 4588 4589 /** 4590 * Get product accessories. 4591 * 4592 * @param int $id_lang Language identifier 4593 * @param bool $active 4594 * 4595 * @return array Product accessories 4596 */ 4597 public function getAccessories($id_lang, $active = true) 4598 { 4599 $sql = 'SELECT p.*, product_shop.*, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`link_rewrite`, 4600 pl.`meta_description`, pl.`meta_keywords`, pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`, 4601 image_shop.`id_image` id_image, il.`legend`, m.`name` as manufacturer_name, cl.`name` AS category_default, IFNULL(product_attribute_shop.id_product_attribute, 0) id_product_attribute, 4602 DATEDIFF( 4603 p.`date_add`, 4604 DATE_SUB( 4605 "' . date('Y-m-d') . ' 00:00:00", 4606 INTERVAL ' . (Validate::isUnsignedInt(Configuration::get('PS_NB_DAYS_NEW_PRODUCT')) ? Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20) . ' DAY 4607 ) 4608 ) > 0 AS new 4609 FROM `' . _DB_PREFIX_ . 'accessory` 4610 LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON p.`id_product` = `id_product_2` 4611 ' . Shop::addSqlAssociation('product', 'p') . ' 4612 LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` product_attribute_shop 4613 ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shop.id_shop=' . (int) $this->id_shop . ') 4614 LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON ( 4615 p.`id_product` = pl.`id_product` 4616 AND pl.`id_lang` = ' . (int) $id_lang . Shop::addSqlRestrictionOnLang('pl') . ' 4617 ) 4618 LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl ON ( 4619 product_shop.`id_category_default` = cl.`id_category` 4620 AND cl.`id_lang` = ' . (int) $id_lang . Shop::addSqlRestrictionOnLang('cl') . ' 4621 ) 4622 LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop 4623 ON (image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop=' . (int) $this->id_shop . ') 4624 LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $id_lang . ') 4625 LEFT JOIN `' . _DB_PREFIX_ . 'manufacturer` m ON (p.`id_manufacturer`= m.`id_manufacturer`) 4626 ' . Product::sqlStock('p', 0) . ' 4627 WHERE `id_product_1` = ' . (int) $this->id . 4628 ($active ? ' AND product_shop.`active` = 1 AND product_shop.`visibility` != \'none\'' : '') . ' 4629 GROUP BY product_shop.id_product'; 4630 4631 if (!$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql)) { 4632 return []; 4633 } 4634 4635 foreach ($result as $k => &$row) { 4636 if (!Product::checkAccessStatic((int) $row['id_product'], false)) { 4637 unset($result[$k]); 4638 4639 continue; 4640 } else { 4641 $row['id_product_attribute'] = Product::getDefaultAttribute((int) $row['id_product']); 4642 } 4643 } 4644 4645 return $this->getProductsProperties($id_lang, $result); 4646 } 4647 4648 /** 4649 * @param int $accessory_id Product identifier 4650 * 4651 * @return array 4652 */ 4653 public static function getAccessoryById($accessory_id) 4654 { 4655 return Db::getInstance()->getRow('SELECT `id_product`, `name` FROM `' . _DB_PREFIX_ . 'product_lang` WHERE `id_product` = ' . (int) $accessory_id); 4656 } 4657 4658 /** 4659 * Link accessories with product 4660 * Wrapper to static method changeAccessories($accessories_id, $product_id). 4661 * 4662 * @param array $accessories_id Accessories ids 4663 */ 4664 public function changeAccessories($accessories_id) 4665 { 4666 self::changeAccessoriesForProduct($accessories_id, $this->id); 4667 } 4668 4669 /** 4670 * Link accessories with product. No need to inflate a full Product (better performances). 4671 * 4672 * @param array $accessories_id Accessories ids 4673 * @param int Product identifier 4674 */ 4675 public static function changeAccessoriesForProduct($accessories_id, $product_id) 4676 { 4677 foreach ($accessories_id as $id_product_2) { 4678 Db::getInstance()->insert('accessory', [ 4679 'id_product_1' => (int) $product_id, 4680 'id_product_2' => (int) $id_product_2, 4681 ]); 4682 } 4683 } 4684 4685 /** 4686 * Add new feature to product. 4687 * 4688 * @param int $id_value Feature identifier 4689 * @param int $lang Language identifier 4690 * @param string $cust Text of custom value 4691 * 4692 * @return bool 4693 */ 4694 public function addFeaturesCustomToDB($id_value, $lang, $cust) 4695 { 4696 $row = ['id_feature_value' => (int) $id_value, 'id_lang' => (int) $lang, 'value' => pSQL($cust)]; 4697 4698 return Db::getInstance()->insert('feature_value_lang', $row); 4699 } 4700 4701 /** 4702 * @param int $id_feature Feature identifier 4703 * @param int $id_value FeatureValue identifier 4704 * @param int $cust 1 = use a custom value, 0 = use $id_value 4705 * 4706 * @return int|string|void FeatureValue identifier or void if it fail 4707 */ 4708 public function addFeaturesToDB($id_feature, $id_value, $cust = 0) 4709 { 4710 if ($cust) { 4711 $row = ['id_feature' => (int) $id_feature, 'custom' => 1]; 4712 Db::getInstance()->insert('feature_value', $row); 4713 $id_value = Db::getInstance()->Insert_ID(); 4714 } 4715 $row = ['id_feature' => (int) $id_feature, 'id_product' => (int) $this->id, 'id_feature_value' => (int) $id_value]; 4716 Db::getInstance()->insert('feature_product', $row); 4717 SpecificPriceRule::applyAllRules([(int) $this->id]); 4718 if ($id_value) { 4719 return $id_value; 4720 } 4721 } 4722 4723 /** 4724 * @param int $id_product Product identifier 4725 * @param int $id_feature Feature identifier 4726 * @param int $id_feature_value FeatureValue identifier 4727 * 4728 * @return bool 4729 */ 4730 public static function addFeatureProductImport($id_product, $id_feature, $id_feature_value) 4731 { 4732 return Db::getInstance()->execute( 4733 ' 4734 INSERT INTO `' . _DB_PREFIX_ . 'feature_product` (`id_feature`, `id_product`, `id_feature_value`) 4735 VALUES (' . (int) $id_feature . ', ' . (int) $id_product . ', ' . (int) $id_feature_value . ') 4736 ON DUPLICATE KEY UPDATE `id_feature_value` = ' . (int) $id_feature_value 4737 ); 4738 } 4739 4740 /** 4741 * Select all features for the object. 4742 * 4743 * @return array Array with feature product's data 4744 */ 4745 public function getFeatures() 4746 { 4747 return Product::getFeaturesStatic((int) $this->id); 4748 } 4749 4750 /** 4751 * @param int $id_product Product identifier 4752 * 4753 * @return array 4754 */ 4755 public static function getFeaturesStatic($id_product) 4756 { 4757 if (!Feature::isFeatureActive()) { 4758 return []; 4759 } 4760 if (!array_key_exists($id_product, self::$_cacheFeatures)) { 4761 self::$_cacheFeatures[$id_product] = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 4762 ' 4763 SELECT fp.id_feature, fp.id_product, fp.id_feature_value, custom 4764 FROM `' . _DB_PREFIX_ . 'feature_product` fp 4765 LEFT JOIN `' . _DB_PREFIX_ . 'feature_value` fv ON (fp.id_feature_value = fv.id_feature_value) 4766 WHERE `id_product` = ' . (int) $id_product 4767 ); 4768 } 4769 4770 return self::$_cacheFeatures[$id_product]; 4771 } 4772 4773 /** 4774 * @param int[] $product_ids 4775 */ 4776 public static function cacheProductsFeatures($product_ids) 4777 { 4778 if (!Feature::isFeatureActive()) { 4779 return; 4780 } 4781 4782 $product_implode = []; 4783 foreach ($product_ids as $id_product) { 4784 if ((int) $id_product && !array_key_exists($id_product, self::$_cacheFeatures)) { 4785 $product_implode[] = (int) $id_product; 4786 } 4787 } 4788 if (!count($product_implode)) { 4789 return; 4790 } 4791 4792 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' 4793 SELECT id_feature, id_product, id_feature_value 4794 FROM `' . _DB_PREFIX_ . 'feature_product` 4795 WHERE `id_product` IN (' . implode(',', $product_implode) . ')'); 4796 foreach ($result as $row) { 4797 if (!array_key_exists($row['id_product'], self::$_cacheFeatures)) { 4798 self::$_cacheFeatures[$row['id_product']] = []; 4799 } 4800 self::$_cacheFeatures[$row['id_product']][] = $row; 4801 } 4802 } 4803 4804 /** 4805 * @param int[] $product_ids Product identifier(s) 4806 * @param int $id_lang Language identifier 4807 */ 4808 public static function cacheFrontFeatures($product_ids, $id_lang) 4809 { 4810 if (!Feature::isFeatureActive()) { 4811 return; 4812 } 4813 4814 $product_implode = []; 4815 foreach ($product_ids as $id_product) { 4816 if ((int) $id_product && !array_key_exists($id_product . '-' . $id_lang, self::$_cacheFeatures)) { 4817 $product_implode[] = (int) $id_product; 4818 } 4819 } 4820 if (!count($product_implode)) { 4821 return; 4822 } 4823 4824 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' 4825 SELECT id_product, name, value, pf.id_feature 4826 FROM ' . _DB_PREFIX_ . 'feature_product pf 4827 LEFT JOIN ' . _DB_PREFIX_ . 'feature_lang fl ON (fl.id_feature = pf.id_feature AND fl.id_lang = ' . (int) $id_lang . ') 4828 LEFT JOIN ' . _DB_PREFIX_ . 'feature_value_lang fvl ON (fvl.id_feature_value = pf.id_feature_value AND fvl.id_lang = ' . (int) $id_lang . ') 4829 LEFT JOIN ' . _DB_PREFIX_ . 'feature f ON (f.id_feature = pf.id_feature) 4830 ' . Shop::addSqlAssociation('feature', 'f') . ' 4831 WHERE `id_product` IN (' . implode(',', $product_implode) . ') 4832 ORDER BY f.position ASC'); 4833 4834 foreach ($result as $row) { 4835 if (!array_key_exists($row['id_product'] . '-' . $id_lang, self::$_frontFeaturesCache)) { 4836 self::$_frontFeaturesCache[$row['id_product'] . '-' . $id_lang] = []; 4837 } 4838 if (!isset(self::$_frontFeaturesCache[$row['id_product'] . '-' . $id_lang][$row['id_feature']])) { 4839 self::$_frontFeaturesCache[$row['id_product'] . '-' . $id_lang][$row['id_feature']] = $row; 4840 } 4841 } 4842 } 4843 4844 /** 4845 * Admin panel product search. 4846 * 4847 * @param int $id_lang Language identifier 4848 * @param string $query Search query 4849 * @param Context|null $context Deprecated, obsolete parameter not used anymore 4850 * @param int|null $limit 4851 * 4852 * @return array|false Matching products 4853 */ 4854 public static function searchByName($id_lang, $query, Context $context = null, $limit = null) 4855 { 4856 if ($context !== null) { 4857 Tools::displayParameterAsDeprecated('context'); 4858 } 4859 $sql = new DbQuery(); 4860 $sql->select('p.`id_product`, pl.`name`, p.`ean13`, p.`isbn`, p.`upc`, p.`mpn`, p.`active`, p.`reference`, m.`name` AS manufacturer_name, stock.`quantity`, product_shop.advanced_stock_management, p.`customizable`'); 4861 $sql->from('product', 'p'); 4862 $sql->join(Shop::addSqlAssociation('product', 'p')); 4863 $sql->leftJoin( 4864 'product_lang', 4865 'pl', 4866 'p.`id_product` = pl.`id_product` 4867 AND pl.`id_lang` = ' . (int) $id_lang . Shop::addSqlRestrictionOnLang('pl') 4868 ); 4869 $sql->leftJoin('manufacturer', 'm', 'm.`id_manufacturer` = p.`id_manufacturer`'); 4870 4871 $where = 'pl.`name` LIKE \'%' . pSQL($query) . '%\' 4872 OR p.`ean13` LIKE \'%' . pSQL($query) . '%\' 4873 OR p.`isbn` LIKE \'%' . pSQL($query) . '%\' 4874 OR p.`upc` LIKE \'%' . pSQL($query) . '%\' 4875 OR p.`mpn` LIKE \'%' . pSQL($query) . '%\' 4876 OR p.`reference` LIKE \'%' . pSQL($query) . '%\' 4877 OR p.`supplier_reference` LIKE \'%' . pSQL($query) . '%\' 4878 OR EXISTS(SELECT * FROM `' . _DB_PREFIX_ . 'product_supplier` sp WHERE sp.`id_product` = p.`id_product` AND `product_supplier_reference` LIKE \'%' . pSQL($query) . '%\')'; 4879 4880 $sql->orderBy('pl.`name` ASC'); 4881 4882 if ($limit) { 4883 $sql->limit($limit); 4884 } 4885 4886 if (Combination::isFeatureActive()) { 4887 $where .= ' OR EXISTS(SELECT * FROM `' . _DB_PREFIX_ . 'product_attribute` `pa` WHERE pa.`id_product` = p.`id_product` AND (pa.`reference` LIKE \'%' . pSQL($query) . '%\' 4888 OR pa.`supplier_reference` LIKE \'%' . pSQL($query) . '%\' 4889 OR pa.`ean13` LIKE \'%' . pSQL($query) . '%\' 4890 OR pa.`isbn` LIKE \'%' . pSQL($query) . '%\' 4891 OR pa.`mpn` LIKE \'%' . pSQL($query) . '%\' 4892 OR pa.`upc` LIKE \'%' . pSQL($query) . '%\'))'; 4893 } 4894 $sql->where($where); 4895 $sql->join(Product::sqlStock('p', 0)); 4896 4897 $result = Db::getInstance()->executeS($sql); 4898 4899 if (!$result) { 4900 return false; 4901 } 4902 4903 $results_array = []; 4904 foreach ($result as $row) { 4905 $row['price_tax_incl'] = Product::getPriceStatic($row['id_product'], true, null, 2); 4906 $row['price_tax_excl'] = Product::getPriceStatic($row['id_product'], false, null, 2); 4907 $results_array[] = $row; 4908 } 4909 4910 return $results_array; 4911 } 4912 4913 /** 4914 * Duplicate attributes when duplicating a product. 4915 * 4916 * @param int $id_product_old Old Product identifier 4917 * @param int $id_product_new New Product identifier 4918 * 4919 * @return array|false 4920 */ 4921 public static function duplicateAttributes($id_product_old, $id_product_new) 4922 { 4923 $return = true; 4924 $combination_images = []; 4925 4926 $result = Db::getInstance()->executeS( 4927 ' 4928 SELECT pa.*, product_attribute_shop.* 4929 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 4930 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 4931 WHERE pa.`id_product` = ' . (int) $id_product_old 4932 ); 4933 $combinations = []; 4934 $product_supplier_keys = []; 4935 4936 foreach ($result as $row) { 4937 $id_product_attribute_old = (int) $row['id_product_attribute']; 4938 if (!isset($combinations[$id_product_attribute_old])) { 4939 $id_combination = null; 4940 $id_shop = null; 4941 $result2 = Db::getInstance()->executeS( 4942 ' 4943 SELECT * 4944 FROM `' . _DB_PREFIX_ . 'product_attribute_combination` 4945 WHERE `id_product_attribute` = ' . $id_product_attribute_old 4946 ); 4947 } else { 4948 $id_combination = (int) $combinations[$id_product_attribute_old]; 4949 $id_shop = (int) $row['id_shop']; 4950 $context_old = Shop::getContext(); 4951 $context_shop_id_old = Shop::getContextShopID(); 4952 Shop::setContext(Shop::CONTEXT_SHOP, $id_shop); 4953 } 4954 4955 $row['id_product'] = $id_product_new; 4956 unset($row['id_product_attribute']); 4957 4958 $combination = new Combination($id_combination, null, $id_shop); 4959 foreach ($row as $k => $v) { 4960 $combination->$k = $v; 4961 } 4962 $return &= $combination->save(); 4963 4964 $id_product_attribute_new = (int) $combination->id; 4965 4966 if ($result_images = Product::_getAttributeImageAssociations($id_product_attribute_old)) { 4967 $combination_images['old'][$id_product_attribute_old] = $result_images; 4968 $combination_images['new'][$id_product_attribute_new] = $result_images; 4969 } 4970 4971 if (!isset($combinations[$id_product_attribute_old])) { 4972 $combinations[$id_product_attribute_old] = (int) $id_product_attribute_new; 4973 foreach ($result2 as $row2) { 4974 $row2['id_product_attribute'] = $id_product_attribute_new; 4975 $return &= Db::getInstance()->insert('product_attribute_combination', $row2); 4976 } 4977 } else { 4978 Shop::setContext($context_old, $context_shop_id_old); 4979 } 4980 4981 //Copy suppliers 4982 $result3 = Db::getInstance()->executeS(' 4983 SELECT * 4984 FROM `' . _DB_PREFIX_ . 'product_supplier` 4985 WHERE `id_product_attribute` = ' . (int) $id_product_attribute_old . ' 4986 AND `id_product` = ' . (int) $id_product_old); 4987 4988 foreach ($result3 as $row3) { 4989 $current_supplier_key = $id_product_new . '_' . $id_product_attribute_new . '_' . $row3['id_supplier']; 4990 4991 if (in_array($current_supplier_key, $product_supplier_keys)) { 4992 continue; 4993 } 4994 4995 $product_supplier_keys[] = $current_supplier_key; 4996 4997 unset($row3['id_product_supplier']); 4998 $row3['id_product'] = $id_product_new; 4999 $row3['id_product_attribute'] = $id_product_attribute_new; 5000 $return &= Db::getInstance()->insert('product_supplier', $row3); 5001 } 5002 } 5003 5004 $impacts = self::getAttributesImpacts($id_product_old); 5005 5006 if (is_array($impacts) && count($impacts)) { 5007 $impact_sql = 'INSERT INTO `' . _DB_PREFIX_ . 'attribute_impact` (`id_product`, `id_attribute`, `weight`, `price`) VALUES '; 5008 5009 foreach ($impacts as $id_attribute => $impact) { 5010 $impact_sql .= '(' . (int) $id_product_new . ', ' . (int) $id_attribute . ', ' . (float) $impacts[$id_attribute]['weight'] . ', ' 5011 . (float) $impacts[$id_attribute]['price'] . '),'; 5012 } 5013 5014 $impact_sql = substr_replace($impact_sql, '', -1); 5015 $impact_sql .= ' ON DUPLICATE KEY UPDATE `price` = VALUES(price), `weight` = VALUES(weight)'; 5016 5017 Db::getInstance()->execute($impact_sql); 5018 } 5019 5020 return !$return ? false : $combination_images; 5021 } 5022 5023 /** 5024 * @param int $id_product Product identifier 5025 * 5026 * @return array 5027 */ 5028 public static function getAttributesImpacts($id_product) 5029 { 5030 $return = []; 5031 $result = Db::getInstance()->executeS( 5032 'SELECT ai.`id_attribute`, ai.`price`, ai.`weight` 5033 FROM `' . _DB_PREFIX_ . 'attribute_impact` ai 5034 WHERE ai.`id_product` = ' . (int) $id_product 5035 ); 5036 5037 if (!$result) { 5038 return []; 5039 } 5040 foreach ($result as $impact) { 5041 $return[$impact['id_attribute']]['price'] = (float) $impact['price']; 5042 $return[$impact['id_attribute']]['weight'] = (float) $impact['weight']; 5043 } 5044 5045 return $return; 5046 } 5047 5048 /** 5049 * Get product attribute image associations. 5050 * 5051 * @param int $id_product_attribute Attribute identifier 5052 * 5053 * @return array 5054 */ 5055 public static function _getAttributeImageAssociations($id_product_attribute) 5056 { 5057 $combination_images = []; 5058 $data = Db::getInstance()->executeS(' 5059 SELECT `id_image` 5060 FROM `' . _DB_PREFIX_ . 'product_attribute_image` 5061 WHERE `id_product_attribute` = ' . (int) $id_product_attribute); 5062 foreach ($data as $row) { 5063 $combination_images[] = (int) $row['id_image']; 5064 } 5065 5066 return $combination_images; 5067 } 5068 5069 /** 5070 * @param int $id_product_old Old Product identifier 5071 * @param int $id_product_new New Product identifier 5072 * 5073 * @return bool|int 5074 */ 5075 public static function duplicateAccessories($id_product_old, $id_product_new) 5076 { 5077 $return = true; 5078 5079 $result = Db::getInstance()->executeS(' 5080 SELECT * 5081 FROM `' . _DB_PREFIX_ . 'accessory` 5082 WHERE `id_product_1` = ' . (int) $id_product_old); 5083 foreach ($result as $row) { 5084 $data = [ 5085 'id_product_1' => (int) $id_product_new, 5086 'id_product_2' => (int) $row['id_product_2'], 5087 ]; 5088 $return &= Db::getInstance()->insert('accessory', $data); 5089 } 5090 5091 return $return; 5092 } 5093 5094 /** 5095 * @param int $id_product_old Old Product identifier 5096 * @param int $id_product_new New Product identifier 5097 * 5098 * @return bool 5099 */ 5100 public static function duplicateTags($id_product_old, $id_product_new) 5101 { 5102 $tags = Db::getInstance()->executeS('SELECT `id_tag`, `id_lang` FROM `' . _DB_PREFIX_ . 'product_tag` WHERE `id_product` = ' . (int) $id_product_old); 5103 if (!Db::getInstance()->numRows()) { 5104 return true; 5105 } 5106 5107 $data = []; 5108 foreach ($tags as $tag) { 5109 $data[] = [ 5110 'id_product' => (int) $id_product_new, 5111 'id_tag' => (int) $tag['id_tag'], 5112 'id_lang' => (int) $tag['id_lang'], 5113 ]; 5114 } 5115 5116 return Db::getInstance()->insert('product_tag', $data); 5117 } 5118 5119 /** 5120 * @param int $id_product_old Old Product identifier 5121 * @param int $id_product_new New Product identifier 5122 * 5123 * @return bool 5124 */ 5125 public static function duplicateTaxes($id_product_old, $id_product_new) 5126 { 5127 $query = new DbQuery(); 5128 $query->select('id_tax_rules_group, id_shop'); 5129 $query->from('product_shop'); 5130 $query->where('`id_product` = ' . (int) $id_product_old); 5131 5132 $results = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query->build()); 5133 5134 if (!empty($results)) { 5135 foreach ($results as $result) { 5136 if (!Db::getInstance()->update( 5137 'product_shop', 5138 ['id_tax_rules_group' => (int) $result['id_tax_rules_group']], 5139 'id_product=' . (int) $id_product_new . ' AND id_shop = ' . (int) $result['id_shop'] 5140 )) { 5141 return false; 5142 } 5143 } 5144 } 5145 5146 return true; 5147 } 5148 5149 /** 5150 * Duplicate prices when duplicating a product. 5151 * 5152 * @param int $id_product_old Old Product identifier 5153 * @param int $id_product_new New Product identifier 5154 * 5155 * @return bool 5156 */ 5157 public static function duplicatePrices($id_product_old, $id_product_new) 5158 { 5159 $query = new DbQuery(); 5160 $query->select('price, unit_price_ratio, id_shop'); 5161 $query->from('product_shop'); 5162 $query->where('`id_product` = ' . (int) $id_product_old); 5163 $results = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query->build()); 5164 if (!empty($results)) { 5165 foreach ($results as $result) { 5166 if (!Db::getInstance()->update( 5167 'product_shop', 5168 ['price' => pSQL($result['price']), 'unit_price_ratio' => pSQL($result['unit_price_ratio'])], 5169 'id_product=' . (int) $id_product_new . ' AND id_shop = ' . (int) $result['id_shop'] 5170 )) { 5171 return false; 5172 } 5173 } 5174 } 5175 5176 return true; 5177 } 5178 5179 /** 5180 * @param int $id_product_old Old Product identifier 5181 * @param int $id_product_new New Product identifier 5182 * 5183 * @return bool 5184 */ 5185 public static function duplicateDownload($id_product_old, $id_product_new) 5186 { 5187 $sql = 'SELECT `display_filename`, `filename`, `date_add`, `date_expiration`, `nb_days_accessible`, `nb_downloadable`, `active`, `is_shareable` 5188 FROM `' . _DB_PREFIX_ . 'product_download` 5189 WHERE `id_product` = ' . (int) $id_product_old; 5190 $results = Db::getInstance()->executeS($sql); 5191 if (!$results) { 5192 return true; 5193 } 5194 5195 $data = []; 5196 foreach ($results as $row) { 5197 $new_filename = ProductDownload::getNewFilename(); 5198 copy(_PS_DOWNLOAD_DIR_ . $row['filename'], _PS_DOWNLOAD_DIR_ . $new_filename); 5199 5200 $data[] = [ 5201 'id_product' => (int) $id_product_new, 5202 'display_filename' => pSQL($row['display_filename']), 5203 'filename' => pSQL($new_filename), 5204 'date_expiration' => pSQL($row['date_expiration']), 5205 'nb_days_accessible' => (int) $row['nb_days_accessible'], 5206 'nb_downloadable' => (int) $row['nb_downloadable'], 5207 'active' => (int) $row['active'], 5208 'is_shareable' => (int) $row['is_shareable'], 5209 'date_add' => date('Y-m-d H:i:s'), 5210 ]; 5211 } 5212 5213 return Db::getInstance()->insert('product_download', $data); 5214 } 5215 5216 /** 5217 * @param int $id_product_old Old Product identifier 5218 * @param int $id_product_new New Product identifier 5219 * 5220 * @return bool 5221 */ 5222 public static function duplicateAttachments($id_product_old, $id_product_new) 5223 { 5224 // Get all ids attachments of the old product 5225 $sql = 'SELECT `id_attachment` FROM `' . _DB_PREFIX_ . 'product_attachment` WHERE `id_product` = ' . (int) $id_product_old; 5226 $results = Db::getInstance()->executeS($sql); 5227 5228 if (!$results) { 5229 return true; 5230 } 5231 5232 $data = []; 5233 5234 // Prepare data of table product_attachment 5235 foreach ($results as $row) { 5236 $data[] = [ 5237 'id_product' => (int) $id_product_new, 5238 'id_attachment' => (int) $row['id_attachment'], 5239 ]; 5240 } 5241 5242 // Duplicate product attachement 5243 $res = Db::getInstance()->insert('product_attachment', $data); 5244 Product::updateCacheAttachment((int) $id_product_new); 5245 5246 return $res; 5247 } 5248 5249 /** 5250 * Duplicate features when duplicating a product. 5251 * 5252 * @param int $id_product_old Old Product identifier 5253 * @param int $id_product_new New Product identifier 5254 * 5255 * @return bool 5256 */ 5257 public static function duplicateFeatures($id_product_old, $id_product_new) 5258 { 5259 $return = true; 5260 5261 $result = Db::getInstance()->executeS(' 5262 SELECT * 5263 FROM `' . _DB_PREFIX_ . 'feature_product` 5264 WHERE `id_product` = ' . (int) $id_product_old); 5265 foreach ($result as $row) { 5266 $result2 = Db::getInstance()->getRow(' 5267 SELECT * 5268 FROM `' . _DB_PREFIX_ . 'feature_value` 5269 WHERE `id_feature_value` = ' . (int) $row['id_feature_value']); 5270 // Custom feature value, need to duplicate it 5271 if ($result2['custom']) { 5272 $old_id_feature_value = $result2['id_feature_value']; 5273 unset($result2['id_feature_value']); 5274 $return &= Db::getInstance()->insert('feature_value', $result2); 5275 $max_fv = Db::getInstance()->getRow(' 5276 SELECT MAX(`id_feature_value`) AS nb 5277 FROM `' . _DB_PREFIX_ . 'feature_value`'); 5278 $new_id_feature_value = $max_fv['nb']; 5279 5280 foreach (Language::getIDs(false) as $id_lang) { 5281 $result3 = Db::getInstance()->getRow(' 5282 SELECT * 5283 FROM `' . _DB_PREFIX_ . 'feature_value_lang` 5284 WHERE `id_feature_value` = ' . (int) $old_id_feature_value . ' 5285 AND `id_lang` = ' . (int) $id_lang); 5286 5287 if ($result3) { 5288 $result3['id_feature_value'] = (int) $new_id_feature_value; 5289 $result3['value'] = pSQL($result3['value']); 5290 $return &= Db::getInstance()->insert('feature_value_lang', $result3); 5291 } 5292 } 5293 $row['id_feature_value'] = $new_id_feature_value; 5294 } 5295 5296 $row['id_product'] = (int) $id_product_new; 5297 $return &= Db::getInstance()->insert('feature_product', $row); 5298 } 5299 5300 return $return; 5301 } 5302 5303 /** 5304 * @param int $product_id Product identifier 5305 * @param int|null $id_shop Shop identifier 5306 * 5307 * @return array|false 5308 */ 5309 protected static function _getCustomizationFieldsNLabels($product_id, $id_shop = null) 5310 { 5311 if (!Customization::isFeatureActive()) { 5312 return false; 5313 } 5314 5315 if (Shop::isFeatureActive() && !$id_shop) { 5316 $id_shop = (int) Context::getContext()->shop->id; 5317 } 5318 5319 $customizations = []; 5320 if (($customizations['fields'] = Db::getInstance()->executeS(' 5321 SELECT `id_customization_field`, `type`, `required` 5322 FROM `' . _DB_PREFIX_ . 'customization_field` 5323 WHERE `id_product` = ' . (int) $product_id . ' 5324 ORDER BY `id_customization_field`')) === false) { 5325 return false; 5326 } 5327 5328 if (empty($customizations['fields'])) { 5329 return []; 5330 } 5331 5332 $customization_field_ids = []; 5333 foreach ($customizations['fields'] as $customization_field) { 5334 $customization_field_ids[] = (int) $customization_field['id_customization_field']; 5335 } 5336 5337 if (($customization_labels = Db::getInstance()->executeS(' 5338 SELECT `id_customization_field`, `id_lang`, `id_shop`, `name` 5339 FROM `' . _DB_PREFIX_ . 'customization_field_lang` 5340 WHERE `id_customization_field` IN (' . implode(', ', $customization_field_ids) . ')' . ($id_shop ? ' AND `id_shop` = ' . (int) $id_shop : '') . ' 5341 ORDER BY `id_customization_field`')) === false) { 5342 return false; 5343 } 5344 5345 foreach ($customization_labels as $customization_label) { 5346 $customizations['labels'][$customization_label['id_customization_field']][] = $customization_label; 5347 } 5348 5349 return $customizations; 5350 } 5351 5352 /** 5353 * @param int $old_product_id Old Product identifier 5354 * @param int $product_id New Product identifier 5355 * 5356 * @return bool 5357 */ 5358 public static function duplicateSpecificPrices($old_product_id, $product_id) 5359 { 5360 foreach (SpecificPrice::getIdsByProductId((int) $old_product_id) as $data) { 5361 $specific_price = new SpecificPrice((int) $data['id_specific_price']); 5362 if (!$specific_price->duplicate((int) $product_id)) { 5363 return false; 5364 } 5365 } 5366 5367 return true; 5368 } 5369 5370 /** 5371 * @param int $old_product_id Old Product identifier 5372 * @param int $product_id New Product identifier 5373 * 5374 * @return bool 5375 */ 5376 public static function duplicateCustomizationFields($old_product_id, $product_id) 5377 { 5378 // If customization is not activated, return success 5379 if (!Customization::isFeatureActive()) { 5380 return true; 5381 } 5382 if (($customizations = Product::_getCustomizationFieldsNLabels($old_product_id)) === false) { 5383 return false; 5384 } 5385 if (empty($customizations)) { 5386 return true; 5387 } 5388 foreach ($customizations['fields'] as $customization_field) { 5389 /* The new datas concern the new product */ 5390 $customization_field['id_product'] = (int) $product_id; 5391 $old_customization_field_id = (int) $customization_field['id_customization_field']; 5392 5393 unset($customization_field['id_customization_field']); 5394 5395 if (!Db::getInstance()->insert('customization_field', $customization_field) 5396 || !$customization_field_id = Db::getInstance()->Insert_ID()) { 5397 return false; 5398 } 5399 5400 if (isset($customizations['labels'])) { 5401 foreach ($customizations['labels'][$old_customization_field_id] as $customization_label) { 5402 $data = [ 5403 'id_customization_field' => (int) $customization_field_id, 5404 'id_lang' => (int) $customization_label['id_lang'], 5405 'id_shop' => (int) $customization_label['id_shop'], 5406 'name' => pSQL($customization_label['name']), 5407 ]; 5408 5409 if (!Db::getInstance()->insert('customization_field_lang', $data)) { 5410 return false; 5411 } 5412 } 5413 } 5414 } 5415 5416 return true; 5417 } 5418 5419 /** 5420 * Adds suppliers from old product onto a newly duplicated product. 5421 * 5422 * @param int $id_product_old Old Product identifier 5423 * @param int $id_product_new New Product identifier 5424 * 5425 * @return bool 5426 */ 5427 public static function duplicateSuppliers($id_product_old, $id_product_new) 5428 { 5429 $result = Db::getInstance()->executeS(' 5430 SELECT * 5431 FROM `' . _DB_PREFIX_ . 'product_supplier` 5432 WHERE `id_product` = ' . (int) $id_product_old . ' AND `id_product_attribute` = 0'); 5433 5434 foreach ($result as $row) { 5435 unset($row['id_product_supplier']); 5436 $row['id_product'] = $id_product_new; 5437 if (!Db::getInstance()->insert('product_supplier', $row)) { 5438 return false; 5439 } 5440 } 5441 5442 return true; 5443 } 5444 5445 /** 5446 * Adds carriers from old product onto a newly duplicated product. 5447 * 5448 * @param int $oldProductId Old Product identifier 5449 * @param int $newProductId New Product identifier 5450 * 5451 * @return bool 5452 */ 5453 public static function duplicateCarriers(int $oldProductId, int $newProductId): bool 5454 { 5455 //@todo: this will copy carriers from all shops. todo - Handle multishop according context & specifications. 5456 $oldProductCarriers = Db::getInstance()->executeS(' 5457 SELECT * 5458 FROM `' . _DB_PREFIX_ . 'product_carrier` 5459 WHERE `id_product` = ' . (int) $oldProductId 5460 ); 5461 5462 foreach ($oldProductCarriers as $row) { 5463 $row['id_product'] = $newProductId; 5464 if (!Db::getInstance()->insert('product_carrier', $row)) { 5465 return false; 5466 } 5467 } 5468 5469 return true; 5470 } 5471 5472 /** 5473 * Associates attachments from old product onto a newly duplicated product. 5474 * 5475 * @param int $oldProductId Old Product identifier 5476 * @param int $newProductId New Product identifier 5477 * 5478 * @return bool 5479 */ 5480 public static function duplicateAttachmentAssociation(int $oldProductId, int $newProductId): bool 5481 { 5482 $oldProductAttachments = Db::getInstance()->executeS(' 5483 SELECT * 5484 FROM `' . _DB_PREFIX_ . 'product_attachment` 5485 WHERE `id_product` = ' . (int) $oldProductId 5486 ); 5487 5488 foreach ($oldProductAttachments as $row) { 5489 $row['id_product'] = $newProductId; 5490 if (!Db::getInstance()->insert('product_attachment', $row)) { 5491 return false; 5492 } 5493 } 5494 5495 return true; 5496 } 5497 5498 /** 5499 * Get the link of the product page of this product. 5500 * 5501 * @param Context|null $context 5502 * 5503 * @return string 5504 */ 5505 public function getLink(Context $context = null) 5506 { 5507 if (!$context) { 5508 $context = Context::getContext(); 5509 } 5510 5511 return $context->link->getProductLink($this); 5512 } 5513 5514 /** 5515 * @param int $id_lang Language identifier 5516 * 5517 * @return string 5518 */ 5519 public function getTags($id_lang) 5520 { 5521 if (!$this->isFullyLoaded && null === $this->tags) { 5522 $this->tags = Tag::getProductTags($this->id); 5523 } 5524 5525 if (!($this->tags && array_key_exists($id_lang, $this->tags))) { 5526 return ''; 5527 } 5528 5529 $result = ''; 5530 foreach ($this->tags[$id_lang] as $tag_name) { 5531 $result .= $tag_name . ', '; 5532 } 5533 5534 return rtrim($result, ', '); 5535 } 5536 5537 /** 5538 * @param array $row 5539 * @param int $id_lang Language identifier 5540 * 5541 * @return string 5542 */ 5543 public static function defineProductImage($row, $id_lang) 5544 { 5545 if (isset($row['id_image']) && $row['id_image']) { 5546 return $row['id_product'] . '-' . $row['id_image']; 5547 } 5548 5549 return Language::getIsoById((int) $id_lang) . '-default'; 5550 } 5551 5552 /** 5553 * @param int $id_lang Language identifier 5554 * @param array $row 5555 * @param Context|null $context 5556 * 5557 * @return array|false 5558 */ 5559 public static function getProductProperties($id_lang, $row, Context $context = null) 5560 { 5561 Hook::exec('actionGetProductPropertiesBefore', [ 5562 'id_lang' => $id_lang, 5563 'product' => &$row, 5564 'context' => $context, 5565 ]); 5566 5567 if (!$row['id_product']) { 5568 return false; 5569 } 5570 5571 if ($context == null) { 5572 $context = Context::getContext(); 5573 } 5574 5575 $id_product_attribute = $row['id_product_attribute'] = (!empty($row['id_product_attribute']) ? (int) $row['id_product_attribute'] : null); 5576 5577 // Product::getDefaultAttribute is only called if id_product_attribute is missing from the SQL query at the origin of it: 5578 // consider adding it in order to avoid unnecessary queries 5579 $row['allow_oosp'] = Product::isAvailableWhenOutOfStock($row['out_of_stock']); 5580 if (Combination::isFeatureActive() && $id_product_attribute === null 5581 && ((isset($row['cache_default_attribute']) && ($ipa_default = $row['cache_default_attribute']) !== null) 5582 || ($ipa_default = Product::getDefaultAttribute($row['id_product'], !$row['allow_oosp'])))) { 5583 $id_product_attribute = $row['id_product_attribute'] = $ipa_default; 5584 } 5585 if (!Combination::isFeatureActive() || !isset($row['id_product_attribute'])) { 5586 $id_product_attribute = $row['id_product_attribute'] = 0; 5587 } 5588 5589 // Tax 5590 $usetax = !Tax::excludeTaxeOption(); 5591 5592 $cache_key = $row['id_product'] . '-' . $id_product_attribute . '-' . $id_lang . '-' . (int) $usetax; 5593 if (isset($row['id_product_pack'])) { 5594 $cache_key .= '-pack' . $row['id_product_pack']; 5595 } 5596 5597 if (!isset($row['cover_image_id'])) { 5598 $cover = static::getCover($row['id_product']); 5599 if (isset($cover['id_image'])) { 5600 $row['cover_image_id'] = $cover['id_image']; 5601 } 5602 } 5603 5604 if (isset($row['cover_image_id'])) { 5605 $cache_key .= '-cover' . (int) $row['cover_image_id']; 5606 } 5607 5608 if (isset(self::$productPropertiesCache[$cache_key])) { 5609 return array_merge($row, self::$productPropertiesCache[$cache_key]); 5610 } 5611 5612 // Datas 5613 $row['category'] = Category::getLinkRewrite((int) $row['id_category_default'], (int) $id_lang); 5614 $row['category_name'] = Db::getInstance()->getValue('SELECT name FROM ' . _DB_PREFIX_ . 'category_lang WHERE id_shop = ' . (int) $context->shop->id . ' AND id_lang = ' . (int) $id_lang . ' AND id_category = ' . (int) $row['id_category_default']); 5615 $row['link'] = $context->link->getProductLink((int) $row['id_product'], $row['link_rewrite'], $row['category'], $row['ean13']); 5616 5617 $row['attribute_price'] = 0; 5618 if ($id_product_attribute) { 5619 $row['attribute_price'] = (float) Combination::getPrice($id_product_attribute); 5620 } 5621 5622 if (isset($row['quantity_wanted'])) { 5623 // 'quantity_wanted' may very well be zero even if set 5624 $quantity = max((int) $row['minimal_quantity'], (int) $row['quantity_wanted']); 5625 } elseif (isset($row['cart_quantity'])) { 5626 $quantity = max((int) $row['minimal_quantity'], (int) $row['cart_quantity']); 5627 } else { 5628 $quantity = (int) $row['minimal_quantity']; 5629 } 5630 5631 $row['price_tax_exc'] = Product::getPriceStatic( 5632 (int) $row['id_product'], 5633 false, 5634 $id_product_attribute, 5635 (self::$_taxCalculationMethod == PS_TAX_EXC ? Context::getContext()->getComputingPrecision() : 6), 5636 null, 5637 false, 5638 true, 5639 $quantity 5640 ); 5641 5642 if (self::$_taxCalculationMethod == PS_TAX_EXC) { 5643 $row['price_tax_exc'] = Tools::ps_round($row['price_tax_exc'], Context::getContext()->getComputingPrecision()); 5644 $row['price'] = Product::getPriceStatic( 5645 (int) $row['id_product'], 5646 true, 5647 $id_product_attribute, 5648 6, 5649 null, 5650 false, 5651 true, 5652 $quantity 5653 ); 5654 $row['price_without_reduction'] = 5655 $row['price_without_reduction_without_tax'] = Product::getPriceStatic( 5656 (int) $row['id_product'], 5657 false, 5658 $id_product_attribute, 5659 2, 5660 null, 5661 false, 5662 false, 5663 $quantity 5664 ); 5665 } else { 5666 $row['price'] = Tools::ps_round( 5667 Product::getPriceStatic( 5668 (int) $row['id_product'], 5669 true, 5670 $id_product_attribute, 5671 6, 5672 null, 5673 false, 5674 true, 5675 $quantity 5676 ), 5677 Context::getContext()->getComputingPrecision() 5678 ); 5679 $row['price_without_reduction'] = Product::getPriceStatic( 5680 (int) $row['id_product'], 5681 true, 5682 $id_product_attribute, 5683 6, 5684 null, 5685 false, 5686 false, 5687 $quantity 5688 ); 5689 $row['price_without_reduction_without_tax'] = Product::getPriceStatic( 5690 (int) $row['id_product'], 5691 false, 5692 $id_product_attribute, 5693 6, 5694 null, 5695 false, 5696 false, 5697 $quantity 5698 ); 5699 } 5700 5701 $row['reduction'] = Product::getPriceStatic( 5702 (int) $row['id_product'], 5703 (bool) $usetax, 5704 $id_product_attribute, 5705 6, 5706 null, 5707 true, 5708 true, 5709 $quantity, 5710 true, 5711 null, 5712 null, 5713 null, 5714 $specific_prices 5715 ); 5716 5717 $row['reduction_without_tax'] = Product::getPriceStatic( 5718 (int) $row['id_product'], 5719 false, 5720 $id_product_attribute, 5721 6, 5722 null, 5723 true, 5724 true, 5725 $quantity, 5726 true, 5727 null, 5728 null, 5729 null, 5730 $specific_prices 5731 ); 5732 5733 $row['specific_prices'] = $specific_prices; 5734 5735 $row['quantity'] = Product::getQuantity( 5736 (int) $row['id_product'], 5737 0, 5738 isset($row['cache_is_pack']) ? $row['cache_is_pack'] : null, 5739 $context->cart 5740 ); 5741 5742 $row['quantity_all_versions'] = $row['quantity']; 5743 5744 if ($row['id_product_attribute']) { 5745 $row['quantity'] = Product::getQuantity( 5746 (int) $row['id_product'], 5747 $id_product_attribute, 5748 isset($row['cache_is_pack']) ? $row['cache_is_pack'] : null, 5749 $context->cart 5750 ); 5751 5752 $row['available_date'] = Product::getAvailableDate( 5753 (int) $row['id_product'], 5754 $id_product_attribute 5755 ); 5756 } 5757 5758 $row['id_image'] = Product::defineProductImage($row, $id_lang); 5759 $row['features'] = Product::getFrontFeaturesStatic((int) $id_lang, $row['id_product']); 5760 5761 $row['attachments'] = []; 5762 if (!isset($row['cache_has_attachments']) || $row['cache_has_attachments']) { 5763 $row['attachments'] = Product::getAttachmentsStatic((int) $id_lang, $row['id_product']); 5764 } 5765 5766 $row['virtual'] = ((!isset($row['is_virtual']) || $row['is_virtual']) ? 1 : 0); 5767 5768 // Pack management 5769 $row['pack'] = (!isset($row['cache_is_pack']) ? Pack::isPack($row['id_product']) : (int) $row['cache_is_pack']); 5770 $row['packItems'] = $row['pack'] ? Pack::getItemTable($row['id_product'], $id_lang) : []; 5771 $row['nopackprice'] = $row['pack'] ? Pack::noPackPrice($row['id_product']) : 0; 5772 5773 if ($row['pack'] && !Pack::isInStock($row['id_product'], $quantity, $context->cart)) { 5774 $row['quantity'] = 0; 5775 } 5776 5777 $row['customization_required'] = false; 5778 if (isset($row['customizable']) && $row['customizable'] && Customization::isFeatureActive()) { 5779 if (count(Product::getRequiredCustomizableFieldsStatic((int) $row['id_product']))) { 5780 $row['customization_required'] = true; 5781 } 5782 } 5783 5784 if (!isset($row['attributes'])) { 5785 $attributes = Product::getAttributesParams($row['id_product'], $row['id_product_attribute']); 5786 5787 foreach ($attributes as $attribute) { 5788 $row['attributes'][$attribute['id_attribute_group']] = $attribute; 5789 } 5790 } 5791 5792 $row = Product::getTaxesInformations($row, $context); 5793 5794 $row['ecotax_rate'] = (float) Tax::getProductEcotaxRate($context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}); 5795 5796 Hook::exec('actionGetProductPropertiesAfter', [ 5797 'id_lang' => $id_lang, 5798 'product' => &$row, 5799 'context' => $context, 5800 ]); 5801 5802 $combination = new Combination($id_product_attribute); 5803 5804 if (0 != $combination->unit_price_impact && 0 != $row['unit_price_ratio']) { 5805 $unitPrice = ($row['price_tax_exc'] / $row['unit_price_ratio']) + $combination->unit_price_impact; 5806 $row['unit_price_ratio'] = $row['price_tax_exc'] / $unitPrice; 5807 } 5808 5809 if (isset($row['unit_price_ratio'])) { 5810 $row['unit_price'] = ($row['unit_price_ratio'] != 0 ? $row['price'] / $row['unit_price_ratio'] : 0); 5811 } else { 5812 $row['unit_price'] = 0.0; 5813 } 5814 5815 Hook::exec('actionGetProductPropertiesAfterUnitPrice', [ 5816 'id_lang' => $id_lang, 5817 'product' => &$row, 5818 'context' => $context, 5819 ]); 5820 5821 self::$productPropertiesCache[$cache_key] = $row; 5822 5823 return self::$productPropertiesCache[$cache_key]; 5824 } 5825 5826 /** 5827 * @param array $row 5828 * @param Context|null $context 5829 * 5830 * @return array 5831 */ 5832 public static function getTaxesInformations($row, Context $context = null) 5833 { 5834 static $address = null; 5835 5836 if ($context === null) { 5837 $context = Context::getContext(); 5838 } 5839 if ($address === null) { 5840 $address = new Address(); 5841 } 5842 5843 $address->id_country = (int) $context->country->id; 5844 $address->id_state = 0; 5845 $address->postcode = 0; 5846 5847 $tax_manager = TaxManagerFactory::getManager($address, Product::getIdTaxRulesGroupByIdProduct((int) $row['id_product'], $context)); 5848 $row['rate'] = $tax_manager->getTaxCalculator()->getTotalRate(); 5849 $row['tax_name'] = $tax_manager->getTaxCalculator()->getTaxesName(); 5850 5851 return $row; 5852 } 5853 5854 /** 5855 * @param int $id_lang Language identifier 5856 * @param array $query_result 5857 * 5858 * @return array 5859 */ 5860 public static function getProductsProperties($id_lang, $query_result) 5861 { 5862 $results_array = []; 5863 5864 if (is_array($query_result)) { 5865 foreach ($query_result as $row) { 5866 if ($row2 = Product::getProductProperties($id_lang, $row)) { 5867 $results_array[] = $row2; 5868 } 5869 } 5870 } 5871 5872 return $results_array; 5873 } 5874 5875 /** 5876 * Select all features for a given language 5877 * 5878 * @param int $id_lang Language identifier 5879 * @param int $id_product Product identifier 5880 * 5881 * @return array Array with feature's data 5882 */ 5883 public static function getFrontFeaturesStatic($id_lang, $id_product) 5884 { 5885 if (!Feature::isFeatureActive()) { 5886 return []; 5887 } 5888 if (!array_key_exists($id_product . '-' . $id_lang, self::$_frontFeaturesCache)) { 5889 self::$_frontFeaturesCache[$id_product . '-' . $id_lang] = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 5890 ' 5891 SELECT name, value, pf.id_feature, f.position 5892 FROM ' . _DB_PREFIX_ . 'feature_product pf 5893 LEFT JOIN ' . _DB_PREFIX_ . 'feature_lang fl ON (fl.id_feature = pf.id_feature AND fl.id_lang = ' . (int) $id_lang . ') 5894 LEFT JOIN ' . _DB_PREFIX_ . 'feature_value_lang fvl ON (fvl.id_feature_value = pf.id_feature_value AND fvl.id_lang = ' . (int) $id_lang . ') 5895 LEFT JOIN ' . _DB_PREFIX_ . 'feature f ON (f.id_feature = pf.id_feature AND fl.id_lang = ' . (int) $id_lang . ') 5896 ' . Shop::addSqlAssociation('feature', 'f') . ' 5897 WHERE pf.id_product = ' . (int) $id_product . ' 5898 ORDER BY f.position ASC' 5899 ); 5900 } 5901 5902 return self::$_frontFeaturesCache[$id_product . '-' . $id_lang]; 5903 } 5904 5905 /** 5906 * @param int $id_lang Language identifier 5907 * 5908 * @return array 5909 */ 5910 public function getFrontFeatures($id_lang) 5911 { 5912 return Product::getFrontFeaturesStatic($id_lang, $this->id); 5913 } 5914 5915 /** 5916 * @param int $id_lang Language identifier 5917 * @param int $id_product Product identifier 5918 * 5919 * @return array 5920 */ 5921 public static function getAttachmentsStatic($id_lang, $id_product) 5922 { 5923 return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' 5924 SELECT * 5925 FROM ' . _DB_PREFIX_ . 'product_attachment pa 5926 LEFT JOIN ' . _DB_PREFIX_ . 'attachment a ON a.id_attachment = pa.id_attachment 5927 LEFT JOIN ' . _DB_PREFIX_ . 'attachment_lang al ON (a.id_attachment = al.id_attachment AND al.id_lang = ' . (int) $id_lang . ') 5928 WHERE pa.id_product = ' . (int) $id_product); 5929 } 5930 5931 /** 5932 * @return int[] 5933 * 5934 * @throws PrestaShopDatabaseException 5935 */ 5936 public function getAssociatedAttachmentIds(): array 5937 { 5938 $results = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' 5939 SELECT id_attachment 5940 FROM ' . _DB_PREFIX_ . 'product_attachment 5941 WHERE id_product = ' . (int) $this->id 5942 ); 5943 5944 if (!$results) { 5945 return []; 5946 } 5947 5948 return array_map(function (array $result): int { 5949 return (int) $result['id_attachment']; 5950 }, $results); 5951 } 5952 5953 /** 5954 * @param int $id_lang Language identifier 5955 * 5956 * @return array 5957 */ 5958 public function getAttachments($id_lang) 5959 { 5960 return Product::getAttachmentsStatic($id_lang, $this->id); 5961 } 5962 5963 /** 5964 * Customization management 5965 * 5966 * @param int $id_cart Cart identifier 5967 * @param int|null $id_lang Language identifier 5968 * @param bool $only_in_cart 5969 * @param int|null $id_shop Shop identifier 5970 * @param int|null $id_customization Customization identifier 5971 * 5972 * @return array|false 5973 */ 5974 public static function getAllCustomizedDatas($id_cart, $id_lang = null, $only_in_cart = true, $id_shop = null, $id_customization = null) 5975 { 5976 if (!Customization::isFeatureActive()) { 5977 return false; 5978 } 5979 5980 // No need to query if there isn't any real cart! 5981 if (!$id_cart) { 5982 return false; 5983 } 5984 5985 if ($id_customization === 0) { 5986 // Backward compatibility: check if there are no products in cart with specific `id_customization` before returning false 5987 $product_customizations = (int) Db::getInstance()->getValue(' 5988 SELECT COUNT(`id_customization`) FROM `' . _DB_PREFIX_ . 'cart_product` 5989 WHERE `id_cart` = ' . (int) $id_cart . 5990 ' AND `id_customization` != 0'); 5991 if ($product_customizations) { 5992 return false; 5993 } 5994 } 5995 5996 if (!$id_lang) { 5997 $id_lang = Context::getContext()->language->id; 5998 } 5999 if (Shop::isFeatureActive() && !$id_shop) { 6000 $id_shop = (int) Context::getContext()->shop->id; 6001 } 6002 6003 if (!$result = Db::getInstance()->executeS(' 6004 SELECT cd.`id_customization`, c.`id_address_delivery`, c.`id_product`, cfl.`id_customization_field`, c.`id_product_attribute`, 6005 cd.`type`, cd.`index`, cd.`value`, cd.`id_module`, cfl.`name` 6006 FROM `' . _DB_PREFIX_ . 'customized_data` cd 6007 NATURAL JOIN `' . _DB_PREFIX_ . 'customization` c 6008 LEFT JOIN `' . _DB_PREFIX_ . 'customization_field_lang` cfl ON (cfl.id_customization_field = cd.`index` AND id_lang = ' . (int) $id_lang . 6009 ($id_shop ? ' AND cfl.`id_shop` = ' . (int) $id_shop : '') . ') 6010 WHERE c.`id_cart` = ' . (int) $id_cart . 6011 ($only_in_cart ? ' AND c.`in_cart` = 1' : '') . 6012 ((int) $id_customization ? ' AND cd.`id_customization` = ' . (int) $id_customization : '') . ' 6013 ORDER BY `id_product`, `id_product_attribute`, `type`, `index`')) { 6014 return false; 6015 } 6016 6017 $customized_datas = []; 6018 6019 foreach ($result as $row) { 6020 if ((int) $row['id_module'] && (int) $row['type'] == Product::CUSTOMIZE_TEXTFIELD) { 6021 // Hook displayCustomization: Call only the module in question 6022 // When a module saves a customization programmatically, it should add its ID in the `id_module` column 6023 $row['value'] = Hook::exec('displayCustomization', ['customization' => $row], (int) $row['id_module']); 6024 } 6025 $customized_datas[(int) $row['id_product']][(int) $row['id_product_attribute']][(int) $row['id_address_delivery']][(int) $row['id_customization']]['datas'][(int) $row['type']][] = $row; 6026 } 6027 6028 if (!$result = Db::getInstance()->executeS( 6029 'SELECT `id_product`, `id_product_attribute`, `id_customization`, `id_address_delivery`, `quantity`, `quantity_refunded`, `quantity_returned` 6030 FROM `' . _DB_PREFIX_ . 'customization` 6031 WHERE `id_cart` = ' . (int) $id_cart . 6032 ((int) $id_customization ? ' AND `id_customization` = ' . (int) $id_customization : '') . 6033 ($only_in_cart ? ' AND `in_cart` = 1' : '') 6034 )) { 6035 return false; 6036 } 6037 6038 foreach ($result as $row) { 6039 $customized_datas[(int) $row['id_product']][(int) $row['id_product_attribute']][(int) $row['id_address_delivery']][(int) $row['id_customization']]['quantity'] = (int) $row['quantity']; 6040 $customized_datas[(int) $row['id_product']][(int) $row['id_product_attribute']][(int) $row['id_address_delivery']][(int) $row['id_customization']]['quantity_refunded'] = (int) $row['quantity_refunded']; 6041 $customized_datas[(int) $row['id_product']][(int) $row['id_product_attribute']][(int) $row['id_address_delivery']][(int) $row['id_customization']]['quantity_returned'] = (int) $row['quantity_returned']; 6042 $customized_datas[(int) $row['id_product']][(int) $row['id_product_attribute']][(int) $row['id_address_delivery']][(int) $row['id_customization']]['id_customization'] = (int) $row['id_customization']; 6043 } 6044 6045 return $customized_datas; 6046 } 6047 6048 /** 6049 * @param array $products 6050 * @param array $customized_datas 6051 */ 6052 public static function addCustomizationPrice(&$products, &$customized_datas) 6053 { 6054 if (!$customized_datas) { 6055 return; 6056 } 6057 6058 foreach ($products as &$product_update) { 6059 if (!Customization::isFeatureActive()) { 6060 $product_update['customizationQuantityTotal'] = 0; 6061 $product_update['customizationQuantityRefunded'] = 0; 6062 $product_update['customizationQuantityReturned'] = 0; 6063 } else { 6064 $customization_quantity = 0; 6065 $customization_quantity_refunded = 0; 6066 $customization_quantity_returned = 0; 6067 6068 /* Compatibility */ 6069 $product_id = isset($product_update['id_product']) ? (int) $product_update['id_product'] : (int) $product_update['product_id']; 6070 $product_attribute_id = isset($product_update['id_product_attribute']) ? (int) $product_update['id_product_attribute'] : (int) $product_update['product_attribute_id']; 6071 $id_address_delivery = (int) $product_update['id_address_delivery']; 6072 $product_quantity = isset($product_update['cart_quantity']) ? (int) $product_update['cart_quantity'] : (int) $product_update['product_quantity']; 6073 $price = isset($product_update['price']) ? $product_update['price'] : $product_update['product_price']; 6074 if (isset($product_update['price_wt']) && $product_update['price_wt']) { 6075 $price_wt = $product_update['price_wt']; 6076 } else { 6077 $price_wt = $price * (1 + ((isset($product_update['tax_rate']) ? $product_update['tax_rate'] : $product_update['rate']) * 0.01)); 6078 } 6079 6080 if (!isset($customized_datas[$product_id][$product_attribute_id][$id_address_delivery])) { 6081 $id_address_delivery = 0; 6082 } 6083 if (isset($customized_datas[$product_id][$product_attribute_id][$id_address_delivery])) { 6084 foreach ($customized_datas[$product_id][$product_attribute_id][$id_address_delivery] as $customization) { 6085 if ((int) $product_update['id_customization'] && $customization['id_customization'] != $product_update['id_customization']) { 6086 continue; 6087 } 6088 $customization_quantity += (int) $customization['quantity']; 6089 $customization_quantity_refunded += (int) $customization['quantity_refunded']; 6090 $customization_quantity_returned += (int) $customization['quantity_returned']; 6091 } 6092 } 6093 6094 $product_update['customizationQuantityTotal'] = $customization_quantity; 6095 $product_update['customizationQuantityRefunded'] = $customization_quantity_refunded; 6096 $product_update['customizationQuantityReturned'] = $customization_quantity_returned; 6097 6098 if ($customization_quantity) { 6099 $product_update['total_wt'] = $price_wt * ($product_quantity - $customization_quantity); 6100 $product_update['total_customization_wt'] = $price_wt * $customization_quantity; 6101 $product_update['total'] = $price * ($product_quantity - $customization_quantity); 6102 $product_update['total_customization'] = $price * $customization_quantity; 6103 } 6104 } 6105 } 6106 } 6107 6108 /** 6109 * Add customization price for a single product 6110 * 6111 * @param array $product Product data 6112 * @param array $customized_datas Customized data 6113 */ 6114 public static function addProductCustomizationPrice(&$product, &$customized_datas) 6115 { 6116 if (!$customized_datas) { 6117 return; 6118 } 6119 6120 $products = [$product]; 6121 self::addCustomizationPrice($products, $customized_datas); 6122 $product = $products[0]; 6123 } 6124 6125 /** 6126 * Customization fields label management 6127 * 6128 * @param string $field 6129 * @param string $value 6130 * 6131 * @return array|false 6132 */ 6133 protected function _checkLabelField($field, $value) 6134 { 6135 if (!Validate::isLabel($value)) { 6136 return false; 6137 } 6138 $tmp = explode('_', $field); 6139 if (count($tmp) < 4) { 6140 return false; 6141 } 6142 6143 return $tmp; 6144 } 6145 6146 /** 6147 * @return bool 6148 */ 6149 protected function _deleteOldLabels() 6150 { 6151 $max = [ 6152 Product::CUSTOMIZE_FILE => (int) $this->uploadable_files, 6153 Product::CUSTOMIZE_TEXTFIELD => (int) $this->text_fields, 6154 ]; 6155 6156 /* Get customization field ids */ 6157 if (( 6158 $result = Db::getInstance()->executeS( 6159 'SELECT `id_customization_field`, `type` 6160 FROM `' . _DB_PREFIX_ . 'customization_field` 6161 WHERE `id_product` = ' . (int) $this->id . ' 6162 ORDER BY `id_customization_field`' 6163 ) 6164 ) === false) { 6165 return false; 6166 } 6167 6168 if (empty($result)) { 6169 return true; 6170 } 6171 6172 $customization_fields = [ 6173 Product::CUSTOMIZE_FILE => [], 6174 Product::CUSTOMIZE_TEXTFIELD => [], 6175 ]; 6176 6177 foreach ($result as $row) { 6178 $customization_fields[(int) $row['type']][] = (int) $row['id_customization_field']; 6179 } 6180 6181 $extra_file = count($customization_fields[Product::CUSTOMIZE_FILE]) - $max[Product::CUSTOMIZE_FILE]; 6182 $extra_text = count($customization_fields[Product::CUSTOMIZE_TEXTFIELD]) - $max[Product::CUSTOMIZE_TEXTFIELD]; 6183 6184 /* If too much inside the database, deletion */ 6185 if ($extra_file > 0 && count($customization_fields[Product::CUSTOMIZE_FILE]) - $extra_file >= 0 && 6186 (!Db::getInstance()->execute( 6187 'DELETE `' . _DB_PREFIX_ . 'customization_field`,`' . _DB_PREFIX_ . 'customization_field_lang` 6188 FROM `' . _DB_PREFIX_ . 'customization_field` JOIN `' . _DB_PREFIX_ . 'customization_field_lang` 6189 WHERE `' . _DB_PREFIX_ . 'customization_field`.`id_product` = ' . (int) $this->id . ' 6190 AND `' . _DB_PREFIX_ . 'customization_field`.`type` = ' . Product::CUSTOMIZE_FILE . ' 6191 AND `' . _DB_PREFIX_ . 'customization_field_lang`.`id_customization_field` = `' . _DB_PREFIX_ . 'customization_field`.`id_customization_field` 6192 AND `' . _DB_PREFIX_ . 'customization_field`.`id_customization_field` >= ' . (int) $customization_fields[Product::CUSTOMIZE_FILE][count($customization_fields[Product::CUSTOMIZE_FILE]) - $extra_file] 6193 ))) { 6194 return false; 6195 } 6196 6197 if ($extra_text > 0 && count($customization_fields[Product::CUSTOMIZE_TEXTFIELD]) - $extra_text >= 0 && 6198 (!Db::getInstance()->execute( 6199 'DELETE `' . _DB_PREFIX_ . 'customization_field`,`' . _DB_PREFIX_ . 'customization_field_lang` 6200 FROM `' . _DB_PREFIX_ . 'customization_field` JOIN `' . _DB_PREFIX_ . 'customization_field_lang` 6201 WHERE `' . _DB_PREFIX_ . 'customization_field`.`id_product` = ' . (int) $this->id . ' 6202 AND `' . _DB_PREFIX_ . 'customization_field`.`type` = ' . Product::CUSTOMIZE_TEXTFIELD . ' 6203 AND `' . _DB_PREFIX_ . 'customization_field_lang`.`id_customization_field` = `' . _DB_PREFIX_ . 'customization_field`.`id_customization_field` 6204 AND `' . _DB_PREFIX_ . 'customization_field`.`id_customization_field` >= ' . (int) $customization_fields[Product::CUSTOMIZE_TEXTFIELD][count($customization_fields[Product::CUSTOMIZE_TEXTFIELD]) - $extra_text] 6205 ))) { 6206 return false; 6207 } 6208 6209 // Refresh cache of feature detachable 6210 Configuration::updateGlobalValue('PS_CUSTOMIZATION_FEATURE_ACTIVE', Customization::isCurrentlyUsed()); 6211 6212 return true; 6213 } 6214 6215 /** 6216 * @param array $languages An array of language data 6217 * @param int $type Product::CUSTOMIZE_FILE or Product::CUSTOMIZE_TEXTFIELD 6218 * 6219 * @return bool 6220 */ 6221 protected function _createLabel($languages, $type) 6222 { 6223 // Label insertion 6224 if (!Db::getInstance()->execute(' 6225 INSERT INTO `' . _DB_PREFIX_ . 'customization_field` (`id_product`, `type`, `required`) 6226 VALUES (' . (int) $this->id . ', ' . (int) $type . ', 0)') || 6227 !$id_customization_field = (int) Db::getInstance()->Insert_ID()) { 6228 return false; 6229 } 6230 6231 // Multilingual label name creation 6232 $values = ''; 6233 6234 foreach ($languages as $language) { 6235 foreach (Shop::getContextListShopID() as $id_shop) { 6236 $values .= '(' . (int) $id_customization_field . ', ' . (int) $language['id_lang'] . ', ' . (int) $id_shop . ',\'\'), '; 6237 } 6238 } 6239 6240 $values = rtrim($values, ', '); 6241 if (!Db::getInstance()->execute(' 6242 INSERT INTO `' . _DB_PREFIX_ . 'customization_field_lang` (`id_customization_field`, `id_lang`, `id_shop`, `name`) 6243 VALUES ' . $values)) { 6244 return false; 6245 } 6246 6247 // Set cache of feature detachable to true 6248 Configuration::updateGlobalValue('PS_CUSTOMIZATION_FEATURE_ACTIVE', '1'); 6249 6250 return true; 6251 } 6252 6253 /** 6254 * @param int $uploadable_files 6255 * @param int $text_fields 6256 * 6257 * @return bool 6258 */ 6259 public function createLabels($uploadable_files, $text_fields) 6260 { 6261 $languages = Language::getLanguages(); 6262 if ((int) $uploadable_files > 0) { 6263 for ($i = 0; $i < (int) $uploadable_files; ++$i) { 6264 if (!$this->_createLabel($languages, Product::CUSTOMIZE_FILE)) { 6265 return false; 6266 } 6267 } 6268 } 6269 6270 if ((int) $text_fields > 0) { 6271 for ($i = 0; $i < (int) $text_fields; ++$i) { 6272 if (!$this->_createLabel($languages, Product::CUSTOMIZE_TEXTFIELD)) { 6273 return false; 6274 } 6275 } 6276 } 6277 6278 return true; 6279 } 6280 6281 /** 6282 * @return bool 6283 */ 6284 public function updateLabels() 6285 { 6286 $has_required_fields = 0; 6287 foreach ($_POST as $field => $value) { 6288 /* Label update */ 6289 if (strncmp($field, 'label_', 6) == 0) { 6290 if (!$tmp = $this->_checkLabelField($field, $value)) { 6291 return false; 6292 } 6293 /* Multilingual label name update */ 6294 if (Shop::isFeatureActive()) { 6295 foreach (Shop::getContextListShopID() as $id_shop) { 6296 if (!Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'customization_field_lang` 6297 (`id_customization_field`, `id_lang`, `id_shop`, `name`) VALUES (' . (int) $tmp[2] . ', ' . (int) $tmp[3] . ', ' . (int) $id_shop . ', \'' . pSQL($value) . '\') 6298 ON DUPLICATE KEY UPDATE `name` = \'' . pSQL($value) . '\'')) { 6299 return false; 6300 } 6301 } 6302 } elseif (!Db::getInstance()->execute(' 6303 INSERT INTO `' . _DB_PREFIX_ . 'customization_field_lang` 6304 (`id_customization_field`, `id_lang`, `name`) VALUES (' . (int) $tmp[2] . ', ' . (int) $tmp[3] . ', \'' . pSQL($value) . '\') 6305 ON DUPLICATE KEY UPDATE `name` = \'' . pSQL($value) . '\'')) { 6306 return false; 6307 } 6308 6309 $is_required = isset($_POST['require_' . (int) $tmp[1] . '_' . (int) $tmp[2]]) ? 1 : 0; 6310 $has_required_fields |= $is_required; 6311 /* Require option update */ 6312 if (!Db::getInstance()->execute( 6313 'UPDATE `' . _DB_PREFIX_ . 'customization_field` 6314 SET `required` = ' . (int) $is_required . ' 6315 WHERE `id_customization_field` = ' . (int) $tmp[2] 6316 )) { 6317 return false; 6318 } 6319 } 6320 } 6321 6322 if ($has_required_fields && !ObjectModel::updateMultishopTable('product', ['customizable' => 2], 'a.id_product = ' . (int) $this->id)) { 6323 return false; 6324 } 6325 6326 if (!$this->_deleteOldLabels()) { 6327 return false; 6328 } 6329 6330 return true; 6331 } 6332 6333 /** 6334 * @param int|false $id_lang Language identifier 6335 * @param int|null $id_shop Shop identifier 6336 * 6337 * @return array 6338 */ 6339 public function getCustomizationFields($id_lang = false, $id_shop = null) 6340 { 6341 if (!Customization::isFeatureActive()) { 6342 return false; 6343 } 6344 6345 if (Shop::isFeatureActive() && !$id_shop) { 6346 $id_shop = (int) Context::getContext()->shop->id; 6347 } 6348 6349 // Hide the modules fields in the front-office 6350 // When a module adds a customization programmatically, it should set the `is_module` to 1 6351 $context = Context::getContext(); 6352 $front = isset($context->controller->controller_type) && in_array($context->controller->controller_type, ['front']); 6353 6354 if (!$result = Db::getInstance()->executeS(' 6355 SELECT cf.`id_customization_field`, cf.`type`, cf.`required`, cfl.`name`, cfl.`id_lang` 6356 FROM `' . _DB_PREFIX_ . 'customization_field` cf 6357 NATURAL JOIN `' . _DB_PREFIX_ . 'customization_field_lang` cfl 6358 WHERE cf.`id_product` = ' . (int) $this->id . ($id_lang ? ' AND cfl.`id_lang` = ' . (int) $id_lang : '') . 6359 ($id_shop ? ' AND cfl.`id_shop` = ' . (int) $id_shop : '') . 6360 ($front ? ' AND !cf.`is_module`' : '') . ' 6361 AND cf.`is_deleted` = 0 6362 ORDER BY cf.`id_customization_field`') 6363 ) { 6364 return false; 6365 } 6366 6367 if ($id_lang) { 6368 return $result; 6369 } 6370 6371 $customization_fields = []; 6372 foreach ($result as $row) { 6373 $customization_fields[(int) $row['type']][(int) $row['id_customization_field']][(int) $row['id_lang']] = $row; 6374 } 6375 6376 return $customization_fields; 6377 } 6378 6379 /** 6380 * check if product has an activated and required customizationFields. 6381 * 6382 * @return bool 6383 * 6384 * @throws PrestaShopDatabaseException 6385 */ 6386 public function hasActivatedRequiredCustomizableFields() 6387 { 6388 if (!Customization::isFeatureActive()) { 6389 return false; 6390 } 6391 6392 return (bool) Db::getInstance()->executeS( 6393 ' 6394 SELECT 1 6395 FROM `' . _DB_PREFIX_ . 'customization_field` 6396 WHERE `id_product` = ' . (int) $this->id . ' 6397 AND `required` = 1 6398 AND `is_deleted` = 0' 6399 ); 6400 } 6401 6402 /** 6403 * @return array 6404 */ 6405 public function getCustomizationFieldIds() 6406 { 6407 if (!Customization::isFeatureActive()) { 6408 return []; 6409 } 6410 6411 return Db::getInstance()->executeS(' 6412 SELECT `id_customization_field`, `type`, `required` 6413 FROM `' . _DB_PREFIX_ . 'customization_field` 6414 WHERE `id_product` = ' . (int) $this->id); 6415 } 6416 6417 /** 6418 * @return array 6419 */ 6420 public function getNonDeletedCustomizationFieldIds() 6421 { 6422 if (!Customization::isFeatureActive()) { 6423 return []; 6424 } 6425 6426 $results = Db::getInstance()->executeS(' 6427 SELECT `id_customization_field` 6428 FROM `' . _DB_PREFIX_ . 'customization_field` 6429 WHERE `is_deleted` = 0 6430 AND `id_product` = ' . (int) $this->id 6431 ); 6432 6433 return array_map(function ($result) { 6434 return (int) $result['id_customization_field']; 6435 }, $results); 6436 } 6437 6438 /** 6439 * @param int $fieldType |null 6440 * 6441 * @return int 6442 * 6443 * @throws PrestaShopDatabaseException 6444 */ 6445 public function countCustomizationFields(?int $fieldType = null): int 6446 { 6447 $query = ' 6448 SELECT COUNT(`id_customization_field`) as customizations_count 6449 FROM `' . _DB_PREFIX_ . 'customization_field` 6450 WHERE `is_deleted` = 0 6451 AND `id_product` = ' . (int) $this->id 6452 ; 6453 6454 if (null !== $fieldType) { 6455 $query .= sprintf(' AND type = %d', $fieldType); 6456 } 6457 6458 $results = Db::getInstance()->executeS($query); 6459 6460 if (empty($results)) { 6461 return 0; 6462 } 6463 6464 return (int) reset($results)['customizations_count']; 6465 } 6466 6467 /** 6468 * @return array 6469 */ 6470 public function getRequiredCustomizableFields() 6471 { 6472 if (!Customization::isFeatureActive()) { 6473 return []; 6474 } 6475 6476 return Product::getRequiredCustomizableFieldsStatic($this->id); 6477 } 6478 6479 /** 6480 * @param int $id Product identifier 6481 * 6482 * @return array 6483 */ 6484 public static function getRequiredCustomizableFieldsStatic($id) 6485 { 6486 if (!$id || !Customization::isFeatureActive()) { 6487 return []; 6488 } 6489 6490 return Db::getInstance()->executeS( 6491 ' 6492 SELECT `id_customization_field`, `type` 6493 FROM `' . _DB_PREFIX_ . 'customization_field` 6494 WHERE `id_product` = ' . (int) $id . ' 6495 AND `required` = 1 AND `is_deleted` = 0' 6496 ); 6497 } 6498 6499 /** 6500 * @param Context|null $context 6501 * 6502 * @return bool 6503 */ 6504 public function hasAllRequiredCustomizableFields(Context $context = null) 6505 { 6506 if (!Customization::isFeatureActive()) { 6507 return true; 6508 } 6509 if (!$context) { 6510 $context = Context::getContext(); 6511 } 6512 6513 $fields = $context->cart->getProductCustomization($this->id, null, true); 6514 if (($required_fields = $this->getRequiredCustomizableFields()) === false) { 6515 return false; 6516 } 6517 6518 $fields_present = []; 6519 foreach ($fields as $field) { 6520 $fields_present[] = ['id_customization_field' => $field['index'], 'type' => $field['type']]; 6521 } 6522 6523 if (is_array($required_fields) && count($required_fields)) { 6524 foreach ($required_fields as $required_field) { 6525 if (!in_array($required_field, $fields_present)) { 6526 return false; 6527 } 6528 } 6529 } 6530 6531 return true; 6532 } 6533 6534 /** 6535 * Return the list of old temp products. 6536 * 6537 * @return array 6538 */ 6539 public static function getOldTempProducts() 6540 { 6541 $sql = 'SELECT id_product FROM `' . _DB_PREFIX_ . 'product` WHERE state=' . Product::STATE_TEMP . ' AND date_upd < NOW() - INTERVAL 1 DAY'; 6542 6543 return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql, true, false); 6544 } 6545 6546 /** 6547 * Checks if the product is in at least one of the submited categories. 6548 * 6549 * @param int $id_product Product identifier 6550 * @param array $categories array of category arrays 6551 * 6552 * @return bool is the product in at least one category 6553 */ 6554 public static function idIsOnCategoryId($id_product, $categories) 6555 { 6556 if (!((int) $id_product > 0) || !is_array($categories) || empty($categories)) { 6557 return false; 6558 } 6559 $sql = 'SELECT id_product FROM `' . _DB_PREFIX_ . 'category_product` WHERE `id_product` = ' . (int) $id_product . ' AND `id_category` IN ('; 6560 foreach ($categories as $category) { 6561 $sql .= (int) $category['id_category'] . ','; 6562 } 6563 $sql = rtrim($sql, ',') . ')'; 6564 6565 $hash = md5($sql); 6566 if (!isset(self::$_incat[$hash])) { 6567 if (!Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql)) { 6568 return false; 6569 } 6570 self::$_incat[$hash] = (Db::getInstance(_PS_USE_SQL_SLAVE_)->numRows() > 0 ? true : false); 6571 } 6572 6573 return self::$_incat[$hash]; 6574 } 6575 6576 /** 6577 * @return string 6578 */ 6579 public function getNoPackPrice() 6580 { 6581 $context = Context::getContext(); 6582 6583 return Tools::getContextLocale($context)->formatPrice(Pack::noPackPrice((int) $this->id), $context->currency->iso_code); 6584 } 6585 6586 /** 6587 * @param int $id_customer Customer identifier 6588 * 6589 * @return bool 6590 */ 6591 public function checkAccess($id_customer) 6592 { 6593 return Product::checkAccessStatic((int) $this->id, (int) $id_customer); 6594 } 6595 6596 /** 6597 * @param int $id_product Product identifier 6598 * @param int $id_customer Customer identifier 6599 * 6600 * @return bool 6601 */ 6602 public static function checkAccessStatic($id_product, $id_customer) 6603 { 6604 if (!Group::isFeatureActive()) { 6605 return true; 6606 } 6607 6608 $cache_id = 'Product::checkAccess_' . (int) $id_product . '-' . (int) $id_customer . (!$id_customer ? '-' . (int) Group::getCurrent()->id : ''); 6609 if (!Cache::isStored($cache_id)) { 6610 if (!$id_customer) { 6611 $result = (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(' 6612 SELECT ctg.`id_group` 6613 FROM `' . _DB_PREFIX_ . 'category_product` cp 6614 INNER JOIN `' . _DB_PREFIX_ . 'category_group` ctg ON (ctg.`id_category` = cp.`id_category`) 6615 WHERE cp.`id_product` = ' . (int) $id_product . ' AND ctg.`id_group` = ' . (int) Group::getCurrent()->id); 6616 } else { 6617 $result = (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(' 6618 SELECT cg.`id_group` 6619 FROM `' . _DB_PREFIX_ . 'category_product` cp 6620 INNER JOIN `' . _DB_PREFIX_ . 'category_group` ctg ON (ctg.`id_category` = cp.`id_category`) 6621 INNER JOIN `' . _DB_PREFIX_ . 'customer_group` cg ON (cg.`id_group` = ctg.`id_group`) 6622 WHERE cp.`id_product` = ' . (int) $id_product . ' AND cg.`id_customer` = ' . (int) $id_customer); 6623 } 6624 6625 Cache::store($cache_id, $result); 6626 6627 return $result; 6628 } 6629 6630 return Cache::retrieve($cache_id); 6631 } 6632 6633 /** 6634 * Add a stock movement for current product. 6635 * 6636 * Since 1.5, this method only permit to add/remove available quantities of the current product in the current shop 6637 * 6638 * @see StockManager if you want to manage real stock 6639 * @see StockAvailable if you want to manage available quantities for sale on your shop(s) 6640 * @deprecated since 1.5.0 6641 * 6642 * @param int $quantity 6643 * @param int $id_reason StockMvtReason identifier - useless 6644 * @param int|null $id_product_attribute Attribute identifier 6645 * @param int|null $id_order Order identifier - DEPRECATED 6646 * @param int|null $id_employee Employee identifier - DEPRECATED 6647 * 6648 * @return bool 6649 */ 6650 public function addStockMvt($quantity, $id_reason, $id_product_attribute = null, $id_order = null, $id_employee = null) 6651 { 6652 if (!$this->id || !$id_reason) { 6653 return false; 6654 } 6655 6656 if ($id_product_attribute == null) { 6657 $id_product_attribute = 0; 6658 } 6659 6660 $reason = new StockMvtReason((int) $id_reason); 6661 if (!Validate::isLoadedObject($reason)) { 6662 return false; 6663 } 6664 6665 $quantity = abs((int) $quantity) * $reason->sign; 6666 6667 return StockAvailable::updateQuantity($this->id, $id_product_attribute, $quantity); 6668 } 6669 6670 /** 6671 * @deprecated since 1.5.0 6672 * 6673 * @param int $id_lang Language identifier 6674 * 6675 * @return array 6676 */ 6677 public function getStockMvts($id_lang) 6678 { 6679 Tools::displayAsDeprecated(); 6680 6681 return Db::getInstance()->executeS(' 6682 SELECT sm.id_stock_mvt, sm.date_add, sm.quantity, sm.id_order, 6683 CONCAT(pl.name, \' \', GROUP_CONCAT(IFNULL(al.name, \'\'), \'\')) product_name, CONCAT(e.lastname, \' \', e.firstname) employee, mrl.name reason 6684 FROM `' . _DB_PREFIX_ . 'stock_mvt` sm 6685 LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON ( 6686 sm.id_product = pl.id_product 6687 AND pl.id_lang = ' . (int) $id_lang . Shop::addSqlRestrictionOnLang('pl') . ' 6688 ) 6689 LEFT JOIN `' . _DB_PREFIX_ . 'stock_mvt_reason_lang` mrl ON ( 6690 sm.id_stock_mvt_reason = mrl.id_stock_mvt_reason 6691 AND mrl.id_lang = ' . (int) $id_lang . ' 6692 ) 6693 LEFT JOIN `' . _DB_PREFIX_ . 'employee` e ON ( 6694 e.id_employee = sm.id_employee 6695 ) 6696 LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON ( 6697 pac.id_product_attribute = sm.id_product_attribute 6698 ) 6699 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ON ( 6700 al.id_attribute = pac.id_attribute 6701 AND al.id_lang = ' . (int) $id_lang . ' 6702 ) 6703 WHERE sm.id_product=' . (int) $this->id . ' 6704 GROUP BY sm.id_stock_mvt 6705 '); 6706 } 6707 6708 /** 6709 * @param int $id_product Product identifier 6710 * 6711 * @return array 6712 */ 6713 public static function getUrlRewriteInformations($id_product) 6714 { 6715 return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' 6716 SELECT pl.`id_lang`, pl.`link_rewrite`, p.`ean13`, cl.`link_rewrite` AS category_rewrite 6717 FROM `' . _DB_PREFIX_ . 'product` p 6718 LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (p.`id_product` = pl.`id_product`' . Shop::addSqlRestrictionOnLang('pl') . ') 6719 ' . Shop::addSqlAssociation('product', 'p') . ' 6720 LEFT JOIN `' . _DB_PREFIX_ . 'lang` l ON (pl.`id_lang` = l.`id_lang`) 6721 LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl ON (cl.`id_category` = product_shop.`id_category_default` AND cl.`id_lang` = pl.`id_lang`' . Shop::addSqlRestrictionOnLang('cl') . ') 6722 WHERE p.`id_product` = ' . (int) $id_product . ' 6723 AND l.`active` = 1 6724 '); 6725 } 6726 6727 /** 6728 * @return int TaxRulesGroup identifier 6729 */ 6730 public function getIdTaxRulesGroup() 6731 { 6732 return $this->id_tax_rules_group; 6733 } 6734 6735 /** 6736 * @param int $id_product Product identifier 6737 * @param Context|null $context 6738 * 6739 * @return int TaxRulesGroup identifier 6740 */ 6741 public static function getIdTaxRulesGroupByIdProduct($id_product, Context $context = null) 6742 { 6743 if (!$context) { 6744 $context = Context::getContext(); 6745 } 6746 $key = 'product_id_tax_rules_group_' . (int) $id_product . '_' . (int) $context->shop->id; 6747 if (!Cache::isStored($key)) { 6748 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(' 6749 SELECT `id_tax_rules_group` 6750 FROM `' . _DB_PREFIX_ . 'product_shop` 6751 WHERE `id_product` = ' . (int) $id_product . ' AND id_shop=' . (int) $context->shop->id); 6752 Cache::store($key, (int) $result); 6753 6754 return (int) $result; 6755 } 6756 6757 return Cache::retrieve($key); 6758 } 6759 6760 /** 6761 * Returns tax rate. 6762 * 6763 * @param Address|null $address 6764 * 6765 * @return float The total taxes rate applied to the product 6766 */ 6767 public function getTaxesRate(Address $address = null) 6768 { 6769 if (!$address || !$address->id_country) { 6770 $address = Address::initialize(); 6771 } 6772 6773 $tax_manager = TaxManagerFactory::getManager($address, $this->id_tax_rules_group); 6774 $tax_calculator = $tax_manager->getTaxCalculator(); 6775 6776 return $tax_calculator->getTotalRate(); 6777 } 6778 6779 /** 6780 * Webservice getter : get product features association. 6781 * 6782 * @return array 6783 */ 6784 public function getWsProductFeatures() 6785 { 6786 $rows = $this->getFeatures(); 6787 foreach ($rows as $keyrow => $row) { 6788 foreach ($row as $keyfeature => $feature) { 6789 if ($keyfeature == 'id_feature') { 6790 $rows[$keyrow]['id'] = $feature; 6791 unset($rows[$keyrow]['id_feature']); 6792 } 6793 unset( 6794 $rows[$keyrow]['id_product'], 6795 $rows[$keyrow]['custom'] 6796 ); 6797 } 6798 asort($rows[$keyrow]); 6799 } 6800 6801 return $rows; 6802 } 6803 6804 /** 6805 * Webservice setter : set product features association. 6806 * 6807 * @param array $product_features Feature data 6808 * 6809 * @return bool 6810 */ 6811 public function setWsProductFeatures($product_features) 6812 { 6813 Db::getInstance()->execute( 6814 ' 6815 DELETE FROM `' . _DB_PREFIX_ . 'feature_product` 6816 WHERE `id_product` = ' . (int) $this->id 6817 ); 6818 foreach ($product_features as $product_feature) { 6819 $this->addFeaturesToDB($product_feature['id'], $product_feature['id_feature_value']); 6820 } 6821 6822 return true; 6823 } 6824 6825 /** 6826 * Webservice getter : get virtual field default combination. 6827 * 6828 * @return int Default Attribute identifier 6829 */ 6830 public function getWsDefaultCombination() 6831 { 6832 return Product::getDefaultAttribute($this->id); 6833 } 6834 6835 /** 6836 * Webservice setter : set virtual field default combination. 6837 * 6838 * @param int $id_combination Default Attribute identifier 6839 * 6840 * @return bool 6841 */ 6842 public function setWsDefaultCombination($id_combination) 6843 { 6844 $this->deleteDefaultAttributes(); 6845 6846 return $this->setDefaultAttribute((int) $id_combination); 6847 } 6848 6849 /** 6850 * Webservice getter : get category ids of current product for association. 6851 * 6852 * @return array 6853 */ 6854 public function getWsCategories() 6855 { 6856 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 6857 'SELECT cp.`id_category` AS id 6858 FROM `' . _DB_PREFIX_ . 'category_product` cp 6859 LEFT JOIN `' . _DB_PREFIX_ . 'category` c ON (c.id_category = cp.id_category) 6860 ' . Shop::addSqlAssociation('category', 'c') . ' 6861 WHERE cp.`id_product` = ' . (int) $this->id 6862 ); 6863 6864 return $result; 6865 } 6866 6867 /** 6868 * Webservice setter : set category ids of current product for association. 6869 * 6870 * @param array $category_ids category ids 6871 * 6872 * @return bool 6873 */ 6874 public function setWsCategories($category_ids) 6875 { 6876 $ids = []; 6877 foreach ($category_ids as $value) { 6878 if ($value instanceof Category) { 6879 $ids[] = (int) $value->id; 6880 } elseif (is_array($value) && array_key_exists('id', $value)) { 6881 $ids[] = (int) $value['id']; 6882 } else { 6883 $ids[] = (int) $value; 6884 } 6885 } 6886 $ids = array_unique($ids); 6887 6888 $positions = Db::getInstance()->executeS( 6889 'SELECT `id_category`, `position` 6890 FROM `' . _DB_PREFIX_ . 'category_product` 6891 WHERE `id_product` = ' . (int) $this->id 6892 ); 6893 6894 $max_positions = Db::getInstance()->executeS( 6895 'SELECT `id_category`, max(`position`) as maximum 6896 FROM `' . _DB_PREFIX_ . 'category_product` 6897 GROUP BY id_category' 6898 ); 6899 6900 $positions_lookup = []; 6901 $max_position_lookup = []; 6902 6903 foreach ($positions as $row) { 6904 $positions_lookup[(int) $row['id_category']] = (int) $row['position']; 6905 } 6906 foreach ($max_positions as $row) { 6907 $max_position_lookup[(int) $row['id_category']] = (int) $row['maximum']; 6908 } 6909 6910 $return = true; 6911 if ($this->deleteCategories() && !empty($ids)) { 6912 $sql_values = []; 6913 foreach ($ids as $id) { 6914 $pos = 1; 6915 if (array_key_exists((int) $id, $positions_lookup)) { 6916 $pos = (int) $positions_lookup[(int) $id]; 6917 } elseif (array_key_exists((int) $id, $max_position_lookup)) { 6918 $pos = (int) $max_position_lookup[(int) $id] + 1; 6919 } 6920 6921 $sql_values[] = '(' . (int) $id . ', ' . (int) $this->id . ', ' . $pos . ')'; 6922 } 6923 6924 $return = Db::getInstance()->execute( 6925 ' 6926 INSERT INTO `' . _DB_PREFIX_ . 'category_product` (`id_category`, `id_product`, `position`) 6927 VALUES ' . implode(',', $sql_values) 6928 ); 6929 } 6930 6931 Hook::exec('actionProductUpdate', ['id_product' => (int) $this->id]); 6932 6933 return $return; 6934 } 6935 6936 /** 6937 * Webservice getter : get product accessories ids of current product for association. 6938 * 6939 * @return array 6940 */ 6941 public function getWsAccessories() 6942 { 6943 $result = Db::getInstance()->executeS( 6944 'SELECT p.`id_product` AS id 6945 FROM `' . _DB_PREFIX_ . 'accessory` a 6946 LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON (p.id_product = a.id_product_2) 6947 ' . Shop::addSqlAssociation('product', 'p') . ' 6948 WHERE a.`id_product_1` = ' . (int) $this->id 6949 ); 6950 6951 return $result; 6952 } 6953 6954 /** 6955 * Webservice setter : set product accessories ids of current product for association. 6956 * 6957 * @param array $accessories product ids 6958 * 6959 * @return bool 6960 */ 6961 public function setWsAccessories($accessories) 6962 { 6963 $this->deleteAccessories(); 6964 foreach ($accessories as $accessory) { 6965 Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'accessory` (`id_product_1`, `id_product_2`) VALUES (' . (int) $this->id . ', ' . (int) $accessory['id'] . ')'); 6966 } 6967 6968 return true; 6969 } 6970 6971 /** 6972 * Webservice getter : get combination ids of current product for association. 6973 * 6974 * @return array 6975 */ 6976 public function getWsCombinations() 6977 { 6978 $result = Db::getInstance()->executeS( 6979 'SELECT pa.`id_product_attribute` as id 6980 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 6981 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 6982 WHERE pa.`id_product` = ' . (int) $this->id 6983 ); 6984 6985 return $result; 6986 } 6987 6988 /** 6989 * Webservice setter : set combination ids of current product for association. 6990 * 6991 * @param array $combinations combination ids 6992 * 6993 * @return bool 6994 */ 6995 public function setWsCombinations($combinations) 6996 { 6997 // No hook exec 6998 $ids_new = []; 6999 foreach ($combinations as $combination) { 7000 $ids_new[] = (int) $combination['id']; 7001 } 7002 7003 $ids_orig = []; 7004 $original = Db::getInstance()->executeS( 7005 'SELECT pa.`id_product_attribute` as id 7006 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 7007 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 7008 WHERE pa.`id_product` = ' . (int) $this->id 7009 ); 7010 7011 if (is_array($original)) { 7012 foreach ($original as $id) { 7013 $ids_orig[] = $id['id']; 7014 } 7015 } 7016 7017 $all_ids = []; 7018 $all = Db::getInstance()->executeS('SELECT pa.`id_product_attribute` as id FROM `' . _DB_PREFIX_ . 'product_attribute` pa ' . Shop::addSqlAssociation('product_attribute', 'pa')); 7019 if (is_array($all)) { 7020 foreach ($all as $id) { 7021 $all_ids[] = $id['id']; 7022 } 7023 } 7024 7025 $to_add = []; 7026 foreach ($ids_new as $id) { 7027 if (!in_array($id, $ids_orig)) { 7028 $to_add[] = $id; 7029 } 7030 } 7031 7032 $to_delete = []; 7033 foreach ($ids_orig as $id) { 7034 if (!in_array($id, $ids_new)) { 7035 $to_delete[] = $id; 7036 } 7037 } 7038 7039 // Delete rows 7040 if (count($to_delete) > 0) { 7041 foreach ($to_delete as $id) { 7042 $combination = new Combination($id); 7043 $combination->delete(); 7044 } 7045 } 7046 7047 foreach ($to_add as $id) { 7048 // Update id_product if exists else create 7049 if (in_array($id, $all_ids)) { 7050 Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'product_attribute` SET id_product = ' . (int) $this->id . ' WHERE id_product_attribute=' . $id); 7051 } else { 7052 Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'product_attribute` (`id_product`) VALUES (' . (int) $this->id . ')'); 7053 } 7054 } 7055 7056 return true; 7057 } 7058 7059 /** 7060 * Webservice getter : get product option ids of current product for association. 7061 * 7062 * @return array 7063 */ 7064 public function getWsProductOptionValues() 7065 { 7066 $result = Db::getInstance()->executeS('SELECT DISTINCT pac.id_attribute as id 7067 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 7068 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 7069 LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON (pac.id_product_attribute = pa.id_product_attribute) 7070 WHERE pa.id_product = ' . (int) $this->id); 7071 7072 return $result; 7073 } 7074 7075 /** 7076 * Webservice getter : get virtual field position in category. 7077 * 7078 * @return int|string 7079 */ 7080 public function getWsPositionInCategory() 7081 { 7082 $result = Db::getInstance()->executeS( 7083 'SELECT `position` 7084 FROM `' . _DB_PREFIX_ . 'category_product` 7085 WHERE `id_category` = ' . (int) $this->id_category_default . ' 7086 AND `id_product` = ' . (int) $this->id); 7087 if (count($result) > 0) { 7088 return $result[0]['position']; 7089 } 7090 7091 return ''; 7092 } 7093 7094 /** 7095 * Webservice setter : set virtual field position in category. 7096 * 7097 * @param int $position 7098 * 7099 * @return bool 7100 */ 7101 public function setWsPositionInCategory($position) 7102 { 7103 if ($position <= 0) { 7104 WebserviceRequest::getInstance()->setError( 7105 500, 7106 $this->trans( 7107 'You cannot set 0 or a negative position, the minimum is 1.', 7108 [], 7109 'Admin.Catalog.Notification' 7110 ), 7111 134 7112 ); 7113 7114 return false; 7115 } 7116 7117 $result = Db::getInstance()->executeS( 7118 'SELECT `id_product` ' . 7119 'FROM `' . _DB_PREFIX_ . 'category_product` ' . 7120 'WHERE `id_category` = ' . (int) $this->id_category_default . ' ' . 7121 'ORDER BY `position`' 7122 ); 7123 7124 if ($position > count($result)) { 7125 WebserviceRequest::getInstance()->setError( 7126 500, 7127 $this->trans( 7128 'You cannot set a position greater than the total number of products in the category, starting at 1.', 7129 [], 7130 'Admin.Catalog.Notification' 7131 ), 7132 135 7133 ); 7134 7135 return false; 7136 } 7137 7138 // result is indexed by recordset order and not position. positions start at index 1 so we need an empty element 7139 array_unshift($result, null); 7140 foreach ($result as &$value) { 7141 $value = $value['id_product']; 7142 } 7143 7144 $current_position = $this->getWsPositionInCategory(); 7145 7146 if ($current_position && isset($result[$current_position])) { 7147 $save = $result[$current_position]; 7148 unset($result[$current_position]); 7149 array_splice($result, (int) $position, 0, $save); 7150 } 7151 7152 foreach ($result as $position => $id_product) { 7153 Db::getInstance()->update('category_product', [ 7154 'position' => $position, 7155 ], '`id_category` = ' . (int) $this->id_category_default . ' AND `id_product` = ' . (int) $id_product); 7156 } 7157 7158 return true; 7159 } 7160 7161 /** 7162 * Webservice getter : get virtual field id_default_image in category. 7163 * 7164 * @return int|string 7165 */ 7166 public function getCoverWs() 7167 { 7168 $result = $this->getCover($this->id); 7169 7170 return $result['id_image']; 7171 } 7172 7173 /** 7174 * Webservice setter : set virtual field id_default_image in category. 7175 * 7176 * @param int $id_image 7177 * 7178 * @return bool 7179 */ 7180 public function setCoverWs($id_image) 7181 { 7182 Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'image_shop` image_shop, `' . _DB_PREFIX_ . 'image` i 7183 SET image_shop.`cover` = NULL 7184 WHERE i.`id_product` = ' . (int) $this->id . ' AND i.id_image = image_shop.id_image 7185 AND image_shop.id_shop=' . (int) Context::getContext()->shop->id); 7186 7187 Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'image_shop` 7188 SET `cover` = 1 WHERE `id_image` = ' . (int) $id_image); 7189 7190 return true; 7191 } 7192 7193 /** 7194 * Webservice getter : get image ids of current product for association. 7195 * 7196 * @return array 7197 */ 7198 public function getWsImages() 7199 { 7200 return Db::getInstance()->executeS(' 7201 SELECT i.`id_image` as id 7202 FROM `' . _DB_PREFIX_ . 'image` i 7203 ' . Shop::addSqlAssociation('image', 'i') . ' 7204 WHERE i.`id_product` = ' . (int) $this->id . ' 7205 ORDER BY i.`position`'); 7206 } 7207 7208 /** 7209 * Webservice getter : Get StockAvailable identifier and Attribute identifier 7210 * 7211 * @return array 7212 */ 7213 public function getWsStockAvailables() 7214 { 7215 return Db::getInstance()->executeS('SELECT `id_stock_available` id, `id_product_attribute` 7216 FROM `' . _DB_PREFIX_ . 'stock_available` 7217 WHERE `id_product`=' . (int) $this->id . StockAvailable::addSqlShopRestriction()); 7218 } 7219 7220 /** 7221 * Webservice getter: Get product attachments ids of current product for association 7222 * 7223 * @return array<int, array{id: string}> 7224 */ 7225 public function getWsAttachments(): array 7226 { 7227 return Db::getInstance()->executeS( 7228 'SELECT a.`id_attachment` AS id ' . 7229 'FROM `' . _DB_PREFIX_ . 'product_attachment` pa ' . 7230 'INNER JOIN `' . _DB_PREFIX_ . 'attachment` a ON (pa.id_attachment = a.id_attachment) ' . 7231 Shop::addSqlAssociation('attachment', 'a') . ' ' . 7232 'WHERE pa.`id_product` = ' . (int) $this->id 7233 ); 7234 } 7235 7236 /** 7237 * Webservice setter: set product attachments ids of current product for association 7238 * 7239 * @param array<array{id: int|string}> $attachments ids 7240 */ 7241 public function setWsAttachments(array $attachments): bool 7242 { 7243 $this->deleteAttachments(true); 7244 foreach ($attachments as $attachment) { 7245 Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'product_attachment` 7246 (`id_product`, `id_attachment`) VALUES (' . (int) $this->id . ', ' . (int) $attachment['id'] . ')'); 7247 } 7248 Product::updateCacheAttachment((int) $this->id); 7249 7250 return true; 7251 } 7252 7253 public function getWsTags() 7254 { 7255 return Db::getInstance()->executeS(' 7256 SELECT `id_tag` as id 7257 FROM `' . _DB_PREFIX_ . 'product_tag` 7258 WHERE `id_product` = ' . (int) $this->id); 7259 } 7260 7261 /** 7262 * Webservice setter : set tag ids of current product for association. 7263 * 7264 * @param array $tag_ids Tag identifiers 7265 * 7266 * @return bool 7267 */ 7268 public function setWsTags($tag_ids) 7269 { 7270 $ids = []; 7271 foreach ($tag_ids as $value) { 7272 $ids[] = $value['id']; 7273 } 7274 if ($this->deleteWsTags()) { 7275 if ($ids) { 7276 $sql_values = []; 7277 $ids = array_map('intval', $ids); 7278 foreach ($ids as $position => $id) { 7279 $id_lang = Db::getInstance()->getValue('SELECT `id_lang` FROM `' . _DB_PREFIX_ . 'tag` WHERE `id_tag`=' . (int) $id); 7280 $sql_values[] = '(' . (int) $this->id . ', ' . (int) $id . ', ' . (int) $id_lang . ')'; 7281 } 7282 $result = Db::getInstance()->execute( 7283 ' 7284 INSERT INTO `' . _DB_PREFIX_ . 'product_tag` (`id_product`, `id_tag`, `id_lang`) 7285 VALUES ' . implode(',', $sql_values) 7286 ); 7287 7288 return $result; 7289 } 7290 } 7291 7292 return true; 7293 } 7294 7295 /** 7296 * Delete products tags entries without delete tags for webservice usage. 7297 * 7298 * @return bool Deletion result 7299 */ 7300 public function deleteWsTags() 7301 { 7302 return Db::getInstance()->delete('product_tag', 'id_product = ' . (int) $this->id); 7303 } 7304 7305 /** 7306 * @return string 7307 */ 7308 public function getWsManufacturerName() 7309 { 7310 return Manufacturer::getNameById((int) $this->id_manufacturer); 7311 } 7312 7313 /** 7314 * @return bool 7315 */ 7316 public static function resetEcoTax() 7317 { 7318 return ObjectModel::updateMultishopTable('product', [ 7319 'ecotax' => 0, 7320 ]); 7321 } 7322 7323 /** 7324 * Set Group reduction if needed. 7325 */ 7326 public function setGroupReduction() 7327 { 7328 return GroupReduction::setProductReduction($this->id); 7329 } 7330 7331 /** 7332 * Checks if reference exists. 7333 * 7334 * @param string $reference Product reference 7335 * 7336 * @return bool 7337 */ 7338 public function existsRefInDatabase($reference) 7339 { 7340 $row = Db::getInstance()->getRow(' 7341 SELECT `reference` 7342 FROM `' . _DB_PREFIX_ . 'product` p 7343 WHERE p.reference = "' . pSQL($reference) . '"', false); 7344 7345 return isset($row['reference']); 7346 } 7347 7348 /** 7349 * Get all product attributes ids. 7350 * 7351 * @since 1.5.0 7352 * 7353 * @param int $id_product Product identifier 7354 * @param bool $shop_only 7355 * 7356 * @return array Attribute identifiers list 7357 */ 7358 public static function getProductAttributesIds($id_product, $shop_only = false) 7359 { 7360 return Db::getInstance()->executeS(' 7361 SELECT pa.id_product_attribute 7362 FROM `' . _DB_PREFIX_ . 'product_attribute` pa' . 7363 ($shop_only ? Shop::addSqlAssociation('product_attribute', 'pa') : '') . ' 7364 WHERE pa.`id_product` = ' . (int) $id_product); 7365 } 7366 7367 /** 7368 * Get label by lang and value by lang too. 7369 * 7370 * @param int $id_product Product identifier 7371 * @param int $id_product_attribute Attribute identifier 7372 * 7373 * @return array 7374 */ 7375 public static function getAttributesParams($id_product, $id_product_attribute) 7376 { 7377 if ($id_product_attribute == 0) { 7378 return []; 7379 } 7380 $id_lang = (int) Context::getContext()->language->id; 7381 $cache_id = 'Product::getAttributesParams_' . (int) $id_product . '-' . (int) $id_product_attribute . '-' . (int) $id_lang; 7382 7383 if (!Cache::isStored($cache_id)) { 7384 $result = Db::getInstance()->executeS(' 7385 SELECT a.`id_attribute`, a.`id_attribute_group`, al.`name`, agl.`name` as `group`, pa.`reference`, pa.`ean13`, pa.`isbn`, pa.`upc`, pa.`mpn` 7386 FROM `' . _DB_PREFIX_ . 'attribute` a 7387 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al 7388 ON (al.`id_attribute` = a.`id_attribute` AND al.`id_lang` = ' . (int) $id_lang . ') 7389 LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac 7390 ON (pac.`id_attribute` = a.`id_attribute`) 7391 LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa 7392 ON (pa.`id_product_attribute` = pac.`id_product_attribute`) 7393 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 7394 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl 7395 ON (a.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) $id_lang . ') 7396 WHERE pa.`id_product` = ' . (int) $id_product . ' 7397 AND pac.`id_product_attribute` = ' . (int) $id_product_attribute . ' 7398 AND agl.`id_lang` = ' . (int) $id_lang); 7399 Cache::store($cache_id, $result); 7400 } else { 7401 $result = Cache::retrieve($cache_id); 7402 } 7403 7404 return $result; 7405 } 7406 7407 /** 7408 * @param int $id_product Product identifier 7409 * 7410 * @return array 7411 */ 7412 public static function getAttributesInformationsByProduct($id_product) 7413 { 7414 $result = Db::getInstance()->executeS(' 7415 SELECT DISTINCT a.`id_attribute`, a.`id_attribute_group`, al.`name` as `attribute`, agl.`name` as `group`,pa.`reference`, pa.`ean13`, pa.`isbn`, pa.`upc`, pa.`mpn` 7416 FROM `' . _DB_PREFIX_ . 'attribute` a 7417 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al 7418 ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) Context::getContext()->language->id . ') 7419 LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl 7420 ON (a.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) Context::getContext()->language->id . ') 7421 LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac 7422 ON (a.`id_attribute` = pac.`id_attribute`) 7423 LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa 7424 ON (pac.`id_product_attribute` = pa.`id_product_attribute`) 7425 ' . Shop::addSqlAssociation('product_attribute', 'pa') . ' 7426 ' . Shop::addSqlAssociation('attribute', 'pac') . ' 7427 WHERE pa.`id_product` = ' . (int) $id_product); 7428 7429 return $result; 7430 } 7431 7432 /** 7433 * @return bool 7434 */ 7435 public function hasCombinations() 7436 { 7437 if (null === $this->id || 0 >= $this->id) { 7438 return false; 7439 } 7440 $attributes = self::getAttributesInformationsByProduct($this->id); 7441 7442 return !empty($attributes); 7443 } 7444 7445 /** 7446 * Get an id_product_attribute by an id_product and one or more 7447 * id_attribute. 7448 * 7449 * e.g: id_product 8 with id_attribute 4 (size medium) and 7450 * id_attribute 5 (color blue) returns id_product_attribute 9 which 7451 * is the dress size medium and color blue. 7452 * 7453 * @param int $idProduct Product identifier 7454 * @param int|int[] $idAttributes Attribute identifier(s) 7455 * @param bool $findBest 7456 * 7457 * @return int 7458 * 7459 * @throws PrestaShopException 7460 */ 7461 public static function getIdProductAttributeByIdAttributes($idProduct, $idAttributes, $findBest = false) 7462 { 7463 $idProduct = (int) $idProduct; 7464 7465 if (!is_array($idAttributes) && is_numeric($idAttributes)) { 7466 $idAttributes = [(int) $idAttributes]; 7467 } 7468 7469 if (!is_array($idAttributes) || empty($idAttributes)) { 7470 throw new PrestaShopException(sprintf('Invalid parameter $idAttributes with value: "%s"', print_r($idAttributes, true))); 7471 } 7472 7473 $idAttributesImploded = implode(',', array_map('intval', $idAttributes)); 7474 $idProductAttribute = Db::getInstance()->getValue( 7475 ' 7476 SELECT 7477 pac.`id_product_attribute` 7478 FROM 7479 `' . _DB_PREFIX_ . 'product_attribute_combination` pac 7480 INNER JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON pa.id_product_attribute = pac.id_product_attribute 7481 WHERE 7482 pa.id_product = ' . $idProduct . ' 7483 AND pac.id_attribute IN (' . $idAttributesImploded . ') 7484 GROUP BY 7485 pac.`id_product_attribute` 7486 HAVING 7487 COUNT(pa.id_product) = ' . count($idAttributes) 7488 ); 7489 7490 if ($idProductAttribute === false && $findBest) { 7491 //find the best possible combination 7492 //first we order $idAttributes by the group position 7493 $orderred = []; 7494 $result = Db::getInstance()->executeS( 7495 ' 7496 SELECT 7497 a.`id_attribute` 7498 FROM 7499 `' . _DB_PREFIX_ . 'attribute` a 7500 INNER JOIN `' . _DB_PREFIX_ . 'attribute_group` g ON a.`id_attribute_group` = g.`id_attribute_group` 7501 WHERE 7502 a.`id_attribute` IN (' . $idAttributesImploded . ') 7503 ORDER BY 7504 g.`position` ASC' 7505 ); 7506 7507 foreach ($result as $row) { 7508 $orderred[] = $row['id_attribute']; 7509 } 7510 7511 while ($idProductAttribute === false && count($orderred) > 1) { 7512 array_pop($orderred); 7513 $idProductAttribute = Db::getInstance()->getValue( 7514 ' 7515 SELECT 7516 pac.`id_product_attribute` 7517 FROM 7518 `' . _DB_PREFIX_ . 'product_attribute_combination` pac 7519 INNER JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON pa.id_product_attribute = pac.id_product_attribute 7520 WHERE 7521 pa.id_product = ' . (int) $idProduct . ' 7522 AND pac.id_attribute IN (' . implode(',', array_map('intval', $orderred)) . ') 7523 GROUP BY 7524 pac.id_product_attribute 7525 HAVING 7526 COUNT(pa.id_product) = ' . count($orderred) 7527 ); 7528 } 7529 } 7530 7531 if (empty($idProductAttribute)) { 7532 throw new PrestaShopObjectNotFoundException('Can not retrieve the id_product_attribute'); 7533 } 7534 7535 return $idProductAttribute; 7536 } 7537 7538 /** 7539 * @see Product::getIdProductAttributeByIdAttributes() 7540 * @deprecated 1.7.3.1 7541 * 7542 * @param int $id_product Product identifier 7543 * @param int|int[] $id_attributes Attribute identifier(s) 7544 * @param bool $find_best 7545 * 7546 * @return int 7547 */ 7548 public static function getIdProductAttributesByIdAttributes($id_product, $id_attributes, $find_best = false) 7549 { 7550 return self::getIdProductAttributeByIdAttributes($id_product, $id_attributes, $find_best); 7551 } 7552 7553 /** 7554 * Get the combination url anchor of the product. 7555 * 7556 * @param int $id_product_attribute Attribute identifier 7557 * @param bool $with_id 7558 * 7559 * @return string 7560 */ 7561 public function getAnchor($id_product_attribute, $with_id = false) 7562 { 7563 $attributes = Product::getAttributesParams($this->id, $id_product_attribute); 7564 $anchor = '#'; 7565 $sep = Configuration::get('PS_ATTRIBUTE_ANCHOR_SEPARATOR'); 7566 foreach ($attributes as &$a) { 7567 foreach ($a as &$b) { 7568 $b = str_replace($sep, '_', Tools::link_rewrite($b)); 7569 } 7570 $anchor .= '/' . ($with_id && isset($a['id_attribute']) && $a['id_attribute'] ? (int) $a['id_attribute'] . $sep : '') . $a['group'] . $sep . $a['name']; 7571 } 7572 7573 return $anchor; 7574 } 7575 7576 /** 7577 * Gets the name of a given product, in the given lang. 7578 * 7579 * @since 1.5.0 7580 * 7581 * @param int $id_product Product identifier 7582 * @param int|null $id_product_attribute Attribute identifier 7583 * @param int|null $id_lang Language identifier 7584 * 7585 * @return string 7586 */ 7587 public static function getProductName($id_product, $id_product_attribute = null, $id_lang = null) 7588 { 7589 // use the lang in the context if $id_lang is not defined 7590 if (!$id_lang) { 7591 $id_lang = (int) Context::getContext()->language->id; 7592 } 7593 7594 // creates the query object 7595 $query = new DbQuery(); 7596 7597 // selects different names, if it is a combination 7598 if ($id_product_attribute) { 7599 $query->select('IFNULL(CONCAT(pl.name, \' : \', GROUP_CONCAT(DISTINCT agl.`name`, \' - \', al.name SEPARATOR \', \')),pl.name) as name'); 7600 } else { 7601 $query->select('DISTINCT pl.name as name'); 7602 } 7603 7604 // adds joins & where clauses for combinations 7605 if ($id_product_attribute) { 7606 $query->from('product_attribute', 'pa'); 7607 $query->join(Shop::addSqlAssociation('product_attribute', 'pa')); 7608 $query->innerJoin('product_lang', 'pl', 'pl.id_product = pa.id_product AND pl.id_lang = ' . (int) $id_lang . Shop::addSqlRestrictionOnLang('pl')); 7609 $query->leftJoin('product_attribute_combination', 'pac', 'pac.id_product_attribute = pa.id_product_attribute'); 7610 $query->leftJoin('attribute', 'atr', 'atr.id_attribute = pac.id_attribute'); 7611 $query->leftJoin('attribute_lang', 'al', 'al.id_attribute = atr.id_attribute AND al.id_lang = ' . (int) $id_lang); 7612 $query->leftJoin('attribute_group_lang', 'agl', 'agl.id_attribute_group = atr.id_attribute_group AND agl.id_lang = ' . (int) $id_lang); 7613 $query->where('pa.id_product = ' . (int) $id_product . ' AND pa.id_product_attribute = ' . (int) $id_product_attribute); 7614 } else { 7615 // or just adds a 'where' clause for a simple product 7616 7617 $query->from('product_lang', 'pl'); 7618 $query->where('pl.id_product = ' . (int) $id_product); 7619 $query->where('pl.id_lang = ' . (int) $id_lang . Shop::addSqlRestrictionOnLang('pl')); 7620 } 7621 7622 return Db::getInstance()->getValue($query); 7623 } 7624 7625 /** 7626 * @param bool $autodate 7627 * @param bool $null_values 7628 * 7629 * @return bool 7630 */ 7631 public function addWs($autodate = true, $null_values = false) 7632 { 7633 $success = $this->add($autodate, $null_values); 7634 if ($success && Configuration::get('PS_SEARCH_INDEXATION')) { 7635 Search::indexation(false, $this->id); 7636 } 7637 7638 return $success; 7639 } 7640 7641 /** 7642 * @param bool $null_values 7643 * 7644 * @return bool 7645 */ 7646 public function updateWs($null_values = false) 7647 { 7648 if (null === $this->price) { 7649 $this->price = Product::getPriceStatic((int) $this->id, false, null, 6, null, false, true, 1, false, null, null, null, $this->specificPrice); 7650 } 7651 7652 if (null === $this->unit_price) { 7653 $this->unit_price = ($this->unit_price_ratio != 0 ? $this->price / $this->unit_price_ratio : 0); 7654 } 7655 7656 $success = parent::update($null_values); 7657 if ($success && Configuration::get('PS_SEARCH_INDEXATION')) { 7658 Search::indexation(false, $this->id); 7659 } 7660 Hook::exec('actionProductUpdate', ['id_product' => (int) $this->id]); 7661 7662 return $success; 7663 } 7664 7665 /** 7666 * For a given product, returns its real quantity. 7667 * 7668 * @since 1.5.0 7669 * 7670 * @param int $id_product Product identifier 7671 * @param int $id_product_attribute Attribute identifier 7672 * @param int $id_warehouse Warehouse identifier 7673 * @param int|null $id_shop Shop identifier 7674 * 7675 * @return int real_quantity 7676 */ 7677 public static function getRealQuantity($id_product, $id_product_attribute = 0, $id_warehouse = 0, $id_shop = null) 7678 { 7679 static $manager = null; 7680 7681 if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && null === $manager) { 7682 $manager = StockManagerFactory::getManager(); 7683 } 7684 7685 if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && Product::usesAdvancedStockManagement($id_product) && 7686 StockAvailable::dependsOnStock($id_product, $id_shop)) { 7687 return $manager->getProductRealQuantities($id_product, $id_product_attribute, $id_warehouse, true); 7688 } else { 7689 return StockAvailable::getQuantityAvailableByProduct($id_product, $id_product_attribute, $id_shop); 7690 } 7691 } 7692 7693 /** 7694 * For a given product, tells if it uses the advanced stock management. 7695 * 7696 * @since 1.5.0 7697 * 7698 * @param int $id_product Product identifier 7699 * 7700 * @return bool 7701 */ 7702 public static function usesAdvancedStockManagement($id_product) 7703 { 7704 $query = new DbQuery(); 7705 $query->select('product_shop.advanced_stock_management'); 7706 $query->from('product', 'p'); 7707 $query->join(Shop::addSqlAssociation('product', 'p')); 7708 $query->where('p.id_product = ' . (int) $id_product); 7709 7710 return (bool) Db::getInstance()->getValue($query); 7711 } 7712 7713 /** 7714 * This method allows to flush price cache. 7715 * 7716 * @since 1.5.0 7717 */ 7718 public static function flushPriceCache() 7719 { 7720 self::$_prices = []; 7721 self::$_pricesLevel2 = []; 7722 } 7723 7724 /** 7725 * Get list of parent categories. 7726 * 7727 * @since 1.5.0 7728 * 7729 * @param int|null $id_lang Language identifier 7730 * 7731 * @return array 7732 */ 7733 public function getParentCategories($id_lang = null) 7734 { 7735 if (!$id_lang) { 7736 $id_lang = Context::getContext()->language->id; 7737 } 7738 7739 $interval = Category::getInterval($this->id_category_default); 7740 $sql = new DbQuery(); 7741 $sql->from('category', 'c'); 7742 $sql->leftJoin('category_lang', 'cl', 'c.id_category = cl.id_category AND id_lang = ' . (int) $id_lang . Shop::addSqlRestrictionOnLang('cl')); 7743 $sql->where('c.nleft <= ' . (int) $interval['nleft'] . ' AND c.nright >= ' . (int) $interval['nright']); 7744 $sql->orderBy('c.nleft'); 7745 7746 return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); 7747 } 7748 7749 /** 7750 * Fill the variables used for stock management. 7751 */ 7752 public function loadStockData() 7753 { 7754 if (false === Validate::isLoadedObject($this)) { 7755 return; 7756 } 7757 7758 // Default product quantity is available quantity to sell in current shop 7759 $this->quantity = StockAvailable::getQuantityAvailableByProduct($this->id, 0); 7760 $this->out_of_stock = StockAvailable::outOfStock($this->id); 7761 $this->depends_on_stock = StockAvailable::dependsOnStock($this->id); 7762 $this->location = StockAvailable::getLocation($this->id) ?: ''; 7763 7764 if (Context::getContext()->shop->getContext() == Shop::CONTEXT_GROUP && Context::getContext()->shop->getContextShopGroup()->share_stock == 1) { 7765 $this->advanced_stock_management = $this->useAdvancedStockManagement(); 7766 } 7767 } 7768 7769 /** 7770 * Get Advanced Stock Management status for this product 7771 * 7772 * @return string 0 for disabled, 1 for enabled 7773 */ 7774 public function useAdvancedStockManagement() 7775 { 7776 return Db::getInstance()->getValue( 7777 ' 7778 SELECT `advanced_stock_management` 7779 FROM ' . _DB_PREFIX_ . 'product_shop 7780 WHERE id_product=' . (int) $this->id . Shop::addSqlRestriction() 7781 ); 7782 } 7783 7784 /** 7785 * Set Advanced Stock Management status for this product 7786 * 7787 * @param int $value 0 for disabled, 1 for enabled 7788 */ 7789 public function setAdvancedStockManagement($value) 7790 { 7791 $this->advanced_stock_management = (int) $value; 7792 if (Context::getContext()->shop->getContext() == Shop::CONTEXT_GROUP && Context::getContext()->shop->getContextShopGroup()->share_stock == 1) { 7793 Db::getInstance()->execute( 7794 ' 7795 UPDATE `' . _DB_PREFIX_ . 'product_shop` 7796 SET `advanced_stock_management`=' . (int) $value . ' 7797 WHERE id_product=' . (int) $this->id . Shop::addSqlRestriction() 7798 ); 7799 } else { 7800 $this->setFieldsToUpdate(['advanced_stock_management' => true]); 7801 $this->save(); 7802 } 7803 } 7804 7805 /** 7806 * Get the default category according to the shop. 7807 * 7808 * @return array 7809 */ 7810 public function getDefaultCategory() 7811 { 7812 $default_category = Db::getInstance()->getValue(' 7813 SELECT product_shop.`id_category_default` 7814 FROM `' . _DB_PREFIX_ . 'product` p 7815 ' . Shop::addSqlAssociation('product', 'p') . ' 7816 WHERE p.`id_product` = ' . (int) $this->id); 7817 7818 if (!$default_category) { 7819 return ['id_category_default' => Context::getContext()->shop->id_category]; 7820 } else { 7821 return $default_category; 7822 } 7823 } 7824 7825 /** 7826 * Get Shop identifiers 7827 * 7828 * @param int $id_product Product identifier 7829 * 7830 * @return array 7831 */ 7832 public static function getShopsByProduct($id_product) 7833 { 7834 return Db::getInstance()->executeS(' 7835 SELECT `id_shop` 7836 FROM `' . _DB_PREFIX_ . 'product_shop` 7837 WHERE `id_product` = ' . (int) $id_product); 7838 } 7839 7840 /** 7841 * Remove all downloadable files for product and its attributes. 7842 * 7843 * @return bool 7844 */ 7845 public function deleteDownload() 7846 { 7847 $result = true; 7848 $collection_download = new PrestaShopCollection('ProductDownload'); 7849 $collection_download->where('id_product', '=', $this->id); 7850 foreach ($collection_download as $product_download) { 7851 /* @var ProductDownload $product_download */ 7852 $result &= $product_download->delete($product_download->checkFile()); 7853 } 7854 7855 return $result; 7856 } 7857 7858 /** 7859 * @deprecated 1.5.0.10 7860 * @see Product::getAttributeCombinations() 7861 * 7862 * @param int $id_lang Language identifier 7863 * 7864 * @return array 7865 */ 7866 public function getAttributeCombinaisons($id_lang) 7867 { 7868 Tools::displayAsDeprecated('Use Product::getAttributeCombinations($id_lang)'); 7869 7870 return $this->getAttributeCombinations($id_lang); 7871 } 7872 7873 /** 7874 * @deprecated 1.5.0.10 7875 * @see Product::deleteAttributeCombination() 7876 * 7877 * @param int $id_product_attribute Attribute identifier 7878 * 7879 * @return array 7880 */ 7881 public function deleteAttributeCombinaison($id_product_attribute) 7882 { 7883 Tools::displayAsDeprecated('Use Product::deleteAttributeCombination($id_product_attribute)'); 7884 7885 return $this->deleteAttributeCombination($id_product_attribute); 7886 } 7887 7888 /** 7889 * Get the product type (simple, virtual, pack). 7890 * 7891 * @since in 1.5.0 7892 * 7893 * @return int 7894 */ 7895 public function getType() 7896 { 7897 if (!$this->id) { 7898 return Product::PTYPE_SIMPLE; 7899 } 7900 if (Pack::isPack($this->id)) { 7901 return Product::PTYPE_PACK; 7902 } 7903 if ($this->is_virtual) { 7904 return Product::PTYPE_VIRTUAL; 7905 } 7906 7907 return Product::PTYPE_SIMPLE; 7908 } 7909 7910 /** 7911 * @return bool 7912 */ 7913 public function hasAttributesInOtherShops() 7914 { 7915 return (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 7916 ' 7917 SELECT pa.id_product_attribute 7918 FROM `' . _DB_PREFIX_ . 'product_attribute` pa 7919 LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` pas ON (pa.`id_product_attribute` = pas.`id_product_attribute`) 7920 WHERE pa.`id_product` = ' . (int) $this->id 7921 ); 7922 } 7923 7924 /** 7925 * @return string TaxRulesGroup identifier most used 7926 */ 7927 public static function getIdTaxRulesGroupMostUsed() 7928 { 7929 return Db::getInstance()->getValue( 7930 ' 7931 SELECT id_tax_rules_group 7932 FROM ( 7933 SELECT COUNT(*) n, product_shop.id_tax_rules_group 7934 FROM ' . _DB_PREFIX_ . 'product p 7935 ' . Shop::addSqlAssociation('product', 'p') . ' 7936 JOIN ' . _DB_PREFIX_ . 'tax_rules_group trg ON (product_shop.id_tax_rules_group = trg.id_tax_rules_group) 7937 WHERE trg.active = 1 AND trg.deleted = 0 7938 GROUP BY product_shop.id_tax_rules_group 7939 ORDER BY n DESC 7940 LIMIT 1 7941 ) most_used' 7942 ); 7943 } 7944 7945 /** 7946 * For a given ean13 reference, returns the corresponding id. 7947 * 7948 * @param string $ean13 7949 * 7950 * @return int|string Product identifier 7951 */ 7952 public static function getIdByEan13($ean13) 7953 { 7954 if (empty($ean13)) { 7955 return 0; 7956 } 7957 7958 if (!Validate::isEan13($ean13)) { 7959 return 0; 7960 } 7961 7962 $query = new DbQuery(); 7963 $query->select('p.id_product'); 7964 $query->from('product', 'p'); 7965 $query->where('p.ean13 = \'' . pSQL($ean13) . '\''); 7966 7967 return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query); 7968 } 7969 7970 /** 7971 * For a given reference, returns the corresponding id. 7972 * 7973 * @param string $reference 7974 * 7975 * @return int|string Product identifier 7976 */ 7977 public static function getIdByReference($reference) 7978 { 7979 if (empty($reference)) { 7980 return 0; 7981 } 7982 7983 if (!Validate::isReference($reference)) { 7984 return 0; 7985 } 7986 7987 $query = new DbQuery(); 7988 $query->select('p.id_product'); 7989 $query->from('product', 'p'); 7990 $query->where('p.reference = \'' . pSQL($reference) . '\''); 7991 7992 return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query); 7993 } 7994 7995 /** 7996 * @return string simple, pack, virtual 7997 */ 7998 public function getWsType() 7999 { 8000 $type_information = [ 8001 Product::PTYPE_SIMPLE => 'simple', 8002 Product::PTYPE_PACK => 'pack', 8003 Product::PTYPE_VIRTUAL => 'virtual', 8004 ]; 8005 8006 return $type_information[$this->getType()]; 8007 } 8008 8009 /** 8010 * Create the link rewrite if not exists or invalid on product creation 8011 * 8012 * @return bool 8013 */ 8014 public function modifierWsLinkRewrite() 8015 { 8016 if (empty($this->link_rewrite)) { 8017 $this->link_rewrite = []; 8018 } 8019 8020 foreach ($this->name as $id_lang => $name) { 8021 if (empty($this->link_rewrite[$id_lang])) { 8022 $this->link_rewrite[$id_lang] = Tools::link_rewrite($name); 8023 } elseif (!Validate::isLinkRewrite($this->link_rewrite[$id_lang])) { 8024 $this->link_rewrite[$id_lang] = Tools::link_rewrite($this->link_rewrite[$id_lang]); 8025 } 8026 } 8027 8028 return true; 8029 } 8030 8031 /** 8032 * @return array 8033 */ 8034 public function getWsProductBundle() 8035 { 8036 return Db::getInstance()->executeS('SELECT id_product_item as id, id_product_attribute_item as id_product_attribute, quantity FROM ' . _DB_PREFIX_ . 'pack WHERE id_product_pack = ' . (int) $this->id); 8037 } 8038 8039 /** 8040 * @param string $type_str simple, pack, virtual 8041 * 8042 * @return bool 8043 */ 8044 public function setWsType($type_str) 8045 { 8046 $reverse_type_information = [ 8047 'simple' => Product::PTYPE_SIMPLE, 8048 'pack' => Product::PTYPE_PACK, 8049 'virtual' => Product::PTYPE_VIRTUAL, 8050 ]; 8051 8052 if (!isset($reverse_type_information[$type_str])) { 8053 return false; 8054 } 8055 8056 $type = $reverse_type_information[$type_str]; 8057 8058 if (Pack::isPack((int) $this->id) && $type != Product::PTYPE_PACK) { 8059 Pack::deleteItems($this->id); 8060 } 8061 8062 $this->cache_is_pack = ($type == Product::PTYPE_PACK); 8063 $this->is_virtual = ($type == Product::PTYPE_VIRTUAL); 8064 $this->product_type = $this->getDynamicProductType(); 8065 8066 return true; 8067 } 8068 8069 /** 8070 * @param array $items 8071 * 8072 * @return bool 8073 */ 8074 public function setWsProductBundle($items) 8075 { 8076 if ($this->is_virtual) { 8077 return false; 8078 } 8079 8080 Pack::deleteItems($this->id); 8081 8082 foreach ($items as $item) { 8083 // Combination of a product is optional, and can be omitted. 8084 if (!isset($item['product_attribute_id'])) { 8085 $item['product_attribute_id'] = 0; 8086 } 8087 if ((int) $item['id'] > 0) { 8088 Pack::addItem($this->id, (int) $item['id'], (int) $item['quantity'], (int) $item['product_attribute_id']); 8089 } 8090 } 8091 8092 return true; 8093 } 8094 8095 /** 8096 * @param int $id_attribute Attribute identifier 8097 * @param int $id_shop Shop identifier 8098 * 8099 * @return string Attribute identifier 8100 */ 8101 public function isColorUnavailable($id_attribute, $id_shop) 8102 { 8103 return Db::getInstance()->getValue( 8104 ' 8105 SELECT sa.id_product_attribute 8106 FROM ' . _DB_PREFIX_ . 'stock_available sa 8107 WHERE id_product=' . (int) $this->id . ' AND quantity <= 0 8108 ' . StockAvailable::addSqlShopRestriction(null, $id_shop, 'sa') . ' 8109 AND EXISTS ( 8110 SELECT 1 8111 FROM ' . _DB_PREFIX_ . 'product_attribute pa 8112 JOIN ' . _DB_PREFIX_ . 'product_attribute_shop product_attribute_shop 8113 ON (product_attribute_shop.id_product_attribute = pa.id_product_attribute AND product_attribute_shop.id_shop=' . (int) $id_shop . ') 8114 JOIN ' . _DB_PREFIX_ . 'product_attribute_combination pac 8115 ON (pac.id_product_attribute AND product_attribute_shop.id_product_attribute) 8116 WHERE sa.id_product_attribute = pa.id_product_attribute AND pa.id_product=' . (int) $this->id . ' AND pac.id_attribute=' . (int) $id_attribute . ' 8117 )' 8118 ); 8119 } 8120 8121 /** 8122 * @param int $id_product Product identifier 8123 * @param bool $full 8124 * 8125 * @return string 8126 */ 8127 public static function getColorsListCacheId($id_product, $full = true) 8128 { 8129 $cache_id = 'productlist_colors'; 8130 if ($id_product) { 8131 $cache_id .= '|' . (int) $id_product; 8132 } 8133 8134 if ($full) { 8135 $cache_id .= '|' . (int) Context::getContext()->shop->id . '|' . (int) Context::getContext()->cookie->id_lang; 8136 } 8137 8138 return $cache_id; 8139 } 8140 8141 /** 8142 * @param int $id_product Product identifier 8143 * @param int $pack_stock_type value of Pack stock type, see constants defined in Pack class 8144 * 8145 * @return bool 8146 */ 8147 public static function setPackStockType($id_product, $pack_stock_type) 8148 { 8149 return Db::getInstance()->execute('UPDATE ' . _DB_PREFIX_ . 'product p 8150 ' . Shop::addSqlAssociation('product', 'p') . ' SET product_shop.pack_stock_type = ' . (int) $pack_stock_type . ' WHERE p.`id_product` = ' . (int) $id_product); 8151 } 8152 8153 /** 8154 * Gets a list of IDs from a list of IDs/Refs. The result will avoid duplicates, and checks if given IDs/Refs exists in DB. 8155 * Useful when a product list should be checked before a bulk operation on them (Only 1 query => performances). 8156 * 8157 * @param int|string|int[]|string[] $ids_or_refs Product identifier(s) or reference(s) 8158 * 8159 * @return array|false Product identifiers, without duplicate and only existing ones 8160 */ 8161 public static function getExistingIdsFromIdsOrRefs($ids_or_refs) 8162 { 8163 // separate IDs and Refs 8164 $ids = []; 8165 $refs = []; 8166 $whereStatements = []; 8167 foreach ((is_array($ids_or_refs) ? $ids_or_refs : [$ids_or_refs]) as $id_or_ref) { 8168 if (is_numeric($id_or_ref)) { 8169 $ids[] = (int) $id_or_ref; 8170 } elseif (is_string($id_or_ref)) { 8171 $refs[] = '\'' . pSQL($id_or_ref) . '\''; 8172 } 8173 } 8174 8175 // construct WHERE statement with OR combination 8176 if (count($ids) > 0) { 8177 $whereStatements[] = ' p.id_product IN (' . implode(',', $ids) . ') '; 8178 } 8179 if (count($refs) > 0) { 8180 $whereStatements[] = ' p.reference IN (' . implode(',', $refs) . ') '; 8181 } 8182 if (!count($whereStatements)) { 8183 return false; 8184 } 8185 8186 $results = Db::getInstance()->executeS(' 8187 SELECT DISTINCT `id_product` 8188 FROM `' . _DB_PREFIX_ . 'product` p 8189 WHERE ' . implode(' OR ', $whereStatements)); 8190 8191 // simplify array since there is 1 useless dimension. 8192 // FIXME : find a better way to avoid this, directly in SQL? 8193 foreach ($results as $k => $v) { 8194 $results[$k] = (int) $v['id_product']; 8195 } 8196 8197 return $results; 8198 } 8199 8200 /** 8201 * Get object of redirect_type. 8202 * 8203 * @return string|false category, product, false if unknown redirect_type 8204 */ 8205 public function getRedirectType() 8206 { 8207 switch ($this->redirect_type) { 8208 case ProductInterface::REDIRECT_TYPE_CATEGORY_MOVED_PERMANENTLY: 8209 case ProductInterface::REDIRECT_TYPE_CATEGORY_FOUND: 8210 return 'category'; 8211 8212 break; 8213 8214 case ProductInterface::REDIRECT_TYPE_PRODUCT_MOVED_PERMANENTLY: 8215 case ProductInterface::REDIRECT_TYPE_PRODUCT_FOUND: 8216 return 'product'; 8217 8218 break; 8219 } 8220 8221 return false; 8222 } 8223 8224 /** 8225 * Return an array of customization fields IDs. 8226 * 8227 * @return array|false 8228 */ 8229 public function getUsedCustomizationFieldsIds() 8230 { 8231 return Db::getInstance()->executeS( 8232 'SELECT cd.`index` FROM `' . _DB_PREFIX_ . 'customized_data` cd 8233 LEFT JOIN `' . _DB_PREFIX_ . 'customization_field` cf ON cf.`id_customization_field` = cd.`index` 8234 WHERE cf.`id_product` = ' . (int) $this->id 8235 ); 8236 } 8237 8238 /** 8239 * Remove unused customization for the product. 8240 * 8241 * @param array $customizationIds - Array of customization fields IDs 8242 * 8243 * @return bool 8244 * 8245 * @throws PrestaShopDatabaseException 8246 */ 8247 public function deleteUnusedCustomizationFields($customizationIds) 8248 { 8249 $return = true; 8250 if (is_array($customizationIds) && !empty($customizationIds)) { 8251 $toDeleteIds = implode(',', $customizationIds); 8252 $return &= Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'customization_field` WHERE 8253 `id_product` = ' . (int) $this->id . ' AND `id_customization_field` IN (' . $toDeleteIds . ')'); 8254 8255 $return &= Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'customization_field_lang` WHERE 8256 `id_customization_field` IN (' . $toDeleteIds . ')'); 8257 } 8258 8259 if (!$return) { 8260 throw new PrestaShopDatabaseException('An error occurred while deletion the customization fields'); 8261 } 8262 8263 return $return; 8264 } 8265 8266 /** 8267 * Update the customization fields to be deleted if not used. 8268 * 8269 * @param array $customizationIds - Array of excluded customization fields IDs 8270 * 8271 * @return bool 8272 * 8273 * @throws PrestaShopDatabaseException 8274 */ 8275 public function softDeleteCustomizationFields($customizationIds) 8276 { 8277 $return = true; 8278 $updateQuery = 'UPDATE `' . _DB_PREFIX_ . 'customization_field` cf 8279 SET cf.`is_deleted` = 1 8280 WHERE 8281 cf.`id_product` = ' . (int) $this->id . ' 8282 AND cf.`is_deleted` = 0 '; 8283 8284 if (is_array($customizationIds) && !empty($customizationIds)) { 8285 $updateQuery .= 'AND cf.`id_customization_field` NOT IN (' . implode(',', array_map('intval', $customizationIds)) . ')'; 8286 } 8287 8288 $return &= Db::getInstance()->execute($updateQuery); 8289 8290 if (!$return) { 8291 throw new PrestaShopDatabaseException('An error occurred while soft deletion the customization fields'); 8292 } 8293 8294 return $return; 8295 } 8296 8297 /** 8298 * Update default supplier data 8299 * 8300 * @param int $idSupplier 8301 * @param float $wholesalePrice 8302 * @param string $supplierReference 8303 * 8304 * @return bool 8305 */ 8306 public function updateDefaultSupplierData(int $idSupplier, string $supplierReference, float $wholesalePrice): bool 8307 { 8308 if (!$this->id) { 8309 return false; 8310 } 8311 8312 $sql = 'UPDATE `' . _DB_PREFIX_ . 'product` ' . 8313 'SET ' . 8314 'id_supplier = %d, ' . 8315 'supplier_reference = "%s", ' . 8316 'wholesale_price = "%s" ' . 8317 'WHERE id_product = %d'; 8318 8319 return Db::getInstance()->execute( 8320 sprintf( 8321 $sql, 8322 $idSupplier, 8323 pSQL($supplierReference), 8324 $wholesalePrice, 8325 $this->id 8326 ) 8327 ); 8328 } 8329 8330 /** 8331 * Get Product ecotax 8332 * 8333 * @param int $precision 8334 * @param bool $include_tax 8335 * @param bool $formated 8336 * 8337 * @return ecotax 8338 */ 8339 public function getEcotax($precision = null, $include_tax = true, $formated = false) 8340 { 8341 $context = Context::getContext(); 8342 $currency = $context->currency; 8343 $precision = $precision ?? $currency->precision; 8344 $ecotax_rate = $include_tax ? (float) Tax::getProductEcotaxRate() : 0; 8345 $ecotax = Tools::ps_round( 8346 (float) $this->ecotax * (1 + $ecotax_rate / 100), 8347 $precision, 8348 null 8349 ); 8350 8351 return $formated ? $context->getCurrentLocale()->formatPrice($ecotax, $currency->iso_code) : $ecotax; 8352 } 8353 8354 /** 8355 * @return string 8356 */ 8357 public function getProductType(): string 8358 { 8359 // Default value is the one saved, but in case it is not set we use dynamic definition 8360 if (!empty($this->product_type) && in_array($this->product_type, ProductType::AVAILABLE_TYPES)) { 8361 return $this->product_type; 8362 } 8363 8364 return $this->getDynamicProductType(); 8365 } 8366 8367 /** 8368 * Returns product type based on existing associations without taking the saved value 8369 * in database into account. 8370 * 8371 * @return string 8372 */ 8373 public function getDynamicProductType(): string 8374 { 8375 if ($this->is_virtual) { 8376 return ProductType::TYPE_VIRTUAL; 8377 } elseif (Pack::isPack($this->id)) { 8378 return ProductType::TYPE_PACK; 8379 } elseif ($this->hasCombinations()) { 8380 return ProductType::TYPE_COMBINATIONS; 8381 } 8382 8383 return ProductType::TYPE_STANDARD; 8384 } 8385} 8386