* @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ use PrestaShop\PrestaShop\Adapter\AddressFactory; use PrestaShop\PrestaShop\Adapter\Cache\CacheAdapter; use PrestaShop\PrestaShop\Adapter\Customer\CustomerDataProvider; use PrestaShop\PrestaShop\Adapter\Database; use PrestaShop\PrestaShop\Adapter\Group\GroupDataProvider; use PrestaShop\PrestaShop\Adapter\Product\PriceCalculator; use PrestaShop\PrestaShop\Adapter\ServiceLocator; use PrestaShop\PrestaShop\Core\Cart\Calculator; use PrestaShop\PrestaShop\Core\Cart\CartRow; use PrestaShop\PrestaShop\Core\Cart\CartRuleData; use PrestaShop\PrestaShop\Core\Localization\Exception\LocalizationException; class CartCore extends ObjectModel { /** @var int|null */ public $id; public $id_shop_group; public $id_shop; /** @var int|null Customer delivery address ID */ public $id_address_delivery; /** @var int|null Customer invoicing address ID */ public $id_address_invoice; /** @var int Customer currency ID */ public $id_currency; /** @var int Customer ID */ public $id_customer; /** @var int Guest ID */ public $id_guest; /** @var int Language ID */ public $id_lang; /** @var bool True if the customer wants a recycled package */ public $recyclable = false; /** @var bool True if the customer wants a gift wrapping */ public $gift = false; /** @var string Gift message if specified */ public $gift_message; /** @var bool Mobile Theme */ public $mobile_theme; /** @var string Object creation date */ public $date_add; /** @var string secure_key */ public $secure_key; /** @var int Carrier ID */ public $id_carrier = 0; /** @var string Object last modification date */ public $date_upd; public $checkedTos = false; public $pictures; public $textFields; public $delivery_option; /** @var bool Allow to seperate order in multiple package in order to recieve as soon as possible the available products */ public $allow_seperated_package = false; protected static $_nbProducts = []; protected static $_isVirtualCart = []; protected $_products = null; protected static $_totalWeight = []; protected $_taxCalculationMethod = PS_TAX_EXC; protected static $_carriers = null; protected static $_taxes_rate = null; protected static $_attributesLists = []; /** @var Customer|null */ protected static $_customer = null; protected static $cacheDeliveryOption = []; protected static $cacheNbPackages = []; protected static $cachePackageList = []; protected static $cacheDeliveryOptionList = []; protected static $cacheMultiAddressDelivery = []; /** * @see ObjectModel::$definition */ public static $definition = [ 'table' => 'cart', 'primary' => 'id_cart', 'fields' => [ 'id_shop_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 'id_shop' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 'id_address_delivery' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 'id_address_invoice' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 'id_carrier' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 'id_currency' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true], 'id_customer' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 'id_guest' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'], 'id_lang' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true], 'recyclable' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 'gift' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 'gift_message' => ['type' => self::TYPE_STRING, 'validate' => 'isCleanHtml', 'size' => 4194303], 'mobile_theme' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 'delivery_option' => ['type' => self::TYPE_STRING, 'size' => 4194303], 'secure_key' => ['type' => self::TYPE_STRING, 'size' => 32], 'allow_seperated_package' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'], 'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'], ], ]; /** @var array Web service parameters */ protected $webserviceParameters = [ 'fields' => [ 'id_address_delivery' => ['xlink_resource' => 'addresses'], 'id_address_invoice' => ['xlink_resource' => 'addresses'], 'id_currency' => ['xlink_resource' => 'currencies'], 'id_customer' => ['xlink_resource' => 'customers'], 'id_guest' => ['xlink_resource' => 'guests'], 'id_lang' => ['xlink_resource' => 'languages'], ], 'associations' => [ 'cart_rows' => [ 'resource' => 'cart_row', 'virtual_entity' => true, 'fields' => [ 'id_product' => ['required' => true, 'xlink_resource' => 'products'], 'id_product_attribute' => ['required' => true, 'xlink_resource' => 'combinations'], 'id_address_delivery' => ['required' => true, 'xlink_resource' => 'addresses'], 'id_customization' => ['required' => false, 'xlink_resource' => 'customizations'], 'quantity' => ['required' => true], ], ], ], ]; protected $configuration; protected $addressFactory; protected $shouldSplitGiftProductsQuantity = false; protected $shouldExcludeGiftsDiscount = false; public const ONLY_PRODUCTS = 1; public const ONLY_DISCOUNTS = 2; public const BOTH = 3; public const BOTH_WITHOUT_SHIPPING = 4; public const ONLY_SHIPPING = 5; public const ONLY_WRAPPING = 6; /** @deprecated since 1.7 **/ public const ONLY_PRODUCTS_WITHOUT_SHIPPING = 7; public const ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING = 8; private const DEFAULT_ATTRIBUTES_KEYS = ['attributes' => '', 'attributes_small' => '']; /** * CartCore constructor. * * @param int|null $id Cart ID * null = new Cart * @param int|null $idLang Language ID * null = Language ID of current Context */ public function __construct($id = null, $idLang = null) { parent::__construct($id); if (null !== $idLang) { $this->id_lang = (int) (Language::getLanguage($idLang) !== false) ? $idLang : Configuration::get('PS_LANG_DEFAULT'); } if ($this->id_customer) { if (isset(Context::getContext()->customer) && Context::getContext()->customer->id == $this->id_customer) { $customer = Context::getContext()->customer; } else { $customer = new Customer((int) $this->id_customer); } Cart::$_customer = $customer; if ((!$this->secure_key || $this->secure_key == '-1') && $customer->secure_key) { $this->secure_key = $customer->secure_key; $this->save(); } } $this->setTaxCalculationMethod(); $this->configuration = ServiceLocator::get('\\PrestaShop\\PrestaShop\\Core\\ConfigurationInterface'); $this->addressFactory = ServiceLocator::get('\\PrestaShop\\PrestaShop\\Adapter\\AddressFactory'); } public static function resetStaticCache() { static::$_nbProducts = []; static::$_isVirtualCart = []; static::$_totalWeight = []; static::$_carriers = null; static::$_taxes_rate = null; static::$_attributesLists = []; static::$_customer = null; static::$cacheDeliveryOption = []; static::$cacheNbPackages = []; static::$cachePackageList = []; static::$cacheDeliveryOptionList = []; static::$cacheMultiAddressDelivery = []; } /** * Set Tax calculation method. */ public function setTaxCalculationMethod() { $this->_taxCalculationMethod = Group::getPriceDisplayMethod(Group::getCurrent()->id); } /** * Adds current Cart as a new Object to the database. * * @param bool $autoDate Automatically set `date_upd` and `date_add` columns * @param bool $nullValues Whether we want to use NULL values instead of empty quotes values * * @return bool Whether the Cart has been successfully added * * @throws PrestaShopDatabaseException * @throws PrestaShopException */ public function add($autoDate = true, $nullValues = false) { if (!$this->id_lang) { $this->id_lang = (int) Configuration::get('PS_LANG_DEFAULT'); } if (!$this->id_shop) { $this->id_shop = Context::getContext()->shop->id; } if (!$this->id_shop_group) { $this->id_shop_group = Context::getContext()->shop->id_shop_group; } $return = parent::add($autoDate, $nullValues); Hook::exec('actionCartSave', ['cart' => $this]); return $return; } /** * Updates the current object in the database. * * @param bool $nullValues Whether we want to use NULL values instead of empty quotes values * * @return bool Whether the Cart has been successfully updated * * @throws PrestaShopDatabaseException * @throws PrestaShopException */ public function update($nullValues = false) { if (isset(self::$_nbProducts[$this->id])) { unset(self::$_nbProducts[$this->id]); } if (isset(self::$_totalWeight[$this->id])) { unset(self::$_totalWeight[$this->id]); } $this->_products = null; $return = parent::update($nullValues); Hook::exec('actionCartSave', ['cart' => $this]); return $return; } /** * Update the Address ID of the Cart. * * @param int $id_address Current Address ID to change * @param int $id_address_new New Address ID */ public function updateAddressId($id_address, $id_address_new) { $to_update = false; if (empty($this->id_address_invoice) || $this->id_address_invoice == $id_address) { $to_update = true; $this->id_address_invoice = $id_address_new; } if (empty($this->id_address_delivery) || $this->id_address_delivery == $id_address) { $to_update = true; $this->id_address_delivery = $id_address_new; } if ($to_update) { $this->update(); } $sql = 'UPDATE `' . _DB_PREFIX_ . 'cart_product` SET `id_address_delivery` = ' . (int) $id_address_new . ' WHERE `id_cart` = ' . (int) $this->id . ' AND `id_address_delivery` = ' . (int) $id_address; Db::getInstance()->execute($sql); $sql = 'UPDATE `' . _DB_PREFIX_ . 'customization` SET `id_address_delivery` = ' . (int) $id_address_new . ' WHERE `id_cart` = ' . (int) $this->id . ' AND `id_address_delivery` = ' . (int) $id_address; Db::getInstance()->execute($sql); } /** * Update the Delivery Address ID of the Cart. * * @param int $currentAddressId Current Address ID to change * @param int $newAddressId New Address ID */ public function updateDeliveryAddressId(int $currentAddressId, int $newAddressId) { if (empty($this->id_address_delivery) || (int) $this->id_address_delivery === $currentAddressId) { $this->id_address_delivery = $newAddressId; $this->update(); } $sql = 'UPDATE `' . _DB_PREFIX_ . 'cart_product` SET `id_address_delivery` = ' . $newAddressId . ' WHERE `id_cart` = ' . (int) $this->id . ' AND `id_address_delivery` = ' . $currentAddressId; Db::getInstance()->execute($sql); $sql = 'UPDATE `' . _DB_PREFIX_ . 'customization` SET `id_address_delivery` = ' . $newAddressId . ' WHERE `id_cart` = ' . (int) $this->id . ' AND `id_address_delivery` = ' . $currentAddressId; Db::getInstance()->execute($sql); } /** * Deletes current Cart from the database. * * @return bool True if delete was successful * * @throws PrestaShopException */ public function delete() { if ($this->orderExists()) { //NOT delete a cart which is associated with an order return false; } $uploaded_files = Db::getInstance()->executeS( 'SELECT cd.`value` FROM `' . _DB_PREFIX_ . 'customized_data` cd INNER JOIN `' . _DB_PREFIX_ . 'customization` c ON (cd.`id_customization`= c.`id_customization`) WHERE cd.`type`= ' . (int) Product::CUSTOMIZE_FILE . ' AND c.`id_cart`=' . (int) $this->id ); foreach ($uploaded_files as $must_unlink) { unlink(_PS_UPLOAD_DIR_ . $must_unlink['value'] . '_small'); unlink(_PS_UPLOAD_DIR_ . $must_unlink['value']); } Db::getInstance()->execute( 'DELETE FROM `' . _DB_PREFIX_ . 'customized_data` WHERE `id_customization` IN ( SELECT `id_customization` FROM `' . _DB_PREFIX_ . 'customization` WHERE `id_cart`=' . (int) $this->id . ' )' ); Db::getInstance()->execute( 'DELETE FROM `' . _DB_PREFIX_ . 'customization` WHERE `id_cart` = ' . (int) $this->id ); if (!Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'cart_cart_rule` WHERE `id_cart` = ' . (int) $this->id) || !Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'cart_product` WHERE `id_cart` = ' . (int) $this->id)) { return false; } return parent::delete(); } /** * Returns the average Tax rate for all products in the cart, as a multiplier. * * The arguments are optional and only serve as return values in case caller needs the details. * * @param float|null $cartAmountTaxExcluded If the reference is given, it will be updated with the * total amount in the Cart excluding Taxes * @param float|null $cartAmountTaxIncluded If the reference is given, it will be updated with the * total amount in the Cart including Taxes * * @return float Average Tax Rate on Products (eg. 0.2 for 20% average rate) */ public function getAverageProductsTaxRate(&$cartAmountTaxExcluded = null, &$cartAmountTaxIncluded = null) { $cartAmountTaxIncluded = $this->getOrderTotal(true, Cart::ONLY_PRODUCTS); $cartAmountTaxExcluded = $this->getOrderTotal(false, Cart::ONLY_PRODUCTS); $cart_vat_amount = $cartAmountTaxIncluded - $cartAmountTaxExcluded; if ($cart_vat_amount == 0 || $cartAmountTaxExcluded == 0) { return 0; } else { return Tools::ps_round($cart_vat_amount / $cartAmountTaxExcluded, Tax::TAX_DEFAULT_PRECISION); } } /** * Get Cart Rules. * * @param int $filter Filter enum: * - FILTER_ACTION_ALL * - FILTER_ACTION_SHIPPING * - FILTER_ACTION_REDUCTION * - FILTER_ACTION_GIFT * - FILTER_ACTION_ALL_NOCAP * @param bool $autoAdd automaticaly adds cart ruls without code to cart * @param bool $useOrderPrices * * @return array|false|mysqli_result|PDOStatement|resource|null Database result */ public function getCartRules($filter = CartRule::FILTER_ACTION_ALL, $autoAdd = true, $useOrderPrices = false) { // Define virtual context to prevent case where the cart is not the in the global context $virtual_context = Context::getContext()->cloneContext(); /* @phpstan-ignore-next-line */ $virtual_context->cart = $this; // If the cart has not been saved, then there can't be any cart rule applied if (!CartRule::isFeatureActive() || !$this->id) { return []; } if ($autoAdd) { CartRule::autoAddToCart($virtual_context, $useOrderPrices); } $cache_key = 'Cart::getCartRules_' . $this->id . '-' . $filter; if (!Cache::isStored($cache_key)) { $result = Db::getInstance()->executeS( 'SELECT cr.*, crl.`id_lang`, crl.`name`, cd.`id_cart` FROM `' . _DB_PREFIX_ . 'cart_cart_rule` cd LEFT JOIN `' . _DB_PREFIX_ . 'cart_rule` cr ON cd.`id_cart_rule` = cr.`id_cart_rule` LEFT JOIN `' . _DB_PREFIX_ . 'cart_rule_lang` crl ON ( cd.`id_cart_rule` = crl.`id_cart_rule` AND crl.id_lang = ' . (int) $this->getAssociatedLanguage()->getId() . ' ) WHERE `id_cart` = ' . (int) $this->id . ' ' . ($filter == CartRule::FILTER_ACTION_SHIPPING ? 'AND free_shipping = 1' : '') . ' ' . ($filter == CartRule::FILTER_ACTION_GIFT ? 'AND gift_product != 0' : '') . ' ' . ($filter == CartRule::FILTER_ACTION_REDUCTION ? 'AND (reduction_percent != 0 OR reduction_amount != 0)' : '') . ' ORDER by cr.priority ASC, cr.gift_product DESC' ); Cache::store($cache_key, $result); } else { $result = Cache::retrieve($cache_key); } // Define virtual context to prevent case where the cart is not the in the global context $virtual_context = Context::getContext()->cloneContext(); /* @phpstan-ignore-next-line */ $virtual_context->cart = $this; // set base cart total values, they will be updated and used for percentage cart rules (because percentage cart rules // are applied to the cart total's value after previously applied cart rules) $virtual_context->virtualTotalTaxExcluded = $virtual_context->cart->getOrderTotal(false, self::ONLY_PRODUCTS); if (Tax::excludeTaxeOption()) { $virtual_context->virtualTotalTaxIncluded = $virtual_context->virtualTotalTaxExcluded; } else { $virtual_context->virtualTotalTaxIncluded = $virtual_context->cart->getOrderTotal(true, self::ONLY_PRODUCTS); } foreach ($result as &$row) { $row['obj'] = new CartRule($row['id_cart_rule'], (int) $this->id_lang); $row['value_real'] = $row['obj']->getContextualValue(true, $virtual_context, $filter); $row['value_tax_exc'] = $row['obj']->getContextualValue(false, $virtual_context, $filter); // Retro compatibility < 1.5.0.2 $row['id_discount'] = $row['id_cart_rule']; $row['description'] = $row['obj']->description; } return $result; } /** * Get cart discounts. */ public function getDiscounts() { return CartRule::getCustomerHighlightedDiscounts($this->id_lang, $this->id_customer, $this); } /** * Return the CartRule IDs in the Cart. * * @param int $filter Filter enum: * - FILTER_ACTION_ALL * - FILTER_ACTION_SHIPPING * - FILTER_ACTION_REDUCTION * - FILTER_ACTION_GIFT * - FILTER_ACTION_ALL_NOCAP * * @return array * * @throws PrestaShopDatabaseException */ public function getOrderedCartRulesIds($filter = CartRule::FILTER_ACTION_ALL) { $cache_key = 'Cart::getOrderedCartRulesIds_' . $this->id . '-' . $filter . '-ids'; if (!Cache::isStored($cache_key)) { $result = Db::getInstance()->executeS( 'SELECT cr.`id_cart_rule` FROM `' . _DB_PREFIX_ . 'cart_cart_rule` cd LEFT JOIN `' . _DB_PREFIX_ . 'cart_rule` cr ON cd.`id_cart_rule` = cr.`id_cart_rule` LEFT JOIN `' . _DB_PREFIX_ . 'cart_rule_lang` crl ON ( cd.`id_cart_rule` = crl.`id_cart_rule` AND crl.id_lang = ' . (int) $this->getAssociatedLanguage()->getId() . ' ) WHERE `id_cart` = ' . (int) $this->id . ' ' . ($filter == CartRule::FILTER_ACTION_SHIPPING ? 'AND free_shipping = 1' : '') . ' ' . ($filter == CartRule::FILTER_ACTION_GIFT ? 'AND gift_product != 0' : '') . ' ' . ($filter == CartRule::FILTER_ACTION_REDUCTION ? 'AND (reduction_percent != 0 OR reduction_amount != 0)' : '') . ' ORDER BY cr.priority ASC, cr.gift_product DESC' ); Cache::store($cache_key, $result); } else { $result = Cache::retrieve($cache_key); } return $result; } /** * Get amount of Customer Discounts. * * @param int $id_cart_rule CartRule ID * * @return int Amount of Customer Discounts * @todo: What are customer discounts? Isn't this just a PriceRule and shouldn't this method be renamed instead? */ public function getDiscountsCustomer($id_cart_rule) { if (!CartRule::isFeatureActive()) { return 0; } $cache_id = 'Cart::getDiscountsCustomer_' . (int) $this->id . '-' . (int) $id_cart_rule; if (!Cache::isStored($cache_id)) { $result = (int) Db::getInstance()->getValue(' SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'cart_cart_rule` WHERE `id_cart_rule` = ' . (int) $id_cart_rule . ' AND `id_cart` = ' . (int) $this->id); Cache::store($cache_id, $result); return $result; } return Cache::retrieve($cache_id); } /** * Get last Product in Cart. * * @return bool|mixed Database result */ public function getLastProduct() { $sql = ' SELECT `id_product`, `id_product_attribute`, id_shop FROM `' . _DB_PREFIX_ . 'cart_product` WHERE `id_cart` = ' . (int) $this->id . ' ORDER BY `date_add` DESC'; $result = Db::getInstance()->getRow($sql); if ($result && isset($result['id_product']) && $result['id_product']) { foreach ($this->getProducts(false, false, null, false) as $product) { if ($result['id_product'] == $product['id_product'] && ( !$result['id_product_attribute'] || $result['id_product_attribute'] == $product['id_product_attribute'] )) { return $product; } } } return false; } /** * Return cart products. * * @param bool $refresh * @param bool|int $id_product * @param int|null $id_country * @param bool $fullInfos * @param bool $keepOrderPrices When true use the Order saved prices instead of the most recent ones from catalog (if Order exists) * * @return array Products */ public function getProducts($refresh = false, $id_product = false, $id_country = null, $fullInfos = true, bool $keepOrderPrices = false) { if (!$this->id) { return []; } // Product cache must be strictly compared to NULL, or else an empty cart will add dozens of queries if ($this->_products !== null && !$refresh) { // Return product row with specified ID if it exists if (is_int($id_product)) { foreach ($this->_products as $product) { if ($product['id_product'] == $id_product) { return [$product]; } } return []; } return $this->_products; } // Build query $sql = new DbQuery(); // Build SELECT $sql->select('cp.`id_product_attribute`, cp.`id_product`, cp.`quantity` AS cart_quantity, cp.id_shop, cp.`id_customization`, pl.`name`, p.`is_virtual`, pl.`description_short`, pl.`available_now`, pl.`available_later`, product_shop.`id_category_default`, p.`id_supplier`, p.`id_manufacturer`, m.`name` AS manufacturer_name, product_shop.`on_sale`, product_shop.`ecotax`, product_shop.`additional_shipping_cost`, product_shop.`available_for_order`, product_shop.`show_price`, product_shop.`price`, product_shop.`active`, product_shop.`unity`, product_shop.`unit_price`, stock.`quantity` AS quantity_available, p.`width`, p.`height`, p.`depth`, stock.`out_of_stock`, p.`weight`, p.`available_date`, p.`date_add`, p.`date_upd`, IFNULL(stock.quantity, 0) as quantity, pl.`link_rewrite`, cl.`link_rewrite` AS category, CONCAT(LPAD(cp.`id_product`, 10, 0), LPAD(IFNULL(cp.`id_product_attribute`, 0), 10, 0), IFNULL(cp.`id_address_delivery`, 0), IFNULL(cp.`id_customization`, 0)) AS unique_id, cp.id_address_delivery, product_shop.advanced_stock_management, ps.product_supplier_reference supplier_reference'); // Build FROM $sql->from('cart_product', 'cp'); // Build JOIN $sql->leftJoin('product', 'p', 'p.`id_product` = cp.`id_product`'); $sql->innerJoin('product_shop', 'product_shop', '(product_shop.`id_shop` = cp.`id_shop` AND product_shop.`id_product` = p.`id_product`)'); $sql->leftJoin( 'product_lang', 'pl', 'p.`id_product` = pl.`id_product` AND pl.`id_lang` = ' . (int) $this->getAssociatedLanguage()->getId() . Shop::addSqlRestrictionOnLang('pl', 'cp.id_shop') ); $sql->leftJoin( 'category_lang', 'cl', 'product_shop.`id_category_default` = cl.`id_category` AND cl.`id_lang` = ' . (int) $this->getAssociatedLanguage()->getId() . Shop::addSqlRestrictionOnLang('cl', 'cp.id_shop') ); $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`'); $sql->leftJoin('manufacturer', 'm', 'm.`id_manufacturer` = p.`id_manufacturer`'); // @todo test if everything is ok, then refactorise call of this method $sql->join(Product::sqlStock('cp', 'cp')); // Build WHERE clauses $sql->where('cp.`id_cart` = ' . (int) $this->id); if ($id_product) { $sql->where('cp.`id_product` = ' . (int) $id_product); } $sql->where('p.`id_product` IS NOT NULL'); // Build ORDER BY $sql->orderBy('cp.`date_add`, cp.`id_product`, cp.`id_product_attribute` ASC'); if (Customization::isFeatureActive()) { $sql->select('cu.`id_customization`, cu.`quantity` AS customization_quantity'); $sql->leftJoin( 'customization', 'cu', 'p.`id_product` = cu.`id_product` AND cp.`id_product_attribute` = cu.`id_product_attribute` AND cp.`id_customization` = cu.`id_customization` AND cu.`id_cart` = ' . (int) $this->id ); $sql->groupBy('cp.`id_product_attribute`, cp.`id_product`, cp.`id_shop`, cp.`id_customization`'); } else { $sql->select('NULL AS customization_quantity, NULL AS id_customization'); } if (Combination::isFeatureActive()) { $sql->select(' product_attribute_shop.`price` AS price_attribute, product_attribute_shop.`ecotax` AS ecotax_attr, IF (IFNULL(pa.`reference`, \'\') = \'\', p.`reference`, pa.`reference`) AS reference, (p.`weight`+ IFNULL(product_attribute_shop.`weight`, pa.`weight`)) weight_attribute, IF (IFNULL(pa.`ean13`, \'\') = \'\', p.`ean13`, pa.`ean13`) AS ean13, IF (IFNULL(pa.`isbn`, \'\') = \'\', p.`isbn`, pa.`isbn`) AS isbn, IF (IFNULL(pa.`upc`, \'\') = \'\', p.`upc`, pa.`upc`) AS upc, IF (IFNULL(pa.`mpn`, \'\') = \'\', p.`mpn`, pa.`mpn`) AS mpn, IFNULL(product_attribute_shop.`minimal_quantity`, product_shop.`minimal_quantity`) as minimal_quantity, IF(product_attribute_shop.wholesale_price > 0, product_attribute_shop.wholesale_price, product_shop.`wholesale_price`) wholesale_price '); $sql->leftJoin('product_attribute', 'pa', 'pa.`id_product_attribute` = cp.`id_product_attribute`'); $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`)'); } else { $sql->select( 'p.`reference` AS reference, p.`ean13`, p.`isbn`, p.`upc` AS upc, p.`mpn` AS mpn, product_shop.`minimal_quantity` AS minimal_quantity, product_shop.`wholesale_price` wholesale_price' ); } $sql->select('image_shop.`id_image` id_image, il.`legend`'); $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); $sql->leftJoin('image_lang', 'il', 'il.`id_image` = image_shop.`id_image` AND il.`id_lang` = ' . (int) $this->getAssociatedLanguage()->getId()); /** @var array|false $products */ $products = Db::getInstance()->executeS($sql); // Reset the cache before the following return, or else an empty cart will add dozens of queries $products_ids = []; $pa_ids = []; if (is_iterable($products)) { foreach ($products as $key => $product) { $products_ids[] = $product['id_product']; $pa_ids[] = $product['id_product_attribute']; $specific_price = SpecificPrice::getSpecificPrice($product['id_product'], $this->id_shop, $this->id_currency, $id_country, $this->id_shop_group, $product['cart_quantity'], $product['id_product_attribute'], $this->id_customer, $this->id); if ($specific_price) { $reduction_type_row = ['reduction_type' => $specific_price['reduction_type']]; } else { $reduction_type_row = ['reduction_type' => 0]; } /* * In case of packs, we need to properly calculate the quantity in stock. We can't just use * the quantity from the query, because stocks can be set to use the quantity of the products * in them. The quantity in stock_available then has no meaning and could be always zero. * * When calling Pack::getQuantity here, you MUST use null for $cart parameter. Otherwise it * will subtract the quantity that is already in the cart. Basically resulting in a nonsense, * half of quantity you have. We need the REAL quantity. */ if (Pack::isPack($product['id_product'])) { $product['quantity_available'] = Pack::getQuantity((int) $product['id_product'], (int) $product['id_product_attribute']); } $products[$key] = array_merge($product, $reduction_type_row); } } // Thus you can avoid one query per product, because there will be only one query for all the products of the cart Product::cacheProductsFeatures($products_ids); Cart::cacheSomeAttributesLists($pa_ids, (int) $this->getAssociatedLanguage()->getId()); if (empty($products)) { $this->_products = []; return []; } if ($fullInfos) { $cart_shop_context = Context::getContext()->cloneContext(); $givenAwayProductsIds = []; if ($this->shouldSplitGiftProductsQuantity && $refresh) { $gifts = $this->getCartRules(CartRule::FILTER_ACTION_GIFT, false); if (count($gifts) > 0) { foreach ($gifts as $gift) { foreach ($products as $rowIndex => $product) { if (!array_key_exists('is_gift', $products[$rowIndex])) { $products[$rowIndex]['is_gift'] = false; } if ( $product['id_product'] == $gift['gift_product'] && $product['id_product_attribute'] == $gift['gift_product_attribute'] ) { $product['is_gift'] = true; $products[$rowIndex] = $product; } } $index = $gift['gift_product'] . '-' . $gift['gift_product_attribute']; if (!array_key_exists($index, $givenAwayProductsIds)) { $givenAwayProductsIds[$index] = 1; } else { ++$givenAwayProductsIds[$index]; } } } } $this->_products = []; foreach ($products as &$product) { if (!array_key_exists('is_gift', $product)) { $product['is_gift'] = false; } $props = Product::getProductProperties((int) $this->id_lang, $product); $product['reduction'] = $props['reduction']; $product['reduction_without_tax'] = $props['reduction_without_tax']; $product['price_without_reduction'] = $props['price_without_reduction']; $product['specific_prices'] = $props['specific_prices']; $product['unit_price'] = $props['unit_price_tax_excluded']; $product['unit_price_ratio'] = $props['unit_price_ratio']; $product['unit_price_tax_excluded'] = $props['unit_price_tax_excluded']; $product['unit_price_tax_included'] = $props['unit_price_tax_included']; unset($props); $givenAwayQuantity = 0; $giftIndex = $product['id_product'] . '-' . $product['id_product_attribute']; if ($product['is_gift'] && array_key_exists($giftIndex, $givenAwayProductsIds)) { $givenAwayQuantity = $givenAwayProductsIds[$giftIndex]; } if (!$product['is_gift'] || (int) $product['cart_quantity'] === $givenAwayQuantity) { $product = $this->applyProductCalculations($product, $cart_shop_context, null, $keepOrderPrices); } else { // Separate products given away from those manually added to cart $this->_products[] = $this->applyProductCalculations($product, $cart_shop_context, $givenAwayQuantity, $keepOrderPrices); unset($product['is_gift']); $product = $this->applyProductCalculations( $product, $cart_shop_context, $product['cart_quantity'] - $givenAwayQuantity, $keepOrderPrices ); } $this->_products[] = $product; } } else { $this->_products = $products; } return $this->_products; } /** * @param array $row * @param Context $shopContext * @param int|null $productQuantity * @param bool $keepOrderPrices When true use the Order saved prices instead of the most recent ones from catalog (if Order exists) * * @return mixed */ protected function applyProductCalculations($row, $shopContext, $productQuantity = null, bool $keepOrderPrices = false) { if (null === $productQuantity) { $productQuantity = (int) $row['cart_quantity']; } if (isset($row['ecotax_attr']) && $row['ecotax_attr'] > 0) { $row['ecotax'] = (float) $row['ecotax_attr']; } $row['stock_quantity'] = (int) $row['quantity']; // for compatibility with 1.2 themes $row['quantity'] = $productQuantity; // get the customization weight impact $customization_weight = Customization::getCustomizationWeight($row['id_customization']); if (isset($row['id_product_attribute']) && (int) $row['id_product_attribute'] && isset($row['weight_attribute'])) { $row['weight_attribute'] += $customization_weight; $row['weight'] = (float) $row['weight_attribute']; } else { $row['weight'] += $customization_weight; } if (Configuration::get('PS_TAX_ADDRESS_TYPE') == 'id_address_invoice') { $address_id = (int) $this->id_address_invoice; } else { $address_id = (int) $row['id_address_delivery']; } if (!Address::addressExists($address_id, true)) { $address_id = null; } if ($shopContext->shop->id != $row['id_shop']) { $shopContext->shop = new Shop((int) $row['id_shop']); } $specific_price_output = null; // Specify the orderId if needed so that Product::getPriceStatic returns the prices saved in OrderDetails $orderId = null; if ($keepOrderPrices) { $orderId = Order::getIdByCartId($this->id); $orderId = (int) $orderId ?: null; } if (!empty($orderId)) { $orderPrices = $this->getOrderPrices($row, $orderId, $productQuantity, $address_id, $shopContext, $specific_price_output); $row = array_merge($row, $orderPrices); } else { $cartPrices = $this->getCartPrices($row, $productQuantity, $address_id, $shopContext, $specific_price_output); $row = array_merge($row, $cartPrices); } switch (Configuration::get('PS_ROUND_TYPE')) { case Order::ROUND_TOTAL: $row['total'] = $row['price_with_reduction_without_tax'] * $productQuantity; $row['total_wt'] = $row['price_with_reduction'] * $productQuantity; break; case Order::ROUND_LINE: $row['total'] = Tools::ps_round( $row['price_with_reduction_without_tax'] * $productQuantity, Context::getContext()->getComputingPrecision() ); $row['total_wt'] = Tools::ps_round( $row['price_with_reduction'] * $productQuantity, Context::getContext()->getComputingPrecision() ); break; case Order::ROUND_ITEM: default: $row['total'] = Tools::ps_round( $row['price_with_reduction_without_tax'], Context::getContext()->getComputingPrecision() ) * $productQuantity; $row['total_wt'] = Tools::ps_round( $row['price_with_reduction'], Context::getContext()->getComputingPrecision() ) * $productQuantity; break; } // Update unit price in case cart reductions happened $row['unit_price'] = $row['unit_price_tax_excluded'] = $row['unit_price_ratio'] != 0 ? $row['price_with_reduction_without_tax'] / $row['unit_price_ratio'] : 0.0; $row['unit_price_tax_included'] = $row['unit_price_ratio'] != 0 ? $row['price_with_reduction'] / $row['unit_price_ratio'] : 0.0; $row['price_wt'] = $row['price_with_reduction']; $row['description_short'] = Tools::nl2br($row['description_short']); // check if a image associated with the attribute exists if ($row['id_product_attribute']) { $row2 = Image::getBestImageAttribute($row['id_shop'], $this->getAssociatedLanguage()->getId(), $row['id_product'], $row['id_product_attribute']); if ($row2) { $row = array_merge($row, $row2); } } $row['reduction_applies'] = ($specific_price_output && (float) $specific_price_output['reduction']); $row['quantity_discount_applies'] = ($specific_price_output && $productQuantity >= (int) $specific_price_output['from_quantity']); $row['id_image'] = Product::defineProductImage($row, $this->getAssociatedLanguage()->getId()); $row['allow_oosp'] = Product::isAvailableWhenOutOfStock($row['out_of_stock']); $row['features'] = Product::getFeaturesStatic((int) $row['id_product']); $productAttributeKey = $row['id_product_attribute'] . '-' . $this->getAssociatedLanguage()->getId(); $row = array_merge( $row, self::$_attributesLists[$productAttributeKey] ?? self::DEFAULT_ATTRIBUTES_KEYS ); return Product::getTaxesInformations($row, $shopContext); } /** * @param array $productRow * @param int $productQuantity * @param int|null $addressId Customer's address id (for tax calculation) * @param Context $shopContext * @param array|false|null $specificPriceOutput * * @return array */ private function getCartPrices( array $productRow, int $productQuantity, ?int $addressId, Context $shopContext, &$specificPriceOutput ): array { $cartPrices = []; $cartPrices['price_without_reduction'] = $this->getCartPriceFromCatalog( (int) $productRow['id_product'], isset($productRow['id_product_attribute']) ? (int) $productRow['id_product_attribute'] : null, (int) $productRow['id_customization'], true, false, true, $productQuantity, $addressId, $shopContext, $specificPriceOutput ); $cartPrices['price_without_reduction_without_tax'] = $this->getCartPriceFromCatalog( (int) $productRow['id_product'], isset($productRow['id_product_attribute']) ? (int) $productRow['id_product_attribute'] : null, (int) $productRow['id_customization'], false, false, true, $productQuantity, $addressId, $shopContext, $specificPriceOutput ); $cartPrices['price_with_reduction'] = $this->getCartPriceFromCatalog( (int) $productRow['id_product'], isset($productRow['id_product_attribute']) ? (int) $productRow['id_product_attribute'] : null, (int) $productRow['id_customization'], true, true, true, $productQuantity, $addressId, $shopContext, $specificPriceOutput ); $cartPrices['price'] = $cartPrices['price_with_reduction_without_tax'] = $this->getCartPriceFromCatalog( (int) $productRow['id_product'], isset($productRow['id_product_attribute']) ? (int) $productRow['id_product_attribute'] : null, (int) $productRow['id_customization'], false, true, true, $productQuantity, $addressId, $shopContext, $specificPriceOutput ); return $cartPrices; } /** * @param int $productId * @param int $combinationId * @param int $customizationId * @param bool $withTaxes * @param bool $useReduction * @param bool $withEcoTax * @param int $productQuantity * @param int|null $addressId Customer's address id (for tax calculation) * @param Context $shopContext * @param array|false|null $specificPriceOutput * * @return float|null */ private function getCartPriceFromCatalog( int $productId, int $combinationId, int $customizationId, bool $withTaxes, bool $useReduction, bool $withEcoTax, int $productQuantity, ?int $addressId, Context $shopContext, &$specificPriceOutput ): ?float { return Product::getPriceStatic( $productId, $withTaxes, $combinationId, 6, null, false, $useReduction, $productQuantity, false, (int) $this->id_customer ? (int) $this->id_customer : null, (int) $this->id, $addressId, $specificPriceOutput, $withEcoTax, true, $shopContext, true, $customizationId ); } /** * @param array $productRow * @param int $orderId * @param int $productQuantity * @param int|null $addressId Customer's address id (for tax calculation) * @param Context $shopContext * @param array|false|null $specificPriceOutput * * @return array */ private function getOrderPrices( array $productRow, int $orderId, int $productQuantity, ?int $addressId, Context $shopContext, &$specificPriceOutput ): array { $orderPrices = []; $orderPrices['price_without_reduction'] = Product::getPriceFromOrder( $orderId, (int) $productRow['id_product'], isset($productRow['id_product_attribute']) ? (int) $productRow['id_product_attribute'] : 0, true, false, true ); $orderPrices['price_without_reduction_without_tax'] = Product::getPriceFromOrder( $orderId, (int) $productRow['id_product'], isset($productRow['id_product_attribute']) ? (int) $productRow['id_product_attribute'] : 0, false, false, true ); $orderPrices['price_with_reduction'] = Product::getPriceFromOrder( $orderId, (int) $productRow['id_product'], isset($productRow['id_product_attribute']) ? (int) $productRow['id_product_attribute'] : 0, true, true, true ); $orderPrices['price'] = $orderPrices['price_with_reduction_without_tax'] = Product::getPriceFromOrder( $orderId, (int) $productRow['id_product'], isset($productRow['id_product_attribute']) ? (int) $productRow['id_product_attribute'] : 0, false, true, true ); // If the product price was not found in the order, use cart prices as fallback if (false !== array_search(null, $orderPrices)) { $cartPrices = $this->getCartPrices( $productRow, $productQuantity, $addressId, $shopContext, $specificPriceOutput ); foreach ($orderPrices as $orderPrice => $value) { if (null === $value) { $orderPrices[$orderPrice] = $cartPrices[$orderPrice]; } } } return $orderPrices; } public static function cacheSomeAttributesLists($ipa_list, $id_lang) { if (!Combination::isFeatureActive()) { return; } $pa_implode = []; $separator = Configuration::get('PS_ATTRIBUTE_ANCHOR_SEPARATOR'); if ($separator === '-') { // Add a space before the dash between attributes $separator = ' -'; } foreach ($ipa_list as $id_product_attribute) { if ((int) $id_product_attribute && !array_key_exists($id_product_attribute . '-' . $id_lang, self::$_attributesLists)) { $pa_implode[] = (int) $id_product_attribute; self::$_attributesLists[(int) $id_product_attribute . '-' . $id_lang] = self::DEFAULT_ATTRIBUTES_KEYS; } } if (!count($pa_implode)) { return; } $result = Db::getInstance()->executeS( 'SELECT pac.`id_product_attribute`, agl.`public_name` AS public_group_name, al.`name` AS attribute_name FROM `' . _DB_PREFIX_ . 'product_attribute_combination` pac LEFT JOIN `' . _DB_PREFIX_ . 'attribute` a ON a.`id_attribute` = pac.`id_attribute` LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group` ag ON ag.`id_attribute_group` = a.`id_attribute_group` LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ON ( a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) $id_lang . ' ) LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl ON ( ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) $id_lang . ' ) WHERE pac.`id_product_attribute` IN (' . implode(',', $pa_implode) . ') ORDER BY ag.`position` ASC, a.`position` ASC' ); $colon = Context::getContext()->getTranslator()->trans(': ', [], 'Shop.Pdf'); foreach ($result as $row) { $key = $row['id_product_attribute'] . '-' . $id_lang; self::$_attributesLists[$key]['attributes'] .= $row['public_group_name'] . $colon . $row['attribute_name'] . $separator . ' '; self::$_attributesLists[$key]['attributes_small'] .= $row['attribute_name'] . $separator . ' '; } foreach ($pa_implode as $id_product_attribute) { self::$_attributesLists[$id_product_attribute . '-' . $id_lang]['attributes'] = rtrim( self::$_attributesLists[$id_product_attribute . '-' . $id_lang]['attributes'], $separator . ' ' ); self::$_attributesLists[$id_product_attribute . '-' . $id_lang]['attributes_small'] = rtrim( self::$_attributesLists[$id_product_attribute . '-' . $id_lang]['attributes_small'], $separator . ' ' ); } } /** * Check if Addresses in the Cart are still valid and update with the next valid Address ID found. * * @return bool Whether the Addresses have been succesfully checked and upated */ public function checkAndUpdateAddresses() { $needUpdate = false; foreach (['invoice', 'delivery'] as $type) { $addr = 'id_address_' . $type; if ($this->{$addr} != 0 && !Address::isValid($this->{$addr})) { $this->{$addr} = 0; $needUpdate = true; } } if ($needUpdate && $this->id) { return $this->update(); } return true; } /** * Return cart products quantity. * * @result integer Products quantity */ public function nbProducts() { if (!$this->id) { return 0; } return Cart::getNbProducts($this->id); } /** * Get number of products in cart * This is the total amount of products, not just the types. * * @param int $id Cart ID * * @return mixed */ public static function getNbProducts($id) { // Must be strictly compared to NULL, or else an empty cart will bypass the cache and add dozens of queries if (isset(self::$_nbProducts[$id]) && self::$_nbProducts[$id] !== null) { return self::$_nbProducts[$id]; } self::$_nbProducts[$id] = (int) Db::getInstance()->getValue( 'SELECT SUM(`quantity`) FROM `' . _DB_PREFIX_ . 'cart_product` WHERE `id_cart` = ' . (int) $id ); return self::$_nbProducts[$id]; } /** * Add a CartRule to the Cart. * * @param int $id_cart_rule CartRule ID * @param bool $useOrderPrices * * @return bool Whether the CartRule has been successfully added */ public function addCartRule($id_cart_rule, bool $useOrderPrices = false) { // You can't add a cart rule that does not exist $cartRule = new CartRule($id_cart_rule, Context::getContext()->language->id); if (!Validate::isLoadedObject($cartRule)) { return false; } if (Db::getInstance()->getValue('SELECT id_cart_rule FROM ' . _DB_PREFIX_ . 'cart_cart_rule WHERE id_cart_rule = ' . (int) $id_cart_rule . ' AND id_cart = ' . (int) $this->id)) { return false; } // Add the cart rule to the cart if (!Db::getInstance()->insert('cart_cart_rule', [ 'id_cart_rule' => (int) $id_cart_rule, 'id_cart' => (int) $this->id, ])) { return false; } Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_ALL); Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_SHIPPING); Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_REDUCTION); Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_GIFT); Cache::clean('Cart::getOrderedCartRulesIds_' . $this->id . '-' . CartRule::FILTER_ACTION_ALL . '-ids'); Cache::clean('Cart::getOrderedCartRulesIds_' . $this->id . '-' . CartRule::FILTER_ACTION_SHIPPING . '-ids'); Cache::clean('Cart::getOrderedCartRulesIds_' . $this->id . '-' . CartRule::FILTER_ACTION_REDUCTION . '-ids'); Cache::clean('Cart::getOrderedCartRulesIds_' . $this->id . '-' . CartRule::FILTER_ACTION_GIFT . '-ids'); if ((int) $cartRule->gift_product) { $this->updateQty( 1, $cartRule->gift_product, $cartRule->gift_product_attribute, false, 'up', 0, null, false, false, true, $useOrderPrices ); } return true; } /** * Check if the Cart contains the given Product (Attribute). * * @param int $idProduct Product ID * @param int $idProductAttribute ProductAttribute ID * @param int|bool $idCustomization Customization ID * @param int $idAddressDelivery Delivery Address ID * * @return array quantity index : number of product in cart without counting those of pack in cart * deep_quantity index: number of product in cart counting those of pack in cart */ public function getProductQuantity($idProduct, $idProductAttribute = 0, $idCustomization = 0, $idAddressDelivery = 0) { $defaultPackStockType = Configuration::get('PS_PACK_STOCK_TYPE'); $packStockTypesAllowed = [ Pack::STOCK_TYPE_PRODUCTS_ONLY, Pack::STOCK_TYPE_PACK_BOTH, ]; $packStockTypesDefaultSupported = (int) in_array($defaultPackStockType, $packStockTypesAllowed); // We need to SUM up cp.`quantity` because multiple rows could be returned when id_customization filtering is skipped. $firstUnionSql = 'SELECT SUM(cp.`quantity`) as first_level_quantity, 0 as pack_quantity FROM `' . _DB_PREFIX_ . 'cart_product` cp'; $secondUnionSql = 'SELECT 0 as first_level_quantity, SUM(cp.`quantity` * p.`quantity`) as pack_quantity FROM `' . _DB_PREFIX_ . 'cart_product` cp' . ' JOIN `' . _DB_PREFIX_ . 'pack` p ON cp.`id_product` = p.`id_product_pack`' . ' JOIN `' . _DB_PREFIX_ . 'product` pr ON p.`id_product_pack` = pr.`id_product`'; if ($idCustomization) { $customizationJoin = ' LEFT JOIN `' . _DB_PREFIX_ . 'customization` c ON ( c.`id_product` = cp.`id_product` AND c.`id_product_attribute` = cp.`id_product_attribute` )'; $firstUnionSql .= $customizationJoin; $secondUnionSql .= $customizationJoin; } // Ignore customizations if $idCustomization is set to false // This is necessary to get products with or without customizations $commonWhere = ' WHERE cp.`id_product_attribute` = ' . (int) $idProductAttribute . ' ' . ($idCustomization !== false ? ' AND cp.`id_customization` = ' . (int) $idCustomization : '') . ' AND cp.`id_cart` = ' . (int) $this->id; if (Configuration::get('PS_ALLOW_MULTISHIPPING') && $this->isMultiAddressDelivery()) { $commonWhere .= ' AND cp.`id_address_delivery` = ' . (int) $idAddressDelivery; } if ($idCustomization) { $commonWhere .= ' AND c.`id_customization` = ' . (int) $idCustomization; } $firstUnionSql .= $commonWhere; $firstUnionSql .= ' AND cp.`id_product` = ' . (int) $idProduct; $secondUnionSql .= $commonWhere; $secondUnionSql .= ' AND p.`id_product_item` = ' . (int) $idProduct; $secondUnionSql .= ' AND (pr.`pack_stock_type` IN (' . implode(',', $packStockTypesAllowed) . ') OR ( pr.`pack_stock_type` = ' . Pack::STOCK_TYPE_DEFAULT . ' AND ' . $packStockTypesDefaultSupported . ' = 1 ))'; $parentSql = 'SELECT COALESCE(SUM(first_level_quantity) + SUM(pack_quantity), 0) as deep_quantity, COALESCE(SUM(first_level_quantity), 0) as quantity FROM (' . $firstUnionSql . ' UNION ' . $secondUnionSql . ') as q'; return Db::getInstance()->getRow($parentSql); } /** * Check if the Cart contains the given Product (Attribute). * * @deprecated 1.7.3.1 * @see Cart::getProductQuantity() * * @param int $id_product Product ID * @param int $id_product_attribute ProductAttribute ID * @param int $id_customization Customization ID * @param int $id_address_delivery Delivery Address ID * * @return array|bool Whether the Cart contains the Product * Result comes directly from the database */ public function containsProduct($id_product, $id_product_attribute = 0, $id_customization = 0, $id_address_delivery = 0) { $result = $this->getProductQuantity( $id_product, $id_product_attribute, $id_customization, $id_address_delivery ); if (empty($result['quantity'])) { return false; } return ['quantity' => $result['quantity']]; } /** * Update Product quantity. * * @param int $quantity Quantity to add (or substract) * @param int $id_product Product ID * @param int|null $id_product_attribute Attribute ID if needed * @param int|false $id_customization Customization ID * @param string $operator Indicate if quantity must be increased or decreased * @param int $id_address_delivery Delivery Address ID * @param Shop|null $shop * @param bool $auto_add_cart_rule * @param bool $skipAvailabilityCheckOutOfStock * @param bool $preserveGiftRemoval * @param bool $useOrderPrices * * @return bool|int Whether the quantity has been successfully updated */ public function updateQty( $quantity, $id_product, $id_product_attribute = null, $id_customization = false, $operator = 'up', $id_address_delivery = 0, Shop $shop = null, $auto_add_cart_rule = true, $skipAvailabilityCheckOutOfStock = false, bool $preserveGiftRemoval = true, bool $useOrderPrices = false ) { if (!$shop) { $shop = Context::getContext()->shop; } if (Validate::isLoadedObject(Context::getContext()->customer)) { if ($id_address_delivery == 0 && (int) $this->id_address_delivery) { // The $id_address_delivery is null, use the cart delivery address $id_address_delivery = $this->id_address_delivery; } elseif ($id_address_delivery == 0) { // The $id_address_delivery is null, get the default customer address $id_address_delivery = (int) Address::getFirstCustomerAddressId( (int) Context::getContext()->customer->id ); } elseif (!Customer::customerHasAddress(Context::getContext()->customer->id, $id_address_delivery)) { // The $id_address_delivery must be linked with customer $id_address_delivery = 0; } } else { $id_address_delivery = 0; } $quantity = (int) $quantity; $id_product = (int) $id_product; $id_product_attribute = (int) $id_product_attribute; $product = new Product($id_product, false, (int) Configuration::get('PS_LANG_DEFAULT'), $shop->id); if ($id_product_attribute) { $combination = new Combination((int) $id_product_attribute); if ($combination->id_product != $id_product) { return false; } } /* If we have a product combination, the minimal quantity is set with the one of this combination */ if (!empty($id_product_attribute)) { $minimal_quantity = (int) ProductAttribute::getAttributeMinimalQty($id_product_attribute); } else { $minimal_quantity = (int) $product->minimal_quantity; } if (!Validate::isLoadedObject($product)) { die(Tools::displayError(sprintf('Product with ID "%s" could not be loaded.', $id_product))); } if (isset(self::$_nbProducts[$this->id])) { unset(self::$_nbProducts[$this->id]); } if (isset(self::$_totalWeight[$this->id])) { unset(self::$_totalWeight[$this->id]); } $data = [ 'cart' => $this, 'product' => $product, 'id_product_attribute' => $id_product_attribute, 'id_customization' => $id_customization, 'quantity' => $quantity, 'operator' => $operator, 'id_address_delivery' => $id_address_delivery, 'shop' => $shop, 'auto_add_cart_rule' => $auto_add_cart_rule, ]; Hook::exec('actionCartUpdateQuantityBefore', $data); if ((int) $quantity <= 0) { return $this->deleteProduct($id_product, $id_product_attribute, (int) $id_customization, (int) $id_address_delivery, $preserveGiftRemoval, $useOrderPrices); } if (!$product->available_for_order || ( Configuration::isCatalogMode() && !defined('_PS_ADMIN_DIR_') ) ) { return false; } /* Check if the product is already in the cart */ $cartProductQuantity = $this->getProductQuantity( $id_product, $id_product_attribute, (int) $id_customization, (int) $id_address_delivery ); /* Update quantity if product already exist */ if (!empty($cartProductQuantity['quantity'])) { $productQuantity = Product::getQuantity($id_product, $id_product_attribute, null, $this, false); $availableOutOfStock = Product::isAvailableWhenOutOfStock(StockAvailable::outOfStock($product->id)); if ($operator == 'up') { $updateQuantity = '+ ' . $quantity; $newProductQuantity = $productQuantity - $quantity; if ($newProductQuantity < 0 && !$availableOutOfStock && !$skipAvailabilityCheckOutOfStock) { return false; } } elseif ($operator == 'down') { $cartFirstLevelProductQuantity = $this->getProductQuantity( (int) $id_product, (int) $id_product_attribute, $id_customization ); $updateQuantity = '- ' . $quantity; if ($cartFirstLevelProductQuantity['quantity'] <= 1 || $cartProductQuantity['quantity'] - $quantity <= 0 ) { return $this->deleteProduct((int) $id_product, (int) $id_product_attribute, (int) $id_customization, (int) $id_address_delivery, $preserveGiftRemoval, $useOrderPrices); } } else { return false; } Db::getInstance()->execute( 'UPDATE `' . _DB_PREFIX_ . 'cart_product` SET `quantity` = `quantity` ' . $updateQuantity . ' WHERE `id_product` = ' . (int) $id_product . ' AND `id_customization` = ' . (int) $id_customization . (!empty($id_product_attribute) ? ' AND `id_product_attribute` = ' . (int) $id_product_attribute : '') . ' AND `id_cart` = ' . (int) $this->id . (Configuration::get('PS_ALLOW_MULTISHIPPING') && $this->isMultiAddressDelivery() ? ' AND `id_address_delivery` = ' . (int) $id_address_delivery : '') . ' LIMIT 1' ); } elseif ($operator == 'up') { /* Add product to the cart */ $sql = 'SELECT stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity FROM ' . _DB_PREFIX_ . 'product p ' . Product::sqlStock('p', $id_product_attribute, true, $shop) . ' WHERE p.id_product = ' . $id_product; $result2 = Db::getInstance()->getRow($sql); // Quantity for product pack if (Pack::isPack($id_product)) { $result2['quantity'] = Pack::getQuantity($id_product, $id_product_attribute, null, $this, false); } if (isset($result2['out_of_stock']) && !Product::isAvailableWhenOutOfStock((int) $result2['out_of_stock']) && !$skipAvailabilityCheckOutOfStock) { if ((int) $quantity > $result2['quantity']) { return false; } } if ((int) $quantity < $minimal_quantity) { return -1; } $result_add = Db::getInstance()->insert('cart_product', [ 'id_product' => (int) $id_product, 'id_product_attribute' => (int) $id_product_attribute, 'id_cart' => (int) $this->id, 'id_address_delivery' => (int) $id_address_delivery, 'id_shop' => $shop->id, 'quantity' => (int) $quantity, 'date_add' => date('Y-m-d H:i:s'), 'id_customization' => (int) $id_customization, ]); if (!$result_add) { return false; } } // refresh cache of self::_products $this->_products = $this->getProducts(true); $this->update(); $context = Context::getContext()->cloneContext(); /* @phpstan-ignore-next-line */ $context->cart = $this; Cache::clean('getContextualValue_*'); CartRule::autoRemoveFromCart(null, $useOrderPrices); if ($auto_add_cart_rule) { CartRule::autoAddToCart($context, $useOrderPrices); } if ($product->customizable) { return $this->_updateCustomizationQuantity( (int) $quantity, (int) $id_customization, (int) $id_product, (int) $id_product_attribute, (int) $id_address_delivery, $operator ); } return true; } /** * Customization management. * * @param int $quantity Quantity value to add or subtract * @param int $id_customization Customization ID * @param int $id_product Product ID * @param int $id_product_attribute ProductAttribute ID * @param int $id_address_delivery Delivery Address ID * @param string $operator Indicate if quantity must be increased (up) or decreased (down) * * @return bool */ protected function _updateCustomizationQuantity($quantity, $id_customization, $id_product, $id_product_attribute, $id_address_delivery, $operator = 'up') { // Link customization to product combination when it is first added to cart if (empty($id_customization)) { $customization = $this->getProductCustomization($id_product, null, true); foreach ($customization as $field) { if ($field['quantity'] == 0) { Db::getInstance()->execute(' UPDATE `' . _DB_PREFIX_ . 'customization` SET `quantity` = ' . (int) $quantity . ', `id_product_attribute` = ' . (int) $id_product_attribute . ', `id_address_delivery` = ' . (int) $id_address_delivery . ', `in_cart` = 1 WHERE `id_customization` = ' . (int) $field['id_customization']); } } } /* Deletion */ if (!empty($id_customization) && (int) $quantity < 1) { return $this->_deleteCustomization((int) $id_customization, (int) $id_product, (int) $id_product_attribute); } /* Quantity update */ if (!empty($id_customization)) { $result = Db::getInstance()->getRow('SELECT `quantity` FROM `' . _DB_PREFIX_ . 'customization` WHERE `id_customization` = ' . (int) $id_customization); if ($result && Db::getInstance()->numRows()) { if ($operator == 'down' && (int) $result['quantity'] - (int) $quantity < 1) { return Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'customization` WHERE `id_customization` = ' . (int) $id_customization); } return Db::getInstance()->execute(' UPDATE `' . _DB_PREFIX_ . 'customization` SET `quantity` = `quantity` ' . ($operator == 'up' ? '+ ' : '- ') . (int) $quantity . ', `id_product_attribute` = ' . (int) $id_product_attribute . ', `id_address_delivery` = ' . (int) $id_address_delivery . ', `in_cart` = 1 WHERE `id_customization` = ' . (int) $id_customization); } else { Db::getInstance()->execute(' UPDATE `' . _DB_PREFIX_ . 'customization` SET `id_address_delivery` = ' . (int) $id_address_delivery . ', `id_product_attribute` = ' . (int) $id_product_attribute . ', `in_cart` = 1 WHERE `id_customization` = ' . (int) $id_customization); } } // refresh cache of self::_products $this->_products = $this->getProducts(true); $this->update(); return true; } /** * Add customization item to database. * * @param int $id_product Product ID * @param int $id_product_attribute ProductAttribute ID * @param int $index Customization field identifier as id_customization_field in table customization_field * @param int $type Customization type can be Product::CUSTOMIZE_FILE or Product::CUSTOMIZE_TEXTFIELD * @param string $value Customization value * @param int $quantity Quantity value * @param bool $returnId if true - returns the customization record id * * @return bool|int */ public function _addCustomization($id_product, $id_product_attribute, $index, $type, $value, $quantity, $returnId = false) { $exising_customization = Db::getInstance()->executeS( 'SELECT cu.`id_customization`, cd.`index`, cd.`value`, cd.`type` FROM `' . _DB_PREFIX_ . 'customization` cu LEFT JOIN `' . _DB_PREFIX_ . 'customized_data` cd ON cu.`id_customization` = cd.`id_customization` WHERE cu.id_cart = ' . (int) $this->id . ' AND cu.id_product = ' . (int) $id_product . ' AND in_cart = 0' ); if ($exising_customization) { // If the customization field is alreay filled, delete it foreach ($exising_customization as $customization) { if ($customization['type'] == $type && $customization['index'] == $index) { Db::getInstance()->execute(' DELETE FROM `' . _DB_PREFIX_ . 'customized_data` WHERE id_customization = ' . (int) $customization['id_customization'] . ' AND type = ' . (int) $customization['type'] . ' AND `index` = ' . (int) $customization['index']); if ($type == Product::CUSTOMIZE_FILE) { @unlink(_PS_UPLOAD_DIR_ . $customization['value']); @unlink(_PS_UPLOAD_DIR_ . $customization['value'] . '_small'); } break; } } $id_customization = $exising_customization[0]['id_customization']; } else { Db::getInstance()->execute( 'INSERT INTO `' . _DB_PREFIX_ . 'customization` (`id_cart`, `id_product`, `id_product_attribute`, `quantity`) VALUES (' . (int) $this->id . ', ' . (int) $id_product . ', ' . (int) $id_product_attribute . ', ' . (int) $quantity . ')' ); $id_customization = Db::getInstance()->Insert_ID(); } $query = 'INSERT INTO `' . _DB_PREFIX_ . 'customized_data` (`id_customization`, `type`, `index`, `value`) VALUES (' . (int) $id_customization . ', ' . (int) $type . ', ' . (int) $index . ', \'' . pSQL($value) . '\')'; if (!Db::getInstance()->execute($query)) { return false; } if (true === $returnId) { return (int) $id_customization; } return true; } /** * Check if order has already been placed. * * @return bool Indicates if the Order exists */ public function orderExists() { return (bool) Db::getInstance()->getValue( 'SELECT count(*) FROM `' . _DB_PREFIX_ . 'orders` WHERE `id_cart` = ' . (int) $this->id, false ); } /** * Remove the CartRule from the Cart. * * @param int $id_cart_rule CartRule ID * @param bool $useOrderPrices * * @return bool Whether the Cart rule has been successfully removed */ public function removeCartRule($id_cart_rule, bool $useOrderPrices = false) { Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_ALL); Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_SHIPPING); Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_REDUCTION); Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_GIFT); Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_ALL . '-ids'); Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_SHIPPING . '-ids'); Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_REDUCTION . '-ids'); Cache::clean('Cart::getCartRules_' . $this->id . '-' . CartRule::FILTER_ACTION_GIFT . '-ids'); $result = Db::getInstance()->delete('cart_cart_rule', '`id_cart_rule` = ' . (int) $id_cart_rule . ' AND `id_cart` = ' . (int) $this->id, 1); $cart_rule = new CartRule($id_cart_rule, (int) Configuration::get('PS_LANG_DEFAULT')); if ((bool) $result && (int) $cart_rule->gift_product) { $this->updateQty(1, $cart_rule->gift_product, $cart_rule->gift_product_attribute, false, 'down', 0, null, false, false, true, $useOrderPrices); } return $result; } /** * Delete a product from the cart. * * @param int $id_product Product ID * @param int|null $id_product_attribute Attribute ID if needed * @param int $id_customization Customization id * @param int $id_address_delivery Delivery Address id * @param bool $preserveGiftsRemoval If true gift are not removed so product is still in cart * @param bool $useOrderPrices If true, will use order prices to re-calculate cartRules after the product is deleted * * @return bool Whether the product has been successfully deleted */ public function deleteProduct( $id_product, $id_product_attribute = 0, $id_customization = 0, $id_address_delivery = 0, bool $preserveGiftsRemoval = true, bool $useOrderPrices = false ) { if (isset(self::$_nbProducts[$this->id])) { unset(self::$_nbProducts[$this->id]); } if (isset(self::$_totalWeight[$this->id])) { unset(self::$_totalWeight[$this->id]); } if ((int) $id_customization) { if (!$this->_deleteCustomization((int) $id_customization, (int) $id_product, (int) $id_product_attribute, (int) $id_address_delivery)) { return false; } } /* Get customization quantity */ $result = Db::getInstance()->getRow(' SELECT SUM(`quantity`) AS \'quantity\' FROM `' . _DB_PREFIX_ . 'customization` WHERE `id_cart` = ' . (int) $this->id . ' AND `id_product` = ' . (int) $id_product . ' AND `id_customization` = ' . (int) $id_customization . ' AND `id_product_attribute` = ' . (int) $id_product_attribute); if ($result === false) { return false; } /* If the product still possesses customization it does not have to be deleted */ if (Db::getInstance()->numRows() && (int) $result['quantity']) { return Db::getInstance()->execute( 'UPDATE `' . _DB_PREFIX_ . 'cart_product` SET `quantity` = ' . (int) $result['quantity'] . ' WHERE `id_cart` = ' . (int) $this->id . ' AND `id_product` = ' . (int) $id_product . ' AND `id_customization` = ' . (int) $id_customization . ($id_product_attribute != null ? ' AND `id_product_attribute` = ' . (int) $id_product_attribute : '') ); } $preservedGifts = []; $giftKey = (int) $id_product . '-' . (int) $id_product_attribute; if ($preserveGiftsRemoval) { $preservedGifts = $this->getProductsGifts($id_product, $id_product_attribute); if (isset($preservedGifts[$giftKey]) && $preservedGifts[$giftKey] > 0) { return Db::getInstance()->execute( 'UPDATE `' . _DB_PREFIX_ . 'cart_product` SET `quantity` = ' . (int) $preservedGifts[(int) $id_product . '-' . (int) $id_product_attribute] . ' WHERE `id_cart` = ' . (int) $this->id . ' AND `id_product` = ' . (int) $id_product . ($id_product_attribute != null ? ' AND `id_product_attribute` = ' . (int) $id_product_attribute : '') ); } } /* Product deletion */ $result = Db::getInstance()->execute(' DELETE FROM `' . _DB_PREFIX_ . 'cart_product` WHERE `id_product` = ' . (int) $id_product . ' AND `id_customization` = ' . (int) $id_customization . (null !== $id_product_attribute ? ' AND `id_product_attribute` = ' . (int) $id_product_attribute : '') . ' AND `id_cart` = ' . (int) $this->id . ' ' . ((int) $id_address_delivery ? 'AND `id_address_delivery` = ' . (int) $id_address_delivery : '')); if ($result) { $return = $this->update(); // refresh cache of self::_products $this->_products = $this->getProducts(true); if (!isset($preservedGifts[$giftKey]) || $preservedGifts[$giftKey] <= 0) { CartRule::autoRemoveFromCart(null, $useOrderPrices); CartRule::autoAddToCart(null, $useOrderPrices); } return $return; } return false; } /** * @param int $id_product * @param int $id_product_attribute * * @return array */ protected function getProductsGifts($id_product, $id_product_attribute) { $id_product_attribute = (int) $id_product_attribute; $gifts = array_filter($this->getProductsWithSeparatedGifts(), function ($product) { return array_key_exists('is_gift', $product) && $product['is_gift']; }); $preservedGifts = [$id_product . '-' . $id_product_attribute => 0]; foreach ($gifts as $gift) { if ( (int) $gift['id_product_attribute'] === $id_product_attribute && (int) $gift['id_product'] === $id_product ) { ++$preservedGifts[$id_product . '-' . $id_product_attribute]; } } return $preservedGifts; } /** * Delete a Customization from the Cart. If the Customization is a Picture, * then the Image is also deleted. * * @param int $id_customization Customization Id * @param int|null $id_product Unused * @param int|null $id_product_attribute Unused * @param int|null $id_address_delivery Unused * * @return bool Indicates if the Customization was successfully deleted * @todo: Remove unused parameters */ protected function _deleteCustomization($id_customization, $id_product, $id_product_attribute, $id_address_delivery = 0) { $result = true; $customization = Db::getInstance()->getRow('SELECT * FROM `' . _DB_PREFIX_ . 'customization` WHERE `id_customization` = ' . (int) $id_customization); if ($customization) { $cust_data = Db::getInstance()->getRow('SELECT * FROM `' . _DB_PREFIX_ . 'customized_data` WHERE `id_customization` = ' . (int) $id_customization); // Delete customization picture if necessary if (isset($cust_data['type']) && $cust_data['type'] == Product::CUSTOMIZE_FILE) { $result &= file_exists(_PS_UPLOAD_DIR_ . $cust_data['value']) ? @unlink(_PS_UPLOAD_DIR_ . $cust_data['value']) : true; $result &= file_exists(_PS_UPLOAD_DIR_ . $cust_data['value'] . '_small') ? @unlink(_PS_UPLOAD_DIR_ . $cust_data['value'] . '_small') : true; } $result &= Db::getInstance()->execute( 'DELETE FROM `' . _DB_PREFIX_ . 'customized_data` WHERE `id_customization` = ' . (int) $id_customization ); if (!$result) { return false; } return Db::getInstance()->execute( 'DELETE FROM `' . _DB_PREFIX_ . 'customization` WHERE `id_customization` = ' . (int) $id_customization ); } return true; } /** * Get formatted total amount in Cart. * * @param int $id_cart Cart ID * @param bool $use_tax_display Whether the tax should be displayed * @param int $type Type enum: * - ONLY_PRODUCTS * - ONLY_DISCOUNTS * - BOTH * - BOTH_WITHOUT_SHIPPING * - ONLY_SHIPPING * - ONLY_WRAPPING * * @return string Formatted amount in Cart */ public static function getTotalCart($id_cart, $use_tax_display = false, $type = Cart::BOTH) { $cart = new Cart($id_cart); if (!Validate::isLoadedObject($cart)) { die(Tools::displayError(sprintf('Cart with ID "%s" could not be loaded.', $id_cart))); } $with_taxes = $use_tax_display ? $cart->_taxCalculationMethod != PS_TAX_EXC : true; return Context::getContext()->getCurrentLocale()->formatPrice( $cart->getOrderTotal($with_taxes, $type), Currency::getIsoCodeById((int) $cart->id_currency) ); } /** * Get total in Cart using a tax calculation method. * * @param int $id_cart Cart ID * * @return string Formatted total amount in Cart * @todo: What is this? */ public static function getOrderTotalUsingTaxCalculationMethod($id_cart) { return Cart::getTotalCart($id_cart, true); } /** * This function returns the total cart amount. * * @param bool $withTaxes With or without taxes * @param int $type Total type enum * - Cart::ONLY_PRODUCTS * - Cart::ONLY_DISCOUNTS * - Cart::BOTH * - Cart::BOTH_WITHOUT_SHIPPING * - Cart::ONLY_SHIPPING * - Cart::ONLY_WRAPPING * - Cart::ONLY_PRODUCTS_WITHOUT_SHIPPING * - Cart::ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING * @param array $products * @param int $id_carrier * @param bool $use_cache @deprecated * @param bool $keepOrderPrices When true use the Order saved prices instead of the most recent ones from catalog (if Order exists) * * @return float Order total * * @throws \Exception */ public function getOrderTotal( $withTaxes = true, $type = Cart::BOTH, $products = null, $id_carrier = null, $use_cache = false, bool $keepOrderPrices = false ) { if ((int) $id_carrier <= 0) { $id_carrier = null; } // deprecated type if ($type == Cart::ONLY_PRODUCTS_WITHOUT_SHIPPING) { $type = Cart::ONLY_PRODUCTS; } // check type $type = (int) $type; $allowedTypes = [ Cart::ONLY_PRODUCTS, Cart::ONLY_DISCOUNTS, Cart::BOTH, Cart::BOTH_WITHOUT_SHIPPING, Cart::ONLY_SHIPPING, Cart::ONLY_WRAPPING, Cart::ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING, ]; if (!in_array($type, $allowedTypes)) { throw new \Exception('Invalid calculation type: ' . $type); } // EARLY RETURNS // if cart rules are not used if ($type == Cart::ONLY_DISCOUNTS && !CartRule::isFeatureActive()) { return 0; } // no shipping cost if is a cart with only virtuals products $virtual = $this->isVirtualCart(); if ($virtual && $type == Cart::ONLY_SHIPPING) { return 0; } if ($virtual && $type == Cart::BOTH) { $type = Cart::BOTH_WITHOUT_SHIPPING; } // filter products if (null === $products) { $products = $this->getProducts(false, false, null, true, $keepOrderPrices); } if ($type == Cart::ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING) { foreach ($products as $key => $product) { if (!empty($product['is_virtual'])) { unset($products[$key]); } } $type = Cart::ONLY_PRODUCTS; } if ($type == Cart::ONLY_PRODUCTS) { foreach ($products as $key => $product) { if (!empty($product['is_gift'])) { unset($products[$key]); } } } if (Tax::excludeTaxeOption()) { $withTaxes = false; } // CART CALCULATION $cartRules = []; if (in_array($type, [Cart::BOTH, Cart::BOTH_WITHOUT_SHIPPING, Cart::ONLY_DISCOUNTS])) { $cartRules = $this->getTotalCalculationCartRules($type, $type == Cart::BOTH); } $computePrecision = Context::getContext()->getComputingPrecision(); $calculator = $this->newCalculator($products, $cartRules, $id_carrier, $computePrecision, $keepOrderPrices); switch ($type) { case Cart::ONLY_SHIPPING: $calculator->calculateRows(); $calculator->calculateFees(); $amount = $calculator->getFees()->getInitialShippingFees(); break; case Cart::ONLY_WRAPPING: $calculator->calculateRows(); $calculator->calculateFees(); $amount = $calculator->getFees()->getInitialWrappingFees(); break; case Cart::BOTH: $calculator->processCalculation(); $amount = $calculator->getTotal(); break; case Cart::BOTH_WITHOUT_SHIPPING: $calculator->calculateRows(); // dont process free shipping to avoid calculation loop (and maximum nested functions !) $calculator->calculateCartRulesWithoutFreeShipping(); $amount = $calculator->getTotal(true); break; case Cart::ONLY_PRODUCTS: $calculator->calculateRows(); $amount = $calculator->getRowTotal(); break; case Cart::ONLY_DISCOUNTS: $calculator->processCalculation(); $amount = $calculator->getDiscountTotal(); break; default: throw new \Exception('unknown cart calculation type : ' . $type); } // TAXES ? $value = $withTaxes ? $amount->getTaxIncluded() : $amount->getTaxExcluded(); // ROUND AND RETURN return Tools::ps_round($value, $computePrecision); } /** * get the populated cart calculator. * * @param array $products list of products to calculate on * @param array $cartRules list of cart rules to apply * @param int $id_carrier carrier id (fees calculation) * @param int|null $computePrecision * @param bool $keepOrderPrices When true use the Order saved prices instead of the most recent ones from catalog (if Order exists) * * @return \PrestaShop\PrestaShop\Core\Cart\Calculator */ public function newCalculator($products, $cartRules, $id_carrier, $computePrecision = null, bool $keepOrderPrices = false) { $orderId = null; if ($keepOrderPrices) { $orderId = Order::getIdByCartId($this->id); $orderId = (int) $orderId ?: null; } $calculator = new Calculator( $this, $id_carrier, $computePrecision, $orderId ); /** @var PriceCalculator $priceCalculator */ $priceCalculator = ServiceLocator::get(PriceCalculator::class); // set cart rows (products) $useEcotax = $this->configuration->get('PS_USE_ECOTAX'); $precision = Context::getContext()->getComputingPrecision(); $configRoundType = $this->configuration->get('PS_ROUND_TYPE'); $roundTypes = [ Order::ROUND_TOTAL => CartRow::ROUND_MODE_TOTAL, Order::ROUND_LINE => CartRow::ROUND_MODE_LINE, Order::ROUND_ITEM => CartRow::ROUND_MODE_ITEM, ]; if (isset($roundTypes[$configRoundType])) { $roundType = $roundTypes[$configRoundType]; } else { $roundType = CartRow::ROUND_MODE_ITEM; } foreach ($products as $product) { $cartRow = new CartRow( $product, $priceCalculator, new AddressFactory(), new CustomerDataProvider(), new CacheAdapter(), new GroupDataProvider(), new Database(), $useEcotax, $precision, $roundType, $orderId ); $calculator->addCartRow($cartRow); } // set cart rules foreach ($cartRules as $cartRule) { $calculator->addCartRule(new CartRuleData($cartRule)); } return $calculator; } /** * @return float */ public function getDiscountSubtotalWithoutGifts($withTaxes = true) { $discountSubtotal = $this->excludeGiftsDiscountFromTotal() ->getOrderTotal($withTaxes, self::ONLY_DISCOUNTS); $this->includeGiftsDiscountInTotal(); return $discountSubtotal; } /** * @param array $products * * @return array */ protected function countProductLines($products) { $productsLines = []; array_map(function ($product) use (&$productsLines) { $productIndex = $product['id_product'] . '-' . $product['id_product_attribute']; if (!array_key_exists($productIndex, $productsLines)) { $productsLines[$product['id_product'] . '-' . $product['id_product_attribute']] = 1; } else { ++$productsLines[$product['id_product'] . '-' . $product['id_product_attribute']]; } }, $products); return $productsLines; } /** * @param array $products * * @return int */ protected function getDeliveryAddressId($products) { return $products[0]['id_address_delivery'] ?? $this->id_address_delivery; } /** * @param int $type * @param bool $withShipping * * @return array */ protected function getTotalCalculationCartRules($type, $withShipping) { if ($withShipping || $type == Cart::ONLY_DISCOUNTS) { $cartRules = $this->getCartRules(CartRule::FILTER_ACTION_ALL, false); } else { $cartRules = $this->getCartRules(CartRule::FILTER_ACTION_REDUCTION, false); // Cart Rules array are merged manually in order to avoid doubles foreach ($this->getCartRules(CartRule::FILTER_ACTION_GIFT, false) as $cartRuleCandidate) { $alreadyAddedCartRule = false; foreach ($cartRules as $cartRule) { if ($cartRuleCandidate['id_cart_rule'] == $cartRule['id_cart_rule']) { $alreadyAddedCartRule = true; } } if (!$alreadyAddedCartRule) { $cartRules[] = $cartRuleCandidate; } } } return $cartRules; } /** * @param bool $withTaxes * @param array $product * @param Context|null $virtualContext * * @return int */ protected function findTaxRulesGroupId($withTaxes, $product, $virtualContext) { if ($withTaxes) { $taxRulesGroupId = Product::getIdTaxRulesGroupByIdProduct((int) $product['id_product'], $virtualContext); $addressId = $this->getProductAddressId($product); $address = $this->addressFactory->findOrCreate($addressId, true); // Refresh cache and execute tax manager factory hook TaxManagerFactory::getManager($address, $taxRulesGroupId)->getTaxCalculator(); } else { $taxRulesGroupId = 0; } return $taxRulesGroupId; } /** * @param array $product * * @return int|null */ public function getProductAddressId($product) { $taxAddressType = $this->configuration->get('PS_TAX_ADDRESS_TYPE'); if ($taxAddressType == 'id_address_invoice') { $addressId = (int) $this->id_address_invoice; } else { $addressId = (int) $product['id_address_delivery']; } // Get delivery address of the product from the cart if (!$this->addressFactory->addressExists($addressId, true)) { $addressId = null; } return $addressId; } /** * Returns the tax address id according to the shop's configuration * * @return int */ public function getTaxAddressId() { $taxAddressType = $this->configuration->get('PS_TAX_ADDRESS_TYPE'); if (Validate::isLoadedObject($this) && !empty($taxAddressType)) { $addressId = $this->$taxAddressType; } else { $addressId = $this->id_address_delivery; } return $addressId; } /** * @param bool $withTaxes * @param int $type * * @return float|int */ protected function calculateWrappingFees($withTaxes, $type) { // Wrapping Fees $wrapping_fees = 0; // With PS_ATCP_SHIPWRAP on the gift wrapping cost computation calls getOrderTotal // with $type === Cart::ONLY_PRODUCTS, so the flag below prevents an infinite recursion. $includeGiftWrapping = (!$this->configuration->get('PS_ATCP_SHIPWRAP') || $type !== Cart::ONLY_PRODUCTS); $computePrecision = Context::getContext()->getComputingPrecision(); if ($this->gift && $includeGiftWrapping) { $wrapping_fees = Tools::convertPrice( Tools::ps_round( $this->getGiftWrappingPrice($withTaxes), $computePrecision ), Currency::getCurrencyInstance((int) $this->id_currency) ); } return $wrapping_fees; } /** * Get the gift wrapping price. * * @param bool $with_taxes With or without taxes * * @return float wrapping price */ public function getGiftWrappingPrice($with_taxes = true, $id_address = null) { static $address = []; // Check if cart is empty, or if the current cart contains at least a real product (not virtual) if (!$this->hasProducts() || !$this->hasRealProducts()) { return 0; } $wrapping_fees = (float) Configuration::get('PS_GIFT_WRAPPING_PRICE'); if ($wrapping_fees <= 0) { return $wrapping_fees; } if ($with_taxes) { if (Configuration::get('PS_ATCP_SHIPWRAP')) { // With PS_ATCP_SHIPWRAP, wrapping fee is by default tax included // so nothing to do here. } else { if (!isset($address[$this->id])) { if ($id_address === null) { $id_address = (int) $this->{Configuration::get('PS_TAX_ADDRESS_TYPE')}; } try { $address[$this->id] = Address::initialize($id_address); } catch (Exception $e) { $address[$this->id] = new Address(); $address[$this->id]->id_country = Configuration::get('PS_COUNTRY_DEFAULT'); } } $tax_manager = TaxManagerFactory::getManager($address[$this->id], (int) Configuration::get('PS_GIFT_WRAPPING_TAX_RULES_GROUP')); $tax_calculator = $tax_manager->getTaxCalculator(); $wrapping_fees = $tax_calculator->addTaxes($wrapping_fees); } } elseif (Configuration::get('PS_ATCP_SHIPWRAP')) { // With PS_ATCP_SHIPWRAP, wrapping fee is by default tax included, so we convert it // when asked for the pre tax price. $wrapping_fees = Tools::ps_round( $wrapping_fees / (1 + $this->getAverageProductsTaxRate()), Context::getContext()->getComputingPrecision() ); } return $wrapping_fees; } /** * Get the number of packages. * * @return int number of packages */ public function getNbOfPackages() { if (!isset(static::$cacheNbPackages[$this->id])) { static::$cacheNbPackages[$this->id] = 0; foreach ($this->getPackageList() as $by_address) { static::$cacheNbPackages[$this->id] += count($by_address); } } return static::$cacheNbPackages[$this->id]; } /** * Get products grouped by package and by addresses to be sent individualy (one package = one shipping cost). * This method tries to separate products to as small number of packages as possible. Ideally one. * * If there is a carrier that sends all products, it will use it. * If not, it will separate it to multiple packages. * * What can also happen is that it will return one package, but with no carrier available. * It can also return more packages, but some of the packages may not have any carrier to send it. * ("carrier_list" => [0 => 0]) * The core needs to handle these cases later in the process. * * @return array array( * 0 => array( // First address * 0 => array( // First package * 'product_list' => array(...), * 'carrier_list' => array(...), * 'id_warehouse' => array(...), * ), * ), * ); * * @todo Add avaibility check */ public function getPackageList($flush = false) { $cache_key = (int) $this->id . '_' . (int) $this->id_address_delivery; if (isset(static::$cachePackageList[$cache_key]) && static::$cachePackageList[$cache_key] !== false && !$flush) { return static::$cachePackageList[$cache_key]; } $product_list = $this->getProducts($flush); // Step 1 : Get product informations (warehouse_list and carrier_list), count warehouse // Determine the best warehouse to determine the packages // For that we count the number of time we can use a warehouse for a specific delivery address $warehouse_count_by_address = []; $stock_management_active = Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT'); foreach ($product_list as &$product) { if ((int) $product['id_address_delivery'] == 0) { $product['id_address_delivery'] = (int) $this->id_address_delivery; } if (!isset($warehouse_count_by_address[$product['id_address_delivery']])) { $warehouse_count_by_address[$product['id_address_delivery']] = []; } $product['warehouse_list'] = []; if ($stock_management_active && (int) $product['advanced_stock_management'] == 1) { $warehouse_list = Warehouse::getProductWarehouseList($product['id_product'], $product['id_product_attribute'], $this->id_shop); if (count($warehouse_list) == 0) { $warehouse_list = Warehouse::getProductWarehouseList($product['id_product'], $product['id_product_attribute']); } // Does the product is in stock ? // If yes, get only warehouse where the product is in stock $warehouse_in_stock = []; $manager = StockManagerFactory::getManager(); foreach ($warehouse_list as $key => $warehouse) { $product_real_quantities = $manager->getProductRealQuantities( $product['id_product'], $product['id_product_attribute'], [$warehouse['id_warehouse']], true ); if ($product_real_quantities > 0 || Pack::isPack((int) $product['id_product'])) { $warehouse_in_stock[] = $warehouse; } } if (!empty($warehouse_in_stock)) { $warehouse_list = $warehouse_in_stock; $product['in_stock'] = true; } else { $product['in_stock'] = false; } } else { //simulate default warehouse $warehouse_list = [0 => ['id_warehouse' => 0]]; $product['in_stock'] = StockAvailable::getQuantityAvailableByProduct($product['id_product'], $product['id_product_attribute']) > 0; } foreach ($warehouse_list as $warehouse) { $product['warehouse_list'][$warehouse['id_warehouse']] = $warehouse['id_warehouse']; if (!isset($warehouse_count_by_address[$product['id_address_delivery']][$warehouse['id_warehouse']])) { $warehouse_count_by_address[$product['id_address_delivery']][$warehouse['id_warehouse']] = 0; } ++$warehouse_count_by_address[$product['id_address_delivery']][$warehouse['id_warehouse']]; } } unset($product); arsort($warehouse_count_by_address); // Step 2 : Group product by warehouse $grouped_by_warehouse = []; foreach ($product_list as &$product) { if (!isset($grouped_by_warehouse[$product['id_address_delivery']])) { $grouped_by_warehouse[$product['id_address_delivery']] = [ 'in_stock' => [], 'out_of_stock' => [], ]; } $product['carrier_list'] = []; $id_warehouse = 0; foreach ($warehouse_count_by_address[$product['id_address_delivery']] as $id_war => $val) { if (array_key_exists((int) $id_war, $product['warehouse_list'])) { $product['carrier_list'] = array_replace( $product['carrier_list'], Carrier::getAvailableCarrierList( new Product($product['id_product']), $id_war, $product['id_address_delivery'], null, $this ) ); if (!$id_warehouse) { $id_warehouse = (int) $id_war; } } } if (!isset($grouped_by_warehouse[$product['id_address_delivery']]['in_stock'][$id_warehouse])) { $grouped_by_warehouse[$product['id_address_delivery']]['in_stock'][$id_warehouse] = []; $grouped_by_warehouse[$product['id_address_delivery']]['out_of_stock'][$id_warehouse] = []; } if (!$this->allow_seperated_package) { $key = 'in_stock'; } else { $key = $product['in_stock'] ? 'in_stock' : 'out_of_stock'; $product_quantity_in_stock = StockAvailable::getQuantityAvailableByProduct($product['id_product'], $product['id_product_attribute']); if ($product['in_stock'] && $product['cart_quantity'] > $product_quantity_in_stock) { $out_stock_part = $product['cart_quantity'] - $product_quantity_in_stock; $product_bis = $product; $product_bis['cart_quantity'] = $out_stock_part; $product_bis['in_stock'] = 0; $product['cart_quantity'] -= $out_stock_part; $grouped_by_warehouse[$product['id_address_delivery']]['out_of_stock'][$id_warehouse][] = $product_bis; } } if (empty($product['carrier_list'])) { $product['carrier_list'] = [0 => 0]; } $grouped_by_warehouse[$product['id_address_delivery']][$key][$id_warehouse][] = $product; } unset($product); // Step 3 : grouped product from grouped_by_warehouse by available carriers $grouped_by_carriers = []; foreach ($grouped_by_warehouse as $id_address_delivery => $products_in_stock_list) { if (!isset($grouped_by_carriers[$id_address_delivery])) { $grouped_by_carriers[$id_address_delivery] = [ 'in_stock' => [], 'out_of_stock' => [], ]; } foreach ($products_in_stock_list as $key => $warehouse_list) { if (!isset($grouped_by_carriers[$id_address_delivery][$key])) { $grouped_by_carriers[$id_address_delivery][$key] = []; } foreach ($warehouse_list as $id_warehouse => $product_list) { if (!isset($grouped_by_carriers[$id_address_delivery][$key][$id_warehouse])) { $grouped_by_carriers[$id_address_delivery][$key][$id_warehouse] = []; } foreach ($product_list as $product) { $package_carriers_key = implode(',', $product['carrier_list']); if (!isset($grouped_by_carriers[$id_address_delivery][$key][$id_warehouse][$package_carriers_key])) { $grouped_by_carriers[$id_address_delivery][$key][$id_warehouse][$package_carriers_key] = [ 'product_list' => [], 'carrier_list' => $product['carrier_list'], 'warehouse_list' => $product['warehouse_list'], ]; } $grouped_by_carriers[$id_address_delivery][$key][$id_warehouse][$package_carriers_key]['product_list'][] = $product; } } } } $package_list = []; // Step 4 : merge product from grouped_by_carriers into $package to minimize the number of package foreach ($grouped_by_carriers as $id_address_delivery => $products_in_stock_list) { if (!isset($package_list[$id_address_delivery])) { $package_list[$id_address_delivery] = [ 'in_stock' => [], 'out_of_stock' => [], ]; } foreach ($products_in_stock_list as $key => $warehouse_list) { if (!isset($package_list[$id_address_delivery][$key])) { $package_list[$id_address_delivery][$key] = []; } // Count occurance of each carriers to minimize the number of packages $carrier_count = []; foreach ($warehouse_list as $id_warehouse => $products_grouped_by_carriers) { foreach ($products_grouped_by_carriers as $data) { foreach ($data['carrier_list'] as $id_carrier) { if (!isset($carrier_count[$id_carrier])) { $carrier_count[$id_carrier] = 0; } ++$carrier_count[$id_carrier]; } } } arsort($carrier_count); foreach ($warehouse_list as $id_warehouse => $products_grouped_by_carriers) { if (!isset($package_list[$id_address_delivery][$key][$id_warehouse])) { $package_list[$id_address_delivery][$key][$id_warehouse] = []; } foreach ($products_grouped_by_carriers as $data) { foreach ($carrier_count as $id_carrier => $rate) { if (array_key_exists($id_carrier, $data['carrier_list'])) { if (!isset($package_list[$id_address_delivery][$key][$id_warehouse][$id_carrier])) { $package_list[$id_address_delivery][$key][$id_warehouse][$id_carrier] = [ 'carrier_list' => $data['carrier_list'], 'warehouse_list' => $data['warehouse_list'], 'product_list' => [], ]; } $package_list[$id_address_delivery][$key][$id_warehouse][$id_carrier]['carrier_list'] = array_intersect($package_list[$id_address_delivery][$key][$id_warehouse][$id_carrier]['carrier_list'], $data['carrier_list']); $package_list[$id_address_delivery][$key][$id_warehouse][$id_carrier]['product_list'] = array_merge($package_list[$id_address_delivery][$key][$id_warehouse][$id_carrier]['product_list'], $data['product_list']); break; } } } } } } // Step 5 : Reduce depth of $package_list $final_package_list = []; foreach ($package_list as $id_address_delivery => $products_in_stock_list) { if (!isset($final_package_list[$id_address_delivery])) { $final_package_list[$id_address_delivery] = []; } foreach ($products_in_stock_list as $key => $warehouse_list) { foreach ($warehouse_list as $id_warehouse => $products_grouped_by_carriers) { foreach ($products_grouped_by_carriers as $data) { $final_package_list[$id_address_delivery][] = [ 'product_list' => $data['product_list'], 'carrier_list' => $data['carrier_list'], 'warehouse_list' => $data['warehouse_list'], 'id_warehouse' => $id_warehouse, ]; } } } } static::$cachePackageList[$cache_key] = $final_package_list; return $final_package_list; } public function getPackageIdWarehouse($package, $id_carrier = null) { if ($id_carrier === null) { if (isset($package['id_carrier'])) { $id_carrier = (int) $package['id_carrier']; } } if ($id_carrier == null) { return $package['id_warehouse']; } foreach ($package['warehouse_list'] as $id_warehouse) { $warehouse = new Warehouse((int) $id_warehouse); $available_warehouse_carriers = $warehouse->getCarriers(); if (in_array($id_carrier, $available_warehouse_carriers)) { return (int) $id_warehouse; } } return 0; } /** * Get all deliveries options available for the current cart. * * @param Country $default_country * @param bool $flush Force flushing cache * * @return array array( * 0 => array( // First address * '12,' => array( // First delivery option available for this address * carrier_list => array( * 12 => array( // First carrier for this option * 'instance' => Carrier Object, * 'logo' => , * 'price_with_tax' => 12.4, * 'price_without_tax' => 12.4, * 'package_list' => array( * 1, * 3, * ), * ), * ), * is_best_grade => true, // Does this option have the biggest grade (quick shipping) for this shipping address * is_best_price => true, // Does this option have the lower price for this shipping address * unique_carrier => true, // Does this option use a unique carrier * total_price_with_tax => 12.5, * total_price_without_tax => 12.5, * position => 5, // Average of the carrier position * ), * ), * ); * If there are no carriers available for an address, return an empty array */ public function getDeliveryOptionList(Country $default_country = null, $flush = false) { if (isset(static::$cacheDeliveryOptionList[$this->id]) && !$flush) { return static::$cacheDeliveryOptionList[$this->id]; } $delivery_option_list = []; $carriers_price = []; $carrier_collection = []; /* * We get a list of packages. This list is always composed of * $id_address and corresponding packages of products. * * The $id_address will always be only one. There used to be a feature that allowed * to send some products to different addresses. This is gone now. */ $package_list = $this->getPackageList($flush); // Foreach addresses foreach ($package_list as $id_address => $packages) { // Initialize vars $delivery_option_list[$id_address] = []; $carriers_price[$id_address] = []; $common_carriers = null; $best_price_carriers = []; $best_grade_carriers = []; $carriers_instance = []; /* * We initialize address. If no addres was provided ($id_address can be zero), * we will use the default country ID to fetch our shipping prices. */ if ($id_address) { $address = new Address($id_address); $country = new Country($address->id_country); } else { $country = $default_country; } // Foreach packages, get the carriers with best price, best position and best grade foreach ($packages as $id_package => $package) { /* * Usually, there is only one package of products with multiple carriers. * But, if there is no carrier that sends everything in the cart, the core will * separate the order into multiple packages. (multishipping) * * So, we need to check, if we have AT LEAST ONE carrier for every package. * If there is one package and it doesn't have any carriers => no delivery options. * If there are multiple packages and one of them doesn't have any carriers => no delivery options. * * We can't just use empty($package['carrier_list']) because it looks like [0 => 0] if there are no carriers. */ if (count($package['carrier_list']) == 1 && current($package['carrier_list']) == 0) { $cache[$this->id] = []; return $cache[$this->id]; } $carriers_price[$id_address][$id_package] = []; // Get all common carriers for each packages to the same address if (null === $common_carriers) { $common_carriers = $package['carrier_list']; } else { $common_carriers = array_intersect($common_carriers, $package['carrier_list']); } $best_price = null; $best_price_carrier = null; $best_grade = null; $best_grade_carrier = null; // Foreach carriers of the package, calculate his price, check if it the best price, position and grade foreach ($package['carrier_list'] as $id_carrier) { if (!isset($carriers_instance[$id_carrier])) { $carriers_instance[$id_carrier] = new Carrier($id_carrier); } $price_with_tax = $this->getPackageShippingCost((int) $id_carrier, true, $country, $package['product_list']); $price_without_tax = $this->getPackageShippingCost((int) $id_carrier, false, $country, $package['product_list']); if (null === $best_price || $price_with_tax < $best_price) { $best_price = $price_with_tax; $best_price_carrier = $id_carrier; } $carriers_price[$id_address][$id_package][$id_carrier] = [ 'without_tax' => $price_without_tax, 'with_tax' => $price_with_tax, ]; $grade = $carriers_instance[$id_carrier]->grade; if (null === $best_grade || $grade > $best_grade) { $best_grade = $grade; $best_grade_carrier = $id_carrier; } } $best_price_carriers[$id_package] = $best_price_carrier; $best_grade_carriers[$id_package] = $best_grade_carrier; } // Reset $best_price_carrier, it's now an array $best_price_carrier = []; $key = ''; // Get the delivery option with the lower price foreach ($best_price_carriers as $id_package => $id_carrier) { $key .= $id_carrier . ','; if (!isset($best_price_carrier[$id_carrier])) { $best_price_carrier[$id_carrier] = [ 'price_with_tax' => 0, 'price_without_tax' => 0, 'package_list' => [], 'product_list' => [], ]; } $best_price_carrier[$id_carrier]['price_with_tax'] += $carriers_price[$id_address][$id_package][$id_carrier]['with_tax']; $best_price_carrier[$id_carrier]['price_without_tax'] += $carriers_price[$id_address][$id_package][$id_carrier]['without_tax']; $best_price_carrier[$id_carrier]['package_list'][] = $id_package; $best_price_carrier[$id_carrier]['product_list'] = array_merge($best_price_carrier[$id_carrier]['product_list'], $packages[$id_package]['product_list']); $best_price_carrier[$id_carrier]['instance'] = $carriers_instance[$id_carrier]; $real_best_price = !isset($real_best_price) || $real_best_price > $carriers_price[$id_address][$id_package][$id_carrier]['with_tax'] ? $carriers_price[$id_address][$id_package][$id_carrier]['with_tax'] : $real_best_price; $real_best_price_wt = !isset($real_best_price_wt) || $real_best_price_wt > $carriers_price[$id_address][$id_package][$id_carrier]['without_tax'] ? $carriers_price[$id_address][$id_package][$id_carrier]['without_tax'] : $real_best_price_wt; } // Add the delivery option with best price as best price $delivery_option_list[$id_address][$key] = [ 'carrier_list' => $best_price_carrier, 'is_best_price' => true, 'is_best_grade' => false, 'unique_carrier' => (count($best_price_carrier) <= 1), ]; // Reset $best_grade_carrier, it's now an array $best_grade_carrier = []; $key = ''; // Get the delivery option with the best grade foreach ($best_grade_carriers as $id_package => $id_carrier) { $key .= $id_carrier . ','; if (!isset($best_grade_carrier[$id_carrier])) { $best_grade_carrier[$id_carrier] = [ 'price_with_tax' => 0, 'price_without_tax' => 0, 'package_list' => [], 'product_list' => [], ]; } $best_grade_carrier[$id_carrier]['price_with_tax'] += $carriers_price[$id_address][$id_package][$id_carrier]['with_tax']; $best_grade_carrier[$id_carrier]['price_without_tax'] += $carriers_price[$id_address][$id_package][$id_carrier]['without_tax']; $best_grade_carrier[$id_carrier]['package_list'][] = $id_package; $best_grade_carrier[$id_carrier]['product_list'] = array_merge($best_grade_carrier[$id_carrier]['product_list'], $packages[$id_package]['product_list']); $best_grade_carrier[$id_carrier]['instance'] = $carriers_instance[$id_carrier]; } // Add the delivery option with best grade as best grade if (!isset($delivery_option_list[$id_address][$key])) { $delivery_option_list[$id_address][$key] = [ 'carrier_list' => $best_grade_carrier, 'is_best_price' => false, 'unique_carrier' => (count($best_grade_carrier) <= 1), ]; } $delivery_option_list[$id_address][$key]['is_best_grade'] = true; // Get all delivery options with a unique carrier foreach ($common_carriers as $id_carrier) { $key = ''; $package_list = []; $product_list = []; $price_with_tax = 0; $price_without_tax = 0; foreach ($packages as $id_package => $package) { $key .= $id_carrier . ','; $price_with_tax += $carriers_price[$id_address][$id_package][$id_carrier]['with_tax']; $price_without_tax += $carriers_price[$id_address][$id_package][$id_carrier]['without_tax']; $package_list[] = $id_package; $product_list = array_merge($product_list, $package['product_list']); } if (!isset($delivery_option_list[$id_address][$key])) { $delivery_option_list[$id_address][$key] = [ 'is_best_price' => false, 'is_best_grade' => false, 'unique_carrier' => true, 'carrier_list' => [ $id_carrier => [ 'price_with_tax' => $price_with_tax, 'price_without_tax' => $price_without_tax, 'instance' => $carriers_instance[$id_carrier], 'package_list' => $package_list, 'product_list' => $product_list, ], ], ]; } else { $delivery_option_list[$id_address][$key]['unique_carrier'] = (count($delivery_option_list[$id_address][$key]['carrier_list']) <= 1); } } } $cart_rules = CartRule::getCustomerCartRules( (int) Context::getContext()->cookie->id_lang, (int) Context::getContext()->cookie->id_customer, true, true, false, $this, true ); $result = false; if ($this->id) { $result = Db::getInstance()->executeS('SELECT * FROM ' . _DB_PREFIX_ . 'cart_cart_rule WHERE id_cart = ' . (int) $this->id); } $cart_rules_in_cart = []; if (is_array($result)) { foreach ($result as $row) { $cart_rules_in_cart[] = $row['id_cart_rule']; } } $total_products_wt = $this->getOrderTotal(true, Cart::ONLY_PRODUCTS); $total_products = $this->getOrderTotal(false, Cart::ONLY_PRODUCTS); $free_carriers_rules = []; $context = Context::getContext(); foreach ($cart_rules as $cart_rule) { $total_price = $cart_rule['minimum_amount_tax'] ? $total_products_wt : $total_products; $total_price += ($cart_rule['minimum_amount_tax'] && $cart_rule['minimum_amount_shipping'] && isset($real_best_price)) ? $real_best_price : 0; $total_price += (!$cart_rule['minimum_amount_tax'] && $cart_rule['minimum_amount_shipping'] && isset($real_best_price_wt)) ? $real_best_price_wt : 0; if ($cart_rule['free_shipping'] && $cart_rule['carrier_restriction'] && in_array($cart_rule['id_cart_rule'], $cart_rules_in_cart) && $cart_rule['minimum_amount'] <= $total_price) { $cr = new CartRule((int) $cart_rule['id_cart_rule']); if (Validate::isLoadedObject($cr) && $cr->checkValidity($context, in_array((int) $cart_rule['id_cart_rule'], $cart_rules_in_cart), false, false)) { $carriers = $cr->getAssociatedRestrictions('carrier', true, false); if (is_array($carriers) && count($carriers) && isset($carriers['selected'])) { foreach ($carriers['selected'] as $carrier) { if (isset($carrier['id_carrier']) && $carrier['id_carrier']) { $free_carriers_rules[] = (int) $carrier['id_carrier']; } } } } } } // For each delivery options : // - Set the carrier list // - Calculate the price // - Calculate the average position foreach ($delivery_option_list as $id_address => $delivery_option) { foreach ($delivery_option as $key => $value) { $total_price_with_tax = 0; $total_price_without_tax = 0; $total_price_without_tax_with_rules = 0; $position = 0; foreach ($value['carrier_list'] as $id_carrier => $data) { $total_price_with_tax += $data['price_with_tax']; $total_price_without_tax += $data['price_without_tax']; $total_price_without_tax_with_rules = (in_array($id_carrier, $free_carriers_rules)) ? 0 : $total_price_without_tax; if (!isset($carrier_collection[$id_carrier])) { $carrier_collection[$id_carrier] = new Carrier($id_carrier); } $delivery_option_list[$id_address][$key]['carrier_list'][$id_carrier]['instance'] = $carrier_collection[$id_carrier]; if (file_exists(_PS_SHIP_IMG_DIR_ . $id_carrier . '.jpg')) { $delivery_option_list[$id_address][$key]['carrier_list'][$id_carrier]['logo'] = _THEME_SHIP_DIR_ . $id_carrier . '.jpg'; } else { $delivery_option_list[$id_address][$key]['carrier_list'][$id_carrier]['logo'] = false; } $position += $carrier_collection[$id_carrier]->position; } $delivery_option_list[$id_address][$key]['total_price_with_tax'] = $total_price_with_tax; $delivery_option_list[$id_address][$key]['total_price_without_tax'] = $total_price_without_tax; $delivery_option_list[$id_address][$key]['is_free'] = !$total_price_without_tax_with_rules ? true : false; $delivery_option_list[$id_address][$key]['position'] = $position / count($value['carrier_list']); } } // Sort delivery option list foreach ($delivery_option_list as &$array) { uasort($array, ['Cart', 'sortDeliveryOptionList']); } Hook::exec( 'actionFilterDeliveryOptionList', [ 'delivery_option_list' => &$delivery_option_list, ] ); static::$cacheDeliveryOptionList[$this->id] = $delivery_option_list; return static::$cacheDeliveryOptionList[$this->id]; } /** * Sort list of option delivery by parameters define in the BO. * * @param array $option1 * @param array $option2 * * @return int -1 if $option 1 must be placed before and 1 if the $option1 must be placed after the $option2 */ public static function sortDeliveryOptionList($option1, $option2) { static $order_by_price = null; static $order_way = null; if (null === $order_by_price) { $order_by_price = !Configuration::get('PS_CARRIER_DEFAULT_SORT'); } if (null === $order_way) { $order_way = Configuration::get('PS_CARRIER_DEFAULT_ORDER') ? 1 : -1; } if ($order_by_price) { return $option1['total_price_with_tax'] < $option2['total_price_with_tax'] ? $order_way : -$order_way; } return $option1['position'] < $option2['position'] ? $order_way : -$order_way; } /** * Is the Carrier selected. * * @param int $id_carrier Carrier ID * @param int $id_address Address ID * * @return bool Indicated if the carrier is selected */ public function carrierIsSelected($id_carrier, $id_address) { $delivery_option = $this->getDeliveryOption(); $delivery_option_list = $this->getDeliveryOptionList(); if (!isset($delivery_option[$id_address])) { return false; } if (!isset($delivery_option_list[$id_address][$delivery_option[$id_address]])) { return false; } if (!in_array($id_carrier, array_keys($delivery_option_list[$id_address][$delivery_option[$id_address]]['carrier_list']))) { return false; } return true; } /** * Get all deliveries options available for the current cart formatted like Carriers::getCarriersForOrder * This method was wrote for retrocompatibility with 1.4 theme * New theme need to use Cart::getDeliveryOptionList() to generate carriers option in the checkout process. * * @since 1.5.0 * @deprecated 1.7.0 * * @param Country $default_country Default Country * @param bool $flush Force flushing cache * * @return array */ public function simulateCarriersOutput(Country $default_country = null, $flush = false) { $delivery_option_list = $this->getDeliveryOptionList($default_country, $flush); // This method cannot work if there is multiple address delivery if (count($delivery_option_list) > 1 || empty($delivery_option_list)) { return []; } $carriers = []; foreach (reset($delivery_option_list) as $key => $option) { $price = $option['total_price_with_tax']; $price_tax_exc = $option['total_price_without_tax']; $name = $img = $delay = ''; if ($option['unique_carrier']) { $carrier = reset($option['carrier_list']); if (isset($carrier['instance'])) { $name = $carrier['instance']->name; $delay = $carrier['instance']->delay; $delay = isset($delay[Context::getContext()->language->id]) ? $delay[Context::getContext()->language->id] : $delay[(int) Configuration::get('PS_LANG_DEFAULT')]; } if (isset($carrier['logo'])) { $img = $carrier['logo']; } } else { $nameList = []; foreach ($option['carrier_list'] as $carrier) { $nameList[] = $carrier['instance']->name; } $name = implode(' -', $nameList); $img = ''; // No images if multiple carriers $delay = ''; } $carriers[] = [ 'name' => $name, 'img' => $img, 'delay' => $delay, 'price' => $price, 'price_tax_exc' => $price_tax_exc, 'id_carrier' => Cart::intifier($key), // Need to translate to an integer for retrocompatibility reason, in 1.4 template we used intval 'is_module' => false, ]; } return $carriers; } /** * Simulate output of selected Carrier. * * @param bool $use_cache Use cache * * @return int Intified Cart output */ public function simulateCarrierSelectedOutput($use_cache = true) { $delivery_option = $this->getDeliveryOption(null, false, $use_cache); if (count($delivery_option) > 1 || empty($delivery_option)) { return 0; } return (int) Cart::intifier(reset($delivery_option)); } /** * Translate a string option_delivery identifier ('24,3,') in a int (3240002000). * * The option_delivery identifier is a list of integers separated by a ','. * This method replace the delimiter by a sequence of '0'. * The size of this sequence is fixed by the first digit of the return * * @return string Intified value */ public static function intifier($string, $delimiter = ',') { $elm = explode($delimiter, $string); $max = max($elm); return strlen($max) . implode(str_repeat('0', strlen($max) + 1), $elm); } /** * Translate an int option_delivery identifier (3240002000) in a string ('24,3,'). */ public static function desintifier($int, $delimiter = ',') { /** @var positive-int $delimiter_len */ $delimiter_len = intval($int[0]); $int = strrev(substr($int, 1)); $elm = explode(str_repeat('0', $delimiter_len + 1), $int); return strrev(implode($delimiter, $elm)); } /** * Does the Cart use multiple Addresses? * * @return bool Indicates if the Cart uses multiple Addresses */ public function isMultiAddressDelivery() { if (!isset(static::$cacheMultiAddressDelivery[$this->id])) { $sql = new DbQuery(); $sql->select('count(distinct id_address_delivery)'); $sql->from('cart_product', 'cp'); $sql->where('id_cart = ' . (int) $this->id); static::$cacheMultiAddressDelivery[$this->id] = Db::getInstance()->getValue($sql) > 1; } return static::$cacheMultiAddressDelivery[$this->id]; } /** * Get all delivery Addresses object for the current Cart. */ public function getAddressCollection() { $collection = []; $cache_id = 'Cart::getAddressCollection' . (int) $this->id; if (!Cache::isStored($cache_id)) { $result = Db::getInstance()->executeS( 'SELECT DISTINCT `id_address_delivery` FROM `' . _DB_PREFIX_ . 'cart_product` WHERE id_cart = ' . (int) $this->id ); Cache::store($cache_id, $result); } else { $result = Cache::retrieve($cache_id); } $result[] = ['id_address_delivery' => (int) $this->id_address_delivery]; foreach ($result as $row) { if ((int) $row['id_address_delivery'] != 0) { $collection[(int) $row['id_address_delivery']] = new Address((int) $row['id_address_delivery']); } } return $collection; } /** * Set the delivery option and Carrier ID, if there is only one Carrier. * * @param array $delivery_option Delivery option array */ public function setDeliveryOption($delivery_option = null) { if (empty($delivery_option)) { $this->delivery_option = ''; $this->id_carrier = 0; return; } Cache::clean('getContextualValue_*'); $delivery_option_list = $this->getDeliveryOptionList(null, true); foreach ($delivery_option_list as $id_address => $options) { if (!isset($delivery_option[$id_address])) { foreach ($options as $key => $option) { if ($option['is_best_price']) { $delivery_option[$id_address] = $key; break; } } } } if (count($delivery_option) == 1) { $this->id_carrier = $this->getIdCarrierFromDeliveryOption($delivery_option); } $this->delivery_option = json_encode($delivery_option); // update auto cart rules CartRule::autoRemoveFromCart(); CartRule::autoAddToCart(); } /** * Get Carrier ID from Delivery Option. * * @param array $delivery_option Delivery options array * * @return int|mixed Carrier ID */ protected function getIdCarrierFromDeliveryOption($delivery_option) { $delivery_option_list = $this->getDeliveryOptionList(); foreach ($delivery_option as $key => $value) { if (isset($delivery_option_list[$key][$value])) { if (count($delivery_option_list[$key][$value]['carrier_list']) == 1) { return current(array_keys($delivery_option_list[$key][$value]['carrier_list'])); } } } return 0; } /** * Get the delivery option selected, or if no delivery option was selected, * the cheapest option for each address. * * @param Country|null $default_country Default country * @param bool $dontAutoSelectOptions Do not auto select delivery option * @param bool $use_cache Use cache * * @return array|false Delivery option */ public function getDeliveryOption($default_country = null, $dontAutoSelectOptions = false, $use_cache = true) { $cache_id = (int) (is_object($default_country) ? $default_country->id : 0) . '-' . (int) $dontAutoSelectOptions; if (isset(static::$cacheDeliveryOption[$cache_id]) && $use_cache) { return static::$cacheDeliveryOption[$cache_id]; } $delivery_option_list = $this->getDeliveryOptionList($default_country); // The delivery option was selected if (isset($this->delivery_option) && $this->delivery_option != '') { $delivery_option = json_decode($this->delivery_option, true); $validated = true; if (is_array($delivery_option)) { foreach ($delivery_option as $id_address => $key) { if (!isset($delivery_option_list[$id_address][$key])) { $validated = false; break; } } if ($validated) { static::$cacheDeliveryOption[$cache_id] = $delivery_option; return $delivery_option; } } } if ($dontAutoSelectOptions) { return false; } // No delivery option selected or delivery option selected is not valid, get the better for all options $delivery_option = []; foreach ($delivery_option_list as $id_address => $options) { foreach ($options as $key => $option) { if (Configuration::get('PS_CARRIER_DEFAULT') == -1 && $option['is_best_price']) { $delivery_option[$id_address] = $key; break; } elseif (Configuration::get('PS_CARRIER_DEFAULT') == -2 && $option['is_best_grade']) { $delivery_option[$id_address] = $key; break; } elseif ($option['unique_carrier'] && in_array(Configuration::get('PS_CARRIER_DEFAULT'), array_keys($option['carrier_list']))) { $delivery_option[$id_address] = $key; break; } } reset($options); if (!isset($delivery_option[$id_address])) { $delivery_option[$id_address] = key($options); } } static::$cacheDeliveryOption[$cache_id] = $delivery_option; return $delivery_option; } /** * Return shipping total for the cart. * * @param array|null $delivery_option Array of the delivery option for each address * @param bool $use_tax Use taxes * @param Country|null $default_country Default Country * * @return float Shipping total */ public function getTotalShippingCost($delivery_option = null, $use_tax = true, Country $default_country = null) { if (isset(Context::getContext()->cookie->id_country)) { $default_country = new Country((int) Context::getContext()->cookie->id_country); } if (null === $delivery_option) { $delivery_option = $this->getDeliveryOption($default_country, false, false); } $_total_shipping = [ 'with_tax' => 0, 'without_tax' => 0, ]; $delivery_option_list = $this->getDeliveryOptionList($default_country); foreach ($delivery_option as $id_address => $key) { if (!isset($delivery_option_list[$id_address]) || !isset($delivery_option_list[$id_address][$key])) { continue; } $_total_shipping['with_tax'] += $delivery_option_list[$id_address][$key]['total_price_with_tax']; $_total_shipping['without_tax'] += $delivery_option_list[$id_address][$key]['total_price_without_tax']; } return ($use_tax) ? $_total_shipping['with_tax'] : $_total_shipping['without_tax']; } /** * Return shipping total of a specific carriers for the cart. * * @param int $id_carrier Carrier ID * @param array $delivery_option Array of the delivery option for each address * @param bool $useTax Use Taxes * @param Country|null $default_country Default Country * @param array|null $delivery_option Delivery options array * * @return float Shipping total */ public function getCarrierCost($id_carrier, $useTax = true, Country $default_country = null, $delivery_option = null) { if (null === $delivery_option) { $delivery_option = $this->getDeliveryOption($default_country); } $total_shipping = 0; $delivery_option_list = $this->getDeliveryOptionList(); foreach ($delivery_option as $id_address => $key) { if (!isset($delivery_option_list[$id_address]) || !isset($delivery_option_list[$id_address][$key])) { continue; } if (isset($delivery_option_list[$id_address][$key]['carrier_list'][$id_carrier])) { if ($useTax) { $total_shipping += $delivery_option_list[$id_address][$key]['carrier_list'][$id_carrier]['price_with_tax']; } else { $total_shipping += $delivery_option_list[$id_address][$key]['carrier_list'][$id_carrier]['price_without_tax']; } } } return $total_shipping; } /** * Return package shipping cost. * * @param int $id_carrier Carrier ID (default : current carrier) * @param bool $use_tax * @param Country|null $default_country * @param array|null $product_list list of product concerned by the shipping. * If null, all the product of the cart are used to calculate the shipping cost * @param int|null $id_zone Zone ID * @param bool $keepOrderPrices When true use the Order saved prices instead of the most recent ones from catalog (if Order exists) * * @return float|bool Shipping total, false if not possible to ship with the given carrier */ public function getPackageShippingCost( $id_carrier = null, $use_tax = true, Country $default_country = null, $product_list = null, $id_zone = null, bool $keepOrderPrices = false ) { if ($this->isVirtualCart()) { return 0; } if (!$default_country) { $default_country = Context::getContext()->country; } if (null === $product_list) { $products = $this->getProducts(false, false, null, true, $keepOrderPrices); } else { foreach ($product_list as $key => $value) { if ($value['is_virtual'] == 1) { unset($product_list[$key]); } } $products = $product_list; } if (Configuration::get('PS_TAX_ADDRESS_TYPE') == 'id_address_invoice') { $address_id = (int) $this->id_address_invoice; } elseif (is_array($product_list) && count($product_list)) { $prod = current($product_list); $address_id = (int) $prod['id_address_delivery']; } else { $address_id = null; } if (!Address::addressExists($address_id, true)) { $address_id = null; } if (null === $id_carrier && !empty($this->id_carrier)) { $id_carrier = (int) $this->id_carrier; } $cache_id = 'getPackageShippingCost_' . (int) $this->id . '_' . (int) $address_id . '_' . (int) $id_carrier . '_' . (int) $use_tax . '_' . (int) $default_country->id . '_' . (int) $id_zone; if ($products) { foreach ($products as $product) { $cache_id .= '_' . (int) $product['id_product'] . '_' . (int) $product['id_product_attribute']; } } if (Cache::isStored($cache_id)) { return Cache::retrieve($cache_id); } // Order total in default currency without fees $order_total = $this->getOrderTotal(true, Cart::BOTH_WITHOUT_SHIPPING, $product_list, $id_carrier, false, $keepOrderPrices); // Start with shipping cost at 0 $shipping_cost = 0; // If no product added, return 0 if (!count($products)) { Cache::store($cache_id, $shipping_cost); return $shipping_cost; } if (!isset($id_zone)) { // Get id zone if (!$this->isMultiAddressDelivery() && isset($this->id_address_delivery) // Be careful, id_address_delivery is not useful one 1.5 && $this->id_address_delivery && Customer::customerHasAddress($this->id_customer, $this->id_address_delivery) ) { $id_zone = Address::getZoneById((int) $this->id_address_delivery); } else { if (!Validate::isLoadedObject($default_country)) { $default_country = new Country( (int) Configuration::get('PS_COUNTRY_DEFAULT'), (int) Configuration::get('PS_LANG_DEFAULT') ); } $id_zone = (int) $default_country->id_zone; } } if ($id_carrier && !$this->isCarrierInRange((int) $id_carrier, (int) $id_zone)) { $id_carrier = ''; } if (empty($id_carrier) && $this->isCarrierInRange((int) Configuration::get('PS_CARRIER_DEFAULT'), (int) $id_zone)) { $id_carrier = (int) Configuration::get('PS_CARRIER_DEFAULT'); } if (empty($id_carrier)) { if ((int) $this->id_customer) { $customer = new Customer((int) $this->id_customer); $result = Carrier::getCarriers((int) Configuration::get('PS_LANG_DEFAULT'), true, false, (int) $id_zone, $customer->getGroups()); unset($customer); } else { $result = Carrier::getCarriers((int) Configuration::get('PS_LANG_DEFAULT'), true, false, (int) $id_zone); } foreach ($result as $k => $row) { if ($row['id_carrier'] == Configuration::get('PS_CARRIER_DEFAULT')) { continue; } if (!isset(self::$_carriers[$row['id_carrier']])) { self::$_carriers[$row['id_carrier']] = new Carrier((int) $row['id_carrier']); } /** @var Carrier $carrier */ $carrier = self::$_carriers[$row['id_carrier']]; $shipping_method = $carrier->getShippingMethod(); // Get only carriers that are compliant with shipping method if (($shipping_method == Carrier::SHIPPING_METHOD_WEIGHT && $carrier->getMaxDeliveryPriceByWeight((int) $id_zone) === false) || ($shipping_method == Carrier::SHIPPING_METHOD_PRICE && $carrier->getMaxDeliveryPriceByPrice((int) $id_zone) === false)) { unset($result[$k]); continue; } // If out-of-range behavior carrier is set on "Desactivate carrier" if ($row['range_behavior']) { $check_delivery_price_by_weight = Carrier::checkDeliveryPriceByWeight($row['id_carrier'], $this->getTotalWeight(), (int) $id_zone); $check_delivery_price_by_price = Carrier::checkDeliveryPriceByPrice($row['id_carrier'], $order_total, (int) $id_zone, (int) $this->id_currency); // Get only carriers that have a range compatible with cart if (($shipping_method == Carrier::SHIPPING_METHOD_WEIGHT && $check_delivery_price_by_weight === false) || ($shipping_method == Carrier::SHIPPING_METHOD_PRICE && $check_delivery_price_by_price === false)) { unset($result[$k]); continue; } } if ($shipping_method == Carrier::SHIPPING_METHOD_WEIGHT) { $shipping = $carrier->getDeliveryPriceByWeight($this->getTotalWeight($product_list), (int) $id_zone); } else { $shipping = $carrier->getDeliveryPriceByPrice($order_total, (int) $id_zone, (int) $this->id_currency); } if (!isset($min_shipping_price)) { $min_shipping_price = $shipping; } if ($shipping <= $min_shipping_price) { $id_carrier = (int) $row['id_carrier']; $min_shipping_price = $shipping; } } } if (empty($id_carrier)) { $id_carrier = Configuration::get('PS_CARRIER_DEFAULT'); } if (!isset(self::$_carriers[$id_carrier])) { self::$_carriers[$id_carrier] = new Carrier((int) $id_carrier, (int) Configuration::get('PS_LANG_DEFAULT')); } $carrier = self::$_carriers[$id_carrier]; // No valid Carrier or $id_carrier <= 0 ? if (!Validate::isLoadedObject($carrier)) { Cache::store($cache_id, 0); return 0; } $shipping_method = $carrier->getShippingMethod(); if (!$carrier->active) { Cache::store($cache_id, $shipping_cost); return $shipping_cost; } // Free fees if free carrier if ($carrier->is_free == 1) { Cache::store($cache_id, 0); return 0; } // Select carrier tax if ($use_tax && !Tax::excludeTaxeOption()) { $address = Address::initialize((int) $address_id); if (Configuration::get('PS_ATCP_SHIPWRAP')) { // With PS_ATCP_SHIPWRAP, pre-tax price is deduced // from post tax price, so no $carrier_tax here // even though it sounds weird. $carrier_tax = 0; } else { $carrier_tax = $carrier->getTaxesRate($address); } } $configuration = Configuration::getMultiple([ 'PS_SHIPPING_FREE_PRICE', 'PS_SHIPPING_HANDLING', 'PS_SHIPPING_METHOD', 'PS_SHIPPING_FREE_WEIGHT', ]); // Free fees $free_fees_price = 0; if (isset($configuration['PS_SHIPPING_FREE_PRICE'])) { $free_fees_price = Tools::convertPrice((float) $configuration['PS_SHIPPING_FREE_PRICE'], Currency::getCurrencyInstance((int) $this->id_currency)); } $orderTotalwithDiscounts = $this->getOrderTotal(true, Cart::BOTH_WITHOUT_SHIPPING, null, null, false); if ($orderTotalwithDiscounts >= (float) ($free_fees_price) && (float) ($free_fees_price) > 0) { $shipping_cost = $this->getPackageShippingCostFromModule($carrier, $shipping_cost, $products); Cache::store($cache_id, $shipping_cost); return $shipping_cost; } if (isset($configuration['PS_SHIPPING_FREE_WEIGHT']) && $this->getTotalWeight() >= (float) $configuration['PS_SHIPPING_FREE_WEIGHT'] && (float) $configuration['PS_SHIPPING_FREE_WEIGHT'] > 0) { $shipping_cost = $this->getPackageShippingCostFromModule($carrier, $shipping_cost, $products); Cache::store($cache_id, $shipping_cost); return $shipping_cost; } // Get shipping cost using correct method if ($carrier->range_behavior) { if (($shipping_method == Carrier::SHIPPING_METHOD_WEIGHT && Carrier::checkDeliveryPriceByWeight($carrier->id, $this->getTotalWeight(), (int) $id_zone) === false) || ( $shipping_method == Carrier::SHIPPING_METHOD_PRICE && Carrier::checkDeliveryPriceByPrice($carrier->id, $order_total, $id_zone, (int) $this->id_currency) === false )) { $shipping_cost += 0; } else { if ($shipping_method == Carrier::SHIPPING_METHOD_WEIGHT) { $shipping_cost += $carrier->getDeliveryPriceByWeight($this->getTotalWeight($product_list), $id_zone); } else { // by price $shipping_cost += $carrier->getDeliveryPriceByPrice($order_total, $id_zone, (int) $this->id_currency); } } } else { if ($shipping_method == Carrier::SHIPPING_METHOD_WEIGHT) { $shipping_cost += $carrier->getDeliveryPriceByWeight($this->getTotalWeight($product_list), $id_zone); } else { $shipping_cost += $carrier->getDeliveryPriceByPrice($order_total, $id_zone, (int) $this->id_currency); } } // Adding handling charges if (isset($configuration['PS_SHIPPING_HANDLING']) && $carrier->shipping_handling) { $shipping_cost += (float) $configuration['PS_SHIPPING_HANDLING']; } // Additional Shipping Cost per product foreach ($products as $product) { if (!$product['is_virtual']) { $shipping_cost += $product['additional_shipping_cost'] * $product['cart_quantity']; } } $shipping_cost = Tools::convertPrice($shipping_cost, Currency::getCurrencyInstance((int) $this->id_currency)); //get external shipping cost from module $shipping_cost = $this->getPackageShippingCostFromModule($carrier, $shipping_cost, $products); if ($shipping_cost === false) { Cache::store($cache_id, false); return false; } if (Configuration::get('PS_ATCP_SHIPWRAP')) { if (!$use_tax) { // With PS_ATCP_SHIPWRAP, we deduce the pre-tax price from the post-tax // price. This is on purpose and required in Germany. $shipping_cost /= (1 + $this->getAverageProductsTaxRate()); } } else { // Apply tax if ($use_tax && isset($carrier_tax)) { $shipping_cost *= 1 + ($carrier_tax / 100); } } $shipping_cost = (float) Tools::ps_round((float) $shipping_cost, Context::getContext()->getComputingPrecision()); Cache::store($cache_id, $shipping_cost); return $shipping_cost; } /** * Ask the module the package shipping cost. * * If a carrier has been linked to a carrier module, we call it order to review the shipping costs. * * @param Carrier $carrier The concerned carrier (Your module may have several carriers) * @param float $shipping_cost The calculated shipping cost from the core, regarding package dimension and cart total * @param array $products The list of products * * @return bool|float The package price for the module (0 if free, false is disabled) */ protected function getPackageShippingCostFromModule(Carrier $carrier, $shipping_cost, $products) { if (!$carrier->shipping_external) { return $shipping_cost; } /** @var CarrierModule $module */ $module = Module::getInstanceByName($carrier->external_module_name); if (!Validate::isLoadedObject($module)) { return false; } if (property_exists($module, 'id_carrier')) { $module->id_carrier = $carrier->id; } if (!$carrier->need_range) { return $module->getOrderShippingCostExternal($this); } if (method_exists($module, 'getPackageShippingCost')) { $shipping_cost = $module->getPackageShippingCost($this, $shipping_cost, $products); } else { $shipping_cost = $module->getOrderShippingCost($this, $shipping_cost); } return $shipping_cost; } /** * Return total Cart weight. * * @return float Total Cart weight */ public function getTotalWeight($products = null) { if (null !== $products) { $total_weight = 0; foreach ($products as $product) { $total_weight += ($product['weight_attribute'] ?? $product['weight']) * $product['cart_quantity']; } return $total_weight; } if (!isset(self::$_totalWeight[$this->id])) { $this->updateProductWeight($this->id); } return self::$_totalWeight[(int) $this->id]; } /** * @param int $productId */ protected function updateProductWeight($productId) { $productId = (int) $productId; if (Combination::isFeatureActive()) { $weight_product_with_attribute = Db::getInstance()->getValue(' SELECT SUM((p.`weight` + pa.`weight`) * cp.`quantity`) as nb FROM `' . _DB_PREFIX_ . 'cart_product` cp LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON (cp.`id_product` = p.`id_product`) LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON (cp.`id_product_attribute` = pa.`id_product_attribute`) WHERE (cp.`id_product_attribute` IS NOT NULL AND cp.`id_product_attribute` != 0) AND cp.`id_cart` = ' . $productId); } else { $weight_product_with_attribute = 0; } $weight_product_without_attribute = Db::getInstance()->getValue(' SELECT SUM(p.`weight` * cp.`quantity`) as nb FROM `' . _DB_PREFIX_ . 'cart_product` cp LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON (cp.`id_product` = p.`id_product`) WHERE (cp.`id_product_attribute` IS NULL OR cp.`id_product_attribute` = 0) AND cp.`id_cart` = ' . $productId); $weight_cart_customizations = Db::getInstance()->getValue(' SELECT SUM(cd.`weight` * c.`quantity`) FROM `' . _DB_PREFIX_ . 'customization` c LEFT JOIN `' . _DB_PREFIX_ . 'customized_data` cd ON (c.`id_customization` = cd.`id_customization`) WHERE c.`in_cart` = 1 AND c.`id_cart` = ' . $productId); self::$_totalWeight[$productId] = round( (float) $weight_product_with_attribute + (float) $weight_product_without_attribute + (float) $weight_cart_customizations, 6 ); } /** * Return useful information about the cart for display purpose. * Products are splitted between paid ones and gift * Gift price and shipping (if shipping is free) are removed from Discounts * Any cart data modification for display purpose is made here. * * @return array Cart details */ public function getSummaryDetails($id_lang = null, $refresh = false) { if (!$id_lang) { $id_lang = Context::getContext()->language->id; } $summary = $this->getRawSummaryDetails($id_lang, (bool) $refresh); return $this->alterSummaryForDisplay($summary, (bool) $refresh); } /** * Returns useful raw information about the cart. * Products, Discounts, Prices ... are returned in an array without any modification. * * @param int $id_lang * @param bool $refresh * * @return array Cart details * * @throws PrestaShopException * @throws LocalizationException */ public function getRawSummaryDetails(int $id_lang, bool $refresh = false): array { $context = Context::getContext(); $delivery = new Address((int) $this->id_address_delivery); $invoice = new Address((int) $this->id_address_invoice); // New layout system with personalization fields $formatted_addresses = [ 'delivery' => AddressFormat::getFormattedLayoutData($delivery), 'invoice' => AddressFormat::getFormattedLayoutData($invoice), ]; $base_total_tax_inc = $this->getOrderTotal(true); $base_total_tax_exc = $this->getOrderTotal(false); $total_tax = $base_total_tax_inc - $base_total_tax_exc; if ($total_tax < 0) { $total_tax = 0; } $products = $this->getProducts($refresh); foreach ($products as $key => &$product) { $product['price_without_quantity_discount'] = Product::getPriceStatic( $product['id_product'], !Product::getTaxCalculationMethod(), $product['id_product_attribute'], 6, null, false, false ); if ($product['reduction_type'] == 'amount') { $reduction = (!Product::getTaxCalculationMethod() ? (float) $product['price_wt'] : (float) $product['price']) - (float) $product['price_without_quantity_discount']; $product['reduction_formatted'] = Tools::getContextLocale($context)->formatPrice($reduction, $context->currency->iso_code); } } $total_shipping = $this->getTotalShippingCost(); $summary = [ 'delivery' => $delivery, 'delivery_state' => State::getNameById($delivery->id_state), 'invoice' => $invoice, 'invoice_state' => State::getNameById($invoice->id_state), 'formattedAddresses' => $formatted_addresses, 'products' => array_values($products), 'discounts' => array_values($this->getCartRules()), 'is_virtual_cart' => (int) $this->isVirtualCart(), 'total_discounts' => $this->getOrderTotal(true, Cart::ONLY_DISCOUNTS), 'total_discounts_tax_exc' => $this->getOrderTotal(false, Cart::ONLY_DISCOUNTS), 'total_wrapping' => $this->getOrderTotal(true, Cart::ONLY_WRAPPING), 'total_wrapping_tax_exc' => $this->getOrderTotal(false, Cart::ONLY_WRAPPING), 'total_shipping' => $total_shipping, 'total_shipping_tax_exc' => $this->getTotalShippingCost(null, false), 'total_products_wt' => $this->getOrderTotal(true, Cart::ONLY_PRODUCTS), 'total_products' => $this->getOrderTotal(false, Cart::ONLY_PRODUCTS), 'total_price' => $base_total_tax_inc, 'total_tax' => $total_tax, 'total_price_without_tax' => $base_total_tax_exc, 'is_multi_address_delivery' => $this->isMultiAddressDelivery() || ((int) Tools::getValue('multi-shipping') == 1), 'free_ship' => !$total_shipping && !count($this->getDeliveryAddressesWithoutCarriers(true, $errors)), 'carrier' => new Carrier($this->id_carrier, $id_lang), ]; // An array [module_name => module_output] will be returned $hook = Hook::exec('actionCartSummary', $summary, null, true); if (is_array($hook)) { $summary = array_merge($summary, (array) array_shift($hook)); } return $summary; } /** * Check if product quantities in Cart are available. * * @param bool $returnProductOnFailure Return the first found product with not enough quantity * * @return bool|array If all products are in stock: true; if not: either false or an array * containing the first found product which is not in stock in the * requested amount */ public function checkQuantities($returnProductOnFailure = false) { if (Configuration::isCatalogMode() && !defined('_PS_ADMIN_DIR_')) { return false; } foreach ($this->getProducts() as $product) { // This code is not used, because advanced_stock_management is never enabled // on 1.7 and newer. if ( !$this->allow_seperated_package && !$product['allow_oosp'] && StockAvailable::dependsOnStock($product['id_product']) && $product['advanced_stock_management'] && (bool) Context::getContext()->customer->isLogged() ) { $delivery = $this->getDeliveryOption(); if (!empty($delivery)) { $product['stock_quantity'] = StockManager::getStockByCarrier( (int) $product['id_product'], (int) $product['id_product_attribute'], $delivery ); } } /* * We have an immediate failure if the product is not active or available for order. * @todo The second condition with quantity is not needed because it's checked properly further down. */ if ( !$product['active'] || !$product['available_for_order'] ) { return $returnProductOnFailure ? $product : false; } if (!$product['allow_oosp']) { $productQuantity = Product::getQuantity( $product['id_product'], $product['id_product_attribute'], null, $this, false ); if ($productQuantity < 0) { return $returnProductOnFailure ? $product : false; } } } return true; } /** * Check if the product can be accessed by the Customer. * * @return bool Indicates if the Customer in the current Cart has access */ public function checkProductsAccess() { if (Configuration::isCatalogMode()) { return true; } foreach ($this->getProducts() as $product) { if (!Product::checkAccessStatic($product['id_product'], $this->id_customer)) { return $product['id_product']; } } return false; } /** * Last abandoned Cart. * * @param int $id_customer Customer ID * * @return bool|int Last abandoned Cart ID * false if not found */ public static function lastNoneOrderedCart($id_customer) { $sql = 'SELECT c.`id_cart` FROM ' . _DB_PREFIX_ . 'cart c WHERE NOT EXISTS (SELECT 1 FROM ' . _DB_PREFIX_ . 'orders o WHERE o.`id_cart` = c.`id_cart` AND o.`id_customer` = ' . (int) $id_customer . ') AND c.`id_customer` = ' . (int) $id_customer . ' AND c.`id_cart` = (SELECT `id_cart` FROM `' . _DB_PREFIX_ . 'cart` c2 WHERE c2.`id_customer` = ' . (int) $id_customer . ' ORDER BY `id_cart` DESC LIMIT 1) AND c.`id_guest` != 0 ' . Shop::addSqlRestriction(Shop::SHARE_ORDER, 'c') . ' ORDER BY c.`date_upd` DESC'; if (!$id_cart = Db::getInstance()->getValue($sql)) { return false; } return (int) $id_cart; } /** * Check if cart contains only virtual products. * * @return bool true if is a virtual cart or false */ public function isVirtualCart() { if (!ProductDownload::isFeatureActive()) { return false; } if (!isset(self::$_isVirtualCart[$this->id])) { if (!$this->hasProducts()) { $isVirtual = false; } else { $isVirtual = !$this->hasRealProducts(); } self::$_isVirtualCart[$this->id] = $isVirtual; } return self::$_isVirtualCart[$this->id]; } /** * Check if there's a product in the cart. * * @return bool */ public function hasProducts() { return (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( 'SELECT 1 FROM ' . _DB_PREFIX_ . 'cart_product cp ' . 'INNER JOIN ' . _DB_PREFIX_ . 'product p ON (p.id_product = cp.id_product) ' . 'INNER JOIN ' . _DB_PREFIX_ . 'product_shop ps ON (ps.id_shop = cp.id_shop AND ps.id_product = p.id_product) ' . 'WHERE cp.id_cart=' . (int) $this->id ); } /** * Return true if the current cart contains a real product. * * @return bool */ public function hasRealProducts() { // Check for non-virtual products which are not packs $sql = 'SELECT 1 FROM %scart_product cp INNER JOIN %sproduct p ON (p.id_product = cp.id_product AND cache_is_pack = 0 and p.is_virtual = 0) INNER JOIN %sproduct_shop ps ON (ps.id_shop = cp.id_shop AND ps.id_product = p.id_product) WHERE cp.id_cart=%d'; $sql = sprintf($sql, _DB_PREFIX_, _DB_PREFIX_, _DB_PREFIX_, $this->id); if ((bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql)) { return true; } // Check for non-virtual products which are in packs $sql = 'SELECT 1 FROM %scart_product cp INNER JOIN %spack pa ON (pa.id_product_pack = cp.id_product) INNER JOIN %sproduct p ON (p.id_product = pa.id_product_item AND p.is_virtual = 0) INNER JOIN %sproduct_shop ps ON (ps.id_shop = cp.id_shop AND ps.id_product = p.id_product) WHERE cp.id_cart=%d'; $sql = sprintf($sql, _DB_PREFIX_, _DB_PREFIX_, _DB_PREFIX_, _DB_PREFIX_, $this->id); return (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql); } /** * Build cart object from provided id_order. * * @param int $id_order * * @return Cart|bool */ public static function getCartByOrderId($id_order) { if ($id_cart = Cart::getCartIdByOrderId($id_order)) { return new Cart((int) $id_cart); } return false; } /** * Get Cart ID by Order ID. * * @param int $id_order Order ID * * @return int|bool Cart ID, false if not found */ public static function getCartIdByOrderId($id_order) { $result = Db::getInstance()->getRow('SELECT `id_cart` FROM ' . _DB_PREFIX_ . 'orders WHERE `id_order` = ' . (int) $id_order); if (empty($result) || !array_key_exists('id_cart', $result)) { return false; } return $result['id_cart']; } /** * Add customer's text. * * @param int $id_product Product ID * @param int $index Customization field identifier as id_customization_field in table customization_field * @param int $type Customization type can be Product::CUSTOMIZE_FILE or Product::CUSTOMIZE_TEXTFIELD * @param string $text_value * @param bool $returnCustomizationId if true - returns the customizationId * * @return bool Always true */ public function addTextFieldToProduct($id_product, $index, $type, $text_value, $returnCustomizationId = false) { return $this->_addCustomization( $id_product, 0, $index, $type, $text_value, 0, $returnCustomizationId ); } /** * Add customer's pictures. * * @param int $id_product Product ID * @param int $index Customization field identifier as id_customization_field in table customization_field * @param int $type Customization type can be Product::CUSTOMIZE_FILE or Product::CUSTOMIZE_TEXTFIELD * @param string $file Filename * @param bool $returnCustomizationId if true - returns the customizationId * * @return bool Always true */ public function addPictureToProduct($id_product, $index, $type, $file, $returnCustomizationId = false) { return $this->_addCustomization( $id_product, 0, $index, $type, $file, 0, $returnCustomizationId ); } /** * Remove a customer's customization. * * @param int $id_product Product ID * @param int $index Customization field identifier as id_customization_field in table customization_field * * @return bool */ public function deleteCustomizationToProduct($id_product, $index) { $cust_data = Db::getInstance()->getRow( 'SELECT cu.`id_customization`, cd.`index`, cd.`value`, cd.`type` FROM `' . _DB_PREFIX_ . 'customization` cu LEFT JOIN `' . _DB_PREFIX_ . 'customized_data` cd ON cu.`id_customization` = cd.`id_customization` WHERE cu.`id_cart` = ' . (int) $this->id . ' AND cu.`id_product` = ' . (int) $id_product . ' AND `index` = ' . (int) $index . ' AND `in_cart` = 0' ); if (!$cust_data) { return true; } $result = true; // Delete customization picture if necessary if ($cust_data['type'] == Product::CUSTOMIZE_FILE) { $result = !file_exists(_PS_UPLOAD_DIR_ . $cust_data['value']) || @unlink(_PS_UPLOAD_DIR_ . $cust_data['value']); $result = !($result && file_exists(_PS_UPLOAD_DIR_ . $cust_data['value'] . '_small')) || @unlink(_PS_UPLOAD_DIR_ . $cust_data['value'] . '_small'); } $result = $result && Db::getInstance()->execute( 'DELETE FROM `' . _DB_PREFIX_ . 'customized_data` WHERE `id_customization` = ' . (int) $cust_data['id_customization'] . ' AND `index` = ' . (int) $index ); $hasRemainingCustomData = Db::getInstance()->getValue( 'SELECT 1 FROM `' . _DB_PREFIX_ . 'customized_data` WHERE `id_customization` = ' . (int) $cust_data['id_customization'] ); if (!$hasRemainingCustomData) { $result = $result && Db::getInstance()->execute( 'DELETE FROM `' . _DB_PREFIX_ . 'customization` WHERE `id_customization` = ' . (int) $cust_data['id_customization'] ); } return $result; } /** * Return custom pictures in this cart for a specified product. * * @param int $id_product Product ID * @param int|null $type Only return customization of this type, can be Product::CUSTOMIZE_FILE or Product::CUSTOMIZE_TEXTFIELD * @param bool $not_in_cart Only return customizations that are not in the cart already * * @return array Result from DB */ public function getProductCustomization($id_product, $type = null, $not_in_cart = false) { if (!Customization::isFeatureActive()) { return []; } // if cart is not set, return nothing to prevent loading of other users data. if (0 === (int) $this->id) { return []; } $result = Db::getInstance()->executeS( 'SELECT cu.id_customization, cd.index, cd.value, cd.type, cu.in_cart, cu.quantity FROM `' . _DB_PREFIX_ . 'customization` cu LEFT JOIN `' . _DB_PREFIX_ . 'customized_data` cd ON (cu.`id_customization` = cd.`id_customization`) WHERE cu.id_cart = ' . (int) $this->id . ' AND cu.id_product = ' . (int) $id_product . ($type === Product::CUSTOMIZE_FILE ? ' AND type = ' . (int) Product::CUSTOMIZE_FILE : '') . ($type === Product::CUSTOMIZE_TEXTFIELD ? ' AND type = ' . (int) Product::CUSTOMIZE_TEXTFIELD : '') . ($not_in_cart ? ' AND in_cart = 0' : '') ); return $result; } /** * Get Carts by Customer ID. * * @param int $id_customer Customer ID * @param bool $with_order Only return Carts that have been converted into an Order * * @return array|false|mysqli_result|PDOStatement|resource|null DB result */ public static function getCustomerCarts($id_customer, $with_order = true) { return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' SELECT * FROM ' . _DB_PREFIX_ . 'cart c WHERE c.`id_customer` = ' . (int) $id_customer . ' ' . (!$with_order ? 'AND NOT EXISTS (SELECT 1 FROM ' . _DB_PREFIX_ . 'orders o WHERE o.`id_cart` = c.`id_cart`)' : '') . ' ORDER BY c.`date_add` DESC'); } /** * Duplicate this Cart in the database. * * @return array|bool Duplicated cart, with success bool */ public function duplicate() { if (!Validate::isLoadedObject($this)) { return false; } $cart = new Cart($this->id); $cart->id = null; $cart->id_shop = $this->id_shop; $cart->id_shop_group = $this->id_shop_group; if (!Customer::customerHasAddress((int) $cart->id_customer, (int) $cart->id_address_delivery)) { $cart->id_address_delivery = (int) Address::getFirstCustomerAddressId((int) $cart->id_customer); } if (!Customer::customerHasAddress((int) $cart->id_customer, (int) $cart->id_address_invoice)) { $cart->id_address_invoice = (int) Address::getFirstCustomerAddressId((int) $cart->id_customer); } if ($cart->id_customer) { $cart->secure_key = Cart::$_customer->secure_key; } $cart->add(); if (!Validate::isLoadedObject($cart)) { return false; } $success = true; $products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT * FROM `' . _DB_PREFIX_ . 'cart_product` WHERE `id_cart` = ' . (int) $this->id); $orderId = Order::getIdByCartId((int) $this->id); $product_gift = []; if ($orderId) { $product_gift = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT cr.`gift_product`, cr.`gift_product_attribute` FROM `' . _DB_PREFIX_ . 'cart_rule` cr LEFT JOIN `' . _DB_PREFIX_ . 'order_cart_rule` ocr ON (ocr.`id_order` = ' . (int) $orderId . ') WHERE ocr.`deleted` = 0 AND ocr.`id_cart_rule` = cr.`id_cart_rule`'); } $id_address_delivery = Configuration::get('PS_ALLOW_MULTISHIPPING') ? $cart->id_address_delivery : 0; // Customized products: duplicate customizations before products so that we get new id_customizations $customs = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( 'SELECT * FROM ' . _DB_PREFIX_ . 'customization c LEFT JOIN ' . _DB_PREFIX_ . 'customized_data cd ON cd.id_customization = c.id_customization WHERE c.id_cart = ' . (int) $this->id ); // Get datas from customization table $customs_by_id = []; foreach ($customs as $custom) { if (!isset($customs_by_id[$custom['id_customization']])) { $customs_by_id[$custom['id_customization']] = [ 'id_product_attribute' => $custom['id_product_attribute'], 'id_product' => $custom['id_product'], 'quantity' => $custom['quantity'], ]; } } // Backward compatibility: if true set customizations quantity to 0, they will be updated in Cart::_updateCustomizationQuantity $new_customization_method = (int) Db::getInstance()->getValue( ' SELECT COUNT(`id_customization`) FROM `' . _DB_PREFIX_ . 'cart_product` WHERE `id_cart` = ' . (int) $this->id . ' AND `id_customization` != 0' ) > 0; // Insert new customizations $custom_ids = []; foreach ($customs_by_id as $customization_id => $val) { if ($new_customization_method) { $val['quantity'] = 0; } Db::getInstance()->execute( 'INSERT INTO `' . _DB_PREFIX_ . 'customization` (id_cart, id_product_attribute, id_product, `id_address_delivery`, quantity, `quantity_refunded`, `quantity_returned`, `in_cart`) VALUES(' . (int) $cart->id . ', ' . (int) $val['id_product_attribute'] . ', ' . (int) $val['id_product'] . ', ' . (int) $id_address_delivery . ', ' . (int) $val['quantity'] . ', 0, 0, 1)' ); $custom_ids[$customization_id] = Db::getInstance(_PS_USE_SQL_SLAVE_)->Insert_ID(); } // Insert customized_data if (count($customs)) { $first = true; $sql_custom_data = 'INSERT INTO ' . _DB_PREFIX_ . 'customized_data (`id_customization`, `type`, `index`, `value`, `id_module`, `price`, `weight`) VALUES '; foreach ($customs as $custom) { if (!$first) { $sql_custom_data .= ','; } else { $first = false; } $customized_value = $custom['value']; if ((int) $custom['type'] == Product::CUSTOMIZE_FILE) { $customized_value = md5(uniqid((string) mt_rand(0, mt_getrandmax()), true)); Tools::copy(_PS_UPLOAD_DIR_ . $custom['value'], _PS_UPLOAD_DIR_ . $customized_value); Tools::copy(_PS_UPLOAD_DIR_ . $custom['value'] . '_small', _PS_UPLOAD_DIR_ . $customized_value . '_small'); } $sql_custom_data .= '(' . (int) $custom_ids[$custom['id_customization']] . ', ' . (int) $custom['type'] . ', ' . (int) $custom['index'] . ', \'' . pSQL($customized_value) . '\', ' . (int) $custom['id_module'] . ', ' . (float) $custom['price'] . ', ' . (float) $custom['weight'] . ')'; } Db::getInstance()->execute($sql_custom_data); } foreach ($products as $product) { if ($id_address_delivery) { if (Customer::customerHasAddress((int) $cart->id_customer, $product['id_address_delivery'])) { $id_address_delivery = $product['id_address_delivery']; } } foreach ($product_gift as $gift) { if (isset($gift['gift_product'], $gift['gift_product_attribute']) && (int) $gift['gift_product'] == (int) $product['id_product'] && (int) $gift['gift_product_attribute'] == (int) $product['id_product_attribute']) { $product['quantity'] = (int) $product['quantity'] - 1; } } $id_customization = (int) $product['id_customization']; $success &= $cart->updateQty( (int) $product['quantity'], (int) $product['id_product'], (int) $product['id_product_attribute'], isset($custom_ids[$id_customization]) ? (int) $custom_ids[$id_customization] : 0, 'up', (int) $id_address_delivery, new Shop((int) $cart->id_shop), false, false ); } return ['cart' => $cart, 'success' => $success]; } /** * Get Cart rows from DB for the webservice. * * @return array|false|mysqli_result|PDOStatement|resource|null DB result */ public function getWsCartRows() { return Db::getInstance()->executeS( 'SELECT id_product, id_product_attribute, quantity, id_address_delivery, id_customization FROM `' . _DB_PREFIX_ . 'cart_product` WHERE id_cart = ' . (int) $this->id . ' AND id_shop = ' . (int) Context::getContext()->shop->id ); } /** * Insert cart rows from webservice. * * @param array $values Values from webservice * * @return bool Whether the values have been successfully inserted * @todo: This function always returns true, make it depend on actual result of DB query */ public function setWsCartRows($values) { if ($this->deleteAssociations()) { $query = 'INSERT INTO `' . _DB_PREFIX_ . 'cart_product`(`id_cart`, `id_product`, `id_product_attribute`, `id_address_delivery`, `id_customization`, `quantity`, `date_add`, `id_shop`) VALUES '; foreach ($values as $value) { $query .= '(' . (int) $this->id . ', ' . (int) $value['id_product'] . ', ' . (isset($value['id_product_attribute']) ? (int) $value['id_product_attribute'] : 'NULL') . ', ' . (isset($value['id_address_delivery']) ? (int) $value['id_address_delivery'] : 0) . ', ' . (isset($value['id_customization']) ? (int) $value['id_customization'] : 0) . ', ' . (int) $value['quantity'] . ', NOW(), ' . (int) Context::getContext()->shop->id . '),'; } Db::getInstance()->execute(rtrim($query, ',')); } return true; } /** * Set delivery Address of a Product in the Cart. * * @param int $id_product Product ID * @param int $id_product_attribute Product Attribute ID * @param int $old_id_address_delivery Old delivery Address ID * @param int $new_id_address_delivery New delivery Address ID * * @return bool Whether the delivery Address of the product in the Cart has been successfully updated */ public function setProductAddressDelivery($id_product, $id_product_attribute, $old_id_address_delivery, $new_id_address_delivery) { // Check address is linked with the customer if (!Customer::customerHasAddress(Context::getContext()->customer->id, $new_id_address_delivery)) { return false; } if ($new_id_address_delivery == $old_id_address_delivery) { return false; } // Checking if the product with the old address delivery exists $sql = new DbQuery(); $sql->select('count(*)'); $sql->from('cart_product', 'cp'); $sql->where('id_product = ' . (int) $id_product); $sql->where('id_product_attribute = ' . (int) $id_product_attribute); $sql->where('id_address_delivery = ' . (int) $old_id_address_delivery); $sql->where('id_cart = ' . (int) $this->id); $result = Db::getInstance()->getValue($sql); if ($result == 0) { return false; } // Checking if there is no others similar products with this new address delivery $sql = new DbQuery(); $sql->select('sum(quantity) as qty'); $sql->from('cart_product', 'cp'); $sql->where('id_product = ' . (int) $id_product); $sql->where('id_product_attribute = ' . (int) $id_product_attribute); $sql->where('id_address_delivery = ' . (int) $new_id_address_delivery); $sql->where('id_cart = ' . (int) $this->id); $result = Db::getInstance()->getValue($sql); // Removing similar products with this new address delivery $sql = 'DELETE FROM ' . _DB_PREFIX_ . 'cart_product WHERE id_product = ' . (int) $id_product . ' AND id_product_attribute = ' . (int) $id_product_attribute . ' AND id_address_delivery = ' . (int) $new_id_address_delivery . ' AND id_cart = ' . (int) $this->id . ' LIMIT 1'; Db::getInstance()->execute($sql); // Changing the address $sql = 'UPDATE ' . _DB_PREFIX_ . 'cart_product SET `id_address_delivery` = ' . (int) $new_id_address_delivery . ', `quantity` = `quantity` + ' . (int) $result . ' WHERE id_product = ' . (int) $id_product . ' AND id_product_attribute = ' . (int) $id_product_attribute . ' AND id_address_delivery = ' . (int) $old_id_address_delivery . ' AND id_cart = ' . (int) $this->id . ' LIMIT 1'; Db::getInstance()->execute($sql); // Changing the address of the customizations $sql = 'UPDATE ' . _DB_PREFIX_ . 'customization SET `id_address_delivery` = ' . (int) $new_id_address_delivery . ' WHERE id_product = ' . (int) $id_product . ' AND id_product_attribute = ' . (int) $id_product_attribute . ' AND id_address_delivery = ' . (int) $old_id_address_delivery . ' AND id_cart = ' . (int) $this->id; Db::getInstance()->execute($sql); return true; } /** * Set customized data of a product. * * @param Product $product Referenced Product object * @param array $customized_datas Customized data */ public function setProductCustomizedDatas(&$product, $customized_datas) { $product['customizedDatas'] = null; if (isset($customized_datas[$product['id_product']][$product['id_product_attribute']])) { $product['customizedDatas'] = $customized_datas[$product['id_product']][$product['id_product_attribute']]; } else { $product['customizationQuantityTotal'] = 0; } } /** * Duplicate Product. * * @param int $id_product Product ID * @param int $id_product_attribute Product Attribute ID * @param int $id_address_delivery Delivery Address ID * @param int $new_id_address_delivery New Delivery Address ID * @param int $quantity Quantity value * @param bool $keep_quantity Keep the quantity, do not reset if true * * @return bool Whether the product has been successfully duplicated */ public function duplicateProduct( $id_product, $id_product_attribute, $id_address_delivery, $new_id_address_delivery, $quantity = 1, $keep_quantity = false ) { // Check address is linked with the customer if (!Customer::customerHasAddress(Context::getContext()->customer->id, $new_id_address_delivery)) { return false; } // Checking the product do not exist with the new address $sql = new DbQuery(); $sql->select('count(*)'); $sql->from('cart_product', 'c'); $sql->where('id_product = ' . (int) $id_product); $sql->where('id_product_attribute = ' . (int) $id_product_attribute); $sql->where('id_address_delivery = ' . (int) $new_id_address_delivery); $sql->where('id_cart = ' . (int) $this->id); $result = Db::getInstance()->getValue($sql); if ($result > 0) { return false; } // Duplicating cart_product line $sql = 'INSERT INTO ' . _DB_PREFIX_ . 'cart_product (`id_cart`, `id_product`, `id_shop`, `id_product_attribute`, `quantity`, `date_add`, `id_address_delivery`) values( ' . (int) $this->id . ', ' . (int) $id_product . ', ' . (int) $this->id_shop . ', ' . (int) $id_product_attribute . ', ' . (int) $quantity . ', NOW(), ' . (int) $new_id_address_delivery . ')'; Db::getInstance()->execute($sql); if (!$keep_quantity) { $sql = new DbQuery(); $sql->select('quantity'); $sql->from('cart_product', 'c'); $sql->where('id_product = ' . (int) $id_product); $sql->where('id_product_attribute = ' . (int) $id_product_attribute); $sql->where('id_address_delivery = ' . (int) $id_address_delivery); $sql->where('id_cart = ' . (int) $this->id); $duplicatedQuantity = Db::getInstance()->getValue($sql); if ($duplicatedQuantity > $quantity) { $sql = 'UPDATE ' . _DB_PREFIX_ . 'cart_product SET `quantity` = `quantity` - ' . (int) $quantity . ' WHERE id_cart = ' . (int) $this->id . ' AND id_product = ' . (int) $id_product . ' AND id_shop = ' . (int) $this->id_shop . ' AND id_product_attribute = ' . (int) $id_product_attribute . ' AND id_address_delivery = ' . (int) $id_address_delivery; Db::getInstance()->execute($sql); } } // Checking if there is customizations $sql = new DbQuery(); $sql->select('*'); $sql->from('customization', 'c'); $sql->where('id_product = ' . (int) $id_product); $sql->where('id_product_attribute = ' . (int) $id_product_attribute); $sql->where('id_address_delivery = ' . (int) $id_address_delivery); $sql->where('id_cart = ' . (int) $this->id); $results = Db::getInstance()->executeS($sql); foreach ($results as $customization) { // Duplicate customization $sql = 'INSERT INTO ' . _DB_PREFIX_ . 'customization (`id_product_attribute`, `id_address_delivery`, `id_cart`, `id_product`, `quantity`, `in_cart`) VALUES ( ' . (int) $customization['id_product_attribute'] . ', ' . (int) $new_id_address_delivery . ', ' . (int) $customization['id_cart'] . ', ' . (int) $customization['id_product'] . ', ' . (int) $quantity . ', ' . (int) $customization['in_cart'] . ')'; Db::getInstance()->execute($sql); // Save last insert ID before doing another query $last_id = (int) Db::getInstance()->Insert_ID(); // Get data from duplicated customizations $sql = new DbQuery(); $sql->select('`type`, `index`, `value`'); $sql->from('customized_data'); $sql->where('id_customization = ' . $customization['id_customization']); $last_row = Db::getInstance()->getRow($sql); // Insert new copied data with new customization ID into customized_data table $last_row['id_customization'] = $last_id; Db::getInstance()->insert('customized_data', $last_row); } $customization_count = count($results); if ($customization_count > 0) { $sql = 'UPDATE ' . _DB_PREFIX_ . 'cart_product SET `quantity` = `quantity` + ' . (int) $customization_count * $quantity . ' WHERE id_cart = ' . (int) $this->id . ' AND id_product = ' . (int) $id_product . ' AND id_shop = ' . (int) $this->id_shop . ' AND id_product_attribute = ' . (int) $id_product_attribute . ' AND id_address_delivery = ' . (int) $new_id_address_delivery; Db::getInstance()->execute($sql); } return true; } /** * Update products cart address delivery with the address delivery of the cart. */ public function setNoMultishipping() { $emptyCache = false; if (Configuration::get('PS_ALLOW_MULTISHIPPING')) { // Upgrading quantities $sql = 'SELECT sum(`quantity`) as quantity, id_product, id_product_attribute, count(*) as count FROM `' . _DB_PREFIX_ . 'cart_product` WHERE `id_cart` = ' . (int) $this->id . ' AND `id_shop` = ' . (int) $this->id_shop . ' GROUP BY id_product, id_product_attribute HAVING count > 1'; foreach (Db::getInstance()->executeS($sql) as $product) { $sql = 'UPDATE `' . _DB_PREFIX_ . 'cart_product` SET `quantity` = ' . $product['quantity'] . ' WHERE `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']; if (Db::getInstance()->execute($sql)) { $emptyCache = true; } } // Merging multiple lines $sql = 'DELETE cp1 FROM `' . _DB_PREFIX_ . 'cart_product` cp1 INNER JOIN `' . _DB_PREFIX_ . 'cart_product` cp2 ON ( (cp1.id_cart = cp2.id_cart) AND (cp1.id_product = cp2.id_product) AND (cp1.id_product_attribute = cp2.id_product_attribute) AND (cp1.id_address_delivery <> cp2.id_address_delivery) AND (cp1.date_add > cp2.date_add) )'; Db::getInstance()->execute($sql); } // Update delivery address for each product line $sql = 'UPDATE `' . _DB_PREFIX_ . 'cart_product` SET `id_address_delivery` = ( SELECT `id_address_delivery` FROM `' . _DB_PREFIX_ . 'cart` WHERE `id_cart` = ' . (int) $this->id . ' AND `id_shop` = ' . (int) $this->id_shop . ' ) WHERE `id_cart` = ' . (int) $this->id . ' ' . (Configuration::get('PS_ALLOW_MULTISHIPPING') ? ' AND `id_shop` = ' . (int) $this->id_shop : ''); $cache_id = 'Cart::setNoMultishipping' . (int) $this->id . '-' . (int) $this->id_shop . ((isset($this->id_address_delivery) && $this->id_address_delivery) ? '-' . (int) $this->id_address_delivery : ''); if (!Cache::isStored($cache_id)) { if ($result = (bool) Db::getInstance()->execute($sql)) { $emptyCache = true; } Cache::store($cache_id, $result); } if (Customization::isFeatureActive()) { Db::getInstance()->execute(' UPDATE `' . _DB_PREFIX_ . 'customization` SET `id_address_delivery` = ( SELECT `id_address_delivery` FROM `' . _DB_PREFIX_ . 'cart` WHERE `id_cart` = ' . (int) $this->id . ' ) WHERE `id_cart` = ' . (int) $this->id); } if ($emptyCache) { $this->_products = null; } } /** * Set an address to all products on the cart without address delivery. */ public function autosetProductAddress() { $id_address_delivery = 0; // Get the main address of the customer if ((int) $this->id_address_delivery > 0) { $id_address_delivery = (int) $this->id_address_delivery; } else { $id_address_delivery = (int) Address::getFirstCustomerAddressId(Context::getContext()->customer->id); } if (!$id_address_delivery) { return; } // Update $sql = 'UPDATE `' . _DB_PREFIX_ . 'cart_product` SET `id_address_delivery` = ' . (int) $id_address_delivery . ' WHERE `id_cart` = ' . (int) $this->id . ' AND (`id_address_delivery` = 0 OR `id_address_delivery` IS NULL) AND `id_shop` = ' . (int) $this->id_shop; Db::getInstance()->execute($sql); $sql = 'UPDATE `' . _DB_PREFIX_ . 'customization` SET `id_address_delivery` = ' . (int) $id_address_delivery . ' WHERE `id_cart` = ' . (int) $this->id . ' AND (`id_address_delivery` = 0 OR `id_address_delivery` IS NULL)'; Db::getInstance()->execute($sql); } public function deleteAssociations() { return Db::getInstance()->execute(' DELETE FROM `' . _DB_PREFIX_ . 'cart_product` WHERE `id_cart` = ' . (int) $this->id) !== false; } /** * isCarrierInRange. * * Check if the specified carrier is in range * * @id_carrier int * @id_zone int */ public function isCarrierInRange($id_carrier, $id_zone) { $carrier = new Carrier((int) $id_carrier, (int) Configuration::get('PS_LANG_DEFAULT')); $shipping_method = $carrier->getShippingMethod(); if (!$carrier->range_behavior) { return true; } if ($shipping_method == Carrier::SHIPPING_METHOD_FREE) { return true; } $check_delivery_price_by_weight = Carrier::checkDeliveryPriceByWeight( (int) $id_carrier, $this->getTotalWeight(), $id_zone ); if ($shipping_method == Carrier::SHIPPING_METHOD_WEIGHT && $check_delivery_price_by_weight !== false) { return true; } $check_delivery_price_by_price = Carrier::checkDeliveryPriceByPrice( (int) $id_carrier, $this->getOrderTotal( true, Cart::BOTH_WITHOUT_SHIPPING ), $id_zone, (int) $this->id_currency ); if ($shipping_method == Carrier::SHIPPING_METHOD_PRICE && $check_delivery_price_by_price !== false) { return true; } return false; } /** * Is the Cart from a guest? * * @param int $id_cart Cart ID * * @return bool True if the Cart has been made by a guest Customer */ public static function isGuestCartByCartId($id_cart) { if (!(int) $id_cart) { return false; } return (bool) Db::getInstance()->getValue(' SELECT `is_guest` FROM `' . _DB_PREFIX_ . 'customer` cu LEFT JOIN `' . _DB_PREFIX_ . 'cart` ca ON (ca.`id_customer` = cu.`id_customer`) WHERE ca.`id_cart` = ' . (int) $id_cart); } /** * Checks if all products of the cart are still available in the current state. They might have been converted to another * type of product since then, ordering disabled or deactivated. * * @return bool false if one of the product not publicly orderable anymore */ public function checkAllProductsAreStillAvailableInThisState() { foreach ($this->getProducts(false, false, null, false) as $product) { $currentProduct = new Product(); $currentProduct->hydrate($product); // Check if the product combinations state is still valid if ($currentProduct->hasAttributes() && $product['id_product_attribute'] === '0') { return false; } // Check if product is still active and possible to order if (!$product['active'] || !$product['available_for_order']) { return false; } } return true; } /** * Are all products of the Cart in stock? * * @param bool $ignoreVirtual Ignore virtual products * @param bool $exclusive (DEPRECATED) If true, the validation is exclusive : it must be present product in stock and out of stock * * @since 1.5.0 * * @return bool False if not all products in the cart are in stock */ public function isAllProductsInStock($ignoreVirtual = false, $exclusive = false) { if (func_num_args() > 1) { @trigger_error( '$exclusive parameter is deprecated since version 1.7.3.2 and will be removed in the next major version.', E_USER_DEPRECATED ); } $productOutOfStock = 0; $productInStock = 0; foreach ($this->getProducts(false, false, null, false) as $product) { if ($ignoreVirtual && $product['is_virtual']) { continue; } $idProductAttribute = !empty($product['id_product_attribute']) ? $product['id_product_attribute'] : null; $availableOutOfStock = Product::isAvailableWhenOutOfStock($product['out_of_stock']); $productQuantity = Product::getQuantity( $product['id_product'], $idProductAttribute, null, $this, false ); if (!$exclusive && ($productQuantity < 0 && !$availableOutOfStock) ) { return false; } elseif ($exclusive) { if ($productQuantity <= 0) { ++$productOutOfStock; } else { ++$productInStock; } if ($productInStock > 0 && $productOutOfStock > 0) { return false; } } } return true; } /** * Checks that all products in cart have minimal required quantities * * @return bool */ public function checkAllProductsHaveMinimalQuantities() { $productList = $this->getProducts(true); foreach ($productList as $product) { if ($product['minimal_quantity'] > $product['cart_quantity']) { return false; } } return true; } /** * Get all the IDs of the delivery Addresses without Carriers. * * @param bool $return_collection Returns sa collection * @param array $error Contains an error message if an error occurs * * @return array Array of address id or of address object */ public function getDeliveryAddressesWithoutCarriers($return_collection = false, &$error = []) { $addresses_without_carriers = []; foreach ($this->getProducts(false, false, null, false) as $product) { if (!in_array($product['id_address_delivery'], $addresses_without_carriers) && !count(Carrier::getAvailableCarrierList(new Product($product['id_product']), null, $product['id_address_delivery'], null, null, $error))) { $addresses_without_carriers[] = $product['id_address_delivery']; } } if (!$return_collection) { return $addresses_without_carriers; } else { $addresses_instance_without_carriers = []; foreach ($addresses_without_carriers as $id_address) { $addresses_instance_without_carriers[] = new Address($id_address); } return $addresses_instance_without_carriers; } } /** * Set flag to split lines of products given away and also manually added to cart. */ protected function splitGiftsProductsQuantity() { $this->shouldSplitGiftProductsQuantity = true; $this->_products = null; return $this; } /** * Set flag to merge lines of products given away and also manually added to cart. */ protected function mergeGiftsProductsQuantity() { $this->shouldSplitGiftProductsQuantity = false; $this->_products = null; return $this; } protected function excludeGiftsDiscountFromTotal() { $this->shouldExcludeGiftsDiscount = true; $this->_products = null; return $this; } protected function includeGiftsDiscountInTotal() { $this->shouldExcludeGiftsDiscount = false; $this->_products = null; return $this; } /** * Get products with gifts and manually added occurrences separated. * * @return array|null */ public function getProductsWithSeparatedGifts() { $products = $this->splitGiftsProductsQuantity() ->getProducts($refresh = true); $this->mergeGiftsProductsQuantity(); return $products; } /** * @return Country * * @throws \PrestaShopDatabaseException * @throws \PrestaShopException */ public function getTaxCountry(): Country { $taxAddressType = Configuration::get('PS_TAX_ADDRESS_TYPE'); $taxAddressId = property_exists($this, $taxAddressType) ? $this->{$taxAddressType} : $this->id_address_delivery; $taxAddress = new Address($taxAddressId); return new Country($taxAddress->id_country); } /** * Alter raw cart details to adapt to display use case. * * @param array $summary * @param bool $refresh * * @return array */ private function alterSummaryForDisplay(array $summary, bool $refresh = false): array { $context = Context::getContext(); $currency = new Currency($this->id_currency); $gift_products = []; $products = $summary['products']; $cart_rules = $summary['discounts']; $total_shipping = $summary['total_shipping']; $total_discounts = $summary['total_discounts']; $total_discounts_tax_exc = $summary['total_discounts_tax_exc']; $total_shipping_tax_exc = $summary['total_shipping_tax_exc']; $total_products = $summary['total_products']; $total_products_wt = $summary['total_products_wt']; // The cart content is altered for display foreach ($cart_rules as &$cart_rule) { // If the cart rule is automatic (without any code) and include free shipping, it should not be displayed as a cart rule but only set the shipping cost to 0 if ($cart_rule['free_shipping'] && (empty($cart_rule['code']) || preg_match('/^' . CartRule::BO_ORDER_CODE_PREFIX . '[0-9]+/', $cart_rule['code']))) { $cart_rule['value_real'] -= $total_shipping; $cart_rule['value_tax_exc'] -= $total_shipping_tax_exc; $cart_rule['value_real'] = Tools::ps_round($cart_rule['value_real'], (int) $context->currency->decimals * Context::getContext()->getComputingPrecision()); $cart_rule['value_tax_exc'] = Tools::ps_round($cart_rule['value_tax_exc'], (int) $context->currency->decimals * Context::getContext()->getComputingPrecision()); if ($total_discounts > $cart_rule['value_real']) { $total_discounts -= $total_shipping; } if ($total_discounts_tax_exc > $cart_rule['value_tax_exc']) { $total_discounts_tax_exc -= $total_shipping_tax_exc; } // Update total shipping $total_shipping = 0; $total_shipping_tax_exc = 0; } if ($cart_rule['gift_product']) { foreach ($products as $key => &$product) { if (empty($product['is_gift']) && $product['id_product'] == $cart_rule['gift_product'] && $product['id_product_attribute'] == $cart_rule['gift_product_attribute']) { // Update total products $total_products_wt = Tools::ps_round($total_products_wt - $product['price_wt'], (int) $context->currency->decimals * Context::getContext()->getComputingPrecision()); $total_products = Tools::ps_round($total_products - $product['price'], (int) $context->currency->decimals * Context::getContext()->getComputingPrecision()); // Update total discounts $total_discounts = Tools::ps_round($total_discounts - $product['price_wt'], (int) $context->currency->decimals * Context::getContext()->getComputingPrecision()); $total_discounts_tax_exc = Tools::ps_round($total_discounts_tax_exc - $product['price'], (int) $context->currency->decimals * Context::getContext()->getComputingPrecision()); // Update cart rule value $cart_rule['value_real'] = Tools::ps_round($cart_rule['value_real'] - $product['price_wt'], (int) $context->currency->decimals * Context::getContext()->getComputingPrecision()); $cart_rule['value_tax_exc'] = Tools::ps_round($cart_rule['value_tax_exc'] - $product['price'], (int) $context->currency->decimals * Context::getContext()->getComputingPrecision()); // Update product quantity $product['total_wt'] = Tools::ps_round($product['total_wt'] - $product['price_wt'], (int) $currency->decimals * Context::getContext()->getComputingPrecision()); $product['total'] = Tools::ps_round($product['total'] - $product['price'], (int) $currency->decimals * Context::getContext()->getComputingPrecision()); --$product['cart_quantity']; if (!$product['cart_quantity']) { unset($products[$key]); } // Add a new product line $gift_product = $product; $gift_product['cart_quantity'] = 1; $gift_product['price'] = 0; $gift_product['price_wt'] = 0; $gift_product['total_wt'] = 0; $gift_product['total'] = 0; $gift_product['is_gift'] = true; $gift_products[] = $gift_product; break; // One gift product per cart rule } } } } foreach ($cart_rules as $key => &$cart_rule) { if (((float) $cart_rule['value_real'] == 0 && (int) $cart_rule['free_shipping'] == 0)) { unset($cart_rules[$key]); } } $summary['discounts'] = $cart_rules; $summary['total_shipping'] = $total_shipping; $summary['total_discounts'] = $total_discounts; $summary['total_discounts_tax_exc'] = $total_discounts_tax_exc; $summary['total_shipping_tax_exc'] = $total_shipping_tax_exc; $summary['total_products'] = $total_products; $summary['total_products_wt'] = $total_products_wt; $summary['products'] = $products; $summary['gift_products'] = $gift_products; return $summary; } /** * @return float */ public function getCartTotalPrice() { $summary = $this->getSummaryDetails(); $id_order = (int) Order::getIdByCartId($this->id); $order = new Order($id_order); if (Validate::isLoadedObject($order)) { $taxCalculationMethod = $order->getTaxCalculationMethod(); } else { $taxCalculationMethod = Group::getPriceDisplayMethod(Group::getCurrent()->id); } return $taxCalculationMethod == PS_TAX_EXC ? $summary['total_price_without_tax'] : $summary['total_price']; } }