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