* @copyright 2007-2014 PrestaShop SA * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) * International Registered Trademark & Property of PrestaShop SA */ class OrderDetailCore extends ObjectModel { /** @var integer */ public $id_order_detail; /** @var integer */ public $id_order; /** @var integer */ public $id_order_invoice; /** @var integer */ public $product_id; /** @var integer */ public $id_shop; /** @var integer */ public $product_attribute_id; /** @var string */ public $product_name; /** @var integer */ public $product_quantity; /** @var integer */ public $product_quantity_in_stock; /** @var integer */ public $product_quantity_return; /** @var integer */ public $product_quantity_refunded; /** @var integer */ public $product_quantity_reinjected; /** @var float */ public $product_price; /** @var float */ public $original_product_price; /** @var float */ public $unit_price_tax_incl; /** @var float */ public $unit_price_tax_excl; /** @var float */ public $total_price_tax_incl; /** @var float */ public $total_price_tax_excl; /** @var float */ public $reduction_percent; /** @var float */ public $reduction_amount; /** @var float */ public $reduction_amount_tax_excl; /** @var float */ public $reduction_amount_tax_incl; /** @var float */ public $group_reduction; /** @var float */ public $product_quantity_discount; /** @var string */ public $product_ean13; /** @var string */ public $product_upc; /** @var string */ public $product_reference; /** @var string */ public $product_supplier_reference; /** @var float */ public $product_weight; /** @var float */ public $ecotax; /** @var float */ public $ecotax_tax_rate; /** @var integer */ public $discount_quantity_applied; /** @var string */ public $download_hash; /** @var integer */ public $download_nb; /** @var date */ public $download_deadline; /** @var string $tax_name **/ public $tax_name; /** @var float $tax_rate **/ public $tax_rate; /** @var float $tax_computation_method **/ public $tax_computation_method; /** @var int Id warehouse */ public $id_warehouse; /** @var float additional shipping price tax excl */ public $total_shipping_price_tax_excl; /** @var float additional shipping price tax incl */ public $total_shipping_price_tax_incl; /** @var float */ public $purchase_supplier_price; /** * @see ObjectModel::$definition */ public static $definition = array( 'table' => 'order_detail', 'primary' => 'id_order_detail', 'fields' => array( 'id_order' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), 'id_order_invoice' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'), 'id_warehouse' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), 'id_shop' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), 'product_id' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'), 'product_attribute_id' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'), 'product_name' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true), 'product_quantity' => array('type' => self::TYPE_INT, 'validate' => 'isInt', 'required' => true), 'product_quantity_in_stock' => array('type' => self::TYPE_INT, 'validate' => 'isInt'), 'product_quantity_return' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), 'product_quantity_refunded' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), 'product_quantity_reinjected' =>array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), 'product_price' => array('type' => self::TYPE_FLOAT, 'validate' => 'isPrice', 'required' => true), 'reduction_percent' => array('type' => self::TYPE_FLOAT, 'validate' => 'isFloat'), 'reduction_amount' => array('type' => self::TYPE_FLOAT, 'validate' => 'isPrice'), 'reduction_amount_tax_incl' => array('type' => self::TYPE_FLOAT, 'validate' => 'isPrice'), 'reduction_amount_tax_excl' => array('type' => self::TYPE_FLOAT, 'validate' => 'isPrice'), 'group_reduction' => array('type' => self::TYPE_FLOAT, 'validate' => 'isFloat'), 'product_quantity_discount' => array('type' => self::TYPE_FLOAT, 'validate' => 'isFloat'), 'product_ean13' => array('type' => self::TYPE_STRING, 'validate' => 'isEan13'), 'product_upc' => array('type' => self::TYPE_STRING, 'validate' => 'isUpc'), 'product_reference' => array('type' => self::TYPE_STRING, 'validate' => 'isReference'), 'product_supplier_reference' => array('type' => self::TYPE_STRING, 'validate' => 'isReference'), 'product_weight' => array('type' => self::TYPE_FLOAT, 'validate' => 'isFloat'), 'tax_name' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName'), 'tax_rate' => array('type' => self::TYPE_FLOAT, 'validate' => 'isFloat'), 'tax_computation_method' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'), 'ecotax' => array('type' => self::TYPE_FLOAT, 'validate' => 'isFloat'), 'ecotax_tax_rate' => array('type' => self::TYPE_FLOAT, 'validate' => 'isFloat'), 'discount_quantity_applied' => array('type' => self::TYPE_INT, 'validate' => 'isInt'), 'download_hash' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName'), 'download_nb' => array('type' => self::TYPE_INT, 'validate' => 'isInt'), 'download_deadline' => array('type' => self::TYPE_DATE, 'validate' => 'isDateFormat'), 'unit_price_tax_incl' => array('type' => self::TYPE_FLOAT, 'validate' => 'isPrice'), 'unit_price_tax_excl' => array('type' => self::TYPE_FLOAT, 'validate' => 'isPrice'), 'total_price_tax_incl' => array('type' => self::TYPE_FLOAT, 'validate' => 'isPrice'), 'total_price_tax_excl' => array('type' => self::TYPE_FLOAT, 'validate' => 'isPrice'), 'purchase_supplier_price' => array('type' => self::TYPE_FLOAT, 'validate' => 'isPrice'), 'original_product_price' => array('type' => self::TYPE_FLOAT, 'validate' => 'isPrice') ), ); protected $webserviceParameters = array( 'fields' => array ( 'id_order' => array('xlink_resource' => 'orders'), 'product_id' => array('xlink_resource' => 'products'), 'product_attribute_id' => array('xlink_resource' => 'combinations'), 'product_quantity_reinjected' => array(), 'group_reduction' => array(), 'discount_quantity_applied' => array(), 'download_hash' => array(), 'download_deadline' => array() ), 'hidden_fields' => array('tax_rate', 'tax_name'), 'associations' => array( 'taxes' => array('resource' => 'taxes', 'getter' => 'getWsTaxes', 'setter' => false, 'fields' => array('id' => array(), ), ), )); /** @var bool */ protected $outOfStock = false; /** @var TaxCalculator object */ protected $tax_calculator = null; /** @var Address object */ protected $vat_address = null; /** @var Address object */ protected $specificPrice = null; /** @var Customer object */ protected $customer = null; /** @var Context object */ protected $context = null; public function __construct($id = null, $id_lang = null, $context = null) { $this->context = $context; $id_shop = null; if ($this->context != null && isset($this->context->shop)) $id_shop = $this->context->shop->id; parent::__construct($id, $id_lang, $id_shop); if ($context == null) $context = Context::getContext(); $this->context = $context->cloneContext(); } public function delete() { if(!$res = parent::delete()) return false; Db::getInstance()->delete('order_detail_tax', 'id_order_detail='.(int)$this->id); return $res; } protected function setContext($id_shop) { if ($this->context->shop->id != $id_shop) $this->context->shop = new Shop((int)$id_shop); } public static function getDownloadFromHash($hash) { if ($hash == '') return false; $sql = 'SELECT * FROM `'._DB_PREFIX_.'order_detail` od LEFT JOIN `'._DB_PREFIX_.'product_download` pd ON (od.`product_id`=pd.`id_product`) WHERE od.`download_hash` = \''.pSQL(strval($hash)).'\' AND pd.`active` = 1'; return Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql); } public static function incrementDownload($id_order_detail, $increment = 1) { $sql = 'UPDATE `'._DB_PREFIX_.'order_detail` SET `download_nb` = `download_nb` + '.(int)$increment.' WHERE `id_order_detail`= '.(int)$id_order_detail.' LIMIT 1'; return Db::getInstance()->execute($sql); } /** * Returns the tax calculator associated to this order detail. * @since 1.5.0.1 * @return TaxCalculator */ public function getTaxCalculator() { return OrderDetail::getTaxCalculatorStatic($this->id); } /** * Return the tax calculator associated to this order_detail * @since 1.5.0.1 * @param int $id_order_detail * @return TaxCalculator */ public static function getTaxCalculatorStatic($id_order_detail) { $sql = 'SELECT t.*, d.`tax_computation_method` FROM `'._DB_PREFIX_.'order_detail_tax` t LEFT JOIN `'._DB_PREFIX_.'order_detail` d ON (d.`id_order_detail` = t.`id_order_detail`) WHERE d.`id_order_detail` = '.(int)$id_order_detail; $computation_method = 1; $taxes = array(); if ($results = Db::getInstance()->executeS($sql)) { foreach ($results as $result) $taxes[] = new Tax((int)$result['id_tax']); $computation_method = $result['tax_computation_method']; } return new TaxCalculator($taxes, $computation_method); } /** * Save the tax calculator * @since 1.5.0.1 * @return boolean */ public function saveTaxCalculator(Order $order, $replace = false) { // Nothing to save if ($this->tax_calculator == null) return true; if (!($this->tax_calculator instanceOf TaxCalculator)) return false; if (count($this->tax_calculator->taxes) == 0) return true; if ($order->total_products <= 0) return true; $ratio = $this->unit_price_tax_excl / $order->total_products; $order_reduction_amount = $order->total_discounts_tax_excl * $ratio; $discounted_price_tax_excl = $this->unit_price_tax_excl - $order_reduction_amount; $values = ''; foreach ($this->tax_calculator->getTaxesAmount($discounted_price_tax_excl) as $id_tax => $amount) { $unit_amount = (float)Tools::ps_round($amount, 2); $total_amount = $unit_amount * $this->product_quantity; $values .= '('.(int)$this->id.','.(float)$id_tax.','.$unit_amount.','.(float)$total_amount.'),'; } if ($replace) Db::getInstance()->execute('DELETE FROM `'._DB_PREFIX_.'order_detail_tax` WHERE id_order_detail='.(int)$this->id); $values = rtrim($values, ','); $sql = 'INSERT INTO `'._DB_PREFIX_.'order_detail_tax` (id_order_detail, id_tax, unit_amount, total_amount) VALUES '.$values; return Db::getInstance()->execute($sql); } public function updateTaxAmount($order) { $this->setContext((int)$this->id_shop); $address = new Address((int)($order->{Configuration::get('PS_TAX_ADDRESS_TYPE')})); $tax_manager = TaxManagerFactory::getManager($address, (int)Product::getIdTaxRulesGroupByIdProduct((int)$this->product_id, $this->context)); $this->tax_calculator = $tax_manager->getTaxCalculator(); return $this->saveTaxCalculator($order, true); } /** * Get a detailed order list of an id_order * @param int $id_order * @return array */ public static function getList($id_order) { return Db::getInstance()->executeS('SELECT * FROM `'._DB_PREFIX_.'order_detail` WHERE `id_order` = '.(int)$id_order); } /* * Set virtual product information * @param array $product */ protected function setVirtualProductInformation($product) { // Add some informations for virtual products $this->download_deadline = '0000-00-00 00:00:00'; $this->download_hash = null; if ($id_product_download = ProductDownload::getIdFromIdProduct((int)($product['id_product']))) { $productDownload = new ProductDownload((int)($id_product_download)); $this->download_deadline = $productDownload->getDeadLine(); $this->download_hash = $productDownload->getHash(); unset($productDownload); } } /** * Check the order status * @param array $product * @param int $id_order_state */ protected function checkProductStock($product, $id_order_state) { if ($id_order_state != Configuration::get('PS_OS_CANCELED') && $id_order_state != Configuration::get('PS_OS_ERROR')) { $update_quantity = true; if (!StockAvailable::dependsOnStock($product['id_product'])) $update_quantity = StockAvailable::updateQuantity($product['id_product'], $product['id_product_attribute'], -(int)$product['cart_quantity']); if ($update_quantity) $product['stock_quantity'] -= $product['cart_quantity']; if ($product['stock_quantity'] < 0 && Configuration::get('PS_STOCK_MANAGEMENT')) $this->outOfStock = true; Product::updateDefaultAttribute($product['id_product']); } } /** * Apply tax to the product * @param object $order * @param array $product */ protected function setProductTax(Order $order, $product) { $this->ecotax = Tools::convertPrice(floatval($product['ecotax']), intval($order->id_currency)); // Exclude VAT if (!Tax::excludeTaxeOption()) { $this->setContext((int)$product['id_shop']); $id_tax_rules = (int)Product::getIdTaxRulesGroupByIdProduct((int)$product['id_product'], $this->context); $tax_manager = TaxManagerFactory::getManager($this->vat_address, $id_tax_rules); $this->tax_calculator = $tax_manager->getTaxCalculator(); $this->tax_computation_method = (int)$this->tax_calculator->computation_method; } $this->ecotax_tax_rate = 0; if (!empty($product['ecotax'])) $this->ecotax_tax_rate = Tax::getProductEcotaxRate($order->{Configuration::get('PS_TAX_ADDRESS_TYPE')}); } /** * Set specific price of the product * @param object $order */ protected function setSpecificPrice(Order $order, $product = null) { $this->reduction_amount = 0.00; $this->reduction_percent = 0.00; $this->reduction_amount_tax_incl = 0.00; $this->reduction_amount_tax_excl = 0.00; if ($this->specificPrice) switch ($this->specificPrice['reduction_type']) { case 'percentage': $this->reduction_percent = (float)$this->specificPrice['reduction'] * 100; break; case 'amount': $price = Tools::convertPrice($this->specificPrice['reduction'], $order->id_currency); $this->reduction_amount = (float)(!$this->specificPrice['id_currency'] ? $price : $this->specificPrice['reduction']); if ($product !== null) $this->setContext((int)$product['id_shop']); $id_tax_rules = (int)Product::getIdTaxRulesGroupByIdProduct((int)$this->specificPrice['id_product'], $this->context); $tax_manager = TaxManagerFactory::getManager($this->vat_address, $id_tax_rules); $this->tax_calculator = $tax_manager->getTaxCalculator(); $this->reduction_amount_tax_incl = $this->reduction_amount; $this->reduction_amount_tax_excl = Tools::ps_round($this->tax_calculator->removeTaxes($this->reduction_amount_tax_incl), 2); break; } } /** * Set detailed product price to the order detail * @param object $order * @param object $cart * @param array $product */ protected function setDetailProductPrice(Order $order, Cart $cart, $product) { $this->setContext((int)$product['id_shop']); Product::getPriceStatic((int)$product['id_product'], true, (int)$product['id_product_attribute'], 6, null, false, true, $product['cart_quantity'], false, (int)$order->id_customer, (int)$order->id_cart, (int)$order->{Configuration::get('PS_TAX_ADDRESS_TYPE')}, $specific_price, true, true, $this->context); $this->specificPrice = $specific_price; $this->original_product_price = Product::getPriceStatic($product['id_product'], false, (int)$product['id_product_attribute'], 6, null, false, false, 1, false, null, null, null, $null, true, true, $this->context); $this->product_price = $this->original_product_price; $this->unit_price_tax_incl = (float)$product['price_wt']; $this->unit_price_tax_excl = (float)$product['price']; $this->total_price_tax_incl = (float)$product['total_wt']; $this->total_price_tax_excl = (float)$product['total']; $this->purchase_supplier_price = (float)$product['wholesale_price']; if ($product['id_supplier'] > 0) $this->purchase_supplier_price = (float)ProductSupplier::getProductPrice((int)$product['id_supplier'], $product['id_product'], $product['id_product_attribute'], true); $this->setSpecificPrice($order, $product); $this->group_reduction = (float)(Group::getReduction((int)($order->id_customer))); $shop_id = $this->context->shop->id; $quantityDiscount = SpecificPrice::getQuantityDiscount((int)$product['id_product'], $shop_id, (int)$cart->id_currency, (int)$this->vat_address->id_country, (int)$this->customer->id_default_group, (int)$product['cart_quantity'], false, null, null, $null, true, true, $this->context); $unitPrice = Product::getPriceStatic((int)$product['id_product'], true, ($product['id_product_attribute'] ? intval($product['id_product_attribute']) : null), 2, null, false, true, 1, false, (int)$order->id_customer, null, (int)$order->{Configuration::get('PS_TAX_ADDRESS_TYPE')}, $null, true, true, $this->context); $this->product_quantity_discount = 0.00; if ($quantityDiscount) { $this->product_quantity_discount = $unitPrice; if (Product::getTaxCalculationMethod((int)$order->id_customer) == PS_TAX_EXC) $this->product_quantity_discount = Tools::ps_round($unitPrice, 2); if (isset($this->tax_calculator)) $this->product_quantity_discount -= $this->tax_calculator->addTaxes($quantityDiscount['price']); } $this->discount_quantity_applied = (($this->specificPrice && $this->specificPrice['from_quantity'] > 1) ? 1 : 0); } /** * Create an order detail liable to an id_order * @param object $order * @param object $cart * @param array $product * @param int $id_order_status * @param int $id_order_invoice * @param bool $use_taxes set to false if you don't want to use taxes */ protected function create(Order $order, Cart $cart, $product, $id_order_state, $id_order_invoice, $use_taxes = true, $id_warehouse = 0) { if ($use_taxes) $this->tax_calculator = new TaxCalculator(); $this->id = null; $this->product_id = (int)($product['id_product']); $this->product_attribute_id = (int)($product['id_product_attribute'] ? (int)($product['id_product_attribute']) : null); $this->product_name = $product['name']. ((isset($product['attributes']) && $product['attributes'] != null) ? ' - '.$product['attributes'] : ''); $this->product_quantity = (int)($product['cart_quantity']); $this->product_ean13 = empty($product['ean13']) ? null : pSQL($product['ean13']); $this->product_upc = empty($product['upc']) ? null : pSQL($product['upc']); $this->product_reference = empty($product['reference']) ? null : pSQL($product['reference']); $this->product_supplier_reference = empty($product['supplier_reference']) ? null : pSQL($product['supplier_reference']); $this->product_weight = (float)($product['id_product_attribute'] ? $product['weight_attribute'] : $product['weight']); $this->id_warehouse = $id_warehouse; $productQuantity = (int)(Product::getQuantity($this->product_id, $this->product_attribute_id)); $this->product_quantity_in_stock = ($productQuantity - (int)($product['cart_quantity']) < 0) ? $productQuantity : (int)($product['cart_quantity']); $this->setVirtualProductInformation($product); $this->checkProductStock($product, $id_order_state); if ($use_taxes) $this->setProductTax($order, $product); $this->setShippingCost($order, $product); $this->setDetailProductPrice($order, $cart, $product); // Set order invoice id $this->id_order_invoice = (int)$id_order_invoice; // Set shop id $this->id_shop = (int)$product['id_shop']; // Add new entry to the table $this->save(); if ($use_taxes) $this->saveTaxCalculator($order); unset($this->tax_calculator); } /** * Create a list of order detail for a specified id_order using cart * @param object $order * @param object $cart * @param int $id_order_status * @param int $id_order_invoice * @param bool $use_taxes set to false if you don't want to use taxes */ public function createList(Order $order, Cart $cart, $id_order_state, $product_list, $id_order_invoice = 0, $use_taxes = true, $id_warehouse = 0) { $this->vat_address = new Address((int)($order->{Configuration::get('PS_TAX_ADDRESS_TYPE')})); $this->customer = new Customer((int)($order->id_customer)); $this->id_order = $order->id; $this->outOfStock = false; foreach ($product_list as $product) $this->create($order, $cart, $product, $id_order_state, $id_order_invoice, $use_taxes, $id_warehouse); unset($this->vat_address); unset($products); unset($this->customer); } /** * Get the state of the current stock product * @return array */ public function getStockState() { return $this->outOfStock; } /** * Set the additional shipping information * * @param Order $order * @param $product */ public function setShippingCost(Order $order, $product) { $tax_rate = 0; $carrier = OrderInvoice::getCarrier((int)$this->id_order_invoice); if (isset($carrier) && Validate::isLoadedObject($carrier)) $tax_rate = $carrier->getTaxesRate(new Address((int)$order->{Configuration::get('PS_TAX_ADDRESS_TYPE')})); $this->total_shipping_price_tax_excl = (float)$product['additional_shipping_cost']; $this->total_shipping_price_tax_incl = (float)($this->total_shipping_price_tax_excl * (1 + ($tax_rate / 100))); $this->total_shipping_price_tax_incl = Tools::ps_round($this->total_shipping_price_tax_incl, 2); } public function getWsTaxes() { $query = new DbQuery(); $query->select('id_tax as id'); $query->from('order_detail_tax', 'tax'); $query->join('LEFT JOIN `'._DB_PREFIX_.'order_detail` od ON (tax.`id_order_detail` = od.`id_order_detail`)'); $query->where('od.`id_order_detail` = '.(int)$this->id_order_detail); return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query); } public static function getCrossSells($id_product, $id_lang, $limit = 12) { if (!$id_product || !$id_lang) return; $front = true; if (!in_array(Context::getContext()->controller->controller_type, array('front', 'modulefront'))) $front = false; $orders = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' SELECT o.id_order FROM '._DB_PREFIX_.'orders o LEFT JOIN '._DB_PREFIX_.'order_detail od ON (od.id_order = o.id_order) WHERE o.valid = 1 AND od.product_id = '.(int)$id_product); if (sizeof($orders)) { $list = ''; foreach ($orders AS $order) $list .= (int)$order['id_order'].','; $list = rtrim($list, ','); $orderProducts = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' SELECT DISTINCT od.product_id, p.id_product, pl.name, pl.link_rewrite, p.reference, i.id_image, product_shop.show_price, cl.link_rewrite category, p.ean13, p.out_of_stock, p.id_category_default FROM '._DB_PREFIX_.'order_detail od LEFT JOIN '._DB_PREFIX_.'product p ON (p.id_product = od.product_id) '.Shop::addSqlAssociation('product', 'p').' LEFT JOIN '._DB_PREFIX_.'product_lang pl ON (pl.id_product = od.product_id'.Shop::addSqlRestrictionOnLang('pl').') LEFT JOIN '._DB_PREFIX_.'category_lang cl ON (cl.id_category = product_shop.id_category_default'.Shop::addSqlRestrictionOnLang('cl').') LEFT JOIN '._DB_PREFIX_.'image i ON (i.id_product = od.product_id) WHERE od.id_order IN ('.$list.') AND pl.id_lang = '.(int)$id_lang.' AND cl.id_lang = '.(int)$id_lang.' AND od.product_id != '.(int)$id_product.' AND i.cover = 1 AND product_shop.active = 1' .($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '').' ORDER BY RAND() LIMIT '.(int)$limit.' '); $taxCalc = Product::getTaxCalculationMethod(); if (is_array($orderProducts)) { foreach ($orderProducts AS &$orderProduct) { $orderProduct['image'] = Context::getContext()->link->getImageLink($orderProduct['link_rewrite'], (int)$orderProduct['product_id'].'-'.(int)$orderProduct['id_image'], ImageType::getFormatedName('medium')); $orderProduct['link'] = Context::getContext()->link->getProductLink((int)$orderProduct['product_id'], $orderProduct['link_rewrite'], $orderProduct['category'], $orderProduct['ean13']); if ($taxCalc == 0 OR $taxCalc == 2) $orderProduct['displayed_price'] = Product::getPriceStatic((int)$orderProduct['product_id'], true, NULL); elseif ($taxCalc == 1) $orderProduct['displayed_price'] = Product::getPriceStatic((int)$orderProduct['product_id'], false, NULL); } return Product::getProductsProperties($id_lang, $orderProducts); } } } }