Skip to content
Order.php 98.6 KiB
Newer Older
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
 * 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)
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
 * 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
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
 * needs please refer to https://devdocs.prestashop.com/ for more information.
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
 * @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)
use PrestaShop\PrestaShop\Adapter\ServiceLocator;

class OrderCore extends ObjectModel
{
    const ROUND_ITEM = 1;
    const ROUND_LINE = 2;
    const ROUND_TOTAL = 3;
Rémi Gaillard's avatar
Rémi Gaillard committed

    /** @var int Delivery address id */
    public $id_address_delivery;
    /** @var int Invoice address id */
    public $id_address_invoice;
    public $id_shop_group;
    /** @var int Cart id */
    public $id_cart;
    /** @var int Currency id */
    public $id_currency;
    /** @var int Language id */
    public $id_lang;
    /** @var int Customer id */
    public $id_customer;
    // todo: string received instead of int
    /** @var int Carrier id */
    public $id_carrier;
    /** @var int Order Status id */
    public $current_state;
    /** @var string Secure key */
    public $secure_key;
    /** @var string Payment method */
    public $payment;
    /** @var string Payment module */
    public $module;
    /** @var float Currency exchange rate */
    public $conversion_rate;
    /** @var bool Customer is ok for a recyclable package */
    public $recyclable = 1;
    /** @var bool True if the customer wants a gift wrapping */
    public $gift = 0;
    /** @var string Gift message if specified */
    public $gift_message;
    /** @var bool Mobile Theme */
    public $mobile_theme;
    /**
     * @var string Shipping number
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     *
     * @deprecated 1.5.0.4
     * @see OrderCarrier->tracking_number
     */
    public $shipping_number;
    /** @var float Discounts total */
    public $total_discounts;
    public $total_discounts_tax_incl;
    public $total_discounts_tax_excl;
    /** @var float Total to pay */
    public $total_paid;
    /** @var float Total to pay tax included */
    public $total_paid_tax_incl;
    /** @var float Total to pay tax excluded */
    public $total_paid_tax_excl;
    /** @var float Total really paid @deprecated 1.5.0.1 */
    public $total_paid_real;
    /** @var float Products total */
    public $total_products;
    /** @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
     * @var int Round type method used for this order
     */
    public $round_type;

    /**
     * @see ObjectModel::$definition
     */
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
    public static $definition = [
        'table' => 'orders',
        'primary' => 'id_order',
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
        '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'],
        ],
    ];

    protected $webserviceParameters = [
        'objectMethods' => ['add' => 'addWs'],
        'objectNodeName' => 'order',
        'objectsNodeName' => 'orders',
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
        '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
                'xlink_resource' => 'order_states',
                'setter' => 'setWsCurrentState',
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
            ],
            'module' => ['required' => true],
            'invoice_number' => [],
            'invoice_date' => [],
            'delivery_number' => [],
            'delivery_date' => [],
            'valid' => [],
            'date_add' => [],
            'date_upd' => [],
            'shipping_number' => [
                'getter' => 'getWsShippingNumber',
Mickaël Andrieu's avatar
Mickaël Andrieu committed
                'setter' => 'setWsShippingNumber',
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
            ],
        ],
        '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],
                ], ],
        ],
    ];

    protected $_taxCalculationMethod = PS_TAX_EXC;

Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
    protected static $_historyCache = [];

    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');
        if ($this->id_customer && !$is_admin) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            $customer = new Customer((int) $this->id_customer);
            $this->_taxCalculationMethod = Group::getPriceDisplayMethod((int) $customer->id_default_group);
            $this->_taxCalculationMethod = Group::getDefaultPriceDisplayMethod();
    }

    /**
     * @see ObjectModel::getFields()
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     *
     * @return array
     */
    public function getFields()
    {
        if (!$this->id_lang) {
            $this->id_lang = Configuration::get('PS_LANG_DEFAULT', null, null, $this->id_shop);

        return parent::getFields();
    }

    public function add($autodate = true, $null_values = true)
    {
        if (parent::add($autodate, $null_values)) {
            return SpecificPrice::deleteByIdCart($this->id_cart);
        return false;
    }

    public function getTaxCalculationMethod()
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
        return (int) $this->_taxCalculationMethod;
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     * Does NOT delete a product but "cancel" it (which means return/refund/delete it depending of the case).
     *
     * @param $order
     * @param OrderDetail $order_detail
     * @param int $quantity
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     *
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     *
     * @throws PrestaShopException
     */
    public function deleteProduct($order, $order_detail, $quantity)
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
        if (!(int) $this->getCurrentState() || !validate::isLoadedObject($order_detail)) {
        if ($this->hasBeenDelivered()) {
            if (!Configuration::get('PS_ORDER_RETURN', null, null, $this->id_shop)) {
                throw new PrestaShopException('PS_ORDER_RETURN is not defined in table configuration');
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            $order_detail->product_quantity_return += (int) $quantity;

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

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

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

    /**
     * This function return products of the orders
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     * It's similar to Order::getProducts but with similar outputs of Cart::getProducts.
     *
     * @return array
     */
    public function getCartProducts()
    {
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
        $product_id_list = [];
        $products = $this->getProducts();
        foreach ($products as &$product) {
            $product['id_product_attribute'] = $product['product_attribute_id'];
            $product['cart_quantity'] = $product['product_quantity'];
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            $product_id_list[] = $this->id_address_delivery . '_'
                . $product['product_id'] . '_'
                . $product['product_attribute_id'] . '_'
                . (isset($product['id_customization']) ? $product['id_customization'] : '0');
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
        $product_list = [];
        foreach ($products as $product) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            $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');
            if (in_array($key, $product_id_list)) {
                $product_list[] = $product;
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     * DOES delete the product.
     *
     * @param OrderDetail $order_detail
     * @param int $quantity
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     *
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     *
     * @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
        $fields = [
            '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
            'total_paid_real',
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
        ];

        /* Prevent from floating precision issues */
        foreach ($fields as $field) {
            if ($this->{$field} < 0) {
                $this->{$field} = 0;

        /* Prevent from floating precision issues */
        foreach ($fields as $field) {
            $this->{$field} = number_format($this->{$field}, Context::getContext()->getComputingPrecision(), '.', '');

        /* Update order detail */
Mickaël Andrieu's avatar
Mickaël Andrieu committed
        $order_detail->product_quantity -= (int) $quantity;
        if ($order_detail->product_quantity == 0) {
            if (!$order_detail->delete()) {
            }
            if (count($this->getProductsDetail()) == 0) {
                $history = new OrderHistory();
Mickaël Andrieu's avatar
Mickaël Andrieu committed
                $history->id_order = (int) $this->id;
                $history->changeIdOrderState(Configuration::get('PS_OS_CANCELED'), $this);
                if (!$history->addWithemail()) {
            return $this->update();
            $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;
        }
        return $order_detail->update() && $this->update();
    }

    public function deleteCustomization($id_customization, $quantity, $order_detail)
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
        if (!(int) $this->getCurrentState()) {
        if ($this->hasBeenDelivered()) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            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);
        } elseif ($this->hasBeenPaid()) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            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);
Mickaël Andrieu's avatar
Mickaël Andrieu committed
        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)) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
        if (!Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'customization` WHERE `quantity` = 0')) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed

        return $this->_deleteProduct($order_detail, (int) $quantity);
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     * Get order history.
     *
     * @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)
    {
        if (!$id_order_state) {
            $id_order_state = 0;

        $logable = false;
        $delivery = false;
        $paid = false;
        $shipped = false;
        if ($filters > 0) {
            if ($filters & OrderState::FLAG_NO_HIDDEN) {
            }
            if ($filters & OrderState::FLAG_DELIVERY) {
            }
            if ($filters & OrderState::FLAG_LOGABLE) {
            }
            if ($filters & OrderState::FLAG_PAID) {
            }
            if ($filters & OrderState::FLAG_SHIPPED) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
        if (!isset(self::$_historyCache[$this->id . '_' . $id_order_state . '_' . $filters]) || $no_hidden) {
            $id_lang = $id_lang ? (int) $id_lang : 'o.`id_lang`';
            $result = Db::getInstance()->executeS('
            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
            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 : '') . '
            ORDER BY oh.date_add DESC, oh.id_order_history DESC');
            if ($no_hidden) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            self::$_historyCache[$this->id . '_' . $id_order_state . '_' . $filters] = $result;
Mickaël Andrieu's avatar
Mickaël Andrieu committed

        return self::$_historyCache[$this->id . '_' . $id_order_state . '_' . $filters];
    /**
     * 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 = [];
    }

    public function getProductsDetail()
    {
        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
Mickaël Andrieu's avatar
Mickaël Andrieu committed
        FROM `' . _DB_PREFIX_ . 'order_detail` od
        LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON (p.id_product = od.product_id)
        LEFT JOIN `' . _DB_PREFIX_ . 'product_shop` ps ON (ps.id_product = p.id_product AND ps.id_shop = od.id_shop)
        WHERE od.`id_order` = ' . (int) $this->id);
    public function getFirstMessage()
    {
        return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
            SELECT `message`
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            FROM `' . _DB_PREFIX_ . 'message`
            WHERE `id_order` = ' . (int) $this->id . '
            ORDER BY `id_message`
        ');
    }

    /**
     * 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
     * (Order History).
     *
     * @deprecated
     */
    public function setProductPrices(&$row)
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
        $tax_calculator = OrderDetail::getTaxCalculatorStatic((int) $row['id_order_detail']);
        $row['tax_calculator'] = $tax_calculator;
        $row['tax_rate'] = $tax_calculator->getTotalRate();

matks's avatar
matks committed
        $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());
        if ($row['group_reduction'] > 0) {
            $group_reduction = 1 - $row['group_reduction'] / 100;

        $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
     * Get order products.
     * @param bool $products
     * @param bool $selected_products
     * @param bool $selected_qty
     * @param bool $fullInfos
     *
     * @return array Products with price, quantity (with taxe and without)
     */
    public function getProducts($products = false, $selected_products = false, $selected_qty = false, $fullInfos = true)
        if (!$products) {
            $products = $this->getProductsDetail();
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
        $result_array = [];
        foreach ($products as $row) {
            // Change qty if selected
            if ($selected_qty) {
                $row['product_quantity'] = 0;
                foreach ($selected_products as $key => $id_product) {
                    if ($row['id_order_detail'] == $id_product) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
                        $row['product_quantity'] = (int) $selected_qty[$key];
                    }
                }
                if (!$row['product_quantity']) {
            }

            $this->setProductImageInformations($row);
            $this->setProductCurrentStock($row);

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

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

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

                Product::addProductCustomizationPrice($row, $customized_datas);
            }
            /* Stock product */
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            $result_array[(int) $row['id_order_detail']] = $row;
        }

        return $result_array;
    }

    public static function getIdOrderProduct($id_customer, $id_product)
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
        return (int) Db::getInstance()->getValue('
            SELECT o.id_order
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            FROM ' . _DB_PREFIX_ . 'orders o
            LEFT JOIN ' . _DB_PREFIX_ . 'order_detail od
                ON o.id_order = od.id_order
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            WHERE o.id_customer = ' . (int) $id_customer . '
                AND od.product_id = ' . (int) $id_product . '
            ORDER BY o.date_add DESC
        ');
    }

    protected function setProductCustomizedDatas(&$product, $customized_datas)
    {
        $product['customizedDatas'] = null;
        if (isset($customized_datas[$product['product_id']][$product['product_attribute_id']])) {
            $product['customizedDatas'] = $customized_datas[$product['product_id']][$product['product_attribute_id']];
            $product['customizationQuantityTotal'] = 0;
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     * This method allow to add stock information on a product detail.
     *
     * 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
     */
    protected function setProductCurrentStock(&$product)
    {
        if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            && (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);
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            $product['current_stock'] = StockAvailable::getQuantityAvailableByProduct($product['product_id'], $product['product_attribute_id'], (int) $this->id_shop);

        $product['location'] = StockAvailable::getLocation($product['product_id'], $product['product_attribute_id'], (int) $this->id_shop);
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     * This method allow to add image information on a product detail.
     *
     * @param array &$product
     */
    protected function setProductImageInformations(&$product)
    {
        if (isset($product['product_attribute_id']) && $product['product_attribute_id']) {
            $id_image = Db::getInstance()->getValue('
                SELECT `image_shop`.id_image
Mickaël Andrieu's avatar
Mickaël Andrieu committed
                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');
        if (!isset($id_image) || !$id_image) {
            $id_image = Db::getInstance()->getValue(
                'SELECT `image_shop`.id_image
Mickaël Andrieu's avatar
Mickaël Andrieu committed
                FROM `' . _DB_PREFIX_ . 'image` i' .
                Shop::addSqlAssociation('image', 'i', true, 'image_shop.cover=1') . '
                WHERE i.id_product = ' . (int) $product['product_id']

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

        if ($id_image) {
            $product['image'] = new Image($id_image);
    }

    public function getTaxesAverageUsed()
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
        return Cart::getTaxesAverageUsed((int) $this->id_cart);
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     * Count virtual products in order.
     *
     * @return int number of virtual products
     */
    public function getVirtualProducts()
    {
        $sql = '
            SELECT `product_id`, `product_attribute_id`, `download_hash`, `download_deadline`
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            FROM `' . _DB_PREFIX_ . 'order_detail` od
            WHERE od.`id_order` = ' . (int) $this->id . '
                AND `download_hash` <> \'\'';
        return Db::getInstance()->executeS($sql);
    }

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     * 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
     */
    public function isVirtual($strict = true)
    {
        $products = $this->getProducts(false, false, false, false);
        if (count($products) < 1) {
        foreach ($products as $product) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            if ($strict === false && (bool) $product['is_virtual']) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            $virtual &= (bool) $product['is_virtual'];
     * @deprecated 1.5.0.1 use Order::getCartRules() instead
     */
    public function getDiscounts($details = false)
    {
        Tools::displayAsDeprecated('Use Order::getCartRules() instead');
        return Order::getCartRules();
    }

    public function getCartRules()
    {
        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
Mickaël Andrieu's avatar
Mickaël Andrieu committed
        FROM `' . _DB_PREFIX_ . 'order_cart_rule` ocr
        WHERE ocr.`deleted` = 0 AND ocr.`id_order` = ' . (int) $this->id);
    /**
     *  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);
    }

    public static function getDiscountsCustomer($id_customer, $id_cart_rule)
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
        $cache_id = 'Order::getDiscountsCustomer_' . (int) $id_customer . '-' . (int) $id_cart_rule;
        if (!Cache::isStored($cache_id)) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            $result = (int) Db::getInstance()->getValue('
            SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'orders` o
            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);
            Cache::store($cache_id, $result);
        return Cache::retrieve($cache_id);
    }

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     * Get current order status (eg. Awaiting payment, Delivered...).
     *
     * @return int Order status id
     */
    public function getCurrentState()
    {
        return $this->current_state;
    }

    /**
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     * Get current order status name (eg. Awaiting payment, Delivered...).
     *
     * @return array Order status details
     */
    public function getCurrentStateFull($id_lang)
    {
        return Db::getInstance()->getRow('
            SELECT os.`id_order_state`, osl.`name`, os.`logable`, os.`shipped`
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            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);
    }

    public function hasBeenDelivered()
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
        return count($this->getHistory((int) $this->id_lang, false, false, OrderState::FLAG_DELIVERY));
    }

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

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

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

    public function isInPreparation()
    {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
        return count($this->getHistory((int) $this->id_lang, Configuration::get('PS_OS_PREPARATION')));
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     * Checks if the current order status is paid and shipped.
     *
     * @return bool
     */
    public function isPaidAndShipped()
    {
        $order_state = $this->getCurrentOrderState();
        if ($order_state && $order_state->paid && $order_state->shipped) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
     * Get customer orders.
     *
     * @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
     *
     * @return array Customer orders
     */
    public static function getCustomerOrders($id_customer, $show_hidden_status = false, Context $context = null)
    {
        if (!$context) {
            $context = Context::getContext();
        $orderStates = OrderState::getOrderStates((int) $context->language->id, false);
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
        $indexedOrderStates = [];
        foreach ($orderStates as $orderState) {
            $indexedOrderStates[$orderState['id_order_state']] = $orderState;
        }
        $res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
Mickaël Andrieu's avatar
Mickaël Andrieu committed
          (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' : '') .
            ' 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
        FROM `' . _DB_PREFIX_ . 'orders` o
        WHERE o.`id_customer` = ' . (int) $id_customer .
            Shop::addSqlRestriction(Shop::SHARE_ORDER) . '
        GROUP BY o.`id_order`
        ORDER BY o.`date_add` DESC');
Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
            return [];
        foreach ($res as $key => $val) {
            // In case order creation crashed midway some data might be absent
            $orderState = !empty($val['id_order_state']) ? $indexedOrderStates[$val['id_order_state']] : null;
            $res[$key]['order_state'] = $orderState['name'] ?: null;
            $res[$key]['invoice'] = $orderState['invoice'] ?: null;
            $res[$key]['order_state_color'] = $orderState['color'] ?: null;
    public static function getOrdersIdByDate($date_from, $date_to, $id_customer = null, $type = null)
    {
        $sql = 'SELECT `id_order`
Mickaël Andrieu's avatar
Mickaël Andrieu committed
                FROM `' . _DB_PREFIX_ . 'orders`
                WHERE DATE_ADD(date_upd, INTERVAL -1 DAY) <= \'' . pSQL($date_to) . '\' AND date_upd >= \'' . pSQL($date_from) . '\'
                    ' . Shop::addSqlRestriction()
                    . ($type ? ' AND `' . bqSQL($type) . '_number` != 0' : '')
                    . ($id_customer ? ' AND id_customer = ' . (int) $id_customer : '');
        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);

Pierre RAMBAUD's avatar
Pierre RAMBAUD committed
        $orders = [];
        foreach ($result as $order) {
Mickaël Andrieu's avatar
Mickaël Andrieu committed
            $orders[] = (int) $order['id_order'];
        return $orders;
    }

    public static function getOrdersWithInformations($limit = null, Context $context = null)
    {
        if (!$context) {
            $context = Context::getContext();