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