1<?php
2/**
3 * 2007-2016 PrestaShop
4 *
5 * thirty bees is an extension to the PrestaShop e-commerce software developed by PrestaShop SA
6 * Copyright (C) 2017-2018 thirty bees
7 *
8 * NOTICE OF LICENSE
9 *
10 * This source file is subject to the Open Software License (OSL 3.0)
11 * that is bundled with this package in the file LICENSE.txt.
12 * It is also available through the world-wide-web at this URL:
13 * http://opensource.org/licenses/osl-3.0.php
14 * If you did not receive a copy of the license and are unable to
15 * obtain it through the world-wide-web, please send an email
16 * to license@thirtybees.com so we can send you a copy immediately.
17 *
18 * DISCLAIMER
19 *
20 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
21 * versions in the future. If you wish to customize PrestaShop for your
22 * needs please refer to https://www.thirtybees.com for more information.
23 *
24 * @author    thirty bees <contact@thirtybees.com>
25 * @author    PrestaShop SA <contact@prestashop.com>
26 * @copyright 2017-2018 thirty bees
27 * @copyright 2007-2016 PrestaShop SA
28 * @license   http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
29 *  PrestaShop is an internationally registered trademark & property of PrestaShop SA
30 */
31
32/**
33 * Class CartCore
34 *
35 * @since 1.0.0
36 */
37class CartCore extends ObjectModel
38{
39    // @codingStandardsIgnoreStart
40    const ONLY_PRODUCTS = 1;
41    const ONLY_DISCOUNTS = 2;
42    const BOTH = 3;
43    const BOTH_WITHOUT_SHIPPING = 4;
44    const ONLY_SHIPPING = 5;
45    const ONLY_WRAPPING = 6;
46    const ONLY_PRODUCTS_WITHOUT_SHIPPING = 7;
47    const ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING = 8;
48    /**
49     * @see ObjectModel::$definition
50     */
51    public static $definition = [
52        'table'   => 'cart',
53        'primary' => 'id_cart',
54        'fields'  => [
55            'id_shop_group'           => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
56            'id_shop'                 => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
57            'id_address_delivery'     => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
58            'id_address_invoice'      => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
59            'id_carrier'              => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
60            'id_currency'             => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
61            'id_customer'             => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
62            'id_guest'                => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
63            'id_lang'                 => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
64            'recyclable'              => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
65            'gift'                    => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
66            'gift_message'            => ['type' => self::TYPE_STRING, 'validate' => 'isMessage'],
67            'mobile_theme'            => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
68            'delivery_option'         => ['type' => self::TYPE_STRING],
69            'secure_key'              => ['type' => self::TYPE_STRING, 'size' => 32],
70            'allow_seperated_package' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
71            'date_add'                => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
72            'date_upd'                => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
73        ],
74    ];
75    /** @var array $_nbProducts */
76    protected static $_nbProducts = [];
77    /** @var array $_isVirtualCart */
78    protected static $_isVirtualCart = [];
79    /** @var array $_totalWeight */
80    protected static $_totalWeight = [];
81    protected static $_carriers = null;
82    protected static $_taxes_rate = null;
83    protected static $_attributesLists = [];
84    /** @var Customer|null */
85    protected static $_customer = null;
86    public $id_shop_group;
87    public $id_shop;
88    /** @var int Customer delivery address ID */
89    public $id_address_delivery;
90    /** @var int Customer invoicing address ID */
91    public $id_address_invoice;
92    /** @var int Customer currency ID */
93    public $id_currency;
94    /** @var int Customer ID */
95    public $id_customer;
96    /** @var int Guest ID */
97    public $id_guest;
98    /** @var int Language ID */
99    public $id_lang;
100    /** @var bool True if the customer wants a recycled package */
101    public $recyclable = 0;
102    /** @var bool True if the customer wants a gift wrapping */
103    public $gift = 0;
104    /** @var string Gift message if specified */
105    public $gift_message;
106    /** @var bool Mobile Theme */
107    public $mobile_theme;
108    /** @var string Object creation date */
109    public $date_add;
110    /** @var string secure_key */
111    public $secure_key;
112    /** @var int Carrier ID */
113    public $id_carrier = 0;
114    /** @var string Object last modification date */
115    public $date_upd;
116    /** @var bool $checkedTos */
117    public $checkedTos = false;
118    public $pictures;
119    public $textFields;
120    public $delivery_option;
121    /** @var bool Allow to seperate order in multiple package in order to recieve as soon as possible the available products */
122    public $allow_seperated_package = false;
123    protected $_products = null;
124    protected $_taxCalculationMethod = PS_TAX_EXC;
125    protected $webserviceParameters = [
126        'fields'       => [
127            'id_address_delivery' => ['xlink_resource' => 'addresses'],
128            'id_address_invoice'  => ['xlink_resource' => 'addresses'],
129            'id_currency'         => ['xlink_resource' => 'currencies'],
130            'id_customer'         => ['xlink_resource' => 'customers'],
131            'id_guest'            => ['xlink_resource' => 'guests'],
132            'id_lang'             => ['xlink_resource' => 'languages'],
133        ],
134        'associations' => [
135            'cart_rows' => [
136                'resource' => 'cart_row', 'virtual_entity' => true, 'fields' => [
137                    'id_product'           => ['required' => true, 'xlink_resource' => 'products'],
138                    'id_product_attribute' => ['required' => true, 'xlink_resource' => 'combinations'],
139                    'id_address_delivery'  => ['required' => true, 'xlink_resource' => 'addresses'],
140                    'quantity'             => ['required' => true],
141                ],
142            ],
143        ],
144    ];
145    // @codingStandardsIgnoreEnd
146
147    /**
148     * CartCore constructor.
149     *
150     * @param null $id
151     * @param null $idLang
152     *
153     * @since   1.0.0
154     * @version 1.0.0 Initial version
155     * @throws PrestaShopException
156     */
157    public function __construct($id = null, $idLang = null)
158    {
159        parent::__construct($id);
160
161        if (!is_null($idLang)) {
162            $this->id_lang = (int) (Language::getLanguage($idLang) !== false) ? $idLang : Configuration::get('PS_LANG_DEFAULT');
163        }
164
165        if ($this->id_customer) {
166            if (isset(Context::getContext()->customer) && Context::getContext()->customer->id == $this->id_customer) {
167                $customer = Context::getContext()->customer;
168            } else {
169                $customer = new Customer((int) $this->id_customer);
170            }
171
172            static::$_customer = $customer;
173
174            if ((!$this->secure_key || $this->secure_key == '-1') && $customer->secure_key) {
175                $this->secure_key = $customer->secure_key;
176                $this->save();
177            }
178        }
179
180        $this->setTaxCalculationMethod();
181    }
182
183    /**
184     * @since   1.0.0
185     * @version 1.0.0 Initial version
186     * @throws PrestaShopException
187     */
188    public function setTaxCalculationMethod()
189    {
190        $this->_taxCalculationMethod = (int) Group::getPriceDisplayMethod(
191            Group::getCurrent()->id
192        );
193    }
194
195    /**
196     * Get the average tax used in the Cart
197     *
198     * @param int $idCart
199     *
200     * @return float|int
201     * @throws PrestaShopException
202     */
203    public static function getTaxesAverageUsed($idCart)
204    {
205        $cart = new Cart((int) $idCart);
206        if (!Validate::isLoadedObject($cart)) {
207            die(Tools::displayError());
208        }
209
210        if (!Configuration::get('PS_TAX')) {
211            return 0;
212        }
213
214        $products = $cart->getProducts();
215        $totalProductsMoy = 0;
216        $ratioTax = 0;
217
218        if (!count($products)) {
219            return 0;
220        }
221
222        foreach ($products as $product) {
223            // products refer to the cart details
224
225            if (Configuration::get('PS_TAX_ADDRESS_TYPE') == 'id_address_invoice') {
226                $addressId = (int) $cart->id_address_invoice;
227            } else {
228                $addressId = (int) $product['id_address_delivery'];
229            } // Get delivery address of the product from the cart
230            if (!Address::addressExists($addressId)) {
231                $addressId = null;
232            }
233
234            $totalProductsMoy += $product['total_wt'];
235            $ratioTax += $product['total_wt'] * Tax::getProductTaxRate((int) $product['id_product'], (int) $addressId);
236        }
237
238        if ($totalProductsMoy > 0) {
239            return $ratioTax / $totalProductsMoy;
240        }
241
242        return 0;
243    }
244
245    /**
246     * Return cart products
247     *
248     * @param bool $refresh
249     * @param bool $idProduct
250     * @param null $idCountry
251     *
252     * @return array|null
253     * @throws PrestaShopException
254     * @throws PrestaShopException
255     */
256    public function getProducts($refresh = false, $idProduct = false, $idCountry = null)
257    {
258        if (!$this->id) {
259            return [];
260        }
261
262        // Product cache must be strictly compared to NULL, or else an empty cart will add dozens of queries
263        if ($this->_products !== null && !$refresh) {
264            // Return product row with specified ID if it exists
265            if (is_int($idProduct)) {
266                foreach ($this->_products as $product) {
267                    if ($product['id_product'] == $idProduct) {
268                        return [$product];
269                    }
270                }
271
272                return [];
273            }
274
275            return $this->_products;
276        }
277
278        // Build query
279        $sql = new DbQuery();
280
281        // Build SELECT
282        $sql->select('cp.`id_product_attribute`');
283        $sql->select('cp.`id_product`');
284        $sql->select('cp.`quantity` AS `cart_quantity`');
285        $sql->select('cp.`id_shop`');
286        $sql->select('pl.`name`');
287        $sql->select('p.`is_virtual`');
288        $sql->select('pl.`description_short`');
289        $sql->select('pl.`available_now`');
290        $sql->select('pl.`available_later`');
291        $sql->select('product_shop.`id_category_default`');
292        $sql->select('p.`id_supplier`');
293        $sql->select('p.`id_manufacturer`');
294        $sql->select('product_shop.`on_sale`');
295        $sql->select('product_shop.`ecotax`');
296        $sql->select('product_shop.`additional_shipping_cost`');
297        $sql->select('product_shop.`available_for_order`');
298        $sql->select('product_shop.`price`');
299        $sql->select('product_shop.`active`');
300        $sql->select('product_shop.`unity`');
301        $sql->select('product_shop.`unit_price_ratio`');
302        $sql->select('stock.`quantity` AS `quantity_available`');
303        $sql->select('p.`width`');
304        $sql->select('p.`height`');
305        $sql->select('p.`depth`');
306        $sql->select('p.`weight`');
307        $sql->select('stock.`out_of_stock`');
308        $sql->select('p.`date_add`');
309        $sql->select('p.`date_upd`');
310        $sql->select('IFNULL(stock.`quantity`, 0) AS `quantity`');
311        $sql->select('pl.`link_rewrite`');
312        $sql->select('cl.`link_rewrite` AS `category`');
313        $sql->select('CONCAT(LPAD(cp.`id_product`, 10, 0), LPAD(IFNULL(cp.`id_product_attribute`, 0), 10, 0), IFNULL(cp.`id_address_delivery`, 0)) AS unique_id');
314        $sql->select('cp.`id_address_delivery`');
315        $sql->select('product_shop.`advanced_stock_management`');
316        $sql->select('ps.`product_supplier_reference` AS `supplier_reference`');
317
318        // Build FROM
319        $sql->from('cart_product', 'cp');
320
321        // Build JOIN
322        $sql->leftJoin('product', 'p', 'p.`id_product` = cp.`id_product`');
323        $sql->innerJoin('product_shop', 'product_shop', '(product_shop.`id_shop` = cp.`id_shop` AND product_shop.`id_product` = p.`id_product`)');
324        $sql->leftJoin(
325            'product_lang',
326            'pl',
327            'p.`id_product` = pl.`id_product` AND pl.`id_lang` = '.(int) $this->id_lang.Shop::addSqlRestrictionOnLang('pl', 'cp.id_shop')
328        );
329
330        $sql->leftJoin(
331            'category_lang',
332            'cl',
333            'product_shop.`id_category_default` = cl.`id_category` AND cl.`id_lang` = '.(int) $this->id_lang.Shop::addSqlRestrictionOnLang('cl', 'cp.id_shop')
334        );
335
336        $sql->leftJoin('product_supplier', 'ps', 'ps.`id_product` = cp.`id_product` AND ps.`id_product_attribute` = cp.`id_product_attribute` AND ps.`id_supplier` = p.`id_supplier`');
337
338        // @todo test if everything is ok, then refactorise call of this method
339        $sql->join(Product::sqlStock('cp', 'cp'));
340
341        // Build WHERE clauses
342        $sql->where('cp.`id_cart` = '.(int) $this->id);
343        if ($idProduct) {
344            $sql->where('cp.`id_product` = '.(int) $idProduct);
345        }
346        $sql->where('p.`id_product` IS NOT NULL');
347
348        // Build ORDER BY
349        $sql->orderBy('cp.`date_add`, cp.`id_product`, cp.`id_product_attribute` ASC');
350
351        if (Customization::isFeatureActive()) {
352            $sql->select('cu.`id_customization`, cu.`quantity` AS customization_quantity');
353            $sql->leftJoin(
354                'customization',
355                'cu',
356                'p.`id_product` = cu.`id_product` AND cp.`id_product_attribute` = cu.`id_product_attribute` AND cu.`id_cart` = '.(int) $this->id
357            );
358            $sql->groupBy('cp.`id_product_attribute`, cp.`id_product`, cp.`id_shop`');
359        } else {
360            $sql->select('NULL AS customization_quantity, NULL AS id_customization');
361        }
362
363        if (Combination::isFeatureActive()) {
364            $sql->select('product_attribute_shop.`price` AS price_attribute, product_attribute_shop.`ecotax` AS ecotax_attr');
365            $sql->select('IF (IFNULL(pa.`reference`, \'\') = \'\', p.`reference`, pa.`reference`) AS reference');
366            $sql->select('(p.`weight`+ pa.`weight`) weight_attribute');
367            $sql->select('IF (IFNULL(pa.`ean13`, \'\') = \'\', p.`ean13`, pa.`ean13`) AS ean13');
368            $sql->select('IF (IFNULL(pa.`upc`, \'\') = \'\', p.`upc`, pa.`upc`) AS upc');
369            $sql->select('IFNULL(product_attribute_shop.`minimal_quantity`, product_shop.`minimal_quantity`) as minimal_quantity');
370            $sql->select('IF(product_attribute_shop.wholesale_price > 0,  product_attribute_shop.wholesale_price, product_shop.`wholesale_price`) wholesale_price');
371            $sql->leftJoin('product_attribute', 'pa', 'pa.`id_product_attribute` = cp.`id_product_attribute`');
372            $sql->leftJoin('product_attribute_shop', 'product_attribute_shop', '(product_attribute_shop.`id_shop` = cp.`id_shop` AND product_attribute_shop.`id_product_attribute` = pa.`id_product_attribute`)');
373        } else {
374            $sql->select('p.`reference` AS `reference`');
375            $sql->select('p.`ean13`');
376            $sql->select('p.`upc` AS `upc`');
377            $sql->select('product_shop.`minimal_quantity` AS `minimal_quantity`');
378            $sql->select('product_shop.`wholesale_price` AS `wholesale_price`');
379        }
380
381        $sql->select('image_shop.`id_image` id_image, il.`legend`');
382        $sql->leftJoin('image_shop', 'image_shop', 'image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop='.(int) $this->id_shop);
383        $sql->leftJoin('image_lang', 'il', 'il.`id_image` = image_shop.`id_image` AND il.`id_lang` = '.(int) $this->id_lang);
384
385        $result = Db::getInstance()->executeS($sql);
386
387        // Reset the cache before the following return, or else an empty cart will add dozens of queries
388        $productsIds = [];
389        $paIds = [];
390        if ($result) {
391            foreach ($result as $key => $row) {
392                $productsIds[] = $row['id_product'];
393                $paIds[] = $row['id_product_attribute'];
394                $specificPrice = SpecificPrice::getSpecificPrice($row['id_product'], $this->id_shop, $this->id_currency, $idCountry, $this->id_shop_group, $row['cart_quantity'], $row['id_product_attribute'], $this->id_customer, $this->id);
395                if ($specificPrice) {
396                    $reductionTypeRow = ['reduction_type' => $specificPrice['reduction_type']];
397                } else {
398                    $reductionTypeRow = ['reduction_type' => 0];
399                }
400
401                $result[$key] = array_merge($row, $reductionTypeRow);
402            }
403        }
404        // Thus you can avoid one query per product, because there will be only one query for all the products of the cart
405        Product::cacheProductsFeatures($productsIds);
406        static::cacheSomeAttributesLists($paIds, $this->id_lang);
407
408        $this->_products = [];
409        if (empty($result)) {
410            return [];
411        }
412
413        $ecotaxRate = (float) Tax::getProductEcotaxRate($this->{Configuration::get('PS_TAX_ADDRESS_TYPE')});
414        $applyEcoTax = Product::$_taxCalculationMethod == PS_TAX_INC && (int) Configuration::get('PS_TAX');
415        $cartShopContext = Context::getContext()->cloneContext();
416
417        foreach ($result as &$row) {
418            if (isset($row['ecotax_attr']) && $row['ecotax_attr'] > 0) {
419                $row['ecotax'] = (float) $row['ecotax_attr'];
420            }
421
422            $row['stock_quantity'] = (int) $row['quantity'];
423            $row['quantity'] = (int) $row['cart_quantity'];
424
425            if (isset($row['id_product_attribute']) && (int) $row['id_product_attribute'] && isset($row['weight_attribute'])) {
426                $row['weight'] = (float) $row['weight_attribute'];
427            }
428
429            if (Configuration::get('PS_TAX_ADDRESS_TYPE') == 'id_address_invoice') {
430                $addressId = (int) $this->id_address_invoice;
431            } else {
432                $addressId = (int) $row['id_address_delivery'];
433            }
434            if (!Address::addressExists($addressId)) {
435                $addressId = null;
436            }
437
438            if ($cartShopContext->shop->id != $row['id_shop']) {
439                $cartShopContext->shop = new Shop((int) $row['id_shop']);
440            }
441
442            $address = Address::initialize($addressId, true);
443            $idTaxRulesGroup = Product::getIdTaxRulesGroupByIdProduct((int) $row['id_product'], $cartShopContext);
444            $taxCalculator = TaxManagerFactory::getManager($address, $idTaxRulesGroup)->getTaxCalculator();
445
446            $row['price_without_reduction'] = Product::getPriceStatic(
447                (int) $row['id_product'],
448                true,
449                isset($row['id_product_attribute']) ? (int) $row['id_product_attribute'] : null,
450                _TB_PRICE_DATABASE_PRECISION_,
451                null,
452                false,
453                false,
454                $row['cart_quantity'],
455                false,
456                (int) $this->id_customer ? (int) $this->id_customer : null,
457                (int) $this->id,
458                $addressId,
459                $specificPriceOutput,
460                true,
461                true,
462                $cartShopContext
463            );
464
465            $row['price_with_reduction'] = Product::getPriceStatic(
466                (int) $row['id_product'],
467                true,
468                isset($row['id_product_attribute']) ? (int) $row['id_product_attribute'] : null,
469                _TB_PRICE_DATABASE_PRECISION_,
470                null,
471                false,
472                true,
473                $row['cart_quantity'],
474                false,
475                (int) $this->id_customer ? (int) $this->id_customer : null,
476                (int) $this->id,
477                $addressId,
478                $specificPriceOutput,
479                true,
480                true,
481                $cartShopContext
482            );
483
484            $row['price'] = $row['price_with_reduction_without_tax'] = Product::getPriceStatic(
485                (int) $row['id_product'],
486                false,
487                isset($row['id_product_attribute']) ? (int) $row['id_product_attribute'] : null,
488                _TB_PRICE_DATABASE_PRECISION_,
489                null,
490                false,
491                true,
492                $row['cart_quantity'],
493                false,
494                (int) $this->id_customer ? (int) $this->id_customer : null,
495                (int) $this->id,
496                $addressId,
497                $specificPriceOutput,
498                true,
499                true,
500                $cartShopContext
501            );
502
503            $row['total'] = $this->roundPrice(
504                $row['price_with_reduction_without_tax'],
505                $row['price_with_reduction'],
506                $row['cart_quantity'],
507                false
508            );
509            $row['total_wt'] = $this->roundPrice(
510                $row['price_with_reduction_without_tax'],
511                $row['price_with_reduction'],
512                $row['cart_quantity'],
513                true
514            );
515
516            // Recalculate prices after rounding, these go into an order.
517            $row['price'] = round(
518                $row['total'] / $row['cart_quantity'],
519                _TB_PRICE_DATABASE_PRECISION_
520            );
521            $row['price_wt'] = round(
522                $row['total_wt'] / $row['cart_quantity'],
523                _TB_PRICE_DATABASE_PRECISION_
524            );
525
526            $row['description_short'] = Tools::nl2br($row['description_short']);
527
528            // check if a image associated with the attribute exists
529            if ($row['id_product_attribute']) {
530                $row2 = Image::getBestImageAttribute($row['id_shop'], $this->id_lang, $row['id_product'], $row['id_product_attribute']);
531                if ($row2) {
532                    $row = array_merge($row, $row2);
533                }
534            }
535
536            $row['reduction_applies'] = ($specificPriceOutput && (float) $specificPriceOutput['reduction']);
537            $row['quantity_discount_applies'] = ($specificPriceOutput && $row['cart_quantity'] >= (int) $specificPriceOutput['from_quantity']);
538            $row['id_image'] = Product::defineProductImage($row, $this->id_lang);
539            $row['allow_oosp'] = Product::isAvailableWhenOutOfStock($row['out_of_stock']);
540            $row['features'] = Product::getFeaturesStatic((int) $row['id_product']);
541
542            if (array_key_exists($row['id_product_attribute'].'-'.$this->id_lang, static::$_attributesLists)) {
543                $row = array_merge($row, static::$_attributesLists[$row['id_product_attribute'].'-'.$this->id_lang]);
544            }
545
546            $row = Product::getTaxesInformations($row, $cartShopContext);
547
548            $this->_products[] = $row;
549        }
550
551        return $this->_products;
552    }
553
554    /**
555     * Round a quantity of a price for display. This is non-trivial, because
556     * thirty bees features multiple rounding strategies.
557     *
558     * @param float $priceWithoutTax  Single price of the product, without tax.
559     * @param float $priceWithTax     Single price of the product, with tax.
560     * @param int   $quantity         Quantity of the product.
561     * @param bool  $withTax          Whether the price with or without tax
562     *                                should get returned. Rounding gets
563     *                                applied to the displayed price, so
564     *                                rounding the other variant requires to
565     *                                recalculate taxes.
566     *
567     * return float Rounded and multiplied price.
568     *
569     * @since 1.1.0
570     */
571    protected function roundPrice($priceWithoutTax, $priceWithTax,
572                                  $quantity, $withTax)
573    {
574        static $displayPrecision = false;
575        if ($displayPrecision === false) {
576            $displayPrecision = 0;
577            if (Currency::getCurrencyInstance($this->id_currency)->decimals) {
578                $displayPrecision =
579                    Configuration::get('PS_PRICE_DISPLAY_PRECISION');
580            }
581        }
582
583        $roundType = (int) Configuration::get('PS_ROUND_TYPE');
584
585        $price = $priceWithoutTax;
586        if ($this->_taxCalculationMethod === PS_TAX_INC) {
587            $price = $priceWithTax;
588        }
589        $price = round($price, _TB_PRICE_DATABASE_PRECISION_);
590
591        switch ($roundType) {
592            case Order::ROUND_ITEM:
593                $price = Tools::ps_round($price, $displayPrecision);
594                // Intentionally fall through.
595            case Order::ROUND_LINE:
596            case Order::ROUND_TOTAL:
597                $total = $price * (int) $quantity;
598        }
599
600        // Add/remove taxes as appropriate. Ignore the obvious calculation
601        // precision limitation, please, it should be negligible.
602        if ($priceWithTax
603            && $this->_taxCalculationMethod === PS_TAX_INC
604            && ! $withTax) {
605            // Remove taxes.
606            $total = round(
607                $total / $priceWithTax * $priceWithoutTax,
608                _TB_PRICE_DATABASE_PRECISION_
609            );
610        } elseif ($priceWithoutTax
611                  && $this->_taxCalculationMethod === PS_TAX_EXC
612                  && $withTax) {
613            // Add taxes.
614            $total = round(
615                $total * $priceWithTax / $priceWithoutTax,
616                _TB_PRICE_DATABASE_PRECISION_
617            );
618        } // else nothing to change.
619
620        if ($roundType === Order::ROUND_LINE) {
621            $total = Tools::ps_round($total, $displayPrecision);
622        }
623
624        return $total;
625    }
626
627    /**
628     * @param array $ipaList
629     * @param int   $idLang
630     *
631     * @throws PrestaShopDatabaseException
632     * @throws PrestaShopException
633     * @since   1.0.0
634     * @version 1.0.0 Initial version
635     */
636    public static function cacheSomeAttributesLists($ipaList, $idLang)
637    {
638        if (!Combination::isFeatureActive()) {
639            return;
640        }
641
642        $paImplode = [];
643
644        foreach ($ipaList as $idProductAttribute) {
645            if ((int) $idProductAttribute && !array_key_exists($idProductAttribute.'-'.$idLang, static::$_attributesLists)) {
646                $paImplode[] = (int) $idProductAttribute;
647                static::$_attributesLists[(int) $idProductAttribute.'-'.$idLang] = ['attributes' => '', 'attributes_small' => ''];
648            }
649        }
650
651        if (!count($paImplode)) {
652            return;
653        }
654
655        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
656            (new DbQuery())
657                ->select('pac.`id_product_attribute`, agl.`public_name` AS `public_group_name`, al.`name` AS `attribute_name`')
658                ->from('product_attribute_combination', 'pac')
659                ->leftJoin('attribute', 'a', 'a.`id_attribute` = pac.`id_attribute`')
660                ->leftJoin('attribute_group', 'ag', 'ag.`id_attribute_group` = a.`id_attribute_group`')
661                ->leftJoin('attribute_lang', 'al', 'a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = '.(int) $idLang)
662                ->leftJoin('attribute_group_lang', 'agl', 'ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = '.(int) $idLang)
663                ->where('pac.`id_product_attribute` IN ('.implode(',', $paImplode).')')
664                ->orderBy('ag.`position` ASC, a.`position` ASC')
665        );
666
667        foreach ($result as $row) {
668            static::$_attributesLists[$row['id_product_attribute'].'-'.$idLang]['attributes'] .= $row['public_group_name'].' : '.$row['attribute_name'].', ';
669            static::$_attributesLists[$row['id_product_attribute'].'-'.$idLang]['attributes_small'] .= $row['attribute_name'].', ';
670        }
671
672        foreach ($paImplode as $idProductAttribute) {
673            static::$_attributesLists[$idProductAttribute.'-'.$idLang]['attributes'] = rtrim(
674                static::$_attributesLists[$idProductAttribute.'-'.$idLang]['attributes'],
675                ', '
676            );
677
678            static::$_attributesLists[$idProductAttribute.'-'.$idLang]['attributes_small'] = rtrim(
679                static::$_attributesLists[$idProductAttribute.'-'.$idLang]['attributes_small'],
680                ', '
681            );
682        }
683    }
684
685    /**
686     * @param int $idCart
687     *
688     * @return string
689     *
690     * @since   1.0.0
691     * @version 1.0.0 Initial version
692     */
693    public static function getOrderTotalUsingTaxCalculationMethod($idCart)
694    {
695        return static::getTotalCart($idCart, true);
696    }
697
698    /**
699     * @param int  $idCart
700     * @param bool $useTaxDisplay
701     * @param int  $type
702     *
703     * @return string
704     *
705     * @since   1.0.0
706     * @version 1.0.0 Initial version
707     */
708    public static function getTotalCart($idCart, $useTaxDisplay = false, $type = self::BOTH)
709    {
710        $cart = new Cart($idCart);
711        if (!Validate::isLoadedObject($cart)) {
712            die(Tools::displayError());
713        }
714
715        $withTaxes = $useTaxDisplay ? $cart->_taxCalculationMethod !== PS_TAX_EXC : true;
716
717        return Tools::displayPrice($cart->getOrderTotal($withTaxes, $type), Currency::getCurrencyInstance((int) $cart->id_currency), false);
718    }
719
720    /**
721     * This function returns the total cart amount
722     *
723     * Possible values for $type:
724     * static::ONLY_PRODUCTS
725     * static::ONLY_DISCOUNTS
726     * static::BOTH
727     * static::BOTH_WITHOUT_SHIPPING
728     * static::ONLY_SHIPPING
729     * static::ONLY_WRAPPING
730     * static::ONLY_PRODUCTS_WITHOUT_SHIPPING
731     * static::ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING
732     *
733     * @param bool       $withTaxes With or without taxes
734     * @param int        $type      Total type
735     * @param array|null $products
736     * @param int|null   $idCarrier
737     * @param bool       $useCache  Allow using cache of the method CartRule::getContextualValue
738     *
739     * @return float Order total
740     *
741     * @throws Adapter_Exception
742     * @throws PrestaShopDatabaseException
743     * @throws PrestaShopException
744     * @since   1.0.0
745     * @version 1.0.0 Initial version
746     */
747    public function getOrderTotal($withTaxes = true, $type = self::BOTH, $products = null, $idCarrier = null, $useCache = true)
748    {
749        static $displayPrecision = false;
750        if ($displayPrecision === false) {
751            $displayPrecision = 0;
752            if (Currency::getCurrencyInstance($this->id_currency)->decimals) {
753                $displayPrecision =
754                    Configuration::get('PS_PRICE_DISPLAY_PRECISION');
755            }
756        }
757
758        // Dependencies
759        /** @var Adapter_AddressFactory $addressFactory */
760        $addressFactory = Adapter_ServiceLocator::get('Adapter_AddressFactory');
761        /** @var Adapter_ProductPriceCalculator $priceCalculator */
762        $priceCalculator = Adapter_ServiceLocator::get('Adapter_ProductPriceCalculator');
763        /** @var Core_Business_ConfigurationInterface $configuration */
764        $configuration = Adapter_ServiceLocator::get('Core_Business_ConfigurationInterface');
765
766        $psTaxAddressType = $configuration->get('PS_TAX_ADDRESS_TYPE');
767        $psUseEcotax = $configuration->get('PS_USE_ECOTAX');
768
769        if (!$this->id) {
770            return 0;
771        }
772
773        $type = (int) $type;
774        $arrayType = [
775            static::ONLY_PRODUCTS,
776            static::ONLY_DISCOUNTS,
777            static::BOTH,
778            static::BOTH_WITHOUT_SHIPPING,
779            static::ONLY_SHIPPING,
780            static::ONLY_WRAPPING,
781            static::ONLY_PRODUCTS_WITHOUT_SHIPPING,
782            static::ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING,
783        ];
784
785        // Define virtual context to prevent case where the cart is not the in the global context
786        $virtualContext = Context::getContext()->cloneContext();
787        $virtualContext->cart = $this;
788
789        if (!in_array($type, $arrayType)) {
790            die(Tools::displayError());
791        }
792
793        $withShipping = in_array($type, [static::BOTH, static::ONLY_SHIPPING]);
794
795        // if cart rules are not used
796        if ($type == static::ONLY_DISCOUNTS && !CartRule::isFeatureActive()) {
797            return 0;
798        }
799
800        // no shipping cost if is a cart with only virtuals products
801        $virtual = $this->isVirtualCart();
802        if ($virtual && $type == static::ONLY_SHIPPING) {
803            return 0;
804        }
805
806        if ($virtual && $type == static::BOTH) {
807            $type = static::BOTH_WITHOUT_SHIPPING;
808        }
809
810        if ($withShipping || $type == static::ONLY_DISCOUNTS) {
811            if (is_null($products) && is_null($idCarrier)) {
812                $shippingFees = $this->getTotalShippingCost(null, (bool) $withTaxes);
813            } else {
814                $shippingFees = $this->getPackageShippingCost((int) $idCarrier, (bool) $withTaxes, null, $products);
815            }
816        } else {
817            $shippingFees = 0;
818        }
819
820        if ($type == static::ONLY_SHIPPING) {
821            return $shippingFees;
822        }
823
824        if ($type == static::ONLY_PRODUCTS_WITHOUT_SHIPPING) {
825            $type = static::ONLY_PRODUCTS;
826        }
827
828        $paramProduct = true;
829        if (is_null($products)) {
830            $paramProduct = false;
831            $products = $this->getProducts();
832        }
833
834        if ($type == static::ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING) {
835            foreach ($products as $key => $product) {
836                if ($product['is_virtual']) {
837                    unset($products[$key]);
838                }
839            }
840            $type = static::ONLY_PRODUCTS;
841        }
842
843        $orderTotal = 0;
844        if (Tax::excludeTaxeOption()) {
845            $withTaxes = false;
846        }
847
848        $productsTotal = [];
849
850        foreach ($products as $product) {
851            // products refer to the cart details
852            if ($virtualContext->shop->id != $product['id_shop']) {
853                $virtualContext->shop = new Shop((int) $product['id_shop']);
854            }
855
856            if ($psTaxAddressType == 'id_address_invoice') {
857                $idAddress = (int) $this->id_address_invoice;
858            } else {
859                $idAddress = (int) $product['id_address_delivery'];
860            } // Get delivery address of the product from the cart
861            if (!$addressFactory->addressExists($idAddress)) {
862                $idAddress = null;
863            }
864
865            // The $null variable below is not used,
866            // but it is necessary to pass it to getProductPrice because
867            // it expects a reference.
868            $null = null;
869            $priceWithoutTax = $priceCalculator->getProductPrice(
870                (int) $product['id_product'],
871                false,
872                (int) $product['id_product_attribute'],
873                _TB_PRICE_DATABASE_PRECISION_,
874                null,
875                false,
876                true,
877                $product['cart_quantity'],
878                false,
879                (int) $this->id_customer ? (int) $this->id_customer : null,
880                (int) $this->id,
881                $idAddress,
882                $null,
883                $psUseEcotax,
884                true,
885                $virtualContext
886            );
887            $priceWithTax = $priceCalculator->getProductPrice(
888                (int) $product['id_product'],
889                true,
890                (int) $product['id_product_attribute'],
891                _TB_PRICE_DATABASE_PRECISION_,
892                null,
893                false,
894                true,
895                $product['cart_quantity'],
896                false,
897                (int) $this->id_customer ? (int) $this->id_customer : null,
898                (int) $this->id,
899                $idAddress,
900                $null,
901                $psUseEcotax,
902                true,
903                $virtualContext
904            );
905
906            if ($withTaxes) {
907                $idTaxRulesGroup = Product::getIdTaxRulesGroupByIdProduct((int) $product['id_product'], $virtualContext);
908            } else {
909                $idTaxRulesGroup = 0;
910            }
911
912            $index = $idTaxRulesGroup;
913            if (Configuration::get('PS_ROUND_TYPE') == Order::ROUND_TOTAL) {
914                $index = $idTaxRulesGroup.'_'.$idAddress;
915            }
916            if ( ! isset($productsTotal[$index])) {
917                $productsTotal[$index] = 0;
918            }
919
920            $productsTotal[$index] += $this->roundPrice(
921                $priceWithoutTax,
922                $priceWithTax,
923                $product['cart_quantity'],
924                $withTaxes
925            );
926        }
927
928        foreach ($productsTotal as $key => $price) {
929            $orderTotal += $price;
930        }
931
932        $orderTotalProducts = $orderTotal;
933
934        if ($type == static::ONLY_DISCOUNTS) {
935            $orderTotal = 0;
936        }
937
938        // Wrapping Fees
939        $wrappingFees = 0;
940
941        // With PS_ATCP_SHIPWRAP on the gift wrapping cost computation calls getOrderTotal with $type === static::ONLY_PRODUCTS, so the flag below prevents an infinite recursion.
942        $includeGiftWrapping = (!$configuration->get('PS_ATCP_SHIPWRAP') || $type !== static::ONLY_PRODUCTS);
943
944        if ($this->gift && $includeGiftWrapping) {
945            $wrappingFees = Tools::ps_round(
946                Tools::convertPrice(
947                    $this->getGiftWrappingPrice($withTaxes),
948                    Currency::getCurrencyInstance((int) $this->id_currency)
949                ),
950                $displayPrecision
951            );
952        }
953        if ($type == static::ONLY_WRAPPING) {
954            return $wrappingFees;
955        }
956
957        $orderTotalDiscount = 0;
958        $orderShippingDiscount = 0;
959        if (!in_array($type, [static::ONLY_SHIPPING, static::ONLY_PRODUCTS]) && CartRule::isFeatureActive()) {
960            // First, retrieve the cart rules associated to this "getOrderTotal"
961            if ($withShipping || $type == static::ONLY_DISCOUNTS) {
962                $cartRules = $this->getCartRules(CartRule::FILTER_ACTION_ALL);
963            } else {
964                $cartRules = $this->getCartRules(CartRule::FILTER_ACTION_REDUCTION);
965                // Cart Rules array are merged manually in order to avoid doubles
966                foreach ($this->getCartRules(CartRule::FILTER_ACTION_GIFT) as $tmpCartRule) {
967                    $flag = false;
968                    foreach ($cartRules as $cartRule) {
969                        if ($tmpCartRule['id_cart_rule'] == $cartRule['id_cart_rule']) {
970                            $flag = true;
971                        }
972                    }
973                    if (!$flag) {
974                        $cartRules[] = $tmpCartRule;
975                    }
976                }
977            }
978
979            $idAddressDelivery = 0;
980            if (isset($products[0])) {
981                $idAddressDelivery = (is_null($products) ? $this->id_address_delivery : $products[0]['id_address_delivery']);
982            }
983            $package = ['id_carrier' => $idCarrier, 'id_address' => $idAddressDelivery, 'products' => $products];
984
985            // Then, calculate the contextual value for each one
986            $flag = false;
987            foreach ($cartRules as $cartRule) {
988                /** @var CartRule $cartRuleObject */
989                $cartRuleObject = $cartRule['obj'];
990                // If the cart rule offers free shipping, add the shipping cost
991                if (($withShipping || $type == static::ONLY_DISCOUNTS) && $cartRuleObject->free_shipping && !$flag) {
992                    $orderShippingDiscount = (float) $cartRuleObject->getContextualValue($withTaxes, $virtualContext, CartRule::FILTER_ACTION_SHIPPING, ($paramProduct ? $package : null), $useCache);
993                    $flag = true;
994                }
995
996                // If the cart rule is a free gift, then add the free gift value only if the gift is in this package
997                if ((int) $cartRuleObject->gift_product) {
998                    $inOrder = false;
999                    if (is_null($products)) {
1000                        $inOrder = true;
1001                    } else {
1002                        foreach ($products as $product) {
1003                            if ($cartRuleObject->gift_product == $product['id_product'] && $cartRuleObject->gift_product_attribute == $product['id_product_attribute']) {
1004                                $inOrder = true;
1005                            }
1006                        }
1007                    }
1008
1009                    if ($inOrder) {
1010                        $orderTotalDiscount += $cartRuleObject->getContextualValue($withTaxes, $virtualContext, CartRule::FILTER_ACTION_GIFT, $package, $useCache);
1011                    }
1012                }
1013
1014                // If the cart rule offers a reduction, the amount is prorated (with the products in the package)
1015                if ($cartRuleObject->reduction_percent > 0 || $cartRuleObject->reduction_amount > 0) {
1016                    $orderTotalDiscount += $cartRuleObject->getContextualValue($withTaxes, $virtualContext, CartRule::FILTER_ACTION_REDUCTION, $package, $useCache);
1017                }
1018            }
1019            $orderTotalDiscount = min($orderTotalDiscount, (float) $orderTotalProducts) + (float) $orderShippingDiscount;
1020            $orderTotal -= $orderTotalDiscount;
1021        }
1022
1023        if ($type == static::BOTH) {
1024            $orderTotal += $shippingFees + $wrappingFees;
1025        }
1026
1027        if ($orderTotal < 0 && $type != static::ONLY_DISCOUNTS) {
1028            return 0;
1029        }
1030
1031        if ($type == static::ONLY_DISCOUNTS) {
1032            return $orderTotalDiscount;
1033        }
1034
1035        return Tools::ps_round((float) $orderTotal, $displayPrecision);
1036    }
1037
1038    /**
1039     * Check if cart contains only virtual products
1040     *
1041     * @return bool true if is a virtual cart or false
1042     *
1043     * @since   1.0.0
1044     * @version 1.0.0 Initial version
1045     * @throws PrestaShopException
1046     */
1047    public function isVirtualCart()
1048    {
1049        if (!ProductDownload::isFeatureActive()) {
1050            return false;
1051        }
1052
1053        if (!isset(static::$_isVirtualCart[$this->id])) {
1054            $products = $this->getProducts();
1055            if (!count($products)) {
1056                return false;
1057            }
1058
1059            $isVirtual = 1;
1060            foreach ($products as $product) {
1061                if (empty($product['is_virtual'])) {
1062                    $isVirtual = 0;
1063                }
1064            }
1065            static::$_isVirtualCart[$this->id] = (int) $isVirtual;
1066        }
1067
1068        return static::$_isVirtualCart[$this->id];
1069    }
1070
1071    /**
1072     * Return shipping total for the cart
1073     *
1074     * @param array|null   $deliveryOption Array of the delivery option for each address
1075     * @param bool         $useTax
1076     * @param Country|null $defaultCountry
1077     *
1078     * @return float Shipping total
1079     *
1080     * @throws Adapter_Exception
1081     * @throws PrestaShopDatabaseException
1082     * @throws PrestaShopException
1083     * @since   1.0.0
1084     * @version 1.0.0 Initial version
1085     */
1086    public function getTotalShippingCost($deliveryOption = null, $useTax = true, Country $defaultCountry = null)
1087    {
1088        if (isset(Context::getContext()->cookie->id_country)) {
1089            $defaultCountry = new Country(Context::getContext()->cookie->id_country);
1090        }
1091        if (is_null($deliveryOption)) {
1092            $deliveryOption = $this->getDeliveryOption($defaultCountry, false, false);
1093        }
1094
1095        $totalShipping = 0;
1096        $deliveryOptionList = $this->getDeliveryOptionList($defaultCountry);
1097        foreach ($deliveryOption as $idAddress => $key) {
1098            if (!isset($deliveryOptionList[$idAddress]) || !isset($deliveryOptionList[$idAddress][$key])) {
1099                continue;
1100            }
1101            if ($useTax) {
1102                $totalShipping += $deliveryOptionList[$idAddress][$key]['total_price_with_tax'];
1103            } else {
1104                $totalShipping += $deliveryOptionList[$idAddress][$key]['total_price_without_tax'];
1105            }
1106        }
1107
1108        return $totalShipping;
1109    }
1110
1111    /**
1112     * Get the delivery option selected, or if no delivery option was selected,
1113     * the cheapest option for each address
1114     *
1115     * @param Country|null $defaultCountry
1116     * @param bool         $dontAutoSelectOptions
1117     * @param bool         $useCache
1118     *
1119     * @return array|bool|mixed Delivery option
1120     *
1121     * @throws PrestaShopDatabaseException
1122     * @throws PrestaShopException
1123     * @since   1.0.0
1124     * @version 1.0.0 Initial version
1125     * @throws Adapter_Exception
1126     */
1127    public function getDeliveryOption($defaultCountry = null, $dontAutoSelectOptions = false, $useCache = true)
1128    {
1129        static $cache = [];
1130        $cacheId = (int) (is_object($defaultCountry) ? $defaultCountry->id : 0).'-'.(int) $dontAutoSelectOptions;
1131        if (isset($cache[$cacheId]) && $useCache) {
1132            return $cache[$cacheId];
1133        }
1134
1135        $deliveryOptionList = $this->getDeliveryOptionList($defaultCountry);
1136
1137        // The delivery option was selected
1138        if (isset($this->delivery_option) && $this->delivery_option != '') {
1139            $deliveryOption = json_decode($this->delivery_option, true);
1140            $validated = true;
1141            if (is_array($deliveryOption)) {
1142                foreach ($deliveryOption as $idAddress => $key) {
1143                    if (!isset($deliveryOptionList[$idAddress][$key])) {
1144                        $validated = false;
1145                        break;
1146                    }
1147                }
1148
1149                if ($validated) {
1150                    $cache[$cacheId] = $deliveryOption;
1151
1152                    return $deliveryOption;
1153                }
1154            }
1155        }
1156
1157        if ($dontAutoSelectOptions) {
1158            return false;
1159        }
1160
1161        // No delivery option selected or delivery option selected is not valid, get the better for all options
1162        $deliveryOption = [];
1163        foreach ($deliveryOptionList as $idAddress => $options) {
1164            foreach ($options as $key => $option) {
1165                if (Configuration::get('PS_CARRIER_DEFAULT') == -1 && $option['is_best_price']) {
1166                    $deliveryOption[$idAddress] = $key;
1167                    break;
1168                } elseif (Configuration::get('PS_CARRIER_DEFAULT') == -2 && $option['is_best_grade']) {
1169                    $deliveryOption[$idAddress] = $key;
1170                    break;
1171                } elseif ($option['unique_carrier'] && in_array(Configuration::get('PS_CARRIER_DEFAULT'), array_keys($option['carrier_list']))) {
1172                    $deliveryOption[$idAddress] = $key;
1173                    break;
1174                }
1175            }
1176
1177            reset($options);
1178            if (!isset($deliveryOption[$idAddress])) {
1179                $deliveryOption[$idAddress] = key($options);
1180            }
1181        }
1182
1183        $cache[$cacheId] = $deliveryOption;
1184
1185        return $deliveryOption;
1186    }
1187
1188    /**
1189     * Set the delivery option and id_carrier, if there is only one carrier
1190     *
1191     * @param array ?null $deliveryOption
1192     *
1193     * @throws Adapter_Exception
1194     * @throws PrestaShopDatabaseException
1195     * @throws PrestaShopException
1196     * @since   1.0.0
1197     * @version 1.0.0 Initial version
1198     */
1199    public function setDeliveryOption($deliveryOption = null)
1200    {
1201        if (empty($deliveryOption) || count($deliveryOption) == 0) {
1202            $this->delivery_option = '';
1203            $this->id_carrier = 0;
1204
1205            return;
1206        }
1207        Cache::clean('getContextualValue_*');
1208        $deliveryOptionList = $this->getDeliveryOptionList(null, true);
1209
1210        foreach ($deliveryOptionList as $idAddress => $options) {
1211            if (!isset($deliveryOption[$idAddress])) {
1212                foreach ($options as $key => $option) {
1213                    if ($option['is_best_price']) {
1214                        $deliveryOption[$idAddress] = $key;
1215                        break;
1216                    }
1217                }
1218            }
1219        }
1220
1221        if (count($deliveryOption) == 1) {
1222            $this->id_carrier = $this->getIdCarrierFromDeliveryOption($deliveryOption);
1223        }
1224
1225        $this->delivery_option = json_encode($deliveryOption);
1226    }
1227
1228    /**
1229     * Get all deliveries options available for the current cart
1230     *
1231     * @param Country $defaultCountry
1232     * @param bool    $flush Force flushing cache
1233     *
1234     * @return array array(
1235     *                   0 => array( // First address
1236     *                       '12,' => array(  // First delivery option available for this address
1237     *                           carrier_list => array(
1238     *                               12 => array( // First carrier for this option
1239     *                                   'instance' => Carrier Object,
1240     *                                   'logo' => <url to the carriers logo>,
1241     *                                   'price_with_tax' => 12.4,
1242     *                                   'price_without_tax' => 12.4,
1243     *                                   'package_list' => array(
1244     *                                       1,
1245     *                                       3,
1246     *                                   ),
1247     *                               ),
1248     *                           ),
1249     *                           is_best_grade => true, // Does this option have the biggest grade (quick shipping) for this shipping address
1250     *                           is_best_price => true, // Does this option have the lower price for this shipping address
1251     *                           unique_carrier => true, // Does this option use a unique carrier
1252     *                           total_price_with_tax => 12.5,
1253     *                           total_price_without_tax => 12.5,
1254     *                           position => 5, // Average of the carrier position
1255     *                       ),
1256     *                   ),
1257     *               );
1258     *               If there are no carriers available for an address, return an empty  array
1259     * @throws Adapter_Exception
1260     * @throws PrestaShopDatabaseException
1261     * @throws PrestaShopException
1262     */
1263    public function getDeliveryOptionList(Country $defaultCountry = null, $flush = false)
1264    {
1265        static $cache = [];
1266        if (isset($cache[$this->id]) && !$flush) {
1267            return $cache[$this->id];
1268        }
1269
1270        $deliveryOptionList = [];
1271        $carriersPrice = [];
1272        $carrierCollection = [];
1273        $packageList = $this->getPackageList($flush);
1274
1275        // Foreach addresses
1276        foreach ($packageList as $idAddress => $packages) {
1277            // Initialize vars
1278            $deliveryOptionList[$idAddress] = [];
1279            $carriersPrice[$idAddress] = [];
1280            $commonCarriers = null;
1281            $bestPriceCarriers = [];
1282            $bestGradeCarriers = [];
1283            $carriersInstance = [];
1284
1285            // Get country
1286            if ($idAddress) {
1287                $address = new Address($idAddress);
1288                $country = new Country($address->id_country);
1289            } else {
1290                $country = $defaultCountry;
1291            }
1292
1293            // Foreach packages, get the carriers with best price, best position and best grade
1294            foreach ($packages as $idPackage => $package) {
1295                // No carriers available
1296                if (count($packages) == 1 && count($package['carrier_list']) == 1 && current($package['carrier_list']) == 0) {
1297                    $cache[$this->id] = [];
1298
1299                    return $cache[$this->id];
1300                }
1301
1302                $carriersPrice[$idAddress][$idPackage] = [];
1303
1304                // Get all common carriers for each packages to the same address
1305                if (is_null($commonCarriers)) {
1306                    $commonCarriers = $package['carrier_list'];
1307                } else {
1308                    $commonCarriers = array_intersect($commonCarriers, $package['carrier_list']);
1309                }
1310
1311                $bestPrice = null;
1312                $bestPriceCarrier = null;
1313                $bestGrade = null;
1314                $bestGradeCarrier = null;
1315
1316                // Foreach carriers of the package, calculate his price, check if it the best price, position and grade
1317                foreach ($package['carrier_list'] as $idCarrier) {
1318                    if (!isset($carriersInstance[$idCarrier])) {
1319                        $carriersInstance[$idCarrier] = new Carrier($idCarrier);
1320                    }
1321
1322                    $priceWithTax = $this->getPackageShippingCost((int) $idCarrier, true, $country, $package['product_list']);
1323                    $priceWithoutTax = $this->getPackageShippingCost((int) $idCarrier, false, $country, $package['product_list']);
1324                    if (is_null($bestPrice) || $priceWithTax < $bestPrice) {
1325                        $bestPrice = $priceWithTax;
1326                        $bestPriceCarrier = $idCarrier;
1327                    }
1328                    $carriersPrice[$idAddress][$idPackage][$idCarrier] = [
1329                        'without_tax' => $priceWithoutTax,
1330                        'with_tax'    => $priceWithTax,
1331                    ];
1332
1333                    $grade = $carriersInstance[$idCarrier]->grade;
1334                    if (is_null($bestGrade) || $grade > $bestGrade) {
1335                        $bestGrade = $grade;
1336                        $bestGradeCarrier = $idCarrier;
1337                    }
1338                }
1339
1340                $bestPriceCarriers[$idPackage] = $bestPriceCarrier;
1341                $bestGradeCarriers[$idPackage] = $bestGradeCarrier;
1342            }
1343
1344            // Reset $best_price_carrier, it's now an array
1345            $bestPriceCarrier = [];
1346            $key = '';
1347
1348            // Get the delivery option with the lower price
1349            foreach ($bestPriceCarriers as $idPackage => $idCarrier) {
1350                $key .= $idCarrier.',';
1351                if (!isset($bestPriceCarrier[$idCarrier])) {
1352                    $bestPriceCarrier[$idCarrier] = [
1353                        'price_with_tax'    => 0,
1354                        'price_without_tax' => 0,
1355                        'package_list'      => [],
1356                        'product_list'      => [],
1357                    ];
1358                }
1359                $bestPriceCarrier[$idCarrier]['price_with_tax'] += $carriersPrice[$idAddress][$idPackage][$idCarrier]['with_tax'];
1360                $bestPriceCarrier[$idCarrier]['price_without_tax'] += $carriersPrice[$idAddress][$idPackage][$idCarrier]['without_tax'];
1361                $bestPriceCarrier[$idCarrier]['package_list'][] = $idPackage;
1362                $bestPriceCarrier[$idCarrier]['product_list'] = array_merge($bestPriceCarrier[$idCarrier]['product_list'], $packages[$idPackage]['product_list']);
1363                $bestPriceCarrier[$idCarrier]['instance'] = $carriersInstance[$idCarrier];
1364                $realBestPrice = !isset($realBestPrice) || $realBestPrice > $carriersPrice[$idAddress][$idPackage][$idCarrier]['with_tax'] ?
1365                    $carriersPrice[$idAddress][$idPackage][$idCarrier]['with_tax'] : $realBestPrice;
1366                $realBestPriceWt = !isset($realBestPriceWt) || $realBestPriceWt > $carriersPrice[$idAddress][$idPackage][$idCarrier]['without_tax'] ?
1367                    $carriersPrice[$idAddress][$idPackage][$idCarrier]['without_tax'] : $realBestPriceWt;
1368            }
1369
1370            // Add the delivery option with best price as best price
1371            $deliveryOptionList[$idAddress][$key] = [
1372                'carrier_list'   => $bestPriceCarrier,
1373                'is_best_price'  => true,
1374                'is_best_grade'  => false,
1375                'unique_carrier' => (count($bestPriceCarrier) <= 1),
1376            ];
1377
1378            // Reset $best_grade_carrier, it's now an array
1379            $bestGradeCarrier = [];
1380            $key = '';
1381
1382            // Get the delivery option with the best grade
1383            foreach ($bestGradeCarriers as $idPackage => $idCarrier) {
1384                $key .= $idCarrier.',';
1385                if (!isset($bestGradeCarrier[$idCarrier])) {
1386                    $bestGradeCarrier[$idCarrier] = [
1387                        'price_with_tax'    => 0,
1388                        'price_without_tax' => 0,
1389                        'package_list'      => [],
1390                        'product_list'      => [],
1391                    ];
1392                }
1393                $bestGradeCarrier[$idCarrier]['price_with_tax'] += $carriersPrice[$idAddress][$idPackage][$idCarrier]['with_tax'];
1394                $bestGradeCarrier[$idCarrier]['price_without_tax'] += $carriersPrice[$idAddress][$idPackage][$idCarrier]['without_tax'];
1395                $bestGradeCarrier[$idCarrier]['package_list'][] = $idPackage;
1396                $bestGradeCarrier[$idCarrier]['product_list'] = array_merge($bestGradeCarrier[$idCarrier]['product_list'], $packages[$idPackage]['product_list']);
1397                $bestGradeCarrier[$idCarrier]['instance'] = $carriersInstance[$idCarrier];
1398            }
1399
1400            // Add the delivery option with best grade as best grade
1401            if (!isset($deliveryOptionList[$idAddress][$key])) {
1402                $deliveryOptionList[$idAddress][$key] = [
1403                    'carrier_list'   => $bestGradeCarrier,
1404                    'is_best_price'  => false,
1405                    'unique_carrier' => (count($bestGradeCarrier) <= 1),
1406                ];
1407            }
1408            $deliveryOptionList[$idAddress][$key]['is_best_grade'] = true;
1409
1410            // Get all delivery options with a unique carrier
1411            foreach ($commonCarriers as $idCarrier) {
1412                $key = '';
1413                $packageList = [];
1414                $productList = [];
1415                $priceWithTax = 0;
1416                $priceWithoutTax = 0;
1417
1418                foreach ($packages as $idPackage => $package) {
1419                    $key .= $idCarrier.',';
1420                    $priceWithTax += $carriersPrice[$idAddress][$idPackage][$idCarrier]['with_tax'];
1421                    $priceWithoutTax += $carriersPrice[$idAddress][$idPackage][$idCarrier]['without_tax'];
1422                    $packageList[] = $idPackage;
1423                    $productList = array_merge($productList, $package['product_list']);
1424                }
1425
1426                if (!isset($deliveryOptionList[$idAddress][$key])) {
1427                    $deliveryOptionList[$idAddress][$key] = [
1428                        'is_best_price'  => false,
1429                        'is_best_grade'  => false,
1430                        'unique_carrier' => true,
1431                        'carrier_list'   => [
1432                            $idCarrier => [
1433                                'price_with_tax'    => $priceWithTax,
1434                                'price_without_tax' => $priceWithoutTax,
1435                                'instance'          => $carriersInstance[$idCarrier],
1436                                'package_list'      => $packageList,
1437                                'product_list'      => $productList,
1438                            ],
1439                        ],
1440                    ];
1441                } else {
1442                    $deliveryOptionList[$idAddress][$key]['unique_carrier'] = (count($deliveryOptionList[$idAddress][$key]['carrier_list']) <= 1);
1443                }
1444            }
1445        }
1446
1447        $cartRules = CartRule::getCustomerCartRules(Context::getContext()->cookie->id_lang, Context::getContext()->cookie->id_customer, true, true, false, $this, true);
1448
1449        $result = false;
1450        if ($this->id) {
1451            $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
1452                (new DbQuery())
1453                ->select('*')
1454                ->from('cart_cart_rule')
1455                ->where('`id_cart` = '.(int) $this->id)
1456            );
1457        }
1458
1459        $cartRulesInCart = [];
1460
1461        if (is_array($result)) {
1462            foreach ($result as $row) {
1463                $cartRulesInCart[] = $row['id_cart_rule'];
1464            }
1465        }
1466
1467        $totalProductsTaxIncluded = $this->getOrderTotal(true, static::ONLY_PRODUCTS);
1468        $totalProducts = $this->getOrderTotal(false, static::ONLY_PRODUCTS);
1469
1470        $freeCarriersRules = [];
1471
1472        $context = Context::getContext();
1473        foreach ($cartRules as $cartRule) {
1474            $totalPrice = $cartRule['minimum_amount_tax'] ? $totalProductsTaxIncluded : $totalProducts;
1475            $totalPrice += (isset($realBestPrice) && $cartRule['minimum_amount_tax'] && $cartRule['minimum_amount_shipping']) ? $realBestPrice : 0;
1476            $totalPrice += (isset($realBestPriceWt) && !$cartRule['minimum_amount_tax'] && $cartRule['minimum_amount_shipping']) ? $realBestPriceWt : 0;
1477            $condition = ($cartRule['free_shipping'] && $cartRule['carrier_restriction'] && $cartRule['minimum_amount'] <= $totalPrice) ? 1 : 0;
1478            if (isset($cartRule['code']) && !empty($cartRule['code'])) {
1479                $condition = ($cartRule['free_shipping'] && $cartRule['carrier_restriction'] && in_array($cartRule['id_cart_rule'], $cartRulesInCart)
1480                    && $cartRule['minimum_amount'] <= $totalPrice) ? 1 : 0;
1481            }
1482            if ($condition) {
1483                $cr = new CartRule((int) $cartRule['id_cart_rule']);
1484                if (Validate::isLoadedObject($cr) &&
1485                    $cr->checkValidity($context, in_array((int) $cartRule['id_cart_rule'], $cartRulesInCart), false, false)
1486                ) {
1487                    $carriers = $cr->getAssociatedRestrictions('carrier', true, false);
1488                    if (is_array($carriers) && count($carriers) && isset($carriers['selected'])) {
1489                        foreach ($carriers['selected'] as $carrier) {
1490                            if (isset($carrier['id_carrier']) && $carrier['id_carrier']) {
1491                                $freeCarriersRules[] = (int) $carrier['id_carrier'];
1492                            }
1493                        }
1494                    }
1495                }
1496            }
1497        }
1498
1499        // For each delivery options :
1500        //    - Set the carrier list
1501        //    - Calculate the price
1502        //    - Calculate the average position
1503        foreach ($deliveryOptionList as $idAddress => $deliveryOption) {
1504            foreach ($deliveryOption as $key => $value) {
1505                $totalPriceWithTax = 0;
1506                $totalPriceWithoutTax = 0;
1507                $position = 0;
1508                foreach ($value['carrier_list'] as $idCarrier => $data) {
1509                    $totalPriceWithTax += $data['price_with_tax'];
1510                    $totalPriceWithoutTax += $data['price_without_tax'];
1511                    $totalPriceWithoutTaxWithRules = (in_array($idCarrier, $freeCarriersRules)) ? 0 : $totalPriceWithoutTax;
1512
1513                    if (!isset($carrierCollection[$idCarrier])) {
1514                        $carrierCollection[$idCarrier] = new Carrier($idCarrier);
1515                    }
1516                    $deliveryOptionList[$idAddress][$key]['carrier_list'][$idCarrier]['instance'] = $carrierCollection[$idCarrier];
1517
1518                    if (file_exists(_PS_SHIP_IMG_DIR_.$idCarrier.'.jpg')) {
1519                        $deliveryOptionList[$idAddress][$key]['carrier_list'][$idCarrier]['logo'] = _THEME_SHIP_DIR_.$idCarrier.'.jpg';
1520                    } else {
1521                        $deliveryOptionList[$idAddress][$key]['carrier_list'][$idCarrier]['logo'] = false;
1522                    }
1523
1524                    $position += $carrierCollection[$idCarrier]->position;
1525                }
1526                if (!isset($totalPriceWithoutTaxWithRules)) {
1527                    $totalPriceWithoutTaxWithRules = false;
1528                }
1529                $deliveryOptionList[$idAddress][$key]['total_price_with_tax'] = $totalPriceWithTax;
1530                $deliveryOptionList[$idAddress][$key]['total_price_without_tax'] = $totalPriceWithoutTax;
1531                $deliveryOptionList[$idAddress][$key]['is_free'] = !$totalPriceWithoutTaxWithRules ? true : false;
1532                $deliveryOptionList[$idAddress][$key]['position'] = $position / count($value['carrier_list']);
1533            }
1534        }
1535
1536        // Sort delivery option list
1537        foreach ($deliveryOptionList as &$array) {
1538            uasort($array, ['Cart', 'sortDeliveryOptionList']);
1539        }
1540
1541        $cache[$this->id] = $deliveryOptionList;
1542
1543        return $cache[$this->id];
1544    }
1545
1546    /**
1547     * Get products grouped by package and by addresses to be sent individualy (one package = one shipping cost).
1548     *
1549     * @param bool $flush
1550     *
1551     * @return array array(
1552     *                   0 => array( // First address
1553     *                       0 => array(  // First package
1554     *                           'product_list' => array(...),
1555     *                           'carrier_list' => array(...),
1556     *                           'id_warehouse' => array(...),
1557     *                       ),
1558     *                   ),
1559     *               );
1560     * @throws PrestaShopDatabaseException
1561     * @throws PrestaShopException
1562     * @todo Add availability check
1563     */
1564    public function getPackageList($flush = false)
1565    {
1566        static $cache = [];
1567        $cacheKey = (int) $this->id.'_'.(int) $this->id_address_delivery;
1568        if (isset($cache[$cacheKey]) && $cache[$cacheKey] !== false && !$flush) {
1569            return $cache[$cacheKey];
1570        }
1571
1572        $productList = $this->getProducts($flush);
1573        // Step 1 : Get product informations (warehouse_list and carrier_list), count warehouse
1574        // Determine the best warehouse to determine the packages
1575        // For that we count the number of time we can use a warehouse for a specific delivery address
1576        $warehouseCountByAddress = [];
1577
1578        $stockManagementActive = Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT');
1579
1580        foreach ($productList as &$product) {
1581            if ((int) $product['id_address_delivery'] == 0) {
1582                $product['id_address_delivery'] = (int) $this->id_address_delivery;
1583            }
1584
1585            if (!isset($warehouseCountByAddress[$product['id_address_delivery']])) {
1586                $warehouseCountByAddress[$product['id_address_delivery']] = [];
1587            }
1588
1589            $product['warehouse_list'] = [];
1590
1591            if ($stockManagementActive &&
1592                (int) $product['advanced_stock_management'] == 1
1593            ) {
1594                $warehouseList = Warehouse::getProductWarehouseList($product['id_product'], $product['id_product_attribute'], $this->id_shop);
1595                if (count($warehouseList) == 0) {
1596                    $warehouseList = Warehouse::getProductWarehouseList($product['id_product'], $product['id_product_attribute']);
1597                }
1598                // Does the product is in stock ?
1599                // If yes, get only warehouse where the product is in stock
1600
1601                $warehouseInStock = [];
1602                $manager = StockManagerFactory::getManager();
1603
1604                foreach ($warehouseList as $key => $warehouse) {
1605                    $productRealQuantities = $manager->getProductRealQuantities(
1606                        $product['id_product'],
1607                        $product['id_product_attribute'],
1608                        [$warehouse['id_warehouse']],
1609                        true
1610                    );
1611
1612                    if ($productRealQuantities > 0 || Pack::isPack((int) $product['id_product'])) {
1613                        $warehouseInStock[] = $warehouse;
1614                    }
1615                }
1616
1617                if (!empty($warehouseInStock)) {
1618                    $warehouseList = $warehouseInStock;
1619                    $product['in_stock'] = true;
1620                } else {
1621                    $product['in_stock'] = false;
1622                }
1623            } else {
1624                //simulate default warehouse
1625                $warehouseList = [0 => ['id_warehouse' => 0]];
1626                $product['in_stock'] = StockAvailable::getQuantityAvailableByProduct($product['id_product'], $product['id_product_attribute']) > 0;
1627            }
1628
1629            foreach ($warehouseList as $warehouse) {
1630                $product['warehouse_list'][$warehouse['id_warehouse']] = $warehouse['id_warehouse'];
1631                if (!isset($warehouseCountByAddress[$product['id_address_delivery']][$warehouse['id_warehouse']])) {
1632                    $warehouseCountByAddress[$product['id_address_delivery']][$warehouse['id_warehouse']] = 0;
1633                }
1634
1635                $warehouseCountByAddress[$product['id_address_delivery']][$warehouse['id_warehouse']]++;
1636            }
1637        }
1638        unset($product);
1639
1640        arsort($warehouseCountByAddress);
1641
1642        // Step 2 : Group product by warehouse
1643        $groupedByWarehouse = [];
1644
1645        foreach ($productList as &$product) {
1646            if (!isset($groupedByWarehouse[$product['id_address_delivery']])) {
1647                $groupedByWarehouse[$product['id_address_delivery']] = [
1648                    'in_stock'     => [],
1649                    'out_of_stock' => [],
1650                ];
1651            }
1652
1653            $product['carrier_list'] = [];
1654            $idWarehouse = 0;
1655            foreach ($warehouseCountByAddress[$product['id_address_delivery']] as $idWar => $val) {
1656                if (array_key_exists((int) $idWar, $product['warehouse_list'])) {
1657                    $product['carrier_list'] = array_replace($product['carrier_list'], Carrier::getAvailableCarrierList(new Product($product['id_product']), $idWar, $product['id_address_delivery'], null, $this));
1658                    if (!$idWarehouse) {
1659                        $idWarehouse = (int) $idWar;
1660                    }
1661                }
1662            }
1663
1664            if (!isset($groupedByWarehouse[$product['id_address_delivery']]['in_stock'][$idWarehouse])) {
1665                $groupedByWarehouse[$product['id_address_delivery']]['in_stock'][$idWarehouse] = [];
1666                $groupedByWarehouse[$product['id_address_delivery']]['out_of_stock'][$idWarehouse] = [];
1667            }
1668
1669            if (!$this->allow_seperated_package) {
1670                $key = 'in_stock';
1671            } else {
1672                $key = $product['in_stock'] ? 'in_stock' : 'out_of_stock';
1673                $productQuantityInStock = StockAvailable::getQuantityAvailableByProduct($product['id_product'], $product['id_product_attribute']);
1674                if ($product['in_stock'] && $product['cart_quantity'] > $productQuantityInStock) {
1675                    $outStockPart = $product['cart_quantity'] - $productQuantityInStock;
1676                    $productBis = $product;
1677                    $productBis['cart_quantity'] = $outStockPart;
1678                    $productBis['in_stock'] = 0;
1679                    $product['cart_quantity'] -= $outStockPart;
1680                    $groupedByWarehouse[$product['id_address_delivery']]['out_of_stock'][$idWarehouse][] = $productBis;
1681                }
1682            }
1683
1684            if (empty($product['carrier_list'])) {
1685                $product['carrier_list'] = [0 => 0];
1686            }
1687
1688            $groupedByWarehouse[$product['id_address_delivery']][$key][$idWarehouse][] = $product;
1689        }
1690        unset($product);
1691
1692        // Step 3 : grouped product from grouped_by_warehouse by available carriers
1693        $groupedByCarriers = [];
1694        foreach ($groupedByWarehouse as $idAddressDelivery => $productsInStockList) {
1695            if (!isset($groupedByCarriers[$idAddressDelivery])) {
1696                $groupedByCarriers[$idAddressDelivery] = [
1697                    'in_stock'     => [],
1698                    'out_of_stock' => [],
1699                ];
1700            }
1701            foreach ($productsInStockList as $key => $warehouseList) {
1702                if (!isset($groupedByCarriers[$idAddressDelivery][$key])) {
1703                    $groupedByCarriers[$idAddressDelivery][$key] = [];
1704                }
1705                foreach ($warehouseList as $idWarehouse => $productList) {
1706                    if (!isset($groupedByCarriers[$idAddressDelivery][$key][$idWarehouse])) {
1707                        $groupedByCarriers[$idAddressDelivery][$key][$idWarehouse] = [];
1708                    }
1709                    foreach ($productList as $product) {
1710                        $packageCarriersKey = implode(',', $product['carrier_list']);
1711
1712                        if (!isset($groupedByCarriers[$idAddressDelivery][$key][$idWarehouse][$packageCarriersKey])) {
1713                            $groupedByCarriers[$idAddressDelivery][$key][$idWarehouse][$packageCarriersKey] = [
1714                                'product_list'   => [],
1715                                'carrier_list'   => $product['carrier_list'],
1716                                'warehouse_list' => $product['warehouse_list'],
1717                            ];
1718                        }
1719
1720                        $groupedByCarriers[$idAddressDelivery][$key][$idWarehouse][$packageCarriersKey]['product_list'][] = $product;
1721                    }
1722                }
1723            }
1724        }
1725
1726        $packageList = [];
1727        // Step 4 : merge product from grouped_by_carriers into $package to minimize the number of package
1728        foreach ($groupedByCarriers as $idAddressDelivery => $productsInStockList) {
1729            if (!isset($packageList[$idAddressDelivery])) {
1730                $packageList[$idAddressDelivery] = [
1731                    'in_stock'     => [],
1732                    'out_of_stock' => [],
1733                ];
1734            }
1735
1736            foreach ($productsInStockList as $key => $warehouseList) {
1737                if (!isset($packageList[$idAddressDelivery][$key])) {
1738                    $packageList[$idAddressDelivery][$key] = [];
1739                }
1740                // Count occurance of each carriers to minimize the number of packages
1741                $carrierCount = [];
1742                foreach ($warehouseList as $idWarehouse => $productsGroupedByCarriers) {
1743                    foreach ($productsGroupedByCarriers as $data) {
1744                        foreach ($data['carrier_list'] as $idCarrier) {
1745                            if (!isset($carrierCount[$idCarrier])) {
1746                                $carrierCount[$idCarrier] = 0;
1747                            }
1748                            $carrierCount[$idCarrier]++;
1749                        }
1750                    }
1751                }
1752                arsort($carrierCount);
1753                foreach ($warehouseList as $idWarehouse => $productsGroupedByCarriers) {
1754                    if (!isset($packageList[$idAddressDelivery][$key][$idWarehouse])) {
1755                        $packageList[$idAddressDelivery][$key][$idWarehouse] = [];
1756                    }
1757                    foreach ($productsGroupedByCarriers as $data) {
1758                        foreach ($carrierCount as $idCarrier => $rate) {
1759                            if (array_key_exists($idCarrier, $data['carrier_list'])) {
1760                                if (!isset($packageList[$idAddressDelivery][$key][$idWarehouse][$idCarrier])) {
1761                                    $packageList[$idAddressDelivery][$key][$idWarehouse][$idCarrier] = [
1762                                        'carrier_list'   => $data['carrier_list'],
1763                                        'warehouse_list' => $data['warehouse_list'],
1764                                        'product_list'   => [],
1765                                    ];
1766                                }
1767                                $packageList[$idAddressDelivery][$key][$idWarehouse][$idCarrier]['carrier_list'] =
1768                                    array_intersect($packageList[$idAddressDelivery][$key][$idWarehouse][$idCarrier]['carrier_list'], $data['carrier_list']);
1769                                $packageList[$idAddressDelivery][$key][$idWarehouse][$idCarrier]['product_list'] =
1770                                    array_merge($packageList[$idAddressDelivery][$key][$idWarehouse][$idCarrier]['product_list'], $data['product_list']);
1771
1772                                break;
1773                            }
1774                        }
1775                    }
1776                }
1777            }
1778        }
1779
1780        // Step 5 : Reduce depth of $package_list
1781        $finalPackageList = [];
1782        foreach ($packageList as $idAddressDelivery => $productsInStockList) {
1783            if (!isset($finalPackageList[$idAddressDelivery])) {
1784                $finalPackageList[$idAddressDelivery] = [];
1785            }
1786
1787            foreach ($productsInStockList as $key => $warehouseList) {
1788                foreach ($warehouseList as $idWarehouse => $productsGroupedByCarriers) {
1789                    foreach ($productsGroupedByCarriers as $data) {
1790                        $finalPackageList[$idAddressDelivery][] = [
1791                            'product_list'   => $data['product_list'],
1792                            'carrier_list'   => $data['carrier_list'],
1793                            'warehouse_list' => $data['warehouse_list'],
1794                            'id_warehouse'   => $idWarehouse,
1795                        ];
1796                    }
1797                }
1798            }
1799        }
1800        $cache[$cacheKey] = $finalPackageList;
1801
1802        return $finalPackageList;
1803    }
1804
1805    /**
1806     * Return package shipping cost
1807     *
1808     * @param int          $idCarrier      Carrier ID (default: current carrier)
1809     * @param bool         $useTax
1810     * @param Country|null $defaultCountry
1811     * @param array|null   $productList    List of product concerned by the
1812     *                                     shipping. If null, all the product
1813     *                                     of the cart are used to calculate
1814     *                                     the shipping cost.
1815     * @param int|null     $idZone
1816     *
1817     * @return bool|float Shipping total, rounded to
1818     *                    _TB_PRICE_DATABASE_PRECISION_, or false on failure.
1819     *
1820     * @since   1.0.0
1821     * @version 1.0.0 Initial version
1822     * @throws PrestaShopException
1823     * @throws PrestaShopException
1824     * @throws PrestaShopException
1825     * @throws Adapter_Exception
1826     */
1827    public function getPackageShippingCost($idCarrier = null, $useTax = true, Country $defaultCountry = null, $productList = null, $idZone = null)
1828    {
1829        if ($this->isVirtualCart()) {
1830            return 0.;
1831        }
1832
1833        if (!$defaultCountry) {
1834            $defaultCountry = Context::getContext()->country;
1835        }
1836
1837        if (!is_null($productList)) {
1838            foreach ($productList as $key => $value) {
1839                if ($value['is_virtual'] == 1) {
1840                    unset($productList[$key]);
1841                }
1842            }
1843        }
1844
1845        if (is_null($productList)) {
1846            $products = $this->getProducts();
1847        } else {
1848            $products = $productList;
1849        }
1850
1851        if (Configuration::get('PS_TAX_ADDRESS_TYPE') == 'id_address_invoice') {
1852            $addressId = (int) $this->id_address_invoice;
1853        } elseif (is_array($productList) && count($productList)) {
1854            $prod = current($productList);
1855            $addressId = (int) $prod['id_address_delivery'];
1856        } else {
1857            $addressId = null;
1858        }
1859        if (!Address::addressExists($addressId)) {
1860            $addressId = null;
1861        }
1862
1863        if (is_null($idCarrier) && !empty($this->id_carrier)) {
1864            $idCarrier = (int) $this->id_carrier;
1865        }
1866
1867        $cacheId = 'getPackageShippingCost_'.(int) $this->id.'_'.(int) $addressId.'_'.(int) $idCarrier.'_'.(int) $useTax.'_'.(int) $defaultCountry->id.'_'.(int) $idZone;
1868        if ($products) {
1869            foreach ($products as $product) {
1870                $cacheId .= '_'.(int) $product['id_product'].'_'.(int) $product['id_product_attribute'];
1871            }
1872        }
1873
1874        if (Cache::isStored($cacheId)) {
1875            return (float) Cache::retrieve($cacheId);
1876        }
1877
1878        // Order total in default currency without fees
1879        $orderTotal = $this->getOrderTotal(true, static::ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING, $productList);
1880
1881        // Start with shipping cost at 0
1882        $shippingCost = 0.;
1883        // If no product added, return 0
1884        if (!count($products)) {
1885            Cache::store($cacheId, $shippingCost);
1886
1887            return $shippingCost;
1888        }
1889
1890        if (!isset($idZone)) {
1891            // Get id zone
1892            if (!$this->isMultiAddressDelivery()
1893                && isset($this->id_address_delivery) // Be carefull, id_address_delivery is not usefull one 1.5
1894                && $this->id_address_delivery
1895                && Customer::customerHasAddress(
1896                    $this->id_customer,
1897                    $this->id_address_delivery
1898                )
1899            ) {
1900                $idZone = Address::getZoneById((int) $this->id_address_delivery);
1901            } else {
1902                if (!Validate::isLoadedObject($defaultCountry)) {
1903                    $defaultCountry = new Country(Configuration::get('PS_COUNTRY_DEFAULT'), Configuration::get('PS_LANG_DEFAULT'));
1904                }
1905
1906                $idZone = (int) $defaultCountry->id_zone;
1907            }
1908        }
1909
1910        if ($idCarrier && !$this->isCarrierInRange((int) $idCarrier, (int) $idZone)) {
1911            $idCarrier = '';
1912        }
1913
1914        if (empty($idCarrier) && $this->isCarrierInRange((int) Configuration::get('PS_CARRIER_DEFAULT'), (int) $idZone)) {
1915            $idCarrier = (int) Configuration::get('PS_CARRIER_DEFAULT');
1916        }
1917
1918        $totalPackageWithoutShippingTaxInc = $this->getOrderTotal(true, static::BOTH_WITHOUT_SHIPPING, $productList);
1919        if (empty($idCarrier)) {
1920            if ((int) $this->id_customer) {
1921                $customer = new Customer((int) $this->id_customer);
1922                $result = Carrier::getCarriers((int) Configuration::get('PS_LANG_DEFAULT'), true, false, (int) $idZone, $customer->getGroups());
1923                unset($customer);
1924            } else {
1925                $result = Carrier::getCarriers((int) Configuration::get('PS_LANG_DEFAULT'), true, false, (int) $idZone);
1926            }
1927
1928            foreach ($result as $k => $row) {
1929                if ($row['id_carrier'] == Configuration::get('PS_CARRIER_DEFAULT')) {
1930                    continue;
1931                }
1932
1933                if (!isset(static::$_carriers[$row['id_carrier']])) {
1934                    static::$_carriers[$row['id_carrier']] = new Carrier((int) $row['id_carrier']);
1935                }
1936
1937                /** @var Carrier $carrier */
1938                $carrier = static::$_carriers[$row['id_carrier']];
1939
1940                $shippingMethod = $carrier->getShippingMethod();
1941                // Get only carriers that are compliant with shipping method
1942                if (($shippingMethod == Carrier::SHIPPING_METHOD_WEIGHT && $carrier->getMaxDeliveryPriceByWeight((int) $idZone) === false)
1943                    || ($shippingMethod == Carrier::SHIPPING_METHOD_PRICE && $carrier->getMaxDeliveryPriceByPrice((int) $idZone) === false)
1944                ) {
1945                    unset($result[$k]);
1946                    continue;
1947                }
1948
1949                // If out-of-range behavior carrier is set to "Deactivate carrier"
1950                if ($row['range_behavior']) {
1951                    $checkDeliveryPriceByWeight = Carrier::checkDeliveryPriceByWeight($row['id_carrier'], $this->getTotalWeight(), (int) $idZone);
1952
1953                    $totalOrder = $totalPackageWithoutShippingTaxInc;
1954                    $checkDeliveryPriceByPrice = Carrier::checkDeliveryPriceByPrice($row['id_carrier'], $totalOrder, (int) $idZone, (int) $this->id_currency);
1955
1956                    // Get only carriers that have a range compatible with cart
1957                    if (($shippingMethod == Carrier::SHIPPING_METHOD_WEIGHT && !$checkDeliveryPriceByWeight)
1958                        || ($shippingMethod == Carrier::SHIPPING_METHOD_PRICE && !$checkDeliveryPriceByPrice)
1959                    ) {
1960                        unset($result[$k]);
1961                        continue;
1962                    }
1963                }
1964
1965                if ($shippingMethod == Carrier::SHIPPING_METHOD_WEIGHT) {
1966                    $shipping = $carrier->getDeliveryPriceByWeight($this->getTotalWeight($productList), (int) $idZone);
1967                } else {
1968                    $shipping = $carrier->getDeliveryPriceByPrice($orderTotal, (int) $idZone, (int) $this->id_currency);
1969                }
1970
1971                if (!isset($minShippingPrice)) {
1972                    $minShippingPrice = $shipping;
1973                }
1974
1975                if ($shipping <= $minShippingPrice) {
1976                    $idCarrier = (int) $row['id_carrier'];
1977                    $minShippingPrice = $shipping;
1978                }
1979            }
1980        }
1981
1982        if (empty($idCarrier)) {
1983            $idCarrier = Configuration::get('PS_CARRIER_DEFAULT');
1984        }
1985
1986        if (!isset(static::$_carriers[$idCarrier])) {
1987            static::$_carriers[$idCarrier] = new Carrier((int) $idCarrier, Configuration::get('PS_LANG_DEFAULT'));
1988        }
1989
1990        $carrier = static::$_carriers[$idCarrier];
1991
1992        // No valid Carrier or $id_carrier <= 0 ?
1993        if (!Validate::isLoadedObject($carrier)) {
1994            Cache::store($cacheId, 0.);
1995
1996            return 0.;
1997        }
1998        $shippingMethod = $carrier->getShippingMethod();
1999
2000        if (!$carrier->active) {
2001            Cache::store($cacheId, $shippingCost);
2002
2003            return $shippingCost;
2004        }
2005
2006        // Free fees if free carrier
2007        if ($carrier->is_free == 1) {
2008            Cache::store($cacheId, 0.);
2009
2010            return 0.;
2011        }
2012
2013        // Select carrier tax
2014        if ($useTax && !Tax::excludeTaxeOption()) {
2015            $address = Address::initialize((int) $addressId);
2016
2017            if (Configuration::get('PS_ATCP_SHIPWRAP')) {
2018                // With PS_ATCP_SHIPWRAP, pre-tax price is deduced
2019                // from post tax price, so no $carrier_tax here
2020                // even though it sounds weird.
2021                $carrierTax = 0;
2022            } else {
2023                $carrierTax = $carrier->getTaxesRate($address);
2024            }
2025        }
2026
2027        $configuration = Configuration::getMultiple(
2028            [
2029                'PS_SHIPPING_FREE_PRICE',
2030                'PS_SHIPPING_HANDLING',
2031                'PS_SHIPPING_METHOD',
2032                'PS_SHIPPING_FREE_WEIGHT',
2033            ]
2034        );
2035
2036        // Free fees
2037        $freeFeesPrice = 0;
2038        if (isset($configuration['PS_SHIPPING_FREE_PRICE'])) {
2039            $freeFeesPrice = Tools::convertPrice((float) $configuration['PS_SHIPPING_FREE_PRICE'], Currency::getCurrencyInstance((int) $this->id_currency));
2040        }
2041        $orderTotalWithDiscounts = $this->getOrderTotal(true, static::BOTH_WITHOUT_SHIPPING, null, null, false);
2042        if ($orderTotalWithDiscounts >= (float) ($freeFeesPrice) && (float) ($freeFeesPrice) > 0) {
2043            Cache::store($cacheId, $shippingCost);
2044
2045            return $shippingCost;
2046        }
2047
2048        if (isset($configuration['PS_SHIPPING_FREE_WEIGHT'])
2049            && $this->getTotalWeight() >= (float) $configuration['PS_SHIPPING_FREE_WEIGHT']
2050            && (float) $configuration['PS_SHIPPING_FREE_WEIGHT'] > 0
2051        ) {
2052            Cache::store($cacheId, $shippingCost);
2053
2054            return $shippingCost;
2055        }
2056
2057        // Get shipping cost using correct method
2058        if ($carrier->range_behavior) {
2059            if (!isset($idZone)) {
2060                // Get id zone
2061                if (isset($this->id_address_delivery)
2062                    && $this->id_address_delivery
2063                    && Customer::customerHasAddress($this->id_customer, $this->id_address_delivery)
2064                ) {
2065                    $idZone = Address::getZoneById((int) $this->id_address_delivery);
2066                } else {
2067                    $idZone = (int) $defaultCountry->id_zone;
2068                }
2069            }
2070
2071            if (($shippingMethod == Carrier::SHIPPING_METHOD_WEIGHT && !Carrier::checkDeliveryPriceByWeight($carrier->id, $this->getTotalWeight(), (int) $idZone))
2072                || ($shippingMethod == Carrier::SHIPPING_METHOD_PRICE && !Carrier::checkDeliveryPriceByPrice($carrier->id, $totalPackageWithoutShippingTaxInc, $idZone, (int) $this->id_currency)
2073                )
2074            ) {
2075                $shippingCost += 0;
2076            } else {
2077                if ($shippingMethod == Carrier::SHIPPING_METHOD_WEIGHT) {
2078                    $shippingCost += $carrier->getDeliveryPriceByWeight($this->getTotalWeight($productList), $idZone);
2079                } else { // by price
2080                    $shippingCost += $carrier->getDeliveryPriceByPrice($orderTotal, $idZone, (int) $this->id_currency);
2081                }
2082            }
2083        } else {
2084            if ($shippingMethod == Carrier::SHIPPING_METHOD_WEIGHT) {
2085                $shippingCost += $carrier->getDeliveryPriceByWeight($this->getTotalWeight($productList), $idZone);
2086            } else {
2087                $shippingCost += $carrier->getDeliveryPriceByPrice($orderTotal, $idZone, (int) $this->id_currency);
2088            }
2089        }
2090        // Adding handling charges
2091        if (isset($configuration['PS_SHIPPING_HANDLING']) && $carrier->shipping_handling) {
2092            $shippingCost += (float) $configuration['PS_SHIPPING_HANDLING'];
2093        }
2094
2095        // Additional Shipping Cost per product
2096        foreach ($products as $product) {
2097            if (!$product['is_virtual']) {
2098                $shippingCost += $product['additional_shipping_cost'] * $product['cart_quantity'];
2099            }
2100        }
2101
2102        $shippingCost = Tools::convertPrice($shippingCost, Currency::getCurrencyInstance((int) $this->id_currency));
2103
2104        //get external shipping cost from module
2105        if ($carrier->shipping_external) {
2106            $moduleName = $carrier->external_module_name;
2107
2108            /** @var CarrierModule $module */
2109            $module = Module::getInstanceByName($moduleName);
2110
2111            if (Validate::isLoadedObject($module)) {
2112                if (property_exists($module, 'id_carrier')) {
2113                    $module->id_carrier = $carrier->id;
2114                }
2115                if ($carrier->need_range) {
2116                    if (method_exists($module, 'getPackageShippingCost')) {
2117                        $shippingCost = $module->getPackageShippingCost($this, $shippingCost, $products);
2118                    } else {
2119                        $shippingCost = $module->getOrderShippingCost($this, $shippingCost);
2120                    }
2121                } else {
2122                    $shippingCost = $module->getOrderShippingCostExternal($this);
2123                }
2124
2125                // Check if carrier is available
2126                if ($shippingCost === false) {
2127                    Cache::store($cacheId, false);
2128
2129                    return false;
2130                }
2131            } else {
2132                Cache::store($cacheId, false);
2133
2134                return false;
2135            }
2136        }
2137
2138        if (Configuration::get('PS_ATCP_SHIPWRAP')) {
2139            if ($useTax) {
2140                // With PS_ATCP_SHIPWRAP, we apply the proportionate tax rate to the shipping
2141                // costs. This is on purpose and required in many countries in the European Union.
2142                $shippingCost *= 1 + $this->getAverageProductsTaxRate();
2143            }
2144        } else {
2145            // Apply tax
2146            if ($useTax && isset($carrierTax)) {
2147                $shippingCost *= 1 + ($carrierTax / 100);
2148            }
2149        }
2150        $shippingCost = round($shippingCost, _TB_PRICE_DATABASE_PRECISION_);
2151
2152        Cache::store($cacheId, $shippingCost);
2153
2154        return $shippingCost;
2155    }
2156
2157    /**
2158     * Does the cart use multiple address
2159     *
2160     * @return bool
2161     *
2162     * @since   1.0.0
2163     * @version 1.0.0 Initial version
2164     * @throws PrestaShopException
2165     */
2166    public function isMultiAddressDelivery()
2167    {
2168        static $cache = [];
2169
2170        if (!isset($cache[$this->id])) {
2171            $sql = new DbQuery();
2172            $sql->select('count(distinct id_address_delivery)');
2173            $sql->from('cart_product', 'cp');
2174            $sql->where('id_cart = '.(int) $this->id);
2175
2176            $cache[$this->id] = Db::getInstance()->getValue($sql) > 1;
2177        }
2178
2179        return $cache[$this->id];
2180    }
2181
2182    /**
2183     * isCarrierInRange
2184     *
2185     * Check if the specified carrier is in range
2186     *
2187     * @param int $idCarrier
2188     * @param int $idZone
2189     *
2190     * @return bool
2191     *
2192     * @throws Adapter_Exception
2193     * @throws PrestaShopDatabaseException
2194     * @throws PrestaShopException
2195     * @since      1.0.0
2196     * @version    1.0.0 Initial version
2197     */
2198    public function isCarrierInRange($idCarrier, $idZone)
2199    {
2200        $carrier = new Carrier((int) $idCarrier, Configuration::get('PS_LANG_DEFAULT'));
2201        $shippingMethod = $carrier->getShippingMethod();
2202        if (!$carrier->range_behavior) {
2203            return true;
2204        }
2205
2206        if ($shippingMethod == Carrier::SHIPPING_METHOD_FREE) {
2207            return true;
2208        }
2209
2210        $checkDeliveryPriceByWeight = Carrier::checkDeliveryPriceByWeight(
2211            (int) $idCarrier,
2212            $this->getTotalWeight(),
2213            $idZone
2214        );
2215        if ($shippingMethod == Carrier::SHIPPING_METHOD_WEIGHT && $checkDeliveryPriceByWeight) {
2216            return true;
2217        }
2218
2219        $checkDeliveryPriceByPrice = Carrier::checkDeliveryPriceByPrice(
2220            (int) $idCarrier,
2221            $this->getOrderTotal(
2222                true,
2223                static::BOTH_WITHOUT_SHIPPING
2224            ),
2225            $idZone,
2226            (int) $this->id_currency
2227        );
2228        if ($shippingMethod == Carrier::SHIPPING_METHOD_PRICE && $checkDeliveryPriceByPrice) {
2229            return true;
2230        }
2231
2232        return false;
2233    }
2234
2235    /**
2236     * Return cart weight
2237     *
2238     * @param array|null $products
2239     *
2240     * @return float Cart weight
2241     *
2242     * @since   1.0.0
2243     * @version 1.0.0 Initial version
2244     * @throws PrestaShopException
2245     * @throws PrestaShopException
2246     */
2247    public function getTotalWeight($products = null)
2248    {
2249        if (!is_null($products)) {
2250            $totalWeight = 0;
2251            foreach ($products as $product) {
2252                if (!isset($product['weight_attribute']) || is_null($product['weight_attribute'])) {
2253                    $totalWeight += $product['weight'] * $product['cart_quantity'];
2254                } else {
2255                    $totalWeight += $product['weight_attribute'] * $product['cart_quantity'];
2256                }
2257            }
2258
2259            return $totalWeight;
2260        }
2261
2262        if (!isset(static::$_totalWeight[$this->id])) {
2263            if (Combination::isFeatureActive()) {
2264                $weightProductWithAttribute = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
2265                    (new DbQuery())
2266                        ->select('SUM((p.`weight` + pa.`weight`) * cp.`quantity`) AS `nb`')
2267                        ->from('cart_product', 'cp')
2268                        ->leftJoin('product', 'p', 'cp.`id_product` = p.`id_product`')
2269                        ->leftJoin('product_attribute', 'pa', 'cp.`id_product_attribute` = pa.`id_product_attribute`')
2270                        ->where('cp.`id_product_attribute` IS NOT NULL')
2271                        ->where('cp.`id_product_attribute` != 0')
2272                        ->where('cp.`id_cart` = '.(int) $this->id)
2273                );
2274            } else {
2275                $weightProductWithAttribute = 0;
2276            }
2277
2278            $weightProductWithoutAttribute = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
2279                (new DbQuery())
2280                    ->select('SUM(p.`weight` * cp.`quantity`) AS `nb`')
2281                    ->from('cart_product', 'cp')
2282                    ->leftJoin('product', 'p', 'cp.`id_product` = p.`id_product`')
2283                    ->where('cp.`id_product_attribute` IS NULL OR cp.`id_product_attribute` = 0')
2284                    ->where('cp.`id_cart` = '.(int) $this->id)
2285            );
2286
2287            static::$_totalWeight[$this->id] = round((float) $weightProductWithAttribute + (float) $weightProductWithoutAttribute, 6);
2288        }
2289
2290        return static::$_totalWeight[$this->id];
2291    }
2292
2293    /**
2294     * The arguments are optional and only serve as return values in case
2295     * caller needs the details.
2296     *
2297     * @param null $amountTaxExcluded
2298     * @param null $amountTaxIncluded
2299     *
2300     * @return float
2301     *
2302     * @throws Adapter_Exception
2303     * @throws PrestaShopDatabaseException
2304     * @throws PrestaShopException
2305     *
2306     * @since   1.0.0
2307     */
2308    public function getAverageProductsTaxRate(&$amountTaxExcluded = null,
2309                                              &$amountTaxIncluded = null)
2310    {
2311        $amountTaxIncluded = $this->getOrderTotal(true, static::ONLY_PRODUCTS);
2312        $amountTaxExcluded = $this->getOrderTotal(false, static::ONLY_PRODUCTS);
2313
2314        $tax = $amountTaxIncluded - $amountTaxExcluded;
2315        if ($tax == 0 || $amountTaxExcluded == 0) {
2316            return 0.0;
2317        }
2318
2319        return $tax / $amountTaxExcluded;
2320    }
2321
2322    /**
2323     * Get the gift wrapping price
2324     *
2325     * @param bool     $withTaxes With or without taxes
2326     * @param int|null $idAddress Address ID
2327     *
2328     * @return float wrapping price
2329     *
2330     * @throws Adapter_Exception
2331     * @throws PrestaShopDatabaseException
2332     * @throws PrestaShopException
2333     * @since   1.0.0
2334     * @version 1.0.0 Initial version
2335     */
2336    public function getGiftWrappingPrice($withTaxes = true, $idAddress = null)
2337    {
2338        static $address = [];
2339
2340        $wrappingFees = (float) Configuration::get('PS_GIFT_WRAPPING_PRICE');
2341
2342        if ($wrappingFees <= 0) {
2343            return $wrappingFees;
2344        }
2345
2346        if ($withTaxes) {
2347            if (Configuration::get('PS_ATCP_SHIPWRAP')) {
2348                // With PS_ATCP_SHIPWRAP, wrapping fee is by default tax included
2349                // so nothing to do here.
2350            } else {
2351                if (!isset($address[$this->id])) {
2352                    if ($idAddress === null) {
2353                        $idAddress = (int) $this->{Configuration::get('PS_TAX_ADDRESS_TYPE')};
2354                    }
2355                    try {
2356                        $address[$this->id] = Address::initialize($idAddress);
2357                    } catch (Exception $e) {
2358                        $address[$this->id] = new Address();
2359                        $address[$this->id]->id_country = Configuration::get('PS_COUNTRY_DEFAULT');
2360                    }
2361                }
2362
2363                $taxManager = TaxManagerFactory::getManager($address[$this->id], (int) Configuration::get('PS_GIFT_WRAPPING_TAX_RULES_GROUP'));
2364                $taxCalculator = $taxManager->getTaxCalculator();
2365                $wrappingFees = $taxCalculator->addTaxes($wrappingFees);
2366            }
2367
2368            if (Configuration::get('PS_ATCP_SHIPWRAP')) {
2369                // With PS_ATCP_SHIPWRAP, wrapping fee is by default tax included, so we convert it
2370                // when asked for the pre tax price.
2371                $wrappingFees = round(
2372                    $wrappingFees * (1 + $this->getAverageProductsTaxRate()),
2373                    _TB_PRICE_DATABASE_PRECISION_
2374                );
2375            }
2376        }
2377
2378        return $wrappingFees;
2379    }
2380
2381    /**
2382     * @param int $filter
2383     *
2384     * @return array|false|mysqli_result|null|PDOStatement|resource
2385     *
2386     * @throws PrestaShopDatabaseException
2387     * @throws PrestaShopException
2388     * @since   1.0.0
2389     * @version 1.0.0 Initial version
2390     */
2391    public function getCartRules($filter = CartRule::FILTER_ACTION_ALL)
2392    {
2393        // If the cart has not been saved, then there can't be any cart rule applied
2394        if (!CartRule::isFeatureActive() || !$this->id) {
2395            return [];
2396        }
2397
2398        $cacheKey = 'static::getCartRules_'.$this->id.'-'.$filter;
2399        if (!Cache::isStored($cacheKey)) {
2400            $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
2401                (new DbQuery())
2402                    ->select('cr.*, crl.`id_lang`, crl.`name`, cd.`id_cart`')
2403                    ->from('cart_cart_rule', 'cd')
2404                    ->leftJoin('cart_rule', 'cr', 'cd.`id_cart_rule` = cr.`id_cart_rule`')
2405                    ->leftJoin('cart_rule_lang', 'crl', 'cd.`id_cart_rule` = crl.`id_cart_rule` AND crl.`id_lang` = '.(int) $this->id_lang)
2406                    ->where('`id_cart` = '.(int) $this->id)
2407                    ->where((int) $filter === CartRule::FILTER_ACTION_SHIPPING ? '`free_shipping` = 1' : '')
2408                    ->where((int) $filter === CartRule::FILTER_ACTION_GIFT ? '`gift_product` = 0' : '')
2409                    ->where((int) $filter === CartRule::FILTER_ACTION_REDUCTION ? '`reduction_percent` != 0 OR `reduction_amount` != 0' : '')
2410                    ->orderBy('cr.`priority` ASC')
2411            );
2412            Cache::store($cacheKey, $result);
2413        } else {
2414            $result = Cache::retrieve($cacheKey);
2415        }
2416
2417        // Define virtual context to prevent case where the cart is not the in the global context
2418        $virtualContext = Context::getContext()->cloneContext();
2419        $virtualContext->cart = $this;
2420
2421        foreach ($result as &$row) {
2422            $cartRule = new CartRule();
2423            $cartRule->hydrate($row);
2424
2425            $row['obj'] = $cartRule;
2426            $row['value_real'] = $cartRule->getContextualValue(true, $virtualContext, $filter);
2427            $row['value_tax_exc'] = $cartRule->getContextualValue(false, $virtualContext, $filter);
2428            // Retro compatibility < 1.5.0.2
2429            $row['id_discount'] = $row['id_cart_rule'];
2430            $row['description'] = $row['name'];
2431        }
2432
2433        return $result;
2434    }
2435
2436    /*
2437    ** Customization management
2438    */
2439
2440    /**
2441     * @param $deliveryOption
2442     *
2443     * @return int|mixed
2444     *
2445     * @throws Adapter_Exception
2446     * @throws PrestaShopDatabaseException
2447     * @throws PrestaShopException
2448     * @since   1.0.0
2449     * @version 1.0.0 Initial version
2450     */
2451    protected function getIdCarrierFromDeliveryOption($deliveryOption)
2452    {
2453        $deliveryOptionList = $this->getDeliveryOptionList();
2454        foreach ($deliveryOption as $key => $value) {
2455            if (isset($deliveryOptionList[$key]) && isset($deliveryOptionList[$key][$value])) {
2456                if (count($deliveryOptionList[$key][$value]['carrier_list']) == 1) {
2457                    return current(array_keys($deliveryOptionList[$key][$value]['carrier_list']));
2458                }
2459            }
2460        }
2461
2462        return 0;
2463    }
2464
2465    /**
2466     *
2467     * Sort list of option delivery by parameters define in the BO
2468     *
2469     * @param array $option1
2470     * @param array $option2
2471     *
2472     * @return int -1 if $option 1 must be placed before and 1 if the $option1 must be placed after the $option2
2473     *
2474     * @since   1.0.0
2475     * @version 1.0.0 Initial version
2476     * @throws PrestaShopException
2477     */
2478    public static function sortDeliveryOptionList($option1, $option2)
2479    {
2480        static $orderByPrice = null;
2481        static $orderWay = null;
2482        if (is_null($orderByPrice)) {
2483            $orderByPrice = !Configuration::get('PS_CARRIER_DEFAULT_SORT');
2484        }
2485        if (is_null($orderWay)) {
2486            $orderWay = Configuration::get('PS_CARRIER_DEFAULT_ORDER');
2487        }
2488
2489        if ($orderByPrice) {
2490            if ($orderWay) {
2491                return ($option1['total_price_with_tax'] < $option2['total_price_with_tax']) * 2 - 1;
2492            } // return -1 or 1
2493            else {
2494                return ($option1['total_price_with_tax'] >= $option2['total_price_with_tax']) * 2 - 1;
2495            }
2496        } // return -1 or 1
2497        elseif ($orderWay) {
2498            return ($option1['position'] < $option2['position']) * 2 - 1;
2499        } // return -1 or 1
2500        else {
2501            return ($option1['position'] >= $option2['position']) * 2 - 1;
2502        } // return -1 or 1
2503    }
2504
2505    /**
2506     * Translate a int option_delivery identifier (3240002000) in a string ('24,3,')
2507     *
2508     * @param int    $int
2509     * @param string $delimiter
2510     *
2511     * @return string
2512     *
2513     * @since   1.0.0
2514     * @version 1.0.0 Initial version
2515     */
2516    public static function desintifier($int, $delimiter = ',')
2517    {
2518        $delimiterLen = $int[0];
2519        $int = strrev(substr($int, 1));
2520        $elm = explode(str_repeat('0', $delimiterLen + 1), $int);
2521
2522        return strrev(implode($delimiter, $elm));
2523    }
2524
2525    /**
2526     * @param int $idCustomer
2527     *
2528     * @return bool|int
2529     *
2530     * @since   1.0.0
2531     * @version 1.0.0 Initial version
2532     * @throws PrestaShopException
2533     */
2534    public static function lastNoneOrderedCart($idCustomer)
2535    {
2536        if (!$idCart = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
2537            (new DbQuery())
2538                ->select('c.`id_cart`')
2539                ->from('cart', 'c')
2540                ->where('NOT EXISTS (SELECT 1 FROM '._DB_PREFIX_.'orders o WHERE o.`id_cart` = c.`id_cart`AND o.`id_customer` = '.(int) $idCustomer.')')
2541                ->where('c.`id_customer` = '.(int) $idCustomer.' '.Shop::addSqlRestriction(Shop::SHARE_ORDER, 'c'))
2542                ->orderBy('c.`date_upd` DESC')
2543        )) {
2544            return false;
2545        }
2546
2547        return (int) $idCart;
2548    }
2549
2550    /**
2551     * Build cart object from provided id_order
2552     *
2553     * @param int $idOrder
2554     *
2555     * @return Cart|bool
2556     *
2557     * @throws PrestaShopDatabaseException
2558     * @throws PrestaShopException
2559     * @since   1.0.0
2560     * @version 1.0.0 Initial version
2561     */
2562    public static function getCartByOrderId($idOrder)
2563    {
2564        if ($idCart = static::getCartIdByOrderId($idOrder)) {
2565            return new Cart((int) $idCart);
2566        }
2567
2568        return false;
2569    }
2570
2571    /**
2572     * @param int $idOrder
2573     *
2574     * @return bool
2575     *
2576     * @throws PrestaShopDatabaseException
2577     * @throws PrestaShopException
2578     * @since   1.0.0
2579     * @version 1.0.0 Initial version
2580     */
2581    public static function getCartIdByOrderId($idOrder)
2582    {
2583        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow(
2584            (new DbQuery())
2585                ->select('`id_cart`')
2586                ->from('orders')
2587                ->where('`id_order` = '.(int) $idOrder)
2588        );
2589        if (!$result || empty($result) || !array_key_exists('id_cart', $result)) {
2590            return false;
2591        }
2592
2593        return $result['id_cart'];
2594    }
2595
2596    /**
2597     * @param int  $idCustomer
2598     * @param bool $dontRejectOrdered if true, all carts will be returned, otherwise
2599     *             already ordered carts will be filtered out
2600     *
2601     * @return array|false|mysqli_result|null|PDOStatement|resource
2602     *
2603     * @throws PrestaShopDatabaseException
2604     * @throws PrestaShopException
2605     * @since   1.0.0
2606     * @version 1.0.0 Initial version
2607     */
2608    public static function getCustomerCarts($idCustomer, $dontRejectOrdered = true)
2609    {
2610        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
2611            (new DbQuery())
2612                ->select('*')
2613                ->from('cart', 'c')
2614                ->where('c.`id_customer` = '.(int) $idCustomer)
2615                ->where($dontRejectOrdered ? '' : 'NOT EXISTS (SELECT 1 FROM '._DB_PREFIX_.'orders o WHERE o.`id_cart` = c.`id_cart`)')
2616                ->orderBy('c.`date_add` DESC')
2617        );
2618    }
2619
2620    /**
2621     * @param string $echo
2622     * @param mixed  $tr
2623     *
2624     * @return string
2625     *
2626     * @since   1.0.0
2627     * @version 1.0.0 Initial version
2628     * @throws PrestaShopException
2629     */
2630    public static function replaceZeroByShopName($echo, $tr)
2631    {
2632        return ($echo == '0' ? Carrier::getCarrierNameFromShopName() : $echo);
2633    }
2634
2635    /**
2636     * isGuestCartByCartId
2637     *
2638     * @param int $idCart
2639     *
2640     * @return bool true if cart has been made by a guest customer
2641     *
2642     * @since   1.0.0
2643     * @version 1.0.0 Initial version
2644     * @throws PrestaShopException
2645     */
2646    public static function isGuestCartByCartId($idCart)
2647    {
2648        if (!(int) $idCart) {
2649            return false;
2650        }
2651
2652        return (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
2653            (new DbQuery())
2654                ->select('`is_guest`')
2655                ->from('customer', 'cu')
2656                ->leftJoin('cart', 'ca', 'ca.`id_customer` = cu.`id_customer`')
2657                ->where('ca.`id_cart` = '.(int) $idCart)
2658        );
2659    }
2660
2661    /**
2662     *
2663     * Execute hook displayCarrierList (extraCarrier) and merge theme to the $array
2664     *
2665     * @param array $array
2666     *
2667     * @throws PrestaShopDatabaseException
2668     * @throws PrestaShopException
2669     */
2670    public static function addExtraCarriers(&$array)
2671    {
2672        $first = true;
2673        $hookExtracarrierAddr = [];
2674        foreach (Context::getContext()->cart->getAddressCollection() as $address) {
2675            $hook = Hook::exec('displayCarrierList', ['address' => $address]);
2676            $hookExtracarrierAddr[$address->id] = $hook;
2677
2678            if ($first) {
2679                $array = array_merge(
2680                    $array,
2681                    ['HOOK_EXTRACARRIER' => $hook]
2682                );
2683                $first = false;
2684            }
2685            $array = array_merge(
2686                $array,
2687                ['HOOK_EXTRACARRIER_ADDR' => $hookExtracarrierAddr]
2688            );
2689        }
2690    }
2691
2692    /**
2693     * Get all delivery addresses object for the current cart
2694     *
2695     * @return array
2696     * @throws PrestaShopDatabaseException
2697     * @throws PrestaShopException
2698     */
2699    public function getAddressCollection()
2700    {
2701        $collection = [];
2702        $cacheId = 'static::getAddressCollection'.(int) $this->id;
2703        if (!Cache::isStored($cacheId)) {
2704            $result = Db::getInstance()->executeS(
2705                (new DbQuery())
2706                    ->select('DISTINCT `id_address_delivery`')
2707                    ->from('cart_product')
2708                    ->where('`id_cart` = '.(int) $this->id)
2709            );
2710            Cache::store($cacheId, $result);
2711        } else {
2712            $result = Cache::retrieve($cacheId);
2713        }
2714
2715        $result[] = ['id_address_delivery' => (int) $this->id_address_delivery];
2716
2717        foreach ($result as $row) {
2718            if ((int) $row['id_address_delivery'] != 0) {
2719                $collection[(int) $row['id_address_delivery']] = new Address((int) $row['id_address_delivery']);
2720            }
2721        }
2722
2723        return $collection;
2724    }
2725
2726    /**
2727     * Update the address id of the cart
2728     *
2729     * @param int $idAddress    Current address id to change
2730     * @param int $idAddressNew New address id
2731     *
2732     * @throws PrestaShopDatabaseException
2733     * @throws PrestaShopException
2734     * @since   1.0.0
2735     * @version 1.0.0 Initial version
2736     */
2737    public function updateAddressId($idAddress, $idAddressNew)
2738    {
2739        $toUpdate = false;
2740        if (!isset($this->id_address_invoice) || $this->id_address_invoice == $idAddress) {
2741            $toUpdate = true;
2742            $this->id_address_invoice = $idAddressNew;
2743        }
2744        if (!isset($this->id_address_delivery) || $this->id_address_delivery == $idAddress) {
2745            $toUpdate = true;
2746            $this->id_address_delivery = $idAddressNew;
2747        }
2748        if ($toUpdate) {
2749            $this->update();
2750        }
2751
2752        Db::getInstance()->update(
2753            'cart_product',
2754            [
2755                'id_address_delivery' => (int) $idAddressNew,
2756            ],
2757            '`id_cart` = '.(int) $this->id.' AND `id_address_delivery` = '.(int) $idAddress
2758        );
2759
2760        Db::getInstance()->update(
2761            'customization',
2762            [
2763                'id_address_delivery' => (int) $idAddressNew,
2764            ],
2765            '`id_cart` = '.(int) $this->id.' AND `id_address_delivery` = '.(int) $idAddress
2766        );
2767    }
2768
2769    /**
2770     * @param bool $nullValues
2771     *
2772     * @return bool
2773     *
2774     * @since   1.0.0
2775     * @version 1.0.0 Initial version
2776     */
2777    public function update($nullValues = false)
2778    {
2779        if (isset(static::$_nbProducts[$this->id])) {
2780            unset(static::$_nbProducts[$this->id]);
2781        }
2782
2783        if (isset(static::$_totalWeight[$this->id])) {
2784            unset(static::$_totalWeight[$this->id]);
2785        }
2786
2787        $this->_products = null;
2788        $return = parent::update($nullValues);
2789        Hook::exec('actionCartSave', ['cart' => $this]);
2790
2791        return $return;
2792    }
2793
2794    /**
2795     * @return bool
2796     *
2797     * @since   1.0.0
2798     * @version 1.0.0 Initial version
2799     * @throws PrestaShopDatabaseException
2800     * @throws PrestaShopDatabaseException
2801     * @throws PrestaShopDatabaseException
2802     * @throws PrestaShopDatabaseException
2803     * @throws PrestaShopException
2804     */
2805    public function delete()
2806    {
2807        if ($this->OrderExists()) { //NOT delete a cart which is associated with an order
2808            return false;
2809        }
2810
2811        $uploadedFiles = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
2812            (new DbQuery())
2813                ->select('cd.`value`')
2814                ->from('customized_data', 'cd')
2815                ->innerJoin('customization', 'c', 'cd.`id_customization` = c.`id_customization`')
2816                ->where('cd.`type` = 0')
2817                ->where('c.`id_cart` = '.(int) $this->id)
2818        );
2819
2820        foreach ($uploadedFiles as $mustUnlink) {
2821            unlink(_PS_UPLOAD_DIR_.$mustUnlink['value'].'_small');
2822            unlink(_PS_UPLOAD_DIR_.$mustUnlink['value']);
2823        }
2824
2825        Db::getInstance()->delete(
2826            'customized_data',
2827            '`id_customization` IN (SELECT `id_customization` FROM `'._DB_PREFIX_.'customization` WHERE `id_cart`='.(int) $this->id.')'
2828        );
2829
2830        Db::getInstance()->delete(
2831            'customization',
2832            '`id_cart` = '.(int) $this->id
2833        );
2834
2835        if (!Db::getInstance()->delete('cart_cart_rule', '`id_cart` = '.(int) $this->id)
2836            || !Db::getInstance()->delete('cart_product', '`id_cart` = '.(int) $this->id)
2837        ) {
2838            return false;
2839        }
2840
2841        return parent::delete();
2842    }
2843
2844    /**
2845     * Check if order has already been placed
2846     *
2847     * @return bool result
2848     *
2849     * @since   1.0.0
2850     * @version 1.0.0
2851     * @throws PrestaShopException
2852     */
2853    public function orderExists()
2854    {
2855        $cacheId = 'static::orderExists_'.(int) $this->id;
2856        if (!Cache::isStored($cacheId)) {
2857            $result = (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
2858                (new DbQuery())
2859                    ->select('COUNT(*)')
2860                    ->from('orders')
2861                    ->where('`id_cart` = '.(int) $this->id)
2862            );
2863            Cache::store($cacheId, $result);
2864
2865            return $result;
2866        }
2867
2868        return Cache::retrieve($cacheId);
2869    }
2870
2871    /**
2872     * @deprecated 1.0.0, use Cart->getCartRules()
2873     *
2874     * @param bool $lite
2875     * @param bool $refresh
2876     *
2877     * @return array|false|mysqli_result|null|PDOStatement|resource
2878     * @throws PrestaShopDatabaseException
2879     * @throws PrestaShopException
2880     */
2881    public function getDiscounts($lite = false, $refresh = false)
2882    {
2883        Tools::displayAsDeprecated();
2884
2885        return $this->getCartRules();
2886    }
2887
2888    /**
2889     * Return the cart rules Ids on the cart.
2890     *
2891     * @param int $filter
2892     *
2893     * @return array
2894     * @throws PrestaShopDatabaseException
2895     *
2896     * @since   1.0.0
2897     * @version 1.0.0 Initial version
2898     * @throws PrestaShopException
2899     */
2900    public function getOrderedCartRulesIds($filter = CartRule::FILTER_ACTION_ALL)
2901    {
2902        $cacheKey = 'static::getOrderedCartRulesIds_'.$this->id.'-'.$filter.'-ids';
2903        if (!Cache::isStored($cacheKey)) {
2904            $result = Db::getInstance()->executeS(
2905                (new DbQuery())
2906                    ->select('cr.`id_cart_rule`')
2907                    ->from('cart_cart_rule', 'cd')
2908                    ->leftJoin('cart_rule', 'cr', 'cd.`id_cart_rule` = cr.`id_cart_rule`')
2909                    ->leftJoin('cart_rule_lang', 'crl', 'cd.`id_cart_rule` = crl.`id_cart_rule` AND crl.`id_lang` = '.(int) $this->id_lang)
2910                    ->where('cd.`id_cart` = '.(int) $this->id)
2911                    ->where($filter === CartRule::FILTER_ACTION_SHIPPING ? 'cr.`free_shipping` = 1' : '')
2912                    ->where($filter === CartRule::FILTER_ACTION_GIFT ? 'cr.`gift_product` = 1' : '')
2913                    ->where($filter === CartRule::FILTER_ACTION_REDUCTION ? 'cr.`reduction_percent` != 0 OR cr.`reduction_amount` != 0' : '')
2914                    ->orderBy('cr.`priority` ASC')
2915            );
2916            Cache::store($cacheKey, $result);
2917        } else {
2918            $result = Cache::retrieve($cacheKey);
2919        }
2920
2921        return $result;
2922    }
2923
2924    /**
2925     * @param int $idCartRule
2926     *
2927     * @return int|null
2928     *
2929     * @since   1.0.0
2930     * @version 1.0.0 Initial version
2931     * @throws PrestaShopException
2932     */
2933    public function getDiscountsCustomer($idCartRule)
2934    {
2935        if (!CartRule::isFeatureActive()) {
2936            return 0;
2937        }
2938        $cacheId = 'static::getDiscountsCustomer_'.(int) $this->id.'-'.(int) $idCartRule;
2939        if (!Cache::isStored($cacheId)) {
2940            $result = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
2941                (new DbQuery())
2942                    ->select('COUNT(*)')
2943                    ->from('cart_cart_rule')
2944                    ->where('`id_cart_rule` = '.(int) $idCartRule)
2945                    ->where('`id_cart` = '.(int) $this->id)
2946            );
2947            Cache::store($cacheId, $result);
2948
2949            return $result;
2950        }
2951
2952        return Cache::retrieve($cacheId);
2953    }
2954
2955    /**
2956     * @return bool|mixed
2957     *
2958     * @throws PrestaShopDatabaseException
2959     * @throws PrestaShopException
2960     * @since   1.0.0
2961     * @version 1.0.0 Initial version
2962     */
2963    public function getLastProduct()
2964    {
2965        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow(
2966            (new DbQuery())
2967                ->select('`id_product`, `id_product_attribute`, `id_shop`')
2968                ->from('cart_product', 'cp')
2969                ->where('`id_cart` = '.(int) $this->id)
2970                ->orderBy('`date_add` DESC')
2971        );
2972        if ($result && isset($result['id_product']) && $result['id_product']) {
2973            foreach ($this->getProducts() as $product) {
2974                if ($result['id_product'] == $product['id_product']
2975                    && (
2976                        !$result['id_product_attribute']
2977                        || $result['id_product_attribute'] == $product['id_product_attribute']
2978                    )
2979                ) {
2980                    return $product;
2981                }
2982            }
2983        }
2984
2985        return false;
2986    }
2987
2988    /**
2989     * Return cart products quantity
2990     *
2991     * @result  integer Products quantity
2992     *
2993     * @return int
2994     *
2995     * @since   1.0.0
2996     * @version 1.0.0 Initial version
2997     * @throws PrestaShopException
2998     */
2999    public function nbProducts()
3000    {
3001        if (!$this->id) {
3002            return 0;
3003        }
3004
3005        return static::getNbProducts($this->id);
3006    }
3007
3008    /**
3009     * @param int $id
3010     *
3011     * @return mixed
3012     *
3013     * @since   1.0.0
3014     * @version 1.0.0 Initial version
3015     * @throws PrestaShopException
3016     */
3017    public static function getNbProducts($id)
3018    {
3019        // Must be strictly compared to NULL, or else an empty cart will bypass the cache and add dozens of queries
3020        if (isset(static::$_nbProducts[$id]) && static::$_nbProducts[$id] !== null) {
3021            return static::$_nbProducts[$id];
3022        }
3023
3024        static::$_nbProducts[$id] = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
3025            (new DbQuery())
3026                ->select('SUM(`quantity`)')
3027                ->from('cart_product')
3028                ->where('`id_cart` = '.(int) $id)
3029        );
3030
3031        return static::$_nbProducts[$id];
3032    }
3033
3034    /**
3035     * @deprecated 1.0.0, use Cart->addCartRule()
3036     *
3037     * @param int $idCartRule
3038     *
3039     * @return bool
3040     * @throws PrestaShopException
3041     */
3042    public function addDiscount($idCartRule)
3043    {
3044        Tools::displayAsDeprecated();
3045
3046        return $this->addCartRule($idCartRule);
3047    }
3048
3049    /**
3050     * @param int $idCartRule
3051     *
3052     * @return bool
3053     *
3054     * @since   1.0.0
3055     * @version 1.0.0 Initial version
3056     * @throws PrestaShopException
3057     */
3058    public function addCartRule($idCartRule)
3059    {
3060        // You can't add a cart rule that does not exist
3061        $cartRule = new CartRule($idCartRule, Context::getContext()->language->id);
3062
3063        if (!Validate::isLoadedObject($cartRule)) {
3064            return false;
3065        }
3066
3067        if (Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
3068            (new DbQuery())
3069                ->select('`id_cart_rule`')
3070                ->from('cart_cart_rule')
3071                ->where('`id_cart_rule` = '.(int) $idCartRule)
3072                ->where('`id_cart` = '.(int) $this->id)
3073        )) {
3074            return false;
3075        }
3076
3077        // Add the cart rule to the cart
3078        if (!Db::getInstance()->insert(
3079            'cart_cart_rule',
3080            [
3081                'id_cart_rule' => (int) $idCartRule,
3082                'id_cart'      => (int) $this->id,
3083            ]
3084        )
3085        ) {
3086            return false;
3087        }
3088
3089        Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_ALL);
3090        Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_SHIPPING);
3091        Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_REDUCTION);
3092        Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_GIFT);
3093
3094        Cache::clean('static::getOrderedCartRulesIds_'.$this->id.'-'.CartRule::FILTER_ACTION_ALL.'-ids');
3095        Cache::clean('static::getOrderedCartRulesIds_'.$this->id.'-'.CartRule::FILTER_ACTION_SHIPPING.'-ids');
3096        Cache::clean('static::getOrderedCartRulesIds_'.$this->id.'-'.CartRule::FILTER_ACTION_REDUCTION.'-ids');
3097        Cache::clean('static::getOrderedCartRulesIds_'.$this->id.'-'.CartRule::FILTER_ACTION_GIFT.'-ids');
3098
3099        if ((int) $cartRule->gift_product) {
3100            $this->updateQty(1, $cartRule->gift_product, $cartRule->gift_product_attribute, false, 'up', 0, null, false);
3101        }
3102
3103        return true;
3104    }
3105
3106    /**
3107     * Update product quantity
3108     *
3109     * @param int      $quantity           Quantity to add (or substract)
3110     * @param int      $idProduct          Product ID
3111     * @param int      $idProductAttribute Attribute ID if needed
3112     * @param int|bool $idCustomization
3113     * @param string   $operator           Indicate if quantity must be increased or decreased
3114     * @param int      $idAddressDelivery
3115     * @param Shop     $shop
3116     * @param bool     $autoAddCartRule
3117     *
3118     * @return bool
3119     * @throws PrestaShopDatabaseException
3120     * @throws PrestaShopException
3121     */
3122    public function updateQty(
3123        $quantity,
3124        $idProduct,
3125        $idProductAttribute = null,
3126        $idCustomization = false,
3127        $operator = 'up',
3128        $idAddressDelivery = 0,
3129        Shop $shop = null,
3130        $autoAddCartRule = true
3131    ) {
3132        if (!$shop) {
3133            $shop = Context::getContext()->shop;
3134        }
3135
3136        if (Context::getContext()->customer->id) {
3137            if ($idAddressDelivery == 0 && (int) $this->id_address_delivery) { // The $id_address_delivery is null, use the cart delivery address
3138                $idAddressDelivery = $this->id_address_delivery;
3139            } elseif ($idAddressDelivery == 0) { // The $id_address_delivery is null, get the default customer address
3140                $idAddressDelivery = (int) Address::getFirstCustomerAddressId((int) Context::getContext()->customer->id);
3141            } elseif (!Customer::customerHasAddress(Context::getContext()->customer->id, $idAddressDelivery)) { // The $id_address_delivery must be linked with customer
3142                $idAddressDelivery = 0;
3143            }
3144        }
3145
3146        $quantity = (int) $quantity;
3147        $idProduct = (int) $idProduct;
3148        $idProductAttribute = (int) $idProductAttribute;
3149        $product = new Product($idProduct, false, Configuration::get('PS_LANG_DEFAULT'), $shop->id);
3150
3151        if ($idProductAttribute) {
3152            $combination = new Combination((int) $idProductAttribute);
3153            if ($combination->id_product != $idProduct) {
3154                return false;
3155            }
3156        }
3157
3158        /* If we have a product combination, the minimal quantity is set with the one of this combination */
3159        if (!empty($idProductAttribute)) {
3160            $minimalQuantity = (int) Attribute::getAttributeMinimalQty($idProductAttribute);
3161        } else {
3162            $minimalQuantity = (int) $product->minimal_quantity;
3163        }
3164
3165        if (!Validate::isLoadedObject($product)) {
3166            die(Tools::displayError());
3167        }
3168
3169        if (isset(static::$_nbProducts[$this->id])) {
3170            unset(static::$_nbProducts[$this->id]);
3171        }
3172
3173        if (isset(static::$_totalWeight[$this->id])) {
3174            unset(static::$_totalWeight[$this->id]);
3175        }
3176
3177        Hook::exec(
3178            'actionBeforeCartUpdateQty',
3179            [
3180                'cart'                 => $this,
3181                'product'              => $product,
3182                'id_product_attribute' => $idProductAttribute,
3183                'id_customization'     => $idCustomization,
3184                'quantity'             => $quantity,
3185                'operator'             => $operator,
3186                'id_address_delivery'  => $idAddressDelivery,
3187                'shop'                 => $shop,
3188                'auto_add_cart_rule'   => $autoAddCartRule,
3189            ]
3190        );
3191
3192        if ((int) $quantity <= 0) {
3193            return $this->deleteProduct($idProduct, $idProductAttribute, (int) $idCustomization, 0, $autoAddCartRule);
3194        } elseif (!$product->available_for_order || (Configuration::get('PS_CATALOG_MODE') && !defined('_PS_ADMIN_DIR_'))) {
3195            return false;
3196        } else {
3197            /* Check if the product is already in the cart */
3198            $result = $this->containsProduct($idProduct, $idProductAttribute, (int) $idCustomization, (int) $idAddressDelivery);
3199
3200            /* Update quantity if product already exist */
3201            if ($result) {
3202                if ($operator == 'up') {
3203                    $result2 = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow(
3204                        (new DbQuery())
3205                            ->select('stock.`out_of_stock`, IFNULL(stock.`quantity`, 0) AS `quantity`')
3206                            ->from('product', 'p')
3207                            ->join(Product::sqlStock('p', $idProductAttribute, true, $shop))
3208                            ->where('p.`id_product` = '.(int) $idProduct)
3209                    );
3210                    $productQty = (int) $result2['quantity'];
3211                    // Quantity for product pack
3212                    if (Pack::isPack($idProduct)) {
3213                        $productQty = Pack::getQuantity($idProduct, $idProductAttribute);
3214                    }
3215                    $newQty = (int) $result['quantity'] + (int) $quantity;
3216                    $qty = '+ '.(int) $quantity;
3217
3218                    if (!Product::isAvailableWhenOutOfStock((int) $result2['out_of_stock'])) {
3219                        if ($newQty > $productQty) {
3220                            return false;
3221                        }
3222                    }
3223                } elseif ($operator == 'down') {
3224                    $qty = '- '.(int) $quantity;
3225                    $newQty = (int) $result['quantity'] - (int) $quantity;
3226                    if ($newQty < $minimalQuantity && $minimalQuantity > 1) {
3227                        return -1;
3228                    }
3229                } else {
3230                    return false;
3231                }
3232
3233                /* Delete product from cart */
3234                if ($newQty <= 0) {
3235                    return $this->deleteProduct((int) $idProduct, (int) $idProductAttribute, (int) $idCustomization, 0, $autoAddCartRule);
3236                } elseif ($newQty < $minimalQuantity) {
3237                    return -1;
3238                } else {
3239                    Db::getInstance()->update(
3240                        'cart_product',
3241                        [
3242                            'quantity' => ['type' => 'sql', 'value' => '`quantity` '.$qty],
3243                            'date_add' => ['type' => 'sql', 'value' => 'NOW()'],
3244                        ],
3245                        '`id_product` = '.(int) $idProduct.(!empty($idProductAttribute) ? ' AND `id_product_attribute` = '.(int) $idProductAttribute : '').' AND `id_cart` = '.(int) $this->id.(Configuration::get('PS_ALLOW_MULTISHIPPING') && $this->isMultiAddressDelivery() ? ' AND `id_address_delivery` = '.(int) $idAddressDelivery : ''),
3246                        1
3247                    );
3248                }
3249            } elseif ($operator == 'up') {
3250                /* Add product to the cart */
3251                $result2 = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow(
3252                    (new DbQuery())
3253                        ->select('stock.`out_of_stock`, IFNULL(stock.`quantity`, 0) AS `quantity`')
3254                        ->from('product', 'p')
3255                        ->join(Product::sqlStock('p', $idProductAttribute, true, $shop))
3256                        ->where('p.`id_product` = '.(int) $idProduct)
3257                );
3258
3259                // Quantity for product pack
3260                if (Pack::isPack($idProduct)) {
3261                    $result2['quantity'] = Pack::getQuantity($idProduct, $idProductAttribute);
3262                }
3263
3264                if (!Product::isAvailableWhenOutOfStock((int) $result2['out_of_stock'])) {
3265                    if ((int) $quantity > $result2['quantity']) {
3266                        return false;
3267                    }
3268                }
3269
3270                if ((int) $quantity < $minimalQuantity) {
3271                    return -1;
3272                }
3273
3274                $resultAdd = Db::getInstance()->insert(
3275                    'cart_product',
3276                    [
3277                        'id_product'           => (int) $idProduct,
3278                        'id_product_attribute' => (int) $idProductAttribute,
3279                        'id_cart'              => (int) $this->id,
3280                        'id_address_delivery'  => (int) $idAddressDelivery,
3281                        'id_shop'              => $shop->id,
3282                        'quantity'             => (int) $quantity,
3283                        'date_add'             => date('Y-m-d H:i:s'),
3284                    ]
3285                );
3286
3287                if (!$resultAdd) {
3288                    return false;
3289                }
3290            }
3291        }
3292
3293        // refresh cache of static::_products
3294        $this->_products = $this->getProducts(true);
3295        $this->update();
3296        $context = Context::getContext()->cloneContext();
3297        $context->cart = $this;
3298        Cache::clean('getContextualValue_*');
3299        if ($autoAddCartRule) {
3300            CartRule::autoAddToCart($context);
3301        }
3302
3303        if ($product->customizable) {
3304            return $this->_updateCustomizationQuantity((int) $quantity, (int) $idCustomization, (int) $idProduct, (int) $idProductAttribute, (int) $idAddressDelivery, $operator);
3305        } else {
3306            return true;
3307        }
3308    }
3309
3310    /**
3311     * Delete a product from the cart
3312     *
3313     * @param int  $idProduct          Product ID
3314     * @param int  $idProductAttribute Attribute ID if needed
3315     * @param int  $idCustomization    Customization id
3316     * @param int  $idAddressDelivery
3317     * @param bool $autoAddCartRule
3318     *
3319     * @return bool result
3320     * @throws PrestaShopException
3321     * @throws PrestaShopException
3322     * @throws PrestaShopDatabaseException
3323     */
3324    public function deleteProduct($idProduct, $idProductAttribute = null, $idCustomization = null, $idAddressDelivery = 0, $autoAddCartRule = true)
3325    {
3326        if (isset(static::$_nbProducts[$this->id])) {
3327            unset(static::$_nbProducts[$this->id]);
3328        }
3329
3330        if (isset(static::$_totalWeight[$this->id])) {
3331            unset(static::$_totalWeight[$this->id]);
3332        }
3333
3334        if ((int) $idCustomization) {
3335            $productTotalQuantity = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
3336                (new DbQuery())
3337                    ->select('`quantity`')
3338                    ->from('cart_product')
3339                    ->where('`id_cart` = '.(int) $this->id)
3340                    ->where('`id_product` = '.(int) $idProduct)
3341                    ->where('`id_product_attribute` = '.(int) $idProductAttribute)
3342            );
3343
3344            $customizationQuantity = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
3345                (new DbQuery())
3346                    ->select('`quantity`')
3347                    ->from('customization')
3348                    ->where('`id_cart` = '.(int) $this->id)
3349                    ->where('`id_product` = '.(int) $idProduct)
3350                    ->where('`id_product_attribute` = '.(int) $idProductAttribute)
3351                    ->where($idAddressDelivery ? '`id_address_delivery` = '.(int) $idAddressDelivery : '')
3352            );
3353
3354            if (!$this->_deleteCustomization((int) $idCustomization, (int) $idProduct, (int) $idProductAttribute, (int) $idAddressDelivery)) {
3355                return false;
3356            }
3357
3358            // refresh cache of static::_products
3359            $this->_products = $this->getProducts(true);
3360
3361            return ($customizationQuantity == $productTotalQuantity && $this->deleteProduct((int) $idProduct, (int) $idProductAttribute, null, (int) $idAddressDelivery));
3362        }
3363
3364        /* Get customization quantity */
3365        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow(
3366            (new DbQuery())
3367                ->select('SUM(`quantity`)')
3368                ->from('customization')
3369                ->where('`id_cart` = '.(int) $this->id)
3370                ->where('`id_product` = '.(int) $idProduct)
3371                ->where('`id_product_attribute` = '.(int) $idProductAttribute)
3372        );
3373
3374        if ($result === false) {
3375            return false;
3376        }
3377
3378        /* If the product still possesses customization it does not have to be deleted */
3379        if (Db::getInstance()->NumRows() && isset($result['quantity']) && (int) $result['quantity']) {
3380            return Db::getInstance()->update(
3381                'cart_product',
3382                [
3383                    'quantity' => (int) $result['quantity'],
3384                ],
3385                '`id_cart` = '.(int) $this->id.' AND `id_product` = '.(int) $idProduct.($idProductAttribute != null ? ' AND `id_product_attribute` = '.(int) $idProductAttribute : '')
3386            );
3387        }
3388
3389        /* Product deletion */
3390        $result = Db::getInstance()->delete(
3391            'cart_product',
3392            '`id_product` = '.(int) $idProduct.' '.(!is_null($idProductAttribute) ? ' AND `id_product_attribute` = '.(int) $idProductAttribute : '').' AND `id_cart` = '.(int) $this->id.' '.((int) $idAddressDelivery ? 'AND `id_address_delivery` = '.(int) $idAddressDelivery : '')
3393        );
3394
3395        // Remove any specific price for this cart/product combination
3396        SpecificPrice::deleteByIdCart((int) $this->id, (int) $idProduct, (int) $idProductAttribute);
3397
3398        if ($result) {
3399            $return = $this->update();
3400            // refresh cache of static::_products
3401            $this->_products = $this->getProducts(true);
3402            CartRule::autoRemoveFromCart();
3403            if ($autoAddCartRule) {
3404                CartRule::autoAddToCart();
3405            }
3406
3407            return $return;
3408        }
3409
3410        return false;
3411    }
3412
3413    /**
3414     * Delete a customization from the cart. If customization is a Picture,
3415     * then the image is also deleted
3416     *
3417     * @param int $idCustomization
3418     *
3419     * @return bool result
3420     *
3421     * @deprecated 2.0.0
3422     * @throws PrestaShopDatabaseException
3423     * @throws PrestaShopDatabaseException
3424     * @throws PrestaShopException
3425     */
3426    // @codingStandardsIgnoreStart
3427    protected function _deleteCustomization($idCustomization, $idProduct, $idProductAttribute, $idAddressDelivery = 0)
3428    {
3429        // @codingStandardsIgnoreEnd
3430        $result = true;
3431        $customization = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow(
3432            (new DbQuery())
3433                ->select('*')
3434                ->from('customization')
3435                ->where('`id_customization` = '.(int) $idCustomization)
3436        );
3437
3438        if ($customization) {
3439            $custData = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow(
3440                (new DbQuery())
3441                    ->select('*')
3442                    ->from('customized_data')
3443                    ->where('`id_customization` = '.(int) $idCustomization)
3444            );
3445
3446            // Delete customization picture if necessary
3447            if (isset($custData['type']) && $custData['type'] == 0) {
3448                $result &= (@unlink(_PS_UPLOAD_DIR_.$custData['value']) && @unlink(_PS_UPLOAD_DIR_.$custData['value'].'_small'));
3449            }
3450
3451            $result &= Db::getInstance()->delete('customized_data', '`id_customization` = '.(int) $idCustomization);
3452
3453            if ($result) {
3454                $result &= Db::getInstance()->update(
3455                    'cart_product',
3456                    [
3457                        'quantity' => ['type' => 'sql', 'value' => '`quantity` - '.(int) $customization['quantity']],
3458                    ],
3459                    '`id_cart` = '.(int) $this->id.' AND `id_product` = '.(int) $idProduct.((int) $idProductAttribute ? ' AND `id_product_attribute` = '.(int) $idProductAttribute : '').' AND `id_address_delivery` = '.(int) $idAddressDelivery
3460                );
3461            }
3462
3463            if (!$result) {
3464                return false;
3465            }
3466
3467            return Db::getInstance()->delete('customization', '`id_customization` = '.(int) $idCustomization);
3468        }
3469
3470        return true;
3471    }
3472
3473    /**
3474     * @param int $idProduct
3475     * @param int $idProductAttribute
3476     * @param int $idCustomization
3477     * @param int $idAddressDelivery
3478     *
3479     * @return array|bool|null|object
3480     *
3481     * @throws PrestaShopDatabaseException
3482     * @throws PrestaShopException
3483     * @since   1.0.0
3484     * @version 1.0.0 Initial version
3485     */
3486    public function containsProduct($idProduct, $idProductAttribute = 0, $idCustomization = 0, $idAddressDelivery = 0)
3487    {
3488        $sql = (new DbQuery())
3489            ->select('cp.`quantity`')
3490            ->from('cart_product', 'cp');
3491
3492        if ($idCustomization) {
3493            $sql->leftJoin('customization', 'c', 'c.`id_product` = cp.`id_product`');
3494            $sql->where('c.`id_product_attribute` = cp.`id_product_attribute`');
3495        }
3496
3497        $sql->where('cp.`id_product` = '.(int) $idProduct);
3498        $sql->where('cp.`id_product_attribute` = '.(int) $idProductAttribute);
3499        $sql->where('cp.`id_cart` = '.(int) $this->id);
3500        if (Configuration::get('PS_ALLOW_MULTISHIPPING') && $this->isMultiAddressDelivery()) {
3501            $sql->where('cp.`id_address_delivery` = '.(int) $idAddressDelivery);
3502        }
3503
3504        if ($idCustomization) {
3505            $sql->where('c.`id_customization` = '.(int) $idCustomization);
3506        }
3507
3508        return Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql);
3509    }
3510
3511    /**
3512     * @param int    $quantityChange     Quantity change
3513     * @param int    $idCustomization    Customization ID
3514     * @param int    $idProduct          Product ID
3515     * @param int    $idProductAttribute Product Attribute ID
3516     * @param int    $idAddressDelivery  Address ID
3517     * @param string $operator           `up` or `down`
3518     *
3519     * @return bool
3520     *
3521     * @deprecated 2.0.0
3522     * @throws PrestaShopException
3523     * @throws PrestaShopDatabaseException
3524     */
3525    protected function _updateCustomizationQuantity($quantityChange, $idCustomization, $idProduct, $idProductAttribute, $idAddressDelivery, $operator = 'up')
3526    {
3527        // Link customization to product combination when it is first added to cart
3528        if (empty($idCustomization) && $operator === 'up') {
3529            $customization = $this->getProductCustomization($idProduct, null, true);
3530            foreach ($customization as $field) {
3531                if ((int) $field['quantity'] === 0) {
3532                    Db::getInstance()->update(
3533                        'customization',
3534                        [
3535                            'quantity'             => (int) $quantityChange,
3536                            'id_product'           => (int) $idProduct,
3537                            'id_product_attribute' => (int) $idProductAttribute,
3538                            'id_address_delivery'  => (int) $idAddressDelivery,
3539                            'in_cart'              => true,
3540                        ],
3541                        '`id_customization` = '.(int) $field['id_customization']
3542                    );
3543                }
3544            }
3545        }
3546
3547        /* Quantity update */
3548        if (!empty($idCustomization)) {
3549            $result = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
3550                (new DbQuery())
3551                    ->select('`quantity`')
3552                    ->from('customization')
3553                    ->where('`id_customization` = '.(int) $idCustomization)
3554            );
3555
3556            if ($operator === 'down' && ((int) $result - (int) $quantityChange) < 1) {
3557                return Db::getInstance()->delete('customization', '`id_customization` = '.(int) $idCustomization);
3558            }
3559
3560            return Db::getInstance()->update(
3561                'customization',
3562                [
3563                    'quantity'            => ['type' => 'sql', 'value' => '`quantity` '.($operator === 'up' ? '+' : '-').(int) $quantityChange],
3564                    'id_address_delivery' => (int) $idAddressDelivery,
3565                    'in_cart'             => true,
3566                ],
3567                '`id_customization` = '.(int) $idCustomization
3568            );
3569        }
3570        // refresh cache of static::_products
3571        $this->_products = $this->getProducts(true);
3572        $this->update();
3573
3574        return true;
3575    }
3576
3577    /**
3578     * Return custom pictures in this cart for a specified product
3579     *
3580     * @param int  $idProduct
3581     * @param int  $type      only return customization of this type
3582     * @param bool $notInCart only return customizations that are not in cart already
3583     *
3584     * @return array result rows
3585     *
3586     * @throws PrestaShopDatabaseException
3587     * @throws PrestaShopException
3588     * @since   1.0.0
3589     * @version 1.0.0 Initial version
3590     */
3591    public function getProductCustomization($idProduct, $type = null, $notInCart = false)
3592    {
3593        if (!Customization::isFeatureActive()) {
3594            return [];
3595        }
3596
3597        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
3598            (new DbQuery())
3599                ->select('cu.`id_customization`, cd.`index`, cd.`value`, cd.`type`, cu.`in_cart`, cu.`quantity`')
3600                ->from('customization', 'cu')
3601                ->leftJoin('customized_data', 'cd', 'cu.`id_customization` = cd.`id_customization`')
3602                ->where('cu.`id_cart` = '.(int) $this->id)
3603                ->where('cu.`id_product` = '.(int) $idProduct)
3604                ->where($type === Product::CUSTOMIZE_FILE ? 'cd.`type` = '.(int) Product::CUSTOMIZE_FILE : '')
3605                ->where($type === Product::CUSTOMIZE_TEXTFIELD ? 'cd.`type` = '.(int) Product::CUSTOMIZE_TEXTFIELD : '')
3606                ->where($notInCart ? 'cu.`in_cart` = 0' : '')
3607        );
3608
3609        return $result;
3610    }
3611
3612    /**
3613     * @deprecated 1.0.0, use Cart->removeCartRule()
3614     *
3615     * @param int $idCartRule
3616     *
3617     * @return bool
3618     * @throws PrestaShopDatabaseException
3619     * @throws PrestaShopException
3620     */
3621    public function deleteDiscount($idCartRule)
3622    {
3623        Tools::displayAsDeprecated();
3624
3625        return $this->removeCartRule($idCartRule);
3626    }
3627
3628    /**
3629     * @param int $idCartRule
3630     *
3631     * @return bool
3632     *
3633     * @since   1.0.0
3634     * @version 1.0.0 Initial version
3635     * @throws PrestaShopDatabaseException
3636     * @throws PrestaShopException
3637     */
3638    public function removeCartRule($idCartRule)
3639    {
3640        Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_ALL);
3641        Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_SHIPPING);
3642        Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_REDUCTION);
3643        Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_GIFT);
3644
3645        Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_ALL.'-ids');
3646        Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_SHIPPING.'-ids');
3647        Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_REDUCTION.'-ids');
3648        Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_GIFT.'-ids');
3649
3650        $result = Db::getInstance()->delete('cart_cart_rule', '`id_cart_rule` = '.(int) $idCartRule.' AND `id_cart` = '.(int) $this->id, 1);
3651
3652        $cartRule = new CartRule($idCartRule, Configuration::get('PS_LANG_DEFAULT'));
3653        if ((int) $cartRule->gift_product) {
3654            $this->updateQty(1, $cartRule->gift_product, $cartRule->gift_product_attribute, null, 'down', 0, null, false);
3655        }
3656
3657        return $result;
3658    }
3659
3660    /**
3661     * Get the number of packages
3662     *
3663     * @return int number of packages
3664     * @throws PrestaShopDatabaseException
3665     * @throws PrestaShopException
3666     */
3667    public function getNbOfPackages()
3668    {
3669        static $nbPackages = [];
3670
3671        if (!isset($nbPackages[$this->id])) {
3672            $nbPackages[$this->id] = 0;
3673            foreach ($this->getPackageList() as $byAddress) {
3674                $nbPackages[$this->id] += count($byAddress);
3675            }
3676        }
3677
3678        return $nbPackages[$this->id];
3679    }
3680
3681    /**
3682     * @param array    $package
3683     * @param int|null $idCarrier
3684     *
3685     * @return int
3686     *
3687     * @throws PrestaShopDatabaseException
3688     * @throws PrestaShopException
3689     * @since   1.0.0
3690     * @version 1.0.0 Initial version
3691     */
3692    public function getPackageIdWarehouse($package, $idCarrier = null)
3693    {
3694        if ($idCarrier === null) {
3695            if (isset($package['id_carrier'])) {
3696                $idCarrier = (int) $package['id_carrier'];
3697            }
3698        }
3699
3700        if ($idCarrier == null) {
3701            return $package['id_warehouse'];
3702        }
3703
3704        foreach ($package['warehouse_list'] as $idWarehouse) {
3705            $warehouse = new Warehouse((int) $idWarehouse);
3706            $availableWarehouseCarriers = $warehouse->getCarriers();
3707            if (in_array($idCarrier, $availableWarehouseCarriers)) {
3708                return (int) $idWarehouse;
3709            }
3710        }
3711
3712        return 0;
3713    }
3714
3715    /**
3716     * @param int $idCarrier
3717     * @param int $idAddress
3718     *
3719     * @return bool
3720     * @throws Adapter_Exception
3721     * @throws PrestaShopDatabaseException
3722     * @throws PrestaShopException
3723     */
3724    public function carrierIsSelected($idCarrier, $idAddress)
3725    {
3726        $deliveryOption = $this->getDeliveryOption();
3727        $deliveryOptionList = $this->getDeliveryOptionList();
3728
3729        if (!isset($deliveryOption[$idAddress])) {
3730            return false;
3731        }
3732
3733        if (!isset($deliveryOptionList[$idAddress][$deliveryOption[$idAddress]])) {
3734            return false;
3735        }
3736
3737        if (!in_array($idCarrier, array_keys($deliveryOptionList[$idAddress][$deliveryOption[$idAddress]]['carrier_list']))) {
3738            return false;
3739        }
3740
3741        return true;
3742    }
3743
3744    /**
3745     * Get all deliveries options available for the current cart formated like Carriers::getCarriersForOrder
3746     * This method was wrote for retrocompatibility with 1.4 theme
3747     * New theme need to use static::getDeliveryOptionList() to generate carriers option in the checkout process
3748     *
3749     * @param Country $defaultCountry
3750     * @param bool    $flush Force flushing cache
3751     *
3752     * @return array
3753     *
3754     * @throws Adapter_Exception
3755     * @throws PrestaShopDatabaseException
3756     * @throws PrestaShopException
3757     * @since   1.0.0
3758     * @version 1.0.0 Initial version
3759     */
3760    public function simulateCarriersOutput(Country $defaultCountry = null, $flush = false)
3761    {
3762        $deliveryOptionList = $this->getDeliveryOptionList($defaultCountry, $flush);
3763
3764        // This method cannot work if there is multiple address delivery
3765        if (count($deliveryOptionList) > 1 || empty($deliveryOptionList)) {
3766            return [];
3767        }
3768
3769        $carriers = [];
3770        foreach (reset($deliveryOptionList) as $key => $option) {
3771            $price = $option['total_price_with_tax'];
3772            $priceTaxExcluded = $option['total_price_without_tax'];
3773            $name = $img = $delay = '';
3774
3775            if ($option['unique_carrier']) {
3776                $carrier = reset($option['carrier_list']);
3777                if (isset($carrier['instance'])) {
3778                    $name = $carrier['instance']->name;
3779                    $delay = $carrier['instance']->delay;
3780                    $delay = isset($delay[Context::getContext()->language->id]) ?
3781                        $delay[Context::getContext()->language->id] : $delay[(int) Configuration::get('PS_LANG_DEFAULT')];
3782                }
3783                if (isset($carrier['logo'])) {
3784                    $img = $carrier['logo'];
3785                }
3786            } else {
3787                $nameList = [];
3788                foreach ($option['carrier_list'] as $carrier) {
3789                    $nameList[] = $carrier['instance']->name;
3790                }
3791                $name = join(' -', $nameList);
3792                $img = ''; // No images if multiple carriers
3793                $delay = '';
3794            }
3795            $carriers[] = [
3796                'name'          => $name,
3797                'img'           => $img,
3798                'delay'         => $delay,
3799                'price'         => $price,
3800                'price_tax_exc' => $priceTaxExcluded,
3801                'id_carrier'    => static::intifier($key), // Need to translate to an integer for retrocompatibility reason, in 1.4 template we used intval
3802                'is_module'     => false,
3803            ];
3804        }
3805
3806        return $carriers;
3807    }
3808
3809    /**
3810     * Translate a string option_delivery identifier ('24,3,') in a int (3240002000)
3811     *
3812     * The  option_delivery identifier is a list of integers separated by a ','.
3813     * This method replace the delimiter by a sequence of '0'.
3814     * The size of this sequence is fixed by the first digit of the return
3815     *
3816     * @param string $string
3817     * @param string $delimiter
3818     *
3819     * @return int
3820     *
3821     * @since   1.0.0
3822     * @version 1.0.0 Initial version
3823     */
3824    public static function intifier($string, $delimiter = ',')
3825    {
3826        $elm = explode($delimiter, $string);
3827        $max = max($elm);
3828
3829        return strlen($max).implode(str_repeat('0', strlen($max) + 1), $elm);
3830    }
3831
3832    /**
3833     * @param bool $useCache
3834     *
3835     * @return int
3836     *
3837     * @throws PrestaShopDatabaseException
3838     * @throws PrestaShopException
3839     * @since   1.0.0
3840     * @version 1.0.0 Initial version
3841     */
3842    public function simulateCarrierSelectedOutput($useCache = true)
3843    {
3844        $deliveryOption = $this->getDeliveryOption(null, false, $useCache);
3845
3846        if (count($deliveryOption) > 1 || empty($deliveryOption)) {
3847            return 0;
3848        }
3849
3850        return static::intifier(reset($deliveryOption));
3851    }
3852
3853    /**
3854     * Return shipping total of a specific carriers for the cart
3855     *
3856     * @param int          $idCarrier
3857     * @param bool         $useTax
3858     * @param Country|null $defaultCountry
3859     * @param array|null   $deliveryOption Array of the delivery option for each address
3860     *
3861     * @return float Shipping total
3862     *
3863     * @throws Adapter_Exception
3864     * @throws PrestaShopDatabaseException
3865     * @throws PrestaShopException
3866     * @since   1.0.0
3867     * @version 1.0.0 Initial version
3868     */
3869    public function getCarrierCost($idCarrier, $useTax = true, Country $defaultCountry = null, $deliveryOption = null)
3870    {
3871        if (is_null($deliveryOption)) {
3872            $deliveryOption = $this->getDeliveryOption($defaultCountry);
3873        }
3874
3875        $totalShipping = 0;
3876        $deliveryOptionList = $this->getDeliveryOptionList();
3877
3878        foreach ($deliveryOption as $idAddress => $key) {
3879            if (!isset($deliveryOptionList[$idAddress]) || !isset($deliveryOptionList[$idAddress][$key])) {
3880                continue;
3881            }
3882            if (isset($deliveryOptionList[$idAddress][$key]['carrier_list'][$idCarrier])) {
3883                if ($useTax) {
3884                    $totalShipping += $deliveryOptionList[$idAddress][$key]['carrier_list'][$idCarrier]['price_with_tax'];
3885                } else {
3886                    $totalShipping += $deliveryOptionList[$idAddress][$key]['carrier_list'][$idCarrier]['price_without_tax'];
3887                }
3888            }
3889        }
3890
3891        return $totalShipping;
3892    }
3893
3894    /**
3895     * @deprecated 1.0.0, use static::getPackageShippingCost
3896     *
3897     * @param int|null     $idCarrier
3898     * @param bool         $useTax
3899     * @param Country|null $defaultCountry
3900     * @param array|null   $productList
3901     *
3902     * @return bool|float
3903     * @throws Adapter_Exception
3904     * @throws PrestaShopException
3905     */
3906    public function getOrderShippingCost($idCarrier = null, $useTax = true, Country $defaultCountry = null, $productList = null)
3907    {
3908        Tools::displayAsDeprecated();
3909
3910        return $this->getPackageShippingCost((int) $idCarrier, $useTax, $defaultCountry, $productList);
3911    }
3912
3913    /**
3914     * @deprecated 1.0.0
3915     *
3916     * @param CartRule $obj
3917     * @param mixed    $discounts
3918     * @param mixed    $orderTotal
3919     * @param mixed    $products
3920     * @param bool     $checkCartDiscount
3921     *
3922     * @return bool|string
3923     * @throws PrestaShopException
3924     */
3925    public function checkDiscountValidity($obj, $discounts, $orderTotal, $products, $checkCartDiscount = false)
3926    {
3927        Tools::displayAsDeprecated();
3928        $context = Context::getContext()->cloneContext();
3929        $context->cart = $this;
3930
3931        return $obj->checkValidity($context);
3932    }
3933
3934    /**
3935     * Return useful informations for cart
3936     *
3937     * @param int|null $idLang
3938     * @param bool     $refresh
3939     *
3940     * @return array Cart details
3941     *
3942     * @throws Adapter_Exception
3943     * @throws PrestaShopDatabaseException
3944     * @throws PrestaShopException
3945     * @since   1.0.0
3946     * @version 1.0.0 Initial version
3947     */
3948    public function getSummaryDetails($idLang = null, $refresh = false)
3949    {
3950        $context = Context::getContext();
3951        if (!$idLang) {
3952            $idLang = $context->language->id;
3953        }
3954
3955        $delivery = new Address((int) $this->id_address_delivery);
3956        $invoice = new Address((int) $this->id_address_invoice);
3957
3958        // New layout system with personalization fields
3959        $formattedAddresses = [
3960            'delivery' => AddressFormat::getFormattedLayoutData($delivery),
3961            'invoice'  => AddressFormat::getFormattedLayoutData($invoice),
3962        ];
3963
3964        $baseTotalTaxInc = $this->getOrderTotal(true);
3965        $baseTotalTaxExc = $this->getOrderTotal(false);
3966
3967        $totalTax = $baseTotalTaxInc - $baseTotalTaxExc;
3968
3969        if ($totalTax < 0) {
3970            $totalTax = 0;
3971        }
3972
3973        $currency = new Currency($this->id_currency);
3974
3975        $products = $this->getProducts($refresh);
3976
3977        foreach ($products as $key => &$product) {
3978            $product['price_without_quantity_discount'] = Product::getPriceStatic(
3979                $product['id_product'],
3980                !Product::getTaxCalculationMethod(),
3981                $product['id_product_attribute'],
3982                _TB_PRICE_DATABASE_PRECISION_,
3983                null,
3984                false,
3985                false
3986            );
3987
3988            if ($product['reduction_type'] == 'amount') {
3989                $reduction = (!Product::getTaxCalculationMethod() ? (float) $product['price_wt'] : (float) $product['price']) - (float) $product['price_without_quantity_discount'];
3990                $product['reduction_formatted'] = Tools::displayPrice($reduction);
3991            }
3992        }
3993
3994        $giftProducts = [];
3995        $cartRules = $this->getCartRules();
3996        $totalShipping = $this->getTotalShippingCost();
3997        $totalShippingTaxExc = $this->getTotalShippingCost(null, false);
3998        $totalProductsWt = $this->getOrderTotal(true, static::ONLY_PRODUCTS);
3999        $totalProducts = $this->getOrderTotal(false, static::ONLY_PRODUCTS);
4000        $totalDiscounts = $this->getOrderTotal(true, static::ONLY_DISCOUNTS);
4001        $totalDiscountsTaxExc = $this->getOrderTotal(false, static::ONLY_DISCOUNTS);
4002
4003        // The cart content is altered for display
4004        $decimals = 0;
4005        if ($currency->decimals) {
4006            $decimals = Configuration::get('PS_PRICE_DISPLAY_PRECISION');
4007        }
4008        foreach ($cartRules as &$cartRule) {
4009            // If the cart rule is automatic (wihtout any code) and include free shipping, it should not be displayed as a cart rule but only set the shipping cost to 0
4010            if ($cartRule['free_shipping'] && (empty($cartRule['code']) || preg_match('/^'.CartRule::BO_ORDER_CODE_PREFIX.'[0-9]+/', $cartRule['code']))) {
4011                $cartRule['value_real'] -= $totalShipping;
4012                $cartRule['value_tax_exc'] -= $totalShippingTaxExc;
4013                $cartRule['value_real'] = Tools::ps_round(
4014                    $cartRule['value_real'],
4015                    $decimals
4016                );
4017                $cartRule['value_tax_exc'] = Tools::ps_round(
4018                    $cartRule['value_tax_exc'],
4019                    $decimals
4020                );
4021                if ($totalDiscounts > $cartRule['value_real']) {
4022                    $totalDiscounts -= $totalShipping;
4023                }
4024                if ($totalDiscountsTaxExc > $cartRule['value_tax_exc']) {
4025                    $totalDiscountsTaxExc -= $totalShippingTaxExc;
4026                }
4027
4028                // Update total shipping
4029                $totalShipping = 0;
4030                $totalShippingTaxExc = 0;
4031            }
4032
4033            if ($cartRule['gift_product']) {
4034                foreach ($products as $key => &$product) {
4035                    if (empty($product['gift']) && $product['id_product'] == $cartRule['gift_product'] && $product['id_product_attribute'] == $cartRule['gift_product_attribute']) {
4036                        // Update total products
4037                        $totalProductsWt = Tools::ps_round(
4038                            $totalProductsWt - $product['price_wt'],
4039                            $decimals
4040                        );
4041                        $totalProducts = Tools::ps_round(
4042                            $totalProducts - $product['price'],
4043                            $decimals
4044                        );
4045
4046                        // Update total discounts
4047                        $totalDiscounts = $totalDiscounts - $product['price_wt'];
4048                        $totalDiscountsTaxExc = $totalDiscountsTaxExc - $product['price'];
4049
4050                        // Update cart rule value
4051                        $cartRule['value_real'] = Tools::ps_round(
4052                            $cartRule['value_real'] - $product['price_wt'],
4053                            $decimals
4054                        );
4055                        $cartRule['value_tax_exc'] = Tools::ps_round(
4056                            $cartRule['value_tax_exc'] - $product['price'],
4057                            $decimals
4058                        );
4059
4060                        // Update product quantity
4061                        $product['total_wt'] = Tools::ps_round(
4062                            $product['total_wt'] - $product['price_wt'],
4063                            $decimals
4064                        );
4065                        $product['total'] = Tools::ps_round(
4066                            $product['total'] - $product['price'],
4067                            $decimals
4068                        );
4069                        $product['cart_quantity']--;
4070
4071                        if (!$product['cart_quantity']) {
4072                            unset($products[$key]);
4073                        }
4074
4075                        // Add a new product line
4076                        $giftProduct = $product;
4077                        $giftProduct['cart_quantity'] = 1;
4078                        $giftProduct['price'] = 0;
4079                        $giftProduct['price_wt'] = 0;
4080                        $giftProduct['total_wt'] = 0;
4081                        $giftProduct['total'] = 0;
4082                        $giftProduct['gift'] = true;
4083                        $giftProducts[] = $giftProduct;
4084
4085                        break; // One gift product per cart rule
4086                    }
4087                }
4088            }
4089        }
4090
4091        foreach ($cartRules as $key => &$cartRule) {
4092            if (((float) $cartRule['value_real'] == 0 && (int) $cartRule['free_shipping'] == 0)) {
4093                unset($cartRules[$key]);
4094            }
4095        }
4096
4097        $summary = [
4098            'delivery'                  => $delivery,
4099            'delivery_state'            => State::getNameById($delivery->id_state),
4100            'invoice'                   => $invoice,
4101            'invoice_state'             => State::getNameById($invoice->id_state),
4102            'formattedAddresses'        => $formattedAddresses,
4103            'products'                  => array_values($products),
4104            'gift_products'             => $giftProducts,
4105            'discounts'                 => array_values($cartRules),
4106            'is_virtual_cart'           => (int) $this->isVirtualCart(),
4107            'total_discounts'           => $totalDiscounts,
4108            'total_discounts_tax_exc'   => $totalDiscountsTaxExc,
4109            'total_wrapping'            => $this->getOrderTotal(true, static::ONLY_WRAPPING),
4110            'total_wrapping_tax_exc'    => $this->getOrderTotal(false, static::ONLY_WRAPPING),
4111            'total_shipping'            => $totalShipping,
4112            'total_shipping_tax_exc'    => $totalShippingTaxExc,
4113            'total_products_wt'         => $totalProductsWt,
4114            'total_products'            => $totalProducts,
4115            'total_price'               => $baseTotalTaxInc,
4116            'total_tax'                 => $totalTax,
4117            'total_price_without_tax'   => $baseTotalTaxExc,
4118            'is_multi_address_delivery' => $this->isMultiAddressDelivery() || ((int) Tools::getValue('multi-shipping') == 1),
4119            'free_ship'                 => !$totalShipping && !count($this->getDeliveryAddressesWithoutCarriers(true, $errors)),
4120            'carrier'                   => new Carrier($this->id_carrier, $idLang),
4121        ];
4122
4123        $hook = Hook::exec('actionCartSummary', $summary, null, true);
4124        if (is_array($hook)) {
4125            $summary = array_merge($summary, array_shift($hook));
4126        }
4127
4128        return $summary;
4129    }
4130
4131    /**
4132     * Get all the ids of the delivery addresses without carriers
4133     *
4134     * @param bool  $returnCollection Return a collection
4135     * @param array $error            contains an error message if an error occurs
4136     *
4137     * @return array Array of address id or of address object
4138     * @throws PrestaShopException
4139     */
4140    public function getDeliveryAddressesWithoutCarriers($returnCollection = false, &$error = [])
4141    {
4142        $addressesWithoutCarriers = [];
4143        foreach ($this->getProducts() as $product) {
4144            if (!in_array($product['id_address_delivery'], $addressesWithoutCarriers)
4145                && !count(Carrier::getAvailableCarrierList(new Product($product['id_product']), null, $product['id_address_delivery'], null, null, $error))
4146            ) {
4147                $addressesWithoutCarriers[] = $product['id_address_delivery'];
4148            }
4149        }
4150        if (!$returnCollection) {
4151            return $addressesWithoutCarriers;
4152        } else {
4153            $addressesInstanceWithoutCarriers = [];
4154            foreach ($addressesWithoutCarriers as $idAddress) {
4155                $addressesInstanceWithoutCarriers[] = new Address($idAddress);
4156            }
4157
4158            return $addressesInstanceWithoutCarriers;
4159        }
4160    }
4161
4162    /**
4163     * @param bool $returnProduct
4164     *
4165     * @return bool|mixed
4166     *
4167     * @throws PrestaShopDatabaseException
4168     * @throws PrestaShopException
4169     * @since   1.0.0
4170     * @version 1.0.0 Initial version
4171     */
4172    public function checkQuantities($returnProduct = false)
4173    {
4174        if (Configuration::get('PS_CATALOG_MODE') && !defined('_PS_ADMIN_DIR_')) {
4175            return false;
4176        }
4177
4178        foreach ($this->getProducts() as $product) {
4179            if (!$this->allow_seperated_package && !$product['allow_oosp'] && StockAvailable::dependsOnStock($product['id_product']) &&
4180                $product['advanced_stock_management'] && (bool) Context::getContext()->customer->isLogged() && ($delivery = $this->getDeliveryOption()) && !empty($delivery)
4181            ) {
4182                $product['stock_quantity'] = StockManager::getStockByCarrier((int) $product['id_product'], (int) $product['id_product_attribute'], $delivery);
4183            }
4184            if (!$product['active'] || !$product['available_for_order']
4185                || (!$product['allow_oosp'] && $product['stock_quantity'] < $product['cart_quantity'])
4186            ) {
4187                return $returnProduct ? $product : false;
4188            }
4189        }
4190
4191        return true;
4192    }
4193
4194    /**
4195     * @return bool
4196     *
4197     * @since   1.0.0
4198     * @version 1.0.0 Initial version
4199     * @throws PrestaShopException
4200     */
4201    public function checkProductsAccess()
4202    {
4203        if (Configuration::get('PS_CATALOG_MODE')) {
4204            return true;
4205        }
4206
4207        foreach ($this->getProducts() as $product) {
4208            if (!Product::checkAccessStatic($product['id_product'], $this->id_customer)) {
4209                return $product['id_product'];
4210            }
4211        }
4212
4213        return false;
4214    }
4215
4216    /**
4217     * Add customer's text
4218     *
4219     * @param int    $idProduct
4220     * @param int    $index
4221     * @param int    $type
4222     * @param string $textValue
4223     *
4224     * @return bool Always true
4225     *
4226     * @since   1.0.0
4227     * @version 1.0.0 Initial version
4228     * @throws PrestaShopDatabaseException
4229     */
4230    public function addTextFieldToProduct($idProduct, $index, $type, $textValue)
4231    {
4232        return $this->_addCustomization($idProduct, 0, $index, $type, $textValue, 0);
4233    }
4234
4235    /**
4236     * Add customization item to database
4237     *
4238     * @param int    $idProduct
4239     * @param int    $idProductAttribute
4240     * @param int    $index
4241     * @param int    $type
4242     * @param string $field
4243     * @param int    $quantity
4244     *
4245     * @return bool success
4246     *
4247     * @since   1.0.0
4248     * @version 1.0.0
4249     * @throws PrestaShopDatabaseException
4250     * @throws PrestaShopException
4251     */
4252    // @codingStandardsIgnoreStart
4253    public function _addCustomization($idProduct, $idProductAttribute, $index, $type, $field, $quantity)
4254    {
4255        // @codingStandardsIgnoreEnd
4256        $exisingCustomization = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
4257            (new DbQuery())
4258                ->select('cu.`id_customization`, cd.`index`, cd.`value`, cd.`type`')
4259                ->from('customization', 'cu')
4260                ->leftJoin('customized_data', 'cd', 'cu.`id_customization` = cd.`id_customization`')
4261                ->where('cu.`id_cart` = '.(int) $this->id)
4262                ->where('cu.`id_product` = '.(int) $idProduct)
4263                ->where('`in_cart` = 0')
4264        );
4265
4266        if ($exisingCustomization) {
4267            // If the customization field is already filled, delete it
4268            foreach ($exisingCustomization as $customization) {
4269                if ($customization['type'] == $type && $customization['index'] == $index) {
4270                    Db::getInstance()->delete(
4271                        'customized_data',
4272                        'id_customization = '.(int) $customization['id_customization'].' AND type = '.(int) $customization['type'].' AND `index` = '.(int) $customization['index']
4273
4274                    );
4275                    if ($type == Product::CUSTOMIZE_FILE) {
4276                        @unlink(_PS_UPLOAD_DIR_.$customization['value']);
4277                        @unlink(_PS_UPLOAD_DIR_.$customization['value'].'_small');
4278                    }
4279                    break;
4280                }
4281            }
4282            $idCustomization = $exisingCustomization[0]['id_customization'];
4283        } else {
4284            Db::getInstance()->insert(
4285                'customization',
4286                [
4287                    'id_cart'              => (int) $this->id,
4288                    'id_product'           => (int) $idProduct,
4289                    'id_product_attribute' => (int) $idProductAttribute,
4290                    'quantity'             => (int) $quantity,
4291                ]
4292            );
4293            $idCustomization = Db::getInstance()->Insert_ID();
4294        }
4295
4296        if (!Db::getInstance()->insert(
4297            'customized_data',
4298            [
4299                'id_customization' => (int) $idCustomization,
4300                'type'             => (int) $type,
4301                'index'            => (int) $index,
4302                'value'            => pSQL($field),
4303            ]
4304        )) {
4305            return false;
4306        }
4307
4308        return true;
4309    }
4310
4311    /**
4312     * Add customer's pictures
4313     *
4314     * @param int    $idProduct
4315     * @param int    $index
4316     * @param int    $type
4317     * @param string $file
4318     *
4319     * @return bool Always true
4320     *
4321     * @throws PrestaShopDatabaseException
4322     * @throws PrestaShopException
4323     * @since   1.0.0
4324     * @version 1.0.0
4325     */
4326    public function addPictureToProduct($idProduct, $index, $type, $file)
4327    {
4328        return $this->_addCustomization($idProduct, 0, $index, $type, $file, 0);
4329    }
4330
4331    /**
4332     * @deprecated 1.0.0
4333     *
4334     * @param int $idProduct
4335     * @param int $index
4336     *
4337     * @return bool
4338     * @throws PrestaShopDatabaseException
4339     */
4340    public function deletePictureToProduct($idProduct, $index)
4341    {
4342        Tools::displayAsDeprecated();
4343
4344        return $this->deleteCustomizationToProduct($idProduct, 0);
4345    }
4346
4347    /**
4348     * Remove a customer's customization
4349     *
4350     * @param int $idProduct
4351     * @param int $index
4352     *
4353     * @return bool
4354     * @throws PrestaShopDatabaseException
4355     * @throws PrestaShopException
4356     */
4357    public function deleteCustomizationToProduct($idProduct, $index)
4358    {
4359        $result = true;
4360
4361        $custData = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow(
4362            (new DbQuery())
4363                ->select('cu.`id_customization`, cd.`index`, cd.`value`, cd.`type`')
4364                ->from('customization', 'cu')
4365                ->leftJoin('customized_data', 'cd', 'cu.`id_customization` = cd.`id_customization`')
4366                ->where('cu.`id_cart` = '.(int) $this->id)
4367                ->where('cu.`id_product` = '.(int) $idProduct)
4368                ->where('`index` = '.(int) $index)
4369                ->where('`in_cart` = 0')
4370        );
4371
4372        // Delete customization picture if necessary
4373        if ($custData['type'] == 0) {
4374            $result &= (@unlink(_PS_UPLOAD_DIR_.$custData['value']) && @unlink(_PS_UPLOAD_DIR_.$custData['value'].'_small'));
4375        }
4376
4377        $result &= Db::getInstance()->delete('customized_data', '`id_customization` = '.(int) $custData['id_customization'].' AND `index` = '.(int) $index);
4378
4379        return $result;
4380    }
4381
4382    /**
4383     * @return false|array
4384     *
4385     * @throws PrestaShopDatabaseException
4386     * @throws PrestaShopException
4387     * @since   1.0.0
4388     * @version 1.0.0
4389     */
4390    public function duplicate()
4391    {
4392        if (!Validate::isLoadedObject($this)) {
4393            return false;
4394        }
4395
4396        $cart = new Cart($this->id);
4397        $cart->id = null;
4398        $cart->id_shop = $this->id_shop;
4399        $cart->id_shop_group = $this->id_shop_group;
4400
4401        if (!Customer::customerHasAddress((int) $cart->id_customer, (int) $cart->id_address_delivery)) {
4402            $cart->id_address_delivery = (int) Address::getFirstCustomerAddressId((int) $cart->id_customer);
4403        }
4404
4405        if (!Customer::customerHasAddress((int) $cart->id_customer, (int) $cart->id_address_invoice)) {
4406            $cart->id_address_invoice = (int) Address::getFirstCustomerAddressId((int) $cart->id_customer);
4407        }
4408
4409        if ($cart->id_customer) {
4410            $cart->secure_key = static::$_customer->secure_key;
4411        }
4412
4413        $cart->add();
4414
4415        if (!Validate::isLoadedObject($cart)) {
4416            return false;
4417        }
4418
4419        $success = true;
4420        $products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
4421            (new DbQuery())
4422                ->select('*')
4423                ->from('cart_product')
4424                ->where('`id_cart` = '.(int) $this->id)
4425        );
4426
4427        $productGift = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
4428            (new DbQuery())
4429                ->select('cr.`gift_product`, cr.`gift_product_attribute`')
4430                ->from('cart_rule', 'cr')
4431                ->leftJoin('order_cart_rule', 'ocr', 'ocr.`id_cart_rule` = cr.`id_cart_rule`')
4432                ->where('ocr.`id_order` = '.(int) $this->id)
4433        );
4434
4435        $idAddressDelivery = Configuration::get('PS_ALLOW_MULTISHIPPING') ? $cart->id_address_delivery : 0;
4436
4437        foreach ($products as $product) {
4438            if ($idAddressDelivery) {
4439                if (Customer::customerHasAddress((int) $cart->id_customer, $product['id_address_delivery'])) {
4440                    $idAddressDelivery = $product['id_address_delivery'];
4441                }
4442            }
4443
4444            foreach ($productGift as $gift) {
4445                if (isset($gift['gift_product']) && isset($gift['gift_product_attribute']) && (int) $gift['gift_product'] == (int) $product['id_product'] && (int) $gift['gift_product_attribute'] == (int) $product['id_product_attribute']) {
4446                    $product['quantity'] = (int) $product['quantity'] - 1;
4447                }
4448            }
4449
4450            $success &= $cart->updateQty(
4451                (int) $product['quantity'],
4452                (int) $product['id_product'],
4453                (int) $product['id_product_attribute'],
4454                null,
4455                'up',
4456                (int) $idAddressDelivery,
4457                new Shop((int) $cart->id_shop),
4458                false
4459            );
4460        }
4461
4462        // Customized products
4463        $customs = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
4464            (new DbQuery())
4465                ->select('*')
4466                ->from('customization', 'c')
4467                ->leftJoin('customized_data', 'cd', 'cd.`id_customization` = c.`id_customization`')
4468                ->where('c.`id_cart` = '.(int) $this->id)
4469        );
4470
4471        // Get datas from customization table
4472        $customsById = [];
4473        foreach ($customs as $custom) {
4474            if (!isset($customsById[$custom['id_customization']])) {
4475                $customsById[$custom['id_customization']] = [
4476                    'id_product_attribute' => $custom['id_product_attribute'],
4477                    'id_product'           => $custom['id_product'],
4478                    'quantity'             => $custom['quantity'],
4479                ];
4480            }
4481        }
4482
4483        // Insert new customizations
4484        $customIds = [];
4485        foreach ($customsById as $customizationId => $val) {
4486            Db::getInstance()->insert(
4487                'customization',
4488                [
4489                    'id_cart'              => (int) $cart->id,
4490                    'id_product_attribute' => (int) $val['id_product_attribute'],
4491                    'id_product'           => (int) $val['id_product'],
4492                    'id_address_delivery'  => (int) $idAddressDelivery,
4493                    'quantity'             => (int) $val['quantity'],
4494                    'quantity_refunded'    => 0,
4495                    'quantity_returned'    => 0,
4496                    'in_cart'              => 1,
4497                ]
4498            );
4499            $customIds[$customizationId] = Db::getInstance(_PS_USE_SQL_SLAVE_)->Insert_ID();
4500        }
4501
4502        // Insert customized_data
4503        if (count($customs)) {
4504            $insert = [];
4505            foreach ($customs as $custom) {
4506                $customizedValue = $custom['value'];
4507
4508                if ((int) $custom['type'] == 0) {
4509                    $customizedValue = md5(uniqid(rand(), true));
4510                    copy(_PS_UPLOAD_DIR_.$custom['value'], _PS_UPLOAD_DIR_.$customizedValue);
4511                    copy(_PS_UPLOAD_DIR_.$custom['value'].'_small', _PS_UPLOAD_DIR_.$customizedValue.'_small');
4512                }
4513
4514                $insert[] = [
4515                    'id_customization' => (int) $customIds[$custom['id_customization']],
4516                    'type'             => (int) $custom['type'],
4517                    'index'            => (int) $custom['index'],
4518                    'value'            => pSQL($customizedValue),
4519                ];
4520            }
4521            Db::getInstance()->insert('customized_data', $insert);
4522        }
4523
4524        return ['cart' => $cart, 'success' => $success];
4525    }
4526
4527    /**
4528     * @param bool $autoDate
4529     * @param bool $nullValues
4530     *
4531     * @return bool
4532     *
4533     * @since   1.0.0
4534     * @version 1.0.0 Initial version
4535     * @throws PrestaShopException
4536     */
4537    public function add($autoDate = true, $nullValues = false)
4538    {
4539        if (!$this->id_lang) {
4540            $this->id_lang = Configuration::get('PS_LANG_DEFAULT');
4541        }
4542        if (!$this->id_shop) {
4543            $this->id_shop = Context::getContext()->shop->id;
4544        }
4545
4546        $return = parent::add($autoDate, $nullValues);
4547        Hook::exec('actionCartSave', ['cart' => $this]);
4548
4549        return $return;
4550    }
4551
4552    /**
4553     * @return array|false|mysqli_result|null|PDOStatement|resource
4554     *
4555     * @throws PrestaShopDatabaseException
4556     * @throws PrestaShopException
4557     * @since   1.0.0
4558     * @version 1.0.0
4559     */
4560    public function getWsCartRows()
4561    {
4562        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
4563            (new DbQuery())
4564                ->select('`id_product`, `id_product_attribute`, `quantity`, `id_address_delivery`')
4565                ->from('cart_product')
4566                ->where('`id_cart` = '.(int) $this->id)
4567                ->where('`id_shop` = '.(int) Context::getContext()->shop->id)
4568        );
4569    }
4570
4571    /**
4572     * @param array $values
4573     *
4574     * @return bool
4575     *
4576     * @throws PrestaShopDatabaseException
4577     * @throws PrestaShopException
4578     * @since   1.0.0
4579     * @version 1.0.0
4580     */
4581    public function setWsCartRows($values)
4582    {
4583        if ($this->deleteAssociations()) {
4584            $insert = [];
4585            foreach ($values as $value) {
4586                $insert[] = [
4587                    'id_cart'              => (int) $this->id,
4588                    'id_product'           => (int) $value['id_product'],
4589                    'id_product_attribute' => isset($value['id_product_attribute']) ? (int) $value['id_product_attribute'] : null,
4590                    'id_address_delivery'  => isset($value['id_address_delivery']) ? (int) $value['id_address_delivery'] : 0,
4591                    'quantity'             => (int) $value['quantity'],
4592                    'date_add'             => ['type' => 'sql', 'value' => 'NOW()'],
4593                    'id_shop'              => (int) Context::getContext()->shop->id,
4594                ];
4595            }
4596
4597            Db::getInstance()->insert('cart_product', $insert);
4598        }
4599
4600        return true;
4601    }
4602
4603    /**
4604     * @return bool
4605     *
4606     * @since   1.0.0
4607     * @version 1.0.0
4608     * @throws PrestaShopDatabaseException
4609     */
4610    public function deleteAssociations()
4611    {
4612        return (bool) Db::getInstance()->delete('cart_product', '`id_cart` = '.(int) $this->id);
4613    }
4614
4615    /**
4616     * @param int $idProduct
4617     * @param int $idProductAttribute
4618     * @param int $oldIdAddressDelivery
4619     * @param int $newIdAddressDelivery
4620     *
4621     * @return bool
4622     *
4623     * @throws PrestaShopDatabaseException
4624     * @throws PrestaShopException
4625     * @since   1.0.0
4626     * @version 1.0.0
4627     */
4628    public function setProductAddressDelivery($idProduct, $idProductAttribute, $oldIdAddressDelivery, $newIdAddressDelivery)
4629    {
4630        // Check address is linked with the customer
4631        if (!Customer::customerHasAddress(Context::getContext()->customer->id, $newIdAddressDelivery)) {
4632            return false;
4633        }
4634
4635        if ($newIdAddressDelivery == $oldIdAddressDelivery) {
4636            return false;
4637        }
4638
4639        // Checking if the product with the old address delivery exists
4640        $sql = new DbQuery();
4641        $sql->select('count(*)');
4642        $sql->from('cart_product', 'cp');
4643        $sql->where('id_product = '.(int) $idProduct);
4644        $sql->where('id_product_attribute = '.(int) $idProductAttribute);
4645        $sql->where('id_address_delivery = '.(int) $oldIdAddressDelivery);
4646        $sql->where('id_cart = '.(int) $this->id);
4647        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql);
4648
4649        if ($result == 0) {
4650            return false;
4651        }
4652
4653        // Checking if there is no others similar products with this new address delivery
4654        $sql = new DbQuery();
4655        $sql->select('sum(quantity) as qty');
4656        $sql->from('cart_product', 'cp');
4657        $sql->where('id_product = '.(int) $idProduct);
4658        $sql->where('id_product_attribute = '.(int) $idProductAttribute);
4659        $sql->where('id_address_delivery = '.(int) $newIdAddressDelivery);
4660        $sql->where('id_cart = '.(int) $this->id);
4661        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql);
4662
4663        // Removing similar products with this new address delivery
4664        Db::getInstance()->delete(
4665            'cart_product',
4666            'id_product = '.(int) $idProduct.' AND id_product_attribute = '.(int) $idProductAttribute.' AND id_address_delivery = '.(int) $newIdAddressDelivery.' AND id_cart = '.(int) $this->id,
4667            1
4668        );
4669
4670        // Changing the address
4671        Db::getInstance()->update(
4672            'cart_product',
4673            [
4674                'id_address_delivery' => (int) $newIdAddressDelivery,
4675                'quantity' => ['type' => 'sql', 'value' => '`quantity` + '.(int) $result],
4676            ],
4677            '`id_product` = '.(int) $idProduct.' AND `id_product_attribute` = '.(int) $idProductAttribute.' AND `id_address_delivery` = '.(int) $oldIdAddressDelivery.' AND `id_cart` = '.(int) $this->id,
4678            1
4679        );
4680
4681        return true;
4682    }
4683
4684    /**
4685     * @param int  $idProduct
4686     * @param int  $idProductAttribute
4687     * @param int  $idAddressDelivery
4688     * @param int  $newIdAddressDelivery
4689     * @param int  $quantity
4690     * @param bool $keepQuantity
4691     *
4692     * @return bool
4693     *
4694     * @throws PrestaShopDatabaseException
4695     * @throws PrestaShopException
4696     * @since   1.0.0
4697     * @version 1.0.0
4698     */
4699    public function duplicateProduct(
4700        $idProduct,
4701        $idProductAttribute,
4702        $idAddressDelivery,
4703        $newIdAddressDelivery,
4704        $quantity = 1,
4705        $keepQuantity = false
4706    ) {
4707        // Check address is linked with the customer
4708        if (!Customer::customerHasAddress(Context::getContext()->customer->id, $newIdAddressDelivery)) {
4709            return false;
4710        }
4711
4712        // Checking the product do not exist with the new address
4713        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
4714            (new DbQuery())
4715                ->select('COUNT(*)')
4716                ->from('cart_product', 'c')
4717                ->where('id_product = '.(int) $idProduct)
4718                ->where('`id_product_attribute` = '.(int) $idProductAttribute)
4719                ->where('`id_address_delivery` = '.(int) $newIdAddressDelivery)
4720                ->where('`id_cart` = '.(int) $this->id)
4721        );
4722
4723        if ($result > 0) {
4724            return false;
4725        }
4726
4727        Db::getInstance()->insert(
4728            'cart_product',
4729            [
4730                'id_cart'              => (int) $this->id,
4731                'id_product'           => (int) $idProduct,
4732                'id_shop'              => (int) $this->id_shop,
4733                'id_product_attribute' => (int) $idProductAttribute,
4734                'quantity'             => (int) $quantity,
4735                'date_add'             => ['type' => 'sql', 'value' => 'NOW()'],
4736                'id_address_delivery'  => (int) $newIdAddressDelivery,
4737            ]
4738        );
4739
4740        if (!$keepQuantity) {
4741            $duplicatedQuantity = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
4742                (new DbQuery())
4743                    ->select('quantity')
4744                    ->from('cart_product', 'c')
4745                    ->where('id_product = '.(int) $idProduct)
4746                    ->where('id_product_attribute = '.(int) $idProductAttribute)
4747                    ->where('id_address_delivery = '.(int) $idAddressDelivery)
4748                    ->where('id_cart = '.(int) $this->id)
4749            );
4750
4751            if ($duplicatedQuantity > $quantity) {
4752                Db::getInstance()->update(
4753                    'cart_product',
4754                    [
4755                        'quantity'             => ['type' => 'sql', 'value' => '`quantity - `'.(int) $quantity],
4756                        'id_product'           => (int) $idProduct,
4757                        'id_shop'              => (int) $this->id_shop,
4758                        'id_product_attribute' => (int) $idProductAttribute,
4759                        'id_address_delivery'  => (int) $idAddressDelivery,
4760                    ],
4761                    '`id_cart` ='.(int) $this->id
4762                );
4763            }
4764        }
4765
4766        // Checking if there is customizations
4767        $results = Db::getInstance()->executeS(
4768            (new DbQuery())
4769                ->select('*')
4770                ->from('customization', 'c')
4771                ->where('id_product = '.(int) $idProduct)
4772                ->where('id_product_attribute = '.(int) $idProductAttribute)
4773                ->where('id_address_delivery = '.(int) $idAddressDelivery)
4774                ->where('id_cart = '.(int) $this->id)
4775        );
4776
4777        foreach ($results as $customization) {
4778            // Duplicate customization
4779            Db::getInstance()->insert(
4780                'customization',
4781                [
4782                    'id_product_attribute' => (int) $customization['id_product_attribute'],
4783                    'id_address_delivery'  => (int) $newIdAddressDelivery,
4784                    'id_cart'              => (int) $customization['id_cart'],
4785                    'id_product'           => (int) $customization['id_product'],
4786                    'quantity'             => (int) $quantity,
4787                    'in_cart'              => $customization['in_cart'],
4788                ]
4789            );
4790
4791            // Save last insert ID before doing another query
4792            $lastId = (int) Db::getInstance()->Insert_ID();
4793
4794            // Get data from duplicated customizations
4795            $lastRow = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow(
4796                (new DbQuery())
4797                    ->select('`type`, `index`, `value`')
4798                    ->from('customized_data')
4799                    ->where('id_customization = '.$customization['id_customization'])
4800            );
4801
4802            // Insert new copied data with new customization ID into customized_data table
4803            $lastRow['id_customization'] = $lastId;
4804            Db::getInstance()->insert('customized_data', $lastRow);
4805        }
4806
4807        $customizationCount = count($results);
4808        if ($customizationCount > 0) {
4809            Db::getInstance()->update(
4810                'cart_product',
4811                [
4812                    'quantity' => ['type' => 'sql', 'value' => '`quantity` + '.(int) $customizationCount * $quantity],
4813                ],
4814                'id_cart = '.(int) $this->id.' AND id_product = '.(int) $idProduct.' AND id_shop = '.(int) $this->id_shop.' AND id_product_attribute = '.(int) $idProductAttribute.' AND id_address_delivery = '.(int) $newIdAddressDelivery
4815            );
4816        }
4817
4818        return true;
4819    }
4820
4821    /**
4822     * Update products cart address delivery with the address delivery of the cart
4823     *
4824     * @since   1.0.0
4825     * @version 1.0.0
4826     */
4827    public function setNoMultishipping()
4828    {
4829        $emptyCache = false;
4830        if (Configuration::get('PS_ALLOW_MULTISHIPPING')) {
4831            // Upgrading quantities
4832            $products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
4833                (new DbQuery())
4834                    ->select('SUM(`quantity`) AS `quantity`, `id_product`, `id_product_attribute`, COUNT(*) AS `count`')
4835                    ->from('cart_product')
4836                    ->where('`id_cart` = '.(int) $this->id)
4837                    ->where('`id_shop` = '.(int) $this->id_shop)
4838                    ->groupBy('`id_product`, `id_product_attribute`')
4839                    ->having('`count` > 1')
4840            );
4841
4842            if (is_array($products)) {
4843                foreach ($products as $product) {
4844                    if (Db::getInstance()->update(
4845                        'cart_product',
4846                        [
4847                            'quantity' => (int) $product['quantity'],
4848                        ],
4849                        '`id_cart` = '.(int) $this->id.' AND `id_shop` = '.(int) $this->id_shop.' AND id_product = '.$product['id_product'].' AND id_product_attribute = '.$product['id_product_attribute']
4850                    )) {
4851                        $emptyCache = true;
4852                    }
4853                }
4854            }
4855
4856            // Merging multiple lines
4857            $sql = 'DELETE cp1
4858				FROM `'._DB_PREFIX_.'cart_product` cp1
4859					INNER JOIN `'._DB_PREFIX_.'cart_product` cp2
4860					ON (
4861						(cp1.id_cart = cp2.id_cart)
4862						AND (cp1.id_product = cp2.id_product)
4863						AND (cp1.id_product_attribute = cp2.id_product_attribute)
4864						AND (cp1.id_address_delivery <> cp2.id_address_delivery)
4865						AND (cp1.date_add > cp2.date_add)
4866					)';
4867            Db::getInstance()->execute($sql);
4868        }
4869
4870        // Update delivery address for each product line
4871        $cacheId = 'static::setNoMultishipping'.(int) $this->id.'-'.(int) $this->id_shop.((isset($this->id_address_delivery) && $this->id_address_delivery) ? '-'.(int) $this->id_address_delivery : '');
4872        if (!Cache::isStored($cacheId)) {
4873            if ($result = (bool) Db::getInstance()->update(
4874                'cart_product',
4875                [
4876                    'id_address_delivery' => ['type' => 'sql', 'value' => '(SELECT `id_address_delivery` FROM `'._DB_PREFIX_.'cart` WHERE `id_cart` = '.(int) $this->id.' AND `id_shop` = '.(int) $this->id_shop.' LIMIT 1)'],
4877                ],
4878                '`id_cart` = '.(int) $this->id.' '.(Configuration::get('PS_ALLOW_MULTISHIPPING') ? ' AND `id_shop` = '.(int) $this->id_shop : '')
4879            )) {
4880                $emptyCache = true;
4881            }
4882            Cache::store($cacheId, $result);
4883        }
4884
4885        if (Customization::isFeatureActive()) {
4886            Db::getInstance()->update(
4887                'customization',
4888                [
4889                    'id_address_delivery' => ['type' => 'sql', 'value' => '(SELECT `id_address_delivery` FROM `'._DB_PREFIX_.'cart` WHERE `id_cart` = '.(int) $this->id.' LIMIT 1)'],
4890                ],
4891                '`id_cart` = '.(int) $this->id
4892            );
4893        }
4894
4895        if ($emptyCache) {
4896            $this->_products = null;
4897        }
4898    }
4899
4900    /**
4901     * Set an address to all products on the cart without address delivery
4902     *
4903     * @since   1.0.0
4904     * @version 1.0.0
4905     */
4906    public function autosetProductAddress()
4907    {
4908        // Get the main address of the customer
4909        if ((int) $this->id_address_delivery > 0) {
4910            $idAddressDelivery = (int) $this->id_address_delivery;
4911        } else {
4912            $idAddressDelivery = (int) Address::getFirstCustomerAddressId(Context::getContext()->customer->id);
4913        }
4914
4915        if (!$idAddressDelivery) {
4916            return;
4917        }
4918
4919        // Update
4920        Db::getInstance()->update(
4921            'cart_product',
4922            [
4923                'id_address_delivery' => (int) $idAddressDelivery,
4924            ],
4925            '`id_cart` = '.(int) $this->id.' AND (`id_address_delivery` = 0 OR `id_address_delivery` IS NULL) AND `id_shop` = '.(int) $this->id_shop
4926        );
4927
4928        Db::getInstance()->update(
4929            'customization',
4930            [
4931                'id_address_delivery' => (int) $idAddressDelivery,
4932            ],
4933            '`id_cart` = '.(int) $this->id.' AND (`id_address_delivery` = 0 OR `id_address_delivery` IS NULL)'
4934        );
4935    }
4936
4937    /**
4938     * @param bool $ignoreVirtual Ignore virtual product
4939     * @param bool $exclusive     If true, the validation is exclusive : it must be present product in stock and out of stock
4940     *
4941     * @return bool false is some products from the cart are out of stock
4942     *
4943     * @since   1.0.0
4944     * @version 1.0.0
4945     * @throws PrestaShopException
4946     */
4947    public function isAllProductsInStock($ignoreVirtual = false, $exclusive = false)
4948    {
4949        $productOutOfStock = 0;
4950        $productInStock = 0;
4951        foreach ($this->getProducts() as $product) {
4952            if (!$exclusive) {
4953                if (((int) $product['quantity_available'] - (int) $product['cart_quantity']) < 0
4954                    && (!$ignoreVirtual || !$product['is_virtual'])
4955                ) {
4956                    return false;
4957                }
4958            } else {
4959                if ((int) $product['quantity_available'] <= 0
4960                    && (!$ignoreVirtual || !$product['is_virtual'])
4961                ) {
4962                    $productOutOfStock++;
4963                }
4964                if ((int) $product['quantity_available'] > 0
4965                    && (!$ignoreVirtual || !$product['is_virtual'])
4966                ) {
4967                    $productInStock++;
4968                }
4969
4970                if ($productInStock > 0 && $productOutOfStock > 0) {
4971                    return false;
4972                }
4973            }
4974        }
4975
4976        return true;
4977    }
4978}
4979