From 968b2cc88f8bdec4a99db0a642994aa223e0864a Mon Sep 17 00:00:00 2001 From: rGaillard Date: Thu, 13 Sep 2012 18:20:47 +0000 Subject: [PATCH] // The END --- classes/Product.php | 48 +- classes/shop/Shop.php | 1 - classes/stock/StockAvailable.php | 18 +- .../AdminAttributeGeneratorController.php | 10 +- product.php | 4968 ++++++++++++++++- 5 files changed, 4999 insertions(+), 46 deletions(-) diff --git a/classes/Product.php b/classes/Product.php index a6420f4bd..83d1cabd7 100644 --- a/classes/Product.php +++ b/classes/Product.php @@ -1002,18 +1002,17 @@ class ProductCore extends ObjectModel FROM `'._DB_PREFIX_.'product_attribute` pa JOIN `'._DB_PREFIX_.'product_attribute_shop` pas ON (pas.id_product_attribute = pa.id_product_attribute) LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac ON (pac.`id_product_attribute` = pa.`id_product_attribute`) - WHERE 1 '.(!$all_shops ? ' AND pas.id_shop ='.(int)$context->shop->id : '').' AND pa.`id_product` = '.(int)$this->id + WHERE 1 '.(!$all_shops ? ' AND pas.id_shop ='.(int)$context->shop->id : '').' AND pa.`id_product` = '.(int)$this->id. + ($all_shops ? ' GROUP BY pac.id_attribute, pac.id_product_attribute ' : '') ); /* If something's wrong */ if (!$result || empty($result)) return false; - /* Product attributes simulation */ $product_attributes = array(); foreach ($result as $product_attribute) $product_attributes[$product_attribute['id_product_attribute']][] = $product_attribute['id_attribute']; - /* Checking product's attribute existence */ foreach ($product_attributes as $key => $product_attribute) if (count($product_attribute) == count($attributes_list)) @@ -1635,23 +1634,33 @@ class ProductCore extends ObjectModel { if (!Combination::isFeatureActive()) return array(); + $add_shop = ''; - $sql = 'SELECT pa.*, product_attribute_shop.*, GROUP_CONCAT(agl.`name`, \''.pSQL($attribute_value_separator).'\', - al.`name` ORDER BY agl.`id_attribute_group` SEPARATOR \''.pSQL($attribute_separator).'\') as attribute_designation + $combinations = Db::getInstance()->executeS('SELECT pa.*, product_attribute_shop.* FROM `'._DB_PREFIX_.'product_attribute` pa '.Shop::addSqlAssociation('product_attribute', 'pa').' - LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac ON pac.`id_product_attribute` = pa.`id_product_attribute` + WHERE pa.`id_product` = '.(int)$this->id.' + GROUP BY pa.`id_product_attribute`'); + + $product_attributes = array(); + foreach ($combinations as $combination) + $product_attributes[] = (int)$combination['id_product_attribute']; + + $lang = Db::getInstance()->executeS('SELECT pac.id_product_attribute, GROUP_CONCAT(agl.`name`, \''.pSQL($attribute_value_separator).'\',al.`name` ORDER BY agl.`id_attribute_group` SEPARATOR \''.pSQL($attribute_separator).'\') as attribute_designation + 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 pa.`id_product` = '.(int)$this->id.' - GROUP BY pa.`id_product_attribute`'; - - $res = Db::getInstance()->executeS($sql); - + WHERE pac.id_product_attribute IN ('.implode(',', $product_attributes).') + GROUP BY pac.id_product_attribute'); + + foreach ($lang as $k => $row) + $combinations[$k]['attribute_designation'] = $row['attribute_designation']; + + //Get quantity of each variations - foreach ($res as $key => $row) + foreach ($combinations as $key => $row) { $cache_key = $row['id_product'].'_'.$row['id_product_attribute'].'_quantity'; @@ -1661,10 +1670,10 @@ class ProductCore extends ObjectModel StockAvailable::getQuantityAvailableByProduct($row['id_product'], $row['id_product_attribute']) ); - $res[$key]['quantity'] = Cache::retrieve($cache_key); + $combinations[$key]['quantity'] = Cache::retrieve($cache_key); } - return $res; + return $combinations; } /** @@ -2455,7 +2464,7 @@ class ProductCore extends ObjectModel $cache_id = $id_product.'-'.$id_shop.'-'.$id_currency.'-'.$id_country.'-'.$id_state.'-'.$zipcode.'-'.$id_group. '-'.$quantity.'-'.(int)$id_product_attribute.'-'.($use_tax?'1':'0').'-'.$decimals.'-'.($only_reduc?'1':'0'). '-'.($use_reduc?'1':'0').'-'.$with_ecotax.'-'.$id_customer; - + // reference parameter is filled before any returns $specific_price = SpecificPrice::getSpecificPrice( (int)$id_product, @@ -2490,7 +2499,7 @@ class ProductCore extends ObjectModel } else $sql->select('0 as id_product_attribute'); - + $res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); foreach ($res as $row) { @@ -2505,7 +2514,6 @@ class ProductCore extends ObjectModel self::$_pricesLevel2[$cache_id_2][0] = $array_tmp; } } - if (!isset(self::$_pricesLevel2[$cache_id_2][(int)$id_product_attribute])) return; @@ -2515,7 +2523,6 @@ class ProductCore extends ObjectModel $price = (float)$result['price']; else $price = (float)$specific_price['price']; - // convert only if the specific price is in the default currency (id_currency = 0) if (!$specific_price || !($specific_price['price'] >= 0 && $specific_price['id_currency'])) $price = Tools::convertPrice($price, $id_currency); @@ -4493,11 +4500,12 @@ class ProductCore extends ObjectModel * @param int $id_product the id of the product * @return array product attribute id list */ - public static function getProductAttributesIds($id_product) + public static function getProductAttributesIds($id_product, $shop_only = false) { return Db::getInstance()->executeS(' SELECT pa.id_product_attribute - FROM `'._DB_PREFIX_.'product_attribute` pa + FROM `'._DB_PREFIX_.'product_attribute` pa'. + ($shop_only ? Shop::addSqlAssociation('product_attribute', 'pa') : '').' WHERE pa.`id_product` = '.(int)$id_product); } diff --git a/classes/shop/Shop.php b/classes/shop/Shop.php index 300cce780..6b8a68e63 100644 --- a/classes/shop/Shop.php +++ b/classes/shop/Shop.php @@ -905,7 +905,6 @@ class ShopCore extends ObjectModel $asso_table = Shop::getAssoTable($table); if ($asso_table === false || $asso_table['type'] != 'shop') return; - $sql = (($inner_join) ? ' INNER' : ' LEFT').' JOIN '._DB_PREFIX_.$table.'_shop '.$table_alias.' ON ('.$table_alias.'.id_'.$table.' = '.$alias.'.id_'.$table; if ((int)self::$context_id_shop) diff --git a/classes/stock/StockAvailable.php b/classes/stock/StockAvailable.php index 0c654f4b7..88c9a4c3c 100644 --- a/classes/stock/StockAvailable.php +++ b/classes/stock/StockAvailable.php @@ -457,7 +457,6 @@ class StockAvailableCore extends ObjectModel if (!$depends_on_stock) { $id_stock_available = (int)StockAvailable::getStockAvailableIdByProductId($id_product, $id_product_attribute, $id_shop); - if ($id_stock_available) { $stock_available = new StockAvailable($id_stock_available); @@ -473,24 +472,19 @@ class StockAvailableCore extends ObjectModel $stock_available->id_product_attribute = (int)$id_product_attribute; $stock_available->quantity = (int)$quantity; - // if we are in shop_group context - if (Shop::getContext() == Shop::CONTEXT_GROUP) + $shop_group = new ShopGroup((int)Shop::getContextShopGroupID()); + + // if quantities are shared between shops of the group + if ($shop_group->share_stock) { - $shop_group = new ShopGroup((int)Shop::getContextShopGroupID()); - - // if quantities are shared between shops of the group - if ($shop_group->share_stock) - { - $stock_available->id_shop = 0; - $stock_available->id_shop_group = (int)$shop_group->id; - } + $stock_available->id_shop = 0; + $stock_available->id_shop_group = (int)$shop_group->id; } else { $stock_available->id_shop = $id_shop; $stock_available->id_shop_group = Shop::getGroupFromShop($id_shop); } - $stock_available->add(); } diff --git a/controllers/admin/AdminAttributeGeneratorController.php b/controllers/admin/AdminAttributeGeneratorController.php index 8e94a3650..a394a64ee 100644 --- a/controllers/admin/AdminAttributeGeneratorController.php +++ b/controllers/admin/AdminAttributeGeneratorController.php @@ -114,18 +114,18 @@ class AdminAttributeGeneratorControllerCore extends AdminController // @since 1.5.0 if ($this->product->depends_on_stock == 0) { - $attributes = Product::getProductAttributesIds($this->product->id); + $attributes = Product::getProductAttributesIds($this->product->id, true); foreach ($attributes as $attribute) - StockAvailable::removeProductFromStockAvailable($this->product->id, $attribute['id_product_attribute']); + StockAvailable::removeProductFromStockAvailable($this->product->id, $attribute['id_product_attribute'], Context::getContext()->shop); } - + $this->product->deleteProductAttributes(); $this->product->generateMultipleCombinations($values, $this->combinations); - + // @since 1.5.0 if ($this->product->depends_on_stock == 0) { - $attributes = Product::getProductAttributesIds($this->product->id); + $attributes = Product::getProductAttributesIds($this->product->id, true); $quantity = (int)Tools::getValue('quantity'); foreach ($attributes as $attribute) StockAvailable::setQuantity($this->product->id, $attribute['id_product_attribute'], $quantity); diff --git a/product.php b/product.php index 73b4e26d2..83d1cabd7 100644 --- a/product.php +++ b/product.php @@ -20,19 +20,4971 @@ * * @author PrestaShop SA * @copyright 2007-2012 PrestaShop SA -* @version Release: $Revision: 7104 $ +* @version Release: $Revision: 7506 $ * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) * International Registered Trademark & Property of PrestaShop SA */ /** - * This file will be removed in 1.6 - * You have to use index.php?controller=page_name instead of this page - * - * @deprecated 1.5.0 + * @deprecated 1.5.0.1 */ +define('_CUSTOMIZE_FILE_', 0); +/** + * @deprecated 1.5.0.1 + */ +define('_CUSTOMIZE_TEXTFIELD_', 1); -require(dirname(__FILE__).'/config/config.inc.php'); -Tools::displayFileAsDeprecated(); +class ProductCore extends ObjectModel +{ + /** @var string Tax name */ + public $tax_name; + + /** @var string Tax rate */ + public $tax_rate; + + /** @var integer Manufacturer id */ + public $id_manufacturer; + + /** @var integer Supplier id */ + public $id_supplier; + + /** @var integer default Category id */ + public $id_category_default; + + /** @var integer default Shop id */ + public $id_shop_default; + + /** @var string Manufacturer name */ + public $manufacturer_name; + + /** @var string Supplier name */ + public $supplier_name; + + /** @var string Name */ + public $name; + + /** @var string Long description */ + public $description; + + /** @var string Short description */ + public $description_short; + + /** @var integer Quantity available */ + public $quantity = 0; + + /** @var integer Minimal quantity for add to cart */ + public $minimal_quantity = 1; + + /** @var string available_now */ + public $available_now; + + /** @var string available_later */ + public $available_later; + + /** @var float Price in euros */ + public $price = 0; + + /** @var float Additional shipping cost */ + public $additional_shipping_cost = 0; + + /** @var float Wholesale Price in euros */ + public $wholesale_price = 0; + + /** @var boolean on_sale */ + public $on_sale = false; + + /** @var boolean online_only */ + public $online_only = false; + + /** @var string unity */ + public $unity = null; + + /** @var float price for product's unity */ + public $unit_price; + + /** @var float price for product's unity ratio */ + public $unit_price_ratio = 0; + + /** @var float Ecotax */ + public $ecotax = 0; + + /** @var string Reference */ + public $reference; + + /** @var string Supplier Reference */ + public $supplier_reference; + + /** @var string Location */ + public $location; + + /** @var string Width in default width unit */ + public $width = 0; + + /** @var string Height in default height unit */ + public $height = 0; + + /** @var string Depth in default depth unit */ + public $depth = 0; + + /** @var string Weight in default weight unit */ + public $weight = 0; + + /** @var string Ean-13 barcode */ + public $ean13; + + /** @var string Upc barcode */ + public $upc; + + /** @var string Friendly URL */ + public $link_rewrite; + + /** @var string Meta tag description */ + public $meta_description; + + /** @var string Meta tag keywords */ + public $meta_keywords; + + /** @var string Meta tag title */ + public $meta_title; + + /** @var boolean Product statuts */ + public $quantity_discount = 0; + + /** @var boolean Product customization */ + public $customizable; + + /** @var boolean Product is new */ + public $new = null; + + /** @var integer Number of uploadable files (concerning customizable products) */ + public $uploadable_files; + + /** @var int Number of text fields */ + public $text_fields; + + /** @var boolean Product statuts */ + public $active = true; + + /** @var boolean Product available for order */ + public $available_for_order = true; + + /** @var string Object available order date */ + public $available_date = '0000-00-00'; + + /** @var enum Product condition (new, used, refurbished) */ + public $condition; + + /** @var boolean Show price of Product */ + public $show_price = true; + + /** @var boolean is the product indexed in the search index? */ + public $indexed = 0; + + /** @var string ENUM('both', 'catalog', 'search', 'none') front office visibility */ + public $visibility; + + /** @var string Object creation date */ + public $date_add; + + /** @var string Object last modification date */ + public $date_upd; + + /*** @var array Tags */ + public $tags; + + public $id_tax_rules_group = 1; + + /** + * We keep this variable for retrocompatibility for themes + * @deprecated 1.5.0 + */ + public $id_color_default = 0; + + /** + * @since 1.5.0 + * @var boolean Tells if the product uses the advanced stock management + */ + public $advanced_stock_management = 0; + public $out_of_stock; + public $depends_on_stock; + + public $isFullyLoaded = false; + + public $cache_is_pack; + public $cache_has_attachments; + public $is_virtual; + public $cache_default_attribute; + + /** + * @var string If product is populated, this property contain the rewrite link of the default category + */ + public $category; + + public static $_taxCalculationMethod = PS_TAX_EXC; + protected static $_prices = array(); + protected static $_pricesLevel2 = array(); + protected static $_incat = array(); + protected static $_cart_quantity = array(); + protected static $_tax_rules_group = array(); + protected static $_cacheFeatures = array(); + protected static $_frontFeaturesCache = array(); + protected static $producPropertiesCache = array(); + + /** @var array cache stock data in getStock() method */ + protected static $cacheStock = array(); + + public static $definition = array( + 'table' => 'product', + 'primary' => 'id_product', + 'multilang' => true, + 'multilang_shop' => true, + 'fields' => array( + // Classic fields + 'id_shop_default' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'), + 'id_manufacturer' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'), + 'id_supplier' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'), + 'reference' => array('type' => self::TYPE_STRING, 'validate' => 'isReference', 'size' => 32), + 'supplier_reference' => array('type' => self::TYPE_STRING, 'validate' => 'isReference', 'size' => 32), + 'location' => array('type' => self::TYPE_STRING, 'validate' => 'isReference', 'size' => 64), + 'width' => array('type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'), + 'height' => array('type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'), + 'depth' => array('type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'), + 'weight' => array('type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'), + 'quantity_discount' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'ean13' => array('type' => self::TYPE_STRING, 'validate' => 'isEan13', 'size' => 13), + 'upc' => array('type' => self::TYPE_STRING, 'validate' => 'isUpc', 'size' => 12), + 'cache_is_pack' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'cache_has_attachments' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'is_virtual' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + + /* Shop fields */ + 'id_category_default' => array('type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'), + 'id_tax_rules_group' => array('type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'), + 'on_sale' => array('type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'), + 'online_only' => array('type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'), + 'ecotax' => array('type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice'), + 'minimal_quantity' => array('type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'), + 'price' => array('type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice', 'required' => true), + 'wholesale_price' => array('type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice'), + 'unity' => array('type' => self::TYPE_STRING, 'shop' => true, 'validate' => 'isString'), + 'unit_price_ratio' => array('type' => self::TYPE_FLOAT, 'shop' => true), + 'additional_shipping_cost' => array('type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice'), + 'customizable' => array('type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'), + 'text_fields' => array('type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'), + 'uploadable_files' => array('type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'), + 'active' => array('type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'), + 'available_for_order' => array('type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'), + 'available_date' => array('type' => self::TYPE_DATE, 'shop' => true, 'validate' => 'isDateFormat'), + 'condition' => array('type' => self::TYPE_STRING, 'shop' => true, 'validate' => 'isGenericName', 'values' => array('new', 'used', 'refurbished'), 'default' => 'new'), + 'show_price' => array('type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'), + 'indexed' => array('type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'), + 'visibility' => array('type' => self::TYPE_STRING, 'shop' => true, 'validate' => 'isProductVisibility', 'values' => array('both', 'catalog', 'search', 'none'), 'default' => 'both'), + 'cache_default_attribute' => array('type' => self::TYPE_INT, 'shop' => true), + 'advanced_stock_management' => array('type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'), + 'date_add' => array('type' => self::TYPE_DATE, 'shop' => true, 'validate' => 'isDateFormat'), + 'date_upd' => array('type' => self::TYPE_DATE, 'shop' => true, 'validate' => 'isDateFormat'), + + /* Lang fields */ + 'meta_description' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255), + 'meta_keywords' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255), + 'meta_title' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 128), + 'link_rewrite' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isLinkRewrite', 'required' => true, 'size' => 128), + 'name' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isCatalogName', 'required' => true, 'size' => 128), + 'description' => array('type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isString'), + 'description_short' => array('type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isString'), + 'available_now' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255), + 'available_later' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'IsGenericName', 'size' => 255), + ), + 'associations' => array( + 'manufacturer' => array('type' => self::HAS_ONE), + 'supplier' => array('type' => self::HAS_ONE), + 'default_category' => array('type' => self::HAS_ONE, 'field' => 'id_category_default', 'object' => 'Category'), + 'tax_rules_group' => array('type' => self::HAS_ONE), + 'categories' => array('type' => self::HAS_MANY, 'field' => 'id_category', 'object' => 'Category', 'association' => 'category_product'), + ), + ); + + protected $webserviceParameters = array( + 'objectMethods' => array( + 'add' => 'addWs', + 'update' => 'updateWs' + ), + 'objectNodeNames' => 'products', + 'fields' => array( + 'id_manufacturer' => array( + 'xlink_resource' => 'manufacturers' + ), + 'id_supplier' => array( + 'xlink_resource' => 'suppliers' + ), + 'id_category_default' => array( + 'xlink_resource' => 'categories' + ), + 'new' => array(), + 'cache_default_attribute' => array(), + 'id_default_image' => array( + 'getter' => 'getCoverWs', + 'setter' => 'setCoverWs', + 'xlink_resource' => array( + 'resourceName' => 'images', + 'subResourceName' => 'products' + ) + ), + 'id_default_combination' => array( + 'getter' => 'getWsDefaultCombination', + 'setter' => 'setWsDefaultCombination', + 'xlink_resource' => array( + 'resourceName' => 'combinations' + ) + ), + 'id_tax_rules_group' => array( + 'xlink_resource' => array( + 'resourceName' => 'tax_rules_group' + ) + ), + 'position_in_category' => array( + 'getter' => 'getWsPositionInCategory', + 'setter' => false + ), + 'manufacturer_name' => array( + 'getter' => 'getWsManufacturerName', + 'setter' => false + ), + 'quantity' => array( + 'getter' => false, + 'setter' => false + ), + ), + 'associations' => array( + 'categories' => array( + 'resource' => 'category', + 'fields' => array( + 'id' => array('required' => true), + ) + ), + 'images' => array( + 'resource' => 'image', + 'fields' => array('id' => array()) + ), + 'combinations' => array( + 'resource' => 'combinations', + 'fields' => array( + 'id' => array('required' => true), + ) + ), + 'product_option_values' => array( + 'resource' => 'product_options_values', + 'fields' => array( + 'id' => array('required' => true), + ) + ), + 'product_features' => array( + 'resource' => 'product_feature', + 'fields' => array( + 'id' => array('required' => true), + 'id_feature_value' => array( + 'required' => true, + 'xlink_resource' => 'product_feature_values' + ), + ) + ), + 'tags' => array('resource' => 'tag', + 'fields' => array( + 'id' => array('required' => true), + )), + ), + ); + + const CUSTOMIZE_FILE = 0; + const CUSTOMIZE_TEXTFIELD = 1; + + /** + * Note: prefix is "PTYPE" because TYPE_ is used in ObjectModel (definition) + */ + const PTYPE_SIMPLE = 0; + const PTYPE_PACK = 1; + const PTYPE_VIRTUAL = 2; + + public function __construct($id_product = null, $full = false, $id_lang = null, $id_shop = null, Context $context = null) + { + parent::__construct($id_product, $id_lang, $id_shop); + if (!$context) + $context = Context::getContext(); + + if ($full && $this->id) + { + $this->isFullyLoaded = $full; + $this->tax_name = 'deprecated'; // The applicable tax may be BOTH the product one AND the state one (moreover this variable is some deadcode) + $this->manufacturer_name = Manufacturer::getNameById((int)$this->id_manufacturer); + $this->supplier_name = Supplier::getNameById((int)$this->id_supplier); + $address = null; + if (is_object($context->cart) && $context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')} != null) + $address = $context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}; + + $this->tax_rate = $this->getTaxesRate(new Address($address)); + + $this->new = $this->isNew(); + $this->price = Product::getPriceStatic((int)$this->id, false, null, 6, null, false, true, 1, false, null, null, null, $this->specificPrice); + $this->unit_price = ($this->unit_price_ratio != 0 ? $this->price / $this->unit_price_ratio : 0); + if ($this->id) + $this->tags = Tag::getProductTags((int)$this->id); + + $this->loadStockData(); + } + + if ($this->id_category_default) + $this->category = Category::getLinkRewrite((int)$this->id_category_default, (int)$id_lang); + } + + /** + * @see ObjectModel::getFieldsShop() + * @return array + */ + public function getFieldsShop() + { + $fields = parent::getFieldsShop(); + if (is_null($this->update_fields) || (!empty($this->update_fields['price']) && !empty($this->update_fields['unit_price']))) + $fields['unit_price_ratio'] = (float)$this->unit_price > 0 ? $this->price / $this->unit_price : 0; + + return $fields; + } + + public function add($autodate = true, $null_values = false) + { + if (!parent::add($autodate, $null_values)) + return false; + + Hook::exec('actionProductSave', array('id_product' => $this->id)); + return true; + } + + public function update($null_values = false) + { + $return = parent::update($null_values); + Hook::exec('actionProductSave', array('id_product' => $this->id)); + return $return; + } + + public static function initPricesComputation($id_customer = null) + { + if ($id_customer) + { + $customer = new Customer((int)$id_customer); + if (!Validate::isLoadedObject($customer)) + die(Tools::displayError()); + self::$_taxCalculationMethod = Group::getPriceDisplayMethod((int)$customer->id_default_group); + } + else if (Validate::isLoadedObject(Context::getContext()->customer)) + self::$_taxCalculationMethod = Group::getPriceDisplayMethod(Context::getContext()->customer->id_default_group); + else + self::$_taxCalculationMethod = Group::getDefaultPriceDisplayMethod(); + } + + public static function getTaxCalculationMethod($id_customer = null) + { + if ($id_customer) + Product::initPricesComputation((int)$id_customer); + return (int)self::$_taxCalculationMethod; + } + + /** + * Move a product inside its category + * @param boolean $way Up (1) or Down (0) + * @param integer $position + * return boolean Update result + */ + public function updatePosition($way, $position) + { + if (!$res = Db::getInstance()->executeS(' + SELECT cp.`id_product`, cp.`position`, cp.`id_category` + FROM `'._DB_PREFIX_.'category_product` cp + WHERE cp.`id_category` = '.(int)Tools::getValue('id_category', 1).' + ORDER BY cp.`position` ASC' + )) + return false; + + foreach ($res as $product) + if ((int)$product['id_product'] == (int)$this->id) + $moved_product = $product; + + if (!isset($moved_product) || !isset($position)) + return false; + + // < and > statements rather than BETWEEN operator + // since BETWEEN is treated differently according to databases + return (Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'category_product` + SET `position`= `position` '.($way ? '- 1' : '+ 1').' + WHERE `position` + '.($way + ? '> '.(int)$moved_product['position'].' AND `position` <= '.(int)$position + : '< '.(int)$moved_product['position'].' AND `position` >= '.(int)$position).' + AND `id_category`='.(int)$moved_product['id_category']) + && Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'category_product` + SET `position` = '.(int)$position.' + WHERE `id_product` = '.(int)$moved_product['id_product'].' + AND `id_category`='.(int)$moved_product['id_category'])); + } + + /* + * Reorder product position in category $id_category. + * Call it after deleting a product from a category. + * + * @param int $id_category + */ + public static function cleanPositions($id_category) + { + $return = true; + + $result = Db::getInstance()->executeS(' + SELECT `id_product` + FROM `'._DB_PREFIX_.'category_product` + WHERE `id_category` = '.(int)$id_category.' + ORDER BY `position` + '); + $total = count($result); + + for ($i = 0; $i < $total; $i++) + $return &= Db::getInstance()->update('category_product', array( + 'position' => $i, + ), '`id_category` = '.(int)$id_category.' AND `id_product` = '.(int)$result[$i]['id_product']); + + return $return; + } + + /** + * Get the default attribute for a product + * + * @return int Attributes list + */ + public static function getDefaultAttribute($id_product, $minimum_quantity = 0) + { + if (!Combination::isFeatureActive()) + return 0; + + $sql = 'SELECT pa.id_product_attribute + FROM '._DB_PREFIX_.'product_attribute pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + '.($minimum_quantity > 0 ? Product::sqlStock('pa', 'pa') : ''). + ' WHERE product_attribute_shop.default_on = 1 ' + .($minimum_quantity > 0 ? ' AND IFNULL(stock.quantity, 0) >= '.(int)$minimum_quantity : ''). + ' AND pa.id_product = '.(int)$id_product; + $result = Db::getInstance()->getValue($sql); + + if (!$result) + { + $sql = 'SELECT pa.id_product_attribute + FROM '._DB_PREFIX_.'product_attribute pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + '.($minimum_quantity > 0 ? Product::sqlStock('pa', 'pa') : ''). + ' WHERE pa.id_product = '.(int)$id_product + .($minimum_quantity > 0 ? ' AND IFNULL(stock.quantity, 0) >= '.(int)$minimum_quantity : ''); + $result = Db::getInstance()->getValue($sql); + } + + if (!$result) + { + $sql = 'SELECT pa.id_product_attribute + FROM '._DB_PREFIX_.'product_attribute pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + WHERE product_attribute_shop.`default_on` = 1 + AND pa.id_product = '.(int)$id_product; + $result = Db::getInstance()->getValue($sql); + } + + if (!$result) + { + $sql = 'SELECT pa.id_product_attribute + FROM '._DB_PREFIX_.'product_attribute pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + WHERE pa.id_product = '.(int)$id_product; + $result = Db::getInstance()->getValue($sql); + } + return $result; + } + + public static function updateDefaultAttribute($id_product) + { + Db::getInstance()->update('product', array( + 'cache_default_attribute' => (int)Product::getDefaultAttribute($id_product), + ), 'id_product = '.(int)$id_product); + } + + public static function updateIsVirtual($id_product) + { + Db::getInstance()->update('product', array( + 'is_virtual' => (int)$id_product, + ), 'id_product = '.(int)$id_product); + } + + /** + * @see ObjectModel::validateFieldsLang() + */ + public function validateFieldsLang($die = true, $error_return = false) + { + $limit = (int)Configuration::get('PS_PRODUCT_SHORT_DESC_LIMIT'); + if ($limit <= 0) + $limit = 800; + $this->def['fields']['description_short']['size'] = $limit; + + return parent::validateFieldsLang($die, $error_return); + } + + public function delete() + { + /* + * @since 1.5.0 + * It is NOT possible to delete a product if there are currently: + * - physical stock for this product + * - supply order(s) for this product + */ + if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && $this->advanced_stock_management) + { + $stock_manager = StockManagerFactory::getManager(); + $physical_quantity = $stock_manager->getProductPhysicalQuantities($this->id, 0); + $real_quantity = $stock_manager->getProductRealQuantities($this->id, 0); + if ($physical_quantity > 0) + return false; + if ($real_quantity > $physical_quantity) + return false; + } + + // Removes the product from StockAvailable, for the current shop + StockAvailable::removeProductFromStockAvailable($this->id); + + $result = parent::delete(); + $result &= ($this->deleteProductAttributes() && $this->deleteImages() && $this->deleteSceneProducts()); + // If there are still entries in product_shop, don't remove completly the product + if ($this->hasMultishopEntries()) + return true; + + Hook::exec('actionProductDelete', array('product' => $this)); + if (!$result || + !GroupReduction::deleteProductReduction($this->id) || + !$this->deleteCategories(true) || + !$this->deleteProductFeatures() || + !$this->deleteTags() || + !$this->deleteCartProducts() || + !$this->deleteAttributesImpacts() || + !$this->deleteAttachments() || + !$this->deleteCustomization() || + !SpecificPrice::deleteByProductId((int)$this->id) || + !$this->deletePack() || + !$this->deleteProductSale() || + !$this->deleteSearchIndexes() || + !$this->deleteAccessories() || + !$this->deleteFromAccessories() || + !$this->deleteFromSupplier() || + !$this->deleteDownload() || + !$this->deleteFromCartRules()) + return false; + + return true; + } + + public function deleteSelection($products) + { + $return = 1; + if (is_array($products) && ($count = count($products))) + { + // Deleting products can be quite long on a cheap server. Let's say 1.5 seconds by product (I've seen it!). + if (intval(ini_get('max_execution_time')) < round($count * 1.5)) + ini_set('max_execution_time', round($count * 1.5)); + + foreach ($products as $id_product) + { + $product = new Product((int)$id_product); + $return &= $product->delete(); + } + } + return $return; + } + + public function deleteFromCartRules() + { + CartRule::cleanProductRuleIntegrity('products', $this->id); + return true; + } + + public function deleteFromSupplier() + { + return Db::getInstance()->delete('product_supplier', 'id_product = '.(int)$this->id); + } + + /** + * addToCategories add this product to the category/ies if not exists. + * + * @param mixed $categories id_category or array of id_category + * @return boolean true if succeed + */ + public function addToCategories($categories = array()) + { + if (empty($categories)) + return false; + + if (!is_array($categories)) + $categories = array($categories); + + if (!count($categories)) + return false; + + $categories = array_map('intval', $categories); + + $current_categories = $this->getCategories(); + $current_categories = array_map('intval', $current_categories); + + // for new categ, put product at last position + $res_categ_new_pos = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' + SELECT id_category, MAX(position)+1 newPos + FROM `'._DB_PREFIX_.'category_product` + WHERE `id_category` IN('.implode(',', $categories).') + GROUP BY id_category'); + foreach ($res_categ_new_pos as $array) + $new_categories[(int)$array['id_category']] = (int)$array['newPos']; + + $new_categ_pos = array(); + foreach ($categories as $id_category) + $new_categ_pos[$id_category] = isset($new_categories[$id_category]) ? $new_categories[$id_category] : 0; + + $product_cats = array(); + + foreach ($categories as $new_id_categ) + if (!in_array($new_id_categ, $current_categories)) + $product_cats[] = array( + 'id_category' => (int)$new_id_categ, + 'id_product' => (int)$this->id, + 'position' => (int)$new_categ_pos[$new_id_categ], + ); + + Db::getInstance()->insert('category_product', $product_cats); + return true; + } + + /** + * Update categories to index product into + * + * @param string $productCategories Categories list to index product into + * @param boolean $keeping_current_pos (deprecated, no more used) + * @return array Update/insertion result + */ + public function updateCategories($categories, $keeping_current_pos = false) + { + if (empty($categories)) + return false; + + $result = Db::getInstance()->executeS(' + SELECT c.`id_category` + FROM `'._DB_PREFIX_.'category_product` cp + LEFT JOIN `'._DB_PREFIX_.'category` c ON (c.`id_category` = cp.`id_category`) + '.Shop::addSqlAssociation('category', 'c', true).' + WHERE cp.`id_category` NOT IN ('.implode(',', array_map('intval', $categories)).') + AND cp.id_product = '.$this->id + ); + + foreach ($result as $categ_to_delete) + $this->deleteCategory($categ_to_delete['id_category']); + // if none are found, it's an error + if (!is_array($result)) + return false; + + if (!$this->addToCategories($categories)) + return false; + + if (!$this->setGroupReduction()) + return false; + SpecificPriceRule::applyAllRules(array((int)$this->id)); + return true; + } + + /** + * deleteCategory delete this product from the category $id_category + * + * @param mixed $id_category + * @param mixed $clean_positions + * @return boolean + */ + public function deleteCategory($id_category, $clean_positions = true) + { + $result = Db::getInstance()->executeS( + 'SELECT `id_category` + FROM `'._DB_PREFIX_.'category_product` + WHERE `id_product` = '.(int)$this->id.' + AND id_category = '.(int)$id_category.'' + ); + + $return = Db::getInstance()->delete('category_product', 'id_product = '.(int)$this->id.' AND id_category = '.(int)$id_category); + if ($clean_positions === true) + foreach ($result as $row) + $this->cleanPositions((int)$row['id_category']); + SpecificPriceRule::applyAllRules(array((int)$this->id)); + return $return; + } + + /** + * Delete all association to category where product is indexed + * + * @param boolean $clean_positions clean category positions after deletion + * @return array Deletion result + */ + public function deleteCategories($clean_positions = false) + { + $return = Db::getInstance()->delete('category_product', 'id_product = '.(int)$this->id); + if ($clean_positions === true) + { + $result = Db::getInstance()->executeS( + 'SELECT `id_category` + FROM `'._DB_PREFIX_.'category_product` + WHERE `id_product` = '.(int)$this->id + ); + + foreach ($result as $row) + $this->cleanPositions((int)$row['id_category']); + } + return $return; + } + + /** + * Delete products tags entries + * + * @return array Deletion result + */ + public function deleteTags() + { + return Db::getInstance()->delete('product_tag', 'id_product = '.(int)$this->id) + && Db::getInstance()->delete('tag', 'id_tag NOT IN (SELECT id_tag FROM '._DB_PREFIX_.'product_tag)'); + } + + /** + * Delete product from cart + * + * @return array Deletion result + */ + public function deleteCartProducts() + { + return Db::getInstance()->delete('cart_product', 'id_product = '.(int)$this->id); + } + + /** + * Delete product images from database + * + * @return bool success + */ + public function deleteImages() + { + $result = Db::getInstance()->executeS(' + SELECT `id_image` + FROM `'._DB_PREFIX_.'image` + WHERE `id_product` = '.(int)$this->id + ); + + $status = true; + if ($result) + foreach ($result as $row) + { + $image = new Image($row['id_image']); + $status &= $image->delete(); + } + return $status; + } + + /** + * @deprecated 1.5.0 Use Combination::getPrice() + */ + public static function getProductAttributePrice($id_product_attribute) + { + return Combination::getPrice($id_product_attribute); + } + + /** + * Get all available products + * + * @param integer $id_lang Language id + * @param integer $start Start number + * @param integer $limit Number of products to return + * @param string $order_by Field for ordering + * @param string $order_way Way for ordering (ASC or DESC) + * @return array Products details + */ + public static function getProducts($id_lang, $start, $limit, $order_by, $order_way, $id_category = false, + $only_active = false, Context $context = null) + { + if (!$context) + $context = Context::getContext(); + + $front = true; + if (!in_array($context->controller->controller_type, array('front', 'modulefront'))) + $front = false; + + if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay($order_way)) + die (Tools::displayError()); + if ($order_by == 'id_product' || $order_by == 'price' || $order_by == 'date_add') + $order_by_prefix = 'p'; + else if ($order_by == 'name') + $order_by_prefix = 'pl'; + else if ($order_by == 'position') + $order_by_prefix = 'c'; + + if (strpos($order_by, '.') > 0) + { + $order_by = explode('.', $order_by); + $order_by_prefix = $order_by[0]; + $order_by = $order_by[1]; + } + $sql = 'SELECT p.*, product_shop.*, pl.* , t.`rate` AS tax_rate, m.`name` AS manufacturer_name, s.`name` AS supplier_name + FROM `'._DB_PREFIX_.'product` p + '.Shop::addSqlAssociation('product', 'p').' + LEFT JOIN `'._DB_PREFIX_.'product_lang` pl ON (p.`id_product` = pl.`id_product` '.Shop::addSqlRestrictionOnLang('pl').') + LEFT JOIN `'._DB_PREFIX_.'tax_rule` tr ON (product_shop.`id_tax_rules_group` = tr.`id_tax_rules_group` + AND tr.`id_country` = '.(int)Context::getContext()->country->id.' + AND tr.`id_state` = 0) + LEFT JOIN `'._DB_PREFIX_.'tax` t ON (t.`id_tax` = tr.`id_tax`) + LEFT JOIN `'._DB_PREFIX_.'manufacturer` m ON (m.`id_manufacturer` = p.`id_manufacturer`) + LEFT JOIN `'._DB_PREFIX_.'supplier` s ON (s.`id_supplier` = p.`id_supplier`)'. + ($id_category ? 'LEFT JOIN `'._DB_PREFIX_.'category_product` c ON (c.`id_product` = p.`id_product`)' : '').' + WHERE pl.`id_lang` = '.(int)$id_lang. + ($id_category ? ' AND c.`id_category` = '.(int)$id_category : ''). + ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : ''). + ($only_active ? ' AND product_shop.`active` = 1' : '').' + ORDER BY '.(isset($order_by_prefix) ? pSQL($order_by_prefix).'.' : '').'`'.pSQL($order_by).'` '.pSQL($order_way). + ($limit > 0 ? ' LIMIT '.(int)$start.','.(int)$limit : ''); + $rq = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); + if ($order_by == 'price') + Tools::orderbyPrice($rq, $order_way); + return ($rq); + } + + public static function getSimpleProducts($id_lang, Context $context = null) + { + if (!$context) + $context = Context::getContext(); + + $front = true; + if (!in_array($context->controller->controller_type, array('front', 'modulefront'))) + $front = false; + + $sql = 'SELECT p.`id_product`, pl.`name` + FROM `'._DB_PREFIX_.'product` p + '.Shop::addSqlAssociation('product', 'p').' + LEFT JOIN `'._DB_PREFIX_.'product_lang` pl ON (p.`id_product` = pl.`id_product` '.Shop::addSqlRestrictionOnLang('pl').') + WHERE pl.`id_lang` = '.(int)$id_lang.' + '.($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '').' + ORDER BY pl.`name`'; + return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); + } + + public function isNew() + { + $result = Db::getInstance()->executeS(' + SELECT p.id_product + FROM `'._DB_PREFIX_.'product` p + '.Shop::addSqlAssociation('product', 'p').' + WHERE p.id_product = '.(int)$this->id.' + AND DATEDIFF( + product_shop.`date_add`, + DATE_SUB( + NOW(), + INTERVAL '.(Validate::isUnsignedInt(Configuration::get('PS_NB_DAYS_NEW_PRODUCT')) ? Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20).' DAY + ) + ) > 0 + '); + return count($result) > 0; + } + + public function productAttributeExists($attributes_list, $current_product_attribute = false, Context $context = null, $all_shops = false, $return_id = false) + { + if (!Combination::isFeatureActive()) + return false; + if ($context === null) + $context = Context::getContext(); + $result = Db::getInstance()->executeS( + 'SELECT pac.`id_attribute`, pac.`id_product_attribute` + FROM `'._DB_PREFIX_.'product_attribute` pa + JOIN `'._DB_PREFIX_.'product_attribute_shop` pas ON (pas.id_product_attribute = pa.id_product_attribute) + LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac ON (pac.`id_product_attribute` = pa.`id_product_attribute`) + WHERE 1 '.(!$all_shops ? ' AND pas.id_shop ='.(int)$context->shop->id : '').' AND pa.`id_product` = '.(int)$this->id. + ($all_shops ? ' GROUP BY pac.id_attribute, pac.id_product_attribute ' : '') + ); + + /* If something's wrong */ + if (!$result || empty($result)) + return false; + /* Product attributes simulation */ + $product_attributes = array(); + foreach ($result as $product_attribute) + $product_attributes[$product_attribute['id_product_attribute']][] = $product_attribute['id_attribute']; + /* Checking product's attribute existence */ + foreach ($product_attributes as $key => $product_attribute) + if (count($product_attribute) == count($attributes_list)) + { + $diff = false; + for ($i = 0; $diff == false && isset($product_attribute[$i]); $i++) + if (!in_array($product_attribute[$i], $attributes_list) || $key == $current_product_attribute) + $diff = true; + if (!$diff) + { + if ($return_id) + return $key; + return true; + } + } + + return false; + } + + /** + * addProductAttribute is deprecated + * + * The quantity params now set StockAvailable for the current shop with the specified quantity + * The supplier_reference params now set the supplier reference of the default supplier of the product if possible + * + * @see StockManager if you want to manage real stock + * @see StockAvailable if you want to manage available quantities for sale on your shop(s) + * @see ProductSupplier for manage supplier reference(s) + * + * @deprecated since 1.5.0 + */ + public function addProductAttribute($price, $weight, $unit_impact, $ecotax, $quantity, $id_images, $reference, + $id_supplier = null, $ean13, $default, $location = null, $upc = null, $minimal_quantity = 1) + { + Tools::displayAsDeprecated(); + + $id_product_attribute = $this->addAttribute( + $price, $weight, $unit_impact, $ecotax, $id_images, + $reference, $ean13, $default, $location, $upc, $minimal_quantity + ); + + if (!$id_product_attribute) + return false; + + StockAvailable::setQuantity($this->id, $id_product_attribute, $quantity); + //Try to set the default supplier reference + $this->addSupplierReference($id_supplier, $id_product_attribute); + return $id_product_attribute; + } + + public function generateMultipleCombinations($combinations, $attributes) + { + $attributes_list = array(); + $res = true; + + foreach ($combinations as $key => $combination) + { + $id_combination = (int)$this->productAttributeExists($attributes[$key], false, null, true, true); + $obj = new Combination($id_combination); + + if ($id_combination) + { + $obj->minimal_quantity = 1; + $obj->available_date = '0000-00-00'; + } + + foreach ($combination as $field => $value) + $obj->$field = $value; + + $obj->save(); + + if (!$id_combination) + { + $attribute_list = array(); + foreach ($attributes[$key] as $id_attribute) + $attribute_list[] = array( + 'id_product_attribute' => (int)$obj->id, + 'id_attribute' => (int)$id_attribute + ); + $res &= Db::getInstance()->insert('product_attribute_combination', $attribute_list); + } + } + + return $res; + } + + /** + * @param integer $quantity DEPRECATED + * @param string $supplier_reference DEPRECATED + */ + public function addCombinationEntity($wholesale_price, $price, $weight, $unit_impact, $ecotax, $quantity, + $id_images, $reference, $id_supplier, $ean13, $default, $location = null, $upc = null, $minimal_quantity = 1, array $id_shop_list = array()) + { + $id_product_attribute = $this->addAttribute( + $price, $weight, $unit_impact, $ecotax, $id_images, + $reference, $ean13, $default, $location, $upc, $minimal_quantity, $id_shop_list); + + $this->addSupplierReference($id_supplier, $id_product_attribute); + $result = ObjectModel::updateMultishopTable('Combination', array( + 'wholesale_price' => (float)$wholesale_price, + ), 'a.id_product_attribute = '.(int)$id_product_attribute); + + if (!$id_product_attribute || !$result) + return false; + + if ($this->getType() == Product::PTYPE_VIRTUAL) + StockAvailable::setProductOutOfStock((int)$this->id, 1, null, $id_product_attribute); + else + StockAvailable::setProductOutOfStock((int)$this->id, StockAvailable::outOfStock($this->id), null, $id_product_attribute); + SpecificPriceRule::applyAllRules(array((int)$this->id)); + return $id_product_attribute; + } + + public function addProductAttributeMultiple($attributes, $set_default = true) + { + Tools::displayAsDeprecated(); + $return = array(); + $default_value = 1; + foreach ($attributes as &$attribute) + { + $obj = new Combination(); + foreach ($attribute as $key => $value) + $obj->$key = $value; + + if ($set_default) + { + $obj->default_on = $default_value; + $default_value = 0; + // if we add a combination for this shop and this product does not use the combination feature in other shop, + // we clone the default combination in every shop linked to this product + if (!$this->hasAttributesInOtherShops()) + { + $id_shop_list_array = Product::getShopsByProduct($this->id); + $id_shop_list = array(); + foreach ($id_shop_list_array as $array_shop) + $id_shop_list[] = $array_shop['id_shop']; + $obj->id_shop_list = $id_shop_list; + } + } + $obj->add(); + $return[] = $obj->id; + } + + return $return; + } + + /** + * Del all default attributes for product + */ + public function deleteDefaultAttributes() + { + return ObjectModel::updateMultishopTable('Combination', array( + 'default_on' => 0, + ), 'id_product = '.(int)$this->id); + } + + public function setDefaultAttribute($id_product_attribute) + { + $result = ObjectModel::updateMultishopTable('Combination', array( + 'default_on' => 1 + ), '`id_product` = '.(int)$this->id.' AND a.`id_product_attribute` = '.(int)$id_product_attribute); + + $result &= ObjectModel::updateMultishopTable('product', array( + 'cache_default_attribute' => (int)$id_product_attribute, + ), 'a.`id_product` = '.(int)$this->id); + + return $result; + } + + /** + * Update a product attribute + * + * @deprecated since 1.5 + * @see updateAttribute() to use instead + * @see ProductSupplier for manage supplier reference(s) + * + */ + public function updateProductAttribute($id_product_attribute, $wholesale_price, $price, $weight, $unit, $ecotax, + $id_images, $reference, $id_supplier = null, $ean13, $default, $location = null, $upc = null, $minimal_quantity, $available_date) + { + Tools::displayAsDeprecated(); + + $return = $this->updateAttribute( + $id_product_attribute, $wholesale_price, $price, $weight, $unit, $ecotax, + $id_images, $reference, $ean13, $default, $location = null, $upc = null, $minimal_quantity, $available_date + ); + $this->addSupplierReference($id_supplier, $id_product_attribute); + + return $return; + } + + /** + * Sets Supplier Reference + * + * @param int $id_supplier + * @param int $id_product_attribute + * @param string $supplier_reference + * @param float $price + * @param int $id_currency + */ + public function addSupplierReference($id_supplier, $id_product_attribute, $supplier_reference = null, $price = null, $id_currency = null) + { + //in some case we need to add price without supplier reference + if ($supplier_reference === null) + $supplier_reference = ''; + + //Try to set the default supplier reference + if ($id_supplier > 0) + { + $id_product_supplier = (int)ProductSupplier::getIdByProductAndSupplier($this->id, $id_product_attribute, $id_supplier); + + if (!$id_product_supplier) + { + //create new record + $product_supplier_entity = new ProductSupplier(); + $product_supplier_entity->id_product = (int)$this->id; + $product_supplier_entity->id_product_attribute = (int)$id_product_attribute; + $product_supplier_entity->id_supplier = (int)$id_supplier; + $product_supplier_entity->product_supplier_reference = pSQL($supplier_reference); + $product_supplier_entity->product_supplier_price_te = (int)$price; + $product_supplier_entity->id_currency = (int)$id_currency; + $product_supplier_entity->save(); + } + else + { + $product_supplier = new ProductSupplier((int)$id_product_supplier); + $product_supplier->product_supplier_reference = pSQL($supplier_reference); + $product_supplier->update(); + } + } + } + + /** + * Update a product attribute + * + * @param integer $id_product_attribute Product attribute id + * @param float $wholesale_price Wholesale price + * @param float $price Additional price + * @param float $weight Additional weight + * @param float $unit + * @param float $ecotax Additional ecotax + * @param integer $id_image Image id + * @param string $reference Reference + * @param string $ean13 Ean-13 barcode + * @param int $default Default On + * @param string $upc Upc barcode + * @param string $minimal_quantity Minimal quantity + * @return array Update result + */ + public function updateAttribute($id_product_attribute, $wholesale_price, $price, $weight, $unit, $ecotax, + $id_images, $reference, $ean13, $default, $location = null, $upc = null, $minimal_quantity = null, $available_date = null, $update_all_fields = true, array $id_shop_list = array()) + { + $combination = new Combination($id_product_attribute); + + if (!$update_all_fields) + $combination->setFieldsToUpdate(array( + 'price' => !is_null($price), + 'wholesale_price' => !is_null($wholesale_price), + 'ecotax' => !is_null($ecotax), + 'weight' => !is_null($weight), + 'unit_price_impact' => !is_null($unit), + 'default_on' => !is_null($ecotax), + 'minimal_quantity' => !is_null($minimal_quantity), + 'available_date' => !is_null($available_date), + )); + + $price = str_replace(',', '.', $price); + $weight = str_replace(',', '.', $weight); + + $combination->price = (float)$price; + $combination->wholesale_price = (float)$wholesale_price; + $combination->ecotax = (float)$ecotax; + $combination->weight = (float)$weight; + $combination->unit_price_impact = (float)$unit; + $combination->reference = pSQL($reference); + $combination->location = pSQL($location); + $combination->ean13 = pSQL($ean13); + $combination->upc = pSQL($upc); + $combination->default_on = (int)$default; + $combination->minimal_quantity = (int)$minimal_quantity; + $combination->available_date = $available_date ? pSQL($available_date) : '0000-00-00'; + + if (count($id_shop_list)) + $combination->id_shop_list = $id_shop_list; + + $combination->save(); + + if (!empty($id_images)) + $combination->setImages($id_images); + + Product::updateDefaultAttribute($this->id); + + Hook::exec('actionProductAttributeUpdate', array('id_product_attribute' => $id_product_attribute)); + + return true; + } + + /** + * Add a product attribute + * @since 1.5.0.1 + * + * @param float $price Additional price + * @param float $weight Additional weight + * @param float $ecotax Additional ecotax + * @param integer $id_images Image ids + * @param string $reference Reference + * @param string $location Location + * @param string $ean13 Ean-13 barcode + * @param boolean $default Is default attribute for product + * @param integer $minimal_quantity Minimal quantity to add to cart + * @return mixed $id_product_attribute or false + */ + public function addAttribute($price, $weight, $unit_impact, $ecotax, $id_images, $reference, $ean13, + $default, $location = null, $upc = null, $minimal_quantity = 1, array $id_shop_list = array()) + { + if (!$this->id) + return; + + $price = str_replace(',', '.', $price); + $weight = str_replace(',', '.', $weight); + + $combination = new Combination(); + $combination->id_product = (int)$this->id; + $combination->price = (float)$price; + $combination->ecotax = (float)$ecotax; + $combination->quantity = 0; + $combination->weight = (float)$weight; + $combination->unit_price_impact = (float)$unit_impact; + $combination->reference = pSQL($reference); + $combination->location = pSQL($location); + $combination->ean13 = pSQL($ean13); + $combination->upc = pSQL($upc); + $combination->default_on = (int)$default; + $combination->minimal_quantity = (int)$minimal_quantity; + + // if we add a combination for this shop and this product does not use the combination feature in other shop, + // we clone the default combination in every shop linked to this product + if ($default && !$this->hasAttributesInOtherShops()) + { + $id_shop_list_array = Product::getShopsByProduct($this->id); + foreach ($id_shop_list_array as $array_shop) + $id_shop_list[] = $array_shop['id_shop']; + } + + if (count($id_shop_list)) + $combination->id_shop_list = $id_shop_list; + + $combination->add(); + + if (!$combination->id) + return false; + + Product::updateDefaultAttribute($this->id); + + if (!empty($id_images)) + $combination->setImages($id_images); + + return (int)$combination->id; + } + + + /** + * @deprecated since 1.5.0 + */ + public function updateQuantityProductWithAttributeQuantity() + { + Tools::displayAsDeprecated(); + + return Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'product` + SET `quantity` = IFNULL( + ( + SELECT SUM(`quantity`) + FROM `'._DB_PREFIX_.'product_attribute` + WHERE `id_product` = '.(int)$this->id.' + ), \'0\') + WHERE `id_product` = '.(int)$this->id); + } + /** + * Delete product attributes + * + * @return array Deletion result + */ + public function deleteProductAttributes() + { + Hook::exec('actionProductAttributeDelete', array('id_product_attribute' => 0, 'id_product' => $this->id, 'deleteAllAttributes' => true)); + + $result = true; + $combinations = new Collection('Combination'); + $combinations->where('id_product', '=', $this->id); + foreach ($combinations as $combination) + $result &= $combination->delete(); + SpecificPriceRule::applyAllRules(array((int)$this->id)); + return $result; + } + + /** + * Delete product attributes impacts + * + * @return Deletion result + */ + public function deleteAttributesImpacts() + { + return Db::getInstance()->execute( + 'DELETE FROM `'._DB_PREFIX_.'attribute_impact` + WHERE `id_product` = '.(int)$this->id + ); + } + + /** + * Delete product features + * + * @return array Deletion result + */ + public function deleteProductFeatures() + { + SpecificPriceRule::applyAllRules(array((int)$this->id)); + return $this->deleteFeatures(); + } + + /** + * Delete product attachments + * + * @return array Deletion result + */ + public function deleteAttachments() + { + return Db::getInstance()->execute(' + DELETE FROM `'._DB_PREFIX_.'product_attachment` + WHERE `id_product` = '.(int)$this->id + ); + } + + /** + * Delete product customizations + * + * @return array Deletion result + */ + public function deleteCustomization() + { + return ( + Db::getInstance()->execute( + 'DELETE FROM `'._DB_PREFIX_.'customization_field` + WHERE `id_product` = '.(int)$this->id + ) + && + Db::getInstance()->execute( + 'DELETE FROM `'._DB_PREFIX_.'customization_field_lang` + WHERE `id_customization_field` NOT IN (SELECT id_customization_field + FROM `'._DB_PREFIX_.'customization_field`)' + ) + ); + } + + /** + * Delete product pack details + * + * @return array Deletion result + */ + public function deletePack() + { + return Db::getInstance()->execute( + 'DELETE FROM `'._DB_PREFIX_.'pack` + WHERE `id_product_pack` = '.(int)$this->id.' + OR `id_product_item` = '.(int)$this->id + ); + } + + /** + * Delete product sales + * + * @return array Deletion result + */ + public function deleteProductSale() + { + return Db::getInstance()->execute( + 'DELETE FROM `'._DB_PREFIX_.'product_sale` + WHERE `id_product` = '.(int)$this->id + ); + } + + /** + * Delete product in its scenes + * + * @return array Deletion result + */ + public function deleteSceneProducts() + { + return Db::getInstance()->execute( + 'DELETE FROM `'._DB_PREFIX_.'scene_products` + WHERE `id_product` = '.(int)$this->id + ); + } + + /** + * Delete product indexed words + * + * @return array Deletion result + */ + public function deleteSearchIndexes() + { + return ( + Db::getInstance()->execute( + 'DELETE FROM `'._DB_PREFIX_.'search_index` + WHERE `id_product` = '.(int)$this->id + ) + && + Db::getInstance()->execute( + 'DELETE FROM `'._DB_PREFIX_.'search_word` + WHERE `id_word` NOT IN ( + SELECT id_word + FROM `'._DB_PREFIX_.'search_index` + )' + ) + ); + } + + /** + * Add a product attributes combinaison + * + * @param integer $id_product_attribute Product attribute id + * @param array $attributes Attributes to forge combinaison + * @return array Insertion result + * @deprecated since 1.5.0.7 + */ + public function addAttributeCombinaison($id_product_attribute, $attributes) + { + Tools::displayAsDeprecated(); + if (!is_array($attributes)) + die(Tools::displayError()); + if (!count($attributes)) + return false; + + $combination = new Combination((int)$id_product_attribute); + return $combination->setAttributes($attributes); + } + + public function addAttributeCombinationMultiple($id_attributes, $combinations) + { + Tools::displayAsDeprecated(); + $attributes_list = array(); + foreach ($id_attributes as $nb => $id_product_attribute) + if (isset($combinations[$nb])) + foreach ($combinations[$nb] as $id_attribute) + $attributes_list[] = array( + 'id_product_attribute' => (int)$id_product_attribute, + 'id_attribute' => (int)$id_attribute, + ); + + return Db::getInstance()->insert('product_attribute_combination', $attributes_list); + } + + + /** + * Delete a product attributes combination + * + * @param integer $id_product_attribute Product attribute id + * @return array Deletion result + */ + public function deleteAttributeCombination($id_product_attribute) + { + if (!$this->id || !$id_product_attribute || !is_numeric($id_product_attribute)) + return false; + + Hook::exec( + 'deleteProductAttribute', + array( + 'id_product_attribute' => $id_product_attribute, + 'id_product' => $this->id, + 'deleteAllAttributes' => false + ) + ); + + $combination = new Combination($id_product_attribute); + $res = $combination->delete(); + SpecificPriceRule::applyAllRules(array((int)$this->id)); + return $res; + } + + /** + * Delete features + * + */ + public function deleteFeatures() + { + // List products features + $features = Db::getInstance()->executeS(' + SELECT p.*, f.* + FROM `'._DB_PREFIX_.'feature_product` as p + LEFT JOIN `'._DB_PREFIX_.'feature_value` as f ON (f.`id_feature_value` = p.`id_feature_value`) + WHERE `id_product` = '.(int)$this->id); + foreach ($features as $tab) + // Delete product custom features + if ($tab['custom']) + { + Db::getInstance()->execute(' + DELETE FROM `'._DB_PREFIX_.'feature_value` + WHERE `id_feature_value` = '.(int)$tab['id_feature_value']); + Db::getInstance()->execute(' + DELETE FROM `'._DB_PREFIX_.'feature_value_lang` + WHERE `id_feature_value` = '.(int)$tab['id_feature_value']); + } + // Delete product features + $result = Db::getInstance()->execute(' + DELETE FROM `'._DB_PREFIX_.'feature_product` + WHERE `id_product` = '.(int)$this->id); + + SpecificPriceRule::applyAllRules(array((int)$this->id)); + return ($result); + } + + /** + * Get all available product attributes resume + * + * @param integer $id_lang Language id + * @return array Product attributes combinations + */ + public function getAttributesResume($id_lang, $attribute_value_separator = ' - ', $attribute_separator = ', ') + { + if (!Combination::isFeatureActive()) + return array(); + $add_shop = ''; + + $combinations = Db::getInstance()->executeS('SELECT pa.*, product_attribute_shop.* + FROM `'._DB_PREFIX_.'product_attribute` pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + WHERE pa.`id_product` = '.(int)$this->id.' + GROUP BY pa.`id_product_attribute`'); + + $product_attributes = array(); + foreach ($combinations as $combination) + $product_attributes[] = (int)$combination['id_product_attribute']; + + $lang = Db::getInstance()->executeS('SELECT pac.id_product_attribute, GROUP_CONCAT(agl.`name`, \''.pSQL($attribute_value_separator).'\',al.`name` ORDER BY agl.`id_attribute_group` SEPARATOR \''.pSQL($attribute_separator).'\') as attribute_designation + 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(',', $product_attributes).') + GROUP BY pac.id_product_attribute'); + + foreach ($lang as $k => $row) + $combinations[$k]['attribute_designation'] = $row['attribute_designation']; + + + //Get quantity of each variations + foreach ($combinations as $key => $row) + { + $cache_key = $row['id_product'].'_'.$row['id_product_attribute'].'_quantity'; + + if (!Cache::isStored($cache_key)) + Cache::store( + $cache_key, + StockAvailable::getQuantityAvailableByProduct($row['id_product'], $row['id_product_attribute']) + ); + + $combinations[$key]['quantity'] = Cache::retrieve($cache_key); + } + + return $combinations; + } + + /** + * Get all available product attributes combinations + * + * @param integer $id_lang Language id + * @return array Product attributes combinations + */ + public function getAttributeCombinations($id_lang) + { + if (!Combination::isFeatureActive()) + return array(); + + $sql = 'SELECT pa.*, product_attribute_shop.*, ag.`id_attribute_group`, ag.`is_color_group`, agl.`name` AS group_name, al.`name` AS attribute_name, + a.`id_attribute`, pa.`unit_price_impact` + FROM `'._DB_PREFIX_.'product_attribute` pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac ON pac.`id_product_attribute` = pa.`id_product_attribute` + 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 pa.`id_product` = '.(int)$this->id.' + GROUP BY pa.`id_product_attribute`, ag.`id_attribute_group` + ORDER BY pa.`id_product_attribute`'; + + $res = Db::getInstance()->executeS($sql); + + //Get quantity of each variations + foreach ($res as $key => $row) + { + $cache_key = $row['id_product'].'_'.$row['id_product_attribute'].'_quantity'; + + if (!Cache::isStored($cache_key)) + Cache::store( + $cache_key, + StockAvailable::getQuantityAvailableByProduct($row['id_product'], $row['id_product_attribute']) + ); + + $res[$key]['quantity'] = Cache::retrieve($cache_key); + } + + return $res; + } + + /** + * Get product attribute combination by id_product_attribute + * + * @param integer $id_product_attribute + * @param integer $id_lang Language id + * @return array Product attribute combination by id_product_attribute + */ + public function getAttributeCombinationsById($id_product_attribute, $id_lang) + { + if (!Combination::isFeatureActive()) + return array(); + $sql = 'SELECT pa.*, product_attribute_shop.*, ag.`id_attribute_group`, ag.`is_color_group`, agl.`name` AS group_name, al.`name` AS attribute_name, + a.`id_attribute`, pa.`unit_price_impact` + FROM `'._DB_PREFIX_.'product_attribute` pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac ON pac.`id_product_attribute` = pa.`id_product_attribute` + 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 pa.`id_product` = '.(int)$this->id.' + AND pa.`id_product_attribute` = '.(int)$id_product_attribute.' + GROUP BY pa.`id_product_attribute`, ag.`id_attribute_group` + ORDER BY pa.`id_product_attribute`'; + + $res = Db::getInstance()->executeS($sql); + + //Get quantity of each variations + foreach ($res as $key => $row) + { + $cache_key = $row['id_product'].'_'.$row['id_product_attribute'].'_quantity'; + + if (!Cache::isStored($cache_key)) + Cache::store( + $cache_key, + StockAvailable::getQuantityAvailableByProduct($row['id_product'], $row['id_product_attribute']) + ); + + $res[$key]['quantity'] = Cache::retrieve($cache_key); + } + + return $res; + } + + public function getCombinationImages($id_lang) + { + if (!Combination::isFeatureActive()) + return false; + + $product_attributes = Db::getInstance()->executeS( + 'SELECT `id_product_attribute` + FROM `'._DB_PREFIX_.'product_attribute` + WHERE `id_product` = '.(int)$this->id + ); + + if (!$product_attributes) + return false; + + $ids = array(); + + foreach ($product_attributes as $product_attribute) + $ids[] = (int)$product_attribute['id_product_attribute']; + + $result = Db::getInstance()->executeS(' + SELECT pai.`id_image`, pai.`id_product_attribute`, il.`legend` + FROM `'._DB_PREFIX_.'product_attribute_image` pai + LEFT JOIN `'._DB_PREFIX_.'image_lang` il ON (il.`id_image` = pai.`id_image`) + LEFT JOIN `'._DB_PREFIX_.'image` i ON (i.`id_image` = pai.`id_image`) + WHERE pai.`id_product_attribute` IN ('.implode(', ', $ids).') AND il.`id_lang` = '.(int)$id_lang.' ORDER by i.`position`' + ); + + if (!$result) + return false; + + $images = array(); + + foreach ($result as $row) + $images[$row['id_product_attribute']][] = $row; + + return $images; + } + + /** + * Check if product has attributes combinations + * + * @return integer Attributes combinations number + */ + public function hasAttributes() + { + if (!Combination::isFeatureActive()) + return 0; + return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(' + SELECT COUNT(*) + FROM `'._DB_PREFIX_.'product_attribute` pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + WHERE pa.`id_product` = '.(int)$this->id + ); + } + + /** + * Get new products + * + * @param integer $id_lang Language id + * @param integer $pageNumber Start from (optional) + * @param integer $nbProducts Number of products to return (optional) + * @return array New products + */ + public static function getNewProducts($id_lang, $page_number = 0, $nb_products = 10, + $count = false, $order_by = null, $order_way = null, Context $context = null) + { + if (!$context) + $context = Context::getContext(); + + $front = true; + if (!in_array($context->controller->controller_type, array('front', 'modulefront'))) + $front = false; + + if ($page_number < 0) $page_number = 0; + if ($nb_products < 1) $nb_products = 10; + if (empty($order_by) || $order_by == 'position') $order_by = 'date_add'; + if (empty($order_way)) $order_way = 'DESC'; + if ($order_by == 'id_product' || $order_by == 'price' || $order_by == 'date_add') + $order_by_prefix = 'p'; + else if ($order_by == 'name') + $order_by_prefix = 'pl'; + if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay($order_way)) + die(Tools::displayError()); + + $groups = FrontController::getCurrentCustomerGroups(); + $sql_groups = (count($groups) ? 'IN ('.implode(',', $groups).')' : '= 1'); + if (strpos($order_by, '.') > 0) + { + $order_by = explode('.', $order_by); + $order_by_prefix = $order_by[0]; + $order_by = $order_by[1]; + } + if ($count) + { + $sql = 'SELECT COUNT(p.`id_product`) AS nb + FROM `'._DB_PREFIX_.'product` p + '.Shop::addSqlAssociation('product', 'p').' + WHERE product_shop.`active` = 1 + AND DATEDIFF( + product_shop.`date_add`, + DATE_SUB( + NOW(), + INTERVAL '.(Validate::isUnsignedInt(Configuration::get('PS_NB_DAYS_NEW_PRODUCT')) ? Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20).' DAY + ) + ) > 0 + '.($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '').' + AND p.`id_product` IN ( + SELECT cp.`id_product` + FROM `'._DB_PREFIX_.'category_group` cg + LEFT JOIN `'._DB_PREFIX_.'category_product` cp ON (cp.`id_category` = cg.`id_category`) + WHERE cg.`id_group` '.$sql_groups.' + )'; + return (int)Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql); + } + + $sql = new DbQuery(); + $sql->select( + 'p.*, product_shop.*, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`link_rewrite`, pl.`meta_description`, + pl.`meta_keywords`, pl.`meta_title`, pl.`name`, i.`id_image`, il.`legend`, t.`rate`, m.`name` AS manufacturer_name, + DATEDIFF( + product_shop.`date_add`, + DATE_SUB( + NOW(), + INTERVAL '.(Validate::isUnsignedInt(Configuration::get('PS_NB_DAYS_NEW_PRODUCT')) ? Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20).' DAY + ) + ) > 0 AS new, + (product_shop.`price` * ((100 + (t.`rate`))/100)) AS orderprice' + ); + + $sql->from('product', 'p'); + $sql->join(Shop::addSqlAssociation('product', 'p')); + $sql->leftJoin('product_lang', 'pl', ' + p.`id_product` = pl.`id_product` + AND pl.`id_lang` = '.(int)$id_lang.Shop::addSqlRestrictionOnLang('pl') + ); + $sql->leftJoin('image', 'i', 'i.`id_product` = p.`id_product` AND i.`cover` = 1'); + $sql->leftJoin('image_lang', 'il', 'i.`id_image` = il.`id_image` AND il.`id_lang` = '.(int)$id_lang); + $sql->leftJoin('tax_rule', 'tr', ' + product_shop.`id_tax_rules_group` = tr.`id_tax_rules_group` + AND tr.`id_country` = '.(int)$context->country->id.' + AND tr.`id_state` = 0' + ); + $sql->leftJoin('tax', 't', 't.`id_tax` = tr.`id_tax`'); + $sql->leftJoin('manufacturer', 'm', 'm.`id_manufacturer` = p.`id_manufacturer`'); + + $sql->where('product_shop.`active` = 1'); + if ($front) + $sql->where('product_shop.`visibility` IN ("both", "catalog")'); + $sql->where(' + DATEDIFF( + product_shop.`date_add`, + DATE_SUB( + NOW(), + INTERVAL '.(Validate::isUnsignedInt(Configuration::get('PS_NB_DAYS_NEW_PRODUCT')) ? Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20).' DAY + ) + ) > 0' + ); + + $sql->where('p.`id_product` IN ( + SELECT cp.`id_product` + FROM `'._DB_PREFIX_.'category_group` cg + LEFT JOIN `'._DB_PREFIX_.'category_product` cp ON (cp.`id_category` = cg.`id_category`) + WHERE cg.`id_group` '.$sql_groups.')' + ); + + $sql->orderBy((isset($order_by_prefix) ? pSQL($order_by_prefix).'.' : '').'`'.pSQL($order_by).'` '.pSQL($order_way)); + $sql->limit($nb_products, $page_number * $nb_products); + + if (Combination::isFeatureActive()) + { + $sql->select('pa.id_product_attribute'); + $sql->leftOuterJoin('product_attribute', 'pa', 'p.`id_product` = pa.`id_product`'); + $sql->join(Shop::addSqlAssociation('product_attribute', 'pa', false, 'product_attribute_shop.default_on = 1')); + $sql->where('(pa.id_product_attribute IS NULL OR product_attribute_shop.id_product_attribute IS NOT NULL)'); + } + $sql->join(Product::sqlStock('p', Combination::isFeatureActive() ? 'product_attribute_shop' : 0)); + + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); + + if ($order_by == 'price') + Tools::orderbyPrice($result, $order_way); + if (!$result) + return false; + + $products_ids = array(); + foreach ($result as $row) + $products_ids[] = $row['id_product']; + // Thus you can avoid one query per product, because there will be only one query for all the products of the cart + Product::cacheFrontFeatures($products_ids, $id_lang); + + return Product::getProductsProperties((int)$id_lang, $result); + } + + protected static function _getProductIdByDate($beginning, $ending, Context $context = null, $with_combination = false) + { + if (!$context) + $context = Context::getContext(); + + $id_address = $context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}; + $ids = Address::getCountryAndState($id_address); + $id_country = (int)($ids['id_country'] ? $ids['id_country'] : Configuration::get('PS_COUNTRY_DEFAULT')); + + return SpecificPrice::getProductIdByDate( + $context->shop->id, + $context->currency->id, + $id_country, + $context->customer->id_default_group, + $beginning, + $ending, + 0, + $with_combination + ); + } + + /** + * Get a random special + * + * @param integer $id_lang Language id + * @return array Special + */ + public static function getRandomSpecial($id_lang, $beginning = false, $ending = false, Context $context = null) + { + if (!$context) + $context = Context::getContext(); + + $front = true; + if (!in_array($context->controller->controller_type, array('front', 'modulefront'))) + $front = false; + + $current_date = date('Y-m-d H:i:s'); + $product_reductions = Product::_getProductIdByDate((!$beginning ? $current_date : $beginning), (!$ending ? $current_date : $ending), $context, true); + + if ($product_reductions) + { + $ids_product = ' AND ('; + foreach ($product_reductions as $product_reduction) + $ids_product .= '( p.`id_product` = '.(int)$product_reduction['id_product'].($product_reduction['id_product_attribute'] ? ' AND pa.`id_product_attribute`='.(int)$product_reduction['id_product_attribute'] :'').') OR'; + $ids_product = rtrim($ids_product, 'OR').')'; + + $groups = FrontController::getCurrentCustomerGroups(); + $sql_groups = (count($groups) ? 'IN ('.implode(',', $groups).')' : '= 1'); + + // Please keep 2 distinct queries because RAND() is an awful way to achieve this result + $sql = 'SELECT p.id_product, pa.id_product_attribute + FROM `'._DB_PREFIX_.'product` p + '.Shop::addSqlAssociation('product', 'p').' + LEFT JOIN `'._DB_PREFIX_.'product_attribute` pa ON (p.id_product = pa.id_product) + '.Shop::addSqlAssociation('product_attribute', 'pa', false, 'product_attribute_shop.default_on = 1').' + WHERE product_shop.`active` = 1 + '.(($ids_product) ? $ids_product : '').' + AND p.`id_product` IN ( + SELECT cp.`id_product` + FROM `'._DB_PREFIX_.'category_group` cg + LEFT JOIN `'._DB_PREFIX_.'category_product` cp ON (cp.`id_category` = cg.`id_category`) + WHERE cg.`id_group` '.$sql_groups.' + ) + GROUP BY p.id_product + ORDER BY RAND()'; + + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql); + + if (!$id_product = $result['id_product']) + return false; + + $sql = 'SELECT p.*, product_shop.*, stock.`out_of_stock` out_of_stock, pl.`description`, pl.`description_short`, + pl.`link_rewrite`, pl.`meta_description`, pl.`meta_keywords`, pl.`meta_title`, pl.`name`, + p.`ean13`, p.`upc`, i.`id_image`, il.`legend`, t.`rate` + FROM `'._DB_PREFIX_.'product` p + LEFT JOIN `'._DB_PREFIX_.'product_lang` pl ON ( + p.`id_product` = pl.`id_product` + AND pl.`id_lang` = '.(int)$id_lang.Shop::addSqlRestrictionOnLang('pl').' + ) + '.Shop::addSqlAssociation('product', 'p').' + LEFT JOIN `'._DB_PREFIX_.'image` i ON (i.`id_product` = p.`id_product` AND i.`cover` = 1) + LEFT JOIN `'._DB_PREFIX_.'image_lang` il ON (i.`id_image` = il.`id_image` AND il.`id_lang` = '.(int)$id_lang.') + LEFT JOIN `'._DB_PREFIX_.'tax_rule` tr ON (product_shop.`id_tax_rules_group` = tr.`id_tax_rules_group` + AND tr.`id_country` = '.(int)Context::getContext()->country->id.' + AND tr.`id_state` = 0) + LEFT JOIN `'._DB_PREFIX_.'tax` t ON (t.`id_tax` = tr.`id_tax`) + '.Product::sqlStock('p', 0).' + WHERE p.id_product = '.(int)$id_product.' + '.($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : ''); + $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql); + if ($result['id_product_attribute']) + $row['id_product_attribute'] = $result['id_product_attribute']; + return Product::getProductProperties($id_lang, $row); + } + else + return false; + } + + /** + * Get prices drop + * + * @param integer $id_lang Language id + * @param integer $pageNumber Start from (optional) + * @param integer $nbProducts Number of products to return (optional) + * @param boolean $count Only in order to get total number (optional) + * @return array Prices drop + */ + public static function getPricesDrop($id_lang, $page_number = 0, $nb_products = 10, $count = false, + $order_by = null, $order_way = null, $beginning = false, $ending = false, Context $context = null) + { + if (!Validate::isBool($count)) + die(Tools::displayError()); + + if (!$context) $context = Context::getContext(); + if ($page_number < 0) $page_number = 0; + if ($nb_products < 1) $nb_products = 10; + if (empty($order_by) || $order_by == 'position') $order_by = 'price'; + if (empty($order_way)) $order_way = 'DESC'; + if ($order_by == 'id_product' || $order_by == 'price' || $order_by == 'date_add') + $order_by_prefix = 'p'; + else if ($order_by == 'name') + $order_by_prefix = 'pl'; + if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay($order_way)) + die (Tools::displayError()); + $current_date = date('Y-m-d H:i:s'); + $ids_product = Product::_getProductIdByDate((!$beginning ? $current_date : $beginning), (!$ending ? $current_date : $ending), $context); + + $tab_id_product = array(); + foreach ($ids_product as $product) + if (is_array($product)) + $tab_id_product[] = (int)$product['id_product']; + else + $tab_id_product[] = (int)$product; + + $front = true; + if (!in_array($context->controller->controller_type, array('front', 'modulefront'))) + $front = false; + + $groups = FrontController::getCurrentCustomerGroups(); + $sql_groups = (count($groups) ? 'IN ('.implode(',', $groups).')' : '= 1'); + + if ($count) + { + $sql = 'SELECT COUNT(DISTINCT p.`id_product`) AS nb + FROM `'._DB_PREFIX_.'product` p + '.Shop::addSqlAssociation('product', 'p').' + WHERE product_shop.`active` = 1 + AND product_shop.`show_price` = 1 + '.($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '').' + '.((!$beginning && !$ending) ? 'AND p.`id_product` IN('.((is_array($tab_id_product) && count($tab_id_product)) ? implode(', ', $tab_id_product) : 0).')' : '').' + AND p.`id_product` IN ( + SELECT cp.`id_product` + FROM `'._DB_PREFIX_.'category_group` cg + LEFT JOIN `'._DB_PREFIX_.'category_product` cp ON (cp.`id_category` = cg.`id_category`) + WHERE cg.`id_group` '.$sql_groups.' + )'; + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql); + return (int)$result['nb']; + } + if (strpos($order_by, '.') > 0) + { + $order_by = explode('.', $order_by); + $order_by = pSQL($order_by[0]).'.`'.pSQL($order_by[1]).'`'; + } + $sql = 'SELECT p.*, product_shop.*, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity, pl.`description`, pl.`description_short`, + pl.`link_rewrite`, pl.`meta_description`, pl.`meta_keywords`, pl.`meta_title`, + pl.`name`, i.`id_image`, il.`legend`, t.`rate`, m.`name` AS manufacturer_name, + DATEDIFF( + p.`date_add`, + DATE_SUB( + NOW(), + INTERVAL '.(Validate::isUnsignedInt(Configuration::get('PS_NB_DAYS_NEW_PRODUCT')) ? Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20).' DAY + ) + ) > 0 AS new + FROM `'._DB_PREFIX_.'product` p + '.Shop::addSqlAssociation('product', 'p').' + '.Product::sqlStock('p', 0, false, $context->shop).' + LEFT JOIN `'._DB_PREFIX_.'product_lang` pl ON ( + p.`id_product` = pl.`id_product` + AND pl.`id_lang` = '.(int)$id_lang.Shop::addSqlRestrictionOnLang('pl').' + ) + LEFT JOIN `'._DB_PREFIX_.'image` i ON (i.`id_product` = p.`id_product` AND i.`cover` = 1) + LEFT JOIN `'._DB_PREFIX_.'image_lang` il ON (i.`id_image` = il.`id_image` AND il.`id_lang` = '.(int)$id_lang.') + LEFT JOIN `'._DB_PREFIX_.'tax_rule` tr ON (product_shop.`id_tax_rules_group` = tr.`id_tax_rules_group` + AND tr.`id_country` = '.(int)Context::getContext()->country->id.' + AND tr.`id_state` = 0) + LEFT JOIN `'._DB_PREFIX_.'tax` t ON (t.`id_tax` = tr.`id_tax`) + LEFT JOIN `'._DB_PREFIX_.'manufacturer` m ON (m.`id_manufacturer` = p.`id_manufacturer`) + WHERE product_shop.`active` = 1 + AND product_shop.`show_price` = 1 + '.($front ? ' AND p.`visibility` IN ("both", "catalog")' : '').' + '.((!$beginning && !$ending) ? ' AND p.`id_product` IN ('.((is_array($tab_id_product) && count($tab_id_product)) ? implode(', ', $tab_id_product) : 0).')' : '').' + AND p.`id_product` IN ( + SELECT cp.`id_product` + FROM `'._DB_PREFIX_.'category_group` cg + LEFT JOIN `'._DB_PREFIX_.'category_product` cp ON (cp.`id_category` = cg.`id_category`) + WHERE cg.`id_group` '.$sql_groups.' + ) + ORDER BY '.(isset($order_by_prefix) ? pSQL($order_by_prefix).'.' : '').pSQL($order_by).' '.pSQL($order_way).' + LIMIT '.(int)($page_number * $nb_products).', '.(int)$nb_products; + + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); + + if ($order_by == 'price') + Tools::orderbyPrice($result, $order_way); + + if (!$result) + return false; + + return Product::getProductsProperties($id_lang, $result); + } + + + /** + * getProductCategories return an array of categories which this product belongs to + * + * @return array of categories + */ + public static function getProductCategories($id_product = '') + { + $ret = array(); + + $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' + SELECT `id_category` FROM `'._DB_PREFIX_.'category_product` + WHERE `id_product` = '.(int)$id_product + ); + + if ($row) + foreach ($row as $val) + $ret[] = $val['id_category']; + + return $ret; + } + + public static function getProductCategoriesFull($id_product = '', $id_lang = null) + { + if (!$id_lang) + $id_lang = Context::getContext()->language->id; + + $ret = array(); + $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' + SELECT cp.`id_category`, cl.`name`, cl.`link_rewrite` FROM `'._DB_PREFIX_.'category_product` cp + LEFT JOIN `'._DB_PREFIX_.'category` c ON (c.id_category = cp.id_category) + LEFT JOIN `'._DB_PREFIX_.'category_lang` cl ON (cp.`id_category` = cl.`id_category`'.Shop::addSqlRestrictionOnLang('cl').') + '.Shop::addSqlAssociation('category', 'c').' + WHERE cp.`id_product` = '.(int)$id_product.' + AND cl.`id_lang` = '.(int)$id_lang + ); + + foreach ($row as $val) + $ret[$val['id_category']] = $val; + + return $ret; + } + + /** + * getCategories return an array of categories which this product belongs to + * + * @return array of categories + */ + public function getCategories() + { + return Product::getProductCategories($this->id); + } + + /** + * Gets carriers assigned to the product + */ + public function getCarriers() + { + return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' + SELECT c.* + FROM `'._DB_PREFIX_.'product_carrier` pc + INNER JOIN `'._DB_PREFIX_.'carrier` c + ON (c.`id_reference` = pc.`id_carrier_reference` AND c.`deleted` = 0) + WHERE pc.`id_product` = '.(int)$this->id.' + AND pc.`id_shop` = '.(int)$this->id_shop); + } + + /** + * Sets carriers assigned to the product + */ + public function setCarriers($carrier_list) + { + $data = array(); + + foreach ($carrier_list as $carrier) + { + $data[] = array( + 'id_product' => (int)$this->id, + 'id_carrier_reference' => (int)$carrier, + 'id_shop' => (int)$this->id_shop + ); + } + Db::getInstance()->execute( + 'DELETE FROM `'._DB_PREFIX_.'product_carrier` + WHERE id_product = '.(int)$this->id.' + AND id_shop = '.(int)$this->id_shop + ); + if (count($data)) + Db::getInstance()->insert('product_carrier', $data); + } + + /** + * Get product images and legends + * + * @param integer $id_lang Language id for multilingual legends + * @return array Product images and legends + */ + public function getImages($id_lang, Context $context = null) + { + if (!$context) + $context = Context::getContext(); + + $sql = 'SELECT i.`cover`, i.`id_image`, il.`legend`, i.`position` + FROM `'._DB_PREFIX_.'image` i + '.Shop::addSqlAssociation('image', 'i').' + LEFT JOIN `'._DB_PREFIX_.'image_lang` il ON (i.`id_image` = il.`id_image` AND il.`id_lang` = '.(int)$id_lang.') + WHERE i.`id_product` = '.(int)$this->id.' + ORDER BY `position`'; + return Db::getInstance()->executeS($sql); + } + + /** + * Get product cover image + * + * @return array Product cover image + */ + public static function getCover($id_product, Context $context = null) + { + if (!$context) + $context = Context::getContext(); + + $sql = 'SELECT i.`id_image` + FROM `'._DB_PREFIX_.'image` i + '.Shop::addSqlAssociation('image', 'i').' + WHERE i.`id_product` = '.(int)$id_product.' + AND i.`cover` = 1'; + return Db::getInstance()->getRow($sql); + } + + /** + * Get product price + * + * @param integer $id_product Product id + * @param boolean $usetax With taxes or not (optional) + * @param integer $id_product_attribute Product attribute id (optional). + * If set to false, do not apply the combination price impact. NULL does apply the default combination price impact. + * @param integer $decimals Number of decimals (optional) + * @param integer $divisor Useful when paying many time without fees (optional) + * @param boolean $only_reduc Returns only the reduction amount + * @param boolean $usereduc Set if the returned amount will include reduction + * @param integer $quantity Required for quantity discount application (default value: 1) + * @param boolean $forceAssociatedTax DEPRECATED - NOT USED Force to apply the associated tax. Only works when the parameter $usetax is true + * @param integer $id_customer Customer ID (for customer group reduction) + * @param integer $id_cart Cart ID. Required when the cookie is not accessible (e.g., inside a payment module, a cron task...) + * @param integer $id_address Customer address ID. Required for price (tax included) calculation regarding the guest localization + * @param variable_reference $specificPriceOutput. + * If a specific price applies regarding the previous parameters, this variable is filled with the corresponding SpecificPrice object + * @param boolean $with_ecotax insert ecotax in price output. + * @return float Product price + */ + public static function getPriceStatic($id_product, $usetax = true, $id_product_attribute = null, $decimals = 6, $divisor = null, + $only_reduc = false, $usereduc = true, $quantity = 1, $force_associated_tax = false, $id_customer = null, $id_cart = null, + $id_address = null, &$specific_price_output = null, $with_ecotax = true, $use_group_reduction = true, Context $context = null, + $use_customer_price = true) + { + if (!$context) + $context = Context::getContext(); + + $cur_cart = $context->cart; + + if ($divisor !== null) + Tools::displayParameterAsDeprecated('divisor'); + + if (!Validate::isBool($usetax) || !Validate::isUnsignedId($id_product)) + die(Tools::displayError()); + // Initializations + $id_group = (isset($context->customer) ? $context->customer->id_default_group : _PS_DEFAULT_CUSTOMER_GROUP_); + + // If there is cart in context or if the specified id_cart is different from the context cart id + if (!is_object($cur_cart) || (Validate::isUnsignedInt($id_cart) && $id_cart && $cur_cart->id != $id_cart)) + { + /* + * When a user (e.g., guest, customer, Google...) is on PrestaShop, he has already its cart as the global (see /init.php) + * When a non-user calls directly this method (e.g., payment module...) is on PrestaShop, he does not have already it BUT knows the cart ID + * When called from the back office, cart ID can be inexistant + */ + if (!$id_cart && !isset($context->employee)) + die(Tools::displayError()); + $cur_cart = new Cart($id_cart); + // Store cart in context to avoid multiple instantiations in BO + if (!Validate::isLoadedObject($context->cart)) + $context->cart = $cur_cart; + } + + $cart_quantity = 0; + if ((int)$id_cart) + { + $condition = ''; + $cache_name = (int)$id_cart.'_'.(int)$id_product; + if (!isset(self::$_cart_quantity[$cache_name]) || self::$_cart_quantity[$cache_name] != (int)$quantity) + self::$_cart_quantity[$cache_name] = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(' + SELECT SUM(`quantity`) + FROM `'._DB_PREFIX_.'cart_product` + WHERE `id_product` = '.(int)$id_product.' + AND `id_cart` = '.(int)$id_cart); + $cart_quantity = self::$_cart_quantity[$cache_name]; + } + + $id_currency = (int)Validate::isLoadedObject($context->currency) ? $context->currency->id : Configuration::get('PS_CURRENCY_DEFAULT'); + + // retrieve address informations + $id_country = (int)$context->country->id; + $id_state = 0; + $zipcode = 0; + + if (!$id_address) + $id_address = $cur_cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}; + + if ($id_address) + { + $address_infos = Address::getCountryAndState($id_address); + if ($address_infos['id_country']) + { + $id_country = (int)$address_infos['id_country']; + $id_state = (int)$address_infos['id_state']; + $zipcode = $address_infos['postcode']; + } + } + else if (isset($context->customer->geoloc_id_country)) + { + $id_country = (int)$context->customer->geoloc_id_country; + $id_state = (int)$context->customer->id_state; + $zipcode = (int)$context->customer->postcode; + } + + if (Tax::excludeTaxeOption()) + $usetax = false; + + if ($usetax != false + && !empty($address_infos['vat_number']) + && $address_infos['id_country'] != Configuration::get('VATNUMBER_COUNTRY') + && Configuration::get('VATNUMBER_MANAGEMENT')) + $usetax = false; + + if (is_null($id_customer) && Validate::isLoadedObject($context->customer)) + $id_customer = $context->customer->id; + + return Product::priceCalculation( + $context->shop->id, + $id_product, + $id_product_attribute, + $id_country, + $id_state, + $zipcode, + $id_currency, + $id_group, + $cart_quantity, + $usetax, + $decimals, + $only_reduc, + $usereduc, + $with_ecotax, + $specific_price_output, + $use_group_reduction, + $id_customer, + $use_customer_price, + $id_cart, + $quantity + ); + } + + /** + * Price calculation / Get product price + * + * @param integer $id_shop Shop id + * @param integer $id_product Product id + * @param integer $id_product_attribute Product attribute id + * @param integer $id_country Country id + * @param integer $id_state State id + * @param integer $id_currency Currency id + * @param integer $id_group Group id + * @param integer $quantity Quantity Required for Specific prices : quantity discount application + * @param boolean $use_tax with (1) or without (0) tax + * @param integer $decimals Number of decimals returned + * @param boolean $only_reduc Returns only the reduction amount + * @param boolean $use_reduc Set if the returned amount will include reduction + * @param boolean $with_ecotax insert ecotax in price output. + * @param variable_reference $specific_price_output + * If a specific price applies regarding the previous parameters, this variable is filled with the corresponding SpecificPrice object + * @return float Product price + **/ + public static function priceCalculation($id_shop, $id_product, $id_product_attribute, $id_country, $id_state, $zipcode, $id_currency, + $id_group, $quantity, $use_tax, $decimals, $only_reduc, $use_reduc, $with_ecotax, &$specific_price, $use_group_reduction, + $id_customer = 0, $use_customer_price = true, $id_cart = 0, $real_quantity = 0) + { + static $address = null; + + if ($address === null) + $address = new Address(); + + if (!$use_customer_price) + $id_customer = 0; + + $cache_id = $id_product.'-'.$id_shop.'-'.$id_currency.'-'.$id_country.'-'.$id_state.'-'.$zipcode.'-'.$id_group. + '-'.$quantity.'-'.(int)$id_product_attribute.'-'.($use_tax?'1':'0').'-'.$decimals.'-'.($only_reduc?'1':'0'). + '-'.($use_reduc?'1':'0').'-'.$with_ecotax.'-'.$id_customer; + + // reference parameter is filled before any returns + $specific_price = SpecificPrice::getSpecificPrice( + (int)$id_product, + $id_shop, + $id_currency, + $id_country, + $id_group, + $quantity, + $id_product_attribute, + $id_customer, + $id_cart, + $real_quantity + ); + + if (isset(self::$_prices[$cache_id])) + return self::$_prices[$cache_id]; + + // fetch price & attribute price + $cache_id_2 = $id_product; + if (!isset(self::$_pricesLevel2[$cache_id_2])) + { + $sql = new DbQuery(); + $sql->select('product_shop.`price`, product_shop.`ecotax`'); + $sql->from('product', 'p'); + $sql->join(Shop::addSqlAssociation('product', 'p')); + $sql->where('p.`id_product` = '.(int)$id_product); + if (Combination::isFeatureActive()) + { + $sql->select('product_attribute_shop.id_product_attribute, product_attribute_shop.`price` AS attribute_price, product_attribute_shop.default_on'); + $sql->leftJoin('product_attribute', 'pa', 'pa.`id_product` = p.`id_product`'); + $sql->join(Shop::addSqlAssociation('product_attribute', 'pa', false, 'product_attribute_shop.id_shop ='.(int)$id_shop)); + } + else + $sql->select('0 as id_product_attribute'); + + $res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); + foreach ($res as $row) + { + $array_tmp = array( + 'price' => $row['price'], + 'ecotax' => $row['ecotax'], + 'attribute_price' => (isset($row['attribute_price']) ? $row['attribute_price'] : null) + ); + self::$_pricesLevel2[$cache_id_2][(int)$row['id_product_attribute']] = $array_tmp; + + if (isset($row['default_on']) && $row['default_on'] == 1) + self::$_pricesLevel2[$cache_id_2][0] = $array_tmp; + } + } + if (!isset(self::$_pricesLevel2[$cache_id_2][(int)$id_product_attribute])) + return; + + $result = self::$_pricesLevel2[$cache_id_2][(int)$id_product_attribute]; + + if (!$specific_price || $specific_price['price'] < 0) + $price = (float)$result['price']; + else + $price = (float)$specific_price['price']; + // convert only if the specific price is in the default currency (id_currency = 0) + if (!$specific_price || !($specific_price['price'] >= 0 && $specific_price['id_currency'])) + $price = Tools::convertPrice($price, $id_currency); + + // Attribute price + if (is_array($result) && (!$specific_price || !$specific_price['id_product_attribute'] || $specific_price['price'] < 0)) + { + $attribute_price = Tools::convertPrice($result['attribute_price'] !== null ? (float)$result['attribute_price'] : 0, $id_currency); + if ($id_product_attribute !== false && !is_null($id_product_attribute)) // If you want the default combination, please use NULL value instead + $price += $attribute_price; + } + + // Tax + $address->id_country = $id_country; + $address->id_state = $id_state; + $address->postcode = $zipcode; + + $tax_manager = TaxManagerFactory::getManager($address, Product::getIdTaxRulesGroupByIdProduct((int)$id_product)); + $product_tax_calculator = $tax_manager->getTaxCalculator(); + + // Add Tax + if ($use_tax) + $price = $product_tax_calculator->addTaxes($price); + $price = Tools::ps_round($price, $decimals); + + // Reduction + $reduc = 0; + if (($only_reduc || $use_reduc) && $specific_price) + { + if ($specific_price['reduction_type'] == 'amount') + { + $reduction_amount = $specific_price['reduction']; + + if (!$specific_price['id_currency']) + $reduction_amount = Tools::convertPrice($reduction_amount, $id_currency); + $reduc = Tools::ps_round(!$use_tax ? $product_tax_calculator->removeTaxes($reduction_amount) : $reduction_amount, $decimals); + } + else + $reduc = Tools::ps_round($price * $specific_price['reduction'], $decimals); + } + + if ($only_reduc) + return $reduc; + if ($use_reduc) + $price -= $reduc; + + // Group reduction + if ($use_group_reduction) + { + if ($reduction_from_category = (float)GroupReduction::getValueForProduct($id_product, $id_group)) + $price -= $price * $reduction_from_category; + else // apply group reduction if there is no group reduction for this category + $price *= ((100 - Group::getReductionByIdGroup($id_group)) / 100); + } + + $price = Tools::ps_round($price, $decimals); + // Eco Tax + if (($result['ecotax'] || isset($result['attribute_ecotax'])) && $with_ecotax) + { + $ecotax = $result['ecotax']; + if (isset($result['attribute_ecotax']) && $result['attribute_ecotax'] > 0) + $ecotax = $result['attribute_ecotax']; + + if ($id_currency) + $ecotax = Tools::convertPrice($ecotax, $id_currency); + if ($use_tax) + { + // reinit the tax manager for ecotax handling + $tax_manager = TaxManagerFactory::getManager( + $address, + (int)Configuration::get('PS_ECOTAX_TAX_RULES_GROUP_ID') + ); + $ecotax_tax_calculator = $tax_manager->getTaxCalculator(); + $price += $ecotax_tax_calculator->addTaxes($ecotax); + } + else + $price += $ecotax; + } + $price = Tools::ps_round($price, $decimals); + if ($price < 0) + $price = 0; + + self::$_prices[$cache_id] = $price; + return self::$_prices[$cache_id]; + } + + public static function convertAndFormatPrice($price, $currency = false, Context $context = null) + { + if (!$context) + $context = Context::getContext(); + if (!$currency) + $currency = $context->currency; + return Tools::displayPrice(Tools::convertPrice($price, $currency), $currency); + } + + public static function isDiscounted($id_product, $quantity = 1, Context $context = null) + { + if (!$context) + $context = Context::getContext(); + + $id_group = $context->customer->id_default_group; + $cart_quantity = !$context->cart ? 0 : Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(' + SELECT SUM(`quantity`) + FROM `'._DB_PREFIX_.'cart_product` + WHERE `id_product` = '.(int)$id_product.' AND `id_cart` = '.(int)$context->cart->id + ); + $quantity = $cart_quantity ? $cart_quantity : $quantity; + $id_currency = (int)$context->currency->id; + $ids = Address::getCountryAndState((int)$context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}); + $id_country = (int)($ids['id_country'] ? $ids['id_country'] : Configuration::get('PS_COUNTRY_DEFAULT')); + + return (bool)SpecificPrice::getSpecificPrice((int)$id_product, $context->shop->id, $id_currency, $id_country, $id_group, $quantity); + } + + /** + * Get product price + * Same as static function getPriceStatic, no need to specify product id + * + * @param boolean $tax With taxes or not (optional) + * @param integer $id_product_attribute Product attribute id (optional) + * @param integer $decimals Number of decimals (optional) + * @param integer $divisor Util when paying many time without fees (optional) + * @return float Product price in euros + */ + public function getPrice($tax = true, $id_product_attribute = null, $decimals = 6, + $divisor = null, $only_reduc = false, $usereduc = true, $quantity = 1) + { + return Product::getPriceStatic((int)$this->id, $tax, $id_product_attribute, $decimals, $divisor, $only_reduc, $usereduc, $quantity); + } + + public function getPublicPrice($tax = true, $id_product_attribute = null, $decimals = 6, + $divisor = null, $only_reduc = false, $usereduc = true, $quantity = 1) + { + $specific_price_output = null; + return Product::getPriceStatic((int)$this->id, $tax, $id_product_attribute, $decimals, $divisor, $only_reduc, $usereduc, $quantity, + false, null, null, null, $specific_price_output, true, true, null, false); + } + + public function getIdProductAttributeMostExpensive() + { + if (!Combination::isFeatureActive()) + return 0; + + $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow(' + SELECT pa.`id_product_attribute` + FROM `'._DB_PREFIX_.'product_attribute` pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + WHERE pa.`id_product` = '.(int)$this->id.' + ORDER BY product_attribute_shop.`price` DESC'); + + return (isset($row['id_product_attribute']) && $row['id_product_attribute']) ? (int)$row['id_product_attribute'] : 0; + } + + public function getDefaultIdProductAttribute() + { + if (!Combination::isFeatureActive()) + return 0; + + $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow(' + SELECT pa.`id_product_attribute` + FROM `'._DB_PREFIX_.'product_attribute` pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + WHERE pa.`id_product` = '.(int)$this->id.' + AND product_attribute_shop.default_on = 1' + ); + + return (isset($row['id_product_attribute']) && $row['id_product_attribute']) ? (int)$row['id_product_attribute'] : 0; + } + + public function getPriceWithoutReduct($notax = false, $id_product_attribute = false) + { + return Product::getPriceStatic((int)$this->id, !$notax, $id_product_attribute, 6, null, false, false); + } + + /** + * Display price with right format and currency + * + * @param array $params Params + * @param $smarty Smarty object + * @return string Price with right format and currency + */ + public static function convertPrice($params, &$smarty) + { + return Tools::displayPrice($params['price'], Context::getContext()->currency); + } + + /** + * Convert price with currency + * + * @param array $params + * @param object $smarty DEPRECATED + * @return Ambigous > + */ + public static function convertPriceWithCurrency($params, &$smarty) + { + return Tools::displayPrice($params['price'], $params['currency'], false); + } + + public static function displayWtPrice($params, &$smarty) + { + return Tools::displayPrice($params['p'], Context::getContext()->currency); + } + + /** + * Display WT price with currency + * + * @param array $params + * @param object DEPRECATED $smarty + * @return Ambigous > + */ + public static function displayWtPriceWithCurrency($params, &$smarty) + { + return Tools::displayPrice($params['price'], $params['currency'], false); + } + + /** + * Get available product quantities + * + * @param integer $id_product Product id + * @param integer $id_product_attribute Product attribute id (optional) + * @return integer Available quantities + */ + public static function getQuantity($id_product, $id_product_attribute = null, $cache_is_pack = null) + { + $lang = Configuration::get('PS_LANG_DEFAULT'); + if ((int)$cache_is_pack || ($cache_is_pack === null && Pack::isPack((int)$id_product))) + { + if (!Pack::isInStock((int)$id_product)) + return 0; + } + + // @since 1.5.0 + return (StockAvailable::getQuantityAvailableByProduct($id_product, $id_product_attribute)); + } + + /** + * Create JOIN query with 'stock_available' table + * + * @param string $productAlias Alias of product table + * @param string|int $productAttribute If string : alias of PA table ; if int : value of PA ; if null : nothing about PA + * @param bool $innerJoin LEFT JOIN or INNER JOIN + * @param Shop $shop + * @return string + */ + public static function sqlStock($product_alias, $product_attribute = 0, $inner_join = false, Shop $shop = null) + { + $id_shop = ($shop !== null ? (int)$shop->id : null); + $sql = (($inner_join) ? ' INNER ' : ' LEFT ').' + JOIN '._DB_PREFIX_.'stock_available stock + ON (stock.id_product = '.pSQL($product_alias).'.id_product'; + + if (!is_null($product_attribute)) + { + if (!Combination::isFeatureActive()) + $sql .= ' AND stock.id_product_attribute = 0'; + elseif (is_numeric($product_attribute)) + $sql .= ' AND stock.id_product_attribute = '.$product_attribute; + elseif (is_string($product_attribute)) + $sql .= ' AND stock.id_product_attribute = IFNULL(`'.bqSQL($product_attribute).'`.id_product_attribute, 0)'; + } + + $sql .= StockAvailable::addSqlShopRestriction(null, $id_shop, 'stock').' )'; + + return $sql; + } + + /** + * @deprecated since 1.5.0 + * + * It's not possible to use this method with new stockManager and stockAvailable features + * Now this method do nothing + * + * @see StockManager if you want to manage real stock + * @see StockAvailable if you want to manage available quantities for sale on your shop(s) + * + * @param array $product Array with ordered product (quantity, id_product_attribute if applicable) + * @return mixed Query result + */ + public static function updateQuantity() + { + Tools::displayAsDeprecated(); + + return false; + } + + /** + * @deprecated since 1.5.0 + * + * It's not possible to use this method with new stockManager and stockAvailable features + * Now this method do nothing + * + * @see StockManager if you want to manage real stock + * @see StockAvailable if you want to manage available quantities for sale on your shop(s) + * + */ + public static function reinjectQuantities() + { + Tools::displayAsDeprecated(); + + return false; + } + + public static function isAvailableWhenOutOfStock($out_of_stock) + { + // @TODO 1.5.0 Update of STOCK_MANAGEMENT & ORDER_OUT_OF_STOCK + $return = (int)$out_of_stock == 2 ? (int)Configuration::get('PS_ORDER_OUT_OF_STOCK') : (int)$out_of_stock; + return !Configuration::get('PS_STOCK_MANAGEMENT') ? true : $return; + } + + /** + * Check product availability + * + * @param integer $qty Quantity desired + * @return boolean True if product is available with this quantity + */ + public function checkQty($qty) + { + if (Pack::isPack((int)$this->id) && !Pack::isInStock((int)$this->id)) + return false; + + if ($this->isAvailableWhenOutOfStock(StockAvailable::outOfStock($this->id))) + return true; + + if (isset($this->id_product_attribute)) + $id_product_attribute = $this->id_product_attribute; + else + $id_product_attribute = 0; + + return ($qty <= StockAvailable::getQuantityAvailableByProduct($this->id, $id_product_attribute)); + } + + /** + * Check if there is not a default attribute and create it not + */ + public function checkDefaultAttributes() + { + if (!$this->id) + return false; + + $row = Db::getInstance()->getRow(' + SELECT pa.id_product + FROM `'._DB_PREFIX_.'product_attribute` pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + WHERE product_attribute_shop.`default_on` = 1 + AND pa.`id_product` = '.(int)$this->id + ); + if ($row) + return true; + + $mini = Db::getInstance()->getRow(' + SELECT MIN(pa.id_product_attribute) as `id_attr` + FROM `'._DB_PREFIX_.'product_attribute` pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + WHERE `id_product` = '.(int)$this->id + ); + if (!$mini) + return false; + + if (!ObjectModel::updateMultishopTable('Combination', array('default_on' => 1), 'a.id_product_attribute = '.(int)$mini['id_attr'])) + return false; + return true; + } + + /** + * Get all available attribute groups + * + * @param integer $id_lang Language id + * @return array Attribute groups + */ + public function getAttributesGroups($id_lang) + { + if (!Combination::isFeatureActive()) + return array(); + $sql = 'SELECT ag.`id_attribute_group`, ag.`is_color_group`, agl.`name` AS group_name, agl.`public_name` AS public_group_name, + a.`id_attribute`, al.`name` AS attribute_name, a.`color` AS attribute_color, pa.`id_product_attribute`, + IFNULL(stock.quantity, 0) as quantity, product_attribute_shop.`price`, product_attribute_shop.`ecotax`, pa.`weight`, + product_attribute_shop.`default_on`, pa.`reference`, product_attribute_shop.`unit_price_impact`, + pa.`minimal_quantity`, pa.`available_date`, ag.`group_type` + FROM `'._DB_PREFIX_.'product_attribute` pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + '.Product::sqlStock('pa', 'pa').' + LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac ON pac.`id_product_attribute` = pa.`id_product_attribute` + 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` + LEFT JOIN `'._DB_PREFIX_.'attribute_group_lang` agl ON ag.`id_attribute_group` = agl.`id_attribute_group` + '.Shop::addSqlAssociation('attribute', 'a').' + WHERE pa.`id_product` = '.(int)$this->id.' + AND al.`id_lang` = '.(int)$id_lang.' + AND agl.`id_lang` = '.(int)$id_lang.' + GROUP BY id_attribute_group, id_product_attribute + ORDER BY ag.`position` ASC, a.`position` ASC'; + return Db::getInstance()->executeS($sql); + } + + /** + * Delete product accessories + * + * @return mixed Deletion result + */ + public function deleteAccessories() + { + return Db::getInstance()->execute('DELETE FROM `'._DB_PREFIX_.'accessory` WHERE `id_product_1` = '.(int)$this->id); + } + + /** + * Delete product from other products accessories + * + * @return mixed Deletion result + */ + public function deleteFromAccessories() + { + return Db::getInstance()->execute('DELETE FROM `'._DB_PREFIX_.'accessory` WHERE `id_product_2` = '.(int)$this->id); + } + + /** + * Get product accessories (only names) + * + * @param integer $id_lang Language id + * @param integer $id_product Product id + * @return array Product accessories + */ + public static function getAccessoriesLight($id_lang, $id_product, Context $context = null) + { + if (!$context) + $context = Context::getContext(); + + $sql = 'SELECT p.`id_product`, p.`reference`, pl.`name` + FROM `'._DB_PREFIX_.'accessory` + LEFT JOIN `'._DB_PREFIX_.'product` p ON (p.`id_product`= `id_product_2`) + '.Shop::addSqlAssociation('product', 'p').' + LEFT JOIN `'._DB_PREFIX_.'product_lang` pl ON ( + p.`id_product` = pl.`id_product` + AND pl.`id_lang` = '.(int)$id_lang.Shop::addSqlRestrictionOnLang('pl').' + ) + WHERE `id_product_1` = '.(int)$id_product; + + return Db::getInstance()->executeS($sql); + } + + /** + * Get product accessories + * + * @param integer $id_lang Language id + * @return array Product accessories + */ + public function getAccessories($id_lang, $active = true, Context $context = null) + { + if (!$context) + $context = Context::getContext(); + + $sql = 'SELECT p.*, product_shop.*, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`link_rewrite`, + pl.`meta_description`, pl.`meta_keywords`, pl.`meta_title`, pl.`name`, + i.`id_image`, il.`legend`, t.`rate`, m.`name` as manufacturer_name, cl.`name` AS category_default, + DATEDIFF( + p.`date_add`, + DATE_SUB( + NOW(), + INTERVAL '.(Validate::isUnsignedInt(Configuration::get('PS_NB_DAYS_NEW_PRODUCT')) ? Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20).' DAY + ) + ) > 0 AS new + FROM `'._DB_PREFIX_.'accessory` + LEFT JOIN `'._DB_PREFIX_.'product` p ON p.`id_product` = `id_product_2` + '.Shop::addSqlAssociation('product', 'p').' + LEFT JOIN `'._DB_PREFIX_.'product_lang` pl ON ( + p.`id_product` = pl.`id_product` + AND pl.`id_lang` = '.(int)$id_lang.Shop::addSqlRestrictionOnLang('pl').' + ) + LEFT JOIN `'._DB_PREFIX_.'category_lang` cl ON ( + product_shop.`id_category_default` = cl.`id_category` + AND cl.`id_lang` = '.(int)$id_lang.Shop::addSqlRestrictionOnLang('cl').' + ) + LEFT JOIN `'._DB_PREFIX_.'image` i ON (i.`id_product` = p.`id_product` AND i.`cover` = 1) + LEFT JOIN `'._DB_PREFIX_.'image_lang` il ON (i.`id_image` = il.`id_image` AND il.`id_lang` = '.(int)$id_lang.') + LEFT JOIN `'._DB_PREFIX_.'manufacturer` m ON (p.`id_manufacturer`= m.`id_manufacturer`) + LEFT JOIN `'._DB_PREFIX_.'tax_rule` tr ON (product_shop.`id_tax_rules_group` = tr.`id_tax_rules_group` + AND tr.`id_country` = '.(int)$context->country->id.' + AND tr.`id_state` = 0) + LEFT JOIN `'._DB_PREFIX_.'tax` t ON (t.`id_tax` = tr.`id_tax`) + '.Product::sqlStock('p', 0).' + WHERE `id_product_1` = '.(int)$this->id. + ($active ? ' AND product_shop.`active` = 1' : ''); + if (!$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql)) + return false; + foreach ($result as &$row) + $row['id_product_attribute'] = Product::getDefaultAttribute((int)$row['id_product']); + return $this->getProductsProperties($id_lang, $result); + } + + public static function getAccessoryById($accessory_id) + { + return Db::getInstance()->getRow('SELECT `id_product`, `name` FROM `'._DB_PREFIX_.'product_lang` WHERE `id_product` = '.(int)$accessory_id); + } + + /** + * Link accessories with product + * + * @param array $accessories_id Accessories ids + */ + public function changeAccessories($accessories_id) + { + foreach ($accessories_id as $id_product_2) + Db::getInstance()->insert('accessory', array( + 'id_product_1' => (int)$this->id, + 'id_product_2' => (int)$id_product_2 + )); + } + + /** + * Add new feature to product + */ + public function addFeaturesCustomToDB($id_value, $lang, $cust) + { + $row = array('id_feature_value' => (int)$id_value, 'id_lang' => (int)$lang, 'value' => pSQL($cust)); + return Db::getInstance()->insert('feature_value_lang', $row); + } + + public function addFeaturesToDB($id_feature, $id_value, $cust = 0) + { + if ($cust) + { + $row = array('id_feature' => (int)$id_feature, 'custom' => 1); + Db::getInstance()->insert('feature_value', $row); + $id_value = Db::getInstance()->Insert_ID(); + } + $row = array('id_feature' => (int)$id_feature, 'id_product' => (int)$this->id, 'id_feature_value' => (int)$id_value); + Db::getInstance()->insert('feature_product', $row); + SpecificPriceRule::applyAllRules(array((int)$this->id)); + if ($id_value) + return ($id_value); + } + + public static function addFeatureProductImport($id_product, $id_feature, $id_feature_value) + { + return Db::getInstance()->execute(' + INSERT INTO `'._DB_PREFIX_.'feature_product` (`id_feature`, `id_product`, `id_feature_value`) + VALUES ('.(int)$id_feature.', '.(int)$id_product.', '.(int)$id_feature_value.') + ON DUPLICATE KEY UPDATE `id_feature_value` = '.(int)$id_feature_value + ); + } + + /** + * Select all features for the object + * + * @return array Array with feature product's data + */ + public function getFeatures() + { + return Product::getFeaturesStatic((int)$this->id); + } + + public static function getFeaturesStatic($id_product) + { + if (!Feature::isFeatureActive()) + return array(); + if (!array_key_exists($id_product, self::$_cacheFeatures)) + self::$_cacheFeatures[$id_product] = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' + SELECT id_feature, id_product, id_feature_value + FROM `'._DB_PREFIX_.'feature_product` + WHERE `id_product` = '.(int)$id_product + ); + return self::$_cacheFeatures[$id_product]; + } + + public static function cacheProductsFeatures($product_ids) + { + if (!Feature::isFeatureActive()) + return; + + $product_implode = array(); + foreach ($product_ids as $id_product) + if ((int)$id_product && !array_key_exists($id_product, self::$_cacheFeatures)) + $product_implode[] = (int)$id_product; + if (!count($product_implode)) + return; + + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' + SELECT id_feature, id_product, id_feature_value + FROM `'._DB_PREFIX_.'feature_product` + WHERE `id_product` IN ('.implode($product_implode, ',').')'); + foreach ($result as $row) + { + if (!array_key_exists($row['id_product'], self::$_cacheFeatures)) + self::$_cacheFeatures[$row['id_product']] = array(); + self::$_cacheFeatures[$row['id_product']][] = $row; + } + } + + public static function cacheFrontFeatures($product_ids, $id_lang) + { + if (!Feature::isFeatureActive()) + return; + + $product_implode = array(); + foreach ($product_ids as $id_product) + if ((int)$id_product && !array_key_exists($id_product.'-'.$id_lang, self::$_cacheFeatures)) + $product_implode[] = (int)$id_product; + if (!count($product_implode)) + return; + + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' + SELECT id_product, name, value, pf.id_feature + FROM '._DB_PREFIX_.'feature_product pf + LEFT JOIN '._DB_PREFIX_.'feature_lang fl ON (fl.id_feature = pf.id_feature AND fl.id_lang = '.(int)$id_lang.') + LEFT JOIN '._DB_PREFIX_.'feature_value_lang fvl ON (fvl.id_feature_value = pf.id_feature_value AND fvl.id_lang = '.(int)$id_lang.') + LEFT JOIN '._DB_PREFIX_.'feature f ON (f.id_feature = pf.id_feature) + WHERE `id_product` IN ('.implode($product_implode, ',').') + ORDER BY f.position ASC'); + + foreach ($result as $row) + { + if (!array_key_exists($row['id_product'].'-'.$id_lang, self::$_frontFeaturesCache)) + self::$_frontFeaturesCache[$row['id_product'].'-'.$id_lang] = array(); + self::$_frontFeaturesCache[$row['id_product'].'-'.$id_lang][] = $row; + } + } + + /** + * Admin panel product search + * + * @param integer $id_lang Language id + * @param string $query Search query + * @return array Matching products + */ + public static function searchByName($id_lang, $query, Context $context = null) + { + if (!$context) + $context = Context::getContext(); + + $sql = new DbQuery(); + $sql->select('p.`id_product`, pl.`name`, p.`active`, p.`reference`, m.`name` AS manufacturer_name, stock.`quantity`, product_shop.advanced_stock_management, p.`customizable`'); + $sql->from('category_product', 'cp'); + $sql->leftJoin('product', 'p', 'p.`id_product` = cp.`id_product`'); + $sql->join(Shop::addSqlAssociation('product', 'p')); + $sql->leftJoin('product_lang', 'pl', ' + p.`id_product` = pl.`id_product` + AND pl.`id_lang` = '.(int)$id_lang.Shop::addSqlRestrictionOnLang('pl') + ); + $sql->leftJoin('manufacturer', 'm', 'm.`id_manufacturer` = p.`id_manufacturer`'); + + $where = 'pl.`name` LIKE \'%'.pSQL($query).'%\' OR p.`reference` LIKE \'%'.pSQL($query).'%\' OR p.`supplier_reference` LIKE \'%'.pSQL($query).'%\''; + $sql->groupBy('`id_product`'); + $sql->orderBy('pl.`name` ASC'); + + if (Combination::isFeatureActive()) + { + $sql->leftJoin('product_attribute', 'pa', 'pa.`id_product` = p.`id_product`'); + $sql->join(Shop::addSqlAssociation('product_attribute', 'pa', false)); + $where .= ' OR pa.`reference` LIKE \'%'.pSQL($query).'%\''; + } + $sql->where($where); + $sql->join(Product::sqlStock('p', 'pa', false, $context->shop)); + + $result = Db::getInstance()->executeS($sql); + + if (!$result) + return false; + + $results_array = array(); + foreach ($result as $row) + { + $row['price_tax_incl'] = Product::getPriceStatic($row['id_product'], true, null, 2); + $row['price_tax_excl'] = Product::getPriceStatic($row['id_product'], false, null, 2); + $results_array[] = $row; + } + return $results_array; + } + + /** + * Duplicate attributes when duplicating a product + * + * @param integer $id_product_old Old product id + * @param integer $id_product_new New product id + */ + public static function duplicateAttributes($id_product_old, $id_product_new) + { + $return = true; + $combination_images = array(); + + $result = Db::getInstance()->executeS(' + SELECT * + FROM `'._DB_PREFIX_.'product_attribute` pa + WHERE pa.`id_product` = '.(int)$id_product_old + ); + + foreach ($result as $row) + { + $id_product_attribute_old = (int)$row['id_product_attribute']; + $result2 = Db::getInstance()->executeS(' + SELECT * + FROM `'._DB_PREFIX_.'product_attribute_combination` + WHERE `id_product_attribute` = '.$id_product_attribute_old + ); + + $row['id_product'] = $id_product_new; + unset($row['id_product_attribute']); + $combination = new Combination(); + foreach ($row as $k => $v) + $combination->$k = $v; + $return &= $combination->add(); + + $id_product_attribute_new = (int)$combination->id; + if ($result_images = Product::_getAttributeImageAssociations($id_product_attribute_old)) + { + $combination_images['old'][$id_product_attribute_old] = $result_images; + $combination_images['new'][$id_product_attribute_new] = $result_images; + } + foreach ($result2 as $row2) + { + $row2['id_product_attribute'] = $id_product_attribute_new; + $return &= Db::getInstance()->insert('product_attribute_combination', $row2); + } + } + return !$return ? false : $combination_images; + } + + /** + * Get product attribute image associations + * @param integer $id_product_attribute + * @return array + */ + public static function _getAttributeImageAssociations($id_product_attribute) + { + $combination_images = array(); + $data = Db::getInstance()->executeS(' + SELECT `id_image` + FROM `'._DB_PREFIX_.'product_attribute_image` + WHERE `id_product_attribute` = '.(int)$id_product_attribute); + foreach ($data as $row) + $combination_images[] = (int)$row['id_image']; + return $combination_images; + } + + public static function duplicateAccessories($id_product_old, $id_product_new) + { + $return = true; + + $result = Db::getInstance()->executeS(' + SELECT * + FROM `'._DB_PREFIX_.'accessory` + WHERE `id_product_1` = '.(int)$id_product_old); + foreach ($result as $row) + { + $data = array( + 'id_product_1' => (int)$id_product_new, + 'id_product_2' => (int)$row['id_product_2']); + $return &= Db::getInstance()->insert('accessory', $data); + } + return $return; + } + + public static function duplicateTags($id_product_old, $id_product_new) + { + $tags = Db::getInstance()->executeS('SELECT `id_tag` FROM `'._DB_PREFIX_.'product_tag` WHERE `id_product` = '.(int)$id_product_old); + if (!Db::getInstance()->NumRows()) + return true; + + $data = array(); + foreach ($tags as $tag) + $data[] = array( + 'id_product' => (int)$id_product_new, + 'id_tag' => (int)$tag['id_tag'], + ); + + return Db::getInstance()->insert('product_tag', $data); + } + + public static function duplicateDownload($id_product_old, $id_product_new) + { + $sql = 'SELECT `display_filename`, `filename`, `date_add`, `date_expiration`, `nb_days_accessible`, `nb_downloadable`, `active`, `is_shareable` + FROM `'._DB_PREFIX_.'product_download` + WHERE `id_product` = '.(int)$id_product_old; + $results = Db::getInstance()->executeS($sql); + if (!$results) + return true; + + $data = array(); + foreach ($results as $row) + $data[] = array( + 'id_product' => (int)$id_product_new, + 'display_filename' => pSQL($row['display_filename']), + 'filename' => pSQL($row['filename']), + 'date_expiration' => pSQL($row['date_expiration']), + 'nb_days_accessible' => (int)$row['nb_days_accessible'], + 'nb_downloadable' => (int)$row['nb_downloadable'], + 'active' => (int)$row['active'], + 'is_shareable' => (int)$row['is_shareable'] + ); + return Db::getInstance()->insert('product_download', $data); + } + + public static function duplicateAttachments($id_product_old, $id_product_new) + { + // Get all ids attachments of the old product + $sql = 'SELECT `id_attachment` FROM `'._DB_PREFIX_.'product_attachment` WHERE `id_product` = '.(int)$id_product_old; + $results = Db::getInstance()->executeS($sql); + + if (!$results) + return true; + + $data = array(); + + // Prepare data of table product_attachment + foreach ($results as $row) + $data[] = array( + 'id_product' => (int)$id_product_new, + 'id_attachment' => (int)$row['id_attachment'] + ); + + // Duplicate product attachement + return Db::getInstance()->insert('product_attachment', $data); + } + + /** + * Duplicate features when duplicating a product + * + * @param integer $id_product_old Old product id + * @param integer $id_product_old New product id + */ + public static function duplicateFeatures($id_product_old, $id_product_new) + { + $return = true; + + $result = Db::getInstance()->executeS(' + SELECT * + FROM `'._DB_PREFIX_.'feature_product` + WHERE `id_product` = '.(int)$id_product_old); + foreach ($result as $row) + { + $result2 = Db::getInstance()->getRow(' + SELECT * + FROM `'._DB_PREFIX_.'feature_value` + WHERE `id_feature_value` = '.(int)$row['id_feature_value']); + // Custom feature value, need to duplicate it + if ($result2['custom']) + { + $old_id_feature_value = $result2['id_feature_value']; + unset($result2['id_feature_value']); + $return &= Db::getInstance()->insert('feature_value', $result2); + $max_fv = Db::getInstance()->getRow(' + SELECT MAX(`id_feature_value`) AS nb + FROM `'._DB_PREFIX_.'feature_value`'); + $new_id_feature_value = $max_fv['nb']; + $languages = Language::getLanguages(); + foreach ($languages as $language) + { + $result3 = Db::getInstance()->getRow(' + SELECT * + FROM `'._DB_PREFIX_.'feature_value_lang` + WHERE `id_feature_value` = '.(int)$old_id_feature_value.' + AND `id_lang` = '.(int)$language['id_lang']); + + if ($result3) + { + $result3['id_feature_value'] = $new_id_feature_value; + $return &= Db::getInstance()->insert('feature_value_lang', $result3); + } + } + $row['id_feature_value'] = $new_id_feature_value; + } + $row['id_product'] = $id_product_new; + $return &= Db::getInstance()->insert('feature_product', $row); + } + return $return; + } + + protected static function _getCustomizationFieldsNLabels($product_id) + { + if (!Customization::isFeatureActive()) + return false; + + $customizations = array(); + if (($customizations['fields'] = Db::getInstance()->executeS(' + SELECT `id_customization_field`, `type`, `required` + FROM `'._DB_PREFIX_.'customization_field` + WHERE `id_product` = '.(int)$product_id.' + ORDER BY `id_customization_field`')) === false) + return false; + + if (empty($customizations['fields'])) + return array(); + + $customization_field_ids = array(); + foreach ($customizations['fields'] as $customization_field) + $customization_field_ids[] = (int)$customization_field['id_customization_field']; + + if (($customization_labels = Db::getInstance()->executeS(' + SELECT `id_customization_field`, `id_lang`, `name` + FROM `'._DB_PREFIX_.'customization_field_lang` + WHERE `id_customization_field` IN ('.implode(', ', $customization_field_ids).') + ORDER BY `id_customization_field`')) === false) + return false; + + foreach ($customization_labels as $customization_label) + $customizations['labels'][$customization_label['id_customization_field']][] = $customization_label; + + return $customizations; + } + + public static function duplicateSpecificPrices($old_product_id, $product_id) + { + foreach (SpecificPrice::getIdsByProductId((int)$old_product_id) as $data) + { + $specific_price = new SpecificPrice((int)$data['id_specific_price']); + if (!$specific_price->duplicate((int)$product_id)) + return false; + } + return true; + } + + public static function duplicateCustomizationFields($old_product_id, $product_id) + { + // If customization is not activated, return success + if (!Customization::isFeatureActive()) + return true; + if (($customizations = Product::_getCustomizationFieldsNLabels($old_product_id)) === false) + return false; + if (empty($customizations)) + return true; + foreach ($customizations['fields'] as $customization_field) + { + /* The new datas concern the new product */ + $customization_field['id_product'] = (int)$product_id; + $old_customization_field_id = (int)$customization_field['id_customization_field']; + + unset($customization_field['id_customization_field']); + + if (!Db::getInstance()->insert('customization_field', $customization_field) + || !$customization_field_id = Db::getInstance()->Insert_ID()) + return false; + + if (isset($customizations['labels'])) + { + $query = 'INSERT INTO `'._DB_PREFIX_.'` (`id_customization_field`, `id_lang`, `name`) VALUES '; + $data = array(); + foreach ($customizations['labels'][$old_customization_field_id] as $customization_label) + $data = array( + 'id_customization_field' => (int)$customization_field_id, + 'id_lang' => (int)$customization_label['id_lang'], + 'name' => pSQL($customization_label['name']), + ); + + if (!Db::getInstance()->insert('customization_field_lang', $data)) + return false; + } + } + return true; + } + + /** + * Get the link of the product page of this product + */ + public function getLink(Context $context = null) + { + if (!$context) + $context = Context::getContext(); + return $context->link->getProductLink($this); + } + + public function getTags($id_lang) + { + if (!$this->isFullyLoaded && is_null($this->tags)) + $this->tags = Tag::getProductTags($this->id); + + if (!($this->tags && key_exists($id_lang, $this->tags))) + return ''; + + $result = ''; + foreach ($this->tags[$id_lang] as $tag_name) + $result .= $tag_name.', '; + + return rtrim($result, ', '); + } + + public static function defineProductImage($row, $id_lang) + { + if (isset($row['id_image'])) + if ($row['id_image']) + return $row['id_product'].'-'.$row['id_image']; + + return Language::getIsoById((int)$id_lang).'-default'; + } + + public static function getProductProperties($id_lang, $row, Context $context = null) + { + if (!$row['id_product']) + return false; + + if ($context == null) + $context = Context::getContext(); + + // Product::getDefaultAttribute is only called if id_product_attribute is missing from the SQL query at the origin of it: + // consider adding it in order to avoid unnecessary queries + $row['allow_oosp'] = Product::isAvailableWhenOutOfStock($row['out_of_stock']); + if (Combination::isFeatureActive() && (!isset($row['id_product_attribute']) || !$row['id_product_attribute']) + && ((isset($row['cache_default_attribute']) && ($ipa_default = $row['cache_default_attribute']) !== null) + || ($ipa_default = Product::getDefaultAttribute($row['id_product'], !$row['allow_oosp'])))) + $row['id_product_attribute'] = $ipa_default; + if (!Combination::isFeatureActive() || !isset($row['id_product_attribute'])) + $row['id_product_attribute'] = 0; + + // Tax + $usetax = Tax::excludeTaxeOption(); + + $cache_key = $row['id_product'].'-'.$row['id_product_attribute'].'-'.$id_lang.'-'.(int)$usetax; + if (isset($row['id_product_pack'])) + $cache_key .= '-pack'.$row['id_product_pack']; + if (isset(self::$producPropertiesCache[$cache_key])) + return self::$producPropertiesCache[$cache_key]; + + // Datas + $row['category'] = Category::getLinkRewrite((int)$row['id_category_default'], (int)$id_lang); + $row['link'] = $context->link->getProductLink((int)$row['id_product'], $row['link_rewrite'], $row['category'], $row['ean13']); + + $row['attribute_price'] = 0; + if (isset($row['id_product_attribute']) && $row['id_product_attribute']) + $row['attribute_price'] = (float)Product::getProductAttributePrice($row['id_product_attribute']); + + $row['price_tax_exc'] = Product::getPriceStatic( + (int)$row['id_product'], + false, + ((isset($row['id_product_attribute']) && !empty($row['id_product_attribute'])) ? (int)$row['id_product_attribute'] : null), + (self::$_taxCalculationMethod == PS_TAX_EXC ? 2 : 6) + ); + + if (self::$_taxCalculationMethod == PS_TAX_EXC) + { + $row['price_tax_exc'] = Tools::ps_round($row['price_tax_exc'], 2); + $row['price'] = Product::getPriceStatic( + (int)$row['id_product'], + true, + ((isset($row['id_product_attribute']) && !empty($row['id_product_attribute'])) ? (int)$row['id_product_attribute'] : null), + 6 + ); + $row['price_without_reduction'] = Product::getPriceStatic( + (int)$row['id_product'], + false, + ((isset($row['id_product_attribute']) && !empty($row['id_product_attribute'])) ? (int)$row['id_product_attribute'] : null), + 2, + null, + false, + false + ); + } + else + { + $row['price'] = Tools::ps_round( + Product::getPriceStatic( + (int)$row['id_product'], + true, + ((isset($row['id_product_attribute']) && !empty($row['id_product_attribute'])) ? (int)$row['id_product_attribute'] : null), + 2 + ), + 2 + ); + + $row['price_without_reduction'] = Product::getPriceStatic( + (int)$row['id_product'], + true, + ((isset($row['id_product_attribute']) && !empty($row['id_product_attribute'])) ? (int)$row['id_product_attribute'] : null), + 6, + null, + false, + false + ); + } + + $row['reduction'] = Product::getPriceStatic( + (int)$row['id_product'], + (bool)$usetax, + (int)$row['id_product_attribute'], + 6, + null, + true, + true, + 1, + true, + null, + null, + null, + $specific_prices + ); + + $row['specific_prices'] = $specific_prices; + + if ($row['id_product_attribute']) + { + $row['quantity_all_versions'] = $row['quantity']; + $row['quantity'] = Product::getQuantity( + (int)$row['id_product'], + $row['id_product_attribute'], + isset($row['cache_is_pack']) ? $row['cache_is_pack'] : null + ); + } + else + $row['quantity'] = Product::getQuantity((int)$row['id_product']); + + $row['id_image'] = Product::defineProductImage($row, $id_lang); + $row['features'] = Product::getFrontFeaturesStatic((int)$id_lang, $row['id_product']); + + $row['attachments'] = array(); + if (!isset($row['cache_has_attachments']) || $row['cache_has_attachments']) + $row['attachments'] = Product::getAttachmentsStatic((int)$id_lang, $row['id_product']); + + $row['virtual'] = ((!isset($row['is_virtual']) || $row['is_virtual']) ? 1 : 0); + + // Pack management + $row['pack'] = (!isset($row['cache_is_pack']) ? Pack::isPack($row['id_product']) : (int)$row['cache_is_pack']); + $row['packItems'] = $row['pack'] ? Pack::getItemTable($row['id_product'], $id_lang) : array(); + $row['nopackprice'] = $row['pack'] ? Pack::noPackPrice($row['id_product']) : 0; + if ($row['pack'] && !Pack::isInStock($row['id_product'])) + $row['quantity'] = 0; + + self::$producPropertiesCache[$cache_key] = $row; + return self::$producPropertiesCache[$cache_key]; + } + + public static function getProductsProperties($id_lang, $query_result) + { + $results_array = array(); + + if (is_array($query_result)) + foreach ($query_result as $row) + if ($row2 = Product::getProductProperties($id_lang, $row)) + $results_array[] = $row2; + + return $results_array; + } + + /* + * Select all features for a given language + * + * @param $id_lang Language id + * @return array Array with feature's data + */ + public static function getFrontFeaturesStatic($id_lang, $id_product) + { + if (!Feature::isFeatureActive()) + return array(); + if (!array_key_exists($id_product.'-'.$id_lang, self::$_frontFeaturesCache)) + { + self::$_frontFeaturesCache[$id_product.'-'.$id_lang] = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' + SELECT name, value, pf.id_feature + FROM '._DB_PREFIX_.'feature_product pf + LEFT JOIN '._DB_PREFIX_.'feature_lang fl ON (fl.id_feature = pf.id_feature AND fl.id_lang = '.(int)$id_lang.') + LEFT JOIN '._DB_PREFIX_.'feature_value_lang fvl ON (fvl.id_feature_value = pf.id_feature_value AND fvl.id_lang = '.(int)$id_lang.') + LEFT JOIN '._DB_PREFIX_.'feature f ON (f.id_feature = pf.id_feature AND fl.id_lang = '.(int)$id_lang.') + WHERE pf.id_product = '.(int)$id_product.' + ORDER BY f.position ASC' + ); + } + return self::$_frontFeaturesCache[$id_product.'-'.$id_lang]; + } + + public function getFrontFeatures($id_lang) + { + return Product::getFrontFeaturesStatic($id_lang, $this->id); + } + + public static function getAttachmentsStatic($id_lang, $id_product) + { + return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' + SELECT * + FROM '._DB_PREFIX_.'product_attachment pa + LEFT JOIN '._DB_PREFIX_.'attachment a ON a.id_attachment = pa.id_attachment + LEFT JOIN '._DB_PREFIX_.'attachment_lang al ON (a.id_attachment = al.id_attachment AND al.id_lang = '.(int)$id_lang.') + WHERE pa.id_product = '.(int)$id_product); + } + + public function getAttachments($id_lang) + { + return Product::getAttachmentsStatic($id_lang, $this->id); + } + + /* + ** Customization management + */ + + public static function getAllCustomizedDatas($id_cart, $id_lang = null, $only_in_cart = true) + { + if (!Customization::isFeatureActive()) + return false; + + // No need to query if there isn't any real cart! + if (!$id_cart) + return false; + if (!$id_lang) + $id_lang = Context::getContext()->language->id; + + if (!$result = Db::getInstance()->executeS(' + SELECT cd.`id_customization`, c.`id_address_delivery`, c.`id_product`, cfl.`id_customization_field`, c.`id_product_attribute`, + cd.`type`, cd.`index`, cd.`value`, cfl.`name` + FROM `'._DB_PREFIX_.'customized_data` cd + NATURAL JOIN `'._DB_PREFIX_.'customization` c + LEFT JOIN `'._DB_PREFIX_.'customization_field_lang` cfl ON (cfl.id_customization_field = cd.`index` AND id_lang = '.(int)$id_lang.') + WHERE c.`id_cart` = '.(int)$id_cart. + ($only_in_cart ? ' AND c.`in_cart` = 1' : '').' + ORDER BY `id_product`, `id_product_attribute`, `type`, `index`')) + return false; + + $customized_datas = array(); + + foreach ($result as $row) + $customized_datas[(int)$row['id_product']][(int)$row['id_product_attribute']][(int)$row['id_address_delivery']][(int)$row['id_customization']]['datas'][(int)$row['type']][] = $row; + + if (!$result = Db::getInstance()->executeS( + 'SELECT `id_product`, `id_product_attribute`, `id_customization`, `id_address_delivery`, `quantity`, `quantity_refunded`, `quantity_returned` + FROM `'._DB_PREFIX_.'customization` + WHERE `id_cart` = '.(int)($id_cart).($only_in_cart ? ' + AND `in_cart` = 1' : ''))) + return false; + + foreach ($result as $row) + { + $customized_datas[(int)$row['id_product']][(int)$row['id_product_attribute']][(int)$row['id_address_delivery']][(int)$row['id_customization']]['quantity'] = (int)$row['quantity']; + $customized_datas[(int)$row['id_product']][(int)$row['id_product_attribute']][(int)$row['id_address_delivery']][(int)$row['id_customization']]['quantity_refunded'] = (int)$row['quantity_refunded']; + $customized_datas[(int)$row['id_product']][(int)$row['id_product_attribute']][(int)$row['id_address_delivery']][(int)$row['id_customization']]['quantity_returned'] = (int)$row['quantity_returned']; + } + + return $customized_datas; + } + + public static function addCustomizationPrice(&$products, &$customized_datas) + { + if (!$customized_datas) + return; + + foreach ($products as &$product_update) + { + if (!Customization::isFeatureActive()) + { + $product_update['customizationQuantityTotal'] = 0; + $product_update['customizationQuantityRefunded'] = 0; + $product_update['customizationQuantityReturned'] = 0; + } + else + { + $customization_quantity = 0; + $customization_quantity_refunded = 0; + $customization_quantity_returned = 0; + + /* Compatibility */ + $product_id = (int)(isset($product_update['id_product']) ? $product_update['id_product'] : $product_update['product_id']); + $product_attribute_id = (int)(isset($product_update['id_product_attribute']) ? $product_update['id_product_attribute'] : $product_update['product_attribute_id']); + $id_address_delivery = (int)$product_update['id_address_delivery']; + $product_quantity = (int)(isset($product_update['cart_quantity']) ? $product_update['cart_quantity'] : $product_update['product_quantity']); + $price = isset($product_update['price']) ? $product_update['price'] : $product_update['product_price']; + $price_wt = $price * (1 + ((isset($product_update['tax_rate']) ? $product_update['tax_rate'] : $product_update['rate']) * 0.01)); + + if (isset($customized_datas[$product_id][$product_attribute_id])) + foreach ($customized_datas[$product_id][$product_attribute_id][$id_address_delivery] as $customization) + { + $customization_quantity += (int)$customization['quantity']; + $customization_quantity_refunded += (int)$customization['quantity_refunded']; + $customization_quantity_returned += (int)$customization['quantity_returned']; + } + + $product_update['customizationQuantityTotal'] = $customization_quantity; + $product_update['customizationQuantityRefunded'] = $customization_quantity_refunded; + $product_update['customizationQuantityReturned'] = $customization_quantity_returned; + + if ($customization_quantity) + { + $product_update['total_wt'] = $price_wt * ($product_quantity - $customization_quantity); + $product_update['total_customization_wt'] = $price_wt * $customization_quantity; + $product_update['total'] = $price * ($product_quantity - $customization_quantity); + $product_update['total_customization'] = $price * $customization_quantity; + } + } + } + } + + /* + ** Customization fields' label management + */ + + protected function _checkLabelField($field, $value) + { + if (!Validate::isLabel($value)) + return false; + $tmp = explode('_', $field); + if (count($tmp) < 4) + return false; + return $tmp; + } + + protected function _deleteOldLabels() + { + $max = array( + Product::CUSTOMIZE_FILE => (int)Tools::getValue('uploadable_files'), + Product::CUSTOMIZE_TEXTFIELD => (int)Tools::getValue('text_fields') + ); + + /* Get customization field ids */ + if (($result = Db::getInstance()->executeS( + 'SELECT `id_customization_field`, `type` + FROM `'._DB_PREFIX_.'customization_field` + WHERE `id_product` = '.(int)$this->id.' + ORDER BY `id_customization_field`') + ) === false) + return false; + + if (empty($result)) + return true; + + $customization_fields = array( + Product::CUSTOMIZE_FILE => array(), + Product::CUSTOMIZE_TEXTFIELD => array() + ); + + foreach ($result as $row) + $customization_fields[(int)$row['type']][] = (int)$row['id_customization_field']; + + $extra_file = count($customization_fields[Product::CUSTOMIZE_FILE]) - $max[Product::CUSTOMIZE_FILE]; + $extra_text = count($customization_fields[Product::CUSTOMIZE_TEXTFIELD]) - $max[Product::CUSTOMIZE_TEXTFIELD]; + + /* If too much inside the database, deletion */ + if ($extra_file > 0 && count($customization_fields[Product::CUSTOMIZE_FILE]) - $extra_file >= 0 && + (!Db::getInstance()->execute( + 'DELETE FROM `'._DB_PREFIX_.'customization_field` + WHERE `id_product` = '.(int)$this->id.' + AND `type` = '.Product::CUSTOMIZE_FILE.' + AND `id_customization_field` >= '.(int)$customization_fields[Product::CUSTOMIZE_FILE][count($customization_fields[Product::CUSTOMIZE_FILE]) - $extra_file] + ) + || !Db::getInstance()->execute( + 'DELETE FROM `'._DB_PREFIX_.'customization_field_lang` + WHERE `id_customization_field` + NOT IN ( + SELECT `id_customization_field` + FROM `'._DB_PREFIX_.'customization_field` + )' + ))) + return false; + + if ($extra_text > 0 && count($customization_fields[Product::CUSTOMIZE_TEXTFIELD]) - $extra_text >= 0 && + (!Db::getInstance()->execute( + 'DELETE FROM `'._DB_PREFIX_.'customization_field` + WHERE `id_product` = '.(int)$this->id.' + AND `type` = '.Product::CUSTOMIZE_TEXTFIELD.' + AND `id_customization_field` >= '.(int)$customization_fields[Product::CUSTOMIZE_TEXTFIELD][count($customization_fields[Product::CUSTOMIZE_TEXTFIELD]) - $extra_text] + ) + || !Db::getInstance()->execute( + 'DELETE FROM `'._DB_PREFIX_.'customization_field_lang` + WHERE `id_customization_field` + NOT IN ( + SELECT `id_customization_field` + FROM `'._DB_PREFIX_.'customization_field` + )' + ))) + return false; + + // Refresh cache of feature detachable + Configuration::updateGlobalValue('PS_CUSTOMIZATION_FEATURE_ACTIVE', Customization::isCurrentlyUsed()); + + return true; + } + + protected function _createLabel(&$languages, $type) + { + // Label insertion + if (!Db::getInstance()->execute(' + INSERT INTO `'._DB_PREFIX_.'customization_field` (`id_product`, `type`, `required`) + VALUES ('.(int)$this->id.', '.(int)$type.', 0)') || + !$id_customization_field = (int)Db::getInstance()->Insert_ID()) + return false; + + // Multilingual label name creation + $values = ''; + foreach ($languages as $language) + $values .= '('.(int)$id_customization_field.', '.(int)$language['id_lang'].', \'\'), '; + + $values = rtrim($values, ', '); + if (!Db::getInstance()->execute(' + INSERT INTO `'._DB_PREFIX_.'customization_field_lang` (`id_customization_field`, `id_lang`, `name`) + VALUES '.$values)) + return false; + + // Set cache of feature detachable to true + Configuration::updateGlobalValue('PS_CUSTOMIZATION_FEATURE_ACTIVE', '1'); + + return true; + } + + public function createLabels($uploadable_files, $text_fields) + { + $languages = Language::getLanguages(); + if ((int)$uploadable_files > 0) + for ($i = 0; $i < (int)$uploadable_files; $i++) + if (!$this->_createLabel($languages, Product::CUSTOMIZE_FILE)) + return false; + + if ((int)$text_fields > 0) + for ($i = 0; $i < (int)$text_fields; $i++) + if (!$this->_createLabel($languages, Product::CUSTOMIZE_TEXTFIELD)) + return false; + + return true; + } + + public function updateLabels() + { + $has_required_fields = 0; + foreach ($_POST as $field => $value) + /* Label update */ + if (strncmp($field, 'label_', 6) == 0) + { + if (!$tmp = $this->_checkLabelField($field, $value)) + return false; + /* Multilingual label name update */ + if (!Db::getInstance()->execute(' + INSERT INTO `'._DB_PREFIX_.'customization_field_lang` + (`id_customization_field`, `id_lang`, `name`) VALUES ('.(int)$tmp[2].', '.(int)$tmp[3].', \''.pSQL($value).'\') + ON DUPLICATE KEY UPDATE `name` = \''.pSQL($value).'\'')) + return false; + $is_required = isset($_POST['require_'.(int)$tmp[1].'_'.(int)$tmp[2]]) ? 1 : 0; + $has_required_fields |= $is_required; + /* Require option update */ + if (!Db::getInstance()->execute( + 'UPDATE `'._DB_PREFIX_.'customization_field` + SET `required` = '.(int)$is_required.' + WHERE `id_customization_field` = '.(int)$tmp[2])) + return false; + } + + if ($has_required_fields && !ObjectModel::updateMultishopTable('product', array('customizable' => 2), 'a.id_product = '.(int)$this->id)) + return false; + + if (!$this->_deleteOldLabels()) + return false; + + return true; + } + + public function getCustomizationFields($id_lang = false) + { + if (!Customization::isFeatureActive()) + return false; + + if (!$result = Db::getInstance()->executeS(' + SELECT cf.`id_customization_field`, cf.`type`, cf.`required`, cfl.`name`, cfl.`id_lang` + FROM `'._DB_PREFIX_.'customization_field` cf + NATURAL JOIN `'._DB_PREFIX_.'customization_field_lang` cfl + WHERE cf.`id_product` = '.(int)$this->id.($id_lang ? ' AND cfl.`id_lang` = '.(int)$id_lang : '').' + ORDER BY cf.`id_customization_field`')) + return false; + + if ($id_lang) + return $result; + + $customization_fields = array(); + foreach ($result as $row) + $customization_fields[(int)$row['type']][(int)$row['id_customization_field']][(int)$row['id_lang']] = $row; + + return $customization_fields; + } + + public function getCustomizationFieldIds() + { + if (!Customization::isFeatureActive()) + return array(); + return Db::getInstance()->executeS(' + SELECT `id_customization_field`, `type`, `required` + FROM `'._DB_PREFIX_.'customization_field` + WHERE `id_product` = '.(int)$this->id); + } + + public function getRequiredCustomizableFields() + { + if (!Customization::isFeatureActive()) + return array(); + return Db::getInstance()->executeS(' + SELECT `id_customization_field`, `type` + FROM `'._DB_PREFIX_.'customization_field` + WHERE `id_product` = '.(int)$this->id.' + AND `required` = 1' + ); + } + + public function hasAllRequiredCustomizableFields(Context $context = null) + { + if (!Customization::isFeatureActive()) + return true; + if (!$context) + $context = Context::getContext(); + + $fields = $context->cart->getProductCustomization($this->id, null, true); + if (($required_fields = $this->getRequiredCustomizableFields()) === false) + return false; + + $fields_present = array(); + foreach ($fields as $field) + $fields_present[] = array('id_customization_field' => $field['index'], 'type' => $field['type']); + foreach ($required_fields as $required_field) + if (!in_array($required_field, $fields_present)) + return false; + return true; + } + + + /** + * Checks if the product is in at least one of the submited categories + * + * @param int $id_product + * @param array $categories array of category arrays + * @return boolean is the product in at least one category + */ + public static function idIsOnCategoryId($id_product, $categories) + { + if (!((int)$id_product > 0) || !is_array($categories) || empty($categories)) + return false; + $sql = 'SELECT id_product FROM `'._DB_PREFIX_.'category_product` WHERE `id_product` = '.(int)$id_product.' AND `id_category` IN ('; + foreach ($categories as $category) + $sql .= (int)$category['id_category'].','; + $sql = rtrim($sql, ',').')'; + + $hash = md5($sql); + if (!isset(self::$_incat[$hash])) + { + if (!Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql)) + return false; + self::$_incat[$hash] = (Db::getInstance(_PS_USE_SQL_SLAVE_)->NumRows() > 0 ? true : false); + } + return self::$_incat[$hash]; + } + + public function getNoPackPrice() + { + return Pack::noPackPrice($this->id); + } + + public function checkAccess($id_customer) + { + if (!$id_customer) + return (bool)Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(' + SELECT ctg.`id_group` + FROM `'._DB_PREFIX_.'category_product` cp + INNER JOIN `'._DB_PREFIX_.'category_group` ctg ON (ctg.`id_category` = cp.`id_category`) + WHERE cp.`id_product` = '.(int)$this->id.' AND ctg.`id_group` = 1'); + else + return (bool)Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(' + SELECT cg.`id_group` + FROM `'._DB_PREFIX_.'category_product` cp + INNER JOIN `'._DB_PREFIX_.'category_group` ctg ON (ctg.`id_category` = cp.`id_category`) + INNER JOIN `'._DB_PREFIX_.'customer_group` cg ON (cg.`id_group` = ctg.`id_group`) + WHERE cp.`id_product` = '.(int)$this->id.' AND cg.`id_customer` = '.(int)$id_customer); + } + + + /** + * Add a stock movement for current product + * + * Since 1.5, this method only permit to add/remove available quantities of the current product in the current shop + * + * @see StockManager if you want to manage real stock + * @see StockAvailable if you want to manage available quantities for sale on your shop(s) + * + * @deprecated since 1.5.0 + * + * @param int $quantity + * @param int $id_reason - useless + * @param int $id_product_attribute + * @param int $id_order - useless + * @param int $id_employee - useless + * @return bool + */ + public function addStockMvt($quantity, $id_reason, $id_product_attribute = null, $id_order = null, $id_employee = null) + { + if (!$this->id || !$id_reason) + return false; + + if ($id_product_attribute == null) + $id_product_attribute = 0; + + $reason = new StockMvtReason((int)$id_reason); + if (!Validate::isLoadedObject($reason)) + return false; + + $quantity = abs((int)$quantity) * $reason->sign; + + return StockAvailable::updateQuantity($this->id, $id_product_attribute, $quantity); + } + + /** + * @deprecated since 1.5.0 + */ + public function getStockMvts($id_lang) + { + Tools::displayAsDeprecated(); + + return Db::getInstance()->executeS(' + SELECT sm.id_stock_mvt, sm.date_add, sm.quantity, sm.id_order, + CONCAT(pl.name, \' \', GROUP_CONCAT(IFNULL(al.name, \'\'), \'\')) product_name, CONCAT(e.lastname, \' \', e.firstname) employee, mrl.name reason + FROM `'._DB_PREFIX_.'stock_mvt` sm + LEFT JOIN `'._DB_PREFIX_.'product_lang` pl ON ( + sm.id_product = pl.id_product + AND pl.id_lang = '.(int)$id_lang.Shop::addSqlRestrictionOnLang('pl').' + ) + LEFT JOIN `'._DB_PREFIX_.'stock_mvt_reason_lang` mrl ON ( + sm.id_stock_mvt_reason = mrl.id_stock_mvt_reason + AND mrl.id_lang = '.(int)$id_lang.' + ) + LEFT JOIN `'._DB_PREFIX_.'employee` e ON ( + e.id_employee = sm.id_employee + ) + LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac ON ( + pac.id_product_attribute = sm.id_product_attribute + ) + LEFT JOIN `'._DB_PREFIX_.'attribute_lang` al ON ( + al.id_attribute = pac.id_attribute + AND al.id_lang = '.(int)$id_lang.' + ) + WHERE sm.id_product='.(int)$this->id.' + GROUP BY sm.id_stock_mvt + '); + } + + public static function getUrlRewriteInformations($id_product) + { + return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(' + SELECT pl.`id_lang`, pl.`link_rewrite`, p.`ean13`, cl.`link_rewrite` AS category_rewrite + FROM `'._DB_PREFIX_.'product` p + LEFT JOIN `'._DB_PREFIX_.'product_lang` pl ON (p.`id_product` = pl.`id_product`'.Shop::addSqlRestrictionOnLang('pl').') + '.Shop::addSqlAssociation('product', 'p').' + LEFT JOIN `'._DB_PREFIX_.'lang` l ON (pl.`id_lang` = l.`id_lang`) + LEFT JOIN `'._DB_PREFIX_.'category_lang` cl ON (cl.`id_category` = product_shop.`id_category_default` AND cl.`id_lang` = pl.`id_lang`'.Shop::addSqlRestrictionOnLang('cl').') + WHERE p.`id_product` = '.(int)$id_product.' + AND l.`active` = 1 + '); + } + + public function getIdTaxRulesGroup() + { + return $this->id_tax_rules_group; + } + + public static function getIdTaxRulesGroupByIdProduct($id_product, Context $context = null) + { + if (!$context) + $context = Context::getContext(); + + $key = 'product_id_tax_rules_group_'.(int)$id_product.'_'.(int)$context->shop->id; + if (!Cache::isStored($key)) + Cache::store($key, + Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(' + SELECT `id_tax_rules_group` + FROM `'._DB_PREFIX_.'product_shop` + WHERE `id_product` = '.(int)$id_product.' AND id_shop='.(int)Context::getContext()->shop->id)); + + return Cache::retrieve($key); + } + + /** + * @return the total taxes rate applied to the product + */ + public function getTaxesRate(Address $address = null) + { + if (!$address || !$address->id_country) + $address = Address::initialize(); + + $tax_manager = TaxManagerFactory::getManager($address, $this->id_tax_rules_group); + $tax_calculator = $tax_manager->getTaxCalculator(); + + return $tax_calculator->getTotalRate(); + } + + /** + * Webservice getter : get product features association + * + * @return array + */ + public function getWsProductFeatures() + { + $rows = $this->getFeatures(); + foreach ($rows as $keyrow => $row) + { + foreach ($row as $keyfeature => $feature) + { + if ($keyfeature == 'id_feature') + { + $rows[$keyrow]['id'] = $feature; + unset($rows[$keyrow]['id_feature']); + } + unset($rows[$keyrow]['id_product']); + } + asort($rows[$keyrow]); + } + return $rows; + } + + /** + * Webservice setter : set product features association + * + * @param $productFeatures Product Feature ids + * @return boolean + */ + public function setWsProductFeatures($product_features) + { + $this->deleteProductFeatures(); + foreach ($product_features as $product_feature) + $this->addFeaturesToDB($product_feature['id'], $product_feature['id_feature_value']); + return true; + } + + /** + * Webservice getter : get virtual field default combination + * + * @return int + */ + public function getWsDefaultCombination() + { + return Product::getDefaultAttribute($this->id); + } + + /** + * Webservice setter : set virtual field default combination + * + * @param $id_combination id default combination + */ + public function setWsDefaultCombination($id_combination) + { + $this->deleteDefaultAttributes(); + return $this->setDefaultAttribute((int)$id_combination); + } + + /** + * Webservice getter : get category ids of current product for association + * + * @return array + */ + public function getWsCategories() + { + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + 'SELECT cp.`id_category` AS id + FROM `'._DB_PREFIX_.'category_product` cp + LEFT JOIN `'._DB_PREFIX_.'category` c ON (c.id_category = cp.id_category) + '.Shop::addSqlAssociation('category', 'c').' + WHERE cp.`id_product` = '.(int)$this->id + ); + return $result; + } + + /** + * Webservice setter : set category ids of current product for association + * + * @param $category_ids category ids + */ + public function setWsCategories($category_ids) + { + $ids = array(); + foreach ($category_ids as $value) + $ids[] = $value['id']; + if ($this->deleteCategories()) + { + if ($ids) + { + $sql_values = ''; + $ids = array_map('intval', $ids); + foreach ($ids as $position => $id) + $sql_values[] = '('.(int)$id.', '.(int)$this->id.', '.(int)$position.')'; + $result = Db::getInstance()->execute(' + INSERT INTO `'._DB_PREFIX_.'category_product` (`id_category`, `id_product`, `position`) + VALUES '.implode(',', $sql_values) + ); + return $result; + } + } + return true; + } + + /** + * Webservice getter : get combination ids of current product for association + * + * @return array + */ + public function getWsCombinations() + { + $result = Db::getInstance()->executeS( + 'SELECT pa.`id_product_attribute` as id + FROM `'._DB_PREFIX_.'product_attribute` pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + WHERE `id_product` = '.(int)$this->id + ); + + return $result; + } + + /** + * Webservice setter : set combination ids of current product for association + * + * @param $combinations combination ids + */ + public function setWsCombinations($combinations) + { + // No hook exec + $ids_new = array(); + foreach ($combinations as $combination) + $ids_new[] = (int)$combination['id']; + + $ids_orig = array(); + $original = Db::getInstance()->executeS( + 'SELECT pa.`id_product_attribute` as id + FROM `'._DB_PREFIX_.'product_attribute` pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + WHERE `id_product` = '.(int)$this->id + ); + + if (is_array($original)) + foreach ($original as $id) + $ids_orig[] = $id['id']; + + $all_ids = array(); + $all = Db::getInstance()->executeS('SELECT pa.`id_product_attribute` as id FROM `'._DB_PREFIX_.'product_attribute` pa '.Shop::addSqlAssociation('product_attribute', 'pa')); + if (is_array($all)) + foreach ($all as $id) + $all_ids[] = $id['id']; + + $to_add = array(); + foreach ($ids_new as $id) + if (!in_array($id, $ids_orig)) + $to_add[] = $id; + + $to_delete = array(); + foreach ($ids_orig as $id) + if (!in_array($id, $ids_new)) + $to_delete[] = $id; + + // Delete rows + if (count($to_delete) > 0) + foreach ($to_delete as $id) + { + $combination = new Combination($id); + $combination->delete(); + } + + foreach ($to_add as $id) + { + // Update id_product if exists else create + if (in_array($id, $all_ids)) + Db::getInstance()->execute('UPDATE `'._DB_PREFIX_.'product_attribute` SET id_product = '.(int)$this->id.' WHERE id_product_attribute='.$id); + else + Db::getInstance()->execute('INSERT INTO `'._DB_PREFIX_.'product_attribute` (`id_product`) VALUES ('.$this->id.')'); + } + return true; + } + + /** + * Webservice getter : get product option ids of current product for association + * + * @return array + */ + public function getWsProductOptionValues() + { + $result = Db::getInstance()->executeS('SELECT DISTINCT pac.id_attribute as id + FROM `'._DB_PREFIX_.'product_attribute` pa + '.Shop::addSqlAssociation('product_attribute', 'pa').' + LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac ON (pac.id_product_attribute = pa.id_product_attribute) + WHERE pa.id_product = '.(int)$this->id); + return $result; + } + + /** + * Webservice getter : get virtual field position in category + * + * @return int + */ + public function getWsPositionInCategory() + { + $result = Db::getInstance()->executeS('SELECT position + FROM `'._DB_PREFIX_.'category_product` + WHERE id_category = '.(int)$this->id_category_default.' + AND id_product = '.(int)$this->id); + if (count($result) > 0) + return $result[0]['position']; + return ''; + } + + /** + * Webservice getter : get virtual field id_default_image in category + * + * @return int + */ + public function getCoverWs() + { + $result = $this->getCover($this->id); + return $result['id_image']; + } + + /** + * Webservice setter : set virtual field id_default_image in category + * + * @return bool + */ + public function setCoverWs($id_image) + { + Db::getInstance()->execute('UPDATE `'._DB_PREFIX_.'image` + SET `cover` = 0 WHERE `id_product` = '.(int)$this->id.' + '); + Db::getInstance()->execute('UPDATE `'._DB_PREFIX_.'image` + SET `cover` = 1 WHERE `id_product` = '.(int)$this->id.' AND `id_image` = '.(int)$id_image + ); + + return true; + } + + /** + * Webservice getter : get image ids of current product for association + * + * @return array + */ + public function getWsImages() + { + return Db::getInstance()->executeS(' + SELECT i.`id_image` as id + FROM `'._DB_PREFIX_.'image` i + '.Shop::addSqlAssociation('image', 'i').' + WHERE i.`id_product` = '.(int)$this->id.' + ORDER BY i.`position`'); + } + + public function getWsTags() + { + return Db::getInstance()->executeS(' + SELECT `id_tag` as id + FROM `'._DB_PREFIX_.'product_tag` + WHERE `id_product` = '.(int)$this->id); + } + + + public function getWsManufacturerName() + { + return Manufacturer::getNameById((int)$this->id_manufacturer); + } + + public static function resetEcoTax() + { + return ObjectModel::updateMultishopTable('product', array( + 'ecotax' => 0, + ), ''); + } + + /** + * Set Group reduction if needed + */ + public function setGroupReduction() + { + $row = GroupReduction::getGroupByCategoryId($this->id_category_default); + if (!$row) // Remove + { + if (!GroupReduction::deleteProductReduction($this->id)) + return false; + } + else if (!GroupReduction::setProductReduction($this->id, $row['id_group'], $this->id_category_default, (float)$row['reduction'])) + return false; + return true; + } + + /** + * Checks if reference exists + * @return boolean + */ + public function existsRefInDatabase($reference) + { + $row = Db::getInstance()->getRow(' + SELECT `reference` + FROM `'._DB_PREFIX_.'product` p + WHERE p.reference = "'.pSQL($reference).'"'); + + return isset($row['reference']); + } + + /** + * Get all product attributes ids + * + * @since 1.5.0 + * @param int $id_product the id of the product + * @return array product attribute id list + */ + public static function getProductAttributesIds($id_product, $shop_only = false) + { + return Db::getInstance()->executeS(' + SELECT pa.id_product_attribute + FROM `'._DB_PREFIX_.'product_attribute` pa'. + ($shop_only ? Shop::addSqlAssociation('product_attribute', 'pa') : '').' + WHERE pa.`id_product` = '.(int)$id_product); + } + + /** + * Get label by lang and value by lang too + * @todo Remove existing module condition + * @param int $id_product + * @param int $product_attribute_id + * @return array + */ + public static function getAttributesParams($id_product, $id_product_attribute) + { + // if blocklayered module is installed we check if user has set custom attribute name + if (Module::isInstalled('blocklayered')) + { + $nb_custom_values = Db::getInstance()->executeS(' + SELECT DISTINCT la.`id_attribute`, la.`url_name` as `name` + FROM `'._DB_PREFIX_.'attribute` a + LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac + ON (a.`id_attribute` = pac.`id_attribute`) + LEFT JOIN `'._DB_PREFIX_.'product_attribute` pa + ON (pac.`id_product_attribute` = pa.`id_product_attribute`) + '.Shop::addSqlAssociation('product_attribute', 'pa').' + LEFT JOIN `'._DB_PREFIX_.'layered_indexable_attribute_lang_value` la + ON (la.`id_attribute` = a.`id_attribute` AND la.`id_lang` = '.(int)Context::getContext()->language->id.') + WHERE la.`url_name` IS NOT NULL + AND pa.`id_product` = '.(int)$id_product); + + if (!empty($nb_custom_values)) + { + $tab_id_attribute = array(); + foreach ($nb_custom_values as $attribute) + { + $tab_id_attribute[] = $attribute['id_attribute']; + + $group = Db::getInstance()->executeS(' + SELECT g.`id_attribute_group`, g.`url_name` as `group` + FROM `'._DB_PREFIX_.'layered_indexable_attribute_group_lang_value` g + LEFT JOIN `'._DB_PREFIX_.'attribute` a + ON (a.`id_attribute_group` = g.`id_attribute_group`) + WHERE a.`id_attribute` = '.(int)$attribute['id_attribute'].' + AND g.`id_lang` = '.(int)Context::getContext()->language->id.' + AND g.`url_name` IS NOT NULL'); + if (empty($group)) + { + $group = Db::getInstance()->executeS(' + SELECT g.`id_attribute_group`, g.`name` as `group` + FROM `'._DB_PREFIX_.'attribute_group_lang` g + LEFT JOIN `'._DB_PREFIX_.'attribute` a + ON (a.`id_attribute_group` = g.`id_attribute_group`) + WHERE a.`id_attribute` = '.(int)$attribute['id_attribute'].' + AND g.`id_lang` = '.(int)Context::getContext()->language->id.' + AND g.`name` IS NOT NULL'); + } + $result[] = array_merge($attribute, $group[0]); + } + $values_not_custom = Db::getInstance()->executeS(' + SELECT DISTINCT a.`id_attribute_group`, al.`name`, agl.`name` as `group` + FROM `'._DB_PREFIX_.'attribute` a + LEFT JOIN `'._DB_PREFIX_.'attribute_lang` al + ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = '.(int)Context::getContext()->language->id.') + LEFT JOIN `'._DB_PREFIX_.'attribute_group_lang` agl + ON (a.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = '.(int)Context::getContext()->language->id.') + LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac + ON (a.`id_attribute` = pac.`id_attribute`) + LEFT JOIN `'._DB_PREFIX_.'product_attribute` pa + ON (pac.`id_product_attribute` = pa.`id_product_attribute`) + '.Shop::addSqlAssociation('product_attribute', 'pa').' + WHERE pa.`id_product` = '.(int)$id_product.' + AND a.`id_attribute` NOT IN('.implode(', ', $tab_id_attribute).')'); + $result = array_merge($values_not_custom, $result); + } + else + { + $result = Db::getInstance()->executeS(' + SELECT al.`name`, agl.`name` as `group` + FROM `'._DB_PREFIX_.'attribute` a + LEFT JOIN `'._DB_PREFIX_.'attribute_lang` al + ON (al.`id_attribute` = a.`id_attribute` AND al.`id_lang` = '.(int)Context::getContext()->language->id.') + LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac + ON (pac.`id_attribute` = a.`id_attribute`) + LEFT JOIN `'._DB_PREFIX_.'product_attribute` pa + ON (pa.`id_product_attribute` = pac.`id_product_attribute`) + '.Shop::addSqlAssociation('product_attribute', 'pa').' + LEFT JOIN `'._DB_PREFIX_.'attribute_group_lang` agl + ON (a.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = '.(int)Context::getContext()->language->id.') + WHERE pa.`id_product` = '.(int)$id_product.' + AND pac.`id_product_attribute` = '.(int)$id_product_attribute.' + AND agl.`id_lang` = '.(int)Context::getContext()->language->id); + } + } + else + { + $result = Db::getInstance()->executeS(' + SELECT al.`name`, agl.`name` as `group` + FROM `'._DB_PREFIX_.'attribute` a + LEFT JOIN `'._DB_PREFIX_.'attribute_lang` al + ON (al.`id_attribute` = a.`id_attribute` AND al.`id_lang` = '.(int)Context::getContext()->language->id.') + LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac + ON (pac.`id_attribute` = a.`id_attribute`) + LEFT JOIN `'._DB_PREFIX_.'product_attribute` pa + ON (pa.`id_product_attribute` = pac.`id_product_attribute`) + '.Shop::addSqlAssociation('product_attribute', 'pa').' + LEFT JOIN `'._DB_PREFIX_.'attribute_group_lang` agl + ON (a.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = '.(int)Context::getContext()->language->id.') + WHERE pa.`id_product` = '.(int)$id_product.' + AND pac.`id_product_attribute` = '.(int)$id_product_attribute.' + AND agl.`id_lang` = '.(int)Context::getContext()->language->id); + } + return $result; + } + + /** + * @todo Remove existing module condition + * @param int $id_product + */ + public static function getAttributesInformationsByProduct($id_product) + { + // if blocklayered module is installed we check if user has set custom attribute name + if (Module::isInstalled('blocklayered')) + { + $nb_custom_values = Db::getInstance()->executeS(' + SELECT DISTINCT la.`id_attribute`, la.`url_name` as `attribute` + FROM `'._DB_PREFIX_.'attribute` a + LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac + ON (a.`id_attribute` = pac.`id_attribute`) + LEFT JOIN `'._DB_PREFIX_.'product_attribute` pa + ON (pac.`id_product_attribute` = pa.`id_product_attribute`) + '.Shop::addSqlAssociation('product_attribute', 'pa').' + LEFT JOIN `'._DB_PREFIX_.'layered_indexable_attribute_lang_value` la + ON (la.`id_attribute` = a.`id_attribute` AND la.`id_lang` = '.(int)Context::getContext()->language->id.') + WHERE la.`url_name` IS NOT NULL + AND pa.`id_product` = '.(int)$id_product); + + if (!empty($nb_custom_values)) + { + $tab_id_attribute = array(); + foreach ($nb_custom_values as $attribute) + { + $tab_id_attribute[] = $attribute['id_attribute']; + + $group = Db::getInstance()->executeS(' + SELECT g.`id_attribute_group`, g.`url_name` as `group` + FROM `'._DB_PREFIX_.'layered_indexable_attribute_group_lang_value` g + LEFT JOIN `'._DB_PREFIX_.'attribute` a + ON (a.`id_attribute_group` = g.`id_attribute_group`) + WHERE a.`id_attribute` = '.(int)$attribute['id_attribute'].' + AND g.`id_lang` = '.(int)Context::getContext()->language->id.' + AND g.`url_name` IS NOT NULL'); + if (empty($group)) + { + $group = Db::getInstance()->executeS(' + SELECT g.`id_attribute_group`, g.`name` as `group` + FROM `'._DB_PREFIX_.'attribute_group_lang` g + LEFT JOIN `'._DB_PREFIX_.'attribute` a + ON (a.`id_attribute_group` = g.`id_attribute_group`) + WHERE a.`id_attribute` = '.(int)$attribute['id_attribute'].' + AND g.`id_lang` = '.(int)Context::getContext()->language->id.' + AND g.`name` IS NOT NULL'); + } + $result[] = array_merge($attribute, $group[0]); + } + $values_not_custom = Db::getInstance()->executeS(' + SELECT DISTINCT a.`id_attribute`, a.`id_attribute_group`, a.`id_attribute_group`, al.`name` as `attribute`, agl.`name` as `group` + FROM `'._DB_PREFIX_.'attribute` a + LEFT JOIN `'._DB_PREFIX_.'attribute_lang` al + ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = '.(int)Context::getContext()->language->id.') + LEFT JOIN `'._DB_PREFIX_.'attribute_group_lang` agl + ON (a.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = '.(int)Context::getContext()->language->id.') + LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac + ON (a.`id_attribute` = pac.`id_attribute`) + LEFT JOIN `'._DB_PREFIX_.'product_attribute` pa + ON (pac.`id_product_attribute` = pa.`id_product_attribute`) + '.Shop::addSqlAssociation('product_attribute', 'pa').' + '.Shop::addSqlAssociation('attribute', 'pac').' + WHERE pa.`id_product` = '.(int)$id_product.' + AND a.`id_attribute` NOT IN('.implode(', ', $tab_id_attribute).')'); + $result = array_merge($values_not_custom, $result); + } + else + { + $result = Db::getInstance()->executeS(' + SELECT DISTINCT a.`id_attribute`, a.`id_attribute_group`, al.`name` as `attribute`, agl.`name` as `group` + FROM `'._DB_PREFIX_.'attribute` a + LEFT JOIN `'._DB_PREFIX_.'attribute_lang` al + ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = '.(int)Context::getContext()->language->id.') + LEFT JOIN `'._DB_PREFIX_.'attribute_group_lang` agl + ON (a.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = '.(int)Context::getContext()->language->id.') + LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac + ON (a.`id_attribute` = pac.`id_attribute`) + LEFT JOIN `'._DB_PREFIX_.'product_attribute` pa + ON (pac.`id_product_attribute` = pa.`id_product_attribute`) + '.Shop::addSqlAssociation('product_attribute', 'pa').' + '.Shop::addSqlAssociation('attribute', 'pac').' + WHERE pa.`id_product` = '.(int)$id_product); + } + } + else + { + $result = Db::getInstance()->executeS(' + SELECT DISTINCT a.`id_attribute`, a.`id_attribute_group`, al.`name` as `attribute`, agl.`name` as `group` + FROM `'._DB_PREFIX_.'attribute` a + LEFT JOIN `'._DB_PREFIX_.'attribute_lang` al + ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = '.(int)Context::getContext()->language->id.') + LEFT JOIN `'._DB_PREFIX_.'attribute_group_lang` agl + ON (a.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = '.(int)Context::getContext()->language->id.') + LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac + ON (a.`id_attribute` = pac.`id_attribute`) + LEFT JOIN `'._DB_PREFIX_.'product_attribute` pa + ON (pac.`id_product_attribute` = pa.`id_product_attribute`) + '.Shop::addSqlAssociation('product_attribute', 'pa').' + '.Shop::addSqlAssociation('attribute', 'pac').' + WHERE pa.`id_product` = '.(int)$id_product); + } + return $result; + } + + /** + * Get the combination url anchor of the product + * + * @param integer $id_product_attribute + * @return string + */ + public function getAnchor($id_product_attribute) + { + $attributes = Product::getAttributesParams($this->id, $id_product_attribute); + $anchor = '#'; + foreach ($attributes as &$a) + { + foreach ($a as &$b) + $b = str_replace('-', '_', Tools::link_rewrite($b)); + $anchor .= '/'.$a['group'].'-'.$a['name']; + } + return $anchor; + } + + /** + * Gets the name of a given product, in the given lang + * + * @since 1.5.0 + * @param int $id_product + * @param int $id_product_attribute Optional + * @param int $id_lang Optional + * @return string + */ + public static function getProductName($id_product, $id_product_attribute = null, $id_lang = null) + { + // use the lang in the context if $id_lang is not defined + if (!$id_lang) + $id_lang = (int)Context::getContext()->language->id; + + // creates the query object + $query = new DbQuery(); + + // selects different names, if it is a combination + if ($id_product_attribute) + $query->select('IFNULL(CONCAT(pl.name, \' : \', GROUP_CONCAT(DISTINCT agl.`name`, \' - \', al.name SEPARATOR \', \')),pl.name) as name'); + else + $query->select('DISTINCT pl.name as name'); + + // adds joins & where clauses for combinations + if ($id_product_attribute) + { + $query->from('product_attribute', 'pa'); + $query->join(Shop::addSqlAssociation('product_attribute', 'pa')); + $query->innerJoin('product_lang', 'pl', 'pl.id_product = pa.id_product AND pl.id_lang = '.(int)$id_lang); + $query->leftJoin('product_attribute_combination', 'pac', 'pac.id_product_attribute = pa.id_product_attribute'); + $query->leftJoin('attribute', 'atr', 'atr.id_attribute = pac.id_attribute'); + $query->leftJoin('attribute_lang', 'al', 'al.id_attribute = atr.id_attribute AND al.id_lang = '.(int)$id_lang); + $query->leftJoin('attribute_group_lang', 'agl', 'agl.id_attribute_group = atr.id_attribute_group AND agl.id_lang = '.(int)$id_lang); + $query->where('pa.id_product = '.(int)$id_product.' AND pa.id_product_attribute = '.(int)$id_product_attribute); + } + else // or just adds a 'where' clause for a simple product + { + $query->from('product_lang', 'pl'); + $query->where('pl.id_product = '.(int)$id_product); + } + + return Db::getInstance()->getValue($query); + } + + public function addWs($autodate = true, $null_values = false) + { + $success = parent::add($autodate, $null_values); + if ($success && Configuration::get('PS_SEARCH_INDEXATION')) + Search::indexation(false, $this->id); + return $success; + } + + public function updateWs($null_values = false) + { + $success = parent::update($null_values); + if ($success && Configuration::get('PS_SEARCH_INDEXATION')) + Search::indexation(false, $this->id); + return $success; + } + + /** + * For a given product, returns its real quantity + * + * @since 1.5.0 + * @param int $id_product + * @param int $id_product_attribute + * @param int $id_warehouse + * @param int $id_shop + * @return int real_quantity + */ + public static function getRealQuantity($id_product, $id_product_attribute = 0, $id_warehouse = 0, $id_shop = null) + { + static $manager = null; + + if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && is_null($manager)) + $manager = StockManagerFactory::getManager(); + + if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && Product::usesAdvancedStockManagement($id_product) && + StockAvailable::dependsOnStock($id_product, $id_shop)) + return $manager->getProductRealQuantities($id_product, $id_product_attribute, $id_warehouse, true); + else + return StockAvailable::getQuantityAvailableByProduct($id_product, $id_product_attribute, $id_shop); + } + + /** + * For a given product, tells if it uses the advanced stock management + * + * @since 1.5.0 + * @param int $id_product + * @return bool + */ + public static function usesAdvancedStockManagement($id_product) + { + $query = new DbQuery; + $query->select('product_shop.advanced_stock_management'); + $query->from('product', 'p'); + $query->join(Shop::addSqlAssociation('product', 'p')); + $query->where('p.id_product = '.(int)$id_product); + + return (bool)Db::getInstance()->getValue($query); + } + + /** + * This method allows to flush price cache + * @static + * @since 1.5.0 + */ + public static function flushPriceCache() + { + self::$_prices = array(); + self::$_pricesLevel2 = array(); + } + + /** + * Get list of parent categories + * + * @since 1.5.0 + * @param int $id_lang + * @return array + */ + public function getParentCategories($id_lang = null) + { + if (!$id_lang) + $id_lang = Context::getContext()->language->id; + + $interval = Category::getInterval($this->id_category_default); + $sql = new DbQuery(); + $sql->from('category', 'c'); + $sql->leftJoin('category_lang', 'cl', 'c.id_category = cl.id_category AND id_lang = '.(int)$id_lang.Shop::addSqlRestrictionOnLang('cl')); + $sql->where('c.nleft <= '.(int)$interval['nleft'].' AND c.nright >= '.(int)$interval['nright']); + $sql->orderBy('c.nleft'); + + return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); + } + + /** + * Fill the variables used for stock management + */ + public function loadStockData() + { + if (Validate::isLoadedObject($this)) + { + // By default, the product quantity correspond to the available quantity to sell in the current shop + $this->quantity = StockAvailable::getQuantityAvailableByProduct($this->id, 0); + $this->out_of_stock = StockAvailable::outOfStock($this->id); + $this->depends_on_stock = StockAvailable::dependsOnStock($this->id); + if (Context::getContext()->shop->getContext() == Shop::CONTEXT_GROUP && Context::getContext()->shop->getContextShopGroup()->share_stock == 1) + $this->advanced_stock_management = Db::getInstance()->getValue('SELECT `advanced_stock_management` + FROM '._DB_PREFIX_.'product_shop + WHERE id_product='.(int)$this->id.Shop::addSqlRestriction()); + } + } + + /** + * get the default category according to the shop + */ + public function getDefaultCategory() + { + $default_category = Db::getInstance()->getValue(' + SELECT product_shop.`id_category_default` + FROM `'._DB_PREFIX_.'product` p + '.Shop::addSqlAssociation('product', 'p').' + WHERE p.`id_product` = '.(int)$this->id); + + if (!$default_category) + return array('id_category_default' => Context::getContext()->shop->id_category); + else + return $default_category; + + } + + public static function getShopsByProduct($id_product) + { + return Db::getInstance()->executeS(' + SELECT `id_shop` + FROM `'._DB_PREFIX_.'product_shop` + WHERE `id_product` = '.(int)$id_product); + } + + /** + * Remove all downloadable files for product and its attributes + * + * @return bool + */ + public function deleteDownload() + { + $result = true; + $collection_download = new Collection('ProductDownload'); + $collection_download->where('id_product', '=', $this->id); + foreach ($collection_download as $product_download) + $result &= $product_download->delete(true); + return $result; + } + + /** + * @deprecated 1.5.0.10 + * @see Product::getAttributeCombinations() + * @param int $id_lang + */ + public function getAttributeCombinaisons($id_lang) + { + Tools::displayAsDeprecated('Use Product::getAttributeCombinations($id_lang)'); + return $this->getAttributeCombinations($id_lang); + } + + /** + * @deprecated 1.5.0.10 + * @see Product::deleteAttributeCombination() + * @param int $id_product_attribute + */ + public function deleteAttributeCombinaison($id_product_attribute) + { + Tools::displayAsDeprecated('Use Product::deleteAttributeCombination($id_product_attribute)'); + return $this->deleteAttributeCombination($id_product_attribute); + } + + /** + * Get the product type (simple, virtual, pack) + * @since in 1.5.0 + * + * @return int + */ + public function getType() + { + if (!$this->id) + return Product::PTYPE_SIMPLE; + if (Pack::isPack($this->id)) + return Product::PTYPE_PACK; + if ($this->is_virtual) + return Product::PTYPE_VIRTUAL; + + return Product::PTYPE_SIMPLE; + } + + public function hasAttributesInOtherShops() + { + return (bool)Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(' + SELECT pa.id_product_attribute + FROM `'._DB_PREFIX_.'product_attribute` pa + LEFT JOIN `'._DB_PREFIX_.'product_attribute_shop` pas ON (pa.`id_product_attribute` = pas.`id_product_attribute`) + WHERE pa.`id_product` = '.(int)$this->id + ); + } +} -Tools::redirect('index.php?controller=product'.($_REQUEST ? '&'.http_build_query($_REQUEST, '', '&') : ''), __PS_BASE_URI__, null, 'HTTP/1.1 301 Moved Permanently'); \ No newline at end of file