1<?php
2/**
3 * Copyright since 2007 PrestaShop SA and Contributors
4 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
5 *
6 * NOTICE OF LICENSE
7 *
8 * This source file is subject to the Open Software License (OSL 3.0)
9 * that is bundled with this package in the file LICENSE.md.
10 * It is also available through the world-wide-web at this URL:
11 * https://opensource.org/licenses/OSL-3.0
12 * If you did not receive a copy of the license and are unable to
13 * obtain it through the world-wide-web, please send an email
14 * to license@prestashop.com so we can send you a copy immediately.
15 *
16 * DISCLAIMER
17 *
18 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
19 * versions in the future. If you wish to customize PrestaShop for your
20 * needs please refer to https://devdocs.prestashop.com/ for more information.
21 *
22 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
23 * @copyright Since 2007 PrestaShop SA and Contributors
24 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
25 */
26use PrestaShop\PrestaShop\Adapter\ServiceLocator;
27
28class OrderCore extends ObjectModel
29{
30    const ROUND_ITEM = 1;
31    const ROUND_LINE = 2;
32    const ROUND_TOTAL = 3;
33
34    /** @var int Delivery address id */
35    public $id_address_delivery;
36
37    /** @var int Invoice address id */
38    public $id_address_invoice;
39
40    public $id_shop_group;
41
42    public $id_shop;
43
44    /** @var int Cart id */
45    public $id_cart;
46
47    /** @var int Currency id */
48    public $id_currency;
49
50    /** @var int Language id */
51    public $id_lang;
52
53    /** @var int Customer id */
54    public $id_customer;
55
56    // todo: string received instead of int
57    /** @var int Carrier id */
58    public $id_carrier;
59
60    /** @var int Order Status id */
61    public $current_state;
62
63    /** @var string Secure key */
64    public $secure_key;
65
66    /** @var string Payment method */
67    public $payment;
68
69    /** @var string Payment module */
70    public $module;
71
72    /** @var float Currency exchange rate */
73    public $conversion_rate;
74
75    /** @var bool Customer is ok for a recyclable package */
76    public $recyclable = 1;
77
78    /** @var bool True if the customer wants a gift wrapping */
79    public $gift = 0;
80
81    /** @var string Gift message if specified */
82    public $gift_message;
83
84    /** @var bool Mobile Theme */
85    public $mobile_theme;
86
87    /**
88     * @var string Shipping number
89     *
90     * @deprecated 1.5.0.4
91     * @see OrderCarrier->tracking_number
92     */
93    public $shipping_number;
94
95    /** @var float Discounts total */
96    public $total_discounts;
97
98    public $total_discounts_tax_incl;
99    public $total_discounts_tax_excl;
100
101    /** @var float Total to pay */
102    public $total_paid;
103
104    /** @var float Total to pay tax included */
105    public $total_paid_tax_incl;
106
107    /** @var float Total to pay tax excluded */
108    public $total_paid_tax_excl;
109
110    /** @var float Total really paid @deprecated 1.5.0.1 */
111    public $total_paid_real;
112
113    /** @var float Products total */
114    public $total_products;
115
116    /** @var float Products total tax included */
117    public $total_products_wt;
118
119    /** @var float Shipping total */
120    public $total_shipping;
121
122    /** @var float Shipping total tax included */
123    public $total_shipping_tax_incl;
124
125    /** @var float Shipping total tax excluded */
126    public $total_shipping_tax_excl;
127
128    /** @var float Shipping tax rate */
129    public $carrier_tax_rate;
130
131    /** @var float Wrapping total */
132    public $total_wrapping;
133
134    /** @var float Wrapping total tax included */
135    public $total_wrapping_tax_incl;
136
137    /** @var float Wrapping total tax excluded */
138    public $total_wrapping_tax_excl;
139
140    /** @var int Invoice number */
141    public $invoice_number;
142
143    /** @var int Delivery number */
144    public $delivery_number;
145
146    /** @var string Invoice creation date */
147    public $invoice_date;
148
149    /** @var string Delivery creation date */
150    public $delivery_date;
151
152    /** @var bool Order validity: current order status is logable (usually paid and not canceled) */
153    public $valid;
154
155    /** @var string Object creation date */
156    public $date_add;
157
158    /** @var string Object last modification date */
159    public $date_upd;
160
161    /**
162     * @var string Order reference, this reference is not unique, but unique for a payment
163     */
164    public $reference;
165
166    /**
167     * @var int Round mode method used for this order
168     */
169    public $round_mode;
170
171    /**
172     * @var int Round type method used for this order
173     */
174    public $round_type;
175
176    /**
177     * @var string internal order note, what is only available in BO
178     */
179    public $note = '';
180
181    /**
182     * @see ObjectModel::$definition
183     */
184    public static $definition = [
185        'table' => 'orders',
186        'primary' => 'id_order',
187        'fields' => [
188            'id_address_delivery' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
189            'id_address_invoice' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
190            'id_cart' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
191            'id_currency' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
192            'id_shop_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
193            'id_shop' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
194            'id_lang' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
195            'id_customer' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
196            'id_carrier' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
197            'current_state' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
198            'secure_key' => ['type' => self::TYPE_STRING, 'validate' => 'isMd5'],
199            'payment' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true],
200            'module' => ['type' => self::TYPE_STRING, 'validate' => 'isModuleName', 'required' => true],
201            'recyclable' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
202            'gift' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
203            'gift_message' => ['type' => self::TYPE_STRING, 'validate' => 'isMessage'],
204            'mobile_theme' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
205            'total_discounts' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
206            'total_discounts_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
207            'total_discounts_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
208            'total_paid' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice', 'required' => true],
209            'total_paid_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
210            'total_paid_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
211            'total_paid_real' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice', 'required' => true],
212            'total_products' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice', 'required' => true],
213            'total_products_wt' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice', 'required' => true],
214            'total_shipping' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
215            'total_shipping_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
216            'total_shipping_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
217            'carrier_tax_rate' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
218            'total_wrapping' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
219            'total_wrapping_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
220            'total_wrapping_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
221            'round_mode' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
222            'round_type' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
223            'shipping_number' => ['type' => self::TYPE_STRING, 'validate' => 'isTrackingNumber'],
224            'conversion_rate' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat', 'required' => true],
225            'invoice_number' => ['type' => self::TYPE_INT],
226            'delivery_number' => ['type' => self::TYPE_INT],
227            'invoice_date' => ['type' => self::TYPE_DATE],
228            'delivery_date' => ['type' => self::TYPE_DATE],
229            'valid' => ['type' => self::TYPE_BOOL],
230            'reference' => ['type' => self::TYPE_STRING],
231            'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
232            'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
233            'note' => ['type' => self::TYPE_HTML],
234        ],
235    ];
236
237    protected $webserviceParameters = [
238        'objectMethods' => ['add' => 'addWs'],
239        'objectNodeName' => 'order',
240        'objectsNodeName' => 'orders',
241        'fields' => [
242            'id_address_delivery' => ['xlink_resource' => 'addresses'],
243            'id_address_invoice' => ['xlink_resource' => 'addresses'],
244            'id_cart' => ['xlink_resource' => 'carts'],
245            'id_currency' => ['xlink_resource' => 'currencies'],
246            'id_lang' => ['xlink_resource' => 'languages'],
247            'id_customer' => ['xlink_resource' => 'customers'],
248            'id_carrier' => ['xlink_resource' => 'carriers'],
249            'current_state' => [
250                'xlink_resource' => 'order_states',
251                'setter' => 'setWsCurrentState',
252            ],
253            'module' => ['required' => true],
254            'invoice_number' => [],
255            'invoice_date' => [],
256            'delivery_number' => [],
257            'delivery_date' => [],
258            'valid' => [],
259            'date_add' => [],
260            'date_upd' => [],
261            'shipping_number' => [
262                'getter' => 'getWsShippingNumber',
263                'setter' => 'setWsShippingNumber',
264            ],
265            'note' => [],
266        ],
267        'associations' => [
268            'order_rows' => ['resource' => 'order_row', 'setter' => false, 'virtual_entity' => true,
269                'fields' => [
270                    'id' => [],
271                    'product_id' => ['required' => true, 'xlink_resource' => 'products'],
272                    'product_attribute_id' => ['required' => true],
273                    'product_quantity' => ['required' => true],
274                    'product_name' => ['setter' => false],
275                    'product_reference' => ['setter' => false],
276                    'product_ean13' => ['setter' => false],
277                    'product_isbn' => ['setter' => false],
278                    'product_upc' => ['setter' => false],
279                    'product_price' => ['setter' => false],
280                    'id_customization' => ['required' => false, 'xlink_resource' => 'customizations'],
281                    'unit_price_tax_incl' => ['setter' => false],
282                    'unit_price_tax_excl' => ['setter' => false],
283                ], ],
284        ],
285    ];
286
287    protected $_taxCalculationMethod = PS_TAX_EXC;
288
289    protected static $_historyCache = [];
290
291    public function __construct($id = null, $id_lang = null)
292    {
293        parent::__construct($id, $id_lang);
294
295        $is_admin = (is_object(Context::getContext()->controller) && Context::getContext()->controller->controller_type == 'admin');
296        if ($this->id_customer && !$is_admin) {
297            $customer = new Customer((int) $this->id_customer);
298            $this->_taxCalculationMethod = Group::getPriceDisplayMethod((int) $customer->id_default_group);
299        } else {
300            $this->_taxCalculationMethod = Group::getDefaultPriceDisplayMethod();
301        }
302    }
303
304    /**
305     * @see ObjectModel::getFields()
306     *
307     * @return array
308     */
309    public function getFields()
310    {
311        if (!$this->id_lang) {
312            $this->id_lang = Configuration::get('PS_LANG_DEFAULT', null, null, $this->id_shop);
313        }
314
315        return parent::getFields();
316    }
317
318    public function add($autodate = true, $null_values = true)
319    {
320        if (parent::add($autodate, $null_values)) {
321            return SpecificPrice::deleteByIdCart($this->id_cart);
322        }
323
324        return false;
325    }
326
327    public function getTaxCalculationMethod()
328    {
329        return (int) $this->_taxCalculationMethod;
330    }
331
332    /**
333     * Does NOT delete a product but "cancel" it (which means return/refund/delete it depending of the case).
334     *
335     * @param $order
336     * @param OrderDetail $order_detail
337     * @param int $quantity
338     *
339     * @return bool
340     *
341     * @throws PrestaShopException
342     */
343    public function deleteProduct($order, $order_detail, $quantity)
344    {
345        if (!(int) $this->getCurrentState() || !validate::isLoadedObject($order_detail)) {
346            return false;
347        }
348
349        if ($this->hasBeenDelivered()) {
350            if (!Configuration::get('PS_ORDER_RETURN', null, null, $this->id_shop)) {
351                throw new PrestaShopException('PS_ORDER_RETURN is not defined in table configuration');
352            }
353            $order_detail->product_quantity_return += (int) $quantity;
354
355            return $order_detail->update();
356        } elseif ($this->hasBeenPaid()) {
357            $order_detail->product_quantity_refunded += (int) $quantity;
358
359            return $order_detail->update();
360        }
361
362        return $this->_deleteProduct($order_detail, (int) $quantity);
363    }
364
365    /**
366     * This function return products of the orders
367     * It's similar to Order::getProducts but with similar outputs of Cart::getProducts.
368     *
369     * @return array
370     */
371    public function getCartProducts()
372    {
373        $product_id_list = [];
374        $products = $this->getProducts();
375        foreach ($products as &$product) {
376            $product['id_product_attribute'] = $product['product_attribute_id'];
377            $product['cart_quantity'] = $product['product_quantity'];
378            $product_id_list[] = $this->id_address_delivery . '_'
379                . $product['product_id'] . '_'
380                . $product['product_attribute_id'] . '_'
381                . (isset($product['id_customization']) ? $product['id_customization'] : '0');
382        }
383        unset($product);
384
385        $product_list = [];
386        foreach ($products as $product) {
387            $key = $this->id_address_delivery . '_'
388                . $product['id_product'] . '_'
389                . (isset($product['id_product_attribute']) ? $product['id_product_attribute'] : '0') . '_'
390                . (isset($product['id_customization']) ? $product['id_customization'] : '0');
391
392            if (in_array($key, $product_id_list)) {
393                $product_list[] = $product;
394            }
395        }
396
397        return $product_list;
398    }
399
400    /**
401     * DOES delete the product.
402     *
403     * @param OrderDetail $order_detail
404     * @param int $quantity
405     *
406     * @return bool
407     *
408     * @throws PrestaShopException
409     */
410    protected function _deleteProduct($order_detail, $quantity)
411    {
412        $product_price_tax_excl = $order_detail->unit_price_tax_excl * $quantity;
413        $product_price_tax_incl = $order_detail->unit_price_tax_incl * $quantity;
414
415        /* Update cart */
416        $cart = new Cart($this->id_cart);
417        $cart->updateQty($quantity, $order_detail->product_id, $order_detail->product_attribute_id, false, 'down'); // customization are deleted in deleteCustomization
418        $cart->update();
419
420        /* Update order */
421        $shipping_diff_tax_incl = $this->total_shipping_tax_incl - $cart->getPackageShippingCost($this->id_carrier, true, null, $this->getCartProducts());
422        $shipping_diff_tax_excl = $this->total_shipping_tax_excl - $cart->getPackageShippingCost($this->id_carrier, false, null, $this->getCartProducts());
423        $this->total_shipping -= $shipping_diff_tax_incl;
424        $this->total_shipping_tax_excl -= $shipping_diff_tax_excl;
425        $this->total_shipping_tax_incl -= $shipping_diff_tax_incl;
426        $this->total_products -= $product_price_tax_excl;
427        $this->total_products_wt -= $product_price_tax_incl;
428        $this->total_paid -= $product_price_tax_incl + $shipping_diff_tax_incl;
429        $this->total_paid_tax_incl -= $product_price_tax_incl + $shipping_diff_tax_incl;
430        $this->total_paid_tax_excl -= $product_price_tax_excl + $shipping_diff_tax_excl;
431        $this->total_paid_real -= $product_price_tax_incl + $shipping_diff_tax_incl;
432
433        $fields = [
434            'total_shipping',
435            'total_shipping_tax_excl',
436            'total_shipping_tax_incl',
437            'total_products',
438            'total_products_wt',
439            'total_paid',
440            'total_paid_tax_incl',
441            'total_paid_tax_excl',
442            'total_paid_real',
443        ];
444
445        /* Prevent from floating precision issues */
446        foreach ($fields as $field) {
447            if ($this->{$field} < 0) {
448                $this->{$field} = 0;
449            }
450        }
451
452        /* Prevent from floating precision issues */
453        foreach ($fields as $field) {
454            $this->{$field} = number_format($this->{$field}, Context::getContext()->getComputingPrecision(), '.', '');
455        }
456
457        /* Update order detail */
458        $order_detail->product_quantity -= (int) $quantity;
459        if ($order_detail->product_quantity == 0) {
460            if (!$order_detail->delete()) {
461                return false;
462            }
463            if (count($this->getProductsDetail()) == 0) {
464                $history = new OrderHistory();
465                $history->id_order = (int) $this->id;
466                $history->changeIdOrderState(Configuration::get('PS_OS_CANCELED'), $this);
467                if (!$history->addWithemail()) {
468                    return false;
469                }
470            }
471
472            return $this->update();
473        } else {
474            $order_detail->total_price_tax_incl -= $product_price_tax_incl;
475            $order_detail->total_price_tax_excl -= $product_price_tax_excl;
476            $order_detail->total_shipping_price_tax_incl -= $shipping_diff_tax_incl;
477            $order_detail->total_shipping_price_tax_excl -= $shipping_diff_tax_excl;
478        }
479
480        return $order_detail->update() && $this->update();
481    }
482
483    public function deleteCustomization($id_customization, $quantity, $order_detail)
484    {
485        if (!(int) $this->getCurrentState()) {
486            return false;
487        }
488
489        if ($this->hasBeenDelivered()) {
490            return Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'customization` SET `quantity_returned` = `quantity_returned` + ' . (int) $quantity . ' WHERE `id_customization` = ' . (int) $id_customization . ' AND `id_cart` = ' . (int) $this->id_cart . ' AND `id_product` = ' . (int) $order_detail->product_id);
491        } elseif ($this->hasBeenPaid()) {
492            return Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'customization` SET `quantity_refunded` = `quantity_refunded` + ' . (int) $quantity . ' WHERE `id_customization` = ' . (int) $id_customization . ' AND `id_cart` = ' . (int) $this->id_cart . ' AND `id_product` = ' . (int) $order_detail->product_id);
493        }
494        if (!Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'customization` SET `quantity` = `quantity` - ' . (int) $quantity . ' WHERE `id_customization` = ' . (int) $id_customization . ' AND `id_cart` = ' . (int) $this->id_cart . ' AND `id_product` = ' . (int) $order_detail->product_id)) {
495            return false;
496        }
497        if (!Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'customization` WHERE `quantity` = 0')) {
498            return false;
499        }
500
501        return $this->_deleteProduct($order_detail, (int) $quantity);
502    }
503
504    /**
505     * Get order history.
506     *
507     * @param int $id_lang Language id
508     * @param int $id_order_state Filter a specific order status
509     * @param int $no_hidden Filter no hidden status
510     * @param int $filters Flag to use specific field filter
511     *
512     * @return array History entries ordered by date DESC
513     */
514    public function getHistory($id_lang, $id_order_state = false, $no_hidden = false, $filters = 0)
515    {
516        if (!$id_order_state) {
517            $id_order_state = 0;
518        }
519
520        $logable = false;
521        $delivery = false;
522        $paid = false;
523        $shipped = false;
524        if ($filters > 0) {
525            if ($filters & OrderState::FLAG_NO_HIDDEN) {
526                $no_hidden = true;
527            }
528            if ($filters & OrderState::FLAG_DELIVERY) {
529                $delivery = true;
530            }
531            if ($filters & OrderState::FLAG_LOGABLE) {
532                $logable = true;
533            }
534            if ($filters & OrderState::FLAG_PAID) {
535                $paid = true;
536            }
537            if ($filters & OrderState::FLAG_SHIPPED) {
538                $shipped = true;
539            }
540        }
541
542        if (!isset(self::$_historyCache[$this->id . '_' . $id_order_state . '_' . $filters]) || $no_hidden) {
543            $id_lang = $id_lang ? (int) $id_lang : 'o.`id_lang`';
544            $result = Db::getInstance()->executeS('
545            SELECT os.*, oh.*, e.`firstname` as employee_firstname, e.`lastname` as employee_lastname, osl.`name` as ostate_name
546            FROM `' . _DB_PREFIX_ . 'orders` o
547            LEFT JOIN `' . _DB_PREFIX_ . 'order_history` oh ON o.`id_order` = oh.`id_order`
548            LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON os.`id_order_state` = oh.`id_order_state`
549            LEFT JOIN `' . _DB_PREFIX_ . 'order_state_lang` osl ON (os.`id_order_state` = osl.`id_order_state` AND osl.`id_lang` = ' . (int) ($id_lang) . ')
550            LEFT JOIN `' . _DB_PREFIX_ . 'employee` e ON e.`id_employee` = oh.`id_employee`
551            WHERE oh.id_order = ' . (int) $this->id . '
552            ' . ($no_hidden ? ' AND os.hidden = 0' : '') . '
553            ' . ($logable ? ' AND os.logable = 1' : '') . '
554            ' . ($delivery ? ' AND os.delivery = 1' : '') . '
555            ' . ($paid ? ' AND os.paid = 1' : '') . '
556            ' . ($shipped ? ' AND os.shipped = 1' : '') . '
557            ' . ((int) $id_order_state ? ' AND oh.`id_order_state` = ' . (int) $id_order_state : '') . '
558            ORDER BY oh.date_add DESC, oh.id_order_history DESC');
559            if ($no_hidden) {
560                return $result;
561            }
562            self::$_historyCache[$this->id . '_' . $id_order_state . '_' . $filters] = $result;
563        }
564
565        return self::$_historyCache[$this->id . '_' . $id_order_state . '_' . $filters];
566    }
567
568    /**
569     * Clean static history cache, must be called when an OrderHistory is added as it changes
570     * the order history and may change its value for isPaid/isDelivered/... This way calls to
571     * getHistory will be up to date.
572     */
573    public static function cleanHistoryCache()
574    {
575        self::$_historyCache = [];
576    }
577
578    public function getProductsDetail()
579    {
580        // The `od.ecotax` is a newly added at end as ecotax is used in multiples columns but it's the ecotax value we need
581        $sql = 'SELECT p.*, ps.*, od.*';
582        $sql .= ' FROM `%sorder_detail` od';
583        $sql .= ' LEFT JOIN `%sproduct` p ON (p.id_product = od.product_id)';
584        $sql .= ' LEFT JOIN `%sproduct_shop` ps ON (ps.id_product = p.id_product AND ps.id_shop = od.id_shop)';
585        $sql .= ' WHERE od.`id_order` = %d';
586        $sql = sprintf($sql, _DB_PREFIX_, _DB_PREFIX_, _DB_PREFIX_, (int) $this->id);
587
588        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
589    }
590
591    public function getFirstMessage()
592    {
593        return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
594            SELECT `message`
595            FROM `' . _DB_PREFIX_ . 'message`
596            WHERE `id_order` = ' . (int) $this->id . '
597            ORDER BY `id_message`
598        ');
599    }
600
601    /**
602     * Marked as deprecated but should not throw any "deprecated" message
603     * This function is used in order to keep front office backward compatibility 14 -> 1.5
604     * (Order History).
605     *
606     * @deprecated
607     */
608    public function setProductPrices(&$row)
609    {
610        $tax_calculator = OrderDetail::getTaxCalculatorStatic((int) $row['id_order_detail']);
611        $row['tax_calculator'] = $tax_calculator;
612        $row['tax_rate'] = $tax_calculator->getTotalRate();
613
614        $row['product_price'] = Tools::ps_round($row['unit_price_tax_excl'], Context::getContext()->getComputingPrecision());
615        $row['product_price_wt'] = Tools::ps_round($row['unit_price_tax_incl'], Context::getContext()->getComputingPrecision());
616
617        $group_reduction = 1;
618        if ($row['group_reduction'] > 0) {
619            $group_reduction = 1 - $row['group_reduction'] / 100;
620        }
621
622        $row['product_price_wt_but_ecotax'] = $row['product_price_wt'] - $row['ecotax'];
623
624        $row['total_wt'] = $row['total_price_tax_incl'];
625        $row['total_price'] = $row['total_price_tax_excl'];
626    }
627
628    /**
629     * Get order products.
630     *
631     * @param bool $products
632     * @param bool $selected_products
633     * @param bool $selected_qty
634     * @param bool $fullInfos
635     *
636     * @return array Products with price, quantity (with taxe and without)
637     */
638    public function getProducts($products = false, $selected_products = false, $selected_qty = false, $fullInfos = true)
639    {
640        if (!$products) {
641            $products = $this->getProductsDetail();
642        }
643
644        if (!$fullInfos) {
645            return $products;
646        }
647
648        $result_array = [];
649        foreach ($products as $row) {
650            // Change qty if selected
651            if ($selected_qty) {
652                $row['product_quantity'] = 0;
653                foreach ($selected_products as $key => $id_product) {
654                    if ($row['id_order_detail'] == $id_product) {
655                        $row['product_quantity'] = (int) $selected_qty[$key];
656                    }
657                }
658                if (!$row['product_quantity']) {
659                    continue;
660                }
661            }
662
663            $this->setProductImageInformations($row);
664            $this->setProductCurrentStock($row);
665
666            // Backward compatibility 1.4 -> 1.5
667            $this->setProductPrices($row);
668            $customized_datas = Product::getAllCustomizedDatas($this->id_cart, null, true, $this->id_shop, (int) $row['id_customization']);
669            $this->setProductCustomizedDatas($row, $customized_datas);
670
671            // Add information for virtual product
672            if ($row['download_hash'] && !empty($row['download_hash'])) {
673                $row['filename'] = ProductDownload::getFilenameFromIdProduct((int) $row['product_id']);
674                // Get the display filename
675                $row['display_filename'] = ProductDownload::getFilenameFromFilename($row['filename']);
676            }
677
678            $row['id_address_delivery'] = $this->id_address_delivery;
679
680            if ($customized_datas) {
681                Product::addProductCustomizationPrice($row, $customized_datas);
682            }
683
684            /* Stock product */
685            $result_array[(int) $row['id_order_detail']] = $row;
686        }
687
688        return $result_array;
689    }
690
691    public static function getIdOrderProduct($id_customer, $id_product)
692    {
693        return (int) Db::getInstance()->getValue('
694            SELECT o.id_order
695            FROM ' . _DB_PREFIX_ . 'orders o
696            LEFT JOIN ' . _DB_PREFIX_ . 'order_detail od
697                ON o.id_order = od.id_order
698            WHERE o.id_customer = ' . (int) $id_customer . '
699                AND od.product_id = ' . (int) $id_product . '
700            ORDER BY o.date_add DESC
701        ');
702    }
703
704    protected function setProductCustomizedDatas(&$product, $customized_datas)
705    {
706        $product['customizedDatas'] = null;
707        if (isset($customized_datas[$product['product_id']][$product['product_attribute_id']])) {
708            $product['customizedDatas'] = $customized_datas[$product['product_id']][$product['product_attribute_id']];
709        } else {
710            $product['customizationQuantityTotal'] = 0;
711        }
712    }
713
714    /**
715     * This method allow to add stock information on a product detail.
716     *
717     * If advanced stock management is active, get physical stock of this product in the warehouse associated to the ptoduct for the current order
718     * Else get the available quantity of the product in fucntion of the shop associated to the order
719     *
720     * @param array &$product
721     */
722    protected function setProductCurrentStock(&$product)
723    {
724        if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')
725            && (int) $product['advanced_stock_management'] == 1
726            && (int) $product['id_warehouse'] > 0) {
727            $product['current_stock'] = StockManagerFactory::getManager()->getProductPhysicalQuantities($product['product_id'], $product['product_attribute_id'], (int) $product['id_warehouse'], true);
728        } else {
729            $product['current_stock'] = StockAvailable::getQuantityAvailableByProduct($product['product_id'], $product['product_attribute_id'], (int) $this->id_shop);
730        }
731
732        $product['location'] = StockAvailable::getLocation($product['product_id'], $product['product_attribute_id'], (int) $this->id_shop);
733    }
734
735    /**
736     * This method allow to add image information on a product detail.
737     *
738     * @param array &$product
739     */
740    protected function setProductImageInformations(&$product)
741    {
742        if (isset($product['product_attribute_id']) && $product['product_attribute_id']) {
743            $id_image = Db::getInstance()->getValue('
744                SELECT `image_shop`.id_image
745                FROM `' . _DB_PREFIX_ . 'product_attribute_image` pai' .
746                Shop::addSqlAssociation('image', 'pai', true) . '
747                LEFT JOIN `' . _DB_PREFIX_ . 'image` i ON (i.`id_image` = pai.`id_image`)
748                WHERE id_product_attribute = ' . (int) $product['product_attribute_id'] . ' ORDER by i.position ASC');
749        }
750
751        if (!isset($id_image) || !$id_image) {
752            $id_image = Db::getInstance()->getValue(
753                'SELECT `image_shop`.id_image
754                FROM `' . _DB_PREFIX_ . 'image` i' .
755                Shop::addSqlAssociation('image', 'i', true, 'image_shop.cover=1') . '
756                WHERE i.id_product = ' . (int) $product['product_id']
757            );
758        }
759
760        $product['image'] = null;
761        $product['image_size'] = null;
762
763        if ($id_image) {
764            $product['image'] = new Image($id_image);
765        }
766    }
767
768    public function getTaxesAverageUsed()
769    {
770        return Cart::getTaxesAverageUsed((int) $this->id_cart);
771    }
772
773    /**
774     * Count virtual products in order.
775     *
776     * @return int number of virtual products
777     */
778    public function getVirtualProducts()
779    {
780        $sql = '
781            SELECT `product_id`, `product_attribute_id`, `download_hash`, `download_deadline`
782            FROM `' . _DB_PREFIX_ . 'order_detail` od
783            WHERE od.`id_order` = ' . (int) $this->id . '
784                AND `download_hash` <> \'\'';
785
786        return Db::getInstance()->executeS($sql);
787    }
788
789    /**
790     * Check if order contains (only) virtual products.
791     *
792     * @param bool $strict If false return true if there are at least one product virtual
793     *
794     * @return bool true if is a virtual order or false
795     */
796    public function isVirtual($strict = true)
797    {
798        $products = $this->getProducts(false, false, false, false);
799        if (count($products) < 1) {
800            return false;
801        }
802
803        $virtual = true;
804
805        foreach ($products as $product) {
806            if ($strict === false && (bool) $product['is_virtual']) {
807                return true;
808            }
809
810            $virtual &= (bool) $product['is_virtual'];
811        }
812
813        return $virtual;
814    }
815
816    /**
817     * @deprecated 1.5.0.1 use Order::getCartRules() instead
818     */
819    public function getDiscounts($details = false)
820    {
821        Tools::displayAsDeprecated('Use Order::getCartRules() instead');
822
823        return Order::getCartRules();
824    }
825
826    public function getCartRules()
827    {
828        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
829        SELECT *
830        FROM `' . _DB_PREFIX_ . 'order_cart_rule` ocr
831        WHERE ocr.`deleted` = 0 AND ocr.`id_order` = ' . (int) $this->id);
832    }
833
834    /**
835     *  Return the list of all order cart rules, even the softy deleted ones
836     *
837     * @return array|false
838     *
839     * @throws PrestaShopDatabaseException
840     */
841    public function getDeletedCartRules()
842    {
843        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
844        SELECT *
845        FROM `' . _DB_PREFIX_ . 'order_cart_rule` ocr
846        WHERE ocr.`deleted` = 1 AND ocr.`id_order` = ' . (int) $this->id);
847    }
848
849    public static function getDiscountsCustomer($id_customer, $id_cart_rule)
850    {
851        $cache_id = 'Order::getDiscountsCustomer_' . (int) $id_customer . '-' . (int) $id_cart_rule;
852        if (!Cache::isStored($cache_id)) {
853            $result = (int) Db::getInstance()->getValue('
854            SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'orders` o
855            LEFT JOIN `' . _DB_PREFIX_ . 'order_cart_rule` ocr ON (ocr.`id_order` = o.`id_order`)
856            WHERE o.`id_customer` = ' . (int) $id_customer . '
857            AND ocr.`deleted` = 0 AND ocr.`id_cart_rule` = ' . (int) $id_cart_rule);
858            Cache::store($cache_id, $result);
859
860            return $result;
861        }
862
863        return Cache::retrieve($cache_id);
864    }
865
866    /**
867     * Get current order status (eg. Awaiting payment, Delivered...).
868     *
869     * @return int Order status id
870     */
871    public function getCurrentState()
872    {
873        return $this->current_state;
874    }
875
876    /**
877     * Get current order status name (eg. Awaiting payment, Delivered...).
878     *
879     * @return array Order status details
880     */
881    public function getCurrentStateFull($id_lang)
882    {
883        return Db::getInstance()->getRow('
884            SELECT os.`id_order_state`, osl.`name`, os.`logable`, os.`shipped`
885            FROM `' . _DB_PREFIX_ . 'order_state` os
886            LEFT JOIN `' . _DB_PREFIX_ . 'order_state_lang` osl ON (osl.`id_order_state` = os.`id_order_state`)
887            WHERE osl.`id_lang` = ' . (int) $id_lang . ' AND os.`id_order_state` = ' . (int) $this->current_state);
888    }
889
890    public function hasBeenDelivered()
891    {
892        return count($this->getHistory((int) $this->id_lang, false, false, OrderState::FLAG_DELIVERY));
893    }
894
895    /**
896     * Has products returned by the merchant or by the customer?
897     */
898    public function hasProductReturned()
899    {
900        return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
901            SELECT IFNULL(SUM(ord.product_quantity), SUM(product_quantity_return))
902            FROM `' . _DB_PREFIX_ . 'orders` o
903            INNER JOIN `' . _DB_PREFIX_ . 'order_detail` od
904            ON od.id_order = o.id_order
905            LEFT JOIN `' . _DB_PREFIX_ . 'order_return_detail` ord
906            ON ord.id_order_detail = od.id_order_detail
907            WHERE o.id_order = ' . (int) $this->id);
908    }
909
910    public function hasBeenPaid()
911    {
912        return count($this->getHistory((int) $this->id_lang, false, false, OrderState::FLAG_PAID));
913    }
914
915    public function hasBeenShipped()
916    {
917        return count($this->getHistory((int) $this->id_lang, false, false, OrderState::FLAG_SHIPPED));
918    }
919
920    public function isInPreparation()
921    {
922        return count($this->getHistory((int) $this->id_lang, Configuration::get('PS_OS_PREPARATION')));
923    }
924
925    /**
926     * Checks if the current order status is paid and shipped.
927     *
928     * @return bool
929     */
930    public function isPaidAndShipped()
931    {
932        $order_state = $this->getCurrentOrderState();
933        if ($order_state && $order_state->paid && $order_state->shipped) {
934            return true;
935        }
936
937        return false;
938    }
939
940    /**
941     * Get customer orders.
942     *
943     * @param int $id_customer Customer id
944     * @param bool $show_hidden_status Display or not hidden order statuses
945     *
946     * @return array Customer orders
947     */
948    public static function getCustomerOrders($id_customer, $show_hidden_status = false, Context $context = null)
949    {
950        if (!$context) {
951            $context = Context::getContext();
952        }
953
954        $orderStates = OrderState::getOrderStates((int) $context->language->id, false);
955        $indexedOrderStates = [];
956        foreach ($orderStates as $orderState) {
957            $indexedOrderStates[$orderState['id_order_state']] = $orderState;
958        }
959
960        $res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
961        SELECT o.*,
962          (SELECT SUM(od.`product_quantity`) FROM `' . _DB_PREFIX_ . 'order_detail` od WHERE od.`id_order` = o.`id_order`) nb_products,
963          (SELECT oh.`id_order_state` FROM `' . _DB_PREFIX_ . 'order_history` oh
964           LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON (os.`id_order_state` = oh.`id_order_state`)
965           WHERE oh.`id_order` = o.`id_order` ' .
966            (!$show_hidden_status ? ' AND os.`hidden` != 1' : '') .
967            ' ORDER BY oh.`date_add` DESC, oh.`id_order_history` DESC LIMIT 1) id_order_state
968        FROM `' . _DB_PREFIX_ . 'orders` o
969        WHERE o.`id_customer` = ' . (int) $id_customer .
970            Shop::addSqlRestriction(Shop::SHARE_ORDER) . '
971        GROUP BY o.`id_order`
972        ORDER BY o.`date_add` DESC');
973
974        if (!$res) {
975            return [];
976        }
977
978        foreach ($res as $key => $val) {
979            // In case order creation crashed midway some data might be absent
980            $orderState = !empty($val['id_order_state']) ? $indexedOrderStates[$val['id_order_state']] : null;
981            $res[$key]['order_state'] = $orderState['name'] ?: null;
982            $res[$key]['invoice'] = $orderState['invoice'] ?: null;
983            $res[$key]['order_state_color'] = $orderState['color'] ?: null;
984        }
985
986        return $res;
987    }
988
989    public static function getOrdersIdByDate($date_from, $date_to, $id_customer = null, $type = null)
990    {
991        $sql = 'SELECT `id_order`
992                FROM `' . _DB_PREFIX_ . 'orders`
993                WHERE DATE_ADD(date_upd, INTERVAL -1 DAY) <= \'' . pSQL($date_to) . '\' AND date_upd >= \'' . pSQL($date_from) . '\'
994                    ' . Shop::addSqlRestriction()
995                    . ($type ? ' AND `' . bqSQL($type) . '_number` != 0' : '')
996                    . ($id_customer ? ' AND id_customer = ' . (int) $id_customer : '');
997        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
998
999        $orders = [];
1000        foreach ($result as $order) {
1001            $orders[] = (int) $order['id_order'];
1002        }
1003
1004        return $orders;
1005    }
1006
1007    public static function getOrdersWithInformations($limit = null, Context $context = null)
1008    {
1009        if (!$context) {
1010            $context = Context::getContext();
1011        }
1012
1013        $sql = 'SELECT *, (
1014                    SELECT osl.`name`
1015                    FROM `' . _DB_PREFIX_ . 'order_state_lang` osl
1016                    WHERE osl.`id_order_state` = o.`current_state`
1017                    AND osl.`id_lang` = ' . (int) $context->language->id . '
1018                    LIMIT 1
1019                ) AS `state_name`, o.`date_add` AS `date_add`, o.`date_upd` AS `date_upd`
1020                FROM `' . _DB_PREFIX_ . 'orders` o
1021                LEFT JOIN `' . _DB_PREFIX_ . 'customer` c ON (c.`id_customer` = o.`id_customer`)
1022                WHERE 1
1023                    ' . Shop::addSqlRestriction(false, 'o') . '
1024                ORDER BY o.`date_add` DESC
1025                ' . ((int) $limit ? 'LIMIT 0, ' . (int) $limit : '');
1026
1027        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
1028    }
1029
1030    /**
1031     * @deprecated since 1.5.0.2
1032     *
1033     * @param $date_from
1034     * @param $date_to
1035     * @param $id_customer
1036     * @param $type
1037     *
1038     * @return array
1039     */
1040    public static function getOrdersIdInvoiceByDate($date_from, $date_to, $id_customer = null, $type = null)
1041    {
1042        Tools::displayAsDeprecated();
1043        $sql = 'SELECT `id_order`
1044                FROM `' . _DB_PREFIX_ . 'orders`
1045                WHERE DATE_ADD(invoice_date, INTERVAL -1 DAY) <= \'' . pSQL($date_to) . '\' AND invoice_date >= \'' . pSQL($date_from) . '\'
1046                    ' . Shop::addSqlRestriction()
1047                    . ($type ? ' AND `' . bqSQL($type) . '_number` != 0' : '')
1048                    . ($id_customer ? ' AND id_customer = ' . (int) $id_customer : '') .
1049                ' ORDER BY invoice_date ASC';
1050        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
1051
1052        $orders = [];
1053        foreach ($result as $order) {
1054            $orders[] = (int) $order['id_order'];
1055        }
1056
1057        return $orders;
1058    }
1059
1060    /**
1061     * @deprecated 1.5.0.3
1062     *
1063     * @param $id_order_state
1064     *
1065     * @return array
1066     */
1067    public static function getOrderIdsByStatus($id_order_state)
1068    {
1069        Tools::displayAsDeprecated();
1070        $sql = 'SELECT id_order
1071                FROM ' . _DB_PREFIX_ . 'orders o
1072                WHERE o.`current_state` = ' . (int) $id_order_state . '
1073                ' . Shop::addSqlRestriction(false, 'o') . '
1074                ORDER BY invoice_date ASC';
1075        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
1076
1077        $orders = [];
1078        foreach ($result as $order) {
1079            $orders[] = (int) $order['id_order'];
1080        }
1081
1082        return $orders;
1083    }
1084
1085    /**
1086     * Get product total without taxes.
1087     *
1088     * @return Product total without taxes
1089     */
1090    public function getTotalProductsWithoutTaxes($products = false)
1091    {
1092        return $this->total_products;
1093    }
1094
1095    /**
1096     * Get product total with taxes.
1097     *
1098     * @return Product total with taxes
1099     */
1100    public function getTotalProductsWithTaxes($products = false)
1101    {
1102        if ($this->total_products_wt != '0.00' && !$products) {
1103            return $this->total_products_wt;
1104        }
1105        /* Retro-compatibility (now set directly on the validateOrder() method) */
1106
1107        if (!$products) {
1108            $products = $this->getProductsDetail();
1109        }
1110
1111        $return = 0;
1112        foreach ($products as $row) {
1113            $return += $row['total_price_tax_incl'];
1114        }
1115
1116        if (!$products) {
1117            $this->total_products_wt = $return;
1118            $this->update();
1119        }
1120
1121        return $return;
1122    }
1123
1124    /**
1125     * used to cache order customer.
1126     */
1127    protected $cacheCustomer = null;
1128
1129    /**
1130     * Get order customer.
1131     *
1132     * @return Customer $customer
1133     */
1134    public function getCustomer()
1135    {
1136        if (null === $this->cacheCustomer) {
1137            $this->cacheCustomer = new Customer((int) $this->id_customer);
1138        }
1139
1140        return $this->cacheCustomer;
1141    }
1142
1143    /**
1144     * Get customer orders number.
1145     *
1146     * @param int $id_customer Customer id
1147     *
1148     * @return int Customer orders number
1149     */
1150    public static function getCustomerNbOrders($id_customer)
1151    {
1152        $sql = 'SELECT COUNT(`id_order`) AS nb
1153                FROM `' . _DB_PREFIX_ . 'orders`
1154                WHERE `id_customer` = ' . (int) $id_customer
1155                    . Shop::addSqlRestriction();
1156        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql);
1157
1158        return isset($result['nb']) ? (int) $result['nb'] : 0;
1159    }
1160
1161    /**
1162     * Get an order id by its cart id.
1163     *
1164     * @param int $id_cart Cart id
1165     *
1166     * @return int Order id
1167     *
1168     * @deprecated since 1.7.1.0 Use getIdByCartId() instead
1169     */
1170    public static function getOrderByCartId($id_cart)
1171    {
1172        return self::getIdByCartId($id_cart);
1173    }
1174
1175    /**
1176     * Get an order object by its cart id.
1177     *
1178     * @param int $id_cart Cart id
1179     *
1180     * @return OrderCore
1181     */
1182    public static function getByCartId($id_cart)
1183    {
1184        $id_order = (int) self::getIdByCartId((int) $id_cart);
1185
1186        return ($id_order > 0) ? new static($id_order) : null;
1187    }
1188
1189    /**
1190     * Get the order id by its cart id.
1191     *
1192     * @param int $id_cart Cart id
1193     *
1194     * @return int $id_order
1195     */
1196    public static function getIdByCartId($id_cart)
1197    {
1198        $sql = 'SELECT `id_order`
1199            FROM `' . _DB_PREFIX_ . 'orders`
1200            WHERE `id_cart` = ' . (int) $id_cart .
1201            Shop::addSqlRestriction();
1202
1203        $result = Db::getInstance()->getValue($sql);
1204
1205        return !empty($result) ? (int) $result : false;
1206    }
1207
1208    /**
1209     * @deprecated 1.5.0.1
1210     * @see Order::addCartRule()
1211     *
1212     * @param int $id_cart_rule
1213     * @param string $name
1214     * @param float $value
1215     *
1216     * @return bool
1217     */
1218    public function addDiscount($id_cart_rule, $name, $value)
1219    {
1220        Tools::displayAsDeprecated('Use Order::addCartRule($id_cart_rule, $name, array(\'tax_incl\' => $value, \'tax_excl\' => \'0.00\')) instead');
1221
1222        return Order::addCartRule($id_cart_rule, $name, ['tax_incl' => $value, 'tax_excl' => '0.00']);
1223    }
1224
1225    /**
1226     * @since 1.5.0.1
1227     *
1228     * @param int $id_cart_rule
1229     * @param string $name
1230     * @param array $values
1231     * @param int $id_order_invoice
1232     *
1233     * @return bool
1234     */
1235    public function addCartRule($id_cart_rule, $name, $values, $id_order_invoice = 0, $free_shipping = null)
1236    {
1237        $order_cart_rule = new OrderCartRule();
1238        $order_cart_rule->id_order = $this->id;
1239        $order_cart_rule->id_cart_rule = $id_cart_rule;
1240        $order_cart_rule->id_order_invoice = $id_order_invoice;
1241        $order_cart_rule->name = $name;
1242        $order_cart_rule->value = $values['tax_incl'];
1243        $order_cart_rule->value_tax_excl = $values['tax_excl'];
1244        if ($free_shipping === null) {
1245            $cart_rule = new CartRule($id_cart_rule);
1246            $free_shipping = $cart_rule->free_shipping;
1247        }
1248        $order_cart_rule->free_shipping = (int) $free_shipping;
1249        $order_cart_rule->add();
1250    }
1251
1252    public function getNumberOfDays()
1253    {
1254        $nb_return_days = (int) Configuration::get('PS_ORDER_RETURN_NB_DAYS', null, null, $this->id_shop);
1255        if (!$nb_return_days) {
1256            return true;
1257        }
1258        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('
1259        SELECT TO_DAYS("' . date('Y-m-d') . ' 00:00:00") - TO_DAYS(`delivery_date`)  AS days FROM `' . _DB_PREFIX_ . 'orders`
1260        WHERE `id_order` = ' . (int) $this->id);
1261        if ($result['days'] <= $nb_return_days) {
1262            return true;
1263        }
1264
1265        return false;
1266    }
1267
1268    /**
1269     * Can this order be returned by the client?
1270     *
1271     * @return bool
1272     */
1273    public function isReturnable()
1274    {
1275        if (Configuration::get('PS_ORDER_RETURN', null, null, $this->id_shop) && $this->isPaidAndShipped()) {
1276            return $this->getNumberOfDays();
1277        }
1278
1279        return false;
1280    }
1281
1282    public static function getLastInvoiceNumber()
1283    {
1284        $sql = 'SELECT MAX(`number`) FROM `' . _DB_PREFIX_ . 'order_invoice`';
1285        if (Configuration::get('PS_INVOICE_RESET')) {
1286            $sql .= ' WHERE DATE_FORMAT(`date_add`, "%Y") = ' . (int) date('Y');
1287        }
1288
1289        return Db::getInstance()->getValue($sql);
1290    }
1291
1292    public static function setLastInvoiceNumber($order_invoice_id, $id_shop)
1293    {
1294        if (!$order_invoice_id) {
1295            return false;
1296        }
1297
1298        $number = Configuration::get('PS_INVOICE_START_NUMBER', null, null, $id_shop);
1299        // If invoice start number has been set, you clean the value of this configuration
1300        if ($number) {
1301            Configuration::updateValue('PS_INVOICE_START_NUMBER', false, false, null, $id_shop);
1302        }
1303
1304        $sql = 'UPDATE `' . _DB_PREFIX_ . 'order_invoice` SET number =';
1305
1306        if ($number) {
1307            $sql .= (int) $number;
1308        } else {
1309            $getNumberSql = '(SELECT new_number FROM (SELECT (MAX(`number`) + 1) AS new_number
1310                FROM `' . _DB_PREFIX_ . 'order_invoice`' . (Configuration::get('PS_INVOICE_RESET') ?
1311                ' WHERE DATE_FORMAT(`date_add`, "%Y") = ' . (int) date('Y') : '') . ') AS result)';
1312            $getNumberSqlRow = Db::getInstance()->getRow($getNumberSql);
1313            $newInvoiceNumber = $getNumberSqlRow['new_number'];
1314            $sql .= $newInvoiceNumber;
1315        }
1316
1317        $sql .= ' WHERE `id_order_invoice` = ' . (int) $order_invoice_id;
1318
1319        return Db::getInstance()->execute($sql);
1320    }
1321
1322    public function getInvoiceNumber($order_invoice_id)
1323    {
1324        if (!$order_invoice_id) {
1325            return false;
1326        }
1327
1328        return Db::getInstance()->getValue(
1329            'SELECT `number`
1330            FROM `' . _DB_PREFIX_ . 'order_invoice`
1331            WHERE `id_order_invoice` = ' . (int) $order_invoice_id
1332        );
1333    }
1334
1335    /**
1336     * This method allows to generate first invoice of the current order.
1337     */
1338    public function setInvoice($use_existing_payment = false)
1339    {
1340        if (!$this->hasInvoice()) {
1341            if ($id = (int) $this->getOrderInvoiceIdIfHasDelivery()) {
1342                $order_invoice = new OrderInvoice($id);
1343            } else {
1344                $order_invoice = new OrderInvoice();
1345            }
1346            $order_invoice->id_order = $this->id;
1347            if (!$id) {
1348                $order_invoice->number = 0;
1349            }
1350
1351            // Save Order invoice
1352
1353            $this->setInvoiceDetails($order_invoice);
1354
1355            if (Configuration::get('PS_INVOICE')) {
1356                $this->setLastInvoiceNumber($order_invoice->id, $this->id_shop);
1357            }
1358
1359            // Update order_carrier
1360            $id_order_carrier = Db::getInstance()->getValue('
1361                SELECT `id_order_carrier`
1362                FROM `' . _DB_PREFIX_ . 'order_carrier`
1363                WHERE `id_order` = ' . (int) $order_invoice->id_order . '
1364                AND (`id_order_invoice` IS NULL OR `id_order_invoice` = 0)');
1365
1366            if ($id_order_carrier) {
1367                $order_carrier = new OrderCarrier($id_order_carrier);
1368                $order_carrier->id_order_invoice = (int) $order_invoice->id;
1369                $order_carrier->update();
1370            }
1371
1372            // Update order detail
1373            Db::getInstance()->execute('
1374                UPDATE `' . _DB_PREFIX_ . 'order_detail`
1375                SET `id_order_invoice` = ' . (int) $order_invoice->id . '
1376                WHERE `id_order` = ' . (int) $order_invoice->id_order);
1377
1378            // Update order payment
1379            if ($use_existing_payment) {
1380                $id_order_payments = Db::getInstance()->executeS('
1381                    SELECT DISTINCT op.id_order_payment
1382                    FROM `' . _DB_PREFIX_ . 'order_payment` op
1383                    INNER JOIN `' . _DB_PREFIX_ . 'orders` o ON (o.reference = op.order_reference)
1384                    LEFT JOIN `' . _DB_PREFIX_ . 'order_invoice_payment` oip ON (oip.id_order_payment = op.id_order_payment)
1385                    WHERE (oip.id_order != ' . (int) $order_invoice->id_order . ' OR oip.id_order IS NULL) AND o.id_order = ' . (int) $order_invoice->id_order);
1386
1387                if (count($id_order_payments)) {
1388                    foreach ($id_order_payments as $order_payment) {
1389                        Db::getInstance()->execute('
1390                            INSERT INTO `' . _DB_PREFIX_ . 'order_invoice_payment`
1391                            SET
1392                                `id_order_invoice` = ' . (int) $order_invoice->id . ',
1393                                `id_order_payment` = ' . (int) $order_payment['id_order_payment'] . ',
1394                                `id_order` = ' . (int) $order_invoice->id_order);
1395                    }
1396                    // Clear cache
1397                    Cache::clean('order_invoice_paid_*');
1398                }
1399            }
1400
1401            // Update order cart rule
1402            Db::getInstance()->execute('
1403                UPDATE `' . _DB_PREFIX_ . 'order_cart_rule`
1404                SET `id_order_invoice` = ' . (int) $order_invoice->id . '
1405                WHERE `id_order` = ' . (int) $order_invoice->id_order);
1406
1407            // Keep it for backward compatibility, to remove on 1.6 version
1408            $this->invoice_date = $order_invoice->date_add;
1409
1410            if (Configuration::get('PS_INVOICE')) {
1411                $this->invoice_number = $this->getInvoiceNumber($order_invoice->id);
1412                $invoice_number = Hook::exec('actionSetInvoice', [
1413                    get_class($this) => $this,
1414                    get_class($order_invoice) => $order_invoice,
1415                    'use_existing_payment' => (bool) $use_existing_payment,
1416                ]);
1417
1418                if (is_numeric($invoice_number)) {
1419                    $this->invoice_number = (int) $invoice_number;
1420                } else {
1421                    $this->invoice_number = $this->getInvoiceNumber($order_invoice->id);
1422                }
1423            }
1424
1425            $this->update();
1426        }
1427    }
1428
1429    /**
1430     * This method allows to fulfill the object order_invoice with sales figures.
1431     */
1432    protected function setInvoiceDetails($order_invoice)
1433    {
1434        if (!$order_invoice || !is_object($order_invoice)) {
1435            return;
1436        }
1437
1438        $address = new Address((int) $this->{Configuration::get('PS_TAX_ADDRESS_TYPE')});
1439        $carrier = new Carrier((int) $this->id_carrier);
1440        $tax_calculator = (Configuration::get('PS_ATCP_SHIPWRAP')) ? ServiceLocator::get('AverageTaxOfProductsTaxCalculator')->setIdOrder($this->id) : $carrier->getTaxCalculator($address);
1441        $order_invoice->total_discount_tax_excl = $this->total_discounts_tax_excl;
1442        $order_invoice->total_discount_tax_incl = $this->total_discounts_tax_incl;
1443        $order_invoice->total_paid_tax_excl = $this->total_paid_tax_excl;
1444        $order_invoice->total_paid_tax_incl = $this->total_paid_tax_incl;
1445        $order_invoice->total_products = $this->total_products;
1446        $order_invoice->total_products_wt = $this->total_products_wt;
1447        $order_invoice->total_shipping_tax_excl = $this->total_shipping_tax_excl;
1448        $order_invoice->total_shipping_tax_incl = $this->total_shipping_tax_incl;
1449        $order_invoice->shipping_tax_computation_method = $tax_calculator->computation_method;
1450        $order_invoice->total_wrapping_tax_excl = $this->total_wrapping_tax_excl;
1451        $order_invoice->total_wrapping_tax_incl = $this->total_wrapping_tax_incl;
1452        $order_invoice->save();
1453
1454        if (Configuration::get('PS_ATCP_SHIPWRAP')) {
1455            $wrapping_tax_calculator = ServiceLocator::get('AverageTaxOfProductsTaxCalculator')->setIdOrder($this->id);
1456        } else {
1457            $wrapping_tax_manager = TaxManagerFactory::getManager($address, (int) Configuration::get('PS_GIFT_WRAPPING_TAX_RULES_GROUP'));
1458            $wrapping_tax_calculator = $wrapping_tax_manager->getTaxCalculator();
1459        }
1460
1461        $order_invoice->saveCarrierTaxCalculator(
1462            $tax_calculator->getTaxesAmount(
1463                $order_invoice->total_shipping_tax_excl,
1464                $order_invoice->total_shipping_tax_incl,
1465                Context::getContext()->getComputingPrecision(),
1466                $this->round_mode
1467            )
1468        );
1469        $order_invoice->saveWrappingTaxCalculator(
1470            $wrapping_tax_calculator->getTaxesAmount(
1471                $order_invoice->total_wrapping_tax_excl,
1472                $order_invoice->total_wrapping_tax_incl,
1473                Context::getContext()->getComputingPrecision(),
1474                $this->round_mode
1475            )
1476        );
1477    }
1478
1479    /**
1480     * This method allows to generate first delivery slip of the current order.
1481     */
1482    public function setDeliverySlip()
1483    {
1484        if (!$this->hasInvoice()) {
1485            $order_invoice = new OrderInvoice();
1486            $order_invoice->id_order = $this->id;
1487            $order_invoice->number = 0;
1488            $this->setInvoiceDetails($order_invoice);
1489            $this->delivery_date = $order_invoice->date_add;
1490            $this->delivery_number = $this->getDeliveryNumber($order_invoice->id);
1491            $this->update();
1492        }
1493    }
1494
1495    public function setDeliveryNumber($order_invoice_id, $id_shop)
1496    {
1497        if (!$order_invoice_id) {
1498            return false;
1499        }
1500
1501        $id_shop = shop::getTotalShops() > 1 ? $id_shop : null;
1502
1503        $number = Configuration::get('PS_DELIVERY_NUMBER', null, null, $id_shop);
1504        // If delivery slip start number has been set, you clean the value of this configuration
1505        if ($number) {
1506            Configuration::updateValue('PS_DELIVERY_NUMBER', false, false, null, $id_shop);
1507        }
1508
1509        $sql = 'UPDATE `' . _DB_PREFIX_ . 'order_invoice` SET delivery_number =';
1510
1511        if ($number) {
1512            $sql .= (int) $number;
1513        } else {
1514            $getNumberSql = '(SELECT new_number FROM (SELECT (MAX(`delivery_number`) + 1) AS new_number
1515                FROM `' . _DB_PREFIX_ . 'order_invoice`) AS result)';
1516            $newInvoiceNumber = Db::getInstance()->getValue($getNumberSql);
1517            $sql .= $newInvoiceNumber;
1518        }
1519
1520        $sql .= ' WHERE `id_order_invoice` = ' . (int) $order_invoice_id;
1521
1522        return Db::getInstance()->execute($sql);
1523    }
1524
1525    public function getDeliveryNumber($order_invoice_id)
1526    {
1527        if (!$order_invoice_id) {
1528            return false;
1529        }
1530
1531        return Db::getInstance()->getValue(
1532            'SELECT `delivery_number`
1533            FROM `' . _DB_PREFIX_ . 'order_invoice`
1534            WHERE `id_order_invoice` = ' . (int) $order_invoice_id
1535        );
1536    }
1537
1538    public function setDelivery()
1539    {
1540        // Get all invoice
1541        $order_invoice_collection = $this->getInvoicesCollection();
1542        foreach ($order_invoice_collection as $order_invoice) {
1543            /** @var OrderInvoice $order_invoice */
1544            if ($order_invoice->delivery_number) {
1545                continue;
1546            }
1547
1548            // Set delivery number on invoice
1549            $order_invoice->delivery_number = 0;
1550            $order_invoice->delivery_date = date('Y-m-d H:i:s');
1551            // Update Order Invoice
1552            $order_invoice->update();
1553            $this->setDeliveryNumber($order_invoice->id, $this->id_shop);
1554            $this->delivery_number = $this->getDeliveryNumber($order_invoice->id);
1555        }
1556
1557        // Keep it for backward compatibility, to remove on 1.6 version
1558        // Set delivery date
1559        $this->delivery_date = date('Y-m-d H:i:s');
1560        // Update object
1561        $this->update();
1562    }
1563
1564    public static function getByDelivery($id_delivery)
1565    {
1566        $sql = 'SELECT id_order
1567                FROM `' . _DB_PREFIX_ . 'orders`
1568                WHERE `delivery_number` = ' . (int) $id_delivery . '
1569                ' . Shop::addSqlRestriction();
1570        $res = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql);
1571
1572        return new Order((int) $res['id_order']);
1573    }
1574
1575    /**
1576     * Get a collection of orders using reference.
1577     *
1578     * @since 1.5.0.14
1579     *
1580     * @param string $reference
1581     *
1582     * @return PrestaShopCollection Collection of Order
1583     */
1584    public static function getByReference($reference)
1585    {
1586        $orders = new PrestaShopCollection('Order');
1587        $orders->where('reference', '=', $reference);
1588
1589        return $orders;
1590    }
1591
1592    /**
1593     * The combination (reference, email) should be unique, of multiple entries are found, then we take the first one.
1594     *
1595     * @param $reference Order reference
1596     * @param $email customer email address
1597     *
1598     * @return Order The first order found
1599     */
1600    public static function getByReferenceAndEmail($reference, $email)
1601    {
1602        $sql = '
1603          SELECT id_order
1604            FROM `' . _DB_PREFIX_ . 'orders` o
1605            LEFT JOIN `' . _DB_PREFIX_ . 'customer` c ON (o.`id_customer` = c.`id_customer`)
1606                WHERE o.`reference` = \'' . pSQL($reference) . '\' AND c.`email` = \'' . pSQL($email) . '\'
1607        ';
1608
1609        $id = (int) Db::getInstance()->getValue($sql);
1610
1611        return new Order($id);
1612    }
1613
1614    public function getTotalWeight()
1615    {
1616        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
1617        SELECT SUM(product_weight * product_quantity)
1618        FROM ' . _DB_PREFIX_ . 'order_detail
1619        WHERE id_order = ' . (int) $this->id);
1620
1621        return (float) $result;
1622    }
1623
1624    /**
1625     * @param int $id_invoice
1626     *
1627     * @deprecated 1.5.0.1
1628     */
1629    public static function getInvoice($id_invoice)
1630    {
1631        Tools::displayAsDeprecated();
1632
1633        return Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('
1634        SELECT `invoice_number`, `id_order`
1635        FROM `' . _DB_PREFIX_ . 'orders`
1636        WHERE invoice_number = ' . (int) $id_invoice);
1637    }
1638
1639    public function isAssociatedAtGuest($email)
1640    {
1641        if (!$email) {
1642            return false;
1643        }
1644        $sql = 'SELECT COUNT(*)
1645                FROM `' . _DB_PREFIX_ . 'orders` o
1646                LEFT JOIN `' . _DB_PREFIX_ . 'customer` c ON (c.`id_customer` = o.`id_customer`)
1647                WHERE o.`id_order` = ' . (int) $this->id . '
1648                    AND c.`email` = \'' . pSQL($email) . '\'
1649                    AND c.`is_guest` = 1
1650                    ' . Shop::addSqlRestriction(false, 'c');
1651
1652        return (bool) Db::getInstance()->getValue($sql);
1653    }
1654
1655    /**
1656     * @param int $id_order
1657     * @param int $id_customer optionnal
1658     *
1659     * @return int id_cart
1660     */
1661    public static function getCartIdStatic($id_order, $id_customer = 0)
1662    {
1663        return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
1664            SELECT `id_cart`
1665            FROM `' . _DB_PREFIX_ . 'orders`
1666            WHERE `id_order` = ' . (int) $id_order . '
1667            ' . ($id_customer ? 'AND `id_customer` = ' . (int) $id_customer : ''));
1668    }
1669
1670    public function getWsOrderRows()
1671    {
1672        $query = '
1673            SELECT
1674            `id_order_detail` as `id`,
1675            `product_id`,
1676            `product_price`,
1677            `id_order`,
1678            `product_attribute_id`,
1679            `id_customization`,
1680            `product_quantity`,
1681            `product_name`,
1682            `product_reference`,
1683            `product_ean13`,
1684            `product_isbn`,
1685            `product_upc`,
1686            `unit_price_tax_incl`,
1687            `unit_price_tax_excl`
1688            FROM `' . _DB_PREFIX_ . 'order_detail`
1689            WHERE id_order = ' . (int) $this->id;
1690        $result = Db::getInstance()->executeS($query);
1691
1692        return $result;
1693    }
1694
1695    /** Set current order status
1696     * @param int $id_order_state
1697     * @param int $id_employee (/!\ not optional except for Webservice
1698     */
1699    public function setCurrentState($id_order_state, $id_employee = 0)
1700    {
1701        if (empty($id_order_state) || (int) $id_order_state === (int) $this->current_state) {
1702            return false;
1703        }
1704        $history = new OrderHistory();
1705        $history->id_order = (int) $this->id;
1706        $history->id_employee = (int) $id_employee;
1707        $use_existings_payment = !$this->hasInvoice();
1708        $history->changeIdOrderState((int) $id_order_state, $this, $use_existings_payment);
1709        $res = Db::getInstance()->getRow('
1710            SELECT `invoice_number`, `invoice_date`, `delivery_number`, `delivery_date`
1711            FROM `' . _DB_PREFIX_ . 'orders`
1712            WHERE `id_order` = ' . (int) $this->id);
1713        $this->invoice_date = $res['invoice_date'];
1714        $this->invoice_number = $res['invoice_number'];
1715        $this->delivery_date = $res['delivery_date'];
1716        $this->delivery_number = $res['delivery_number'];
1717        $this->update();
1718
1719        $history->addWithemail();
1720    }
1721
1722    public function addWs($autodate = true, $null_values = false)
1723    {
1724        /** @var PaymentModule $payment_module */
1725        $payment_module = Module::getInstanceByName($this->module);
1726        $customer = new Customer($this->id_customer);
1727        $payment_module->validateOrder($this->id_cart, Configuration::get('PS_OS_WS_PAYMENT'), $this->total_paid, $this->payment, null, [], null, false, $customer->secure_key);
1728        $this->id = $payment_module->currentOrder;
1729
1730        return true;
1731    }
1732
1733    public function deleteAssociations()
1734    {
1735        return Db::getInstance()->execute('
1736                DELETE FROM `' . _DB_PREFIX_ . 'order_detail`
1737                WHERE `id_order` = ' . (int) $this->id) !== false;
1738    }
1739
1740    /**
1741     * This method return the ID of the previous order.
1742     *
1743     * @since 1.5.0.1
1744     *
1745     * @return int
1746     */
1747    public function getPreviousOrderId()
1748    {
1749        return Db::getInstance()->getValue('
1750            SELECT id_order
1751            FROM ' . _DB_PREFIX_ . 'orders
1752            WHERE id_order < ' . (int) $this->id
1753            . Shop::addSqlRestriction() . '
1754            ORDER BY id_order DESC');
1755    }
1756
1757    /**
1758     * This method return the ID of the next order.
1759     *
1760     * @since 1.5.0.1
1761     *
1762     * @return int
1763     */
1764    public function getNextOrderId()
1765    {
1766        return Db::getInstance()->getValue('
1767            SELECT id_order
1768            FROM ' . _DB_PREFIX_ . 'orders
1769            WHERE id_order > ' . (int) $this->id
1770            . Shop::addSqlRestriction() . '
1771            ORDER BY id_order ASC');
1772    }
1773
1774    /**
1775     * Get the an order detail list of the current order.
1776     *
1777     * @return array
1778     */
1779    public function getOrderDetailList()
1780    {
1781        return OrderDetail::getList($this->id);
1782    }
1783
1784    /**
1785     * Generate a unique reference for orders generated with the same cart id
1786     * This references, is useful for check payment.
1787     *
1788     * @return string
1789     */
1790    public static function generateReference()
1791    {
1792        return strtoupper(Tools::passwdGen(9, 'NO_NUMERIC'));
1793    }
1794
1795    public function orderContainProduct($id_product)
1796    {
1797        $product_list = $this->getOrderDetailList();
1798        foreach ($product_list as $product) {
1799            if ($product['product_id'] == (int) $id_product) {
1800                return true;
1801            }
1802        }
1803
1804        return false;
1805    }
1806
1807    /**
1808     * This method returns true if at least one order details uses the
1809     * One After Another tax computation method.
1810     *
1811     * @since 1.5.0.1
1812     *
1813     * @return bool
1814     */
1815    public function useOneAfterAnotherTaxComputationMethod()
1816    {
1817        // if one of the order details use the tax computation method the display will be different
1818        return Db::getInstance()->getValue(
1819            '
1820    		SELECT od.`tax_computation_method`
1821    		FROM `' . _DB_PREFIX_ . 'order_detail_tax` odt
1822    		LEFT JOIN `' . _DB_PREFIX_ . 'order_detail` od ON (od.`id_order_detail` = odt.`id_order_detail`)
1823    		WHERE od.`id_order` = ' . (int) $this->id . '
1824    		AND od.`tax_computation_method` = ' . (int) TaxCalculator::ONE_AFTER_ANOTHER_METHOD
1825        );
1826    }
1827
1828    /**
1829     * This method allows to get all Order Payment for the current order.
1830     *
1831     * @since 1.5.0.1
1832     *
1833     * @return PrestaShopCollection Collection of OrderPayment
1834     */
1835    public function getOrderPaymentCollection()
1836    {
1837        $order_payments = new PrestaShopCollection('OrderPayment');
1838        $order_payments->where('order_reference', '=', $this->reference);
1839
1840        return $order_payments;
1841    }
1842
1843    /**
1844     * Indicates if order has any associated payments.
1845     *
1846     * @return bool
1847     */
1848    public function hasPayments(): bool
1849    {
1850        return $this->getOrderPaymentCollection()->count() > 0;
1851    }
1852
1853    /**
1854     * This method allows to add a payment to the current order.
1855     *
1856     * @since 1.5.0.1
1857     *
1858     * @param float $amount_paid
1859     * @param string $payment_method
1860     * @param string $payment_transaction_id
1861     * @param Currency $currency
1862     * @param string $date
1863     * @param OrderInvoice $order_invoice
1864     *
1865     * @return bool
1866     */
1867    public function addOrderPayment($amount_paid, $payment_method = null, $payment_transaction_id = null, $currency = null, $date = null, $order_invoice = null)
1868    {
1869        $order_payment = new OrderPayment();
1870        $order_payment->order_reference = $this->reference;
1871        $order_payment->id_currency = ($currency ? $currency->id : $this->id_currency);
1872        // we kept the currency rate for historization reasons
1873        $order_payment->conversion_rate = ($currency ? $currency->conversion_rate : 1);
1874        // if payment_method is define, we used this
1875        $order_payment->payment_method = ($payment_method ? $payment_method : $this->payment);
1876        $order_payment->transaction_id = $payment_transaction_id;
1877        $order_payment->amount = $amount_paid;
1878        $order_payment->date_add = ($date ? $date : null);
1879
1880        // Add time to the date if needed
1881        if ($order_payment->date_add != null && preg_match('/^[0-9]+-[0-9]+-[0-9]+$/', $order_payment->date_add)) {
1882            $order_payment->date_add .= ' ' . date('H:i:s');
1883        }
1884
1885        /*
1886         * 4 cases
1887         *
1888         * Order is in default_currency + Payment is in Order currency
1889         *    for example default = 1, order = 1, payment = 1
1890         *    ==> NO conversion to do
1891         * Order is in default_currency + Payment is NOT in Order currency
1892         *    for example default = 1, order = 1, payment = 2
1893         *    ==> convert payment in order's currency
1894         * Order is NOT in default_currency + Payment is in Order currency
1895         *    for example default = 1, order = 2, payment = 2
1896         *    ==> NO conversion to do
1897         * Order is NOT in default_currency + Payment is NOT in Order currency
1898         *    for example default = 1, order = 2, payment = 3
1899         *    ==> As conversion rates are set regarding the default currency,
1900         *        convert payment to default and from default to order's currency
1901         */
1902
1903        // Update total_paid_real value for backward compatibility reasons
1904        if ($order_payment->id_currency == $this->id_currency) {
1905            $this->total_paid_real += $order_payment->amount;
1906        } else {
1907            $default_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT');
1908            if ($this->id_currency === $default_currency) {
1909                $this->total_paid_real += Tools::ps_round(
1910                    Tools::convertPrice($order_payment->amount, $this->id_currency, false),
1911                    Context::getContext()->getComputingPrecision()
1912                );
1913            } else {
1914                $amountInDefaultCurrency = Tools::convertPrice($order_payment->amount, $order_payment->id_currency, false);
1915                $this->total_paid_real += Tools::ps_round(
1916                    Tools::convertPrice($amountInDefaultCurrency, $this->id_currency, true),
1917                    Context::getContext()->getComputingPrecision()
1918                );
1919            }
1920        }
1921
1922        // We put autodate parameter of add method to true if date_add field is null
1923        $res = $order_payment->add(null === $order_payment->date_add) && $this->update();
1924
1925        if (!$res) {
1926            return false;
1927        }
1928
1929        if (null !== $order_invoice) {
1930            $res = Db::getInstance()->execute('
1931            INSERT INTO `' . _DB_PREFIX_ . 'order_invoice_payment` (`id_order_invoice`, `id_order_payment`, `id_order`)
1932            VALUES(' . (int) $order_invoice->id . ', ' . (int) $order_payment->id . ', ' . (int) $this->id . ')');
1933
1934            // Clear cache
1935            Cache::clean('order_invoice_paid_*');
1936        }
1937
1938        return $res;
1939    }
1940
1941    /**
1942     * Returns the correct product taxes breakdown.
1943     *
1944     * Get all documents linked to the current order
1945     *
1946     * @since 1.5.0.1
1947     *
1948     * @return array
1949     */
1950    public function getDocuments()
1951    {
1952        $invoices = $this->getInvoicesCollection()->getResults();
1953        foreach ($invoices as $key => $invoice) {
1954            if (!$invoice->number) {
1955                unset($invoices[$key]);
1956            }
1957        }
1958        $delivery_slips = $this->getDeliverySlipsCollection()->getResults();
1959        // @TODO review
1960        foreach ($delivery_slips as $key => $delivery) {
1961            $delivery->is_delivery = true;
1962            $delivery->date_add = $delivery->delivery_date;
1963            if (!$invoice->delivery_number) {
1964                unset($delivery_slips[$key]);
1965            }
1966        }
1967        $order_slips = $this->getOrderSlipsCollection()->getResults();
1968
1969        $documents = array_merge($invoices, $order_slips, $delivery_slips);
1970        usort($documents, ['Order', 'sortDocuments']);
1971
1972        return $documents;
1973    }
1974
1975    public function getReturn()
1976    {
1977        return OrderReturn::getOrdersReturn($this->id_customer, $this->id);
1978    }
1979
1980    /**
1981     * @return array return all shipping method for the current order
1982     *               state_name sql var is now deprecated - use order_state_name for the state name and carrier_name for the carrier_name
1983     */
1984    public function getShipping()
1985    {
1986        $results = Db::getInstance()->executeS(
1987            'SELECT DISTINCT oc.`id_order_invoice`, oc.`weight`, oc.`shipping_cost_tax_excl`, oc.`shipping_cost_tax_incl`, c.`url`, oc.`id_carrier`, c.`name` as `carrier_name`, oc.`date_add`, "Delivery" as `type`, "true" as `can_edit`, oc.`tracking_number`, oc.`id_order_carrier`, osl.`name` as order_state_name, c.`name` as state_name
1988            FROM `' . _DB_PREFIX_ . 'orders` o
1989            LEFT JOIN `' . _DB_PREFIX_ . 'order_history` oh
1990                ON (o.`id_order` = oh.`id_order`)
1991            LEFT JOIN `' . _DB_PREFIX_ . 'order_carrier` oc
1992                ON (o.`id_order` = oc.`id_order`)
1993            LEFT JOIN `' . _DB_PREFIX_ . 'carrier` c
1994                ON (oc.`id_carrier` = c.`id_carrier`)
1995            LEFT JOIN `' . _DB_PREFIX_ . 'order_state_lang` osl
1996                ON (oh.`id_order_state` = osl.`id_order_state` AND osl.`id_lang` = ' . (int) Context::getContext()->language->id . ')
1997            WHERE o.`id_order` = ' . (int) $this->id . '
1998            GROUP BY c.id_carrier'
1999        );
2000        foreach ($results as &$row) {
2001            $row['carrier_name'] = Cart::replaceZeroByShopName($row['carrier_name'], null);
2002        }
2003
2004        return $results;
2005    }
2006
2007    /**
2008     * Get all order_slips for the current order.
2009     *
2010     * @since 1.5.0.2
2011     *
2012     * @return PrestaShopCollection Collection of OrderSlip
2013     */
2014    public function getOrderSlipsCollection()
2015    {
2016        $order_slips = new PrestaShopCollection('OrderSlip');
2017        $order_slips->where('id_order', '=', $this->id);
2018
2019        return $order_slips;
2020    }
2021
2022    /**
2023     * Get all invoices for the current order.
2024     *
2025     * @since 1.5.0.1
2026     *
2027     * @return PrestaShopCollection Collection of OrderInvoice
2028     */
2029    public function getInvoicesCollection()
2030    {
2031        $order_invoices = new PrestaShopCollection('OrderInvoice');
2032        $order_invoices->where('id_order', '=', $this->id);
2033
2034        return $order_invoices;
2035    }
2036
2037    /**
2038     * Get all delivery slips for the current order.
2039     *
2040     * @since 1.5.0.2
2041     *
2042     * @return PrestaShopCollection Collection of OrderInvoice
2043     */
2044    public function getDeliverySlipsCollection()
2045    {
2046        $order_invoices = new PrestaShopCollection('OrderInvoice');
2047        $order_invoices->where('id_order', '=', $this->id);
2048        $order_invoices->where('delivery_number', '!=', '0');
2049
2050        return $order_invoices;
2051    }
2052
2053    /**
2054     * Get all not paid invoices for the current order.
2055     *
2056     * @since 1.5.0.2
2057     *
2058     * @return PrestaShopCollection Collection of Order invoice not paid
2059     */
2060    public function getNotPaidInvoicesCollection()
2061    {
2062        $invoices = $this->getInvoicesCollection();
2063        foreach ($invoices as $key => $invoice) {
2064            /** @var OrderInvoice $invoice */
2065            if ($invoice->isPaid()) {
2066                unset($invoices[$key]);
2067            }
2068        }
2069
2070        return $invoices;
2071    }
2072
2073    /**
2074     * Get total paid.
2075     *
2076     * @since 1.5.0.1
2077     *
2078     * @param Currency $currency currency used for the total paid of the current order
2079     *
2080     * @return float amount in the $currency
2081     */
2082    public function getTotalPaid($currency = null)
2083    {
2084        if (!$currency) {
2085            $currency = new Currency($this->id_currency);
2086        }
2087
2088        $total = 0;
2089        // Retrieve all payments
2090        $payments = $this->getOrderPaymentCollection();
2091        foreach ($payments as $payment) {
2092            /** @var OrderPayment $payment */
2093            if ($payment->id_currency == $currency->id) {
2094                $total += $payment->amount;
2095            } else {
2096                $amount = Tools::convertPrice($payment->amount, $payment->id_currency, false);
2097                if ($currency->id == Configuration::get('PS_CURRENCY_DEFAULT', null, null, $this->id_shop)) {
2098                    $total += $amount;
2099                } else {
2100                    $total += Tools::convertPrice($amount, $currency->id, true);
2101                }
2102            }
2103        }
2104
2105        return Tools::ps_round($total, Context::getContext()->getComputingPrecision());
2106    }
2107
2108    /**
2109     * Get the sum of total_paid_tax_incl of the orders with similar reference.
2110     *
2111     * @since 1.5.0.1
2112     *
2113     * @return float
2114     */
2115    public function getOrdersTotalPaid()
2116    {
2117        return Db::getInstance()->getValue(
2118            'SELECT SUM(total_paid_tax_incl)
2119            FROM `' . _DB_PREFIX_ . 'orders`
2120            WHERE `reference` = \'' . pSQL($this->reference) . '\'
2121            AND `id_cart` = ' . (int) $this->id_cart
2122        );
2123    }
2124
2125    /**
2126     * This method allows to change the shipping cost of the current order.
2127     *
2128     * @since 1.5.0.1
2129     *
2130     * @param float $amount
2131     *
2132     * @return bool
2133     */
2134    public function updateShippingCost($amount)
2135    {
2136        $difference = $amount - $this->total_shipping;
2137        // if the current amount is same as the new, we return true
2138        if ($difference == 0) {
2139            return true;
2140        }
2141
2142        // update the total_shipping value
2143        $this->total_shipping = $amount;
2144        // update the total of this order
2145        $this->total_paid += $difference;
2146
2147        // update database
2148        return $this->update();
2149    }
2150
2151    /**
2152     * Returns the correct product taxes breakdown.
2153     *
2154     * @since 1.5.0.1
2155     *
2156     * @return array
2157     */
2158    public function getProductTaxesBreakdown()
2159    {
2160        $tmp_tax_infos = [];
2161        if ($this->useOneAfterAnotherTaxComputationMethod()) {
2162            // sum by taxes
2163            $taxes_by_tax = Db::getInstance()->executeS('
2164                SELECT odt.`id_order_detail`, t.`name`, t.`rate`, SUM(`total_amount`) AS `total_amount`
2165                FROM `' . _DB_PREFIX_ . 'order_detail_tax` odt
2166                LEFT JOIN `' . _DB_PREFIX_ . 'tax` t ON (t.`id_tax` = odt.`id_tax`)
2167                LEFT JOIN `' . _DB_PREFIX_ . 'order_detail` od ON (od.`id_order_detail` = odt.`id_order_detail`)
2168                WHERE od.`id_order` = ' . (int) $this->id . '
2169                GROUP BY odt.`id_tax`
2170            ');
2171
2172            // format response
2173            $tmp_tax_infos = [];
2174            foreach ($taxes_by_tax as $tax_infos) {
2175                $tmp_tax_infos[$tax_infos['rate']]['total_amount'] = $tax_infos['tax_amount'];
2176                $tmp_tax_infos[$tax_infos['rate']]['name'] = $tax_infos['name'];
2177            }
2178        } else {
2179            // sum by order details in order to retrieve real taxes rate
2180            $taxes_infos = Db::getInstance()->executeS('
2181            SELECT odt.`id_order_detail`, t.`rate` AS `name`, SUM(od.`total_price_tax_excl`) AS total_price_tax_excl, SUM(t.`rate`) AS rate, SUM(`total_amount`) AS `total_amount`
2182            FROM `' . _DB_PREFIX_ . 'order_detail_tax` odt
2183            LEFT JOIN `' . _DB_PREFIX_ . 'tax` t ON (t.`id_tax` = odt.`id_tax`)
2184            LEFT JOIN `' . _DB_PREFIX_ . 'order_detail` od ON (od.`id_order_detail` = odt.`id_order_detail`)
2185            WHERE od.`id_order` = ' . (int) $this->id . '
2186            GROUP BY odt.`id_order_detail`
2187            ');
2188
2189            // sum by taxes
2190            $tmp_tax_infos = [];
2191            foreach ($taxes_infos as $tax_infos) {
2192                if (!isset($tmp_tax_infos[$tax_infos['rate']])) {
2193                    $tmp_tax_infos[$tax_infos['rate']] = ['total_amount' => 0,
2194                        'name' => 0,
2195                        'total_price_tax_excl' => 0,
2196                    ];
2197                }
2198
2199                $tmp_tax_infos[$tax_infos['rate']]['total_amount'] += $tax_infos['total_amount'];
2200                $tmp_tax_infos[$tax_infos['rate']]['name'] = $tax_infos['name'];
2201                $tmp_tax_infos[$tax_infos['rate']]['total_price_tax_excl'] += $tax_infos['total_price_tax_excl'];
2202            }
2203        }
2204
2205        return $tmp_tax_infos;
2206    }
2207
2208    /**
2209     * Returns the shipping taxes breakdown.
2210     *
2211     * @since 1.5.0.1
2212     *
2213     * @return array
2214     */
2215    public function getShippingTaxesBreakdown()
2216    {
2217        $taxes_breakdown = [];
2218
2219        $shipping_tax_amount = $this->total_shipping_tax_incl - $this->total_shipping_tax_excl;
2220
2221        if ($shipping_tax_amount > 0) {
2222            $taxes_breakdown[] = [
2223                'rate' => $this->carrier_tax_rate,
2224                'total_amount' => $shipping_tax_amount,
2225            ];
2226        }
2227
2228        return $taxes_breakdown;
2229    }
2230
2231    /**
2232     * Returns the wrapping taxes breakdown.
2233     *
2234     * @todo
2235     *
2236     * @since 1.5.0.1
2237     *
2238     * @return array
2239     */
2240    public function getWrappingTaxesBreakdown()
2241    {
2242        $taxes_breakdown = [];
2243
2244        return $taxes_breakdown;
2245    }
2246
2247    /**
2248     * Returns the ecotax taxes breakdown.
2249     *
2250     * @since 1.5.0.1
2251     *
2252     * @return array
2253     */
2254    public function getEcoTaxTaxesBreakdown()
2255    {
2256        return Db::getInstance()->executeS(
2257            '
2258    		SELECT `ecotax_tax_rate`, SUM(`ecotax`) as `ecotax_tax_excl`, SUM(`ecotax`) as `ecotax_tax_incl`
2259    		FROM `' . _DB_PREFIX_ . 'order_detail`
2260    		WHERE `id_order` = ' . (int) $this->id
2261        );
2262    }
2263
2264    /**
2265     * Has invoice return true if this order has already an invoice.
2266     *
2267     * @return bool
2268     */
2269    public function hasInvoice()
2270    {
2271        return (bool) Db::getInstance()->getValue(
2272            'SELECT `id_order_invoice`
2273            FROM `' . _DB_PREFIX_ . 'order_invoice`
2274            WHERE `id_order` =  ' . (int) $this->id .
2275            (Configuration::get('PS_INVOICE') ? ' AND `number` > 0' : '')
2276        );
2277    }
2278
2279    /**
2280     * Has Delivery return true if this order has already a delivery slip.
2281     *
2282     * @return bool
2283     */
2284    public function hasDelivery()
2285    {
2286        return (bool) $this->getOrderInvoiceIdIfHasDelivery();
2287    }
2288
2289    /**
2290     * Get order invoice id if has delivery return id_order_invoice if this order has already a delivery slip.
2291     *
2292     * @return int
2293     */
2294    public function getOrderInvoiceIdIfHasDelivery()
2295    {
2296        return (int) Db::getInstance()->getValue(
2297            '
2298            SELECT `id_order_invoice`
2299            FROM `' . _DB_PREFIX_ . 'order_invoice`
2300            WHERE `id_order` =  ' . (int) $this->id . '
2301            AND `delivery_number` > 0'
2302        );
2303    }
2304
2305    /**
2306     * Get warehouse associated to the order.
2307     *
2308     * return array List of warehouse
2309     */
2310    public function getWarehouseList()
2311    {
2312        $results = Db::getInstance()->executeS(
2313            'SELECT id_warehouse
2314            FROM `' . _DB_PREFIX_ . 'order_detail`
2315            WHERE `id_order` =  ' . (int) $this->id . '
2316            GROUP BY id_warehouse'
2317        );
2318        if (!$results) {
2319            return [];
2320        }
2321
2322        $warehouse_list = [];
2323        foreach ($results as $row) {
2324            $warehouse_list[] = $row['id_warehouse'];
2325        }
2326
2327        return $warehouse_list;
2328    }
2329
2330    /**
2331     * @since 1.5.0.4
2332     *
2333     * @return OrderState|null null if Order haven't a state
2334     */
2335    public function getCurrentOrderState()
2336    {
2337        if ($this->current_state) {
2338            return new OrderState($this->current_state);
2339        }
2340
2341        return null;
2342    }
2343
2344    /**
2345     * @see ObjectModel::getWebserviceObjectList()
2346     */
2347    public function getWebserviceObjectList($sql_join, $sql_filter, $sql_sort, $sql_limit)
2348    {
2349        $sql_filter .= Shop::addSqlRestriction(Shop::SHARE_ORDER, 'main');
2350
2351        return parent::getWebserviceObjectList($sql_join, $sql_filter, $sql_sort, $sql_limit);
2352    }
2353
2354    /**
2355     * Get all other orders with the same reference.
2356     *
2357     * @since 1.5.0.13
2358     */
2359    public function getBrother()
2360    {
2361        $collection = new PrestaShopCollection('order');
2362        $collection->where('reference', '=', $this->reference);
2363        $collection->where('id_cart', '=', $this->id_cart);
2364        $collection->where('id_order', '<>', $this->id);
2365
2366        return $collection;
2367    }
2368
2369    /**
2370     * Get a collection of order payments.
2371     *
2372     * @since 1.5.0.13
2373     */
2374    public function getOrderPayments()
2375    {
2376        return OrderPayment::getByOrderReference($this->reference);
2377    }
2378
2379    /**
2380     * Return a unique reference like : GWJTHMZUN#2.
2381     *
2382     * With multishipping, order reference are the same for all orders made with the same cart
2383     * in this case this method suffix the order reference by a # and the order number
2384     *
2385     * @since 1.5.0.14
2386     */
2387    public function getUniqReference()
2388    {
2389        $query = new DbQuery();
2390        $query->select('MIN(id_order) as min, MAX(id_order) as max');
2391        $query->from('orders');
2392        $query->where('id_cart = ' . (int) $this->id_cart);
2393
2394        $order = Db::getInstance()->getRow($query);
2395
2396        if ($order['min'] == $order['max']) {
2397            return $this->reference;
2398        } else {
2399            return $this->reference . '#' . ($this->id + 1 - $order['min']);
2400        }
2401    }
2402
2403    /**
2404     * Return a unique reference like : GWJTHMZUN#2.
2405     *
2406     * With multishipping, order reference are the same for all orders made with the same cart
2407     * in this case this method suffix the order reference by a # and the order number
2408     *
2409     * @since 1.5.0.14
2410     */
2411    public static function getUniqReferenceOf($id_order)
2412    {
2413        $order = new Order($id_order);
2414
2415        return $order->getUniqReference();
2416    }
2417
2418    /**
2419     * Return id of carrier.
2420     *
2421     * Get id of the carrier used in order
2422     *
2423     * @since 1.5.5.0
2424     */
2425    public function getIdOrderCarrier()
2426    {
2427        return (int) Db::getInstance()->getValue('
2428                SELECT `id_order_carrier`
2429                FROM `' . _DB_PREFIX_ . 'order_carrier`
2430                WHERE `id_order` = ' . (int) $this->id);
2431    }
2432
2433    public static function sortDocuments($a, $b)
2434    {
2435        if ($a->date_add == $b->date_add) {
2436            return 0;
2437        }
2438
2439        return ($a->date_add < $b->date_add) ? -1 : 1;
2440    }
2441
2442    public function getWsShippingNumber()
2443    {
2444        $id_order_carrier = Db::getInstance()->getValue('
2445            SELECT `id_order_carrier`
2446            FROM `' . _DB_PREFIX_ . 'order_carrier`
2447            WHERE `id_order` = ' . (int) $this->id);
2448        if ($id_order_carrier) {
2449            $order_carrier = new OrderCarrier($id_order_carrier);
2450
2451            return $order_carrier->tracking_number;
2452        }
2453
2454        return $this->shipping_number;
2455    }
2456
2457    public function setWsShippingNumber($shipping_number)
2458    {
2459        $id_order_carrier = Db::getInstance()->getValue('
2460            SELECT `id_order_carrier`
2461            FROM `' . _DB_PREFIX_ . 'order_carrier`
2462            WHERE `id_order` = ' . (int) $this->id);
2463        if ($id_order_carrier) {
2464            $order_carrier = new OrderCarrier($id_order_carrier);
2465            $order_carrier->tracking_number = $shipping_number;
2466            $order_carrier->update();
2467        } else {
2468            $this->shipping_number = $shipping_number;
2469        }
2470
2471        return true;
2472    }
2473
2474    /**
2475     * @deprecated since 1.6.1
2476     */
2477    public function getWsCurrentState()
2478    {
2479        return $this->getCurrentState();
2480    }
2481
2482    public function setWsCurrentState($state)
2483    {
2484        if ($this->id) {
2485            $this->setCurrentState($state);
2486        }
2487
2488        return true;
2489    }
2490
2491    /**
2492     * By default this function was made for invoice, to compute tax amounts and balance delta (because of computation made on round values).
2493     * If you provide $limitToOrderDetails, only these item will be taken into account. This option is useful for order slip for example,
2494     * where only sublist of the order is refunded.
2495     *
2496     * @param $limitToOrderDetails Optional array of OrderDetails to take into account. False by default to take all OrderDetails from the current Order.
2497     *
2498     * @return array a list of tax rows applied to the given OrderDetails (or all OrderDetails linked to the current Order)
2499     */
2500    public function getProductTaxesDetails($limitToOrderDetails = false)
2501    {
2502        $round_type = $this->round_type;
2503        if ($round_type == 0) {
2504            // if this is 0, it means the field did not exist
2505            // at the time the order was made.
2506            // Set it to old type, which was closest to line.
2507            $round_type = Order::ROUND_LINE;
2508        }
2509
2510        // compute products discount
2511        $order_discount_tax_excl = $this->total_discounts_tax_excl;
2512
2513        $free_shipping_tax = 0;
2514        $product_specific_discounts = [];
2515
2516        $expected_total_base = $this->total_products - $this->total_discounts_tax_excl;
2517
2518        foreach ($this->getCartRules() as $order_cart_rule) {
2519            if ($order_cart_rule['free_shipping'] && $free_shipping_tax === 0) {
2520                $free_shipping_tax = $this->total_shipping_tax_incl - $this->total_shipping_tax_excl;
2521                $order_discount_tax_excl -= $this->total_shipping_tax_excl;
2522                $expected_total_base += $this->total_shipping_tax_excl;
2523            }
2524
2525            $cart_rule = new CartRule($order_cart_rule['id_cart_rule']);
2526            if ($cart_rule->reduction_product > 0) {
2527                if (empty($product_specific_discounts[$cart_rule->reduction_product])) {
2528                    $product_specific_discounts[$cart_rule->reduction_product] = 0;
2529                }
2530
2531                $product_specific_discounts[$cart_rule->reduction_product] += $order_cart_rule['value_tax_excl'];
2532                $order_discount_tax_excl -= $order_cart_rule['value_tax_excl'];
2533            }
2534        }
2535
2536        $products_tax = $this->total_products_wt - $this->total_products;
2537        $discounts_tax = $this->total_discounts_tax_incl - $this->total_discounts_tax_excl;
2538
2539        // We add $free_shipping_tax because when there is free shipping, the tax that would
2540        // be paid if there wasn't is included in $discounts_tax.
2541        $expected_total_tax = $products_tax - $discounts_tax + $free_shipping_tax;
2542        $actual_total_tax = 0;
2543        $actual_total_base = 0;
2544
2545        $order_detail_tax_rows = [];
2546
2547        $breakdown = [];
2548
2549        // Get order_details
2550        $order_details = $limitToOrderDetails ? $limitToOrderDetails : $this->getOrderDetailList();
2551
2552        $order_ecotax_tax = 0;
2553
2554        $tax_rates = [];
2555
2556        foreach ($order_details as $order_detail) {
2557            $id_order_detail = $order_detail['id_order_detail'];
2558            $tax_calculator = OrderDetail::getTaxCalculatorStatic($id_order_detail);
2559
2560            // TODO: probably need to make an ecotax tax breakdown here instead,
2561            // but it seems unlikely there will be different tax rates applied to the
2562            // ecotax in the same order in the real world
2563            $unit_ecotax_tax = $order_detail['ecotax'] * $order_detail['ecotax_tax_rate'] / 100.0;
2564            $order_ecotax_tax += $order_detail['product_quantity'] * $unit_ecotax_tax;
2565
2566            $discount_ratio = 0;
2567
2568            if ($this->total_products > 0) {
2569                $discount_ratio = ($order_detail['unit_price_tax_excl'] + $order_detail['ecotax']) / $this->total_products;
2570            }
2571
2572            // share of global discount
2573            $discounted_price_tax_excl = $order_detail['unit_price_tax_excl'] - $discount_ratio * $order_discount_tax_excl;
2574            // specific discount
2575            if (!empty($product_specific_discounts[$order_detail['product_id']])) {
2576                $discounted_price_tax_excl -= $product_specific_discounts[$order_detail['product_id']];
2577            }
2578
2579            $quantity = $order_detail['product_quantity'];
2580
2581            foreach ($tax_calculator->taxes as $tax) {
2582                $tax_rates[$tax->id] = $tax->rate;
2583            }
2584
2585            foreach ($tax_calculator->getTaxesAmount($discounted_price_tax_excl) as $id_tax => $unit_amount) {
2586                $total_tax_base = 0;
2587                switch ($round_type) {
2588                    case Order::ROUND_ITEM:
2589                        $total_tax_base = $quantity * Tools::ps_round($discounted_price_tax_excl, Context::getContext()->getComputingPrecision(), $this->round_mode);
2590                        $total_amount = $quantity * Tools::ps_round($unit_amount, Context::getContext()->getComputingPrecision(), $this->round_mode);
2591
2592                        break;
2593                    case Order::ROUND_LINE:
2594                        $total_tax_base = Tools::ps_round($quantity * $discounted_price_tax_excl, Context::getContext()->getComputingPrecision(), $this->round_mode);
2595                        $total_amount = Tools::ps_round($quantity * $unit_amount, Context::getContext()->getComputingPrecision(), $this->round_mode);
2596
2597                        break;
2598                    case Order::ROUND_TOTAL:
2599                        $total_tax_base = $quantity * $discounted_price_tax_excl;
2600                        $total_amount = $quantity * $unit_amount;
2601
2602                        break;
2603                }
2604
2605                if (!isset($breakdown[$id_tax])) {
2606                    $breakdown[$id_tax] = ['tax_base' => 0, 'tax_amount' => 0];
2607                }
2608
2609                $breakdown[$id_tax]['tax_base'] += $total_tax_base;
2610                $breakdown[$id_tax]['tax_amount'] += $total_amount;
2611
2612                $order_detail_tax_rows[] = [
2613                    'id_order_detail' => $id_order_detail,
2614                    'id_tax' => $id_tax,
2615                    'tax_rate' => $tax_rates[$id_tax],
2616                    'unit_tax_base' => $discounted_price_tax_excl,
2617                    'total_tax_base' => $total_tax_base,
2618                    'unit_amount' => $unit_amount,
2619                    'total_amount' => $total_amount,
2620                    'id_order_invoice' => $order_detail['id_order_invoice'],
2621                ];
2622            }
2623        }
2624
2625        if (!empty($order_detail_tax_rows)) {
2626            foreach ($breakdown as $data) {
2627                $actual_total_tax += Tools::ps_round($data['tax_amount'], Context::getContext()->getComputingPrecision(), $this->round_mode);
2628                $actual_total_base += Tools::ps_round($data['tax_base'], Context::getContext()->getComputingPrecision(), $this->round_mode);
2629            }
2630
2631            $order_ecotax_tax = Tools::ps_round($order_ecotax_tax, Context::getContext()->getComputingPrecision(), $this->round_mode);
2632
2633            $tax_rounding_error = $expected_total_tax - $actual_total_tax - $order_ecotax_tax;
2634            if ($tax_rounding_error !== 0) {
2635                Tools::spreadAmount($tax_rounding_error, Context::getContext()->getComputingPrecision(), $order_detail_tax_rows, 'total_amount');
2636            }
2637
2638            $base_rounding_error = $expected_total_base - $actual_total_base;
2639            if ($base_rounding_error !== 0) {
2640                Tools::spreadAmount($base_rounding_error, Context::getContext()->getComputingPrecision(), $order_detail_tax_rows, 'total_tax_base');
2641            }
2642        }
2643
2644        return $order_detail_tax_rows;
2645    }
2646
2647    /**
2648     * The primary purpose of this method is to be
2649     * called at the end of the generation of each order
2650     * in PaymentModule::validateOrder, to fill in
2651     * the order_detail_tax table with taxes
2652     * that will add up in such a way that
2653     * the sum of the tax amounts in the product tax breakdown
2654     * is equal to the difference between products with tax and
2655     * products without tax.
2656     */
2657    public function updateOrderDetailTax()
2658    {
2659        $order_detail_tax_rows_to_insert = $this->getProductTaxesDetails();
2660
2661        if (empty($order_detail_tax_rows_to_insert)) {
2662            return;
2663        }
2664
2665        $old_id_order_details = [];
2666        $values = [];
2667        foreach ($order_detail_tax_rows_to_insert as $row) {
2668            $old_id_order_details[] = (int) $row['id_order_detail'];
2669            $values[] = '(' . (int) $row['id_order_detail'] . ', ' . (int) $row['id_tax'] . ', ' . (float) $row['unit_amount'] . ', ' . (float) $row['total_amount'] . ')';
2670        }
2671
2672        // Remove current order_detail_tax'es
2673        Db::getInstance()->execute(
2674            'DELETE FROM `' . _DB_PREFIX_ . 'order_detail_tax` WHERE id_order_detail IN (' . implode(', ', $old_id_order_details) . ')'
2675        );
2676
2677        // Insert the adjusted ones instead
2678        Db::getInstance()->execute(
2679            'INSERT INTO `' . _DB_PREFIX_ . 'order_detail_tax` (id_order_detail, id_tax, unit_amount, total_amount) VALUES ' . implode(', ', $values)
2680        );
2681    }
2682
2683    public function getOrderDetailTaxes()
2684    {
2685        return Db::getInstance()->executeS(
2686            'SELECT od.id_tax_rules_group, od.product_quantity, odt.*, t.* FROM ' . _DB_PREFIX_ . 'orders o ' .
2687            'INNER JOIN ' . _DB_PREFIX_ . 'order_detail od ON od.id_order = o.id_order ' .
2688            'INNER JOIN ' . _DB_PREFIX_ . 'order_detail_tax odt ON odt.id_order_detail = od.id_order_detail ' .
2689            'INNER JOIN ' . _DB_PREFIX_ . 'tax t ON t.id_tax = odt.id_tax ' .
2690            'WHERE o.id_order = ' . (int) $this->id
2691        );
2692    }
2693
2694    /**
2695     * @param int $productId
2696     * @param int $productAttributeId
2697     *
2698     * @return int|null
2699     */
2700    public function getProductSpecificPriceId(int $productId, int $productAttributeId): ?int
2701    {
2702        return SpecificPrice::exists(
2703            $productId,
2704            $productAttributeId,
2705            0,
2706            0,
2707            0,
2708            $this->id_currency,
2709            $this->id_customer,
2710            SpecificPrice::ORDER_DEFAULT_FROM_QUANTITY,
2711            SpecificPrice::ORDER_DEFAULT_DATE,
2712            SpecificPrice::ORDER_DEFAULT_DATE,
2713            false,
2714            $this->id_cart
2715        );
2716    }
2717
2718    /**
2719     * Re calculate shipping cost.
2720     *
2721     * @return object $order
2722     */
2723    public function refreshShippingCost()
2724    {
2725        if (empty($this->id)) {
2726            return false;
2727        }
2728
2729        if (!Configuration::get('PS_ORDER_RECALCULATE_SHIPPING')) {
2730            return $this;
2731        }
2732
2733        $fake_cart = new Cart((int) $this->id_cart);
2734        $new_cart = $fake_cart->duplicate();
2735        $new_cart = $new_cart['cart'];
2736
2737        // assign order id_address_delivery to cart
2738        $new_cart->id_address_delivery = (int) $this->id_address_delivery;
2739
2740        // assign id_carrier
2741        $new_cart->id_carrier = (int) $this->id_carrier;
2742
2743        //remove all products : cart (maybe change in the meantime)
2744        foreach ($new_cart->getProducts() as $product) {
2745            $new_cart->deleteProduct((int) $product['id_product'], (int) $product['id_product_attribute']);
2746        }
2747
2748        // add real order products
2749        foreach ($this->getProducts() as $product) {
2750            $new_cart->updateQty(
2751                $product['product_quantity'],
2752                (int) $product['product_id'],
2753                null,
2754                false,
2755                'up',
2756                0,
2757                null,
2758                true,
2759                true
2760            ); // - skipAvailabilityCheckOutOfStock
2761        }
2762
2763        // get new shipping cost
2764        $base_total_shipping_tax_incl = (float) $new_cart->getPackageShippingCost((int) $new_cart->id_carrier, true, null);
2765        $base_total_shipping_tax_excl = (float) $new_cart->getPackageShippingCost((int) $new_cart->id_carrier, false, null);
2766
2767        // calculate diff price, then apply new order totals
2768        $diff_shipping_tax_incl = $this->total_shipping_tax_incl - $base_total_shipping_tax_incl;
2769        $diff_shipping_tax_excl = $this->total_shipping_tax_excl - $base_total_shipping_tax_excl;
2770
2771        $this->total_shipping_tax_excl -= $diff_shipping_tax_excl;
2772        $this->total_shipping_tax_incl -= $diff_shipping_tax_incl;
2773        $this->total_shipping = $this->total_shipping_tax_incl;
2774        $this->total_paid_tax_excl -= $diff_shipping_tax_excl;
2775        $this->total_paid_tax_incl -= $diff_shipping_tax_incl;
2776        $this->total_paid = $this->total_paid_tax_incl;
2777        $this->update();
2778
2779        // save order_carrier prices, we'll save order right after this in update() method
2780        $orderCarrierId = (int) $this->getIdOrderCarrier();
2781        if ($orderCarrierId > 0) {
2782            $order_carrier = new OrderCarrier($orderCarrierId);
2783            $order_carrier->shipping_cost_tax_excl = $this->total_shipping_tax_excl;
2784            $order_carrier->shipping_cost_tax_incl = $this->total_shipping_tax_incl;
2785            $order_carrier->update();
2786        }
2787
2788        // remove fake cart
2789        $new_cart->delete();
2790
2791        return $this;
2792    }
2793}
2794