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