<?php
/**
 * Copyright since 2007 PrestaShop SA and Contributors
 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is bundled with this package in the file LICENSE.md.
 * It is also available through the world-wide-web at this URL:
 * https://opensource.org/licenses/OSL-3.0
 * 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
 * needs please refer to https://devdocs.prestashop.com/ for more information.
 *
 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
 * @copyright Since 2007 PrestaShop SA and Contributors
 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
 */
class OrderDetailCore extends ObjectModel
{
    /** @var int */
    public $id_order_detail;

    /** @var int */
    public $id_order;

    /** @var int */
    public $id_order_invoice;

    /** @var int */
    public $product_id;

    /** @var int */
    public $id_shop;

    /** @var int */
    public $product_attribute_id;

    /** @var int */
    public $id_customization;

    /** @var string */
    public $product_name;

    /** @var int */
    public $product_quantity;

    /** @var int */
    public $product_quantity_in_stock;

    /** @var int */
    public $product_quantity_return;

    /** @var int */
    public $product_quantity_refunded;

    /** @var int */
    public $product_quantity_reinjected;

    /**
     * @deprecated since 1.5 Use unit_price_tax_excl instead
     *
     * @var float Without taxes, includes ecotax
     */
    public $product_price;

    /** @var float */
    public $original_product_price;

    /** @var float With taxes, includes ecotax */
    public $unit_price_tax_incl;

    /** @var float Without taxes, includes ecotax */
    public $unit_price_tax_excl;

    /** @var float With taxes, includes ecotax */
    public $total_price_tax_incl;

    /** @var float Without taxes, includes ecotax */
    public $total_price_tax_excl;

    /** @var float */
    public $reduction_percent;

    /** @var float */
    public $reduction_amount;

    /** @var float */
    public $reduction_amount_tax_excl;

    /** @var float */
    public $reduction_amount_tax_incl;

    /** @var float */
    public $group_reduction;

    /** @var float */
    public $product_quantity_discount;

    /** @var string */
    public $product_ean13;

    /** @var string */
    public $product_isbn;

    /** @var string */
    public $product_upc;

    /** @var string */
    public $product_mpn;

    /** @var string */
    public $product_reference;

    /** @var string */
    public $product_supplier_reference;

    /** @var float */
    public $product_weight;

    /** @var float */
    public $ecotax;

    /** @var float */
    public $ecotax_tax_rate;

    /** @var int */
    public $discount_quantity_applied;

    /** @var string|null */
    public $download_hash;

    /** @var int */
    public $download_nb;

    /** @var string */
    public $download_deadline;

    /**
     * @var string
     *
     * @deprecated Order Detail Tax is saved in order_detail_tax table now
     */
    public $tax_name;

    /**
     * @var float
     *
     * @deprecated Order Detail Tax is saved in order_detail_tax table now
     */
    public $tax_rate;

    /** @var float */
    public $tax_computation_method;

    /** @var int Id tax rules group */
    public $id_tax_rules_group;

    /** @var int Id warehouse */
    public $id_warehouse;

    /** @var float additional shipping price tax excl */
    public $total_shipping_price_tax_excl;

    /** @var float additional shipping price tax incl */
    public $total_shipping_price_tax_incl;

    /** @var float */
    public $purchase_supplier_price;

    /** @var float */
    public $original_wholesale_price;

    /** @var float */
    public $total_refunded_tax_excl;

    /** @var float */
    public $total_refunded_tax_incl;

    /**
     * @see ObjectModel::$definition
     */
    public static $definition = [
        'table' => 'order_detail',
        'primary' => 'id_order_detail',
        'fields' => [
            'id_order' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
            'id_order_invoice' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'id_warehouse' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
            'id_shop' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
            'product_id' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'product_attribute_id' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'id_customization' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'product_name' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 4194303],
            'product_quantity' => ['type' => self::TYPE_INT, 'validate' => 'isInt', 'required' => true],
            'product_quantity_in_stock' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
            'product_quantity_return' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
            'product_quantity_refunded' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
            'product_quantity_reinjected' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
            'product_price' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice', 'required' => true],
            'reduction_percent' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
            'reduction_amount' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'reduction_amount_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'reduction_amount_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'group_reduction' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
            'product_quantity_discount' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
            'product_ean13' => ['type' => self::TYPE_STRING, 'validate' => 'isEan13'],
            'product_isbn' => ['type' => self::TYPE_STRING, 'validate' => 'isIsbn'],
            'product_upc' => ['type' => self::TYPE_STRING, 'validate' => 'isUpc'],
            'product_mpn' => ['type' => self::TYPE_STRING, 'validate' => 'isMpn'],
            'product_reference' => ['type' => self::TYPE_STRING, 'validate' => 'isReference'],
            'product_supplier_reference' => ['type' => self::TYPE_STRING, 'validate' => 'isReference'],
            'product_weight' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
            'tax_name' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName'],
            'tax_rate' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
            'tax_computation_method' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'id_tax_rules_group' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
            'ecotax' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
            'ecotax_tax_rate' => ['type' => self::TYPE_FLOAT, 'validate' => 'isFloat'],
            'discount_quantity_applied' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
            'download_hash' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName'],
            'download_nb' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
            'download_deadline' => ['type' => self::TYPE_DATE, 'validate' => 'isDateFormat'],
            'unit_price_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'unit_price_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'total_price_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'total_price_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'total_shipping_price_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'total_shipping_price_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'purchase_supplier_price' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'original_product_price' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'original_wholesale_price' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'total_refunded_tax_excl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
            'total_refunded_tax_incl' => ['type' => self::TYPE_FLOAT, 'validate' => 'isPrice'],
        ],
    ];

    protected $webserviceParameters = [
        'fields' => [
            'id_order' => ['xlink_resource' => 'orders'],
            'product_id' => ['xlink_resource' => 'products'],
            'product_attribute_id' => ['xlink_resource' => 'combinations'],
            'product_quantity_reinjected' => [],
            'group_reduction' => [],
            'discount_quantity_applied' => [],
            'download_hash' => [],
            'download_deadline' => [],
        ],
        'hidden_fields' => ['tax_rate', 'tax_name'],
        'associations' => [
            'taxes' => ['resource' => 'tax', 'getter' => 'getWsTaxes', 'setter' => false,
                'fields' => ['id' => []],
            ],
        ],
    ];

    /** @var bool */
    protected $outOfStock = false;

    /** @var TaxCalculator|null object */
    protected $tax_calculator = null;

    /** @var Address object */
    protected $vat_address = null;

    /** @var Address|null */
    protected $specificPrice = null;

    /** @var Customer object */
    protected $customer = null;

    /** @var Context object */
    protected $context = null;

    public function __construct($id = null, $id_lang = null, $context = null)
    {
        $this->context = $context;
        $id_shop = null;
        if ($this->context != null && isset($this->context->shop)) {
            $id_shop = $this->context->shop->id;
        }
        parent::__construct($id, $id_lang, $id_shop);

        if ($context == null) {
            $context = Context::getContext();
        }
        $this->context = $context->cloneContext();
    }

    public function delete()
    {
        if (!$res = parent::delete()) {
            return false;
        }

        Db::getInstance()->delete('order_detail_tax', 'id_order_detail=' . (int) $this->id);

        return $res;
    }

    protected function setContext($id_shop)
    {
        if ($this->context->shop->id != $id_shop) {
            $this->context->shop = new Shop((int) $id_shop);
        }
    }

    public static function getDownloadFromHash($hash)
    {
        if ($hash == '') {
            return false;
        }
        $sql = 'SELECT *
        FROM `' . _DB_PREFIX_ . 'order_detail` od
        LEFT JOIN `' . _DB_PREFIX_ . 'product_download` pd ON (od.`product_id`=pd.`id_product`)
        WHERE od.`download_hash` = \'' . pSQL((string) $hash) . '\'
        AND pd.`active` = 1';

        return Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql);
    }

    public static function incrementDownload($id_order_detail, $increment = 1)
    {
        $sql = 'UPDATE `' . _DB_PREFIX_ . 'order_detail`
            SET `download_nb` = `download_nb` + ' . (int) $increment . '
            WHERE `id_order_detail`= ' . (int) $id_order_detail . '
            LIMIT 1';

        return Db::getInstance()->execute($sql);
    }

    /**
     * Returns the tax calculator associated to this order detail.
     *
     * @since 1.5.0.1
     *
     * @return TaxCalculator
     */
    public function getTaxCalculator()
    {
        return OrderDetail::getTaxCalculatorStatic($this->id);
    }

    /**
     * Return the tax calculator associated to this order_detail.
     *
     * @since 1.5.0.1
     *
     * @param int $id_order_detail
     *
     * @return TaxCalculator
     */
    public static function getTaxCalculatorStatic($id_order_detail)
    {
        $sql = 'SELECT t.*, d.`tax_computation_method`
                FROM `' . _DB_PREFIX_ . 'order_detail_tax` t
                LEFT JOIN `' . _DB_PREFIX_ . 'order_detail` d ON (d.`id_order_detail` = t.`id_order_detail`)
                WHERE d.`id_order_detail` = ' . (int) $id_order_detail;

        $computation_method = 1;
        $taxes = [];
        if ($results = Db::getInstance()->executeS($sql)) {
            foreach ($results as $result) {
                $taxes[] = new Tax((int) $result['id_tax']);
                $computation_method = $result['tax_computation_method'];
            }
        }

        return new TaxCalculator($taxes, $computation_method);
    }

    /**
     * Save the tax calculator.
     *
     * @since 1.5.0.1
     * @deprecated Functionality moved to Order::updateOrderDetailTax
     *             because we need the full order object to do a good job here.
     *             Will no longer be supported after 1.6.1
     *             (Note: this one is not that deprecated because Order::updateOrderDetailTax
     *             performs no update unless order_detail_tax is filled. So we rely on updateTaxAmount
     *             which correctly builds the TaxCalculator with up to date taxes unlike getTaxCalculatorStatic)
     *
     * @return bool
     */
    public function saveTaxCalculator(Order $order, $replace = false)
    {
        // Nothing to save
        if ($this->tax_calculator == null) {
            return true;
        }

        if (!($this->tax_calculator instanceof TaxCalculator)) {
            return false;
        }

        $shipping_tax_amount = 0;

        foreach ($order->getCartRules() as $cart_rule) {
            if ($cart_rule['free_shipping']) {
                $shipping_tax_amount = $order->total_shipping_tax_excl;

                break;
            }
        }

        $ratio = ($order->total_products > 0) ? ($this->unit_price_tax_excl / $order->total_products) : 1;

        $order_reduction_amount = ($order->total_discounts_tax_excl - $shipping_tax_amount) * $ratio;
        $discounted_price_tax_excl = $this->unit_price_tax_excl - $order_reduction_amount;

        $values = '';
        foreach ($this->tax_calculator->getTaxesAmount($discounted_price_tax_excl) as $id_tax => $amount) {
            $unit_amount = $total_amount = 0;
            switch (Configuration::get('PS_ROUND_TYPE')) {
                case Order::ROUND_ITEM:
                    $unit_amount = (float) Tools::ps_round($amount, Context::getContext()->getComputingPrecision());
                    $total_amount = $unit_amount * $this->product_quantity;

                    break;
                case Order::ROUND_LINE:
                    $unit_amount = $amount;
                    $total_amount = Tools::ps_round($unit_amount * $this->product_quantity, Context::getContext()->getComputingPrecision());

                    break;
                case Order::ROUND_TOTAL:
                    $unit_amount = $amount;
                    $total_amount = $unit_amount * $this->product_quantity;

                    break;
            }

            $values .= '(' . (int) $this->id . ',' . (int) $id_tax . ',' . (float) $unit_amount . ',' . (float) $total_amount . '),';
        }

        if ($replace) {
            Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'order_detail_tax` WHERE id_order_detail=' . (int) $this->id);
        }

        if (!empty($values)) {
            $values = rtrim($values, ',');
            $sql = 'INSERT INTO `' . _DB_PREFIX_ . 'order_detail_tax` (id_order_detail, id_tax, unit_amount, total_amount)
                VALUES ' . $values;

            return Db::getInstance()->execute($sql);
        }

        return true;
    }

    public function updateTaxAmount($order)
    {
        $address = new Address((int) $order->{Configuration::get('PS_TAX_ADDRESS_TYPE')});
        $this->tax_calculator = $this->getTaxCalculatorByAddress($address);

        return $this->saveTaxCalculator($order, true);
    }

    /**
     * Get a TaxCalculator adapted for the OrderDetail's product and the specified address
     *
     * @param Address $address
     *
     * @return TaxCalculator
     */
    public function getTaxCalculatorByAddress(Address $address): TaxCalculator
    {
        $this->setContext((int) $this->id_shop);
        $tax_manager = TaxManagerFactory::getManager($address, $this->getTaxRulesGroupId());

        return $tax_manager->getTaxCalculator();
    }

    /**
     * Dynamically get the taxRulesGroupId instead of relying one the one saved in database
     *
     * @return int
     */
    public function getTaxRulesGroupId(): int
    {
        return (int) Product::getIdTaxRulesGroupByIdProduct((int) $this->product_id, $this->context);
    }

    /**
     * Get a detailed order list of an id_order.
     *
     * @param int $id_order
     *
     * @return array
     */
    public static function getList($id_order)
    {
        return Db::getInstance()->executeS('SELECT * FROM `' . _DB_PREFIX_ . 'order_detail` WHERE `id_order` = ' . (int) $id_order);
    }

    public function getTaxList()
    {
        return self::getTaxListStatic($this->id);
    }

    public static function getTaxListStatic($id_order_detail)
    {
        $sql = 'SELECT * FROM `' . _DB_PREFIX_ . 'order_detail_tax`
                    WHERE `id_order_detail` = ' . (int) $id_order_detail;

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

    /**
     * Set virtual product information
     *
     * @param array $product
     */
    protected function setVirtualProductInformation($product)
    {
        // Add some informations for virtual products
        $this->download_deadline = '0000-00-00 00:00:00';
        $this->download_hash = null;

        if ($id_product_download = ProductDownload::getIdFromIdProduct((int) $product['id_product'])) {
            $product_download = new ProductDownload((int) $id_product_download);
            $this->download_deadline = $product_download->getDeadLine();
            $this->download_hash = $product_download->getHash();

            unset($product_download);
        }
    }

    /**
     * Updates product quantity in stock, according to order status.
     *
     * @param array $product
     * @param int $orderStateId
     */
    protected function updateProductQuantityInStock($product, $orderStateId): void
    {
        $dismissOrderStateIds = Configuration::getMultiple([
            'PS_OS_CANCELED',
            'PS_OS_ERROR',
        ]);
        if (in_array($orderStateId, $dismissOrderStateIds)) {
            return;
        }
        if (!StockAvailable::dependsOnStock($product['id_product'])) {
            $orderState = new OrderState($orderStateId, $this->id_lang);
            $isQuantityUpdated = StockAvailable::updateQuantity(
                $product['id_product'],
                $product['id_product_attribute'],
                -(int) $product['cart_quantity'],
                $product['id_shop'],
                // Add stock movement only if order state is flagged as shipped
                true === (bool) $orderState->shipped,
                [
                    'id_order' => $this->id_order,
                    // Only one stock movement reason fits a new order creation
                    'id_stock_mvt_reason' => Configuration::get('PS_STOCK_CUSTOMER_ORDER_REASON'),
                ]
            );
        } else {
            $isQuantityUpdated = true;
        }
        if ($isQuantityUpdated === true) {
            $product['stock_quantity'] -= $product['cart_quantity'];
        }
        if ($product['stock_quantity'] < 0 && Configuration::get('PS_STOCK_MANAGEMENT')) {
            $this->outOfStock = true;
        }
        Product::updateDefaultAttribute($product['id_product']);
    }

    /**
     * Apply tax to the product.
     *
     * @param Order $order
     * @param array $product
     */
    protected function setProductTax(Order $order, $product)
    {
        $this->ecotax = Tools::convertPrice((float) ($product['ecotax']), (int) ($order->id_currency));

        // Exclude VAT
        if (!Tax::excludeTaxeOption()) {
            $this->setContext((int) $product['id_shop']);
            $this->id_tax_rules_group = (int) Product::getIdTaxRulesGroupByIdProduct((int) $product['id_product'], $this->context);

            $tax_manager = TaxManagerFactory::getManager($this->vat_address, $this->id_tax_rules_group);
            $this->tax_calculator = $tax_manager->getTaxCalculator();
            $this->tax_computation_method = (int) $this->tax_calculator->computation_method;
            $this->tax_rate = (float) $this->tax_calculator->getTotalRate();
            $this->tax_name = $this->tax_calculator->getTaxesName();
        }

        $this->ecotax_tax_rate = 0;
        if (!empty($product['ecotax'])) {
            $this->ecotax_tax_rate = Tax::getProductEcotaxRate($order->{Configuration::get('PS_TAX_ADDRESS_TYPE')});
        }
    }

    /**
     * Set specific price of the product.
     *
     * @param Order $order
     * @param array|null $product
     */
    protected function setSpecificPrice(Order $order, $product = null)
    {
        $this->reduction_amount = 0.00;
        $this->reduction_percent = 0.00;
        $this->reduction_amount_tax_incl = 0.00;
        $this->reduction_amount_tax_excl = 0.00;

        if ($this->specificPrice) {
            switch ($this->specificPrice['reduction_type']) {
                case 'percentage':
                    $this->reduction_percent = (float) $this->specificPrice['reduction'] * 100;

                    break;

                case 'amount':
                    $price = Tools::convertPrice($this->specificPrice['reduction'], $order->id_currency);
                    $this->reduction_amount = !$this->specificPrice['id_currency'] ? (float) $price : (float) $this->specificPrice['reduction'];
                    if ($product !== null) {
                        $this->setContext((int) $product['id_shop']);
                    }
                    $id_tax_rules = (int) Product::getIdTaxRulesGroupByIdProduct((int) $this->specificPrice['id_product'], $this->context);
                    $tax_manager = TaxManagerFactory::getManager($this->vat_address, $id_tax_rules);
                    $this->tax_calculator = $tax_manager->getTaxCalculator();

                    if ($this->specificPrice['reduction_tax']) {
                        $this->reduction_amount_tax_incl = $this->reduction_amount;
                        $this->reduction_amount_tax_excl = Tools::ps_round($this->tax_calculator->removeTaxes($this->reduction_amount), Context::getContext()->getComputingPrecision());
                    } else {
                        $this->reduction_amount_tax_incl = Tools::ps_round($this->tax_calculator->addTaxes($this->reduction_amount), Context::getContext()->getComputingPrecision());
                        $this->reduction_amount_tax_excl = $this->reduction_amount;
                    }

                    break;
            }
        }
    }

    /**
     * Set detailed product price to the order detail.
     *
     * @param Order $order
     * @param Cart $cart
     * @param array<string, int|float> $product
     */
    protected function setDetailProductPrice(Order $order, Cart $cart, $product)
    {
        $this->setContext((int) $product['id_shop']);
        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);
        $this->specificPrice = $specific_price;
        $this->original_product_price = Product::getPriceStatic(
            $product['id_product'],
            false,
            (int) $product['id_product_attribute'],
            6,
            null,
            false,
            false,
            1,
            false,
            null,
            null,
            null,
            $null,
            true,
            true,
            $this->context
        );
        $this->unit_price_tax_incl = (float) $product['price_wt'];
        $this->product_price = $this->unit_price_tax_excl = (float) $product['price'];
        $this->total_price_tax_incl = (float) $product['total_wt'];
        $this->total_price_tax_excl = (float) $product['total'];

        $this->purchase_supplier_price = (float) $product['wholesale_price'];
        if ($product['id_supplier'] > 0 && ($supplier_price = ProductSupplier::getProductPrice((int) $product['id_supplier'], $product['id_product'], $product['id_product_attribute'], true)) > 0) {
            $this->purchase_supplier_price = (float) $supplier_price;
        }

        $this->setSpecificPrice($order, $product);

        $this->group_reduction = (float) Group::getReduction((int) $order->id_customer);

        $shop_id = $this->context->shop->id;

        $quantity_discount = SpecificPrice::getQuantityDiscount(
            (int) $product['id_product'],
            $shop_id,
            (int) $cart->id_currency,
            (int) $this->vat_address->id_country,
            (int) $this->customer->id_default_group,
            (int) $product['cart_quantity'],
            false,
            null
        );

        $unit_price = Product::getPriceStatic(
            (int) $product['id_product'],
            true,
            ($product['id_product_attribute'] ? (int) ($product['id_product_attribute']) : null),
            2,
            null,
            false,
            true,
            1,
            false,
            (int) $order->id_customer,
            null,
            (int) $order->{Configuration::get('PS_TAX_ADDRESS_TYPE')},
            $null,
            true,
            true,
            $this->context
        );
        $this->product_quantity_discount = 0.00;
        if ($quantity_discount) {
            $this->product_quantity_discount = $unit_price;
            if (Product::getTaxCalculationMethod((int) $order->id_customer) == PS_TAX_EXC) {
                $this->product_quantity_discount = Tools::ps_round($unit_price, Context::getContext()->getComputingPrecision());
            }

            if (isset($this->tax_calculator)) {
                $this->product_quantity_discount -= $this->tax_calculator->addTaxes($quantity_discount['price']);
            }
        }

        $this->discount_quantity_applied = (($this->specificPrice && $this->specificPrice['from_quantity'] > 1) ? 1 : 0);
    }

    /**
     * Create an order detail liable to an id_order.
     *
     * @param Order $order
     * @param Cart $cart
     * @param array $product
     * @param int $id_order_state
     * @param int $id_order_invoice
     * @param bool $use_taxes set to false if you don't want to use taxes
     * @param int $id_warehouse
     */
    protected function create(Order $order, Cart $cart, $product, $id_order_state, $id_order_invoice, $use_taxes = true, $id_warehouse = 0)
    {
        if ($use_taxes) {
            $this->tax_calculator = new TaxCalculator();
        }

        $this->id = null;

        $this->product_id = (int) $product['id_product'];
        $this->product_attribute_id = $product['id_product_attribute'] ? (int) $product['id_product_attribute'] : 0;
        $this->id_customization = $product['id_customization'] ? (int) $product['id_customization'] : 0;
        $this->product_name = $product['name'] .
            ((isset($product['attributes']) && $product['attributes'] != null) ?
                ' (' . $product['attributes'] . ')' : '');

        $this->product_quantity = (int) $product['cart_quantity'];
        $this->product_ean13 = empty($product['ean13']) ? null : pSQL($product['ean13']);
        $this->product_isbn = empty($product['isbn']) ? null : pSQL($product['isbn']);
        $this->product_upc = empty($product['upc']) ? null : pSQL($product['upc']);
        $this->product_mpn = empty($product['mpn']) ? null : pSQL($product['mpn']);
        $this->product_reference = empty($product['reference']) ? null : pSQL($product['reference']);
        $this->product_supplier_reference = empty($product['supplier_reference']) ? null : pSQL($product['supplier_reference']);
        $this->product_weight = $product['id_product_attribute'] ? (float) $product['weight_attribute'] : (float) $product['weight'];
        $this->id_warehouse = $id_warehouse;

        // We get the real quantity of the product in stock and save how much of the ordered quantity was in stock
        $product_quantity_in_stock = (int) Product::getQuantity($this->product_id, $this->product_attribute_id);

        // Ordered 3 pcs, in stock 10 pcs, result is 3
        // Ordered 3 pcs, in stock 1 pcs, result is 1
        $this->product_quantity_in_stock = ($product_quantity_in_stock - (int) $product['cart_quantity'] < 0) ?
            $product_quantity_in_stock : (int) $product['cart_quantity'];

        $this->setVirtualProductInformation($product);
        $this->updateProductQuantityInStock($product, $id_order_state);

        if ($use_taxes) {
            $this->setProductTax($order, $product);
        }
        $this->setShippingCost($order, $product);
        $this->setDetailProductPrice($order, $cart, $product);

        // Set order invoice id
        $this->id_order_invoice = (int) $id_order_invoice;

        // Set shop id
        $this->id_shop = (int) $product['id_shop'];

        // Add new entry to the table
        $this->save();

        if ($use_taxes) {
            $this->saveTaxCalculator($order);
        }
        unset($this->tax_calculator);
    }

    /**
     * Create a list of order detail for a specified id_order using cart.
     *
     * @param Order $order
     * @param Cart $cart
     * @param int $id_order_state
     * @param array $product_list
     * @param int $id_order_invoice
     * @param bool $use_taxes set to false if you don't want to use taxes
     * @param int $id_warehouse
     */
    public function createList(Order $order, Cart $cart, $id_order_state, $product_list, $id_order_invoice = 0, $use_taxes = true, $id_warehouse = 0)
    {
        $this->vat_address = new Address((int) $order->{Configuration::get('PS_TAX_ADDRESS_TYPE')});
        $this->customer = new Customer((int) $order->id_customer);

        $this->id_order = $order->id;
        $this->outOfStock = false;

        foreach ($product_list as $product) {
            $this->create($order, $cart, $product, $id_order_state, $id_order_invoice, $use_taxes, $id_warehouse);
        }

        unset(
            $this->vat_address,
            $this->customer
        );
    }

    /**
     * Get the state of the current stock product.
     *
     * @return bool
     */
    public function getStockState()
    {
        return $this->outOfStock;
    }

    /**
     * Set the additional shipping information.
     *
     * @param Order $order
     * @param array $product
     */
    public function setShippingCost(Order $order, $product)
    {
        $tax_rate = 0;

        $carrier = OrderInvoice::getCarrier((int) $this->id_order_invoice);
        if (isset($carrier) && Validate::isLoadedObject($carrier)) {
            $tax_rate = $carrier->getTaxesRate(new Address((int) $order->{Configuration::get('PS_TAX_ADDRESS_TYPE')}));
        }

        $this->total_shipping_price_tax_excl = (float) $product['additional_shipping_cost'];
        $this->total_shipping_price_tax_incl = (float) ($this->total_shipping_price_tax_excl * (1 + ($tax_rate / 100)));
        $this->total_shipping_price_tax_incl = Tools::ps_round($this->total_shipping_price_tax_incl, Context::getContext()->getComputingPrecision());
    }

    public function getWsTaxes()
    {
        $query = new DbQuery();
        $query->select('id_tax as id');
        $query->from('order_detail_tax', 'tax');
        $query->leftJoin('order_detail', 'od', 'tax.`id_order_detail` = od.`id_order_detail`');
        $query->where('od.`id_order_detail` = ' . (int) $this->id_order_detail);

        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query);
    }

    public static function getCrossSells($id_product, $id_lang, $limit = 12)
    {
        if (!$id_product || !$id_lang) {
            return;
        }

        $front = true;
        if (!in_array(Context::getContext()->controller->controller_type, ['front', 'modulefront'])) {
            $front = false;
        }

        $orders = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
        SELECT o.id_order
        FROM ' . _DB_PREFIX_ . 'orders o
        LEFT JOIN ' . _DB_PREFIX_ . 'order_detail od ON (od.id_order = o.id_order)
        WHERE o.valid = 1 AND od.product_id = ' . (int) $id_product);

        if (count($orders)) {
            $list = '';
            foreach ($orders as $order) {
                $list .= (int) $order['id_order'] . ',';
            }
            $list = rtrim($list, ',');

            $order_products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
                SELECT DISTINCT od.product_id, p.id_product, pl.name, pl.link_rewrite, p.reference, i.id_image, product_shop.show_price,
                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' : '') . '
                FROM ' . _DB_PREFIX_ . 'order_detail od
                LEFT JOIN ' . _DB_PREFIX_ . 'product p ON (p.id_product = od.product_id)
                ' . Shop::addSqlAssociation('product', 'p') .
                (Combination::isFeatureActive() ? 'LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` product_attribute_shop
                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 . ')' : '') . '
                LEFT JOIN ' . _DB_PREFIX_ . 'product_lang pl ON (pl.id_product = od.product_id' . Shop::addSqlRestrictionOnLang('pl') . ')
                LEFT JOIN ' . _DB_PREFIX_ . 'category_lang cl ON (cl.id_category = product_shop.id_category_default' . Shop::addSqlRestrictionOnLang('cl') . ')
                LEFT JOIN ' . _DB_PREFIX_ . 'image i ON (i.id_product = od.product_id)
                ' . Shop::addSqlAssociation('image', 'i', true, 'image_shop.cover=1') . '
                WHERE od.id_order IN (' . $list . ')
                    AND pl.id_lang = ' . (int) $id_lang . '
                    AND cl.id_lang = ' . (int) $id_lang . '
                    AND od.product_id != ' . (int) $id_product . '
                    AND product_shop.active = 1'
                    . ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . '
                ORDER BY RAND()
                LIMIT ' . (int) $limit . '
            ', true, false);

            $tax_calc = Product::getTaxCalculationMethod();
            if (is_array($order_products)) {
                foreach ($order_products as &$order_product) {
                    $order_product['image'] = Context::getContext()->link->getImageLink(
                        $order_product['link_rewrite'],
                        (int) $order_product['product_id'] . '-' . (int) $order_product['id_image'],
                        ImageType::getFormattedName('medium')
                    );
                    $order_product['link'] = Context::getContext()->link->getProductLink(
                        (int) $order_product['product_id'],
                        $order_product['link_rewrite'],
                        $order_product['category'],
                        $order_product['ean13']
                    );
                    if ($tax_calc == 0 || $tax_calc == 2) {
                        $order_product['displayed_price'] = Product::getPriceStatic((int) $order_product['product_id'], true, null);
                    } elseif ($tax_calc == 1) {
                        $order_product['displayed_price'] = Product::getPriceStatic((int) $order_product['product_id'], false, null);
                    }
                }

                return Product::getProductsProperties($id_lang, $order_products);
            }
        }
    }

    public function add($autodate = true, $null_values = false)
    {
        foreach ($this->def['fields'] as $field => $data) {
            if (!empty($data['required']) || !empty($data['lang'])) {
                continue;
            }
            if ($this->validateField($field, $this->$field) !== true) {
                $this->$field = '';
            }
        }

        $this->original_wholesale_price = $this->getWholeSalePrice();

        return parent::add($autodate = true, $null_values = false);
    }

    //return the product OR product attribute whole sale price
    public function getWholeSalePrice()
    {
        $product = new Product($this->product_id);
        $wholesale_price = $product->wholesale_price;

        if ($this->product_attribute_id) {
            $combination = new Combination((int) $this->product_attribute_id);
            if ($combination->wholesale_price != '0.000000') {
                $wholesale_price = $combination->wholesale_price;
            }
        }

        return $wholesale_price;
    }
}
