1<?php 2/** 3 * 2007-2016 PrestaShop 4 * 5 * thirty bees is an extension to the PrestaShop e-commerce software developed by PrestaShop SA 6 * Copyright (C) 2017-2018 thirty bees 7 * 8 * NOTICE OF LICENSE 9 * 10 * This source file is subject to the Open Software License (OSL 3.0) 11 * that is bundled with this package in the file LICENSE.txt. 12 * It is also available through the world-wide-web at this URL: 13 * http://opensource.org/licenses/osl-3.0.php 14 * If you did not receive a copy of the license and are unable to 15 * obtain it through the world-wide-web, please send an email 16 * to license@thirtybees.com so we can send you a copy immediately. 17 * 18 * DISCLAIMER 19 * 20 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer 21 * versions in the future. If you wish to customize PrestaShop for your 22 * needs please refer to https://www.thirtybees.com for more information. 23 * 24 * @author thirty bees <contact@thirtybees.com> 25 * @author PrestaShop SA <contact@prestashop.com> 26 * @copyright 2017-2018 thirty bees 27 * @copyright 2007-2016 PrestaShop SA 28 * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 29 * PrestaShop is an internationally registered trademark & property of PrestaShop SA 30 */ 31 32/** 33 * Class CartCore 34 * 35 * @since 1.0.0 36 */ 37class CartCore extends ObjectModel 38{ 39 // @codingStandardsIgnoreStart 40 const ONLY_PRODUCTS = 1; 41 const ONLY_DISCOUNTS = 2; 42 const BOTH = 3; 43 const BOTH_WITHOUT_SHIPPING = 4; 44 const ONLY_SHIPPING = 5; 45 const ONLY_WRAPPING = 6; 46 const ONLY_PRODUCTS_WITHOUT_SHIPPING = 7; 47 const ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING = 8; 48 /** 49 * @see ObjectModel::$definition 50 */ 51 public static $definition = [ 52 'table' => 'cart', 53 'primary' => 'id_cart', 54 'fields' => [ 55 'id_shop_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 56 'id_shop' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 57 'id_address_delivery' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 58 'id_address_invoice' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 59 'id_carrier' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 60 'id_currency' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true], 61 'id_customer' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 62 'id_guest' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 63 'id_lang' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true], 64 'recyclable' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 65 'gift' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 66 'gift_message' => ['type' => self::TYPE_STRING, 'validate' => 'isMessage'], 67 'mobile_theme' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 68 'delivery_option' => ['type' => self::TYPE_STRING], 69 'secure_key' => ['type' => self::TYPE_STRING, 'size' => 32], 70 'allow_seperated_package' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 71 'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'], 72 'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'], 73 ], 74 ]; 75 /** @var array $_nbProducts */ 76 protected static $_nbProducts = []; 77 /** @var array $_isVirtualCart */ 78 protected static $_isVirtualCart = []; 79 /** @var array $_totalWeight */ 80 protected static $_totalWeight = []; 81 protected static $_carriers = null; 82 protected static $_taxes_rate = null; 83 protected static $_attributesLists = []; 84 /** @var Customer|null */ 85 protected static $_customer = null; 86 public $id_shop_group; 87 public $id_shop; 88 /** @var int Customer delivery address ID */ 89 public $id_address_delivery; 90 /** @var int Customer invoicing address ID */ 91 public $id_address_invoice; 92 /** @var int Customer currency ID */ 93 public $id_currency; 94 /** @var int Customer ID */ 95 public $id_customer; 96 /** @var int Guest ID */ 97 public $id_guest; 98 /** @var int Language ID */ 99 public $id_lang; 100 /** @var bool True if the customer wants a recycled package */ 101 public $recyclable = 0; 102 /** @var bool True if the customer wants a gift wrapping */ 103 public $gift = 0; 104 /** @var string Gift message if specified */ 105 public $gift_message; 106 /** @var bool Mobile Theme */ 107 public $mobile_theme; 108 /** @var string Object creation date */ 109 public $date_add; 110 /** @var string secure_key */ 111 public $secure_key; 112 /** @var int Carrier ID */ 113 public $id_carrier = 0; 114 /** @var string Object last modification date */ 115 public $date_upd; 116 /** @var bool $checkedTos */ 117 public $checkedTos = false; 118 public $pictures; 119 public $textFields; 120 public $delivery_option; 121 /** @var bool Allow to seperate order in multiple package in order to recieve as soon as possible the available products */ 122 public $allow_seperated_package = false; 123 protected $_products = null; 124 protected $_taxCalculationMethod = PS_TAX_EXC; 125 protected $webserviceParameters = [ 126 'fields' => [ 127 'id_address_delivery' => ['xlink_resource' => 'addresses'], 128 'id_address_invoice' => ['xlink_resource' => 'addresses'], 129 'id_currency' => ['xlink_resource' => 'currencies'], 130 'id_customer' => ['xlink_resource' => 'customers'], 131 'id_guest' => ['xlink_resource' => 'guests'], 132 'id_lang' => ['xlink_resource' => 'languages'], 133 ], 134 'associations' => [ 135 'cart_rows' => [ 136 'resource' => 'cart_row', 'virtual_entity' => true, 'fields' => [ 137 'id_product' => ['required' => true, 'xlink_resource' => 'products'], 138 'id_product_attribute' => ['required' => true, 'xlink_resource' => 'combinations'], 139 'id_address_delivery' => ['required' => true, 'xlink_resource' => 'addresses'], 140 'quantity' => ['required' => true], 141 ], 142 ], 143 ], 144 ]; 145 // @codingStandardsIgnoreEnd 146 147 /** 148 * CartCore constructor. 149 * 150 * @param null $id 151 * @param null $idLang 152 * 153 * @since 1.0.0 154 * @version 1.0.0 Initial version 155 * @throws PrestaShopException 156 */ 157 public function __construct($id = null, $idLang = null) 158 { 159 parent::__construct($id); 160 161 if (!is_null($idLang)) { 162 $this->id_lang = (int) (Language::getLanguage($idLang) !== false) ? $idLang : Configuration::get('PS_LANG_DEFAULT'); 163 } 164 165 if ($this->id_customer) { 166 if (isset(Context::getContext()->customer) && Context::getContext()->customer->id == $this->id_customer) { 167 $customer = Context::getContext()->customer; 168 } else { 169 $customer = new Customer((int) $this->id_customer); 170 } 171 172 static::$_customer = $customer; 173 174 if ((!$this->secure_key || $this->secure_key == '-1') && $customer->secure_key) { 175 $this->secure_key = $customer->secure_key; 176 $this->save(); 177 } 178 } 179 180 $this->setTaxCalculationMethod(); 181 } 182 183 /** 184 * @since 1.0.0 185 * @version 1.0.0 Initial version 186 * @throws PrestaShopException 187 */ 188 public function setTaxCalculationMethod() 189 { 190 $this->_taxCalculationMethod = (int) Group::getPriceDisplayMethod( 191 Group::getCurrent()->id 192 ); 193 } 194 195 /** 196 * Get the average tax used in the Cart 197 * 198 * @param int $idCart 199 * 200 * @return float|int 201 * @throws PrestaShopException 202 */ 203 public static function getTaxesAverageUsed($idCart) 204 { 205 $cart = new Cart((int) $idCart); 206 if (!Validate::isLoadedObject($cart)) { 207 die(Tools::displayError()); 208 } 209 210 if (!Configuration::get('PS_TAX')) { 211 return 0; 212 } 213 214 $products = $cart->getProducts(); 215 $totalProductsMoy = 0; 216 $ratioTax = 0; 217 218 if (!count($products)) { 219 return 0; 220 } 221 222 foreach ($products as $product) { 223 // products refer to the cart details 224 225 if (Configuration::get('PS_TAX_ADDRESS_TYPE') == 'id_address_invoice') { 226 $addressId = (int) $cart->id_address_invoice; 227 } else { 228 $addressId = (int) $product['id_address_delivery']; 229 } // Get delivery address of the product from the cart 230 if (!Address::addressExists($addressId)) { 231 $addressId = null; 232 } 233 234 $totalProductsMoy += $product['total_wt']; 235 $ratioTax += $product['total_wt'] * Tax::getProductTaxRate((int) $product['id_product'], (int) $addressId); 236 } 237 238 if ($totalProductsMoy > 0) { 239 return $ratioTax / $totalProductsMoy; 240 } 241 242 return 0; 243 } 244 245 /** 246 * Return cart products 247 * 248 * @param bool $refresh 249 * @param bool $idProduct 250 * @param null $idCountry 251 * 252 * @return array|null 253 * @throws PrestaShopException 254 * @throws PrestaShopException 255 */ 256 public function getProducts($refresh = false, $idProduct = false, $idCountry = null) 257 { 258 if (!$this->id) { 259 return []; 260 } 261 262 // Product cache must be strictly compared to NULL, or else an empty cart will add dozens of queries 263 if ($this->_products !== null && !$refresh) { 264 // Return product row with specified ID if it exists 265 if (is_int($idProduct)) { 266 foreach ($this->_products as $product) { 267 if ($product['id_product'] == $idProduct) { 268 return [$product]; 269 } 270 } 271 272 return []; 273 } 274 275 return $this->_products; 276 } 277 278 // Build query 279 $sql = new DbQuery(); 280 281 // Build SELECT 282 $sql->select('cp.`id_product_attribute`'); 283 $sql->select('cp.`id_product`'); 284 $sql->select('cp.`quantity` AS `cart_quantity`'); 285 $sql->select('cp.`id_shop`'); 286 $sql->select('pl.`name`'); 287 $sql->select('p.`is_virtual`'); 288 $sql->select('pl.`description_short`'); 289 $sql->select('pl.`available_now`'); 290 $sql->select('pl.`available_later`'); 291 $sql->select('product_shop.`id_category_default`'); 292 $sql->select('p.`id_supplier`'); 293 $sql->select('p.`id_manufacturer`'); 294 $sql->select('product_shop.`on_sale`'); 295 $sql->select('product_shop.`ecotax`'); 296 $sql->select('product_shop.`additional_shipping_cost`'); 297 $sql->select('product_shop.`available_for_order`'); 298 $sql->select('product_shop.`price`'); 299 $sql->select('product_shop.`active`'); 300 $sql->select('product_shop.`unity`'); 301 $sql->select('product_shop.`unit_price_ratio`'); 302 $sql->select('stock.`quantity` AS `quantity_available`'); 303 $sql->select('p.`width`'); 304 $sql->select('p.`height`'); 305 $sql->select('p.`depth`'); 306 $sql->select('p.`weight`'); 307 $sql->select('stock.`out_of_stock`'); 308 $sql->select('p.`date_add`'); 309 $sql->select('p.`date_upd`'); 310 $sql->select('IFNULL(stock.`quantity`, 0) AS `quantity`'); 311 $sql->select('pl.`link_rewrite`'); 312 $sql->select('cl.`link_rewrite` AS `category`'); 313 $sql->select('CONCAT(LPAD(cp.`id_product`, 10, 0), LPAD(IFNULL(cp.`id_product_attribute`, 0), 10, 0), IFNULL(cp.`id_address_delivery`, 0)) AS unique_id'); 314 $sql->select('cp.`id_address_delivery`'); 315 $sql->select('product_shop.`advanced_stock_management`'); 316 $sql->select('ps.`product_supplier_reference` AS `supplier_reference`'); 317 318 // Build FROM 319 $sql->from('cart_product', 'cp'); 320 321 // Build JOIN 322 $sql->leftJoin('product', 'p', 'p.`id_product` = cp.`id_product`'); 323 $sql->innerJoin('product_shop', 'product_shop', '(product_shop.`id_shop` = cp.`id_shop` AND product_shop.`id_product` = p.`id_product`)'); 324 $sql->leftJoin( 325 'product_lang', 326 'pl', 327 'p.`id_product` = pl.`id_product` AND pl.`id_lang` = '.(int) $this->id_lang.Shop::addSqlRestrictionOnLang('pl', 'cp.id_shop') 328 ); 329 330 $sql->leftJoin( 331 'category_lang', 332 'cl', 333 'product_shop.`id_category_default` = cl.`id_category` AND cl.`id_lang` = '.(int) $this->id_lang.Shop::addSqlRestrictionOnLang('cl', 'cp.id_shop') 334 ); 335 336 $sql->leftJoin('product_supplier', 'ps', 'ps.`id_product` = cp.`id_product` AND ps.`id_product_attribute` = cp.`id_product_attribute` AND ps.`id_supplier` = p.`id_supplier`'); 337 338 // @todo test if everything is ok, then refactorise call of this method 339 $sql->join(Product::sqlStock('cp', 'cp')); 340 341 // Build WHERE clauses 342 $sql->where('cp.`id_cart` = '.(int) $this->id); 343 if ($idProduct) { 344 $sql->where('cp.`id_product` = '.(int) $idProduct); 345 } 346 $sql->where('p.`id_product` IS NOT NULL'); 347 348 // Build ORDER BY 349 $sql->orderBy('cp.`date_add`, cp.`id_product`, cp.`id_product_attribute` ASC'); 350 351 if (Customization::isFeatureActive()) { 352 $sql->select('cu.`id_customization`, cu.`quantity` AS customization_quantity'); 353 $sql->leftJoin( 354 'customization', 355 'cu', 356 'p.`id_product` = cu.`id_product` AND cp.`id_product_attribute` = cu.`id_product_attribute` AND cu.`id_cart` = '.(int) $this->id 357 ); 358 $sql->groupBy('cp.`id_product_attribute`, cp.`id_product`, cp.`id_shop`'); 359 } else { 360 $sql->select('NULL AS customization_quantity, NULL AS id_customization'); 361 } 362 363 if (Combination::isFeatureActive()) { 364 $sql->select('product_attribute_shop.`price` AS price_attribute, product_attribute_shop.`ecotax` AS ecotax_attr'); 365 $sql->select('IF (IFNULL(pa.`reference`, \'\') = \'\', p.`reference`, pa.`reference`) AS reference'); 366 $sql->select('(p.`weight`+ pa.`weight`) weight_attribute'); 367 $sql->select('IF (IFNULL(pa.`ean13`, \'\') = \'\', p.`ean13`, pa.`ean13`) AS ean13'); 368 $sql->select('IF (IFNULL(pa.`upc`, \'\') = \'\', p.`upc`, pa.`upc`) AS upc'); 369 $sql->select('IFNULL(product_attribute_shop.`minimal_quantity`, product_shop.`minimal_quantity`) as minimal_quantity'); 370 $sql->select('IF(product_attribute_shop.wholesale_price > 0, product_attribute_shop.wholesale_price, product_shop.`wholesale_price`) wholesale_price'); 371 $sql->leftJoin('product_attribute', 'pa', 'pa.`id_product_attribute` = cp.`id_product_attribute`'); 372 $sql->leftJoin('product_attribute_shop', 'product_attribute_shop', '(product_attribute_shop.`id_shop` = cp.`id_shop` AND product_attribute_shop.`id_product_attribute` = pa.`id_product_attribute`)'); 373 } else { 374 $sql->select('p.`reference` AS `reference`'); 375 $sql->select('p.`ean13`'); 376 $sql->select('p.`upc` AS `upc`'); 377 $sql->select('product_shop.`minimal_quantity` AS `minimal_quantity`'); 378 $sql->select('product_shop.`wholesale_price` AS `wholesale_price`'); 379 } 380 381 $sql->select('image_shop.`id_image` id_image, il.`legend`'); 382 $sql->leftJoin('image_shop', 'image_shop', 'image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop='.(int) $this->id_shop); 383 $sql->leftJoin('image_lang', 'il', 'il.`id_image` = image_shop.`id_image` AND il.`id_lang` = '.(int) $this->id_lang); 384 385 $result = Db::getInstance()->executeS($sql); 386 387 // Reset the cache before the following return, or else an empty cart will add dozens of queries 388 $productsIds = []; 389 $paIds = []; 390 if ($result) { 391 foreach ($result as $key => $row) { 392 $productsIds[] = $row['id_product']; 393 $paIds[] = $row['id_product_attribute']; 394 $specificPrice = SpecificPrice::getSpecificPrice($row['id_product'], $this->id_shop, $this->id_currency, $idCountry, $this->id_shop_group, $row['cart_quantity'], $row['id_product_attribute'], $this->id_customer, $this->id); 395 if ($specificPrice) { 396 $reductionTypeRow = ['reduction_type' => $specificPrice['reduction_type']]; 397 } else { 398 $reductionTypeRow = ['reduction_type' => 0]; 399 } 400 401 $result[$key] = array_merge($row, $reductionTypeRow); 402 } 403 } 404 // Thus you can avoid one query per product, because there will be only one query for all the products of the cart 405 Product::cacheProductsFeatures($productsIds); 406 static::cacheSomeAttributesLists($paIds, $this->id_lang); 407 408 $this->_products = []; 409 if (empty($result)) { 410 return []; 411 } 412 413 $ecotaxRate = (float) Tax::getProductEcotaxRate($this->{Configuration::get('PS_TAX_ADDRESS_TYPE')}); 414 $applyEcoTax = Product::$_taxCalculationMethod == PS_TAX_INC && (int) Configuration::get('PS_TAX'); 415 $cartShopContext = Context::getContext()->cloneContext(); 416 417 foreach ($result as &$row) { 418 if (isset($row['ecotax_attr']) && $row['ecotax_attr'] > 0) { 419 $row['ecotax'] = (float) $row['ecotax_attr']; 420 } 421 422 $row['stock_quantity'] = (int) $row['quantity']; 423 $row['quantity'] = (int) $row['cart_quantity']; 424 425 if (isset($row['id_product_attribute']) && (int) $row['id_product_attribute'] && isset($row['weight_attribute'])) { 426 $row['weight'] = (float) $row['weight_attribute']; 427 } 428 429 if (Configuration::get('PS_TAX_ADDRESS_TYPE') == 'id_address_invoice') { 430 $addressId = (int) $this->id_address_invoice; 431 } else { 432 $addressId = (int) $row['id_address_delivery']; 433 } 434 if (!Address::addressExists($addressId)) { 435 $addressId = null; 436 } 437 438 if ($cartShopContext->shop->id != $row['id_shop']) { 439 $cartShopContext->shop = new Shop((int) $row['id_shop']); 440 } 441 442 $address = Address::initialize($addressId, true); 443 $idTaxRulesGroup = Product::getIdTaxRulesGroupByIdProduct((int) $row['id_product'], $cartShopContext); 444 $taxCalculator = TaxManagerFactory::getManager($address, $idTaxRulesGroup)->getTaxCalculator(); 445 446 $row['price_without_reduction'] = Product::getPriceStatic( 447 (int) $row['id_product'], 448 true, 449 isset($row['id_product_attribute']) ? (int) $row['id_product_attribute'] : null, 450 _TB_PRICE_DATABASE_PRECISION_, 451 null, 452 false, 453 false, 454 $row['cart_quantity'], 455 false, 456 (int) $this->id_customer ? (int) $this->id_customer : null, 457 (int) $this->id, 458 $addressId, 459 $specificPriceOutput, 460 true, 461 true, 462 $cartShopContext 463 ); 464 465 $row['price_with_reduction'] = Product::getPriceStatic( 466 (int) $row['id_product'], 467 true, 468 isset($row['id_product_attribute']) ? (int) $row['id_product_attribute'] : null, 469 _TB_PRICE_DATABASE_PRECISION_, 470 null, 471 false, 472 true, 473 $row['cart_quantity'], 474 false, 475 (int) $this->id_customer ? (int) $this->id_customer : null, 476 (int) $this->id, 477 $addressId, 478 $specificPriceOutput, 479 true, 480 true, 481 $cartShopContext 482 ); 483 484 $row['price'] = $row['price_with_reduction_without_tax'] = Product::getPriceStatic( 485 (int) $row['id_product'], 486 false, 487 isset($row['id_product_attribute']) ? (int) $row['id_product_attribute'] : null, 488 _TB_PRICE_DATABASE_PRECISION_, 489 null, 490 false, 491 true, 492 $row['cart_quantity'], 493 false, 494 (int) $this->id_customer ? (int) $this->id_customer : null, 495 (int) $this->id, 496 $addressId, 497 $specificPriceOutput, 498 true, 499 true, 500 $cartShopContext 501 ); 502 503 $row['total'] = $this->roundPrice( 504 $row['price_with_reduction_without_tax'], 505 $row['price_with_reduction'], 506 $row['cart_quantity'], 507 false 508 ); 509 $row['total_wt'] = $this->roundPrice( 510 $row['price_with_reduction_without_tax'], 511 $row['price_with_reduction'], 512 $row['cart_quantity'], 513 true 514 ); 515 516 // Recalculate prices after rounding, these go into an order. 517 $row['price'] = round( 518 $row['total'] / $row['cart_quantity'], 519 _TB_PRICE_DATABASE_PRECISION_ 520 ); 521 $row['price_wt'] = round( 522 $row['total_wt'] / $row['cart_quantity'], 523 _TB_PRICE_DATABASE_PRECISION_ 524 ); 525 526 $row['description_short'] = Tools::nl2br($row['description_short']); 527 528 // check if a image associated with the attribute exists 529 if ($row['id_product_attribute']) { 530 $row2 = Image::getBestImageAttribute($row['id_shop'], $this->id_lang, $row['id_product'], $row['id_product_attribute']); 531 if ($row2) { 532 $row = array_merge($row, $row2); 533 } 534 } 535 536 $row['reduction_applies'] = ($specificPriceOutput && (float) $specificPriceOutput['reduction']); 537 $row['quantity_discount_applies'] = ($specificPriceOutput && $row['cart_quantity'] >= (int) $specificPriceOutput['from_quantity']); 538 $row['id_image'] = Product::defineProductImage($row, $this->id_lang); 539 $row['allow_oosp'] = Product::isAvailableWhenOutOfStock($row['out_of_stock']); 540 $row['features'] = Product::getFeaturesStatic((int) $row['id_product']); 541 542 if (array_key_exists($row['id_product_attribute'].'-'.$this->id_lang, static::$_attributesLists)) { 543 $row = array_merge($row, static::$_attributesLists[$row['id_product_attribute'].'-'.$this->id_lang]); 544 } 545 546 $row = Product::getTaxesInformations($row, $cartShopContext); 547 548 $this->_products[] = $row; 549 } 550 551 return $this->_products; 552 } 553 554 /** 555 * Round a quantity of a price for display. This is non-trivial, because 556 * thirty bees features multiple rounding strategies. 557 * 558 * @param float $priceWithoutTax Single price of the product, without tax. 559 * @param float $priceWithTax Single price of the product, with tax. 560 * @param int $quantity Quantity of the product. 561 * @param bool $withTax Whether the price with or without tax 562 * should get returned. Rounding gets 563 * applied to the displayed price, so 564 * rounding the other variant requires to 565 * recalculate taxes. 566 * 567 * return float Rounded and multiplied price. 568 * 569 * @since 1.1.0 570 */ 571 protected function roundPrice($priceWithoutTax, $priceWithTax, 572 $quantity, $withTax) 573 { 574 static $displayPrecision = false; 575 if ($displayPrecision === false) { 576 $displayPrecision = 0; 577 if (Currency::getCurrencyInstance($this->id_currency)->decimals) { 578 $displayPrecision = 579 Configuration::get('PS_PRICE_DISPLAY_PRECISION'); 580 } 581 } 582 583 $roundType = (int) Configuration::get('PS_ROUND_TYPE'); 584 585 $price = $priceWithoutTax; 586 if ($this->_taxCalculationMethod === PS_TAX_INC) { 587 $price = $priceWithTax; 588 } 589 $price = round($price, _TB_PRICE_DATABASE_PRECISION_); 590 591 switch ($roundType) { 592 case Order::ROUND_ITEM: 593 $price = Tools::ps_round($price, $displayPrecision); 594 // Intentionally fall through. 595 case Order::ROUND_LINE: 596 case Order::ROUND_TOTAL: 597 $total = $price * (int) $quantity; 598 } 599 600 // Add/remove taxes as appropriate. Ignore the obvious calculation 601 // precision limitation, please, it should be negligible. 602 if ($priceWithTax 603 && $this->_taxCalculationMethod === PS_TAX_INC 604 && ! $withTax) { 605 // Remove taxes. 606 $total = round( 607 $total / $priceWithTax * $priceWithoutTax, 608 _TB_PRICE_DATABASE_PRECISION_ 609 ); 610 } elseif ($priceWithoutTax 611 && $this->_taxCalculationMethod === PS_TAX_EXC 612 && $withTax) { 613 // Add taxes. 614 $total = round( 615 $total * $priceWithTax / $priceWithoutTax, 616 _TB_PRICE_DATABASE_PRECISION_ 617 ); 618 } // else nothing to change. 619 620 if ($roundType === Order::ROUND_LINE) { 621 $total = Tools::ps_round($total, $displayPrecision); 622 } 623 624 return $total; 625 } 626 627 /** 628 * @param array $ipaList 629 * @param int $idLang 630 * 631 * @throws PrestaShopDatabaseException 632 * @throws PrestaShopException 633 * @since 1.0.0 634 * @version 1.0.0 Initial version 635 */ 636 public static function cacheSomeAttributesLists($ipaList, $idLang) 637 { 638 if (!Combination::isFeatureActive()) { 639 return; 640 } 641 642 $paImplode = []; 643 644 foreach ($ipaList as $idProductAttribute) { 645 if ((int) $idProductAttribute && !array_key_exists($idProductAttribute.'-'.$idLang, static::$_attributesLists)) { 646 $paImplode[] = (int) $idProductAttribute; 647 static::$_attributesLists[(int) $idProductAttribute.'-'.$idLang] = ['attributes' => '', 'attributes_small' => '']; 648 } 649 } 650 651 if (!count($paImplode)) { 652 return; 653 } 654 655 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 656 (new DbQuery()) 657 ->select('pac.`id_product_attribute`, agl.`public_name` AS `public_group_name`, al.`name` AS `attribute_name`') 658 ->from('product_attribute_combination', 'pac') 659 ->leftJoin('attribute', 'a', 'a.`id_attribute` = pac.`id_attribute`') 660 ->leftJoin('attribute_group', 'ag', 'ag.`id_attribute_group` = a.`id_attribute_group`') 661 ->leftJoin('attribute_lang', 'al', 'a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = '.(int) $idLang) 662 ->leftJoin('attribute_group_lang', 'agl', 'ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = '.(int) $idLang) 663 ->where('pac.`id_product_attribute` IN ('.implode(',', $paImplode).')') 664 ->orderBy('ag.`position` ASC, a.`position` ASC') 665 ); 666 667 foreach ($result as $row) { 668 static::$_attributesLists[$row['id_product_attribute'].'-'.$idLang]['attributes'] .= $row['public_group_name'].' : '.$row['attribute_name'].', '; 669 static::$_attributesLists[$row['id_product_attribute'].'-'.$idLang]['attributes_small'] .= $row['attribute_name'].', '; 670 } 671 672 foreach ($paImplode as $idProductAttribute) { 673 static::$_attributesLists[$idProductAttribute.'-'.$idLang]['attributes'] = rtrim( 674 static::$_attributesLists[$idProductAttribute.'-'.$idLang]['attributes'], 675 ', ' 676 ); 677 678 static::$_attributesLists[$idProductAttribute.'-'.$idLang]['attributes_small'] = rtrim( 679 static::$_attributesLists[$idProductAttribute.'-'.$idLang]['attributes_small'], 680 ', ' 681 ); 682 } 683 } 684 685 /** 686 * @param int $idCart 687 * 688 * @return string 689 * 690 * @since 1.0.0 691 * @version 1.0.0 Initial version 692 */ 693 public static function getOrderTotalUsingTaxCalculationMethod($idCart) 694 { 695 return static::getTotalCart($idCart, true); 696 } 697 698 /** 699 * @param int $idCart 700 * @param bool $useTaxDisplay 701 * @param int $type 702 * 703 * @return string 704 * 705 * @since 1.0.0 706 * @version 1.0.0 Initial version 707 */ 708 public static function getTotalCart($idCart, $useTaxDisplay = false, $type = self::BOTH) 709 { 710 $cart = new Cart($idCart); 711 if (!Validate::isLoadedObject($cart)) { 712 die(Tools::displayError()); 713 } 714 715 $withTaxes = $useTaxDisplay ? $cart->_taxCalculationMethod !== PS_TAX_EXC : true; 716 717 return Tools::displayPrice($cart->getOrderTotal($withTaxes, $type), Currency::getCurrencyInstance((int) $cart->id_currency), false); 718 } 719 720 /** 721 * This function returns the total cart amount 722 * 723 * Possible values for $type: 724 * static::ONLY_PRODUCTS 725 * static::ONLY_DISCOUNTS 726 * static::BOTH 727 * static::BOTH_WITHOUT_SHIPPING 728 * static::ONLY_SHIPPING 729 * static::ONLY_WRAPPING 730 * static::ONLY_PRODUCTS_WITHOUT_SHIPPING 731 * static::ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING 732 * 733 * @param bool $withTaxes With or without taxes 734 * @param int $type Total type 735 * @param array|null $products 736 * @param int|null $idCarrier 737 * @param bool $useCache Allow using cache of the method CartRule::getContextualValue 738 * 739 * @return float Order total 740 * 741 * @throws Adapter_Exception 742 * @throws PrestaShopDatabaseException 743 * @throws PrestaShopException 744 * @since 1.0.0 745 * @version 1.0.0 Initial version 746 */ 747 public function getOrderTotal($withTaxes = true, $type = self::BOTH, $products = null, $idCarrier = null, $useCache = true) 748 { 749 static $displayPrecision = false; 750 if ($displayPrecision === false) { 751 $displayPrecision = 0; 752 if (Currency::getCurrencyInstance($this->id_currency)->decimals) { 753 $displayPrecision = 754 Configuration::get('PS_PRICE_DISPLAY_PRECISION'); 755 } 756 } 757 758 // Dependencies 759 /** @var Adapter_AddressFactory $addressFactory */ 760 $addressFactory = Adapter_ServiceLocator::get('Adapter_AddressFactory'); 761 /** @var Adapter_ProductPriceCalculator $priceCalculator */ 762 $priceCalculator = Adapter_ServiceLocator::get('Adapter_ProductPriceCalculator'); 763 /** @var Core_Business_ConfigurationInterface $configuration */ 764 $configuration = Adapter_ServiceLocator::get('Core_Business_ConfigurationInterface'); 765 766 $psTaxAddressType = $configuration->get('PS_TAX_ADDRESS_TYPE'); 767 $psUseEcotax = $configuration->get('PS_USE_ECOTAX'); 768 769 if (!$this->id) { 770 return 0; 771 } 772 773 $type = (int) $type; 774 $arrayType = [ 775 static::ONLY_PRODUCTS, 776 static::ONLY_DISCOUNTS, 777 static::BOTH, 778 static::BOTH_WITHOUT_SHIPPING, 779 static::ONLY_SHIPPING, 780 static::ONLY_WRAPPING, 781 static::ONLY_PRODUCTS_WITHOUT_SHIPPING, 782 static::ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING, 783 ]; 784 785 // Define virtual context to prevent case where the cart is not the in the global context 786 $virtualContext = Context::getContext()->cloneContext(); 787 $virtualContext->cart = $this; 788 789 if (!in_array($type, $arrayType)) { 790 die(Tools::displayError()); 791 } 792 793 $withShipping = in_array($type, [static::BOTH, static::ONLY_SHIPPING]); 794 795 // if cart rules are not used 796 if ($type == static::ONLY_DISCOUNTS && !CartRule::isFeatureActive()) { 797 return 0; 798 } 799 800 // no shipping cost if is a cart with only virtuals products 801 $virtual = $this->isVirtualCart(); 802 if ($virtual && $type == static::ONLY_SHIPPING) { 803 return 0; 804 } 805 806 if ($virtual && $type == static::BOTH) { 807 $type = static::BOTH_WITHOUT_SHIPPING; 808 } 809 810 if ($withShipping || $type == static::ONLY_DISCOUNTS) { 811 if (is_null($products) && is_null($idCarrier)) { 812 $shippingFees = $this->getTotalShippingCost(null, (bool) $withTaxes); 813 } else { 814 $shippingFees = $this->getPackageShippingCost((int) $idCarrier, (bool) $withTaxes, null, $products); 815 } 816 } else { 817 $shippingFees = 0; 818 } 819 820 if ($type == static::ONLY_SHIPPING) { 821 return $shippingFees; 822 } 823 824 if ($type == static::ONLY_PRODUCTS_WITHOUT_SHIPPING) { 825 $type = static::ONLY_PRODUCTS; 826 } 827 828 $paramProduct = true; 829 if (is_null($products)) { 830 $paramProduct = false; 831 $products = $this->getProducts(); 832 } 833 834 if ($type == static::ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING) { 835 foreach ($products as $key => $product) { 836 if ($product['is_virtual']) { 837 unset($products[$key]); 838 } 839 } 840 $type = static::ONLY_PRODUCTS; 841 } 842 843 $orderTotal = 0; 844 if (Tax::excludeTaxeOption()) { 845 $withTaxes = false; 846 } 847 848 $productsTotal = []; 849 850 foreach ($products as $product) { 851 // products refer to the cart details 852 if ($virtualContext->shop->id != $product['id_shop']) { 853 $virtualContext->shop = new Shop((int) $product['id_shop']); 854 } 855 856 if ($psTaxAddressType == 'id_address_invoice') { 857 $idAddress = (int) $this->id_address_invoice; 858 } else { 859 $idAddress = (int) $product['id_address_delivery']; 860 } // Get delivery address of the product from the cart 861 if (!$addressFactory->addressExists($idAddress)) { 862 $idAddress = null; 863 } 864 865 // The $null variable below is not used, 866 // but it is necessary to pass it to getProductPrice because 867 // it expects a reference. 868 $null = null; 869 $priceWithoutTax = $priceCalculator->getProductPrice( 870 (int) $product['id_product'], 871 false, 872 (int) $product['id_product_attribute'], 873 _TB_PRICE_DATABASE_PRECISION_, 874 null, 875 false, 876 true, 877 $product['cart_quantity'], 878 false, 879 (int) $this->id_customer ? (int) $this->id_customer : null, 880 (int) $this->id, 881 $idAddress, 882 $null, 883 $psUseEcotax, 884 true, 885 $virtualContext 886 ); 887 $priceWithTax = $priceCalculator->getProductPrice( 888 (int) $product['id_product'], 889 true, 890 (int) $product['id_product_attribute'], 891 _TB_PRICE_DATABASE_PRECISION_, 892 null, 893 false, 894 true, 895 $product['cart_quantity'], 896 false, 897 (int) $this->id_customer ? (int) $this->id_customer : null, 898 (int) $this->id, 899 $idAddress, 900 $null, 901 $psUseEcotax, 902 true, 903 $virtualContext 904 ); 905 906 if ($withTaxes) { 907 $idTaxRulesGroup = Product::getIdTaxRulesGroupByIdProduct((int) $product['id_product'], $virtualContext); 908 } else { 909 $idTaxRulesGroup = 0; 910 } 911 912 $index = $idTaxRulesGroup; 913 if (Configuration::get('PS_ROUND_TYPE') == Order::ROUND_TOTAL) { 914 $index = $idTaxRulesGroup.'_'.$idAddress; 915 } 916 if ( ! isset($productsTotal[$index])) { 917 $productsTotal[$index] = 0; 918 } 919 920 $productsTotal[$index] += $this->roundPrice( 921 $priceWithoutTax, 922 $priceWithTax, 923 $product['cart_quantity'], 924 $withTaxes 925 ); 926 } 927 928 foreach ($productsTotal as $key => $price) { 929 $orderTotal += $price; 930 } 931 932 $orderTotalProducts = $orderTotal; 933 934 if ($type == static::ONLY_DISCOUNTS) { 935 $orderTotal = 0; 936 } 937 938 // Wrapping Fees 939 $wrappingFees = 0; 940 941 // With PS_ATCP_SHIPWRAP on the gift wrapping cost computation calls getOrderTotal with $type === static::ONLY_PRODUCTS, so the flag below prevents an infinite recursion. 942 $includeGiftWrapping = (!$configuration->get('PS_ATCP_SHIPWRAP') || $type !== static::ONLY_PRODUCTS); 943 944 if ($this->gift && $includeGiftWrapping) { 945 $wrappingFees = Tools::ps_round( 946 Tools::convertPrice( 947 $this->getGiftWrappingPrice($withTaxes), 948 Currency::getCurrencyInstance((int) $this->id_currency) 949 ), 950 $displayPrecision 951 ); 952 } 953 if ($type == static::ONLY_WRAPPING) { 954 return $wrappingFees; 955 } 956 957 $orderTotalDiscount = 0; 958 $orderShippingDiscount = 0; 959 if (!in_array($type, [static::ONLY_SHIPPING, static::ONLY_PRODUCTS]) && CartRule::isFeatureActive()) { 960 // First, retrieve the cart rules associated to this "getOrderTotal" 961 if ($withShipping || $type == static::ONLY_DISCOUNTS) { 962 $cartRules = $this->getCartRules(CartRule::FILTER_ACTION_ALL); 963 } else { 964 $cartRules = $this->getCartRules(CartRule::FILTER_ACTION_REDUCTION); 965 // Cart Rules array are merged manually in order to avoid doubles 966 foreach ($this->getCartRules(CartRule::FILTER_ACTION_GIFT) as $tmpCartRule) { 967 $flag = false; 968 foreach ($cartRules as $cartRule) { 969 if ($tmpCartRule['id_cart_rule'] == $cartRule['id_cart_rule']) { 970 $flag = true; 971 } 972 } 973 if (!$flag) { 974 $cartRules[] = $tmpCartRule; 975 } 976 } 977 } 978 979 $idAddressDelivery = 0; 980 if (isset($products[0])) { 981 $idAddressDelivery = (is_null($products) ? $this->id_address_delivery : $products[0]['id_address_delivery']); 982 } 983 $package = ['id_carrier' => $idCarrier, 'id_address' => $idAddressDelivery, 'products' => $products]; 984 985 // Then, calculate the contextual value for each one 986 $flag = false; 987 foreach ($cartRules as $cartRule) { 988 /** @var CartRule $cartRuleObject */ 989 $cartRuleObject = $cartRule['obj']; 990 // If the cart rule offers free shipping, add the shipping cost 991 if (($withShipping || $type == static::ONLY_DISCOUNTS) && $cartRuleObject->free_shipping && !$flag) { 992 $orderShippingDiscount = (float) $cartRuleObject->getContextualValue($withTaxes, $virtualContext, CartRule::FILTER_ACTION_SHIPPING, ($paramProduct ? $package : null), $useCache); 993 $flag = true; 994 } 995 996 // If the cart rule is a free gift, then add the free gift value only if the gift is in this package 997 if ((int) $cartRuleObject->gift_product) { 998 $inOrder = false; 999 if (is_null($products)) { 1000 $inOrder = true; 1001 } else { 1002 foreach ($products as $product) { 1003 if ($cartRuleObject->gift_product == $product['id_product'] && $cartRuleObject->gift_product_attribute == $product['id_product_attribute']) { 1004 $inOrder = true; 1005 } 1006 } 1007 } 1008 1009 if ($inOrder) { 1010 $orderTotalDiscount += $cartRuleObject->getContextualValue($withTaxes, $virtualContext, CartRule::FILTER_ACTION_GIFT, $package, $useCache); 1011 } 1012 } 1013 1014 // If the cart rule offers a reduction, the amount is prorated (with the products in the package) 1015 if ($cartRuleObject->reduction_percent > 0 || $cartRuleObject->reduction_amount > 0) { 1016 $orderTotalDiscount += $cartRuleObject->getContextualValue($withTaxes, $virtualContext, CartRule::FILTER_ACTION_REDUCTION, $package, $useCache); 1017 } 1018 } 1019 $orderTotalDiscount = min($orderTotalDiscount, (float) $orderTotalProducts) + (float) $orderShippingDiscount; 1020 $orderTotal -= $orderTotalDiscount; 1021 } 1022 1023 if ($type == static::BOTH) { 1024 $orderTotal += $shippingFees + $wrappingFees; 1025 } 1026 1027 if ($orderTotal < 0 && $type != static::ONLY_DISCOUNTS) { 1028 return 0; 1029 } 1030 1031 if ($type == static::ONLY_DISCOUNTS) { 1032 return $orderTotalDiscount; 1033 } 1034 1035 return Tools::ps_round((float) $orderTotal, $displayPrecision); 1036 } 1037 1038 /** 1039 * Check if cart contains only virtual products 1040 * 1041 * @return bool true if is a virtual cart or false 1042 * 1043 * @since 1.0.0 1044 * @version 1.0.0 Initial version 1045 * @throws PrestaShopException 1046 */ 1047 public function isVirtualCart() 1048 { 1049 if (!ProductDownload::isFeatureActive()) { 1050 return false; 1051 } 1052 1053 if (!isset(static::$_isVirtualCart[$this->id])) { 1054 $products = $this->getProducts(); 1055 if (!count($products)) { 1056 return false; 1057 } 1058 1059 $isVirtual = 1; 1060 foreach ($products as $product) { 1061 if (empty($product['is_virtual'])) { 1062 $isVirtual = 0; 1063 } 1064 } 1065 static::$_isVirtualCart[$this->id] = (int) $isVirtual; 1066 } 1067 1068 return static::$_isVirtualCart[$this->id]; 1069 } 1070 1071 /** 1072 * Return shipping total for the cart 1073 * 1074 * @param array|null $deliveryOption Array of the delivery option for each address 1075 * @param bool $useTax 1076 * @param Country|null $defaultCountry 1077 * 1078 * @return float Shipping total 1079 * 1080 * @throws Adapter_Exception 1081 * @throws PrestaShopDatabaseException 1082 * @throws PrestaShopException 1083 * @since 1.0.0 1084 * @version 1.0.0 Initial version 1085 */ 1086 public function getTotalShippingCost($deliveryOption = null, $useTax = true, Country $defaultCountry = null) 1087 { 1088 if (isset(Context::getContext()->cookie->id_country)) { 1089 $defaultCountry = new Country(Context::getContext()->cookie->id_country); 1090 } 1091 if (is_null($deliveryOption)) { 1092 $deliveryOption = $this->getDeliveryOption($defaultCountry, false, false); 1093 } 1094 1095 $totalShipping = 0; 1096 $deliveryOptionList = $this->getDeliveryOptionList($defaultCountry); 1097 foreach ($deliveryOption as $idAddress => $key) { 1098 if (!isset($deliveryOptionList[$idAddress]) || !isset($deliveryOptionList[$idAddress][$key])) { 1099 continue; 1100 } 1101 if ($useTax) { 1102 $totalShipping += $deliveryOptionList[$idAddress][$key]['total_price_with_tax']; 1103 } else { 1104 $totalShipping += $deliveryOptionList[$idAddress][$key]['total_price_without_tax']; 1105 } 1106 } 1107 1108 return $totalShipping; 1109 } 1110 1111 /** 1112 * Get the delivery option selected, or if no delivery option was selected, 1113 * the cheapest option for each address 1114 * 1115 * @param Country|null $defaultCountry 1116 * @param bool $dontAutoSelectOptions 1117 * @param bool $useCache 1118 * 1119 * @return array|bool|mixed Delivery option 1120 * 1121 * @throws PrestaShopDatabaseException 1122 * @throws PrestaShopException 1123 * @since 1.0.0 1124 * @version 1.0.0 Initial version 1125 * @throws Adapter_Exception 1126 */ 1127 public function getDeliveryOption($defaultCountry = null, $dontAutoSelectOptions = false, $useCache = true) 1128 { 1129 static $cache = []; 1130 $cacheId = (int) (is_object($defaultCountry) ? $defaultCountry->id : 0).'-'.(int) $dontAutoSelectOptions; 1131 if (isset($cache[$cacheId]) && $useCache) { 1132 return $cache[$cacheId]; 1133 } 1134 1135 $deliveryOptionList = $this->getDeliveryOptionList($defaultCountry); 1136 1137 // The delivery option was selected 1138 if (isset($this->delivery_option) && $this->delivery_option != '') { 1139 $deliveryOption = json_decode($this->delivery_option, true); 1140 $validated = true; 1141 if (is_array($deliveryOption)) { 1142 foreach ($deliveryOption as $idAddress => $key) { 1143 if (!isset($deliveryOptionList[$idAddress][$key])) { 1144 $validated = false; 1145 break; 1146 } 1147 } 1148 1149 if ($validated) { 1150 $cache[$cacheId] = $deliveryOption; 1151 1152 return $deliveryOption; 1153 } 1154 } 1155 } 1156 1157 if ($dontAutoSelectOptions) { 1158 return false; 1159 } 1160 1161 // No delivery option selected or delivery option selected is not valid, get the better for all options 1162 $deliveryOption = []; 1163 foreach ($deliveryOptionList as $idAddress => $options) { 1164 foreach ($options as $key => $option) { 1165 if (Configuration::get('PS_CARRIER_DEFAULT') == -1 && $option['is_best_price']) { 1166 $deliveryOption[$idAddress] = $key; 1167 break; 1168 } elseif (Configuration::get('PS_CARRIER_DEFAULT') == -2 && $option['is_best_grade']) { 1169 $deliveryOption[$idAddress] = $key; 1170 break; 1171 } elseif ($option['unique_carrier'] && in_array(Configuration::get('PS_CARRIER_DEFAULT'), array_keys($option['carrier_list']))) { 1172 $deliveryOption[$idAddress] = $key; 1173 break; 1174 } 1175 } 1176 1177 reset($options); 1178 if (!isset($deliveryOption[$idAddress])) { 1179 $deliveryOption[$idAddress] = key($options); 1180 } 1181 } 1182 1183 $cache[$cacheId] = $deliveryOption; 1184 1185 return $deliveryOption; 1186 } 1187 1188 /** 1189 * Set the delivery option and id_carrier, if there is only one carrier 1190 * 1191 * @param array ?null $deliveryOption 1192 * 1193 * @throws Adapter_Exception 1194 * @throws PrestaShopDatabaseException 1195 * @throws PrestaShopException 1196 * @since 1.0.0 1197 * @version 1.0.0 Initial version 1198 */ 1199 public function setDeliveryOption($deliveryOption = null) 1200 { 1201 if (empty($deliveryOption) || count($deliveryOption) == 0) { 1202 $this->delivery_option = ''; 1203 $this->id_carrier = 0; 1204 1205 return; 1206 } 1207 Cache::clean('getContextualValue_*'); 1208 $deliveryOptionList = $this->getDeliveryOptionList(null, true); 1209 1210 foreach ($deliveryOptionList as $idAddress => $options) { 1211 if (!isset($deliveryOption[$idAddress])) { 1212 foreach ($options as $key => $option) { 1213 if ($option['is_best_price']) { 1214 $deliveryOption[$idAddress] = $key; 1215 break; 1216 } 1217 } 1218 } 1219 } 1220 1221 if (count($deliveryOption) == 1) { 1222 $this->id_carrier = $this->getIdCarrierFromDeliveryOption($deliveryOption); 1223 } 1224 1225 $this->delivery_option = json_encode($deliveryOption); 1226 } 1227 1228 /** 1229 * Get all deliveries options available for the current cart 1230 * 1231 * @param Country $defaultCountry 1232 * @param bool $flush Force flushing cache 1233 * 1234 * @return array array( 1235 * 0 => array( // First address 1236 * '12,' => array( // First delivery option available for this address 1237 * carrier_list => array( 1238 * 12 => array( // First carrier for this option 1239 * 'instance' => Carrier Object, 1240 * 'logo' => <url to the carriers logo>, 1241 * 'price_with_tax' => 12.4, 1242 * 'price_without_tax' => 12.4, 1243 * 'package_list' => array( 1244 * 1, 1245 * 3, 1246 * ), 1247 * ), 1248 * ), 1249 * is_best_grade => true, // Does this option have the biggest grade (quick shipping) for this shipping address 1250 * is_best_price => true, // Does this option have the lower price for this shipping address 1251 * unique_carrier => true, // Does this option use a unique carrier 1252 * total_price_with_tax => 12.5, 1253 * total_price_without_tax => 12.5, 1254 * position => 5, // Average of the carrier position 1255 * ), 1256 * ), 1257 * ); 1258 * If there are no carriers available for an address, return an empty array 1259 * @throws Adapter_Exception 1260 * @throws PrestaShopDatabaseException 1261 * @throws PrestaShopException 1262 */ 1263 public function getDeliveryOptionList(Country $defaultCountry = null, $flush = false) 1264 { 1265 static $cache = []; 1266 if (isset($cache[$this->id]) && !$flush) { 1267 return $cache[$this->id]; 1268 } 1269 1270 $deliveryOptionList = []; 1271 $carriersPrice = []; 1272 $carrierCollection = []; 1273 $packageList = $this->getPackageList($flush); 1274 1275 // Foreach addresses 1276 foreach ($packageList as $idAddress => $packages) { 1277 // Initialize vars 1278 $deliveryOptionList[$idAddress] = []; 1279 $carriersPrice[$idAddress] = []; 1280 $commonCarriers = null; 1281 $bestPriceCarriers = []; 1282 $bestGradeCarriers = []; 1283 $carriersInstance = []; 1284 1285 // Get country 1286 if ($idAddress) { 1287 $address = new Address($idAddress); 1288 $country = new Country($address->id_country); 1289 } else { 1290 $country = $defaultCountry; 1291 } 1292 1293 // Foreach packages, get the carriers with best price, best position and best grade 1294 foreach ($packages as $idPackage => $package) { 1295 // No carriers available 1296 if (count($packages) == 1 && count($package['carrier_list']) == 1 && current($package['carrier_list']) == 0) { 1297 $cache[$this->id] = []; 1298 1299 return $cache[$this->id]; 1300 } 1301 1302 $carriersPrice[$idAddress][$idPackage] = []; 1303 1304 // Get all common carriers for each packages to the same address 1305 if (is_null($commonCarriers)) { 1306 $commonCarriers = $package['carrier_list']; 1307 } else { 1308 $commonCarriers = array_intersect($commonCarriers, $package['carrier_list']); 1309 } 1310 1311 $bestPrice = null; 1312 $bestPriceCarrier = null; 1313 $bestGrade = null; 1314 $bestGradeCarrier = null; 1315 1316 // Foreach carriers of the package, calculate his price, check if it the best price, position and grade 1317 foreach ($package['carrier_list'] as $idCarrier) { 1318 if (!isset($carriersInstance[$idCarrier])) { 1319 $carriersInstance[$idCarrier] = new Carrier($idCarrier); 1320 } 1321 1322 $priceWithTax = $this->getPackageShippingCost((int) $idCarrier, true, $country, $package['product_list']); 1323 $priceWithoutTax = $this->getPackageShippingCost((int) $idCarrier, false, $country, $package['product_list']); 1324 if (is_null($bestPrice) || $priceWithTax < $bestPrice) { 1325 $bestPrice = $priceWithTax; 1326 $bestPriceCarrier = $idCarrier; 1327 } 1328 $carriersPrice[$idAddress][$idPackage][$idCarrier] = [ 1329 'without_tax' => $priceWithoutTax, 1330 'with_tax' => $priceWithTax, 1331 ]; 1332 1333 $grade = $carriersInstance[$idCarrier]->grade; 1334 if (is_null($bestGrade) || $grade > $bestGrade) { 1335 $bestGrade = $grade; 1336 $bestGradeCarrier = $idCarrier; 1337 } 1338 } 1339 1340 $bestPriceCarriers[$idPackage] = $bestPriceCarrier; 1341 $bestGradeCarriers[$idPackage] = $bestGradeCarrier; 1342 } 1343 1344 // Reset $best_price_carrier, it's now an array 1345 $bestPriceCarrier = []; 1346 $key = ''; 1347 1348 // Get the delivery option with the lower price 1349 foreach ($bestPriceCarriers as $idPackage => $idCarrier) { 1350 $key .= $idCarrier.','; 1351 if (!isset($bestPriceCarrier[$idCarrier])) { 1352 $bestPriceCarrier[$idCarrier] = [ 1353 'price_with_tax' => 0, 1354 'price_without_tax' => 0, 1355 'package_list' => [], 1356 'product_list' => [], 1357 ]; 1358 } 1359 $bestPriceCarrier[$idCarrier]['price_with_tax'] += $carriersPrice[$idAddress][$idPackage][$idCarrier]['with_tax']; 1360 $bestPriceCarrier[$idCarrier]['price_without_tax'] += $carriersPrice[$idAddress][$idPackage][$idCarrier]['without_tax']; 1361 $bestPriceCarrier[$idCarrier]['package_list'][] = $idPackage; 1362 $bestPriceCarrier[$idCarrier]['product_list'] = array_merge($bestPriceCarrier[$idCarrier]['product_list'], $packages[$idPackage]['product_list']); 1363 $bestPriceCarrier[$idCarrier]['instance'] = $carriersInstance[$idCarrier]; 1364 $realBestPrice = !isset($realBestPrice) || $realBestPrice > $carriersPrice[$idAddress][$idPackage][$idCarrier]['with_tax'] ? 1365 $carriersPrice[$idAddress][$idPackage][$idCarrier]['with_tax'] : $realBestPrice; 1366 $realBestPriceWt = !isset($realBestPriceWt) || $realBestPriceWt > $carriersPrice[$idAddress][$idPackage][$idCarrier]['without_tax'] ? 1367 $carriersPrice[$idAddress][$idPackage][$idCarrier]['without_tax'] : $realBestPriceWt; 1368 } 1369 1370 // Add the delivery option with best price as best price 1371 $deliveryOptionList[$idAddress][$key] = [ 1372 'carrier_list' => $bestPriceCarrier, 1373 'is_best_price' => true, 1374 'is_best_grade' => false, 1375 'unique_carrier' => (count($bestPriceCarrier) <= 1), 1376 ]; 1377 1378 // Reset $best_grade_carrier, it's now an array 1379 $bestGradeCarrier = []; 1380 $key = ''; 1381 1382 // Get the delivery option with the best grade 1383 foreach ($bestGradeCarriers as $idPackage => $idCarrier) { 1384 $key .= $idCarrier.','; 1385 if (!isset($bestGradeCarrier[$idCarrier])) { 1386 $bestGradeCarrier[$idCarrier] = [ 1387 'price_with_tax' => 0, 1388 'price_without_tax' => 0, 1389 'package_list' => [], 1390 'product_list' => [], 1391 ]; 1392 } 1393 $bestGradeCarrier[$idCarrier]['price_with_tax'] += $carriersPrice[$idAddress][$idPackage][$idCarrier]['with_tax']; 1394 $bestGradeCarrier[$idCarrier]['price_without_tax'] += $carriersPrice[$idAddress][$idPackage][$idCarrier]['without_tax']; 1395 $bestGradeCarrier[$idCarrier]['package_list'][] = $idPackage; 1396 $bestGradeCarrier[$idCarrier]['product_list'] = array_merge($bestGradeCarrier[$idCarrier]['product_list'], $packages[$idPackage]['product_list']); 1397 $bestGradeCarrier[$idCarrier]['instance'] = $carriersInstance[$idCarrier]; 1398 } 1399 1400 // Add the delivery option with best grade as best grade 1401 if (!isset($deliveryOptionList[$idAddress][$key])) { 1402 $deliveryOptionList[$idAddress][$key] = [ 1403 'carrier_list' => $bestGradeCarrier, 1404 'is_best_price' => false, 1405 'unique_carrier' => (count($bestGradeCarrier) <= 1), 1406 ]; 1407 } 1408 $deliveryOptionList[$idAddress][$key]['is_best_grade'] = true; 1409 1410 // Get all delivery options with a unique carrier 1411 foreach ($commonCarriers as $idCarrier) { 1412 $key = ''; 1413 $packageList = []; 1414 $productList = []; 1415 $priceWithTax = 0; 1416 $priceWithoutTax = 0; 1417 1418 foreach ($packages as $idPackage => $package) { 1419 $key .= $idCarrier.','; 1420 $priceWithTax += $carriersPrice[$idAddress][$idPackage][$idCarrier]['with_tax']; 1421 $priceWithoutTax += $carriersPrice[$idAddress][$idPackage][$idCarrier]['without_tax']; 1422 $packageList[] = $idPackage; 1423 $productList = array_merge($productList, $package['product_list']); 1424 } 1425 1426 if (!isset($deliveryOptionList[$idAddress][$key])) { 1427 $deliveryOptionList[$idAddress][$key] = [ 1428 'is_best_price' => false, 1429 'is_best_grade' => false, 1430 'unique_carrier' => true, 1431 'carrier_list' => [ 1432 $idCarrier => [ 1433 'price_with_tax' => $priceWithTax, 1434 'price_without_tax' => $priceWithoutTax, 1435 'instance' => $carriersInstance[$idCarrier], 1436 'package_list' => $packageList, 1437 'product_list' => $productList, 1438 ], 1439 ], 1440 ]; 1441 } else { 1442 $deliveryOptionList[$idAddress][$key]['unique_carrier'] = (count($deliveryOptionList[$idAddress][$key]['carrier_list']) <= 1); 1443 } 1444 } 1445 } 1446 1447 $cartRules = CartRule::getCustomerCartRules(Context::getContext()->cookie->id_lang, Context::getContext()->cookie->id_customer, true, true, false, $this, true); 1448 1449 $result = false; 1450 if ($this->id) { 1451 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 1452 (new DbQuery()) 1453 ->select('*') 1454 ->from('cart_cart_rule') 1455 ->where('`id_cart` = '.(int) $this->id) 1456 ); 1457 } 1458 1459 $cartRulesInCart = []; 1460 1461 if (is_array($result)) { 1462 foreach ($result as $row) { 1463 $cartRulesInCart[] = $row['id_cart_rule']; 1464 } 1465 } 1466 1467 $totalProductsTaxIncluded = $this->getOrderTotal(true, static::ONLY_PRODUCTS); 1468 $totalProducts = $this->getOrderTotal(false, static::ONLY_PRODUCTS); 1469 1470 $freeCarriersRules = []; 1471 1472 $context = Context::getContext(); 1473 foreach ($cartRules as $cartRule) { 1474 $totalPrice = $cartRule['minimum_amount_tax'] ? $totalProductsTaxIncluded : $totalProducts; 1475 $totalPrice += (isset($realBestPrice) && $cartRule['minimum_amount_tax'] && $cartRule['minimum_amount_shipping']) ? $realBestPrice : 0; 1476 $totalPrice += (isset($realBestPriceWt) && !$cartRule['minimum_amount_tax'] && $cartRule['minimum_amount_shipping']) ? $realBestPriceWt : 0; 1477 $condition = ($cartRule['free_shipping'] && $cartRule['carrier_restriction'] && $cartRule['minimum_amount'] <= $totalPrice) ? 1 : 0; 1478 if (isset($cartRule['code']) && !empty($cartRule['code'])) { 1479 $condition = ($cartRule['free_shipping'] && $cartRule['carrier_restriction'] && in_array($cartRule['id_cart_rule'], $cartRulesInCart) 1480 && $cartRule['minimum_amount'] <= $totalPrice) ? 1 : 0; 1481 } 1482 if ($condition) { 1483 $cr = new CartRule((int) $cartRule['id_cart_rule']); 1484 if (Validate::isLoadedObject($cr) && 1485 $cr->checkValidity($context, in_array((int) $cartRule['id_cart_rule'], $cartRulesInCart), false, false) 1486 ) { 1487 $carriers = $cr->getAssociatedRestrictions('carrier', true, false); 1488 if (is_array($carriers) && count($carriers) && isset($carriers['selected'])) { 1489 foreach ($carriers['selected'] as $carrier) { 1490 if (isset($carrier['id_carrier']) && $carrier['id_carrier']) { 1491 $freeCarriersRules[] = (int) $carrier['id_carrier']; 1492 } 1493 } 1494 } 1495 } 1496 } 1497 } 1498 1499 // For each delivery options : 1500 // - Set the carrier list 1501 // - Calculate the price 1502 // - Calculate the average position 1503 foreach ($deliveryOptionList as $idAddress => $deliveryOption) { 1504 foreach ($deliveryOption as $key => $value) { 1505 $totalPriceWithTax = 0; 1506 $totalPriceWithoutTax = 0; 1507 $position = 0; 1508 foreach ($value['carrier_list'] as $idCarrier => $data) { 1509 $totalPriceWithTax += $data['price_with_tax']; 1510 $totalPriceWithoutTax += $data['price_without_tax']; 1511 $totalPriceWithoutTaxWithRules = (in_array($idCarrier, $freeCarriersRules)) ? 0 : $totalPriceWithoutTax; 1512 1513 if (!isset($carrierCollection[$idCarrier])) { 1514 $carrierCollection[$idCarrier] = new Carrier($idCarrier); 1515 } 1516 $deliveryOptionList[$idAddress][$key]['carrier_list'][$idCarrier]['instance'] = $carrierCollection[$idCarrier]; 1517 1518 if (file_exists(_PS_SHIP_IMG_DIR_.$idCarrier.'.jpg')) { 1519 $deliveryOptionList[$idAddress][$key]['carrier_list'][$idCarrier]['logo'] = _THEME_SHIP_DIR_.$idCarrier.'.jpg'; 1520 } else { 1521 $deliveryOptionList[$idAddress][$key]['carrier_list'][$idCarrier]['logo'] = false; 1522 } 1523 1524 $position += $carrierCollection[$idCarrier]->position; 1525 } 1526 if (!isset($totalPriceWithoutTaxWithRules)) { 1527 $totalPriceWithoutTaxWithRules = false; 1528 } 1529 $deliveryOptionList[$idAddress][$key]['total_price_with_tax'] = $totalPriceWithTax; 1530 $deliveryOptionList[$idAddress][$key]['total_price_without_tax'] = $totalPriceWithoutTax; 1531 $deliveryOptionList[$idAddress][$key]['is_free'] = !$totalPriceWithoutTaxWithRules ? true : false; 1532 $deliveryOptionList[$idAddress][$key]['position'] = $position / count($value['carrier_list']); 1533 } 1534 } 1535 1536 // Sort delivery option list 1537 foreach ($deliveryOptionList as &$array) { 1538 uasort($array, ['Cart', 'sortDeliveryOptionList']); 1539 } 1540 1541 $cache[$this->id] = $deliveryOptionList; 1542 1543 return $cache[$this->id]; 1544 } 1545 1546 /** 1547 * Get products grouped by package and by addresses to be sent individualy (one package = one shipping cost). 1548 * 1549 * @param bool $flush 1550 * 1551 * @return array array( 1552 * 0 => array( // First address 1553 * 0 => array( // First package 1554 * 'product_list' => array(...), 1555 * 'carrier_list' => array(...), 1556 * 'id_warehouse' => array(...), 1557 * ), 1558 * ), 1559 * ); 1560 * @throws PrestaShopDatabaseException 1561 * @throws PrestaShopException 1562 * @todo Add availability check 1563 */ 1564 public function getPackageList($flush = false) 1565 { 1566 static $cache = []; 1567 $cacheKey = (int) $this->id.'_'.(int) $this->id_address_delivery; 1568 if (isset($cache[$cacheKey]) && $cache[$cacheKey] !== false && !$flush) { 1569 return $cache[$cacheKey]; 1570 } 1571 1572 $productList = $this->getProducts($flush); 1573 // Step 1 : Get product informations (warehouse_list and carrier_list), count warehouse 1574 // Determine the best warehouse to determine the packages 1575 // For that we count the number of time we can use a warehouse for a specific delivery address 1576 $warehouseCountByAddress = []; 1577 1578 $stockManagementActive = Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT'); 1579 1580 foreach ($productList as &$product) { 1581 if ((int) $product['id_address_delivery'] == 0) { 1582 $product['id_address_delivery'] = (int) $this->id_address_delivery; 1583 } 1584 1585 if (!isset($warehouseCountByAddress[$product['id_address_delivery']])) { 1586 $warehouseCountByAddress[$product['id_address_delivery']] = []; 1587 } 1588 1589 $product['warehouse_list'] = []; 1590 1591 if ($stockManagementActive && 1592 (int) $product['advanced_stock_management'] == 1 1593 ) { 1594 $warehouseList = Warehouse::getProductWarehouseList($product['id_product'], $product['id_product_attribute'], $this->id_shop); 1595 if (count($warehouseList) == 0) { 1596 $warehouseList = Warehouse::getProductWarehouseList($product['id_product'], $product['id_product_attribute']); 1597 } 1598 // Does the product is in stock ? 1599 // If yes, get only warehouse where the product is in stock 1600 1601 $warehouseInStock = []; 1602 $manager = StockManagerFactory::getManager(); 1603 1604 foreach ($warehouseList as $key => $warehouse) { 1605 $productRealQuantities = $manager->getProductRealQuantities( 1606 $product['id_product'], 1607 $product['id_product_attribute'], 1608 [$warehouse['id_warehouse']], 1609 true 1610 ); 1611 1612 if ($productRealQuantities > 0 || Pack::isPack((int) $product['id_product'])) { 1613 $warehouseInStock[] = $warehouse; 1614 } 1615 } 1616 1617 if (!empty($warehouseInStock)) { 1618 $warehouseList = $warehouseInStock; 1619 $product['in_stock'] = true; 1620 } else { 1621 $product['in_stock'] = false; 1622 } 1623 } else { 1624 //simulate default warehouse 1625 $warehouseList = [0 => ['id_warehouse' => 0]]; 1626 $product['in_stock'] = StockAvailable::getQuantityAvailableByProduct($product['id_product'], $product['id_product_attribute']) > 0; 1627 } 1628 1629 foreach ($warehouseList as $warehouse) { 1630 $product['warehouse_list'][$warehouse['id_warehouse']] = $warehouse['id_warehouse']; 1631 if (!isset($warehouseCountByAddress[$product['id_address_delivery']][$warehouse['id_warehouse']])) { 1632 $warehouseCountByAddress[$product['id_address_delivery']][$warehouse['id_warehouse']] = 0; 1633 } 1634 1635 $warehouseCountByAddress[$product['id_address_delivery']][$warehouse['id_warehouse']]++; 1636 } 1637 } 1638 unset($product); 1639 1640 arsort($warehouseCountByAddress); 1641 1642 // Step 2 : Group product by warehouse 1643 $groupedByWarehouse = []; 1644 1645 foreach ($productList as &$product) { 1646 if (!isset($groupedByWarehouse[$product['id_address_delivery']])) { 1647 $groupedByWarehouse[$product['id_address_delivery']] = [ 1648 'in_stock' => [], 1649 'out_of_stock' => [], 1650 ]; 1651 } 1652 1653 $product['carrier_list'] = []; 1654 $idWarehouse = 0; 1655 foreach ($warehouseCountByAddress[$product['id_address_delivery']] as $idWar => $val) { 1656 if (array_key_exists((int) $idWar, $product['warehouse_list'])) { 1657 $product['carrier_list'] = array_replace($product['carrier_list'], Carrier::getAvailableCarrierList(new Product($product['id_product']), $idWar, $product['id_address_delivery'], null, $this)); 1658 if (!$idWarehouse) { 1659 $idWarehouse = (int) $idWar; 1660 } 1661 } 1662 } 1663 1664 if (!isset($groupedByWarehouse[$product['id_address_delivery']]['in_stock'][$idWarehouse])) { 1665 $groupedByWarehouse[$product['id_address_delivery']]['in_stock'][$idWarehouse] = []; 1666 $groupedByWarehouse[$product['id_address_delivery']]['out_of_stock'][$idWarehouse] = []; 1667 } 1668 1669 if (!$this->allow_seperated_package) { 1670 $key = 'in_stock'; 1671 } else { 1672 $key = $product['in_stock'] ? 'in_stock' : 'out_of_stock'; 1673 $productQuantityInStock = StockAvailable::getQuantityAvailableByProduct($product['id_product'], $product['id_product_attribute']); 1674 if ($product['in_stock'] && $product['cart_quantity'] > $productQuantityInStock) { 1675 $outStockPart = $product['cart_quantity'] - $productQuantityInStock; 1676 $productBis = $product; 1677 $productBis['cart_quantity'] = $outStockPart; 1678 $productBis['in_stock'] = 0; 1679 $product['cart_quantity'] -= $outStockPart; 1680 $groupedByWarehouse[$product['id_address_delivery']]['out_of_stock'][$idWarehouse][] = $productBis; 1681 } 1682 } 1683 1684 if (empty($product['carrier_list'])) { 1685 $product['carrier_list'] = [0 => 0]; 1686 } 1687 1688 $groupedByWarehouse[$product['id_address_delivery']][$key][$idWarehouse][] = $product; 1689 } 1690 unset($product); 1691 1692 // Step 3 : grouped product from grouped_by_warehouse by available carriers 1693 $groupedByCarriers = []; 1694 foreach ($groupedByWarehouse as $idAddressDelivery => $productsInStockList) { 1695 if (!isset($groupedByCarriers[$idAddressDelivery])) { 1696 $groupedByCarriers[$idAddressDelivery] = [ 1697 'in_stock' => [], 1698 'out_of_stock' => [], 1699 ]; 1700 } 1701 foreach ($productsInStockList as $key => $warehouseList) { 1702 if (!isset($groupedByCarriers[$idAddressDelivery][$key])) { 1703 $groupedByCarriers[$idAddressDelivery][$key] = []; 1704 } 1705 foreach ($warehouseList as $idWarehouse => $productList) { 1706 if (!isset($groupedByCarriers[$idAddressDelivery][$key][$idWarehouse])) { 1707 $groupedByCarriers[$idAddressDelivery][$key][$idWarehouse] = []; 1708 } 1709 foreach ($productList as $product) { 1710 $packageCarriersKey = implode(',', $product['carrier_list']); 1711 1712 if (!isset($groupedByCarriers[$idAddressDelivery][$key][$idWarehouse][$packageCarriersKey])) { 1713 $groupedByCarriers[$idAddressDelivery][$key][$idWarehouse][$packageCarriersKey] = [ 1714 'product_list' => [], 1715 'carrier_list' => $product['carrier_list'], 1716 'warehouse_list' => $product['warehouse_list'], 1717 ]; 1718 } 1719 1720 $groupedByCarriers[$idAddressDelivery][$key][$idWarehouse][$packageCarriersKey]['product_list'][] = $product; 1721 } 1722 } 1723 } 1724 } 1725 1726 $packageList = []; 1727 // Step 4 : merge product from grouped_by_carriers into $package to minimize the number of package 1728 foreach ($groupedByCarriers as $idAddressDelivery => $productsInStockList) { 1729 if (!isset($packageList[$idAddressDelivery])) { 1730 $packageList[$idAddressDelivery] = [ 1731 'in_stock' => [], 1732 'out_of_stock' => [], 1733 ]; 1734 } 1735 1736 foreach ($productsInStockList as $key => $warehouseList) { 1737 if (!isset($packageList[$idAddressDelivery][$key])) { 1738 $packageList[$idAddressDelivery][$key] = []; 1739 } 1740 // Count occurance of each carriers to minimize the number of packages 1741 $carrierCount = []; 1742 foreach ($warehouseList as $idWarehouse => $productsGroupedByCarriers) { 1743 foreach ($productsGroupedByCarriers as $data) { 1744 foreach ($data['carrier_list'] as $idCarrier) { 1745 if (!isset($carrierCount[$idCarrier])) { 1746 $carrierCount[$idCarrier] = 0; 1747 } 1748 $carrierCount[$idCarrier]++; 1749 } 1750 } 1751 } 1752 arsort($carrierCount); 1753 foreach ($warehouseList as $idWarehouse => $productsGroupedByCarriers) { 1754 if (!isset($packageList[$idAddressDelivery][$key][$idWarehouse])) { 1755 $packageList[$idAddressDelivery][$key][$idWarehouse] = []; 1756 } 1757 foreach ($productsGroupedByCarriers as $data) { 1758 foreach ($carrierCount as $idCarrier => $rate) { 1759 if (array_key_exists($idCarrier, $data['carrier_list'])) { 1760 if (!isset($packageList[$idAddressDelivery][$key][$idWarehouse][$idCarrier])) { 1761 $packageList[$idAddressDelivery][$key][$idWarehouse][$idCarrier] = [ 1762 'carrier_list' => $data['carrier_list'], 1763 'warehouse_list' => $data['warehouse_list'], 1764 'product_list' => [], 1765 ]; 1766 } 1767 $packageList[$idAddressDelivery][$key][$idWarehouse][$idCarrier]['carrier_list'] = 1768 array_intersect($packageList[$idAddressDelivery][$key][$idWarehouse][$idCarrier]['carrier_list'], $data['carrier_list']); 1769 $packageList[$idAddressDelivery][$key][$idWarehouse][$idCarrier]['product_list'] = 1770 array_merge($packageList[$idAddressDelivery][$key][$idWarehouse][$idCarrier]['product_list'], $data['product_list']); 1771 1772 break; 1773 } 1774 } 1775 } 1776 } 1777 } 1778 } 1779 1780 // Step 5 : Reduce depth of $package_list 1781 $finalPackageList = []; 1782 foreach ($packageList as $idAddressDelivery => $productsInStockList) { 1783 if (!isset($finalPackageList[$idAddressDelivery])) { 1784 $finalPackageList[$idAddressDelivery] = []; 1785 } 1786 1787 foreach ($productsInStockList as $key => $warehouseList) { 1788 foreach ($warehouseList as $idWarehouse => $productsGroupedByCarriers) { 1789 foreach ($productsGroupedByCarriers as $data) { 1790 $finalPackageList[$idAddressDelivery][] = [ 1791 'product_list' => $data['product_list'], 1792 'carrier_list' => $data['carrier_list'], 1793 'warehouse_list' => $data['warehouse_list'], 1794 'id_warehouse' => $idWarehouse, 1795 ]; 1796 } 1797 } 1798 } 1799 } 1800 $cache[$cacheKey] = $finalPackageList; 1801 1802 return $finalPackageList; 1803 } 1804 1805 /** 1806 * Return package shipping cost 1807 * 1808 * @param int $idCarrier Carrier ID (default: current carrier) 1809 * @param bool $useTax 1810 * @param Country|null $defaultCountry 1811 * @param array|null $productList List of product concerned by the 1812 * shipping. If null, all the product 1813 * of the cart are used to calculate 1814 * the shipping cost. 1815 * @param int|null $idZone 1816 * 1817 * @return bool|float Shipping total, rounded to 1818 * _TB_PRICE_DATABASE_PRECISION_, or false on failure. 1819 * 1820 * @since 1.0.0 1821 * @version 1.0.0 Initial version 1822 * @throws PrestaShopException 1823 * @throws PrestaShopException 1824 * @throws PrestaShopException 1825 * @throws Adapter_Exception 1826 */ 1827 public function getPackageShippingCost($idCarrier = null, $useTax = true, Country $defaultCountry = null, $productList = null, $idZone = null) 1828 { 1829 if ($this->isVirtualCart()) { 1830 return 0.; 1831 } 1832 1833 if (!$defaultCountry) { 1834 $defaultCountry = Context::getContext()->country; 1835 } 1836 1837 if (!is_null($productList)) { 1838 foreach ($productList as $key => $value) { 1839 if ($value['is_virtual'] == 1) { 1840 unset($productList[$key]); 1841 } 1842 } 1843 } 1844 1845 if (is_null($productList)) { 1846 $products = $this->getProducts(); 1847 } else { 1848 $products = $productList; 1849 } 1850 1851 if (Configuration::get('PS_TAX_ADDRESS_TYPE') == 'id_address_invoice') { 1852 $addressId = (int) $this->id_address_invoice; 1853 } elseif (is_array($productList) && count($productList)) { 1854 $prod = current($productList); 1855 $addressId = (int) $prod['id_address_delivery']; 1856 } else { 1857 $addressId = null; 1858 } 1859 if (!Address::addressExists($addressId)) { 1860 $addressId = null; 1861 } 1862 1863 if (is_null($idCarrier) && !empty($this->id_carrier)) { 1864 $idCarrier = (int) $this->id_carrier; 1865 } 1866 1867 $cacheId = 'getPackageShippingCost_'.(int) $this->id.'_'.(int) $addressId.'_'.(int) $idCarrier.'_'.(int) $useTax.'_'.(int) $defaultCountry->id.'_'.(int) $idZone; 1868 if ($products) { 1869 foreach ($products as $product) { 1870 $cacheId .= '_'.(int) $product['id_product'].'_'.(int) $product['id_product_attribute']; 1871 } 1872 } 1873 1874 if (Cache::isStored($cacheId)) { 1875 return (float) Cache::retrieve($cacheId); 1876 } 1877 1878 // Order total in default currency without fees 1879 $orderTotal = $this->getOrderTotal(true, static::ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING, $productList); 1880 1881 // Start with shipping cost at 0 1882 $shippingCost = 0.; 1883 // If no product added, return 0 1884 if (!count($products)) { 1885 Cache::store($cacheId, $shippingCost); 1886 1887 return $shippingCost; 1888 } 1889 1890 if (!isset($idZone)) { 1891 // Get id zone 1892 if (!$this->isMultiAddressDelivery() 1893 && isset($this->id_address_delivery) // Be carefull, id_address_delivery is not usefull one 1.5 1894 && $this->id_address_delivery 1895 && Customer::customerHasAddress( 1896 $this->id_customer, 1897 $this->id_address_delivery 1898 ) 1899 ) { 1900 $idZone = Address::getZoneById((int) $this->id_address_delivery); 1901 } else { 1902 if (!Validate::isLoadedObject($defaultCountry)) { 1903 $defaultCountry = new Country(Configuration::get('PS_COUNTRY_DEFAULT'), Configuration::get('PS_LANG_DEFAULT')); 1904 } 1905 1906 $idZone = (int) $defaultCountry->id_zone; 1907 } 1908 } 1909 1910 if ($idCarrier && !$this->isCarrierInRange((int) $idCarrier, (int) $idZone)) { 1911 $idCarrier = ''; 1912 } 1913 1914 if (empty($idCarrier) && $this->isCarrierInRange((int) Configuration::get('PS_CARRIER_DEFAULT'), (int) $idZone)) { 1915 $idCarrier = (int) Configuration::get('PS_CARRIER_DEFAULT'); 1916 } 1917 1918 $totalPackageWithoutShippingTaxInc = $this->getOrderTotal(true, static::BOTH_WITHOUT_SHIPPING, $productList); 1919 if (empty($idCarrier)) { 1920 if ((int) $this->id_customer) { 1921 $customer = new Customer((int) $this->id_customer); 1922 $result = Carrier::getCarriers((int) Configuration::get('PS_LANG_DEFAULT'), true, false, (int) $idZone, $customer->getGroups()); 1923 unset($customer); 1924 } else { 1925 $result = Carrier::getCarriers((int) Configuration::get('PS_LANG_DEFAULT'), true, false, (int) $idZone); 1926 } 1927 1928 foreach ($result as $k => $row) { 1929 if ($row['id_carrier'] == Configuration::get('PS_CARRIER_DEFAULT')) { 1930 continue; 1931 } 1932 1933 if (!isset(static::$_carriers[$row['id_carrier']])) { 1934 static::$_carriers[$row['id_carrier']] = new Carrier((int) $row['id_carrier']); 1935 } 1936 1937 /** @var Carrier $carrier */ 1938 $carrier = static::$_carriers[$row['id_carrier']]; 1939 1940 $shippingMethod = $carrier->getShippingMethod(); 1941 // Get only carriers that are compliant with shipping method 1942 if (($shippingMethod == Carrier::SHIPPING_METHOD_WEIGHT && $carrier->getMaxDeliveryPriceByWeight((int) $idZone) === false) 1943 || ($shippingMethod == Carrier::SHIPPING_METHOD_PRICE && $carrier->getMaxDeliveryPriceByPrice((int) $idZone) === false) 1944 ) { 1945 unset($result[$k]); 1946 continue; 1947 } 1948 1949 // If out-of-range behavior carrier is set to "Deactivate carrier" 1950 if ($row['range_behavior']) { 1951 $checkDeliveryPriceByWeight = Carrier::checkDeliveryPriceByWeight($row['id_carrier'], $this->getTotalWeight(), (int) $idZone); 1952 1953 $totalOrder = $totalPackageWithoutShippingTaxInc; 1954 $checkDeliveryPriceByPrice = Carrier::checkDeliveryPriceByPrice($row['id_carrier'], $totalOrder, (int) $idZone, (int) $this->id_currency); 1955 1956 // Get only carriers that have a range compatible with cart 1957 if (($shippingMethod == Carrier::SHIPPING_METHOD_WEIGHT && !$checkDeliveryPriceByWeight) 1958 || ($shippingMethod == Carrier::SHIPPING_METHOD_PRICE && !$checkDeliveryPriceByPrice) 1959 ) { 1960 unset($result[$k]); 1961 continue; 1962 } 1963 } 1964 1965 if ($shippingMethod == Carrier::SHIPPING_METHOD_WEIGHT) { 1966 $shipping = $carrier->getDeliveryPriceByWeight($this->getTotalWeight($productList), (int) $idZone); 1967 } else { 1968 $shipping = $carrier->getDeliveryPriceByPrice($orderTotal, (int) $idZone, (int) $this->id_currency); 1969 } 1970 1971 if (!isset($minShippingPrice)) { 1972 $minShippingPrice = $shipping; 1973 } 1974 1975 if ($shipping <= $minShippingPrice) { 1976 $idCarrier = (int) $row['id_carrier']; 1977 $minShippingPrice = $shipping; 1978 } 1979 } 1980 } 1981 1982 if (empty($idCarrier)) { 1983 $idCarrier = Configuration::get('PS_CARRIER_DEFAULT'); 1984 } 1985 1986 if (!isset(static::$_carriers[$idCarrier])) { 1987 static::$_carriers[$idCarrier] = new Carrier((int) $idCarrier, Configuration::get('PS_LANG_DEFAULT')); 1988 } 1989 1990 $carrier = static::$_carriers[$idCarrier]; 1991 1992 // No valid Carrier or $id_carrier <= 0 ? 1993 if (!Validate::isLoadedObject($carrier)) { 1994 Cache::store($cacheId, 0.); 1995 1996 return 0.; 1997 } 1998 $shippingMethod = $carrier->getShippingMethod(); 1999 2000 if (!$carrier->active) { 2001 Cache::store($cacheId, $shippingCost); 2002 2003 return $shippingCost; 2004 } 2005 2006 // Free fees if free carrier 2007 if ($carrier->is_free == 1) { 2008 Cache::store($cacheId, 0.); 2009 2010 return 0.; 2011 } 2012 2013 // Select carrier tax 2014 if ($useTax && !Tax::excludeTaxeOption()) { 2015 $address = Address::initialize((int) $addressId); 2016 2017 if (Configuration::get('PS_ATCP_SHIPWRAP')) { 2018 // With PS_ATCP_SHIPWRAP, pre-tax price is deduced 2019 // from post tax price, so no $carrier_tax here 2020 // even though it sounds weird. 2021 $carrierTax = 0; 2022 } else { 2023 $carrierTax = $carrier->getTaxesRate($address); 2024 } 2025 } 2026 2027 $configuration = Configuration::getMultiple( 2028 [ 2029 'PS_SHIPPING_FREE_PRICE', 2030 'PS_SHIPPING_HANDLING', 2031 'PS_SHIPPING_METHOD', 2032 'PS_SHIPPING_FREE_WEIGHT', 2033 ] 2034 ); 2035 2036 // Free fees 2037 $freeFeesPrice = 0; 2038 if (isset($configuration['PS_SHIPPING_FREE_PRICE'])) { 2039 $freeFeesPrice = Tools::convertPrice((float) $configuration['PS_SHIPPING_FREE_PRICE'], Currency::getCurrencyInstance((int) $this->id_currency)); 2040 } 2041 $orderTotalWithDiscounts = $this->getOrderTotal(true, static::BOTH_WITHOUT_SHIPPING, null, null, false); 2042 if ($orderTotalWithDiscounts >= (float) ($freeFeesPrice) && (float) ($freeFeesPrice) > 0) { 2043 Cache::store($cacheId, $shippingCost); 2044 2045 return $shippingCost; 2046 } 2047 2048 if (isset($configuration['PS_SHIPPING_FREE_WEIGHT']) 2049 && $this->getTotalWeight() >= (float) $configuration['PS_SHIPPING_FREE_WEIGHT'] 2050 && (float) $configuration['PS_SHIPPING_FREE_WEIGHT'] > 0 2051 ) { 2052 Cache::store($cacheId, $shippingCost); 2053 2054 return $shippingCost; 2055 } 2056 2057 // Get shipping cost using correct method 2058 if ($carrier->range_behavior) { 2059 if (!isset($idZone)) { 2060 // Get id zone 2061 if (isset($this->id_address_delivery) 2062 && $this->id_address_delivery 2063 && Customer::customerHasAddress($this->id_customer, $this->id_address_delivery) 2064 ) { 2065 $idZone = Address::getZoneById((int) $this->id_address_delivery); 2066 } else { 2067 $idZone = (int) $defaultCountry->id_zone; 2068 } 2069 } 2070 2071 if (($shippingMethod == Carrier::SHIPPING_METHOD_WEIGHT && !Carrier::checkDeliveryPriceByWeight($carrier->id, $this->getTotalWeight(), (int) $idZone)) 2072 || ($shippingMethod == Carrier::SHIPPING_METHOD_PRICE && !Carrier::checkDeliveryPriceByPrice($carrier->id, $totalPackageWithoutShippingTaxInc, $idZone, (int) $this->id_currency) 2073 ) 2074 ) { 2075 $shippingCost += 0; 2076 } else { 2077 if ($shippingMethod == Carrier::SHIPPING_METHOD_WEIGHT) { 2078 $shippingCost += $carrier->getDeliveryPriceByWeight($this->getTotalWeight($productList), $idZone); 2079 } else { // by price 2080 $shippingCost += $carrier->getDeliveryPriceByPrice($orderTotal, $idZone, (int) $this->id_currency); 2081 } 2082 } 2083 } else { 2084 if ($shippingMethod == Carrier::SHIPPING_METHOD_WEIGHT) { 2085 $shippingCost += $carrier->getDeliveryPriceByWeight($this->getTotalWeight($productList), $idZone); 2086 } else { 2087 $shippingCost += $carrier->getDeliveryPriceByPrice($orderTotal, $idZone, (int) $this->id_currency); 2088 } 2089 } 2090 // Adding handling charges 2091 if (isset($configuration['PS_SHIPPING_HANDLING']) && $carrier->shipping_handling) { 2092 $shippingCost += (float) $configuration['PS_SHIPPING_HANDLING']; 2093 } 2094 2095 // Additional Shipping Cost per product 2096 foreach ($products as $product) { 2097 if (!$product['is_virtual']) { 2098 $shippingCost += $product['additional_shipping_cost'] * $product['cart_quantity']; 2099 } 2100 } 2101 2102 $shippingCost = Tools::convertPrice($shippingCost, Currency::getCurrencyInstance((int) $this->id_currency)); 2103 2104 //get external shipping cost from module 2105 if ($carrier->shipping_external) { 2106 $moduleName = $carrier->external_module_name; 2107 2108 /** @var CarrierModule $module */ 2109 $module = Module::getInstanceByName($moduleName); 2110 2111 if (Validate::isLoadedObject($module)) { 2112 if (property_exists($module, 'id_carrier')) { 2113 $module->id_carrier = $carrier->id; 2114 } 2115 if ($carrier->need_range) { 2116 if (method_exists($module, 'getPackageShippingCost')) { 2117 $shippingCost = $module->getPackageShippingCost($this, $shippingCost, $products); 2118 } else { 2119 $shippingCost = $module->getOrderShippingCost($this, $shippingCost); 2120 } 2121 } else { 2122 $shippingCost = $module->getOrderShippingCostExternal($this); 2123 } 2124 2125 // Check if carrier is available 2126 if ($shippingCost === false) { 2127 Cache::store($cacheId, false); 2128 2129 return false; 2130 } 2131 } else { 2132 Cache::store($cacheId, false); 2133 2134 return false; 2135 } 2136 } 2137 2138 if (Configuration::get('PS_ATCP_SHIPWRAP')) { 2139 if ($useTax) { 2140 // With PS_ATCP_SHIPWRAP, we apply the proportionate tax rate to the shipping 2141 // costs. This is on purpose and required in many countries in the European Union. 2142 $shippingCost *= 1 + $this->getAverageProductsTaxRate(); 2143 } 2144 } else { 2145 // Apply tax 2146 if ($useTax && isset($carrierTax)) { 2147 $shippingCost *= 1 + ($carrierTax / 100); 2148 } 2149 } 2150 $shippingCost = round($shippingCost, _TB_PRICE_DATABASE_PRECISION_); 2151 2152 Cache::store($cacheId, $shippingCost); 2153 2154 return $shippingCost; 2155 } 2156 2157 /** 2158 * Does the cart use multiple address 2159 * 2160 * @return bool 2161 * 2162 * @since 1.0.0 2163 * @version 1.0.0 Initial version 2164 * @throws PrestaShopException 2165 */ 2166 public function isMultiAddressDelivery() 2167 { 2168 static $cache = []; 2169 2170 if (!isset($cache[$this->id])) { 2171 $sql = new DbQuery(); 2172 $sql->select('count(distinct id_address_delivery)'); 2173 $sql->from('cart_product', 'cp'); 2174 $sql->where('id_cart = '.(int) $this->id); 2175 2176 $cache[$this->id] = Db::getInstance()->getValue($sql) > 1; 2177 } 2178 2179 return $cache[$this->id]; 2180 } 2181 2182 /** 2183 * isCarrierInRange 2184 * 2185 * Check if the specified carrier is in range 2186 * 2187 * @param int $idCarrier 2188 * @param int $idZone 2189 * 2190 * @return bool 2191 * 2192 * @throws Adapter_Exception 2193 * @throws PrestaShopDatabaseException 2194 * @throws PrestaShopException 2195 * @since 1.0.0 2196 * @version 1.0.0 Initial version 2197 */ 2198 public function isCarrierInRange($idCarrier, $idZone) 2199 { 2200 $carrier = new Carrier((int) $idCarrier, Configuration::get('PS_LANG_DEFAULT')); 2201 $shippingMethod = $carrier->getShippingMethod(); 2202 if (!$carrier->range_behavior) { 2203 return true; 2204 } 2205 2206 if ($shippingMethod == Carrier::SHIPPING_METHOD_FREE) { 2207 return true; 2208 } 2209 2210 $checkDeliveryPriceByWeight = Carrier::checkDeliveryPriceByWeight( 2211 (int) $idCarrier, 2212 $this->getTotalWeight(), 2213 $idZone 2214 ); 2215 if ($shippingMethod == Carrier::SHIPPING_METHOD_WEIGHT && $checkDeliveryPriceByWeight) { 2216 return true; 2217 } 2218 2219 $checkDeliveryPriceByPrice = Carrier::checkDeliveryPriceByPrice( 2220 (int) $idCarrier, 2221 $this->getOrderTotal( 2222 true, 2223 static::BOTH_WITHOUT_SHIPPING 2224 ), 2225 $idZone, 2226 (int) $this->id_currency 2227 ); 2228 if ($shippingMethod == Carrier::SHIPPING_METHOD_PRICE && $checkDeliveryPriceByPrice) { 2229 return true; 2230 } 2231 2232 return false; 2233 } 2234 2235 /** 2236 * Return cart weight 2237 * 2238 * @param array|null $products 2239 * 2240 * @return float Cart weight 2241 * 2242 * @since 1.0.0 2243 * @version 1.0.0 Initial version 2244 * @throws PrestaShopException 2245 * @throws PrestaShopException 2246 */ 2247 public function getTotalWeight($products = null) 2248 { 2249 if (!is_null($products)) { 2250 $totalWeight = 0; 2251 foreach ($products as $product) { 2252 if (!isset($product['weight_attribute']) || is_null($product['weight_attribute'])) { 2253 $totalWeight += $product['weight'] * $product['cart_quantity']; 2254 } else { 2255 $totalWeight += $product['weight_attribute'] * $product['cart_quantity']; 2256 } 2257 } 2258 2259 return $totalWeight; 2260 } 2261 2262 if (!isset(static::$_totalWeight[$this->id])) { 2263 if (Combination::isFeatureActive()) { 2264 $weightProductWithAttribute = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 2265 (new DbQuery()) 2266 ->select('SUM((p.`weight` + pa.`weight`) * cp.`quantity`) AS `nb`') 2267 ->from('cart_product', 'cp') 2268 ->leftJoin('product', 'p', 'cp.`id_product` = p.`id_product`') 2269 ->leftJoin('product_attribute', 'pa', 'cp.`id_product_attribute` = pa.`id_product_attribute`') 2270 ->where('cp.`id_product_attribute` IS NOT NULL') 2271 ->where('cp.`id_product_attribute` != 0') 2272 ->where('cp.`id_cart` = '.(int) $this->id) 2273 ); 2274 } else { 2275 $weightProductWithAttribute = 0; 2276 } 2277 2278 $weightProductWithoutAttribute = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 2279 (new DbQuery()) 2280 ->select('SUM(p.`weight` * cp.`quantity`) AS `nb`') 2281 ->from('cart_product', 'cp') 2282 ->leftJoin('product', 'p', 'cp.`id_product` = p.`id_product`') 2283 ->where('cp.`id_product_attribute` IS NULL OR cp.`id_product_attribute` = 0') 2284 ->where('cp.`id_cart` = '.(int) $this->id) 2285 ); 2286 2287 static::$_totalWeight[$this->id] = round((float) $weightProductWithAttribute + (float) $weightProductWithoutAttribute, 6); 2288 } 2289 2290 return static::$_totalWeight[$this->id]; 2291 } 2292 2293 /** 2294 * The arguments are optional and only serve as return values in case 2295 * caller needs the details. 2296 * 2297 * @param null $amountTaxExcluded 2298 * @param null $amountTaxIncluded 2299 * 2300 * @return float 2301 * 2302 * @throws Adapter_Exception 2303 * @throws PrestaShopDatabaseException 2304 * @throws PrestaShopException 2305 * 2306 * @since 1.0.0 2307 */ 2308 public function getAverageProductsTaxRate(&$amountTaxExcluded = null, 2309 &$amountTaxIncluded = null) 2310 { 2311 $amountTaxIncluded = $this->getOrderTotal(true, static::ONLY_PRODUCTS); 2312 $amountTaxExcluded = $this->getOrderTotal(false, static::ONLY_PRODUCTS); 2313 2314 $tax = $amountTaxIncluded - $amountTaxExcluded; 2315 if ($tax == 0 || $amountTaxExcluded == 0) { 2316 return 0.0; 2317 } 2318 2319 return $tax / $amountTaxExcluded; 2320 } 2321 2322 /** 2323 * Get the gift wrapping price 2324 * 2325 * @param bool $withTaxes With or without taxes 2326 * @param int|null $idAddress Address ID 2327 * 2328 * @return float wrapping price 2329 * 2330 * @throws Adapter_Exception 2331 * @throws PrestaShopDatabaseException 2332 * @throws PrestaShopException 2333 * @since 1.0.0 2334 * @version 1.0.0 Initial version 2335 */ 2336 public function getGiftWrappingPrice($withTaxes = true, $idAddress = null) 2337 { 2338 static $address = []; 2339 2340 $wrappingFees = (float) Configuration::get('PS_GIFT_WRAPPING_PRICE'); 2341 2342 if ($wrappingFees <= 0) { 2343 return $wrappingFees; 2344 } 2345 2346 if ($withTaxes) { 2347 if (Configuration::get('PS_ATCP_SHIPWRAP')) { 2348 // With PS_ATCP_SHIPWRAP, wrapping fee is by default tax included 2349 // so nothing to do here. 2350 } else { 2351 if (!isset($address[$this->id])) { 2352 if ($idAddress === null) { 2353 $idAddress = (int) $this->{Configuration::get('PS_TAX_ADDRESS_TYPE')}; 2354 } 2355 try { 2356 $address[$this->id] = Address::initialize($idAddress); 2357 } catch (Exception $e) { 2358 $address[$this->id] = new Address(); 2359 $address[$this->id]->id_country = Configuration::get('PS_COUNTRY_DEFAULT'); 2360 } 2361 } 2362 2363 $taxManager = TaxManagerFactory::getManager($address[$this->id], (int) Configuration::get('PS_GIFT_WRAPPING_TAX_RULES_GROUP')); 2364 $taxCalculator = $taxManager->getTaxCalculator(); 2365 $wrappingFees = $taxCalculator->addTaxes($wrappingFees); 2366 } 2367 2368 if (Configuration::get('PS_ATCP_SHIPWRAP')) { 2369 // With PS_ATCP_SHIPWRAP, wrapping fee is by default tax included, so we convert it 2370 // when asked for the pre tax price. 2371 $wrappingFees = round( 2372 $wrappingFees * (1 + $this->getAverageProductsTaxRate()), 2373 _TB_PRICE_DATABASE_PRECISION_ 2374 ); 2375 } 2376 } 2377 2378 return $wrappingFees; 2379 } 2380 2381 /** 2382 * @param int $filter 2383 * 2384 * @return array|false|mysqli_result|null|PDOStatement|resource 2385 * 2386 * @throws PrestaShopDatabaseException 2387 * @throws PrestaShopException 2388 * @since 1.0.0 2389 * @version 1.0.0 Initial version 2390 */ 2391 public function getCartRules($filter = CartRule::FILTER_ACTION_ALL) 2392 { 2393 // If the cart has not been saved, then there can't be any cart rule applied 2394 if (!CartRule::isFeatureActive() || !$this->id) { 2395 return []; 2396 } 2397 2398 $cacheKey = 'static::getCartRules_'.$this->id.'-'.$filter; 2399 if (!Cache::isStored($cacheKey)) { 2400 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 2401 (new DbQuery()) 2402 ->select('cr.*, crl.`id_lang`, crl.`name`, cd.`id_cart`') 2403 ->from('cart_cart_rule', 'cd') 2404 ->leftJoin('cart_rule', 'cr', 'cd.`id_cart_rule` = cr.`id_cart_rule`') 2405 ->leftJoin('cart_rule_lang', 'crl', 'cd.`id_cart_rule` = crl.`id_cart_rule` AND crl.`id_lang` = '.(int) $this->id_lang) 2406 ->where('`id_cart` = '.(int) $this->id) 2407 ->where((int) $filter === CartRule::FILTER_ACTION_SHIPPING ? '`free_shipping` = 1' : '') 2408 ->where((int) $filter === CartRule::FILTER_ACTION_GIFT ? '`gift_product` = 0' : '') 2409 ->where((int) $filter === CartRule::FILTER_ACTION_REDUCTION ? '`reduction_percent` != 0 OR `reduction_amount` != 0' : '') 2410 ->orderBy('cr.`priority` ASC') 2411 ); 2412 Cache::store($cacheKey, $result); 2413 } else { 2414 $result = Cache::retrieve($cacheKey); 2415 } 2416 2417 // Define virtual context to prevent case where the cart is not the in the global context 2418 $virtualContext = Context::getContext()->cloneContext(); 2419 $virtualContext->cart = $this; 2420 2421 foreach ($result as &$row) { 2422 $cartRule = new CartRule(); 2423 $cartRule->hydrate($row); 2424 2425 $row['obj'] = $cartRule; 2426 $row['value_real'] = $cartRule->getContextualValue(true, $virtualContext, $filter); 2427 $row['value_tax_exc'] = $cartRule->getContextualValue(false, $virtualContext, $filter); 2428 // Retro compatibility < 1.5.0.2 2429 $row['id_discount'] = $row['id_cart_rule']; 2430 $row['description'] = $row['name']; 2431 } 2432 2433 return $result; 2434 } 2435 2436 /* 2437 ** Customization management 2438 */ 2439 2440 /** 2441 * @param $deliveryOption 2442 * 2443 * @return int|mixed 2444 * 2445 * @throws Adapter_Exception 2446 * @throws PrestaShopDatabaseException 2447 * @throws PrestaShopException 2448 * @since 1.0.0 2449 * @version 1.0.0 Initial version 2450 */ 2451 protected function getIdCarrierFromDeliveryOption($deliveryOption) 2452 { 2453 $deliveryOptionList = $this->getDeliveryOptionList(); 2454 foreach ($deliveryOption as $key => $value) { 2455 if (isset($deliveryOptionList[$key]) && isset($deliveryOptionList[$key][$value])) { 2456 if (count($deliveryOptionList[$key][$value]['carrier_list']) == 1) { 2457 return current(array_keys($deliveryOptionList[$key][$value]['carrier_list'])); 2458 } 2459 } 2460 } 2461 2462 return 0; 2463 } 2464 2465 /** 2466 * 2467 * Sort list of option delivery by parameters define in the BO 2468 * 2469 * @param array $option1 2470 * @param array $option2 2471 * 2472 * @return int -1 if $option 1 must be placed before and 1 if the $option1 must be placed after the $option2 2473 * 2474 * @since 1.0.0 2475 * @version 1.0.0 Initial version 2476 * @throws PrestaShopException 2477 */ 2478 public static function sortDeliveryOptionList($option1, $option2) 2479 { 2480 static $orderByPrice = null; 2481 static $orderWay = null; 2482 if (is_null($orderByPrice)) { 2483 $orderByPrice = !Configuration::get('PS_CARRIER_DEFAULT_SORT'); 2484 } 2485 if (is_null($orderWay)) { 2486 $orderWay = Configuration::get('PS_CARRIER_DEFAULT_ORDER'); 2487 } 2488 2489 if ($orderByPrice) { 2490 if ($orderWay) { 2491 return ($option1['total_price_with_tax'] < $option2['total_price_with_tax']) * 2 - 1; 2492 } // return -1 or 1 2493 else { 2494 return ($option1['total_price_with_tax'] >= $option2['total_price_with_tax']) * 2 - 1; 2495 } 2496 } // return -1 or 1 2497 elseif ($orderWay) { 2498 return ($option1['position'] < $option2['position']) * 2 - 1; 2499 } // return -1 or 1 2500 else { 2501 return ($option1['position'] >= $option2['position']) * 2 - 1; 2502 } // return -1 or 1 2503 } 2504 2505 /** 2506 * Translate a int option_delivery identifier (3240002000) in a string ('24,3,') 2507 * 2508 * @param int $int 2509 * @param string $delimiter 2510 * 2511 * @return string 2512 * 2513 * @since 1.0.0 2514 * @version 1.0.0 Initial version 2515 */ 2516 public static function desintifier($int, $delimiter = ',') 2517 { 2518 $delimiterLen = $int[0]; 2519 $int = strrev(substr($int, 1)); 2520 $elm = explode(str_repeat('0', $delimiterLen + 1), $int); 2521 2522 return strrev(implode($delimiter, $elm)); 2523 } 2524 2525 /** 2526 * @param int $idCustomer 2527 * 2528 * @return bool|int 2529 * 2530 * @since 1.0.0 2531 * @version 1.0.0 Initial version 2532 * @throws PrestaShopException 2533 */ 2534 public static function lastNoneOrderedCart($idCustomer) 2535 { 2536 if (!$idCart = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 2537 (new DbQuery()) 2538 ->select('c.`id_cart`') 2539 ->from('cart', 'c') 2540 ->where('NOT EXISTS (SELECT 1 FROM '._DB_PREFIX_.'orders o WHERE o.`id_cart` = c.`id_cart`AND o.`id_customer` = '.(int) $idCustomer.')') 2541 ->where('c.`id_customer` = '.(int) $idCustomer.' '.Shop::addSqlRestriction(Shop::SHARE_ORDER, 'c')) 2542 ->orderBy('c.`date_upd` DESC') 2543 )) { 2544 return false; 2545 } 2546 2547 return (int) $idCart; 2548 } 2549 2550 /** 2551 * Build cart object from provided id_order 2552 * 2553 * @param int $idOrder 2554 * 2555 * @return Cart|bool 2556 * 2557 * @throws PrestaShopDatabaseException 2558 * @throws PrestaShopException 2559 * @since 1.0.0 2560 * @version 1.0.0 Initial version 2561 */ 2562 public static function getCartByOrderId($idOrder) 2563 { 2564 if ($idCart = static::getCartIdByOrderId($idOrder)) { 2565 return new Cart((int) $idCart); 2566 } 2567 2568 return false; 2569 } 2570 2571 /** 2572 * @param int $idOrder 2573 * 2574 * @return bool 2575 * 2576 * @throws PrestaShopDatabaseException 2577 * @throws PrestaShopException 2578 * @since 1.0.0 2579 * @version 1.0.0 Initial version 2580 */ 2581 public static function getCartIdByOrderId($idOrder) 2582 { 2583 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( 2584 (new DbQuery()) 2585 ->select('`id_cart`') 2586 ->from('orders') 2587 ->where('`id_order` = '.(int) $idOrder) 2588 ); 2589 if (!$result || empty($result) || !array_key_exists('id_cart', $result)) { 2590 return false; 2591 } 2592 2593 return $result['id_cart']; 2594 } 2595 2596 /** 2597 * @param int $idCustomer 2598 * @param bool $dontRejectOrdered if true, all carts will be returned, otherwise 2599 * already ordered carts will be filtered out 2600 * 2601 * @return array|false|mysqli_result|null|PDOStatement|resource 2602 * 2603 * @throws PrestaShopDatabaseException 2604 * @throws PrestaShopException 2605 * @since 1.0.0 2606 * @version 1.0.0 Initial version 2607 */ 2608 public static function getCustomerCarts($idCustomer, $dontRejectOrdered = true) 2609 { 2610 return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 2611 (new DbQuery()) 2612 ->select('*') 2613 ->from('cart', 'c') 2614 ->where('c.`id_customer` = '.(int) $idCustomer) 2615 ->where($dontRejectOrdered ? '' : 'NOT EXISTS (SELECT 1 FROM '._DB_PREFIX_.'orders o WHERE o.`id_cart` = c.`id_cart`)') 2616 ->orderBy('c.`date_add` DESC') 2617 ); 2618 } 2619 2620 /** 2621 * @param string $echo 2622 * @param mixed $tr 2623 * 2624 * @return string 2625 * 2626 * @since 1.0.0 2627 * @version 1.0.0 Initial version 2628 * @throws PrestaShopException 2629 */ 2630 public static function replaceZeroByShopName($echo, $tr) 2631 { 2632 return ($echo == '0' ? Carrier::getCarrierNameFromShopName() : $echo); 2633 } 2634 2635 /** 2636 * isGuestCartByCartId 2637 * 2638 * @param int $idCart 2639 * 2640 * @return bool true if cart has been made by a guest customer 2641 * 2642 * @since 1.0.0 2643 * @version 1.0.0 Initial version 2644 * @throws PrestaShopException 2645 */ 2646 public static function isGuestCartByCartId($idCart) 2647 { 2648 if (!(int) $idCart) { 2649 return false; 2650 } 2651 2652 return (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 2653 (new DbQuery()) 2654 ->select('`is_guest`') 2655 ->from('customer', 'cu') 2656 ->leftJoin('cart', 'ca', 'ca.`id_customer` = cu.`id_customer`') 2657 ->where('ca.`id_cart` = '.(int) $idCart) 2658 ); 2659 } 2660 2661 /** 2662 * 2663 * Execute hook displayCarrierList (extraCarrier) and merge theme to the $array 2664 * 2665 * @param array $array 2666 * 2667 * @throws PrestaShopDatabaseException 2668 * @throws PrestaShopException 2669 */ 2670 public static function addExtraCarriers(&$array) 2671 { 2672 $first = true; 2673 $hookExtracarrierAddr = []; 2674 foreach (Context::getContext()->cart->getAddressCollection() as $address) { 2675 $hook = Hook::exec('displayCarrierList', ['address' => $address]); 2676 $hookExtracarrierAddr[$address->id] = $hook; 2677 2678 if ($first) { 2679 $array = array_merge( 2680 $array, 2681 ['HOOK_EXTRACARRIER' => $hook] 2682 ); 2683 $first = false; 2684 } 2685 $array = array_merge( 2686 $array, 2687 ['HOOK_EXTRACARRIER_ADDR' => $hookExtracarrierAddr] 2688 ); 2689 } 2690 } 2691 2692 /** 2693 * Get all delivery addresses object for the current cart 2694 * 2695 * @return array 2696 * @throws PrestaShopDatabaseException 2697 * @throws PrestaShopException 2698 */ 2699 public function getAddressCollection() 2700 { 2701 $collection = []; 2702 $cacheId = 'static::getAddressCollection'.(int) $this->id; 2703 if (!Cache::isStored($cacheId)) { 2704 $result = Db::getInstance()->executeS( 2705 (new DbQuery()) 2706 ->select('DISTINCT `id_address_delivery`') 2707 ->from('cart_product') 2708 ->where('`id_cart` = '.(int) $this->id) 2709 ); 2710 Cache::store($cacheId, $result); 2711 } else { 2712 $result = Cache::retrieve($cacheId); 2713 } 2714 2715 $result[] = ['id_address_delivery' => (int) $this->id_address_delivery]; 2716 2717 foreach ($result as $row) { 2718 if ((int) $row['id_address_delivery'] != 0) { 2719 $collection[(int) $row['id_address_delivery']] = new Address((int) $row['id_address_delivery']); 2720 } 2721 } 2722 2723 return $collection; 2724 } 2725 2726 /** 2727 * Update the address id of the cart 2728 * 2729 * @param int $idAddress Current address id to change 2730 * @param int $idAddressNew New address id 2731 * 2732 * @throws PrestaShopDatabaseException 2733 * @throws PrestaShopException 2734 * @since 1.0.0 2735 * @version 1.0.0 Initial version 2736 */ 2737 public function updateAddressId($idAddress, $idAddressNew) 2738 { 2739 $toUpdate = false; 2740 if (!isset($this->id_address_invoice) || $this->id_address_invoice == $idAddress) { 2741 $toUpdate = true; 2742 $this->id_address_invoice = $idAddressNew; 2743 } 2744 if (!isset($this->id_address_delivery) || $this->id_address_delivery == $idAddress) { 2745 $toUpdate = true; 2746 $this->id_address_delivery = $idAddressNew; 2747 } 2748 if ($toUpdate) { 2749 $this->update(); 2750 } 2751 2752 Db::getInstance()->update( 2753 'cart_product', 2754 [ 2755 'id_address_delivery' => (int) $idAddressNew, 2756 ], 2757 '`id_cart` = '.(int) $this->id.' AND `id_address_delivery` = '.(int) $idAddress 2758 ); 2759 2760 Db::getInstance()->update( 2761 'customization', 2762 [ 2763 'id_address_delivery' => (int) $idAddressNew, 2764 ], 2765 '`id_cart` = '.(int) $this->id.' AND `id_address_delivery` = '.(int) $idAddress 2766 ); 2767 } 2768 2769 /** 2770 * @param bool $nullValues 2771 * 2772 * @return bool 2773 * 2774 * @since 1.0.0 2775 * @version 1.0.0 Initial version 2776 */ 2777 public function update($nullValues = false) 2778 { 2779 if (isset(static::$_nbProducts[$this->id])) { 2780 unset(static::$_nbProducts[$this->id]); 2781 } 2782 2783 if (isset(static::$_totalWeight[$this->id])) { 2784 unset(static::$_totalWeight[$this->id]); 2785 } 2786 2787 $this->_products = null; 2788 $return = parent::update($nullValues); 2789 Hook::exec('actionCartSave', ['cart' => $this]); 2790 2791 return $return; 2792 } 2793 2794 /** 2795 * @return bool 2796 * 2797 * @since 1.0.0 2798 * @version 1.0.0 Initial version 2799 * @throws PrestaShopDatabaseException 2800 * @throws PrestaShopDatabaseException 2801 * @throws PrestaShopDatabaseException 2802 * @throws PrestaShopDatabaseException 2803 * @throws PrestaShopException 2804 */ 2805 public function delete() 2806 { 2807 if ($this->OrderExists()) { //NOT delete a cart which is associated with an order 2808 return false; 2809 } 2810 2811 $uploadedFiles = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 2812 (new DbQuery()) 2813 ->select('cd.`value`') 2814 ->from('customized_data', 'cd') 2815 ->innerJoin('customization', 'c', 'cd.`id_customization` = c.`id_customization`') 2816 ->where('cd.`type` = 0') 2817 ->where('c.`id_cart` = '.(int) $this->id) 2818 ); 2819 2820 foreach ($uploadedFiles as $mustUnlink) { 2821 unlink(_PS_UPLOAD_DIR_.$mustUnlink['value'].'_small'); 2822 unlink(_PS_UPLOAD_DIR_.$mustUnlink['value']); 2823 } 2824 2825 Db::getInstance()->delete( 2826 'customized_data', 2827 '`id_customization` IN (SELECT `id_customization` FROM `'._DB_PREFIX_.'customization` WHERE `id_cart`='.(int) $this->id.')' 2828 ); 2829 2830 Db::getInstance()->delete( 2831 'customization', 2832 '`id_cart` = '.(int) $this->id 2833 ); 2834 2835 if (!Db::getInstance()->delete('cart_cart_rule', '`id_cart` = '.(int) $this->id) 2836 || !Db::getInstance()->delete('cart_product', '`id_cart` = '.(int) $this->id) 2837 ) { 2838 return false; 2839 } 2840 2841 return parent::delete(); 2842 } 2843 2844 /** 2845 * Check if order has already been placed 2846 * 2847 * @return bool result 2848 * 2849 * @since 1.0.0 2850 * @version 1.0.0 2851 * @throws PrestaShopException 2852 */ 2853 public function orderExists() 2854 { 2855 $cacheId = 'static::orderExists_'.(int) $this->id; 2856 if (!Cache::isStored($cacheId)) { 2857 $result = (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 2858 (new DbQuery()) 2859 ->select('COUNT(*)') 2860 ->from('orders') 2861 ->where('`id_cart` = '.(int) $this->id) 2862 ); 2863 Cache::store($cacheId, $result); 2864 2865 return $result; 2866 } 2867 2868 return Cache::retrieve($cacheId); 2869 } 2870 2871 /** 2872 * @deprecated 1.0.0, use Cart->getCartRules() 2873 * 2874 * @param bool $lite 2875 * @param bool $refresh 2876 * 2877 * @return array|false|mysqli_result|null|PDOStatement|resource 2878 * @throws PrestaShopDatabaseException 2879 * @throws PrestaShopException 2880 */ 2881 public function getDiscounts($lite = false, $refresh = false) 2882 { 2883 Tools::displayAsDeprecated(); 2884 2885 return $this->getCartRules(); 2886 } 2887 2888 /** 2889 * Return the cart rules Ids on the cart. 2890 * 2891 * @param int $filter 2892 * 2893 * @return array 2894 * @throws PrestaShopDatabaseException 2895 * 2896 * @since 1.0.0 2897 * @version 1.0.0 Initial version 2898 * @throws PrestaShopException 2899 */ 2900 public function getOrderedCartRulesIds($filter = CartRule::FILTER_ACTION_ALL) 2901 { 2902 $cacheKey = 'static::getOrderedCartRulesIds_'.$this->id.'-'.$filter.'-ids'; 2903 if (!Cache::isStored($cacheKey)) { 2904 $result = Db::getInstance()->executeS( 2905 (new DbQuery()) 2906 ->select('cr.`id_cart_rule`') 2907 ->from('cart_cart_rule', 'cd') 2908 ->leftJoin('cart_rule', 'cr', 'cd.`id_cart_rule` = cr.`id_cart_rule`') 2909 ->leftJoin('cart_rule_lang', 'crl', 'cd.`id_cart_rule` = crl.`id_cart_rule` AND crl.`id_lang` = '.(int) $this->id_lang) 2910 ->where('cd.`id_cart` = '.(int) $this->id) 2911 ->where($filter === CartRule::FILTER_ACTION_SHIPPING ? 'cr.`free_shipping` = 1' : '') 2912 ->where($filter === CartRule::FILTER_ACTION_GIFT ? 'cr.`gift_product` = 1' : '') 2913 ->where($filter === CartRule::FILTER_ACTION_REDUCTION ? 'cr.`reduction_percent` != 0 OR cr.`reduction_amount` != 0' : '') 2914 ->orderBy('cr.`priority` ASC') 2915 ); 2916 Cache::store($cacheKey, $result); 2917 } else { 2918 $result = Cache::retrieve($cacheKey); 2919 } 2920 2921 return $result; 2922 } 2923 2924 /** 2925 * @param int $idCartRule 2926 * 2927 * @return int|null 2928 * 2929 * @since 1.0.0 2930 * @version 1.0.0 Initial version 2931 * @throws PrestaShopException 2932 */ 2933 public function getDiscountsCustomer($idCartRule) 2934 { 2935 if (!CartRule::isFeatureActive()) { 2936 return 0; 2937 } 2938 $cacheId = 'static::getDiscountsCustomer_'.(int) $this->id.'-'.(int) $idCartRule; 2939 if (!Cache::isStored($cacheId)) { 2940 $result = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 2941 (new DbQuery()) 2942 ->select('COUNT(*)') 2943 ->from('cart_cart_rule') 2944 ->where('`id_cart_rule` = '.(int) $idCartRule) 2945 ->where('`id_cart` = '.(int) $this->id) 2946 ); 2947 Cache::store($cacheId, $result); 2948 2949 return $result; 2950 } 2951 2952 return Cache::retrieve($cacheId); 2953 } 2954 2955 /** 2956 * @return bool|mixed 2957 * 2958 * @throws PrestaShopDatabaseException 2959 * @throws PrestaShopException 2960 * @since 1.0.0 2961 * @version 1.0.0 Initial version 2962 */ 2963 public function getLastProduct() 2964 { 2965 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( 2966 (new DbQuery()) 2967 ->select('`id_product`, `id_product_attribute`, `id_shop`') 2968 ->from('cart_product', 'cp') 2969 ->where('`id_cart` = '.(int) $this->id) 2970 ->orderBy('`date_add` DESC') 2971 ); 2972 if ($result && isset($result['id_product']) && $result['id_product']) { 2973 foreach ($this->getProducts() as $product) { 2974 if ($result['id_product'] == $product['id_product'] 2975 && ( 2976 !$result['id_product_attribute'] 2977 || $result['id_product_attribute'] == $product['id_product_attribute'] 2978 ) 2979 ) { 2980 return $product; 2981 } 2982 } 2983 } 2984 2985 return false; 2986 } 2987 2988 /** 2989 * Return cart products quantity 2990 * 2991 * @result integer Products quantity 2992 * 2993 * @return int 2994 * 2995 * @since 1.0.0 2996 * @version 1.0.0 Initial version 2997 * @throws PrestaShopException 2998 */ 2999 public function nbProducts() 3000 { 3001 if (!$this->id) { 3002 return 0; 3003 } 3004 3005 return static::getNbProducts($this->id); 3006 } 3007 3008 /** 3009 * @param int $id 3010 * 3011 * @return mixed 3012 * 3013 * @since 1.0.0 3014 * @version 1.0.0 Initial version 3015 * @throws PrestaShopException 3016 */ 3017 public static function getNbProducts($id) 3018 { 3019 // Must be strictly compared to NULL, or else an empty cart will bypass the cache and add dozens of queries 3020 if (isset(static::$_nbProducts[$id]) && static::$_nbProducts[$id] !== null) { 3021 return static::$_nbProducts[$id]; 3022 } 3023 3024 static::$_nbProducts[$id] = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 3025 (new DbQuery()) 3026 ->select('SUM(`quantity`)') 3027 ->from('cart_product') 3028 ->where('`id_cart` = '.(int) $id) 3029 ); 3030 3031 return static::$_nbProducts[$id]; 3032 } 3033 3034 /** 3035 * @deprecated 1.0.0, use Cart->addCartRule() 3036 * 3037 * @param int $idCartRule 3038 * 3039 * @return bool 3040 * @throws PrestaShopException 3041 */ 3042 public function addDiscount($idCartRule) 3043 { 3044 Tools::displayAsDeprecated(); 3045 3046 return $this->addCartRule($idCartRule); 3047 } 3048 3049 /** 3050 * @param int $idCartRule 3051 * 3052 * @return bool 3053 * 3054 * @since 1.0.0 3055 * @version 1.0.0 Initial version 3056 * @throws PrestaShopException 3057 */ 3058 public function addCartRule($idCartRule) 3059 { 3060 // You can't add a cart rule that does not exist 3061 $cartRule = new CartRule($idCartRule, Context::getContext()->language->id); 3062 3063 if (!Validate::isLoadedObject($cartRule)) { 3064 return false; 3065 } 3066 3067 if (Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 3068 (new DbQuery()) 3069 ->select('`id_cart_rule`') 3070 ->from('cart_cart_rule') 3071 ->where('`id_cart_rule` = '.(int) $idCartRule) 3072 ->where('`id_cart` = '.(int) $this->id) 3073 )) { 3074 return false; 3075 } 3076 3077 // Add the cart rule to the cart 3078 if (!Db::getInstance()->insert( 3079 'cart_cart_rule', 3080 [ 3081 'id_cart_rule' => (int) $idCartRule, 3082 'id_cart' => (int) $this->id, 3083 ] 3084 ) 3085 ) { 3086 return false; 3087 } 3088 3089 Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_ALL); 3090 Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_SHIPPING); 3091 Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_REDUCTION); 3092 Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_GIFT); 3093 3094 Cache::clean('static::getOrderedCartRulesIds_'.$this->id.'-'.CartRule::FILTER_ACTION_ALL.'-ids'); 3095 Cache::clean('static::getOrderedCartRulesIds_'.$this->id.'-'.CartRule::FILTER_ACTION_SHIPPING.'-ids'); 3096 Cache::clean('static::getOrderedCartRulesIds_'.$this->id.'-'.CartRule::FILTER_ACTION_REDUCTION.'-ids'); 3097 Cache::clean('static::getOrderedCartRulesIds_'.$this->id.'-'.CartRule::FILTER_ACTION_GIFT.'-ids'); 3098 3099 if ((int) $cartRule->gift_product) { 3100 $this->updateQty(1, $cartRule->gift_product, $cartRule->gift_product_attribute, false, 'up', 0, null, false); 3101 } 3102 3103 return true; 3104 } 3105 3106 /** 3107 * Update product quantity 3108 * 3109 * @param int $quantity Quantity to add (or substract) 3110 * @param int $idProduct Product ID 3111 * @param int $idProductAttribute Attribute ID if needed 3112 * @param int|bool $idCustomization 3113 * @param string $operator Indicate if quantity must be increased or decreased 3114 * @param int $idAddressDelivery 3115 * @param Shop $shop 3116 * @param bool $autoAddCartRule 3117 * 3118 * @return bool 3119 * @throws PrestaShopDatabaseException 3120 * @throws PrestaShopException 3121 */ 3122 public function updateQty( 3123 $quantity, 3124 $idProduct, 3125 $idProductAttribute = null, 3126 $idCustomization = false, 3127 $operator = 'up', 3128 $idAddressDelivery = 0, 3129 Shop $shop = null, 3130 $autoAddCartRule = true 3131 ) { 3132 if (!$shop) { 3133 $shop = Context::getContext()->shop; 3134 } 3135 3136 if (Context::getContext()->customer->id) { 3137 if ($idAddressDelivery == 0 && (int) $this->id_address_delivery) { // The $id_address_delivery is null, use the cart delivery address 3138 $idAddressDelivery = $this->id_address_delivery; 3139 } elseif ($idAddressDelivery == 0) { // The $id_address_delivery is null, get the default customer address 3140 $idAddressDelivery = (int) Address::getFirstCustomerAddressId((int) Context::getContext()->customer->id); 3141 } elseif (!Customer::customerHasAddress(Context::getContext()->customer->id, $idAddressDelivery)) { // The $id_address_delivery must be linked with customer 3142 $idAddressDelivery = 0; 3143 } 3144 } 3145 3146 $quantity = (int) $quantity; 3147 $idProduct = (int) $idProduct; 3148 $idProductAttribute = (int) $idProductAttribute; 3149 $product = new Product($idProduct, false, Configuration::get('PS_LANG_DEFAULT'), $shop->id); 3150 3151 if ($idProductAttribute) { 3152 $combination = new Combination((int) $idProductAttribute); 3153 if ($combination->id_product != $idProduct) { 3154 return false; 3155 } 3156 } 3157 3158 /* If we have a product combination, the minimal quantity is set with the one of this combination */ 3159 if (!empty($idProductAttribute)) { 3160 $minimalQuantity = (int) Attribute::getAttributeMinimalQty($idProductAttribute); 3161 } else { 3162 $minimalQuantity = (int) $product->minimal_quantity; 3163 } 3164 3165 if (!Validate::isLoadedObject($product)) { 3166 die(Tools::displayError()); 3167 } 3168 3169 if (isset(static::$_nbProducts[$this->id])) { 3170 unset(static::$_nbProducts[$this->id]); 3171 } 3172 3173 if (isset(static::$_totalWeight[$this->id])) { 3174 unset(static::$_totalWeight[$this->id]); 3175 } 3176 3177 Hook::exec( 3178 'actionBeforeCartUpdateQty', 3179 [ 3180 'cart' => $this, 3181 'product' => $product, 3182 'id_product_attribute' => $idProductAttribute, 3183 'id_customization' => $idCustomization, 3184 'quantity' => $quantity, 3185 'operator' => $operator, 3186 'id_address_delivery' => $idAddressDelivery, 3187 'shop' => $shop, 3188 'auto_add_cart_rule' => $autoAddCartRule, 3189 ] 3190 ); 3191 3192 if ((int) $quantity <= 0) { 3193 return $this->deleteProduct($idProduct, $idProductAttribute, (int) $idCustomization, 0, $autoAddCartRule); 3194 } elseif (!$product->available_for_order || (Configuration::get('PS_CATALOG_MODE') && !defined('_PS_ADMIN_DIR_'))) { 3195 return false; 3196 } else { 3197 /* Check if the product is already in the cart */ 3198 $result = $this->containsProduct($idProduct, $idProductAttribute, (int) $idCustomization, (int) $idAddressDelivery); 3199 3200 /* Update quantity if product already exist */ 3201 if ($result) { 3202 if ($operator == 'up') { 3203 $result2 = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( 3204 (new DbQuery()) 3205 ->select('stock.`out_of_stock`, IFNULL(stock.`quantity`, 0) AS `quantity`') 3206 ->from('product', 'p') 3207 ->join(Product::sqlStock('p', $idProductAttribute, true, $shop)) 3208 ->where('p.`id_product` = '.(int) $idProduct) 3209 ); 3210 $productQty = (int) $result2['quantity']; 3211 // Quantity for product pack 3212 if (Pack::isPack($idProduct)) { 3213 $productQty = Pack::getQuantity($idProduct, $idProductAttribute); 3214 } 3215 $newQty = (int) $result['quantity'] + (int) $quantity; 3216 $qty = '+ '.(int) $quantity; 3217 3218 if (!Product::isAvailableWhenOutOfStock((int) $result2['out_of_stock'])) { 3219 if ($newQty > $productQty) { 3220 return false; 3221 } 3222 } 3223 } elseif ($operator == 'down') { 3224 $qty = '- '.(int) $quantity; 3225 $newQty = (int) $result['quantity'] - (int) $quantity; 3226 if ($newQty < $minimalQuantity && $minimalQuantity > 1) { 3227 return -1; 3228 } 3229 } else { 3230 return false; 3231 } 3232 3233 /* Delete product from cart */ 3234 if ($newQty <= 0) { 3235 return $this->deleteProduct((int) $idProduct, (int) $idProductAttribute, (int) $idCustomization, 0, $autoAddCartRule); 3236 } elseif ($newQty < $minimalQuantity) { 3237 return -1; 3238 } else { 3239 Db::getInstance()->update( 3240 'cart_product', 3241 [ 3242 'quantity' => ['type' => 'sql', 'value' => '`quantity` '.$qty], 3243 'date_add' => ['type' => 'sql', 'value' => 'NOW()'], 3244 ], 3245 '`id_product` = '.(int) $idProduct.(!empty($idProductAttribute) ? ' AND `id_product_attribute` = '.(int) $idProductAttribute : '').' AND `id_cart` = '.(int) $this->id.(Configuration::get('PS_ALLOW_MULTISHIPPING') && $this->isMultiAddressDelivery() ? ' AND `id_address_delivery` = '.(int) $idAddressDelivery : ''), 3246 1 3247 ); 3248 } 3249 } elseif ($operator == 'up') { 3250 /* Add product to the cart */ 3251 $result2 = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( 3252 (new DbQuery()) 3253 ->select('stock.`out_of_stock`, IFNULL(stock.`quantity`, 0) AS `quantity`') 3254 ->from('product', 'p') 3255 ->join(Product::sqlStock('p', $idProductAttribute, true, $shop)) 3256 ->where('p.`id_product` = '.(int) $idProduct) 3257 ); 3258 3259 // Quantity for product pack 3260 if (Pack::isPack($idProduct)) { 3261 $result2['quantity'] = Pack::getQuantity($idProduct, $idProductAttribute); 3262 } 3263 3264 if (!Product::isAvailableWhenOutOfStock((int) $result2['out_of_stock'])) { 3265 if ((int) $quantity > $result2['quantity']) { 3266 return false; 3267 } 3268 } 3269 3270 if ((int) $quantity < $minimalQuantity) { 3271 return -1; 3272 } 3273 3274 $resultAdd = Db::getInstance()->insert( 3275 'cart_product', 3276 [ 3277 'id_product' => (int) $idProduct, 3278 'id_product_attribute' => (int) $idProductAttribute, 3279 'id_cart' => (int) $this->id, 3280 'id_address_delivery' => (int) $idAddressDelivery, 3281 'id_shop' => $shop->id, 3282 'quantity' => (int) $quantity, 3283 'date_add' => date('Y-m-d H:i:s'), 3284 ] 3285 ); 3286 3287 if (!$resultAdd) { 3288 return false; 3289 } 3290 } 3291 } 3292 3293 // refresh cache of static::_products 3294 $this->_products = $this->getProducts(true); 3295 $this->update(); 3296 $context = Context::getContext()->cloneContext(); 3297 $context->cart = $this; 3298 Cache::clean('getContextualValue_*'); 3299 if ($autoAddCartRule) { 3300 CartRule::autoAddToCart($context); 3301 } 3302 3303 if ($product->customizable) { 3304 return $this->_updateCustomizationQuantity((int) $quantity, (int) $idCustomization, (int) $idProduct, (int) $idProductAttribute, (int) $idAddressDelivery, $operator); 3305 } else { 3306 return true; 3307 } 3308 } 3309 3310 /** 3311 * Delete a product from the cart 3312 * 3313 * @param int $idProduct Product ID 3314 * @param int $idProductAttribute Attribute ID if needed 3315 * @param int $idCustomization Customization id 3316 * @param int $idAddressDelivery 3317 * @param bool $autoAddCartRule 3318 * 3319 * @return bool result 3320 * @throws PrestaShopException 3321 * @throws PrestaShopException 3322 * @throws PrestaShopDatabaseException 3323 */ 3324 public function deleteProduct($idProduct, $idProductAttribute = null, $idCustomization = null, $idAddressDelivery = 0, $autoAddCartRule = true) 3325 { 3326 if (isset(static::$_nbProducts[$this->id])) { 3327 unset(static::$_nbProducts[$this->id]); 3328 } 3329 3330 if (isset(static::$_totalWeight[$this->id])) { 3331 unset(static::$_totalWeight[$this->id]); 3332 } 3333 3334 if ((int) $idCustomization) { 3335 $productTotalQuantity = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 3336 (new DbQuery()) 3337 ->select('`quantity`') 3338 ->from('cart_product') 3339 ->where('`id_cart` = '.(int) $this->id) 3340 ->where('`id_product` = '.(int) $idProduct) 3341 ->where('`id_product_attribute` = '.(int) $idProductAttribute) 3342 ); 3343 3344 $customizationQuantity = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 3345 (new DbQuery()) 3346 ->select('`quantity`') 3347 ->from('customization') 3348 ->where('`id_cart` = '.(int) $this->id) 3349 ->where('`id_product` = '.(int) $idProduct) 3350 ->where('`id_product_attribute` = '.(int) $idProductAttribute) 3351 ->where($idAddressDelivery ? '`id_address_delivery` = '.(int) $idAddressDelivery : '') 3352 ); 3353 3354 if (!$this->_deleteCustomization((int) $idCustomization, (int) $idProduct, (int) $idProductAttribute, (int) $idAddressDelivery)) { 3355 return false; 3356 } 3357 3358 // refresh cache of static::_products 3359 $this->_products = $this->getProducts(true); 3360 3361 return ($customizationQuantity == $productTotalQuantity && $this->deleteProduct((int) $idProduct, (int) $idProductAttribute, null, (int) $idAddressDelivery)); 3362 } 3363 3364 /* Get customization quantity */ 3365 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( 3366 (new DbQuery()) 3367 ->select('SUM(`quantity`)') 3368 ->from('customization') 3369 ->where('`id_cart` = '.(int) $this->id) 3370 ->where('`id_product` = '.(int) $idProduct) 3371 ->where('`id_product_attribute` = '.(int) $idProductAttribute) 3372 ); 3373 3374 if ($result === false) { 3375 return false; 3376 } 3377 3378 /* If the product still possesses customization it does not have to be deleted */ 3379 if (Db::getInstance()->NumRows() && isset($result['quantity']) && (int) $result['quantity']) { 3380 return Db::getInstance()->update( 3381 'cart_product', 3382 [ 3383 'quantity' => (int) $result['quantity'], 3384 ], 3385 '`id_cart` = '.(int) $this->id.' AND `id_product` = '.(int) $idProduct.($idProductAttribute != null ? ' AND `id_product_attribute` = '.(int) $idProductAttribute : '') 3386 ); 3387 } 3388 3389 /* Product deletion */ 3390 $result = Db::getInstance()->delete( 3391 'cart_product', 3392 '`id_product` = '.(int) $idProduct.' '.(!is_null($idProductAttribute) ? ' AND `id_product_attribute` = '.(int) $idProductAttribute : '').' AND `id_cart` = '.(int) $this->id.' '.((int) $idAddressDelivery ? 'AND `id_address_delivery` = '.(int) $idAddressDelivery : '') 3393 ); 3394 3395 // Remove any specific price for this cart/product combination 3396 SpecificPrice::deleteByIdCart((int) $this->id, (int) $idProduct, (int) $idProductAttribute); 3397 3398 if ($result) { 3399 $return = $this->update(); 3400 // refresh cache of static::_products 3401 $this->_products = $this->getProducts(true); 3402 CartRule::autoRemoveFromCart(); 3403 if ($autoAddCartRule) { 3404 CartRule::autoAddToCart(); 3405 } 3406 3407 return $return; 3408 } 3409 3410 return false; 3411 } 3412 3413 /** 3414 * Delete a customization from the cart. If customization is a Picture, 3415 * then the image is also deleted 3416 * 3417 * @param int $idCustomization 3418 * 3419 * @return bool result 3420 * 3421 * @deprecated 2.0.0 3422 * @throws PrestaShopDatabaseException 3423 * @throws PrestaShopDatabaseException 3424 * @throws PrestaShopException 3425 */ 3426 // @codingStandardsIgnoreStart 3427 protected function _deleteCustomization($idCustomization, $idProduct, $idProductAttribute, $idAddressDelivery = 0) 3428 { 3429 // @codingStandardsIgnoreEnd 3430 $result = true; 3431 $customization = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( 3432 (new DbQuery()) 3433 ->select('*') 3434 ->from('customization') 3435 ->where('`id_customization` = '.(int) $idCustomization) 3436 ); 3437 3438 if ($customization) { 3439 $custData = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( 3440 (new DbQuery()) 3441 ->select('*') 3442 ->from('customized_data') 3443 ->where('`id_customization` = '.(int) $idCustomization) 3444 ); 3445 3446 // Delete customization picture if necessary 3447 if (isset($custData['type']) && $custData['type'] == 0) { 3448 $result &= (@unlink(_PS_UPLOAD_DIR_.$custData['value']) && @unlink(_PS_UPLOAD_DIR_.$custData['value'].'_small')); 3449 } 3450 3451 $result &= Db::getInstance()->delete('customized_data', '`id_customization` = '.(int) $idCustomization); 3452 3453 if ($result) { 3454 $result &= Db::getInstance()->update( 3455 'cart_product', 3456 [ 3457 'quantity' => ['type' => 'sql', 'value' => '`quantity` - '.(int) $customization['quantity']], 3458 ], 3459 '`id_cart` = '.(int) $this->id.' AND `id_product` = '.(int) $idProduct.((int) $idProductAttribute ? ' AND `id_product_attribute` = '.(int) $idProductAttribute : '').' AND `id_address_delivery` = '.(int) $idAddressDelivery 3460 ); 3461 } 3462 3463 if (!$result) { 3464 return false; 3465 } 3466 3467 return Db::getInstance()->delete('customization', '`id_customization` = '.(int) $idCustomization); 3468 } 3469 3470 return true; 3471 } 3472 3473 /** 3474 * @param int $idProduct 3475 * @param int $idProductAttribute 3476 * @param int $idCustomization 3477 * @param int $idAddressDelivery 3478 * 3479 * @return array|bool|null|object 3480 * 3481 * @throws PrestaShopDatabaseException 3482 * @throws PrestaShopException 3483 * @since 1.0.0 3484 * @version 1.0.0 Initial version 3485 */ 3486 public function containsProduct($idProduct, $idProductAttribute = 0, $idCustomization = 0, $idAddressDelivery = 0) 3487 { 3488 $sql = (new DbQuery()) 3489 ->select('cp.`quantity`') 3490 ->from('cart_product', 'cp'); 3491 3492 if ($idCustomization) { 3493 $sql->leftJoin('customization', 'c', 'c.`id_product` = cp.`id_product`'); 3494 $sql->where('c.`id_product_attribute` = cp.`id_product_attribute`'); 3495 } 3496 3497 $sql->where('cp.`id_product` = '.(int) $idProduct); 3498 $sql->where('cp.`id_product_attribute` = '.(int) $idProductAttribute); 3499 $sql->where('cp.`id_cart` = '.(int) $this->id); 3500 if (Configuration::get('PS_ALLOW_MULTISHIPPING') && $this->isMultiAddressDelivery()) { 3501 $sql->where('cp.`id_address_delivery` = '.(int) $idAddressDelivery); 3502 } 3503 3504 if ($idCustomization) { 3505 $sql->where('c.`id_customization` = '.(int) $idCustomization); 3506 } 3507 3508 return Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql); 3509 } 3510 3511 /** 3512 * @param int $quantityChange Quantity change 3513 * @param int $idCustomization Customization ID 3514 * @param int $idProduct Product ID 3515 * @param int $idProductAttribute Product Attribute ID 3516 * @param int $idAddressDelivery Address ID 3517 * @param string $operator `up` or `down` 3518 * 3519 * @return bool 3520 * 3521 * @deprecated 2.0.0 3522 * @throws PrestaShopException 3523 * @throws PrestaShopDatabaseException 3524 */ 3525 protected function _updateCustomizationQuantity($quantityChange, $idCustomization, $idProduct, $idProductAttribute, $idAddressDelivery, $operator = 'up') 3526 { 3527 // Link customization to product combination when it is first added to cart 3528 if (empty($idCustomization) && $operator === 'up') { 3529 $customization = $this->getProductCustomization($idProduct, null, true); 3530 foreach ($customization as $field) { 3531 if ((int) $field['quantity'] === 0) { 3532 Db::getInstance()->update( 3533 'customization', 3534 [ 3535 'quantity' => (int) $quantityChange, 3536 'id_product' => (int) $idProduct, 3537 'id_product_attribute' => (int) $idProductAttribute, 3538 'id_address_delivery' => (int) $idAddressDelivery, 3539 'in_cart' => true, 3540 ], 3541 '`id_customization` = '.(int) $field['id_customization'] 3542 ); 3543 } 3544 } 3545 } 3546 3547 /* Quantity update */ 3548 if (!empty($idCustomization)) { 3549 $result = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 3550 (new DbQuery()) 3551 ->select('`quantity`') 3552 ->from('customization') 3553 ->where('`id_customization` = '.(int) $idCustomization) 3554 ); 3555 3556 if ($operator === 'down' && ((int) $result - (int) $quantityChange) < 1) { 3557 return Db::getInstance()->delete('customization', '`id_customization` = '.(int) $idCustomization); 3558 } 3559 3560 return Db::getInstance()->update( 3561 'customization', 3562 [ 3563 'quantity' => ['type' => 'sql', 'value' => '`quantity` '.($operator === 'up' ? '+' : '-').(int) $quantityChange], 3564 'id_address_delivery' => (int) $idAddressDelivery, 3565 'in_cart' => true, 3566 ], 3567 '`id_customization` = '.(int) $idCustomization 3568 ); 3569 } 3570 // refresh cache of static::_products 3571 $this->_products = $this->getProducts(true); 3572 $this->update(); 3573 3574 return true; 3575 } 3576 3577 /** 3578 * Return custom pictures in this cart for a specified product 3579 * 3580 * @param int $idProduct 3581 * @param int $type only return customization of this type 3582 * @param bool $notInCart only return customizations that are not in cart already 3583 * 3584 * @return array result rows 3585 * 3586 * @throws PrestaShopDatabaseException 3587 * @throws PrestaShopException 3588 * @since 1.0.0 3589 * @version 1.0.0 Initial version 3590 */ 3591 public function getProductCustomization($idProduct, $type = null, $notInCart = false) 3592 { 3593 if (!Customization::isFeatureActive()) { 3594 return []; 3595 } 3596 3597 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 3598 (new DbQuery()) 3599 ->select('cu.`id_customization`, cd.`index`, cd.`value`, cd.`type`, cu.`in_cart`, cu.`quantity`') 3600 ->from('customization', 'cu') 3601 ->leftJoin('customized_data', 'cd', 'cu.`id_customization` = cd.`id_customization`') 3602 ->where('cu.`id_cart` = '.(int) $this->id) 3603 ->where('cu.`id_product` = '.(int) $idProduct) 3604 ->where($type === Product::CUSTOMIZE_FILE ? 'cd.`type` = '.(int) Product::CUSTOMIZE_FILE : '') 3605 ->where($type === Product::CUSTOMIZE_TEXTFIELD ? 'cd.`type` = '.(int) Product::CUSTOMIZE_TEXTFIELD : '') 3606 ->where($notInCart ? 'cu.`in_cart` = 0' : '') 3607 ); 3608 3609 return $result; 3610 } 3611 3612 /** 3613 * @deprecated 1.0.0, use Cart->removeCartRule() 3614 * 3615 * @param int $idCartRule 3616 * 3617 * @return bool 3618 * @throws PrestaShopDatabaseException 3619 * @throws PrestaShopException 3620 */ 3621 public function deleteDiscount($idCartRule) 3622 { 3623 Tools::displayAsDeprecated(); 3624 3625 return $this->removeCartRule($idCartRule); 3626 } 3627 3628 /** 3629 * @param int $idCartRule 3630 * 3631 * @return bool 3632 * 3633 * @since 1.0.0 3634 * @version 1.0.0 Initial version 3635 * @throws PrestaShopDatabaseException 3636 * @throws PrestaShopException 3637 */ 3638 public function removeCartRule($idCartRule) 3639 { 3640 Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_ALL); 3641 Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_SHIPPING); 3642 Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_REDUCTION); 3643 Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_GIFT); 3644 3645 Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_ALL.'-ids'); 3646 Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_SHIPPING.'-ids'); 3647 Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_REDUCTION.'-ids'); 3648 Cache::clean('static::getCartRules_'.$this->id.'-'.CartRule::FILTER_ACTION_GIFT.'-ids'); 3649 3650 $result = Db::getInstance()->delete('cart_cart_rule', '`id_cart_rule` = '.(int) $idCartRule.' AND `id_cart` = '.(int) $this->id, 1); 3651 3652 $cartRule = new CartRule($idCartRule, Configuration::get('PS_LANG_DEFAULT')); 3653 if ((int) $cartRule->gift_product) { 3654 $this->updateQty(1, $cartRule->gift_product, $cartRule->gift_product_attribute, null, 'down', 0, null, false); 3655 } 3656 3657 return $result; 3658 } 3659 3660 /** 3661 * Get the number of packages 3662 * 3663 * @return int number of packages 3664 * @throws PrestaShopDatabaseException 3665 * @throws PrestaShopException 3666 */ 3667 public function getNbOfPackages() 3668 { 3669 static $nbPackages = []; 3670 3671 if (!isset($nbPackages[$this->id])) { 3672 $nbPackages[$this->id] = 0; 3673 foreach ($this->getPackageList() as $byAddress) { 3674 $nbPackages[$this->id] += count($byAddress); 3675 } 3676 } 3677 3678 return $nbPackages[$this->id]; 3679 } 3680 3681 /** 3682 * @param array $package 3683 * @param int|null $idCarrier 3684 * 3685 * @return int 3686 * 3687 * @throws PrestaShopDatabaseException 3688 * @throws PrestaShopException 3689 * @since 1.0.0 3690 * @version 1.0.0 Initial version 3691 */ 3692 public function getPackageIdWarehouse($package, $idCarrier = null) 3693 { 3694 if ($idCarrier === null) { 3695 if (isset($package['id_carrier'])) { 3696 $idCarrier = (int) $package['id_carrier']; 3697 } 3698 } 3699 3700 if ($idCarrier == null) { 3701 return $package['id_warehouse']; 3702 } 3703 3704 foreach ($package['warehouse_list'] as $idWarehouse) { 3705 $warehouse = new Warehouse((int) $idWarehouse); 3706 $availableWarehouseCarriers = $warehouse->getCarriers(); 3707 if (in_array($idCarrier, $availableWarehouseCarriers)) { 3708 return (int) $idWarehouse; 3709 } 3710 } 3711 3712 return 0; 3713 } 3714 3715 /** 3716 * @param int $idCarrier 3717 * @param int $idAddress 3718 * 3719 * @return bool 3720 * @throws Adapter_Exception 3721 * @throws PrestaShopDatabaseException 3722 * @throws PrestaShopException 3723 */ 3724 public function carrierIsSelected($idCarrier, $idAddress) 3725 { 3726 $deliveryOption = $this->getDeliveryOption(); 3727 $deliveryOptionList = $this->getDeliveryOptionList(); 3728 3729 if (!isset($deliveryOption[$idAddress])) { 3730 return false; 3731 } 3732 3733 if (!isset($deliveryOptionList[$idAddress][$deliveryOption[$idAddress]])) { 3734 return false; 3735 } 3736 3737 if (!in_array($idCarrier, array_keys($deliveryOptionList[$idAddress][$deliveryOption[$idAddress]]['carrier_list']))) { 3738 return false; 3739 } 3740 3741 return true; 3742 } 3743 3744 /** 3745 * Get all deliveries options available for the current cart formated like Carriers::getCarriersForOrder 3746 * This method was wrote for retrocompatibility with 1.4 theme 3747 * New theme need to use static::getDeliveryOptionList() to generate carriers option in the checkout process 3748 * 3749 * @param Country $defaultCountry 3750 * @param bool $flush Force flushing cache 3751 * 3752 * @return array 3753 * 3754 * @throws Adapter_Exception 3755 * @throws PrestaShopDatabaseException 3756 * @throws PrestaShopException 3757 * @since 1.0.0 3758 * @version 1.0.0 Initial version 3759 */ 3760 public function simulateCarriersOutput(Country $defaultCountry = null, $flush = false) 3761 { 3762 $deliveryOptionList = $this->getDeliveryOptionList($defaultCountry, $flush); 3763 3764 // This method cannot work if there is multiple address delivery 3765 if (count($deliveryOptionList) > 1 || empty($deliveryOptionList)) { 3766 return []; 3767 } 3768 3769 $carriers = []; 3770 foreach (reset($deliveryOptionList) as $key => $option) { 3771 $price = $option['total_price_with_tax']; 3772 $priceTaxExcluded = $option['total_price_without_tax']; 3773 $name = $img = $delay = ''; 3774 3775 if ($option['unique_carrier']) { 3776 $carrier = reset($option['carrier_list']); 3777 if (isset($carrier['instance'])) { 3778 $name = $carrier['instance']->name; 3779 $delay = $carrier['instance']->delay; 3780 $delay = isset($delay[Context::getContext()->language->id]) ? 3781 $delay[Context::getContext()->language->id] : $delay[(int) Configuration::get('PS_LANG_DEFAULT')]; 3782 } 3783 if (isset($carrier['logo'])) { 3784 $img = $carrier['logo']; 3785 } 3786 } else { 3787 $nameList = []; 3788 foreach ($option['carrier_list'] as $carrier) { 3789 $nameList[] = $carrier['instance']->name; 3790 } 3791 $name = join(' -', $nameList); 3792 $img = ''; // No images if multiple carriers 3793 $delay = ''; 3794 } 3795 $carriers[] = [ 3796 'name' => $name, 3797 'img' => $img, 3798 'delay' => $delay, 3799 'price' => $price, 3800 'price_tax_exc' => $priceTaxExcluded, 3801 'id_carrier' => static::intifier($key), // Need to translate to an integer for retrocompatibility reason, in 1.4 template we used intval 3802 'is_module' => false, 3803 ]; 3804 } 3805 3806 return $carriers; 3807 } 3808 3809 /** 3810 * Translate a string option_delivery identifier ('24,3,') in a int (3240002000) 3811 * 3812 * The option_delivery identifier is a list of integers separated by a ','. 3813 * This method replace the delimiter by a sequence of '0'. 3814 * The size of this sequence is fixed by the first digit of the return 3815 * 3816 * @param string $string 3817 * @param string $delimiter 3818 * 3819 * @return int 3820 * 3821 * @since 1.0.0 3822 * @version 1.0.0 Initial version 3823 */ 3824 public static function intifier($string, $delimiter = ',') 3825 { 3826 $elm = explode($delimiter, $string); 3827 $max = max($elm); 3828 3829 return strlen($max).implode(str_repeat('0', strlen($max) + 1), $elm); 3830 } 3831 3832 /** 3833 * @param bool $useCache 3834 * 3835 * @return int 3836 * 3837 * @throws PrestaShopDatabaseException 3838 * @throws PrestaShopException 3839 * @since 1.0.0 3840 * @version 1.0.0 Initial version 3841 */ 3842 public function simulateCarrierSelectedOutput($useCache = true) 3843 { 3844 $deliveryOption = $this->getDeliveryOption(null, false, $useCache); 3845 3846 if (count($deliveryOption) > 1 || empty($deliveryOption)) { 3847 return 0; 3848 } 3849 3850 return static::intifier(reset($deliveryOption)); 3851 } 3852 3853 /** 3854 * Return shipping total of a specific carriers for the cart 3855 * 3856 * @param int $idCarrier 3857 * @param bool $useTax 3858 * @param Country|null $defaultCountry 3859 * @param array|null $deliveryOption Array of the delivery option for each address 3860 * 3861 * @return float Shipping total 3862 * 3863 * @throws Adapter_Exception 3864 * @throws PrestaShopDatabaseException 3865 * @throws PrestaShopException 3866 * @since 1.0.0 3867 * @version 1.0.0 Initial version 3868 */ 3869 public function getCarrierCost($idCarrier, $useTax = true, Country $defaultCountry = null, $deliveryOption = null) 3870 { 3871 if (is_null($deliveryOption)) { 3872 $deliveryOption = $this->getDeliveryOption($defaultCountry); 3873 } 3874 3875 $totalShipping = 0; 3876 $deliveryOptionList = $this->getDeliveryOptionList(); 3877 3878 foreach ($deliveryOption as $idAddress => $key) { 3879 if (!isset($deliveryOptionList[$idAddress]) || !isset($deliveryOptionList[$idAddress][$key])) { 3880 continue; 3881 } 3882 if (isset($deliveryOptionList[$idAddress][$key]['carrier_list'][$idCarrier])) { 3883 if ($useTax) { 3884 $totalShipping += $deliveryOptionList[$idAddress][$key]['carrier_list'][$idCarrier]['price_with_tax']; 3885 } else { 3886 $totalShipping += $deliveryOptionList[$idAddress][$key]['carrier_list'][$idCarrier]['price_without_tax']; 3887 } 3888 } 3889 } 3890 3891 return $totalShipping; 3892 } 3893 3894 /** 3895 * @deprecated 1.0.0, use static::getPackageShippingCost 3896 * 3897 * @param int|null $idCarrier 3898 * @param bool $useTax 3899 * @param Country|null $defaultCountry 3900 * @param array|null $productList 3901 * 3902 * @return bool|float 3903 * @throws Adapter_Exception 3904 * @throws PrestaShopException 3905 */ 3906 public function getOrderShippingCost($idCarrier = null, $useTax = true, Country $defaultCountry = null, $productList = null) 3907 { 3908 Tools::displayAsDeprecated(); 3909 3910 return $this->getPackageShippingCost((int) $idCarrier, $useTax, $defaultCountry, $productList); 3911 } 3912 3913 /** 3914 * @deprecated 1.0.0 3915 * 3916 * @param CartRule $obj 3917 * @param mixed $discounts 3918 * @param mixed $orderTotal 3919 * @param mixed $products 3920 * @param bool $checkCartDiscount 3921 * 3922 * @return bool|string 3923 * @throws PrestaShopException 3924 */ 3925 public function checkDiscountValidity($obj, $discounts, $orderTotal, $products, $checkCartDiscount = false) 3926 { 3927 Tools::displayAsDeprecated(); 3928 $context = Context::getContext()->cloneContext(); 3929 $context->cart = $this; 3930 3931 return $obj->checkValidity($context); 3932 } 3933 3934 /** 3935 * Return useful informations for cart 3936 * 3937 * @param int|null $idLang 3938 * @param bool $refresh 3939 * 3940 * @return array Cart details 3941 * 3942 * @throws Adapter_Exception 3943 * @throws PrestaShopDatabaseException 3944 * @throws PrestaShopException 3945 * @since 1.0.0 3946 * @version 1.0.0 Initial version 3947 */ 3948 public function getSummaryDetails($idLang = null, $refresh = false) 3949 { 3950 $context = Context::getContext(); 3951 if (!$idLang) { 3952 $idLang = $context->language->id; 3953 } 3954 3955 $delivery = new Address((int) $this->id_address_delivery); 3956 $invoice = new Address((int) $this->id_address_invoice); 3957 3958 // New layout system with personalization fields 3959 $formattedAddresses = [ 3960 'delivery' => AddressFormat::getFormattedLayoutData($delivery), 3961 'invoice' => AddressFormat::getFormattedLayoutData($invoice), 3962 ]; 3963 3964 $baseTotalTaxInc = $this->getOrderTotal(true); 3965 $baseTotalTaxExc = $this->getOrderTotal(false); 3966 3967 $totalTax = $baseTotalTaxInc - $baseTotalTaxExc; 3968 3969 if ($totalTax < 0) { 3970 $totalTax = 0; 3971 } 3972 3973 $currency = new Currency($this->id_currency); 3974 3975 $products = $this->getProducts($refresh); 3976 3977 foreach ($products as $key => &$product) { 3978 $product['price_without_quantity_discount'] = Product::getPriceStatic( 3979 $product['id_product'], 3980 !Product::getTaxCalculationMethod(), 3981 $product['id_product_attribute'], 3982 _TB_PRICE_DATABASE_PRECISION_, 3983 null, 3984 false, 3985 false 3986 ); 3987 3988 if ($product['reduction_type'] == 'amount') { 3989 $reduction = (!Product::getTaxCalculationMethod() ? (float) $product['price_wt'] : (float) $product['price']) - (float) $product['price_without_quantity_discount']; 3990 $product['reduction_formatted'] = Tools::displayPrice($reduction); 3991 } 3992 } 3993 3994 $giftProducts = []; 3995 $cartRules = $this->getCartRules(); 3996 $totalShipping = $this->getTotalShippingCost(); 3997 $totalShippingTaxExc = $this->getTotalShippingCost(null, false); 3998 $totalProductsWt = $this->getOrderTotal(true, static::ONLY_PRODUCTS); 3999 $totalProducts = $this->getOrderTotal(false, static::ONLY_PRODUCTS); 4000 $totalDiscounts = $this->getOrderTotal(true, static::ONLY_DISCOUNTS); 4001 $totalDiscountsTaxExc = $this->getOrderTotal(false, static::ONLY_DISCOUNTS); 4002 4003 // The cart content is altered for display 4004 $decimals = 0; 4005 if ($currency->decimals) { 4006 $decimals = Configuration::get('PS_PRICE_DISPLAY_PRECISION'); 4007 } 4008 foreach ($cartRules as &$cartRule) { 4009 // If the cart rule is automatic (wihtout any code) and include free shipping, it should not be displayed as a cart rule but only set the shipping cost to 0 4010 if ($cartRule['free_shipping'] && (empty($cartRule['code']) || preg_match('/^'.CartRule::BO_ORDER_CODE_PREFIX.'[0-9]+/', $cartRule['code']))) { 4011 $cartRule['value_real'] -= $totalShipping; 4012 $cartRule['value_tax_exc'] -= $totalShippingTaxExc; 4013 $cartRule['value_real'] = Tools::ps_round( 4014 $cartRule['value_real'], 4015 $decimals 4016 ); 4017 $cartRule['value_tax_exc'] = Tools::ps_round( 4018 $cartRule['value_tax_exc'], 4019 $decimals 4020 ); 4021 if ($totalDiscounts > $cartRule['value_real']) { 4022 $totalDiscounts -= $totalShipping; 4023 } 4024 if ($totalDiscountsTaxExc > $cartRule['value_tax_exc']) { 4025 $totalDiscountsTaxExc -= $totalShippingTaxExc; 4026 } 4027 4028 // Update total shipping 4029 $totalShipping = 0; 4030 $totalShippingTaxExc = 0; 4031 } 4032 4033 if ($cartRule['gift_product']) { 4034 foreach ($products as $key => &$product) { 4035 if (empty($product['gift']) && $product['id_product'] == $cartRule['gift_product'] && $product['id_product_attribute'] == $cartRule['gift_product_attribute']) { 4036 // Update total products 4037 $totalProductsWt = Tools::ps_round( 4038 $totalProductsWt - $product['price_wt'], 4039 $decimals 4040 ); 4041 $totalProducts = Tools::ps_round( 4042 $totalProducts - $product['price'], 4043 $decimals 4044 ); 4045 4046 // Update total discounts 4047 $totalDiscounts = $totalDiscounts - $product['price_wt']; 4048 $totalDiscountsTaxExc = $totalDiscountsTaxExc - $product['price']; 4049 4050 // Update cart rule value 4051 $cartRule['value_real'] = Tools::ps_round( 4052 $cartRule['value_real'] - $product['price_wt'], 4053 $decimals 4054 ); 4055 $cartRule['value_tax_exc'] = Tools::ps_round( 4056 $cartRule['value_tax_exc'] - $product['price'], 4057 $decimals 4058 ); 4059 4060 // Update product quantity 4061 $product['total_wt'] = Tools::ps_round( 4062 $product['total_wt'] - $product['price_wt'], 4063 $decimals 4064 ); 4065 $product['total'] = Tools::ps_round( 4066 $product['total'] - $product['price'], 4067 $decimals 4068 ); 4069 $product['cart_quantity']--; 4070 4071 if (!$product['cart_quantity']) { 4072 unset($products[$key]); 4073 } 4074 4075 // Add a new product line 4076 $giftProduct = $product; 4077 $giftProduct['cart_quantity'] = 1; 4078 $giftProduct['price'] = 0; 4079 $giftProduct['price_wt'] = 0; 4080 $giftProduct['total_wt'] = 0; 4081 $giftProduct['total'] = 0; 4082 $giftProduct['gift'] = true; 4083 $giftProducts[] = $giftProduct; 4084 4085 break; // One gift product per cart rule 4086 } 4087 } 4088 } 4089 } 4090 4091 foreach ($cartRules as $key => &$cartRule) { 4092 if (((float) $cartRule['value_real'] == 0 && (int) $cartRule['free_shipping'] == 0)) { 4093 unset($cartRules[$key]); 4094 } 4095 } 4096 4097 $summary = [ 4098 'delivery' => $delivery, 4099 'delivery_state' => State::getNameById($delivery->id_state), 4100 'invoice' => $invoice, 4101 'invoice_state' => State::getNameById($invoice->id_state), 4102 'formattedAddresses' => $formattedAddresses, 4103 'products' => array_values($products), 4104 'gift_products' => $giftProducts, 4105 'discounts' => array_values($cartRules), 4106 'is_virtual_cart' => (int) $this->isVirtualCart(), 4107 'total_discounts' => $totalDiscounts, 4108 'total_discounts_tax_exc' => $totalDiscountsTaxExc, 4109 'total_wrapping' => $this->getOrderTotal(true, static::ONLY_WRAPPING), 4110 'total_wrapping_tax_exc' => $this->getOrderTotal(false, static::ONLY_WRAPPING), 4111 'total_shipping' => $totalShipping, 4112 'total_shipping_tax_exc' => $totalShippingTaxExc, 4113 'total_products_wt' => $totalProductsWt, 4114 'total_products' => $totalProducts, 4115 'total_price' => $baseTotalTaxInc, 4116 'total_tax' => $totalTax, 4117 'total_price_without_tax' => $baseTotalTaxExc, 4118 'is_multi_address_delivery' => $this->isMultiAddressDelivery() || ((int) Tools::getValue('multi-shipping') == 1), 4119 'free_ship' => !$totalShipping && !count($this->getDeliveryAddressesWithoutCarriers(true, $errors)), 4120 'carrier' => new Carrier($this->id_carrier, $idLang), 4121 ]; 4122 4123 $hook = Hook::exec('actionCartSummary', $summary, null, true); 4124 if (is_array($hook)) { 4125 $summary = array_merge($summary, array_shift($hook)); 4126 } 4127 4128 return $summary; 4129 } 4130 4131 /** 4132 * Get all the ids of the delivery addresses without carriers 4133 * 4134 * @param bool $returnCollection Return a collection 4135 * @param array $error contains an error message if an error occurs 4136 * 4137 * @return array Array of address id or of address object 4138 * @throws PrestaShopException 4139 */ 4140 public function getDeliveryAddressesWithoutCarriers($returnCollection = false, &$error = []) 4141 { 4142 $addressesWithoutCarriers = []; 4143 foreach ($this->getProducts() as $product) { 4144 if (!in_array($product['id_address_delivery'], $addressesWithoutCarriers) 4145 && !count(Carrier::getAvailableCarrierList(new Product($product['id_product']), null, $product['id_address_delivery'], null, null, $error)) 4146 ) { 4147 $addressesWithoutCarriers[] = $product['id_address_delivery']; 4148 } 4149 } 4150 if (!$returnCollection) { 4151 return $addressesWithoutCarriers; 4152 } else { 4153 $addressesInstanceWithoutCarriers = []; 4154 foreach ($addressesWithoutCarriers as $idAddress) { 4155 $addressesInstanceWithoutCarriers[] = new Address($idAddress); 4156 } 4157 4158 return $addressesInstanceWithoutCarriers; 4159 } 4160 } 4161 4162 /** 4163 * @param bool $returnProduct 4164 * 4165 * @return bool|mixed 4166 * 4167 * @throws PrestaShopDatabaseException 4168 * @throws PrestaShopException 4169 * @since 1.0.0 4170 * @version 1.0.0 Initial version 4171 */ 4172 public function checkQuantities($returnProduct = false) 4173 { 4174 if (Configuration::get('PS_CATALOG_MODE') && !defined('_PS_ADMIN_DIR_')) { 4175 return false; 4176 } 4177 4178 foreach ($this->getProducts() as $product) { 4179 if (!$this->allow_seperated_package && !$product['allow_oosp'] && StockAvailable::dependsOnStock($product['id_product']) && 4180 $product['advanced_stock_management'] && (bool) Context::getContext()->customer->isLogged() && ($delivery = $this->getDeliveryOption()) && !empty($delivery) 4181 ) { 4182 $product['stock_quantity'] = StockManager::getStockByCarrier((int) $product['id_product'], (int) $product['id_product_attribute'], $delivery); 4183 } 4184 if (!$product['active'] || !$product['available_for_order'] 4185 || (!$product['allow_oosp'] && $product['stock_quantity'] < $product['cart_quantity']) 4186 ) { 4187 return $returnProduct ? $product : false; 4188 } 4189 } 4190 4191 return true; 4192 } 4193 4194 /** 4195 * @return bool 4196 * 4197 * @since 1.0.0 4198 * @version 1.0.0 Initial version 4199 * @throws PrestaShopException 4200 */ 4201 public function checkProductsAccess() 4202 { 4203 if (Configuration::get('PS_CATALOG_MODE')) { 4204 return true; 4205 } 4206 4207 foreach ($this->getProducts() as $product) { 4208 if (!Product::checkAccessStatic($product['id_product'], $this->id_customer)) { 4209 return $product['id_product']; 4210 } 4211 } 4212 4213 return false; 4214 } 4215 4216 /** 4217 * Add customer's text 4218 * 4219 * @param int $idProduct 4220 * @param int $index 4221 * @param int $type 4222 * @param string $textValue 4223 * 4224 * @return bool Always true 4225 * 4226 * @since 1.0.0 4227 * @version 1.0.0 Initial version 4228 * @throws PrestaShopDatabaseException 4229 */ 4230 public function addTextFieldToProduct($idProduct, $index, $type, $textValue) 4231 { 4232 return $this->_addCustomization($idProduct, 0, $index, $type, $textValue, 0); 4233 } 4234 4235 /** 4236 * Add customization item to database 4237 * 4238 * @param int $idProduct 4239 * @param int $idProductAttribute 4240 * @param int $index 4241 * @param int $type 4242 * @param string $field 4243 * @param int $quantity 4244 * 4245 * @return bool success 4246 * 4247 * @since 1.0.0 4248 * @version 1.0.0 4249 * @throws PrestaShopDatabaseException 4250 * @throws PrestaShopException 4251 */ 4252 // @codingStandardsIgnoreStart 4253 public function _addCustomization($idProduct, $idProductAttribute, $index, $type, $field, $quantity) 4254 { 4255 // @codingStandardsIgnoreEnd 4256 $exisingCustomization = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 4257 (new DbQuery()) 4258 ->select('cu.`id_customization`, cd.`index`, cd.`value`, cd.`type`') 4259 ->from('customization', 'cu') 4260 ->leftJoin('customized_data', 'cd', 'cu.`id_customization` = cd.`id_customization`') 4261 ->where('cu.`id_cart` = '.(int) $this->id) 4262 ->where('cu.`id_product` = '.(int) $idProduct) 4263 ->where('`in_cart` = 0') 4264 ); 4265 4266 if ($exisingCustomization) { 4267 // If the customization field is already filled, delete it 4268 foreach ($exisingCustomization as $customization) { 4269 if ($customization['type'] == $type && $customization['index'] == $index) { 4270 Db::getInstance()->delete( 4271 'customized_data', 4272 'id_customization = '.(int) $customization['id_customization'].' AND type = '.(int) $customization['type'].' AND `index` = '.(int) $customization['index'] 4273 4274 ); 4275 if ($type == Product::CUSTOMIZE_FILE) { 4276 @unlink(_PS_UPLOAD_DIR_.$customization['value']); 4277 @unlink(_PS_UPLOAD_DIR_.$customization['value'].'_small'); 4278 } 4279 break; 4280 } 4281 } 4282 $idCustomization = $exisingCustomization[0]['id_customization']; 4283 } else { 4284 Db::getInstance()->insert( 4285 'customization', 4286 [ 4287 'id_cart' => (int) $this->id, 4288 'id_product' => (int) $idProduct, 4289 'id_product_attribute' => (int) $idProductAttribute, 4290 'quantity' => (int) $quantity, 4291 ] 4292 ); 4293 $idCustomization = Db::getInstance()->Insert_ID(); 4294 } 4295 4296 if (!Db::getInstance()->insert( 4297 'customized_data', 4298 [ 4299 'id_customization' => (int) $idCustomization, 4300 'type' => (int) $type, 4301 'index' => (int) $index, 4302 'value' => pSQL($field), 4303 ] 4304 )) { 4305 return false; 4306 } 4307 4308 return true; 4309 } 4310 4311 /** 4312 * Add customer's pictures 4313 * 4314 * @param int $idProduct 4315 * @param int $index 4316 * @param int $type 4317 * @param string $file 4318 * 4319 * @return bool Always true 4320 * 4321 * @throws PrestaShopDatabaseException 4322 * @throws PrestaShopException 4323 * @since 1.0.0 4324 * @version 1.0.0 4325 */ 4326 public function addPictureToProduct($idProduct, $index, $type, $file) 4327 { 4328 return $this->_addCustomization($idProduct, 0, $index, $type, $file, 0); 4329 } 4330 4331 /** 4332 * @deprecated 1.0.0 4333 * 4334 * @param int $idProduct 4335 * @param int $index 4336 * 4337 * @return bool 4338 * @throws PrestaShopDatabaseException 4339 */ 4340 public function deletePictureToProduct($idProduct, $index) 4341 { 4342 Tools::displayAsDeprecated(); 4343 4344 return $this->deleteCustomizationToProduct($idProduct, 0); 4345 } 4346 4347 /** 4348 * Remove a customer's customization 4349 * 4350 * @param int $idProduct 4351 * @param int $index 4352 * 4353 * @return bool 4354 * @throws PrestaShopDatabaseException 4355 * @throws PrestaShopException 4356 */ 4357 public function deleteCustomizationToProduct($idProduct, $index) 4358 { 4359 $result = true; 4360 4361 $custData = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( 4362 (new DbQuery()) 4363 ->select('cu.`id_customization`, cd.`index`, cd.`value`, cd.`type`') 4364 ->from('customization', 'cu') 4365 ->leftJoin('customized_data', 'cd', 'cu.`id_customization` = cd.`id_customization`') 4366 ->where('cu.`id_cart` = '.(int) $this->id) 4367 ->where('cu.`id_product` = '.(int) $idProduct) 4368 ->where('`index` = '.(int) $index) 4369 ->where('`in_cart` = 0') 4370 ); 4371 4372 // Delete customization picture if necessary 4373 if ($custData['type'] == 0) { 4374 $result &= (@unlink(_PS_UPLOAD_DIR_.$custData['value']) && @unlink(_PS_UPLOAD_DIR_.$custData['value'].'_small')); 4375 } 4376 4377 $result &= Db::getInstance()->delete('customized_data', '`id_customization` = '.(int) $custData['id_customization'].' AND `index` = '.(int) $index); 4378 4379 return $result; 4380 } 4381 4382 /** 4383 * @return false|array 4384 * 4385 * @throws PrestaShopDatabaseException 4386 * @throws PrestaShopException 4387 * @since 1.0.0 4388 * @version 1.0.0 4389 */ 4390 public function duplicate() 4391 { 4392 if (!Validate::isLoadedObject($this)) { 4393 return false; 4394 } 4395 4396 $cart = new Cart($this->id); 4397 $cart->id = null; 4398 $cart->id_shop = $this->id_shop; 4399 $cart->id_shop_group = $this->id_shop_group; 4400 4401 if (!Customer::customerHasAddress((int) $cart->id_customer, (int) $cart->id_address_delivery)) { 4402 $cart->id_address_delivery = (int) Address::getFirstCustomerAddressId((int) $cart->id_customer); 4403 } 4404 4405 if (!Customer::customerHasAddress((int) $cart->id_customer, (int) $cart->id_address_invoice)) { 4406 $cart->id_address_invoice = (int) Address::getFirstCustomerAddressId((int) $cart->id_customer); 4407 } 4408 4409 if ($cart->id_customer) { 4410 $cart->secure_key = static::$_customer->secure_key; 4411 } 4412 4413 $cart->add(); 4414 4415 if (!Validate::isLoadedObject($cart)) { 4416 return false; 4417 } 4418 4419 $success = true; 4420 $products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 4421 (new DbQuery()) 4422 ->select('*') 4423 ->from('cart_product') 4424 ->where('`id_cart` = '.(int) $this->id) 4425 ); 4426 4427 $productGift = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 4428 (new DbQuery()) 4429 ->select('cr.`gift_product`, cr.`gift_product_attribute`') 4430 ->from('cart_rule', 'cr') 4431 ->leftJoin('order_cart_rule', 'ocr', 'ocr.`id_cart_rule` = cr.`id_cart_rule`') 4432 ->where('ocr.`id_order` = '.(int) $this->id) 4433 ); 4434 4435 $idAddressDelivery = Configuration::get('PS_ALLOW_MULTISHIPPING') ? $cart->id_address_delivery : 0; 4436 4437 foreach ($products as $product) { 4438 if ($idAddressDelivery) { 4439 if (Customer::customerHasAddress((int) $cart->id_customer, $product['id_address_delivery'])) { 4440 $idAddressDelivery = $product['id_address_delivery']; 4441 } 4442 } 4443 4444 foreach ($productGift as $gift) { 4445 if (isset($gift['gift_product']) && isset($gift['gift_product_attribute']) && (int) $gift['gift_product'] == (int) $product['id_product'] && (int) $gift['gift_product_attribute'] == (int) $product['id_product_attribute']) { 4446 $product['quantity'] = (int) $product['quantity'] - 1; 4447 } 4448 } 4449 4450 $success &= $cart->updateQty( 4451 (int) $product['quantity'], 4452 (int) $product['id_product'], 4453 (int) $product['id_product_attribute'], 4454 null, 4455 'up', 4456 (int) $idAddressDelivery, 4457 new Shop((int) $cart->id_shop), 4458 false 4459 ); 4460 } 4461 4462 // Customized products 4463 $customs = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 4464 (new DbQuery()) 4465 ->select('*') 4466 ->from('customization', 'c') 4467 ->leftJoin('customized_data', 'cd', 'cd.`id_customization` = c.`id_customization`') 4468 ->where('c.`id_cart` = '.(int) $this->id) 4469 ); 4470 4471 // Get datas from customization table 4472 $customsById = []; 4473 foreach ($customs as $custom) { 4474 if (!isset($customsById[$custom['id_customization']])) { 4475 $customsById[$custom['id_customization']] = [ 4476 'id_product_attribute' => $custom['id_product_attribute'], 4477 'id_product' => $custom['id_product'], 4478 'quantity' => $custom['quantity'], 4479 ]; 4480 } 4481 } 4482 4483 // Insert new customizations 4484 $customIds = []; 4485 foreach ($customsById as $customizationId => $val) { 4486 Db::getInstance()->insert( 4487 'customization', 4488 [ 4489 'id_cart' => (int) $cart->id, 4490 'id_product_attribute' => (int) $val['id_product_attribute'], 4491 'id_product' => (int) $val['id_product'], 4492 'id_address_delivery' => (int) $idAddressDelivery, 4493 'quantity' => (int) $val['quantity'], 4494 'quantity_refunded' => 0, 4495 'quantity_returned' => 0, 4496 'in_cart' => 1, 4497 ] 4498 ); 4499 $customIds[$customizationId] = Db::getInstance(_PS_USE_SQL_SLAVE_)->Insert_ID(); 4500 } 4501 4502 // Insert customized_data 4503 if (count($customs)) { 4504 $insert = []; 4505 foreach ($customs as $custom) { 4506 $customizedValue = $custom['value']; 4507 4508 if ((int) $custom['type'] == 0) { 4509 $customizedValue = md5(uniqid(rand(), true)); 4510 copy(_PS_UPLOAD_DIR_.$custom['value'], _PS_UPLOAD_DIR_.$customizedValue); 4511 copy(_PS_UPLOAD_DIR_.$custom['value'].'_small', _PS_UPLOAD_DIR_.$customizedValue.'_small'); 4512 } 4513 4514 $insert[] = [ 4515 'id_customization' => (int) $customIds[$custom['id_customization']], 4516 'type' => (int) $custom['type'], 4517 'index' => (int) $custom['index'], 4518 'value' => pSQL($customizedValue), 4519 ]; 4520 } 4521 Db::getInstance()->insert('customized_data', $insert); 4522 } 4523 4524 return ['cart' => $cart, 'success' => $success]; 4525 } 4526 4527 /** 4528 * @param bool $autoDate 4529 * @param bool $nullValues 4530 * 4531 * @return bool 4532 * 4533 * @since 1.0.0 4534 * @version 1.0.0 Initial version 4535 * @throws PrestaShopException 4536 */ 4537 public function add($autoDate = true, $nullValues = false) 4538 { 4539 if (!$this->id_lang) { 4540 $this->id_lang = Configuration::get('PS_LANG_DEFAULT'); 4541 } 4542 if (!$this->id_shop) { 4543 $this->id_shop = Context::getContext()->shop->id; 4544 } 4545 4546 $return = parent::add($autoDate, $nullValues); 4547 Hook::exec('actionCartSave', ['cart' => $this]); 4548 4549 return $return; 4550 } 4551 4552 /** 4553 * @return array|false|mysqli_result|null|PDOStatement|resource 4554 * 4555 * @throws PrestaShopDatabaseException 4556 * @throws PrestaShopException 4557 * @since 1.0.0 4558 * @version 1.0.0 4559 */ 4560 public function getWsCartRows() 4561 { 4562 return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 4563 (new DbQuery()) 4564 ->select('`id_product`, `id_product_attribute`, `quantity`, `id_address_delivery`') 4565 ->from('cart_product') 4566 ->where('`id_cart` = '.(int) $this->id) 4567 ->where('`id_shop` = '.(int) Context::getContext()->shop->id) 4568 ); 4569 } 4570 4571 /** 4572 * @param array $values 4573 * 4574 * @return bool 4575 * 4576 * @throws PrestaShopDatabaseException 4577 * @throws PrestaShopException 4578 * @since 1.0.0 4579 * @version 1.0.0 4580 */ 4581 public function setWsCartRows($values) 4582 { 4583 if ($this->deleteAssociations()) { 4584 $insert = []; 4585 foreach ($values as $value) { 4586 $insert[] = [ 4587 'id_cart' => (int) $this->id, 4588 'id_product' => (int) $value['id_product'], 4589 'id_product_attribute' => isset($value['id_product_attribute']) ? (int) $value['id_product_attribute'] : null, 4590 'id_address_delivery' => isset($value['id_address_delivery']) ? (int) $value['id_address_delivery'] : 0, 4591 'quantity' => (int) $value['quantity'], 4592 'date_add' => ['type' => 'sql', 'value' => 'NOW()'], 4593 'id_shop' => (int) Context::getContext()->shop->id, 4594 ]; 4595 } 4596 4597 Db::getInstance()->insert('cart_product', $insert); 4598 } 4599 4600 return true; 4601 } 4602 4603 /** 4604 * @return bool 4605 * 4606 * @since 1.0.0 4607 * @version 1.0.0 4608 * @throws PrestaShopDatabaseException 4609 */ 4610 public function deleteAssociations() 4611 { 4612 return (bool) Db::getInstance()->delete('cart_product', '`id_cart` = '.(int) $this->id); 4613 } 4614 4615 /** 4616 * @param int $idProduct 4617 * @param int $idProductAttribute 4618 * @param int $oldIdAddressDelivery 4619 * @param int $newIdAddressDelivery 4620 * 4621 * @return bool 4622 * 4623 * @throws PrestaShopDatabaseException 4624 * @throws PrestaShopException 4625 * @since 1.0.0 4626 * @version 1.0.0 4627 */ 4628 public function setProductAddressDelivery($idProduct, $idProductAttribute, $oldIdAddressDelivery, $newIdAddressDelivery) 4629 { 4630 // Check address is linked with the customer 4631 if (!Customer::customerHasAddress(Context::getContext()->customer->id, $newIdAddressDelivery)) { 4632 return false; 4633 } 4634 4635 if ($newIdAddressDelivery == $oldIdAddressDelivery) { 4636 return false; 4637 } 4638 4639 // Checking if the product with the old address delivery exists 4640 $sql = new DbQuery(); 4641 $sql->select('count(*)'); 4642 $sql->from('cart_product', 'cp'); 4643 $sql->where('id_product = '.(int) $idProduct); 4644 $sql->where('id_product_attribute = '.(int) $idProductAttribute); 4645 $sql->where('id_address_delivery = '.(int) $oldIdAddressDelivery); 4646 $sql->where('id_cart = '.(int) $this->id); 4647 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql); 4648 4649 if ($result == 0) { 4650 return false; 4651 } 4652 4653 // Checking if there is no others similar products with this new address delivery 4654 $sql = new DbQuery(); 4655 $sql->select('sum(quantity) as qty'); 4656 $sql->from('cart_product', 'cp'); 4657 $sql->where('id_product = '.(int) $idProduct); 4658 $sql->where('id_product_attribute = '.(int) $idProductAttribute); 4659 $sql->where('id_address_delivery = '.(int) $newIdAddressDelivery); 4660 $sql->where('id_cart = '.(int) $this->id); 4661 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql); 4662 4663 // Removing similar products with this new address delivery 4664 Db::getInstance()->delete( 4665 'cart_product', 4666 'id_product = '.(int) $idProduct.' AND id_product_attribute = '.(int) $idProductAttribute.' AND id_address_delivery = '.(int) $newIdAddressDelivery.' AND id_cart = '.(int) $this->id, 4667 1 4668 ); 4669 4670 // Changing the address 4671 Db::getInstance()->update( 4672 'cart_product', 4673 [ 4674 'id_address_delivery' => (int) $newIdAddressDelivery, 4675 'quantity' => ['type' => 'sql', 'value' => '`quantity` + '.(int) $result], 4676 ], 4677 '`id_product` = '.(int) $idProduct.' AND `id_product_attribute` = '.(int) $idProductAttribute.' AND `id_address_delivery` = '.(int) $oldIdAddressDelivery.' AND `id_cart` = '.(int) $this->id, 4678 1 4679 ); 4680 4681 return true; 4682 } 4683 4684 /** 4685 * @param int $idProduct 4686 * @param int $idProductAttribute 4687 * @param int $idAddressDelivery 4688 * @param int $newIdAddressDelivery 4689 * @param int $quantity 4690 * @param bool $keepQuantity 4691 * 4692 * @return bool 4693 * 4694 * @throws PrestaShopDatabaseException 4695 * @throws PrestaShopException 4696 * @since 1.0.0 4697 * @version 1.0.0 4698 */ 4699 public function duplicateProduct( 4700 $idProduct, 4701 $idProductAttribute, 4702 $idAddressDelivery, 4703 $newIdAddressDelivery, 4704 $quantity = 1, 4705 $keepQuantity = false 4706 ) { 4707 // Check address is linked with the customer 4708 if (!Customer::customerHasAddress(Context::getContext()->customer->id, $newIdAddressDelivery)) { 4709 return false; 4710 } 4711 4712 // Checking the product do not exist with the new address 4713 $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 4714 (new DbQuery()) 4715 ->select('COUNT(*)') 4716 ->from('cart_product', 'c') 4717 ->where('id_product = '.(int) $idProduct) 4718 ->where('`id_product_attribute` = '.(int) $idProductAttribute) 4719 ->where('`id_address_delivery` = '.(int) $newIdAddressDelivery) 4720 ->where('`id_cart` = '.(int) $this->id) 4721 ); 4722 4723 if ($result > 0) { 4724 return false; 4725 } 4726 4727 Db::getInstance()->insert( 4728 'cart_product', 4729 [ 4730 'id_cart' => (int) $this->id, 4731 'id_product' => (int) $idProduct, 4732 'id_shop' => (int) $this->id_shop, 4733 'id_product_attribute' => (int) $idProductAttribute, 4734 'quantity' => (int) $quantity, 4735 'date_add' => ['type' => 'sql', 'value' => 'NOW()'], 4736 'id_address_delivery' => (int) $newIdAddressDelivery, 4737 ] 4738 ); 4739 4740 if (!$keepQuantity) { 4741 $duplicatedQuantity = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 4742 (new DbQuery()) 4743 ->select('quantity') 4744 ->from('cart_product', 'c') 4745 ->where('id_product = '.(int) $idProduct) 4746 ->where('id_product_attribute = '.(int) $idProductAttribute) 4747 ->where('id_address_delivery = '.(int) $idAddressDelivery) 4748 ->where('id_cart = '.(int) $this->id) 4749 ); 4750 4751 if ($duplicatedQuantity > $quantity) { 4752 Db::getInstance()->update( 4753 'cart_product', 4754 [ 4755 'quantity' => ['type' => 'sql', 'value' => '`quantity - `'.(int) $quantity], 4756 'id_product' => (int) $idProduct, 4757 'id_shop' => (int) $this->id_shop, 4758 'id_product_attribute' => (int) $idProductAttribute, 4759 'id_address_delivery' => (int) $idAddressDelivery, 4760 ], 4761 '`id_cart` ='.(int) $this->id 4762 ); 4763 } 4764 } 4765 4766 // Checking if there is customizations 4767 $results = Db::getInstance()->executeS( 4768 (new DbQuery()) 4769 ->select('*') 4770 ->from('customization', 'c') 4771 ->where('id_product = '.(int) $idProduct) 4772 ->where('id_product_attribute = '.(int) $idProductAttribute) 4773 ->where('id_address_delivery = '.(int) $idAddressDelivery) 4774 ->where('id_cart = '.(int) $this->id) 4775 ); 4776 4777 foreach ($results as $customization) { 4778 // Duplicate customization 4779 Db::getInstance()->insert( 4780 'customization', 4781 [ 4782 'id_product_attribute' => (int) $customization['id_product_attribute'], 4783 'id_address_delivery' => (int) $newIdAddressDelivery, 4784 'id_cart' => (int) $customization['id_cart'], 4785 'id_product' => (int) $customization['id_product'], 4786 'quantity' => (int) $quantity, 4787 'in_cart' => $customization['in_cart'], 4788 ] 4789 ); 4790 4791 // Save last insert ID before doing another query 4792 $lastId = (int) Db::getInstance()->Insert_ID(); 4793 4794 // Get data from duplicated customizations 4795 $lastRow = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( 4796 (new DbQuery()) 4797 ->select('`type`, `index`, `value`') 4798 ->from('customized_data') 4799 ->where('id_customization = '.$customization['id_customization']) 4800 ); 4801 4802 // Insert new copied data with new customization ID into customized_data table 4803 $lastRow['id_customization'] = $lastId; 4804 Db::getInstance()->insert('customized_data', $lastRow); 4805 } 4806 4807 $customizationCount = count($results); 4808 if ($customizationCount > 0) { 4809 Db::getInstance()->update( 4810 'cart_product', 4811 [ 4812 'quantity' => ['type' => 'sql', 'value' => '`quantity` + '.(int) $customizationCount * $quantity], 4813 ], 4814 'id_cart = '.(int) $this->id.' AND id_product = '.(int) $idProduct.' AND id_shop = '.(int) $this->id_shop.' AND id_product_attribute = '.(int) $idProductAttribute.' AND id_address_delivery = '.(int) $newIdAddressDelivery 4815 ); 4816 } 4817 4818 return true; 4819 } 4820 4821 /** 4822 * Update products cart address delivery with the address delivery of the cart 4823 * 4824 * @since 1.0.0 4825 * @version 1.0.0 4826 */ 4827 public function setNoMultishipping() 4828 { 4829 $emptyCache = false; 4830 if (Configuration::get('PS_ALLOW_MULTISHIPPING')) { 4831 // Upgrading quantities 4832 $products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 4833 (new DbQuery()) 4834 ->select('SUM(`quantity`) AS `quantity`, `id_product`, `id_product_attribute`, COUNT(*) AS `count`') 4835 ->from('cart_product') 4836 ->where('`id_cart` = '.(int) $this->id) 4837 ->where('`id_shop` = '.(int) $this->id_shop) 4838 ->groupBy('`id_product`, `id_product_attribute`') 4839 ->having('`count` > 1') 4840 ); 4841 4842 if (is_array($products)) { 4843 foreach ($products as $product) { 4844 if (Db::getInstance()->update( 4845 'cart_product', 4846 [ 4847 'quantity' => (int) $product['quantity'], 4848 ], 4849 '`id_cart` = '.(int) $this->id.' AND `id_shop` = '.(int) $this->id_shop.' AND id_product = '.$product['id_product'].' AND id_product_attribute = '.$product['id_product_attribute'] 4850 )) { 4851 $emptyCache = true; 4852 } 4853 } 4854 } 4855 4856 // Merging multiple lines 4857 $sql = 'DELETE cp1 4858 FROM `'._DB_PREFIX_.'cart_product` cp1 4859 INNER JOIN `'._DB_PREFIX_.'cart_product` cp2 4860 ON ( 4861 (cp1.id_cart = cp2.id_cart) 4862 AND (cp1.id_product = cp2.id_product) 4863 AND (cp1.id_product_attribute = cp2.id_product_attribute) 4864 AND (cp1.id_address_delivery <> cp2.id_address_delivery) 4865 AND (cp1.date_add > cp2.date_add) 4866 )'; 4867 Db::getInstance()->execute($sql); 4868 } 4869 4870 // Update delivery address for each product line 4871 $cacheId = 'static::setNoMultishipping'.(int) $this->id.'-'.(int) $this->id_shop.((isset($this->id_address_delivery) && $this->id_address_delivery) ? '-'.(int) $this->id_address_delivery : ''); 4872 if (!Cache::isStored($cacheId)) { 4873 if ($result = (bool) Db::getInstance()->update( 4874 'cart_product', 4875 [ 4876 'id_address_delivery' => ['type' => 'sql', 'value' => '(SELECT `id_address_delivery` FROM `'._DB_PREFIX_.'cart` WHERE `id_cart` = '.(int) $this->id.' AND `id_shop` = '.(int) $this->id_shop.' LIMIT 1)'], 4877 ], 4878 '`id_cart` = '.(int) $this->id.' '.(Configuration::get('PS_ALLOW_MULTISHIPPING') ? ' AND `id_shop` = '.(int) $this->id_shop : '') 4879 )) { 4880 $emptyCache = true; 4881 } 4882 Cache::store($cacheId, $result); 4883 } 4884 4885 if (Customization::isFeatureActive()) { 4886 Db::getInstance()->update( 4887 'customization', 4888 [ 4889 'id_address_delivery' => ['type' => 'sql', 'value' => '(SELECT `id_address_delivery` FROM `'._DB_PREFIX_.'cart` WHERE `id_cart` = '.(int) $this->id.' LIMIT 1)'], 4890 ], 4891 '`id_cart` = '.(int) $this->id 4892 ); 4893 } 4894 4895 if ($emptyCache) { 4896 $this->_products = null; 4897 } 4898 } 4899 4900 /** 4901 * Set an address to all products on the cart without address delivery 4902 * 4903 * @since 1.0.0 4904 * @version 1.0.0 4905 */ 4906 public function autosetProductAddress() 4907 { 4908 // Get the main address of the customer 4909 if ((int) $this->id_address_delivery > 0) { 4910 $idAddressDelivery = (int) $this->id_address_delivery; 4911 } else { 4912 $idAddressDelivery = (int) Address::getFirstCustomerAddressId(Context::getContext()->customer->id); 4913 } 4914 4915 if (!$idAddressDelivery) { 4916 return; 4917 } 4918 4919 // Update 4920 Db::getInstance()->update( 4921 'cart_product', 4922 [ 4923 'id_address_delivery' => (int) $idAddressDelivery, 4924 ], 4925 '`id_cart` = '.(int) $this->id.' AND (`id_address_delivery` = 0 OR `id_address_delivery` IS NULL) AND `id_shop` = '.(int) $this->id_shop 4926 ); 4927 4928 Db::getInstance()->update( 4929 'customization', 4930 [ 4931 'id_address_delivery' => (int) $idAddressDelivery, 4932 ], 4933 '`id_cart` = '.(int) $this->id.' AND (`id_address_delivery` = 0 OR `id_address_delivery` IS NULL)' 4934 ); 4935 } 4936 4937 /** 4938 * @param bool $ignoreVirtual Ignore virtual product 4939 * @param bool $exclusive If true, the validation is exclusive : it must be present product in stock and out of stock 4940 * 4941 * @return bool false is some products from the cart are out of stock 4942 * 4943 * @since 1.0.0 4944 * @version 1.0.0 4945 * @throws PrestaShopException 4946 */ 4947 public function isAllProductsInStock($ignoreVirtual = false, $exclusive = false) 4948 { 4949 $productOutOfStock = 0; 4950 $productInStock = 0; 4951 foreach ($this->getProducts() as $product) { 4952 if (!$exclusive) { 4953 if (((int) $product['quantity_available'] - (int) $product['cart_quantity']) < 0 4954 && (!$ignoreVirtual || !$product['is_virtual']) 4955 ) { 4956 return false; 4957 } 4958 } else { 4959 if ((int) $product['quantity_available'] <= 0 4960 && (!$ignoreVirtual || !$product['is_virtual']) 4961 ) { 4962 $productOutOfStock++; 4963 } 4964 if ((int) $product['quantity_available'] > 0 4965 && (!$ignoreVirtual || !$product['is_virtual']) 4966 ) { 4967 $productInStock++; 4968 } 4969 4970 if ($productInStock > 0 && $productOutOfStock > 0) { 4971 return false; 4972 } 4973 } 4974 } 4975 4976 return true; 4977 } 4978} 4979