Order.php 99.1 KB
Newer Older
1
<?php
2
/**
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
3
4
 * Copyright since 2007 PrestaShop SA and Contributors
 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
5
6
7
8
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
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
13
14
15
16
17
18
19
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashop.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
 * versions in the future. If you wish to customize PrestaShop for your
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
20
 * needs please refer to https://devdocs.prestashop.com/ for more information.
21
 *
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
22
23
 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
 * @copyright Since 2007 PrestaShop SA and Contributors
24
 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
25
 */
Alex Sampaio's avatar
Alex Sampaio committed
26
27
use PrestaShop\PrestaShop\Adapter\ServiceLocator;

28
29
class OrderCore extends ObjectModel
{
30
31
32
    const ROUND_ITEM = 1;
    const ROUND_LINE = 2;
    const ROUND_TOTAL = 3;
Rémi Gaillard's avatar
Rémi Gaillard committed
33

34
35
    /** @var int Delivery address id */
    public $id_address_delivery;
36

37
38
    /** @var int Invoice address id */
    public $id_address_invoice;
39

40
    public $id_shop_group;
41

42
    public $id_shop;
43

44
45
    /** @var int Cart id */
    public $id_cart;
46

47
48
    /** @var int Currency id */
    public $id_currency;
49

50
51
    /** @var int Language id */
    public $id_lang;
52

53
54
    /** @var int Customer id */
    public $id_customer;
55

56
    // todo: string received instead of int
57
58
    /** @var int Carrier id */
    public $id_carrier;
59

60
61
    /** @var int Order Status id */
    public $current_state;
62

63
64
    /** @var string Secure key */
    public $secure_key;
65

66
67
    /** @var string Payment method */
    public $payment;
68

69
70
    /** @var string Payment module */
    public $module;
71

72
73
    /** @var float Currency exchange rate */
    public $conversion_rate;
74

75
76
    /** @var bool Customer is ok for a recyclable package */
    public $recyclable = 1;
77

78
79
    /** @var bool True if the customer wants a gift wrapping */
    public $gift = 0;
80

81
82
    /** @var string Gift message if specified */
    public $gift_message;
83

84
85
    /** @var bool Mobile Theme */
    public $mobile_theme;
86

87
88
    /**
     * @var string Shipping number
Mickaël Andrieu's avatar
Mickaël Andrieu committed
89
     *
90
91
92
93
     * @deprecated 1.5.0.4
     * @see OrderCarrier->tracking_number
     */
    public $shipping_number;
94

95
96
    /** @var float Discounts total */
    public $total_discounts;
97

98
99
    public $total_discounts_tax_incl;
    public $total_discounts_tax_excl;
100

101
102
    /** @var float Total to pay */
    public $total_paid;
103

104
105
    /** @var float Total to pay tax included */
    public $total_paid_tax_incl;
106

107
108
    /** @var float Total to pay tax excluded */
    public $total_paid_tax_excl;
109

110
111
    /** @var float Total really paid @deprecated 1.5.0.1 */
    public $total_paid_real;
112

113
114
    /** @var float Products total */
    public $total_products;
115

116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
    /** @var float Products total tax included */
    public $total_products_wt;

    /** @var float Shipping total */
    public $total_shipping;

    /** @var float Shipping total tax included */
    public $total_shipping_tax_incl;

    /** @var float Shipping total tax excluded */
    public $total_shipping_tax_excl;

    /** @var float Shipping tax rate */
    public $carrier_tax_rate;

    /** @var float Wrapping total */
    public $total_wrapping;

    /** @var float Wrapping total tax included */
    public $total_wrapping_tax_incl;

    /** @var float Wrapping total tax excluded */
    public $total_wrapping_tax_excl;

    /** @var int Invoice number */
    public $invoice_number;

    /** @var int Delivery number */
    public $delivery_number;

    /** @var string Invoice creation date */
    public $invoice_date;

    /** @var string Delivery creation date */
    public $delivery_date;

    /** @var bool Order validity: current order status is logable (usually paid and not canceled) */
    public $valid;

    /** @var string Object creation date */
    public $date_add;

    /** @var string Object last modification date */
    public $date_upd;

    /**
     * @var string Order reference, this reference is not unique, but unique for a payment
     */
    public $reference;

    /**
     * @var int Round mode method used for this order
     */
    public $round_mode;

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
172
173
     * @var int Round type method used for this order
     */
174
175
    public $round_type;

176
    /**
Karlis. S's avatar
Karlis. S committed
177
     * @var string internal order note, what is only available in BO
178
     */
179
    public $note = '';
180

181
182
183
    /**
     * @see ObjectModel::$definition
     */
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
184
    public static $definition = [
185
186
        'table' => 'orders',
        'primary' => 'id_order',
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
        'fields' => [
            'id_address_delivery' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
            'id_address_invoice' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
            'id_cart' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
            'id_currency' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
            'id_shop_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'id_shop' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'id_lang' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
            'id_customer' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
            'id_carrier' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
            'current_state' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'secure_key' => ['type' => self::TYPE_STRING, 'validate' => 'isMd5'],
            'payment' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true],
            'module' => ['type' => self::TYPE_STRING, 'validate' => 'isModuleName', 'required' => true],
            'recyclable' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
            'gift' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
            'gift_message' => ['type' => self::TYPE_STRING, 'validate' => 'isMessage'],
            'mobile_theme' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
            'total_discounts' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'total_discounts_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'total_discounts_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'total_paid' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice', 'required' => true],
            'total_paid_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'total_paid_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'total_paid_real' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice', 'required' => true],
            'total_products' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice', 'required' => true],
            'total_products_wt' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice', 'required' => true],
            'total_shipping' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'total_shipping_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'total_shipping_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'carrier_tax_rate' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
            'total_wrapping' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'total_wrapping_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'total_wrapping_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'round_mode' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'round_type' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'shipping_number' => ['type' => self::TYPE_STRING, 'validate' => 'isTrackingNumber'],
            'conversion_rate' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat', 'required' => true],
            'invoice_number' => ['type' => self::TYPE_INT],
            'delivery_number' => ['type' => self::TYPE_INT],
            'invoice_date' => ['type' => self::TYPE_DATE],
            'delivery_date' => ['type' => self::TYPE_DATE],
            'valid' => ['type' => self::TYPE_BOOL],
            'reference' => ['type' => self::TYPE_STRING],
            'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
            'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
matthieu-rolland's avatar
matthieu-rolland committed
233
            'note' => ['type' => self::TYPE_HTML],
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
234
235
236
237
238
        ],
    ];

    protected $webserviceParameters = [
        'objectMethods' => ['add' => 'addWs'],
239
240
        'objectNodeName' => 'order',
        'objectsNodeName' => 'orders',
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
241
242
243
244
245
246
247
248
249
        'fields' => [
            'id_address_delivery' => ['xlink_resource' => 'addresses'],
            'id_address_invoice' => ['xlink_resource' => 'addresses'],
            'id_cart' => ['xlink_resource' => 'carts'],
            'id_currency' => ['xlink_resource' => 'currencies'],
            'id_lang' => ['xlink_resource' => 'languages'],
            'id_customer' => ['xlink_resource' => 'customers'],
            'id_carrier' => ['xlink_resource' => 'carriers'],
            'current_state' => [
Mickaël Andrieu's avatar
Mickaël Andrieu committed
250
251
                'xlink_resource' => 'order_states',
                'setter' => 'setWsCurrentState',
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
252
253
254
255
256
257
258
259
260
261
            ],
            'module' => ['required' => true],
            'invoice_number' => [],
            'invoice_date' => [],
            'delivery_number' => [],
            'delivery_date' => [],
            'valid' => [],
            'date_add' => [],
            'date_upd' => [],
            'shipping_number' => [
262
                'getter' => 'getWsShippingNumber',
Mickaël Andrieu's avatar
Mickaël Andrieu committed
263
                'setter' => 'setWsShippingNumber',
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
264
            ],
265
            'note' => [],
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
        ],
        'associations' => [
            'order_rows' => ['resource' => 'order_row', 'setter' => false, 'virtual_entity' => true,
                'fields' => [
                    'id' => [],
                    'product_id' => ['required' => true, 'xlink_resource' => 'products'],
                    'product_attribute_id' => ['required' => true],
                    'product_quantity' => ['required' => true],
                    'product_name' => ['setter' => false],
                    'product_reference' => ['setter' => false],
                    'product_ean13' => ['setter' => false],
                    'product_isbn' => ['setter' => false],
                    'product_upc' => ['setter' => false],
                    'product_price' => ['setter' => false],
                    'id_customization' => ['required' => false, 'xlink_resource' => 'customizations'],
                    'unit_price_tax_incl' => ['setter' => false],
                    'unit_price_tax_excl' => ['setter' => false],
                ], ],
        ],
    ];
286
287
288

    protected $_taxCalculationMethod = PS_TAX_EXC;

Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
289
    protected static $_historyCache = [];
290
291
292
293
294
295

    public function __construct($id = null, $id_lang = null)
    {
        parent::__construct($id, $id_lang);

        $is_admin = (is_object(Context::getContext()->controller) && Context::getContext()->controller->controller_type == 'admin');
296
        if ($this->id_customer && !$is_admin) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
297
298
            $customer = new Customer((int) $this->id_customer);
            $this->_taxCalculationMethod = Group::getPriceDisplayMethod((int) $customer->id_default_group);
299
        } else {
300
            $this->_taxCalculationMethod = Group::getDefaultPriceDisplayMethod();
301
        }
302
303
304
305
    }

    /**
     * @see ObjectModel::getFields()
Mickaël Andrieu's avatar
Mickaël Andrieu committed
306
     *
307
308
309
310
     * @return array
     */
    public function getFields()
    {
311
        if (!$this->id_lang) {
312
            $this->id_lang = Configuration::get('PS_LANG_DEFAULT', null, null, $this->id_shop);
313
        }
314
315
316
317
318
319

        return parent::getFields();
    }

    public function add($autodate = true, $null_values = true)
    {
320
        if (parent::add($autodate, $null_values)) {
321
            return SpecificPrice::deleteByIdCart($this->id_cart);
322
        }
Mickaël Andrieu's avatar
Mickaël Andrieu committed
323

324
325
326
327
328
        return false;
    }

    public function getTaxCalculationMethod()
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
329
        return (int) $this->_taxCalculationMethod;
330
331
332
    }

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
333
     * Does NOT delete a product but "cancel" it (which means return/refund/delete it depending of the case).
334
335
336
337
     *
     * @param $order
     * @param OrderDetail $order_detail
     * @param int $quantity
Mickaël Andrieu's avatar
Mickaël Andrieu committed
338
     *
339
     * @return bool
Mickaël Andrieu's avatar
Mickaël Andrieu committed
340
     *
341
342
343
344
     * @throws PrestaShopException
     */
    public function deleteProduct($order, $order_detail, $quantity)
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
345
        if (!(int) $this->getCurrentState() || !validate::isLoadedObject($order_detail)) {
346
            return false;
347
        }
348

349
350
        if ($this->hasBeenDelivered()) {
            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
            }
Mickaël Andrieu's avatar
Mickaël Andrieu committed
353
354
            $order_detail->product_quantity_return += (int) $quantity;

355
            return $order_detail->update();
356
        } elseif ($this->hasBeenPaid()) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
357
358
            $order_detail->product_quantity_refunded += (int) $quantity;

359
360
            return $order_detail->update();
        }
Mickaël Andrieu's avatar
Mickaël Andrieu committed
361
362

        return $this->_deleteProduct($order_detail, (int) $quantity);
363
364
365
366
    }

    /**
     * This function return products of the orders
Mickaël Andrieu's avatar
Mickaël Andrieu committed
367
     * It's similar to Order::getProducts but with similar outputs of Cart::getProducts.
368
369
370
371
372
     *
     * @return array
     */
    public function getCartProducts()
    {
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
373
        $product_id_list = [];
374
        $products = $this->getProducts();
375
        foreach ($products as &$product) {
376
377
            $product['id_product_attribute'] = $product['product_attribute_id'];
            $product['cart_quantity'] = $product['product_quantity'];
Mickaël Andrieu's avatar
Mickaël Andrieu committed
378
379
380
381
            $product_id_list[] = $this->id_address_delivery . '_'
                . $product['product_id'] . '_'
                . $product['product_attribute_id'] . '_'
                . (isset($product['id_customization']) ? $product['id_customization'] : '0');
382
383
384
        }
        unset($product);

Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
385
        $product_list = [];
386
        foreach ($products as $product) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
387
388
389
390
            $key = $this->id_address_delivery . '_'
                . $product['id_product'] . '_'
                . (isset($product['id_product_attribute']) ? $product['id_product_attribute'] : '0') . '_'
                . (isset($product['id_customization']) ? $product['id_customization'] : '0');
391

392
            if (in_array($key, $product_id_list)) {
393
                $product_list[] = $product;
394
            }
395
        }
Mickaël Andrieu's avatar
Mickaël Andrieu committed
396

397
398
399
400
        return $product_list;
    }

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
401
     * DOES delete the product.
402
403
404
     *
     * @param OrderDetail $order_detail
     * @param int $quantity
Mickaël Andrieu's avatar
Mickaël Andrieu committed
405
     *
406
     * @return bool
Mickaël Andrieu's avatar
Mickaël Andrieu committed
407
     *
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
     * @throws PrestaShopException
     */
    protected function _deleteProduct($order_detail, $quantity)
    {
        $product_price_tax_excl = $order_detail->unit_price_tax_excl * $quantity;
        $product_price_tax_incl = $order_detail->unit_price_tax_incl * $quantity;

        /* Update cart */
        $cart = new Cart($this->id_cart);
        $cart->updateQty($quantity, $order_detail->product_id, $order_detail->product_attribute_id, false, 'down'); // customization are deleted in deleteCustomization
        $cart->update();

        /* Update order */
        $shipping_diff_tax_incl = $this->total_shipping_tax_incl - $cart->getPackageShippingCost($this->id_carrier, true, null, $this->getCartProducts());
        $shipping_diff_tax_excl = $this->total_shipping_tax_excl - $cart->getPackageShippingCost($this->id_carrier, false, null, $this->getCartProducts());
        $this->total_shipping -= $shipping_diff_tax_incl;
        $this->total_shipping_tax_excl -= $shipping_diff_tax_excl;
        $this->total_shipping_tax_incl -= $shipping_diff_tax_incl;
        $this->total_products -= $product_price_tax_excl;
        $this->total_products_wt -= $product_price_tax_incl;
        $this->total_paid -= $product_price_tax_incl + $shipping_diff_tax_incl;
        $this->total_paid_tax_incl -= $product_price_tax_incl + $shipping_diff_tax_incl;
        $this->total_paid_tax_excl -= $product_price_tax_excl + $shipping_diff_tax_excl;
        $this->total_paid_real -= $product_price_tax_incl + $shipping_diff_tax_incl;

Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
433
        $fields = [
434
435
436
437
438
439
440
441
            'total_shipping',
            'total_shipping_tax_excl',
            'total_shipping_tax_incl',
            'total_products',
            'total_products_wt',
            'total_paid',
            'total_paid_tax_incl',
            'total_paid_tax_excl',
Mickaël Andrieu's avatar
Mickaël Andrieu committed
442
            'total_paid_real',
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
443
        ];
444
445

        /* Prevent from floating precision issues */
446
447
        foreach ($fields as $field) {
            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 */
Mickaël Andrieu's avatar
Mickaël Andrieu committed
458
        $order_detail->product_quantity -= (int) $quantity;
459
460
        if ($order_detail->product_quantity == 0) {
            if (!$order_detail->delete()) {
461
                return false;
462
463
            }
            if (count($this->getProductsDetail()) == 0) {
464
                $history = new OrderHistory();
Mickaël Andrieu's avatar
Mickaël Andrieu committed
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
            }
Mickaël Andrieu's avatar
Mickaël Andrieu committed
471

472
            return $this->update();
473
        } else {
474
475
476
477
478
            $order_detail->total_price_tax_incl -= $product_price_tax_incl;
            $order_detail->total_price_tax_excl -= $product_price_tax_excl;
            $order_detail->total_shipping_price_tax_incl -= $shipping_diff_tax_incl;
            $order_detail->total_shipping_price_tax_excl -= $shipping_diff_tax_excl;
        }
Mickaël Andrieu's avatar
Mickaël Andrieu committed
479

480
481
482
483
484
        return $order_detail->update() && $this->update();
    }

    public function deleteCustomization($id_customization, $quantity, $order_detail)
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
485
        if (!(int) $this->getCurrentState()) {
486
            return false;
487
        }
488

489
        if ($this->hasBeenDelivered()) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
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()) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
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
        }
Mickaël Andrieu's avatar
Mickaël Andrieu committed
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
        }
Mickaël Andrieu's avatar
Mickaël Andrieu committed
497
        if (!Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'customization` WHERE `quantity` = 0')) {
498
            return false;
499
        }
Mickaël Andrieu's avatar
Mickaël Andrieu committed
500
501

        return $this->_deleteProduct($order_detail, (int) $quantity);
502
503
504
    }

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
505
     * Get order history.
506
507
508
509
510
511
512
513
514
515
     *
     * @param int $id_lang Language id
     * @param int $id_order_state Filter a specific order status
     * @param int $no_hidden Filter no hidden status
     * @param int $filters Flag to use specific field filter
     *
     * @return array History entries ordered by date DESC
     */
    public function getHistory($id_lang, $id_order_state = false, $no_hidden = false, $filters = 0)
    {
516
        if (!$id_order_state) {
517
            $id_order_state = 0;
518
        }
519
520
521
522
523

        $logable = false;
        $delivery = false;
        $paid = false;
        $shipped = false;
524
525
        if ($filters > 0) {
            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
        }

Mickaël Andrieu's avatar
Mickaël Andrieu committed
542
543
        if (!isset(self::$_historyCache[$this->id . '_' . $id_order_state . '_' . $filters]) || $no_hidden) {
            $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
Mickaël Andrieu's avatar
Mickaël Andrieu committed
546
547
548
549
550
551
552
553
554
555
556
557
            FROM `' . _DB_PREFIX_ . 'orders` o
            LEFT JOIN `' . _DB_PREFIX_ . 'order_history` oh ON o.`id_order` = oh.`id_order`
            LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON os.`id_order_state` = oh.`id_order_state`
            LEFT JOIN `' . _DB_PREFIX_ . 'order_state_lang` osl ON (os.`id_order_state` = osl.`id_order_state` AND osl.`id_lang` = ' . (int) ($id_lang) . ')
            LEFT JOIN `' . _DB_PREFIX_ . 'employee` e ON e.`id_employee` = oh.`id_employee`
            WHERE oh.id_order = ' . (int) $this->id . '
            ' . ($no_hidden ? ' AND os.hidden = 0' : '') . '
            ' . ($logable ? ' AND os.logable = 1' : '') . '
            ' . ($delivery ? ' AND os.delivery = 1' : '') . '
            ' . ($paid ? ' AND os.paid = 1' : '') . '
            ' . ($shipped ? ' AND os.shipped = 1' : '') . '
            ' . ((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
            }
Mickaël Andrieu's avatar
Mickaël Andrieu committed
562
            self::$_historyCache[$this->id . '_' . $id_order_state . '_' . $filters] = $result;
563
        }
Mickaël Andrieu's avatar
Mickaël Andrieu committed
564
565

        return self::$_historyCache[$this->id . '_' . $id_order_state . '_' . $filters];
566
567
    }

568
569
570
571
572
573
574
575
576
577
    /**
     * Clean static history cache, must be called when an OrderHistory is added as it changes
     * the order history and may change its value for isPaid/isDelivered/... This way calls to
     * getHistory will be up to date.
     */
    public static function cleanHistoryCache()
    {
        self::$_historyCache = [];
    }

578
579
    public function getProductsDetail()
    {
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
Progi1984's avatar
Progi1984 committed
581
        $sql = 'SELECT p.*, ps.*, od.*';
582
583
584
585
586
587
588
        $sql .= ' FROM `%sorder_detail` od';
        $sql .= ' LEFT JOIN `%sproduct` p ON (p.id_product = od.product_id)';
        $sql .= ' LEFT JOIN `%sproduct_shop` ps ON (ps.id_product = p.id_product AND ps.id_shop = od.id_shop)';
        $sql .= ' WHERE od.`id_order` = %d';
        $sql = sprintf($sql, _DB_PREFIX_, _DB_PREFIX_, _DB_PREFIX_, (int) $this->id);

        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
589
    }
590

591
592
593
    public function getFirstMessage()
    {
        return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
594
            SELECT `message`
Mickaël Andrieu's avatar
Mickaël Andrieu committed
595
596
            FROM `' . _DB_PREFIX_ . 'message`
            WHERE `id_order` = ' . (int) $this->id . '
597
598
            ORDER BY `id_message`
        ');
599
600
601
602
603
    }

    /**
     * Marked as deprecated but should not throw any "deprecated" message
     * This function is used in order to keep front office backward compatibility 14 -> 1.5
Mickaël Andrieu's avatar
Mickaël Andrieu committed
604
     * (Order History).
605
606
607
608
609
     *
     * @deprecated
     */
    public function setProductPrices(&$row)
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
610
        $tax_calculator = OrderDetail::getTaxCalculatorStatic((int) $row['id_order_detail']);
611
612
613
        $row['tax_calculator'] = $tax_calculator;
        $row['tax_rate'] = $tax_calculator->getTotalRate();

matks's avatar
matks committed
614
615
        $row['product_price'] = Tools::ps_round($row['unit_price_tax_excl'], Context::getContext()->getComputingPrecision());
        $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
623
624
625
626
627
628

        $row['product_price_wt_but_ecotax'] = $row['product_price_wt'] - $row['ecotax'];

        $row['total_wt'] = $row['total_price_tax_incl'];
        $row['total_price'] = $row['total_price_tax_excl'];
    }

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
629
     * Get order products.
630
     *
631
632
633
634
635
     * @param bool $products
     * @param bool $selected_products
     * @param bool $selected_qty
     * @param bool $fullInfos
     *
636
637
     * @return array Products with price, quantity (with taxe and without)
     */
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
645
646
647
        if (!$fullInfos) {
            return $products;
        }

Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
648
        $result_array = [];
649
        foreach ($products as $row) {
650
            // Change qty if selected
651
            if ($selected_qty) {
652
                $row['product_quantity'] = 0;
653
654
                foreach ($selected_products as $key => $id_product) {
                    if ($row['id_order_detail'] == $id_product) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
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
667

            // Backward compatibility 1.4 -> 1.5
            $this->setProductPrices($row);
668
            $customized_datas = Product::getAllCustomizedDatas($this->id_cart, null, true, $this->id_shop, (int) $row['id_customization']);
669
670
671
            $this->setProductCustomizedDatas($row, $customized_datas);

            // Add information for virtual product
672
            if ($row['download_hash'] && !empty($row['download_hash'])) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
673
                $row['filename'] = ProductDownload::getFilenameFromIdProduct((int) $row['product_id']);
674
675
676
677
678
679
                // Get the display filename
                $row['display_filename'] = ProductDownload::getFilenameFromFilename($row['filename']);
            }

            $row['id_address_delivery'] = $this->id_address_delivery;

680
            if ($customized_datas) {
681
682
                Product::addProductCustomizationPrice($row, $customized_datas);
            }
683

684
            /* Stock product */
Mickaël Andrieu's avatar
Mickaël Andrieu committed
685
            $result_array[(int) $row['id_order_detail']] = $row;
686
687
688
689
690
691
692
        }

        return $result_array;
    }

    public static function getIdOrderProduct($id_customer, $id_product)
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
693
        return (int) Db::getInstance()->getValue('
694
            SELECT o.id_order
Mickaël Andrieu's avatar
Mickaël Andrieu committed
695
696
            FROM ' . _DB_PREFIX_ . 'orders o
            LEFT JOIN ' . _DB_PREFIX_ . 'order_detail od
697
                ON o.id_order = od.id_order
Mickaël Andrieu's avatar
Mickaël Andrieu committed
698
699
            WHERE o.id_customer = ' . (int) $id_customer . '
                AND od.product_id = ' . (int) $id_product . '
700
701
            ORDER BY o.date_add DESC
        ');
702
703
704
705
706
    }

    protected function setProductCustomizedDatas(&$product, $customized_datas)
    {
        $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
    }

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
715
     * This method allow to add stock information on a product detail.
716
717
718
719
720
721
     *
     * If advanced stock management is active, get physical stock of this product in the warehouse associated to the ptoduct for the current order
     * Else get the available quantity of the product in fucntion of the shop associated to the order
     *
     * @param array &$product
     */
722
    protected function setProductCurrentStock(&$product)
723
724
    {
        if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')
Mickaël Andrieu's avatar
Mickaël Andrieu committed
725
726
727
            && (int) $product['advanced_stock_management'] == 1
            && (int) $product['id_warehouse'] > 0) {
            $product['current_stock'] = StockManagerFactory::getManager()->getProductPhysicalQuantities($product['product_id'], $product['product_attribute_id'], (int) $product['id_warehouse'], true);
728
        } else {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
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
    }

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
736
     * This method allow to add image information on a product detail.
737
738
739
740
741
     *
     * @param array &$product
     */
    protected function setProductImageInformations(&$product)
    {
742
        if (isset($product['product_attribute_id']) && $product['product_attribute_id']) {
743
            $id_image = Db::getInstance()->getValue('
744
                SELECT `image_shop`.id_image
Mickaël Andrieu's avatar
Mickaël Andrieu committed
745
746
747
748
                FROM `' . _DB_PREFIX_ . 'product_attribute_image` pai' .
                Shop::addSqlAssociation('image', 'pai', true) . '
                LEFT JOIN `' . _DB_PREFIX_ . 'image` i ON (i.`id_image` = pai.`id_image`)
                WHERE id_product_attribute = ' . (int) $product['product_attribute_id'] . ' ORDER by i.position ASC');
749
        }
750

751
        if (!isset($id_image) || !$id_image) {
752
753
            $id_image = Db::getInstance()->getValue(
                'SELECT `image_shop`.id_image
Mickaël Andrieu's avatar
Mickaël Andrieu committed
754
755
756
                FROM `' . _DB_PREFIX_ . 'image` i' .
                Shop::addSqlAssociation('image', 'i', true, 'image_shop.cover=1') . '
                WHERE i.id_product = ' . (int) $product['product_id']
757
            );
758
        }
759
760
761
762

        $product['image'] = null;
        $product['image_size'] = null;

763
        if ($id_image) {
764
            $product['image'] = new Image($id_image);
765
        }
766
767
768
769
    }

    public function getTaxesAverageUsed()
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
770
        return Cart::getTaxesAverageUsed((int) $this->id_cart);
771
772
773
    }

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
774
     * Count virtual products in order.
775
776
777
778
779
780
     *
     * @return int number of virtual products
     */
    public function getVirtualProducts()
    {
        $sql = '
781
            SELECT `product_id`, `product_attribute_id`, `download_hash`, `download_deadline`
Mickaël Andrieu's avatar
Mickaël Andrieu committed
782
783
            FROM `' . _DB_PREFIX_ . 'order_detail` od
            WHERE od.`id_order` = ' . (int) $this->id . '
784
                AND `download_hash` <> \'\'';
Mickaël Andrieu's avatar
Mickaël Andrieu committed
785

786
787
788
789
        return Db::getInstance()->executeS($sql);
    }

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
790
791
792
793
794
795
     * Check if order contains (only) virtual products.
     *
     * @param bool $strict If false return true if there are at least one product virtual
     *
     * @return bool true if is a virtual order or false
     */
796
797
    public function isVirtual($strict = true)
    {
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) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
806
            if ($strict === false && (bool) $product['is_virtual']) {
807
                return true;
808
            }
809

Mickaël Andrieu's avatar
Mickaël Andrieu committed
810
            $virtual &= (bool) $product['is_virtual'];
811
        }
812

813
814
815
816
        return $virtual;
    }

    /**
817
     * @deprecated 1.5.0.1 use Order::getCartRules() instead
818
819
820
     */
    public function getDiscounts($details = false)
    {
821
        Tools::displayAsDeprecated('Use Order::getCartRules() instead');
Mickaël Andrieu's avatar
Mickaël Andrieu committed
822

823
824
825
826
827
828
        return Order::getCartRules();
    }

    public function getCartRules()
    {
        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
829
        SELECT *
Mickaël Andrieu's avatar
Mickaël Andrieu committed
830
        FROM `' . _DB_PREFIX_ . 'order_cart_rule` ocr
831
        WHERE ocr.`deleted` = 0 AND ocr.`id_order` = ' . (int) $this->id);
832
833
    }

834
835
836
837
838
839
840
841
842
843
844
845
846
    /**
     *  Return the list of all order cart rules, even the softy deleted ones
     *
     * @return array|false
     *
     * @throws PrestaShopDatabaseException
     */
    public function getDeletedCartRules()
    {
        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
        SELECT *
        FROM `' . _DB_PREFIX_ . 'order_cart_rule` ocr
        WHERE ocr.`deleted` = 1 AND ocr.`id_order` = ' . (int) $this->id);
847
848
849
850
    }

    public static function getDiscountsCustomer($id_customer, $id_cart_rule)
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
851
        $cache_id = 'Order::getDiscountsCustomer_' . (int) $id_customer . '-' . (int) $id_cart_rule;
852
        if (!Cache::isStored($cache_id)) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
853
854
            $result = (int) Db::getInstance()->getValue('
            SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'orders` o
855
856
857
            LEFT JOIN `' . _DB_PREFIX_ . 'order_cart_rule` ocr ON (ocr.`id_order` = o.`id_order`)
            WHERE o.`id_customer` = ' . (int) $id_customer . '
            AND ocr.`deleted` = 0 AND ocr.`id_cart_rule` = ' . (int) $id_cart_rule);
858
            Cache::store($cache_id, $result);
Mickaël Andrieu's avatar
Mickaël Andrieu committed
859

860
861
            return $result;
        }
Mickaël Andrieu's avatar
Mickaël Andrieu committed
862

863
864
865
866
        return Cache::retrieve($cache_id);
    }

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
867
     * Get current order status (eg. Awaiting payment, Delivered...).
868
869
870
871
872
873
874
875
876
     *
     * @return int Order status id
     */
    public function getCurrentState()
    {
        return $this->current_state;
    }

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
877
     * Get current order status name (eg. Awaiting payment, Delivered...).
878
879
880
881
882
883
     *
     * @return array Order status details
     */
    public function getCurrentStateFull($id_lang)
    {
        return Db::getInstance()->getRow('
884
            SELECT os.`id_order_state`, osl.`name`, os.`logable`, os.`shipped`
Mickaël Andrieu's avatar
Mickaël Andrieu committed
885
886
887
            FROM `' . _DB_PREFIX_ . 'order_state` os
            LEFT JOIN `' . _DB_PREFIX_ . 'order_state_lang` osl ON (osl.`id_order_state` = os.`id_order_state`)
            WHERE osl.`id_lang` = ' . (int) $id_lang . ' AND os.`id_order_state` = ' . (int) $this->current_state);
888
889
890
891
    }

    public function hasBeenDelivered()
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
892
        return count($this->getHistory((int) $this->id_lang, false, false, OrderState::FLAG_DELIVERY));
893
894
895
896
897
898
899
900
    }

    /**
     * Has products returned by the merchant or by the customer?
     */
    public function hasProductReturned()
    {
        return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
901
            SELECT IFNULL(SUM(ord.product_quantity), SUM(product_quantity_return))
Mickaël Andrieu's avatar
Mickaël Andrieu committed
902
903
            FROM `' . _DB_PREFIX_ . 'orders` o
            INNER JOIN `' . _DB_PREFIX_ . 'order_detail` od
904
            ON od.id_order = o.id_order
Mickaël Andrieu's avatar
Mickaël Andrieu committed
905
            LEFT JOIN `' . _DB_PREFIX_ . 'order_return_detail` ord
906
            ON ord.id_order_detail = od.id_order_detail
Mickaël Andrieu's avatar
Mickaël Andrieu committed
907
            WHERE o.id_order = ' . (int) $this->id);
908
909
910
911
    }

    public function hasBeenPaid()
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
912
        return count($this->getHistory((int) $this->id_lang, false, false, OrderState::FLAG_PAID));
913
914
915
916
    }

    public function hasBeenShipped()
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
917
        return count($this->getHistory((int) $this->id_lang, false, false, OrderState::FLAG_SHIPPED));
918
919
920
921
    }

    public function isInPreparation()
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
922
        return count($this->getHistory((int) $this->id_lang, Configuration::get('PS_OS_PREPARATION')));
923
924
925
    }

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
926
     * Checks if the current order status is paid and shipped.
927
928
929
930
931
932
     *
     * @return bool
     */
    public function isPaidAndShipped()
    {
        $order_state = $this->getCurrentOrderState();
933
        if ($order_state && $order_state->paid && $order_state->shipped) {
934
            return true;
935
        }
Mickaël Andrieu's avatar
Mickaël Andrieu committed
936

937
938
939
940
        return false;
    }

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
941
     * Get customer orders.
942
943
944
     *
     * @param int $id_customer Customer id
     * @param bool $show_hidden_status Display or not hidden order statuses
Mickaël Andrieu's avatar
Mickaël Andrieu committed
945
     *
946
947
948
949
     * @return array Customer orders
     */
    public static function getCustomerOrders($id_customer, $show_hidden_status = false, Context $context = null)
    {
950
        if (!$context) {
951
            $context = Context::getContext();
952
        }
953

954
        $orderStates = OrderState::getOrderStates((int) $context->language->id, false);
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
955
        $indexedOrderStates = [];
956
957
958
        foreach ($orderStates as $orderState) {
            $indexedOrderStates[$orderState['id_order_state']] = $orderState;
        }
959

960
        $res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
961
        SELECT o.*,
Mickaël Andrieu's avatar
Mickaël Andrieu committed
962
963
964
965
966
          (SELECT SUM(od.`product_quantity`) FROM `' . _DB_PREFIX_ . 'order_detail` od WHERE od.`id_order` = o.`id_order`) nb_products,
          (SELECT oh.`id_order_state` FROM `' . _DB_PREFIX_ . 'order_history` oh
           LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON (os.`id_order_state` = oh.`id_order_state`)
           WHERE oh.`id_order` = o.`id_order` ' .
            (!$show_hidden_status ? ' AND os.`hidden` != 1' : '') .
967
            ' ORDER BY oh.`date_add` DESC, oh.`id_order_history` DESC LIMIT 1) id_order_state
Mickaël Andrieu's avatar
Mickaël Andrieu committed
968
969
970
        FROM `' . _DB_PREFIX_ . 'orders` o
        WHERE o.`id_customer` = ' . (int) $id_customer .
            Shop::addSqlRestriction(Shop::SHARE_ORDER) . '
971
972
        GROUP BY o.`id_order`
        ORDER BY o.`date_add` DESC');
jocelyn fournier's avatar