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 */
26class OrderDetailCore extends ObjectModel
27{
28    /** @var int */
29    public $id_order_detail;
30
31    /** @var int */
32    public $id_order;
33
34    /** @var int */
35    public $id_order_invoice;
36
37    /** @var int */
38    public $product_id;
39
40    /** @var int */
41    public $id_shop;
42
43    /** @var int */
44    public $product_attribute_id;
45
46    /** @var int */
47    public $id_customization;
48
49    /** @var string */
50    public $product_name;
51
52    /** @var int */
53    public $product_quantity;
54
55    /** @var int */
56    public $product_quantity_in_stock;
57
58    /** @var int */
59    public $product_quantity_return;
60
61    /** @var int */
62    public $product_quantity_refunded;
63
64    /** @var int */
65    public $product_quantity_reinjected;
66
67    /**
68     * @deprecated since 1.5 Use unit_price_tax_excl instead
69     *
70     * @var float Without taxes, includes ecotax
71     */
72    public $product_price;
73
74    /** @var float */
75    public $original_product_price;
76
77    /** @var float With taxes, includes ecotax */
78    public $unit_price_tax_incl;
79
80    /** @var float Without taxes, includes ecotax */
81    public $unit_price_tax_excl;
82
83    /** @var float With taxes, includes ecotax */
84    public $total_price_tax_incl;
85
86    /** @var float Without taxes, includes ecotax */
87    public $total_price_tax_excl;
88
89    /** @var float */
90    public $reduction_percent;
91
92    /** @var float */
93    public $reduction_amount;
94
95    /** @var float */
96    public $reduction_amount_tax_excl;
97
98    /** @var float */
99    public $reduction_amount_tax_incl;
100
101    /** @var float */
102    public $group_reduction;
103
104    /** @var float */
105    public $product_quantity_discount;
106
107    /** @var string */
108    public $product_ean13;
109
110    /** @var string */
111    public $product_isbn;
112
113    /** @var string */
114    public $product_upc;
115
116    /** @var string */
117    public $product_mpn;
118
119    /** @var string */
120    public $product_reference;
121
122    /** @var string */
123    public $product_supplier_reference;
124
125    /** @var float */
126    public $product_weight;
127
128    /** @var float */
129    public $ecotax;
130
131    /** @var float */
132    public $ecotax_tax_rate;
133
134    /** @var int */
135    public $discount_quantity_applied;
136
137    /** @var string */
138    public $download_hash;
139
140    /** @var int */
141    public $download_nb;
142
143    /** @var datetime */
144    public $download_deadline;
145
146    /**
147     * @var string @deprecated Order Detail Tax is saved in order_detail_tax table now
148     */
149    public $tax_name;
150
151    /**
152     * @var float @deprecated Order Detail Tax is saved in order_detail_tax table now
153     */
154    public $tax_rate;
155
156    /** @var float */
157    public $tax_computation_method;
158
159    /** @var int Id tax rules group */
160    public $id_tax_rules_group;
161
162    /** @var int Id warehouse */
163    public $id_warehouse;
164
165    /** @var float additional shipping price tax excl */
166    public $total_shipping_price_tax_excl;
167
168    /** @var float additional shipping price tax incl */
169    public $total_shipping_price_tax_incl;
170
171    /** @var float */
172    public $purchase_supplier_price;
173
174    /** @var float */
175    public $original_wholesale_price;
176
177    /** @var float */
178    public $total_refunded_tax_excl;
179
180    /** @var float */
181    public $total_refunded_tax_incl;
182
183    /**
184     * @see ObjectModel::$definition
185     */
186    public static $definition = [
187        'table' => 'order_detail',
188        'primary' => 'id_order_detail',
189        'fields' => [
190            'id_order' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
191            'id_order_invoice' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
192            'id_warehouse' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
193            'id_shop' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
194            'product_id' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
195            'product_attribute_id' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
196            'id_customization' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
197            'product_name' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true],
198            'product_quantity' => ['type' => self::TYPE_INT, 'validate' => 'isInt', 'required' => true],
199            'product_quantity_in_stock' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
200            'product_quantity_return' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
201            'product_quantity_refunded' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
202            'product_quantity_reinjected' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
203            'product_price' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice', 'required' => true],
204            'reduction_percent' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
205            'reduction_amount' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
206            'reduction_amount_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
207            'reduction_amount_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
208            'group_reduction' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
209            'product_quantity_discount' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
210            'product_ean13' => ['type' => self::TYPE_STRING, 'validate' => 'isEan13'],
211            'product_isbn' => ['type' => self::TYPE_STRING, 'validate' => 'isIsbn'],
212            'product_upc' => ['type' => self::TYPE_STRING, 'validate' => 'isUpc'],
213            'product_mpn' => ['type' => self::TYPE_STRING, 'validate' => 'isMpn'],
214            'product_reference' => ['type' => self::TYPE_STRING, 'validate' => 'isReference'],
215            'product_supplier_reference' => ['type' => self::TYPE_STRING, 'validate' => 'isReference'],
216            'product_weight' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
217            'tax_name' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName'],
218            'tax_rate' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
219            'tax_computation_method' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
220            'id_tax_rules_group' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
221            'ecotax' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
222            'ecotax_tax_rate' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
223            'discount_quantity_applied' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
224            'download_hash' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName'],
225            'download_nb' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
226            'download_deadline' => ['type' => self::TYPE_DATE, 'validate' => 'isDateFormat'],
227            'unit_price_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
228            'unit_price_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
229            'total_price_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
230            'total_price_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
231            'total_shipping_price_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
232            'total_shipping_price_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
233            'purchase_supplier_price' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
234            'original_product_price' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
235            'original_wholesale_price' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
236            'total_refunded_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
237            'total_refunded_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
238        ],
239    ];
240
241    protected $webserviceParameters = [
242        'fields' => [
243            'id_order' => ['xlink_resource' => 'orders'],
244            'product_id' => ['xlink_resource' => 'products'],
245            'product_attribute_id' => ['xlink_resource' => 'combinations'],
246            'product_quantity_reinjected' => [],
247            'group_reduction' => [],
248            'discount_quantity_applied' => [],
249            'download_hash' => [],
250            'download_deadline' => [],
251        ],
252        'hidden_fields' => ['tax_rate', 'tax_name'],
253        'associations' => [
254            'taxes' => ['resource' => 'tax', 'getter' => 'getWsTaxes', 'setter' => false,
255                'fields' => ['id' => []],
256            ],
257        ],
258    ];
259
260    /** @var bool */
261    protected $outOfStock = false;
262
263    /** @var TaxCalculator object */
264    protected $tax_calculator = null;
265
266    /** @var Address object */
267    protected $vat_address = null;
268
269    /** @var Address object */
270    protected $specificPrice = null;
271
272    /** @var Customer object */
273    protected $customer = null;
274
275    /** @var Context object */
276    protected $context = null;
277
278    public function __construct($id = null, $id_lang = null, $context = null)
279    {
280        $this->context = $context;
281        $id_shop = null;
282        if ($this->context != null && isset($this->context->shop)) {
283            $id_shop = $this->context->shop->id;
284        }
285        parent::__construct($id, $id_lang, $id_shop);
286
287        if ($context == null) {
288            $context = Context::getContext();
289        }
290        $this->context = $context->cloneContext();
291    }
292
293    public function delete()
294    {
295        if (!$res = parent::delete()) {
296            return false;
297        }
298
299        Db::getInstance()->delete('order_detail_tax', 'id_order_detail=' . (int) $this->id);
300
301        return $res;
302    }
303
304    protected function setContext($id_shop)
305    {
306        if ($this->context->shop->id != $id_shop) {
307            $this->context->shop = new Shop((int) $id_shop);
308        }
309    }
310
311    public static function getDownloadFromHash($hash)
312    {
313        if ($hash == '') {
314            return false;
315        }
316        $sql = 'SELECT *
317        FROM `' . _DB_PREFIX_ . 'order_detail` od
318        LEFT JOIN `' . _DB_PREFIX_ . 'product_download` pd ON (od.`product_id`=pd.`id_product`)
319        WHERE od.`download_hash` = \'' . pSQL((string) $hash) . '\'
320        AND pd.`active` = 1';
321
322        return Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql);
323    }
324
325    public static function incrementDownload($id_order_detail, $increment = 1)
326    {
327        $sql = 'UPDATE `' . _DB_PREFIX_ . 'order_detail`
328            SET `download_nb` = `download_nb` + ' . (int) $increment . '
329            WHERE `id_order_detail`= ' . (int) $id_order_detail . '
330            LIMIT 1';
331
332        return Db::getInstance()->execute($sql);
333    }
334
335    /**
336     * Returns the tax calculator associated to this order detail.
337     *
338     * @since 1.5.0.1
339     *
340     * @return TaxCalculator
341     */
342    public function getTaxCalculator()
343    {
344        return OrderDetail::getTaxCalculatorStatic($this->id);
345    }
346
347    /**
348     * Return the tax calculator associated to this order_detail.
349     *
350     * @since 1.5.0.1
351     *
352     * @param int $id_order_detail
353     *
354     * @return TaxCalculator
355     */
356    public static function getTaxCalculatorStatic($id_order_detail)
357    {
358        $sql = 'SELECT t.*, d.`tax_computation_method`
359                FROM `' . _DB_PREFIX_ . 'order_detail_tax` t
360                LEFT JOIN `' . _DB_PREFIX_ . 'order_detail` d ON (d.`id_order_detail` = t.`id_order_detail`)
361                WHERE d.`id_order_detail` = ' . (int) $id_order_detail;
362
363        $computation_method = 1;
364        $taxes = [];
365        if ($results = Db::getInstance()->executeS($sql)) {
366            foreach ($results as $result) {
367                $taxes[] = new Tax((int) $result['id_tax']);
368                $computation_method = $result['tax_computation_method'];
369            }
370        }
371
372        return new TaxCalculator($taxes, $computation_method);
373    }
374
375    /**
376     * Save the tax calculator.
377     *
378     * @since 1.5.0.1
379     * @deprecated Functionality moved to Order::updateOrderDetailTax
380     *             because we need the full order object to do a good job here.
381     *             Will no longer be supported after 1.6.1
382     *             (Note: this one is not that deprecated because Order::updateOrderDetailTax
383     *             performs no update unless order_detail_tax is filled. So we rely on updateTaxAmount
384     *             which correctly builds the TaxCalculator with up to date taxes unlike getTaxCalculatorStatic)
385     *
386     * @return bool
387     */
388    public function saveTaxCalculator(Order $order, $replace = false)
389    {
390        // Nothing to save
391        if ($this->tax_calculator == null) {
392            return true;
393        }
394
395        if (!($this->tax_calculator instanceof TaxCalculator)) {
396            return false;
397        }
398
399        $shipping_tax_amount = 0;
400
401        foreach ($order->getCartRules() as $cart_rule) {
402            if ($cart_rule['free_shipping']) {
403                $shipping_tax_amount = $order->total_shipping_tax_excl;
404
405                break;
406            }
407        }
408
409        $ratio = ($order->total_products > 0) ? ($this->unit_price_tax_excl / $order->total_products) : 1;
410
411        $order_reduction_amount = ($order->total_discounts_tax_excl - $shipping_tax_amount) * $ratio;
412        $discounted_price_tax_excl = $this->unit_price_tax_excl - $order_reduction_amount;
413
414        $values = '';
415        foreach ($this->tax_calculator->getTaxesAmount($discounted_price_tax_excl) as $id_tax => $amount) {
416            switch (Configuration::get('PS_ROUND_TYPE')) {
417                case Order::ROUND_ITEM:
418                    $unit_amount = (float) Tools::ps_round($amount, Context::getContext()->getComputingPrecision());
419                    $total_amount = $unit_amount * $this->product_quantity;
420
421                    break;
422                case Order::ROUND_LINE:
423                    $unit_amount = $amount;
424                    $total_amount = Tools::ps_round($unit_amount * $this->product_quantity, Context::getContext()->getComputingPrecision());
425
426                    break;
427                case Order::ROUND_TOTAL:
428                    $unit_amount = $amount;
429                    $total_amount = $unit_amount * $this->product_quantity;
430
431                    break;
432            }
433
434            $values .= '(' . (int) $this->id . ',' . (int) $id_tax . ',' . (float) $unit_amount . ',' . (float) $total_amount . '),';
435        }
436
437        if ($replace) {
438            Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'order_detail_tax` WHERE id_order_detail=' . (int) $this->id);
439        }
440
441        if (!empty($values)) {
442            $values = rtrim($values, ',');
443            $sql = 'INSERT INTO `' . _DB_PREFIX_ . 'order_detail_tax` (id_order_detail, id_tax, unit_amount, total_amount)
444                VALUES ' . $values;
445
446            return Db::getInstance()->execute($sql);
447        }
448
449        return true;
450    }
451
452    public function updateTaxAmount($order)
453    {
454        $address = new Address((int) $order->{Configuration::get('PS_TAX_ADDRESS_TYPE')});
455        $this->tax_calculator = $this->getTaxCalculatorByAddress($address);
456
457        return $this->saveTaxCalculator($order, true);
458    }
459
460    /**
461     * Get a TaxCalculator adapted for the OrderDetail's product and the specified address
462     *
463     * @param Address $address
464     *
465     * @return TaxCalculator
466     */
467    public function getTaxCalculatorByAddress(Address $address): TaxCalculator
468    {
469        $this->setContext((int) $this->id_shop);
470        $tax_manager = TaxManagerFactory::getManager($address, $this->getTaxRulesGroupId());
471
472        return $tax_manager->getTaxCalculator();
473    }
474
475    /**
476     * Dynamically get the taxRulesGroupId instead of relying one the one saved in database
477     *
478     * @return int
479     */
480    public function getTaxRulesGroupId(): int
481    {
482        return (int) Product::getIdTaxRulesGroupByIdProduct((int) $this->product_id, $this->context);
483    }
484
485    /**
486     * Get a detailed order list of an id_order.
487     *
488     * @param int $id_order
489     *
490     * @return array
491     */
492    public static function getList($id_order)
493    {
494        return Db::getInstance()->executeS('SELECT * FROM `' . _DB_PREFIX_ . 'order_detail` WHERE `id_order` = ' . (int) $id_order);
495    }
496
497    public function getTaxList()
498    {
499        return self::getTaxListStatic($this->id);
500    }
501
502    public static function getTaxListStatic($id_order_detail)
503    {
504        $sql = 'SELECT * FROM `' . _DB_PREFIX_ . 'order_detail_tax`
505                    WHERE `id_order_detail` = ' . (int) $id_order_detail;
506
507        return Db::getInstance()->executeS($sql);
508    }
509
510    /**
511     * Set virtual product information
512     *
513     * @param array $product
514     */
515    protected function setVirtualProductInformation($product)
516    {
517        // Add some informations for virtual products
518        $this->download_deadline = '0000-00-00 00:00:00';
519        $this->download_hash = null;
520
521        if ($id_product_download = ProductDownload::getIdFromIdProduct((int) $product['id_product'])) {
522            $product_download = new ProductDownload((int) $id_product_download);
523            $this->download_deadline = $product_download->getDeadLine();
524            $this->download_hash = $product_download->getHash();
525
526            unset($product_download);
527        }
528    }
529
530    /**
531     * Check the order status.
532     *
533     * @param array $product
534     * @param int $id_order_state
535     */
536    protected function checkProductStock($product, $id_order_state)
537    {
538        if ($id_order_state != Configuration::get('PS_OS_CANCELED') && $id_order_state != Configuration::get('PS_OS_ERROR')) {
539            $update_quantity = true;
540            if (!StockAvailable::dependsOnStock($product['id_product'])) {
541                $update_quantity = StockAvailable::updateQuantity($product['id_product'], $product['id_product_attribute'], -(int) $product['cart_quantity'], $product['id_shop'], true);
542            }
543
544            if ($update_quantity) {
545                $product['stock_quantity'] -= $product['cart_quantity'];
546            }
547
548            if ($product['stock_quantity'] < 0 && Configuration::get('PS_STOCK_MANAGEMENT')) {
549                $this->outOfStock = true;
550            }
551            Product::updateDefaultAttribute($product['id_product']);
552        }
553    }
554
555    /**
556     * Apply tax to the product.
557     *
558     * @param object $order
559     * @param array $product
560     */
561    protected function setProductTax(Order $order, $product)
562    {
563        $this->ecotax = Tools::convertPrice((float) ($product['ecotax']), (int) ($order->id_currency));
564
565        // Exclude VAT
566        if (!Tax::excludeTaxeOption()) {
567            $this->setContext((int) $product['id_shop']);
568            $this->id_tax_rules_group = (int) Product::getIdTaxRulesGroupByIdProduct((int) $product['id_product'], $this->context);
569
570            $tax_manager = TaxManagerFactory::getManager($this->vat_address, $this->id_tax_rules_group);
571            $this->tax_calculator = $tax_manager->getTaxCalculator();
572            $this->tax_computation_method = (int) $this->tax_calculator->computation_method;
573            $this->tax_rate = (float) $this->tax_calculator->getTotalRate();
574            $this->tax_name = $this->tax_calculator->getTaxesName();
575        }
576
577        $this->ecotax_tax_rate = 0;
578        if (!empty($product['ecotax'])) {
579            $this->ecotax_tax_rate = Tax::getProductEcotaxRate($order->{Configuration::get('PS_TAX_ADDRESS_TYPE')});
580        }
581    }
582
583    /**
584     * Set specific price of the product.
585     *
586     * @param object $order
587     */
588    protected function setSpecificPrice(Order $order, $product = null)
589    {
590        $this->reduction_amount = 0.00;
591        $this->reduction_percent = 0.00;
592        $this->reduction_amount_tax_incl = 0.00;
593        $this->reduction_amount_tax_excl = 0.00;
594
595        if ($this->specificPrice) {
596            switch ($this->specificPrice['reduction_type']) {
597                case 'percentage':
598                    $this->reduction_percent = (float) $this->specificPrice['reduction'] * 100;
599
600                    break;
601
602                case 'amount':
603                    $price = Tools::convertPrice($this->specificPrice['reduction'], $order->id_currency);
604                    $this->reduction_amount = !$this->specificPrice['id_currency'] ? (float) $price : (float) $this->specificPrice['reduction'];
605                    if ($product !== null) {
606                        $this->setContext((int) $product['id_shop']);
607                    }
608                    $id_tax_rules = (int) Product::getIdTaxRulesGroupByIdProduct((int) $this->specificPrice['id_product'], $this->context);
609                    $tax_manager = TaxManagerFactory::getManager($this->vat_address, $id_tax_rules);
610                    $this->tax_calculator = $tax_manager->getTaxCalculator();
611
612                    if ($this->specificPrice['reduction_tax']) {
613                        $this->reduction_amount_tax_incl = $this->reduction_amount;
614                        $this->reduction_amount_tax_excl = Tools::ps_round($this->tax_calculator->removeTaxes($this->reduction_amount), Context::getContext()->getComputingPrecision());
615                    } else {
616                        $this->reduction_amount_tax_incl = Tools::ps_round($this->tax_calculator->addTaxes($this->reduction_amount), Context::getContext()->getComputingPrecision());
617                        $this->reduction_amount_tax_excl = $this->reduction_amount;
618                    }
619
620                    break;
621            }
622        }
623    }
624
625    /**
626     * Set detailed product price to the order detail.
627     *
628     * @param object $order
629     * @param object $cart
630     * @param array $product
631     */
632    protected function setDetailProductPrice(Order $order, Cart $cart, $product)
633    {
634        $this->setContext((int) $product['id_shop']);
635        Product::getPriceStatic((int) $product['id_product'], true, (int) $product['id_product_attribute'], 6, null, false, true, $product['cart_quantity'], false, (int) $order->id_customer, (int) $order->id_cart, (int) $order->{Configuration::get('PS_TAX_ADDRESS_TYPE')}, $specific_price, true, true, $this->context);
636        $this->specificPrice = $specific_price;
637        $this->original_product_price = Product::getPriceStatic(
638            $product['id_product'],
639            false,
640            (int) $product['id_product_attribute'],
641            6,
642            null,
643            false,
644            false,
645            1,
646            false,
647            null,
648            null,
649            null,
650            $null,
651            true,
652            true,
653            $this->context
654        );
655        $this->unit_price_tax_incl = (float) $product['price_wt'];
656        $this->product_price = $this->unit_price_tax_excl = (float) $product['price'];
657        $this->total_price_tax_incl = (float) $product['total_wt'];
658        $this->total_price_tax_excl = (float) $product['total'];
659
660        $this->purchase_supplier_price = (float) $product['wholesale_price'];
661        if ($product['id_supplier'] > 0 && ($supplier_price = ProductSupplier::getProductPrice((int) $product['id_supplier'], $product['id_product'], $product['id_product_attribute'], true)) > 0) {
662            $this->purchase_supplier_price = (float) $supplier_price;
663        }
664
665        $this->setSpecificPrice($order, $product);
666
667        $this->group_reduction = (float) Group::getReduction((int) $order->id_customer);
668
669        $shop_id = $this->context->shop->id;
670
671        $quantity_discount = SpecificPrice::getQuantityDiscount(
672            (int) $product['id_product'],
673            $shop_id,
674            (int) $cart->id_currency,
675            (int) $this->vat_address->id_country,
676            (int) $this->customer->id_default_group,
677            (int) $product['cart_quantity'],
678            false,
679            null,
680            null,
681            $null,
682            true,
683            true,
684            $this->context
685        );
686
687        $unit_price = Product::getPriceStatic(
688            (int) $product['id_product'],
689            true,
690            ($product['id_product_attribute'] ? (int) ($product['id_product_attribute']) : null),
691            2,
692            null,
693            false,
694            true,
695            1,
696            false,
697            (int) $order->id_customer,
698            null,
699            (int) $order->{Configuration::get('PS_TAX_ADDRESS_TYPE')},
700            $null,
701            true,
702            true,
703            $this->context
704        );
705        $this->product_quantity_discount = 0.00;
706        if ($quantity_discount) {
707            $this->product_quantity_discount = $unit_price;
708            if (Product::getTaxCalculationMethod((int) $order->id_customer) == PS_TAX_EXC) {
709                $this->product_quantity_discount = Tools::ps_round($unit_price, Context::getContext()->getComputingPrecision());
710            }
711
712            if (isset($this->tax_calculator)) {
713                $this->product_quantity_discount -= $this->tax_calculator->addTaxes($quantity_discount['price']);
714            }
715        }
716
717        $this->discount_quantity_applied = (($this->specificPrice && $this->specificPrice['from_quantity'] > 1) ? 1 : 0);
718    }
719
720    /**
721     * Create an order detail liable to an id_order.
722     *
723     * @param object $order
724     * @param object $cart
725     * @param array $product
726     * @param int $id_order_status
727     * @param int $id_order_invoice
728     * @param bool $use_taxes set to false if you don't want to use taxes
729     */
730    protected function create(Order $order, Cart $cart, $product, $id_order_state, $id_order_invoice, $use_taxes = true, $id_warehouse = 0)
731    {
732        if ($use_taxes) {
733            $this->tax_calculator = new TaxCalculator();
734        }
735
736        $this->id = null;
737
738        $this->product_id = (int) $product['id_product'];
739        $this->product_attribute_id = $product['id_product_attribute'] ? (int) $product['id_product_attribute'] : 0;
740        $this->id_customization = $product['id_customization'] ? (int) $product['id_customization'] : 0;
741        $this->product_name = $product['name'] .
742            ((isset($product['attributes']) && $product['attributes'] != null) ?
743                ' (' . $product['attributes'] . ')' : '');
744
745        $this->product_quantity = (int) $product['cart_quantity'];
746        $this->product_ean13 = empty($product['ean13']) ? null : pSQL($product['ean13']);
747        $this->product_isbn = empty($product['isbn']) ? null : pSQL($product['isbn']);
748        $this->product_upc = empty($product['upc']) ? null : pSQL($product['upc']);
749        $this->product_mpn = empty($product['mpn']) ? null : pSQL($product['mpn']);
750        $this->product_reference = empty($product['reference']) ? null : pSQL($product['reference']);
751        $this->product_supplier_reference = empty($product['supplier_reference']) ? null : pSQL($product['supplier_reference']);
752        $this->product_weight = $product['id_product_attribute'] ? (float) $product['weight_attribute'] : (float) $product['weight'];
753        $this->id_warehouse = $id_warehouse;
754
755        $product_quantity = (int) Product::getQuantity($this->product_id, $this->product_attribute_id, null, $cart);
756        $this->product_quantity_in_stock = ($product_quantity - (int) $product['cart_quantity'] < 0) ?
757            $product_quantity : (int) $product['cart_quantity'];
758
759        $this->setVirtualProductInformation($product);
760        $this->checkProductStock($product, $id_order_state);
761
762        if ($use_taxes) {
763            $this->setProductTax($order, $product);
764        }
765        $this->setShippingCost($order, $product);
766        $this->setDetailProductPrice($order, $cart, $product);
767
768        // Set order invoice id
769        $this->id_order_invoice = (int) $id_order_invoice;
770
771        // Set shop id
772        $this->id_shop = (int) $product['id_shop'];
773
774        // Add new entry to the table
775        $this->save();
776
777        if ($use_taxes) {
778            $this->saveTaxCalculator($order);
779        }
780        unset($this->tax_calculator);
781    }
782
783    /**
784     * Create a list of order detail for a specified id_order using cart.
785     *
786     * @param object $order
787     * @param object $cart
788     * @param int $id_order_status
789     * @param int $id_order_invoice
790     * @param bool $use_taxes set to false if you don't want to use taxes
791     */
792    public function createList(Order $order, Cart $cart, $id_order_state, $product_list, $id_order_invoice = 0, $use_taxes = true, $id_warehouse = 0)
793    {
794        $this->vat_address = new Address((int) $order->{Configuration::get('PS_TAX_ADDRESS_TYPE')});
795        $this->customer = new Customer((int) $order->id_customer);
796
797        $this->id_order = $order->id;
798        $this->outOfStock = false;
799
800        foreach ($product_list as $product) {
801            $this->create($order, $cart, $product, $id_order_state, $id_order_invoice, $use_taxes, $id_warehouse);
802        }
803
804        unset(
805            $this->vat_address,
806            $this->customer
807        );
808    }
809
810    /**
811     * Get the state of the current stock product.
812     *
813     * @return array
814     */
815    public function getStockState()
816    {
817        return $this->outOfStock;
818    }
819
820    /**
821     * Set the additional shipping information.
822     *
823     * @param Order $order
824     * @param $product
825     */
826    public function setShippingCost(Order $order, $product)
827    {
828        $tax_rate = 0;
829
830        $carrier = OrderInvoice::getCarrier((int) $this->id_order_invoice);
831        if (isset($carrier) && Validate::isLoadedObject($carrier)) {
832            $tax_rate = $carrier->getTaxesRate(new Address((int) $order->{Configuration::get('PS_TAX_ADDRESS_TYPE')}));
833        }
834
835        $this->total_shipping_price_tax_excl = (float) $product['additional_shipping_cost'];
836        $this->total_shipping_price_tax_incl = (float) ($this->total_shipping_price_tax_excl * (1 + ($tax_rate / 100)));
837        $this->total_shipping_price_tax_incl = Tools::ps_round($this->total_shipping_price_tax_incl, Context::getContext()->getComputingPrecision());
838    }
839
840    public function getWsTaxes()
841    {
842        $query = new DbQuery();
843        $query->select('id_tax as id');
844        $query->from('order_detail_tax', 'tax');
845        $query->leftJoin('order_detail', 'od', 'tax.`id_order_detail` = od.`id_order_detail`');
846        $query->where('od.`id_order_detail` = ' . (int) $this->id_order_detail);
847
848        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query);
849    }
850
851    public static function getCrossSells($id_product, $id_lang, $limit = 12)
852    {
853        if (!$id_product || !$id_lang) {
854            return;
855        }
856
857        $front = true;
858        if (!in_array(Context::getContext()->controller->controller_type, ['front', 'modulefront'])) {
859            $front = false;
860        }
861
862        $orders = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
863        SELECT o.id_order
864        FROM ' . _DB_PREFIX_ . 'orders o
865        LEFT JOIN ' . _DB_PREFIX_ . 'order_detail od ON (od.id_order = o.id_order)
866        WHERE o.valid = 1 AND od.product_id = ' . (int) $id_product);
867
868        if (count($orders)) {
869            $list = '';
870            foreach ($orders as $order) {
871                $list .= (int) $order['id_order'] . ',';
872            }
873            $list = rtrim($list, ',');
874
875            $order_products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
876                SELECT DISTINCT od.product_id, p.id_product, pl.name, pl.link_rewrite, p.reference, i.id_image, product_shop.show_price,
877                cl.link_rewrite category, p.ean13, p.isbn, p.out_of_stock, p.id_category_default ' . (Combination::isFeatureActive() ? ', IFNULL(product_attribute_shop.id_product_attribute,0) id_product_attribute' : '') . '
878                FROM ' . _DB_PREFIX_ . 'order_detail od
879                LEFT JOIN ' . _DB_PREFIX_ . 'product p ON (p.id_product = od.product_id)
880                ' . Shop::addSqlAssociation('product', 'p') .
881                (Combination::isFeatureActive() ? 'LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` product_attribute_shop
882                ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shop.id_shop=' . (int) Context::getContext()->shop->id . ')' : '') . '
883                LEFT JOIN ' . _DB_PREFIX_ . 'product_lang pl ON (pl.id_product = od.product_id' . Shop::addSqlRestrictionOnLang('pl') . ')
884                LEFT JOIN ' . _DB_PREFIX_ . 'category_lang cl ON (cl.id_category = product_shop.id_category_default' . Shop::addSqlRestrictionOnLang('cl') . ')
885                LEFT JOIN ' . _DB_PREFIX_ . 'image i ON (i.id_product = od.product_id)
886                ' . Shop::addSqlAssociation('image', 'i', true, 'image_shop.cover=1') . '
887                WHERE od.id_order IN (' . $list . ')
888                    AND pl.id_lang = ' . (int) $id_lang . '
889                    AND cl.id_lang = ' . (int) $id_lang . '
890                    AND od.product_id != ' . (int) $id_product . '
891                    AND product_shop.active = 1'
892                    . ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . '
893                ORDER BY RAND()
894                LIMIT ' . (int) $limit . '
895            ', true, false);
896
897            $tax_calc = Product::getTaxCalculationMethod();
898            if (is_array($order_products)) {
899                foreach ($order_products as &$order_product) {
900                    $order_product['image'] = Context::getContext()->link->getImageLink(
901                        $order_product['link_rewrite'],
902                        (int) $order_product['product_id'] . '-' . (int) $order_product['id_image'],
903                        ImageType::getFormattedName('medium')
904                    );
905                    $order_product['link'] = Context::getContext()->link->getProductLink(
906                        (int) $order_product['product_id'],
907                        $order_product['link_rewrite'],
908                        $order_product['category'],
909                        $order_product['ean13']
910                    );
911                    if ($tax_calc == 0 || $tax_calc == 2) {
912                        $order_product['displayed_price'] = Product::getPriceStatic((int) $order_product['product_id'], true, null);
913                    } elseif ($tax_calc == 1) {
914                        $order_product['displayed_price'] = Product::getPriceStatic((int) $order_product['product_id'], false, null);
915                    }
916                }
917
918                return Product::getProductsProperties($id_lang, $order_products);
919            }
920        }
921    }
922
923    public function add($autodate = true, $null_values = false)
924    {
925        foreach ($this->def['fields'] as $field => $data) {
926            if (!empty($data['required']) || !empty($data['lang'])) {
927                continue;
928            }
929            if ($this->validateField($field, $this->$field) !== true) {
930                $this->$field = '';
931            }
932        }
933
934        $this->original_wholesale_price = $this->getWholeSalePrice();
935
936        return parent::add($autodate = true, $null_values = false);
937    }
938
939    //return the product OR product attribute whole sale price
940    public function getWholeSalePrice()
941    {
942        $product = new Product($this->product_id);
943        $wholesale_price = $product->wholesale_price;
944
945        if ($this->product_attribute_id) {
946            $combination = new Combination((int) $this->product_attribute_id);
947            if ($combination && $combination->wholesale_price != '0.000000') {
948                $wholesale_price = $combination->wholesale_price;
949            }
950        }
951
952        return $wholesale_price;
953    }
954}
955