Online PHP and Javascript Decoder decode hidden script to uncover its real functionality


Show other level

/**
 * Copyright since 2007 PrestaShop SA and Contributors
 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 30)
 * that is bundled with this package in the file LICENSEmd.
 * It is also available through the world-wide-web at this URL:
 * https://opensourceorg/licenses/OSL-30
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashopcom so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
 * versions in the futureIf you wish to customize PrestaShop for your
 * needs please refer to https://devdocsprestashopcom/ for more information.
 *
 * @author    PrestaShop SA and Contributors <contact@prestashopcom>
 * @copyright Since 2007 PrestaShop SA and Contributors
 * @license   https://opensourceorg/licenses/OSL-30 Open Software License (OSL 30)
 */

/*
 * @deprecated 1501
 */
define('_CUSTOMIZE_FILE_', 0);
/*
 * @deprecated 1501
 */
define('_CUSTOMIZE_TEXTFIELD_', 1);

use PrestaShop\PrestaShop\Adapter\ServiceLocator;
use PrestaShop\PrestaShop\Core\Product\ProductInterface;

class ProductCore extends ObjectModel
{
    /** @var string Tax name */
    public $tax_name;

    /** @var string Tax rate */
    public $tax_rate;

    /** @var int Manufacturer id */
    public $id_manufacturer;

    /** @var int Supplier id */
    public $id_supplier;

    /** @var int default Category id */
    public $id_category_default;

    /** @var int 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 int Quantity available */
    public $quantity = 0;

    /** @var int Minimal quantity for add to cart */
    public $minimal_quantity = 1;

    /** @var int|null Low stock for mail alert */
    public $low_stock_threshold = null;

    /** @var bool Low stock mail alert activated */
    public $low_stock_alert = false;

    /** @var string available_now */
    public $available_now;

    /** @var string available_later */
    public $available_later;

    /** @var float Price in euros */
    public $price = 0;

    public $specificPrice = 0;

    /** @var float Additional shipping cost */
    public $additional_shipping_cost = 0;

    /** @var float Wholesale Price in euros */
    public $wholesale_price = 0;

    /** @var bool on_sale */
    public $on_sale = false;

    /** @var bool 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
     *
     * @deprecated since 1770
     */
    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 ISBN */
    public $isbn;

    /** @var string Upc barcode */
    public $upc;

    /** @var string MPN */
    public $mpn;

    /** @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 bool Product statuts */
    public $quantity_discount = 0;

    /** @var bool Product customization */
    public $customizable;

    /** @var bool Product is new */
    public $new = null;

    /** @var int Number of uploadable files (concerning customizable products) */
    public $uploadable_files;

    /** @var int Number of text fields */
    public $text_fields;

    /** @var bool Product statuts */
    public $active = true;

    /** @var bool Product statuts */
    public $redirect_type = '';

    /** @var bool Product statuts */
    public $id_type_redirected = 0;

    /** @var bool Product available for order */
    public $available_for_order = true;

    /** @var string Object available order date */
    public $available_date = '0000-00-00';

    /** @var bool Will the condition select should be visible for this product ? */
    public $show_condition = false;

    /** @var string Enumerated (enum) product condition (new, used, refurbished) */
    public $condition;

    /** @var bool Show price of Product */
    public $show_price = true;

    /** @var bool 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;

    /** @var int temporary or saved object */
    public $state = self::STATE_SAVED;

    /**
     * @var float Base price of the product
     *
     * @deprecated 16013
     */
    public $base_price;

    public $id_tax_rules_group = 1;

    /**
     * We keep this variable for retrocompatibility for themes.
     *
     * @deprecated 150
     */
    public $id_color_default = 0;

    /**
     * @since 150
     *
     * @var bool 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 $id_pack_product_attribute;
    public $cache_default_attribute;

    /**
     * @var string If product is populated, this property contain the rewrite link of the default category
     */
    public $category;

    /**
     * @var int tell the type of stock management to apply on the pack
     */
    public $pack_stock_type = Pack::STOCK_TYPE_DEFAULT;

    /**
     * Type of delivery time.
     *
     * Choose which parameters use for give information delivery.
     * 0 - none
     * 1 - use default information
     * 2 - use product information
     *
     * @var int
     */
    public $additional_delivery_times = 1;

    /**
     * Delivery in-stock information.
     *
     * Long description for delivery in-stock product information.
     *
     * @var string
     */
    public $delivery_in_stock;

    /**
     * Delivery out-stock information.
     *
     * Long description for delivery out-stock product information.
     *
     * @var string
     */
    public $delivery_out_stock;

    public static $_taxCalculationMethod = null;
    protected static $_prices = [];
    protected static $_pricesLevel2 = [];
    protected static $_incat = [];

    /**
     * @since 1561
     *
     * @var array is deprecated since 1561
     */
    protected static $_cart_quantity = [];

    protected static $_tax_rules_group = [];
    protected static $_cacheFeatures = [];
    protected static $_frontFeaturesCache = [];
    protected static $productPropertiesCache = [];

    /** @var array cache stock data in getStock() method */
    protected static $cacheStock = [];

    const STATE_TEMP = 0;
    const STATE_SAVED = 1;

    public static $definition = [
        'table' => 'product',
        'primary' => 'id_product',
        'multilang' => true,
        'multilang_shop' => true,
        'fields' => [
            
            'id_shop_default' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'id_manufacturer' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'id_supplier' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'reference' => ['type' => self::TYPE_STRING, 'validate' => 'isReference', 'size' => 64],
            'supplier_reference' => ['type' => self::TYPE_STRING, 'validate' => 'isReference', 'size' => 64],
            'location' => ['type' => self::TYPE_STRING, 'validate' => 'isReference', 'size' => 64],
            'width' => ['type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'],
            'height' => ['type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'],
            'depth' => ['type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'],
            'weight' => ['type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'],
            'quantity_discount' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
            'ean13' => ['type' => self::TYPE_STRING, 'validate' => 'isEan13', 'size' => 13],
            'isbn' => ['type' => self::TYPE_STRING, 'validate' => 'isIsbn', 'size' => 32],
            'upc' => ['type' => self::TYPE_STRING, 'validate' => 'isUpc', 'size' => 12],
            'mpn' => ['type' => self::TYPE_STRING, 'validate' => 'isMpn', 'size' => 40],
            'cache_is_pack' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
            'cache_has_attachments' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
            'is_virtual' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
            'state' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'additional_delivery_times' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'delivery_in_stock' => [
                'type' => self::TYPE_STRING,
                'lang' => true,
                'validate' => 'isGenericName',
                'size' => 255,
            ],
            'delivery_out_stock' => [
                'type' => self::TYPE_STRING,
                'lang' => true,
                'validate' => 'isGenericName',
                'size' => 255,
            ],

            
            'id_category_default' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'],
            'id_tax_rules_group' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'],
            'on_sale' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'online_only' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'ecotax' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice'],
            'minimal_quantity' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'],
            'low_stock_threshold' => ['type' => self::TYPE_INT, 'shop' => true, 'allow_null' => true, 'validate' => 'isInt'],
            'low_stock_alert' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'price' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice', 'required' => true],
            'wholesale_price' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice'],
            'unity' => ['type' => self::TYPE_STRING, 'shop' => true, 'validate' => 'isString'],
            'unit_price_ratio' => ['type' => self::TYPE_FLOAT, 'shop' => true],
            'additional_shipping_cost' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice'],
            'customizable' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'],
            'text_fields' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'],
            'uploadable_files' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'],
            'active' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'redirect_type' => ['type' => self::TYPE_STRING, 'shop' => true, 'validate' => 'isString'],
            'id_type_redirected' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'],
            'available_for_order' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'available_date' => ['type' => self::TYPE_DATE, 'shop' => true, 'validate' => 'isDateFormat'],
            'show_condition' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'condition' => ['type' => self::TYPE_STRING, 'shop' => true, 'validate' => 'isGenericName', 'values' => ['new', 'used', 'refurbished'], 'default' => 'new'],
            'show_price' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'indexed' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'visibility' => ['type' => self::TYPE_STRING, 'shop' => true, 'validate' => 'isProductVisibility', 'values' => ['both', 'catalog', 'search', 'none'], 'default' => 'both'],
            'cache_default_attribute' => ['type' => self::TYPE_INT, 'shop' => true],
            'advanced_stock_management' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'date_add' => ['type' => self::TYPE_DATE, 'shop' => true, 'validate' => 'isDate'],
            'date_upd' => ['type' => self::TYPE_DATE, 'shop' => true, 'validate' => 'isDate'],
            'pack_stock_type' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'],

            
            'meta_description' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 512],
            'meta_keywords' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255],
            'meta_title' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255],
            'link_rewrite' => [
                'type' => self::TYPE_STRING,
                'lang' => true,
                'validate' => 'isLinkRewrite',
                'required' => false,
                'size' => 128,
                'ws_modifier' => [
                    'http_method' => WebserviceRequest::HTTP_POST,
                    'modifier' => 'modifierWsLinkRewrite',
                ],
            ],
            'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isCatalogName', 'required' => false, 'size' => 128],
            'description' => ['type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml'],
            'description_short' => ['type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml'],
            'available_now' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255],
            'available_later' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'IsGenericName', 'size' => 255],
        ],
        'associations' => [
            'manufacturer' => ['type' => self::HAS_ONE],
            'supplier' => ['type' => self::HAS_ONE],
            'default_category' => ['type' => self::HAS_ONE, 'field' => 'id_category_default', 'object' => 'Category'],
            'tax_rules_group' => ['type' => self::HAS_ONE],
            'categories' => ['type' => self::HAS_MANY, 'field' => 'id_category', 'object' => 'Category', 'association' => 'category_product'],
            'stock_availables' => ['type' => self::HAS_MANY, 'field' => 'id_stock_available', 'object' => 'StockAvailable', 'association' => 'stock_availables'],
        ],
    ];

    protected $webserviceParameters = [
        'objectMethods' => [
            'add' => 'addWs',
            'update' => 'updateWs',
        ],
        'objectNodeNames' => 'products',
        'fields' => [
            'id_manufacturer' => [
                'xlink_resource' => 'manufacturers',
            ],
            'id_supplier' => [
                'xlink_resource' => 'suppliers',
            ],
            'id_category_default' => [
                'xlink_resource' => 'categories',
            ],
            'new' => [],
            'cache_default_attribute' => [],
            'id_default_image' => [
                'getter' => 'getCoverWs',
                'setter' => 'setCoverWs',
                'xlink_resource' => [
                    'resourceName' => 'images',
                    'subResourceName' => 'products',
                ],
            ],
            'id_default_combination' => [
                'getter' => 'getWsDefaultCombination',
                'setter' => 'setWsDefaultCombination',
                'xlink_resource' => [
                    'resourceName' => 'combinations',
                ],
            ],
            'id_tax_rules_group' => [
                'xlink_resource' => [
                    'resourceName' => 'tax_rule_groups',
                ],
            ],
            'position_in_category' => [
                'getter' => 'getWsPositionInCategory',
                'setter' => 'setWsPositionInCategory',
            ],
            'manufacturer_name' => [
                'getter' => 'getWsManufacturerName',
                'setter' => false,
            ],
            'quantity' => [
                'getter' => false,
                'setter' => false,
            ],
            'type' => [
                'getter' => 'getWsType',
                'setter' => 'setWsType',
            ],
        ],
        'associations' => [
            'categories' => [
                'resource' => 'category',
                'fields' => [
                    'id' => ['required' => true],
                ],
            ],
            'images' => [
                'resource' => 'image',
                'fields' => ['id' => []],
            ],
            'combinations' => [
                'resource' => 'combination',
                'fields' => [
                    'id' => ['required' => true],
                ],
            ],
            'product_option_values' => [
                'resource' => 'product_option_value',
                'fields' => [
                    'id' => ['required' => true],
                ],
            ],
            'product_features' => [
                'resource' => 'product_feature',
                'fields' => [
                    'id' => ['required' => true],
                    'id_feature_value' => [
                        'required' => true,
                        'xlink_resource' => 'product_feature_values',
                    ],
                ],
            ],
            'tags' => ['resource' => 'tag',
                'fields' => [
                    'id' => ['required' => true],
                ], ],
            'stock_availables' => ['resource' => 'stock_available',
                'fields' => [
                    'id' => ['required' => true],
                    'id_product_attribute' => ['required' => true],
                ],
                'setter' => false,
            ],
            'accessories' => [
                'resource' => 'product',
                'api' => 'products',
                'fields' => [
                    'id' => [
                        'required' => true,
                        'xlink_resource' => 'product', ],
                ],
            ],
            'product_bundle' => [
                'resource' => 'product',
                'api' => 'products',
                'fields' => [
                    'id' => ['required' => true],
                    'id_product_attribute' => [],
                    'quantity' => [],
                ],
            ],
        ],
    ];

    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 ($full && $this->id) {
            if (!$context) {
                $context = Context::getContext();
            }

            $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();

            // Keep base price
            $this->base_price = $this->price;

            $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);
            $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 (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;
        }
        $fields['unity'] = pSQL($this->unity);

        return $fields;
    }

    public function add($autodate = true, $null_values = false)
    {
        if (!parent::add($autodate, $null_values)) {
            return false;
        }

        $id_shop_list = Shop::getContextListShopID();
        if ($this->getType() == Product::PTYPE_VIRTUAL) {
            foreach ($id_shop_list as $value) {
                StockAvailable::setProductOutOfStock((int) $this->id, 1, $value);
            }

            if ($this->active && !Configuration::get('PS_VIRTUAL_PROD_FEATURE_ACTIVE')) {
                Configuration::updateGlobalValue('PS_VIRTUAL_PROD_FEATURE_ACTIVE', '1');
            }
        } else {
            foreach ($id_shop_list as $value) {
                StockAvailable::setProductOutOfStock((int) $this->id, 2, $value);
            }
        }

        $this->setGroupReduction();
        Hook::exec('actionProductSave', ['id_product' => (int) $this->id, 'product' => $this]);

        return true;
    }

    public function update($null_values = false)
    {
        $return = parent::update($null_values);
        $this->setGroupReduction();

        // Sync stock Reference, EAN13, MPN and UPC
        if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && StockAvailable::dependsOnStock($this->id, Context::getContext()->shop->id)) {
            Db::getInstance()->update('stock', [
                'reference' => pSQL($this->reference),
                'ean13' => pSQL($this->ean13),
                'isbn' => pSQL($this->isbn),
                'upc' => pSQL($this->upc),
                'mpn' => pSQL($this->mpn),
            ], 'id_product = ' . (int) $this->id . ' AND id_product_attribute = 0');
        }

        Hook::exec('actionProductSave', ['id_product' => (int) $this->id, 'product' => $this]);
        Hook::exec('actionProductUpdate', ['id_product' => (int) $this->id, 'product' => $this]);
        if ($this->getType() == Product::PTYPE_VIRTUAL && $this->active && !Configuration::get('PS_VIRTUAL_PROD_FEATURE_ACTIVE')) {
            Configuration::updateGlobalValue('PS_VIRTUAL_PROD_FEATURE_ACTIVE', '1');
        }

        return $return;
    }

    /**
     * Init computation of price display method (ieprice should be including tax or not) for a customer.
     * If customer Id passed as null then this compute price display method with according of current group.
     * Otherwise a price display method will compute with according of a customer address (iecountry).
     *
     * @see Group::getPriceDisplayMethod()
     *
     * @param int|null $id_customer
     */
    public static function initPricesComputation($id_customer = null)
    {
        if ((int) $id_customer > 0) {
            $customer = new Customer((int) $id_customer);
            if (!Validate::isLoadedObject($customer)) {
                die(Tools::displayError());
            }
            self::$_taxCalculationMethod = Group::getPriceDisplayMethod((int) $customer->id_default_group);
            $cur_cart = Context::getContext()->cart;
            $id_address = 0;
            if (Validate::isLoadedObject($cur_cart)) {
                $id_address = (int) $cur_cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')};
            }
            $address_infos = Address::getCountryAndState($id_address);

            if (self::$_taxCalculationMethod != PS_TAX_EXC
                && !empty($address_infos['vat_number'])
                && $address_infos['id_country'] != Configuration::get('VATNUMBER_COUNTRY')
                && Configuration::get('VATNUMBER_MANAGEMENT')) {
                self::$_taxCalculationMethod = PS_TAX_EXC;
            }
        } else {
            self::$_taxCalculationMethod = Group::getPriceDisplayMethod(Group::getCurrent()->id);
        }
    }

    /**
     * Returns price display method for a customer (ieprice should be including tax or not).
     *
     * @see initPricesComputation()
     *
     * @param int|null $id_customer
     *
     * @return int Returns 0 (PS_TAX_INC) if tax should be included, otherwise 1 (PS_TAX_EXC) - tax should be excluded
     */
    public static function getTaxCalculationMethod($id_customer = null)
    {
        if (self::$_taxCalculationMethod === null || $id_customer !== null) {
            Product::initPricesComputation($id_customer);
        }

        return (int) self::$_taxCalculationMethod;
    }

    /**
     * Move a product inside its category.
     *
     * @param bool $way Up (1)  or Down (0)
     * @param int $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
        $result = (
            Db::getInstance()->execute('
            UPDATE `' . _DB_PREFIX_ . 'category_product` cp
            INNER JOIN `' . _DB_PREFIX_ . 'product` p ON (p.`id_product` = cp.`id_product`)
            ' . Shop::addSqlAssociation('product', 'p') . '
            SET cp.`position`= `position` ' . ($way ? '- 1' : '+ 1') . ',
            p.`date_upd` = "' . date('Y-m-d H:i:s') . '", product_shop.`date_upd` = "' . date('Y-m-d H:i:s') . '"
            WHERE cp.`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` cp
            INNER JOIN `' . _DB_PREFIX_ . 'product` p ON (p.`id_product` = cp.`id_product`)
            ' . Shop::addSqlAssociation('product', 'p') . '
            SET cp.`position` = ' . (int) $position . ',
            p.`date_upd` = "' . date('Y-m-d H:i:s') . '", product_shop.`date_upd` = "' . date('Y-m-d H:i:s') . '"
            WHERE cp.`id_product` = ' . (int) $moved_product['id_product'] . '
            AND cp.`id_category`=' . (int) $moved_product['id_category'])
        );
        Hook::exec('actionProductUpdate', ['id_product' => (int) $this->id, 'product' => $this]);

        return $result;
    }

    /**
     * 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, $position = 0)
    {
        $return = true;

        if (!(int) $position) {
            $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',
                    ['position' => $i],
                    '`id_category` = ' . (int) $id_category . ' AND `id_product` = ' . (int) $result[$i]['id_product']
                );
                $return &= Db::getInstance()->execute(
                    'UPDATE `' . _DB_PREFIX_ . 'product` p' . Shop::addSqlAssociation('product', 'p') . '
                    SET p.`date_upd` = "' . date('Y-m-d H:i:s') . '", product_shop.`date_upd` = "' . date('Y-m-d H:i:s') . '"
                    WHERE p.`id_product` = ' . (int) $result[$i]['id_product']
                );
            }
        } else {
            $result = Db::getInstance()->executeS('
                SELECT `id_product`
                FROM `' . _DB_PREFIX_ . 'category_product`
                WHERE `id_category` = ' . (int) $id_category . ' AND `position` > ' . (int) $position . '
                ORDER BY `position`
            ');
            $total = count($result);
            $return &= Db::getInstance()->update(
                'category_product',
                ['position' => ['type' => 'sql', 'value' => '`position`-1']],
                '`id_category` = ' . (int) $id_category . ' AND `position` > ' . (int) $position
            );

            for ($i = 0; $i < $total; ++$i) {
                $return &= Db::getInstance()->execute(
                    'UPDATE `' . _DB_PREFIX_ . 'product` p' . Shop::addSqlAssociation('product', 'p') . '
                    SET p.`date_upd` = "' . date('Y-m-d H:i:s') . '", product_shop.`date_upd` = "' . date('Y-m-d H:i:s') . '"
                    WHERE p.`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, $reset = false)
    {
        static $combinations = [];

        if (!Combination::isFeatureActive()) {
            return 0;
        }

        if ($reset && isset($combinations[$id_product])) {
            unset($combinations[$id_product]);
        }

        if (!isset($combinations[$id_product])) {
            $combinations[$id_product] = [];
        }
        if (isset($combinations[$id_product][$minimum_quantity])) {
            return $combinations[$id_product][$minimum_quantity];
        }

        $sql = 'SELECT product_attribute_shopid_product_attribute
                FROM ' . _DB_PREFIX_ . 'product_attribute pa
                ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
                WHERE paid_product = ' . (int) $id_product;

        $result_no_filter = Db::getInstance()->getValue($sql);
        if (!$result_no_filter) {
            $combinations[$id_product][$minimum_quantity] = 0;

            return 0;
        }

        $sql = 'SELECT product_attribute_shopid_product_attribute
                FROM ' . _DB_PREFIX_ . 'product_attribute pa
                ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
                ' . ($minimum_quantity > 0 ? Product::sqlStock('pa', 'pa') : '') .
                ' WHERE product_attribute_shopdefault_on = 1 '
                . ($minimum_quantity > 0 ? ' AND IFNULL(stockquantity, 0) >= ' . (int) $minimum_quantity : '') .
                ' AND paid_product = ' . (int) $id_product;
        $result = Db::getInstance()->getValue($sql);

        if (!$result) {
            $sql = 'SELECT product_attribute_shopid_product_attribute
                    FROM ' . _DB_PREFIX_ . 'product_attribute pa
                    ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
                    ' . ($minimum_quantity > 0 ? Product::sqlStock('pa', 'pa') : '') .
                    ' WHERE paid_product = ' . (int) $id_product
                    . ($minimum_quantity > 0 ? ' AND IFNULL(stockquantity, 0) >= ' . (int) $minimum_quantity : '');

            $result = Db::getInstance()->getValue($sql);
        }

        if (!$result) {
            $sql = 'SELECT product_attribute_shopid_product_attribute
                    FROM ' . _DB_PREFIX_ . 'product_attribute pa
                    ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
                    WHERE product_attribute_shop.`default_on` = 1
                    AND paid_product = ' . (int) $id_product;

            $result = Db::getInstance()->getValue($sql);
        }

        if (!$result) {
            $result = $result_no_filter;
        }

        $combinations[$id_product][$minimum_quantity] = $result;

        return $result;
    }

    public function setAvailableDate($available_date = '0000-00-00')
    {
        if (Validate::isDateFormat($available_date) && $this->available_date != $available_date) {
            $this->available_date = $available_date;

            return $this->update();
        }

        return false;
    }

    /**
     * For a given id_product and id_product_attribute, return available date.
     *
     * @param int $id_product
     * @param int $id_product_attribute Optional
     *
     * @return string/null
     */
    public static function getAvailableDate($id_product, $id_product_attribute = null)
    {
        $sql = 'SELECT';

        if ($id_product_attribute === null) {
            $sql .= ' p.`available_date`';
        } else {
            $sql .= ' pa.`available_date`';
        }

        $sql .= ' FROM `' . _DB_PREFIX_ . 'product` p';

        if ($id_product_attribute !== null) {
            $sql .= ' LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON (pa.`id_product` = p.`id_product`)';
        }

        $sql .= Shop::addSqlAssociation('product', 'p');

        if ($id_product_attribute !== null) {
            $sql .= Shop::addSqlAssociation('product_attribute', 'pa');
        }

        $sql .= ' WHERE p.`id_product` = ' . (int) $id_product;

        if ($id_product_attribute !== null) {
            $sql .= ' AND pa.`id_product` = ' . (int) $id_product . ' AND pa.`id_product_attribute` = ' . (int) $id_product_attribute;
        }

        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql);

        if ($result == '0000-00-00') {
            $result = null;
        }

        return $result;
    }

    public static function updateIsVirtual($id_product, $is_virtual = true)
    {
        Db::getInstance()->update('product', [
            'is_virtual' => (bool) $is_virtual,
        ], 'id_product = ' . (int) $id_product);
    }

    /**
     * @see ObjectModel::resetStaticCache()
     *
     * reset static cache (eg unit testing purpose).
     */
    public static function resetStaticCache()
    {
        static::$loaded_classes = [];
        static::$productPropertiesCache = [];
        static::$_cacheFeatures = [];
        static::$_frontFeaturesCache = [];
        static::$_prices = [];
        static::$_pricesLevel2 = [];
        static::$_incat = [];
    }

    /**
     * @see ObjectModel::validateField()
     */
    public function validateField($field, $value, $id_lang = null, $skip = [], $human_errors = false)
    {
        if ($field == 'description_short') {
            $limit = (int) Configuration::get('PS_PRODUCT_SHORT_DESC_LIMIT');
            if ($limit <= 0) {
                $limit = 800;
            }

            $size_without_html = Tools::strlen(strip_tags($value));
            $size_with_html = Tools::strlen($value);
            $this->def['fields']['description_short']['size'] = $limit + $size_with_html - $size_without_html;
        }

        return parent::validateField($field, $value, $id_lang, $skip, $human_errors);
    }

    public function toggleStatus()
    {
        //test if the product is active and if redirect_type is empty string and set default value to id_type_redirected & redirect_type
        //  /!\ after parent::toggleStatus() active will be false, that why we set 404 by default :p
        if ($this->active) {
            //case where active will be false after parent::toggleStatus()
            $this->id_type_redirected = 0;
            $this->redirect_type = ProductInterface::REDIRECT_TYPE_CATEGORY_MOVED_PERMANENTLY;
        } else {
            //case where active will be true after parent::toggleStatus()
            $this->id_type_redirected = 0;
            $this->redirect_type = '';
        }

        return parent::toggleStatus();
    }

    public function delete()
    {
        /*
         * @since 150
         * 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;
            }

            $warehouse_product_locations = ServiceLocator::get('\\PrestaShop\\PrestaShop\\Core\\Foundation\\Database\\EntityManager')->getRepository('WarehouseProductLocation')->findByIdProduct($this->id);
            foreach ($warehouse_product_locations as $warehouse_product_location) {
                $warehouse_product_location->delete();
            }

            $stocks = ServiceLocator::get('\\PrestaShop\\PrestaShop\\Core\\Foundation\\Database\\EntityManager')->getRepository('Stock')->findByIdProduct($this->id);
            foreach ($stocks as $stock) {
                $stock->delete();
            }
        }
        $result = parent::delete();

        // Removes the product from StockAvailable, for the current shop
        StockAvailable::removeProductFromStockAvailable($this->id);
        $result &= ($this->deleteProductAttributes() && $this->deleteImages());
        // If there are still entries in product_shop, don't remove completely the product
        if ($this->hasMultishopEntries()) {
            return true;
        }

        Hook::exec('actionProductDelete', ['id_product' => (int) $this->id, 'product' => $this]);
        if (!$result ||
            !GroupReduction::deleteProductReduction($this->id) ||
            !$this->deleteCategories(true) ||
            !$this->deleteProductFeatures() ||
            !$this->deleteTags() ||
            !$this->deleteCartProducts() ||
            !$this->deleteAttributesImpacts() ||
            !$this->deleteAttachments(false) ||
            !$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 serverLet's say 15 seconds by product (I've seen it!).
            if ((int) (ini_get('max_execution_time')) < round($count * 15)) {
                ini_set('max_execution_time', round($count * 15));
            }

            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 bool true if succeed
     */
    public function addToCategories($categories = [])
    {
        if (empty($categories)) {
            return false;
        }

        if (!is_array($categories)) {
            $categories = [$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 = [];
        // The first position must be 1 instead of 0
        foreach ($categories as $id_category) {
            $new_categ_pos[$id_category] = isset($new_categories[$id_category]) ? $new_categories[$id_category] : 1;
        }

        $product_cats = [];

        foreach ($categories as $new_id_categ) {
            if (!in_array($new_id_categ, $current_categories)) {
                $product_cats[] = [
                    '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);

        Cache::clean('Product::getProductCategories_' . (int) $this->id);

        return true;
    }

    /**
     * Update categories to index product into.
     *
     * @param string $productCategories Categories list to index product into
     * @param bool $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, null, true) . '
            WHERE cp.`id_category` NOT IN (' . implode(',', array_map('intval', $categories)) . ')
            AND cpid_product = ' . (int) $this->id
        );

        // if none are found, it's an error
        if (!is_array($result)) {
            return false;
        }

        foreach ($result as $categ_to_delete) {
            $this->deleteCategory($categ_to_delete['id_category']);
        }

        if (!$this->addToCategories($categories)) {
            return false;
        }

        SpecificPriceRule::applyAllRules([(int) $this->id]);

        Cache::clean('Product::getProductCategories_' . (int) $this->id);

        return true;
    }

    /**
     * deleteCategory delete this product from the category $id_category.
     *
     * @param mixed $id_category
     * @param mixed $clean_positions
     *
     * @return bool
     */
    public function deleteCategory($id_category, $clean_positions = true)
    {
        $result = Db::getInstance()->executeS(
            'SELECT `id_category`, `position`
            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) {
                self::cleanPositions((int) $row['id_category'], (int) $row['position']);
            }
        }

        SpecificPriceRule::applyAllRules([(int) $this->id]);

        Cache::clean('Product::getProductCategories_' . (int) $this->id);

        return $return;
    }

    /**
     * Delete all association to category where product is indexed.
     *
     * @param bool $clean_positions clean category positions after deletion
     *
     * @return array Deletion result
     */
    public function deleteCategories($clean_positions = false)
    {
        if ($clean_positions === true) {
            $result = Db::getInstance()->executeS(
                'SELECT `id_category`, `position`
                FROM `' . _DB_PREFIX_ . 'category_product`
                WHERE `id_product` = ' . (int) $this->id
            );
        }

        $return = Db::getInstance()->delete('category_product', 'id_product = ' . (int) $this->id);
        if ($clean_positions === true && is_array($result)) {
            foreach ($result as $row) {
                $return &= self::cleanPositions((int) $row['id_category'], (int) $row['position']);
            }
        }

        Cache::clean('Product::getProductCategories_' . (int) $this->id);

        return $return;
    }

    /**
     * Delete products tags entries.
     *
     * @return array Deletion result
     */
    public function deleteTags()
    {
        return Tag::deleteTagsForProduct((int) $this->id);
    }

    /**
     * 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;
    }

    /**
     * Get all available products.
     *
     * @param int $id_lang Language id
     * @param int $start Start number
     * @param int $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, ['front', 'modulefront'])) {
            $front = false;
        }

        if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay($order_way)) {
            die(Tools::displayError());
        }
        if (
        } elseif (
        } elseif (
        }

        if (strpos($order_by, ) > 0) {
            
            $order_by_prefix = "=";
            
        }
        $sql = 'SELECT p.*, product_shop.*, pl.* , 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_ . '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 (
        }

        foreach ($rq as &$row) {
            $row = Product::getTaxesInformations($row);
        }

        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, ['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 pid_product
            FROM `' . _DB_PREFIX_ . 'product` p
            ' . Shop::addSqlAssociation('product', 'p') . '
            WHERE pid_product = ' . (int) $this->id . '
            AND DATEDIFF(
                product_shop.`date_add`,
                DATE_SUB(
                    "' . date('Y-m-d') . ' 00:00:00",
                    INTERVAL ' . (Validate::isUnsignedInt(Configuration::get('PS_NB_DAYS_NEW_PRODUCT')) ? Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20) . ' DAY
                )
            ) > 0
        ', true, false);

        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 (pasid_product_attribute = paid_product_attribute)
            LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON (pac.`id_product_attribute` = pa.`id_product_attribute`)
            WHERE 1 ' . (!$all_shops ? ' AND pasid_shop =' . (int) $context->shop->id : '') . ' AND pa.`id_product` = ' . (int) $this->id .
            ($all_shops ? ' GROUP BY pacid_attribute, pacid_product_attribute ' : '')
        );

        
        if (!$result || empty($result)) {
            return false;
        }
        
        $product_attributes = [];
        foreach ($result as $product_attribute) {
            $product_attributes[$product_attribute['id_product_attribute']][] = $product_attribute['id_attribute'];
        }
        
        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 150
     */
    public function addProductAttribute(
        $price,
        $weight,
        $unit_impact,
        $ecotax,
        $quantity,
        $id_images,
        $reference,
        $id_supplier,
        $ean13,
        $default,
        $location,
        $upc,
        $minimal_quantity,
        $isbn,
        $low_stock_threshold = null,
        $low_stock_alert = false,
        $mpn = null
    ) {
        Tools::displayAsDeprecated();

        $id_product_attribute = $this->addAttribute(
            $price,
            $weight,
            $unit_impact,
            $ecotax,
            $id_images,
            $reference,
            $ean13,
            $default,
            $location,
            $upc,
            $minimal_quantity,
            [],
            null,
            0,
            $isbn,
            $low_stock_threshold,
            $low_stock_alert,
            $mpn
        );

        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, $resetExistingCombination = true)
    {
        $res = true;
        foreach ($combinations as $key => $combination) {
            $id_combination = (int) $this->productAttributeExists($attributes[$key], false, null, true, true);
            if ($id_combination && !$resetExistingCombination) {
                continue;
            }

            $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->default_on = 0;
            $this->setAvailableDate();

            $obj->save();

            if (!$id_combination) {
                $attribute_list = [];
                foreach ($attributes[$key] as $id_attribute) {
                    $attribute_list[] = [
                        'id_product_attribute' => (int) $obj->id,
                        'id_attribute' => (int) $id_attribute,
                    ];
                }
                $res &= Db::getInstance()->insert('product_attribute_combination', $attribute_list);
            }
        }

        return $res;
    }

    public function sortCombinationByAttributePosition($combinations, $langId)
    {
        $attributes = [];
        foreach ($combinations as $combinationId) {
            $attributeCombination = $this->getAttributeCombinationsById($combinationId, $langId);
            $attributes["="['position']][$combinationId] = "=";
        }

        ksort($attributes);

        return $attributes;
    }

    /**
     * @param int $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 = [],
        $available_date = null,
        $isbn = '',
        $low_stock_threshold = null,
        $low_stock_alert = false,
        $mpn = null
    ) {
        $id_product_attribute = $this->addAttribute(
            $price,
            $weight,
            $unit_impact,
            $ecotax,
            $id_images,
            $reference,
            $ean13,
            $default,
            $location,
            $upc,
            $minimal_quantity,
            $id_shop_list,
            $available_date,
            0,
            $isbn,
            $low_stock_threshold,
            $low_stock_alert,
            $mpn
        );
        $this->addSupplierReference($id_supplier, $id_product_attribute);
        $result = ObjectModel::updateMultishopTable('Combination', [
            'wholesale_price' => (float) $wholesale_price,
        ], 'aid_product_attribute = ' . (int) $id_product_attribute);

        if (!$id_product_attribute || !$result) {
            return false;
        }

        return $id_product_attribute;
    }

    /**
     * @deprecated 1550
     *
     * @param $attributes
     * @param bool $set_default
     *
     * @return array
     */
    public function addProductAttributeMultiple($attributes, $set_default = true)
    {
        Tools::displayAsDeprecated();
        $return = [];
        $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 = [];
                    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', [
            'default_on' => null,
        ], 'a.`id_product` = ' . (int) $this->id);
    }

    public function setDefaultAttribute($id_product_attribute)
    {
        $result = ObjectModel::updateMultishopTable('Combination', [
            'default_on' => 1,
        ], 'a.`id_product` = ' . (int) $this->id . ' AND a.`id_product_attribute` = ' . (int) $id_product_attribute);

        $result &= ObjectModel::updateMultishopTable('product', [
            'cache_default_attribute' => (int) $id_product_attribute,
        ], 'a.`id_product` = ' . (int) $this->id);
        $this->cache_default_attribute = (int) $id_product_attribute;

        return $result;
    }

    public static function updateDefaultAttribute($id_product)
    {
        $id_default_attribute = (int) Product::getDefaultAttribute($id_product, 0, true);

        $result = Db::getInstance()->update('product_shop', [
            'cache_default_attribute' => $id_default_attribute,
        ], 'id_product = ' . (int) $id_productShop::addSqlRestriction());

        $result &= Db::getInstance()->update('product', [
            'cache_default_attribute' => $id_default_attribute,
        ], 'id_product = ' . (int) $id_product);

        if ($result && $id_default_attribute) {
            return $id_default_attribute;
        } else {
            return $result;
        }
    }

    /**
     * Update a product attribute.
     *
     * @deprecated since 15
     * @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,
        $ean13,
        $default,
        $location,
        $upc,
        $minimal_quantity,
        $available_date,
        $isbn = '',
        $low_stock_threshold = null,
        $low_stock_alert = false,
        $mpn = null
    ) {
        Tools::displayAsDeprecated('Use updateAttribute() instead');

        $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,
            true,
            [],
            $isbn,
            $low_stock_threshold,
            $low_stock_alert,
            $mpn = null
        );
        $this->addSupplierReference($id_supplier, $id_product_attribute);

        return $return;
    }

    /**
     * Sets or updates 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) && ($this->id > 0)) {
            $id_product_supplier = (int) ProductSupplier::getIdByProductAndSupplier($this->id, $id_product_attribute, $id_supplier);

            $product_supplier = new ProductSupplier($id_product_supplier);

            if (!$id_product_supplier) {
                $product_supplier->id_product = (int) $this->id;
                $product_supplier->id_product_attribute = (int) $id_product_attribute;
                $product_supplier->id_supplier = (int) $id_supplier;
            }

            $product_supplier->product_supplier_reference = pSQL($supplier_reference);
            $product_supplier->product_supplier_price_te = null !== $price ? (float) $price : (float) $product_supplier->product_supplier_price_te;
            $product_supplier->id_currency = null !== $id_currency ? (int) $id_currency : (int) $product_supplier->id_currency;
            $product_supplier->save();
        }
    }

    /**
     * Update a product attribute.
     *
     * @param int $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 int $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
     * @param string $isbn ISBN reference
     * @param int|null $low_stock_threshold Low stock alert
     * @param bool $low_stock_alert send email on low stock
     * @param string $mpn MPN
     *
     * @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 = [],
        $isbn = '',
        $low_stock_threshold = null,
        $low_stock_alert = false,
        $mpn = null
    ) {
        $combination = new Combination($id_product_attribute);

        if (!$update_all_fields) {
            $combination->setFieldsToUpdate([
                'price' => null !== $price,
                'wholesale_price' => null !== $wholesale_price,
                'ecotax' => null !== $ecotax,
                'weight' => null !== $weight,
                'unit_price_impact' => null !== $unit,
                'default_on' => null !== $default,
                'minimal_quantity' => null !== $minimal_quantity,
                'available_date' => 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->isbn = pSQL($isbn);
        $combination->upc = pSQL($upc);
        $combination->mpn = pSQL($mpn);
        $combination->default_on = (int) $default;
        $combination->minimal_quantity = (int) $minimal_quantity;
        $combination->low_stock_threshold = empty($low_stock_threshold) && '0' != $low_stock_threshold ? null : (int) $low_stock_threshold;
        $combination->low_stock_alert = !empty($low_stock_alert);
        $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 (is_array($id_images) && count($id_images)) {
            $combination->setImages($id_images);
        }

        $id_default_attribute = (int) Product::updateDefaultAttribute($this->id);
        if ($id_default_attribute) {
            $this->cache_default_attribute = $id_default_attribute;
        }

        // Sync stock Reference, EAN13, ISBN, MPN and UPC for this attribute
        if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && StockAvailable::dependsOnStock($this->id, Context::getContext()->shop->id)) {
            Db::getInstance()->update('stock', [
                'reference' => pSQL($reference),
                'ean13' => pSQL($ean13),
                'isbn' => pSQL($isbn),
                'upc' => pSQL($upc),
                'mpn' => pSQL($mpn),
            ], 'id_product = ' . $this->id . ' AND id_product_attribute = ' . (int) $id_product_attribute);
        }

        Hook::exec('actionProductAttributeUpdate', ['id_product_attribute' => (int) $id_product_attribute]);
        Tools::clearColorListCache($this->id);

        return true;
    }

    /**
     * Add a product attribute.
     *
     * @since 1501
     *
     * @param float $price Additional price
     * @param float $weight Additional weight
     * @param float $ecotax Additional ecotax
     * @param int $id_images Image ids
     * @param string $reference Reference
     * @param string $location Location
     * @param string $ean13 Ean-13 barcode
     * @param bool $default Is default attribute for product
     * @param int $minimal_quantity Minimal quantity to add to cart
     * @param string $isbn ISBN reference
     * @param int|null $low_stock Low stock alert
     *
     * @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 = [],
        $available_date = null,
        $quantity = 0,
        $isbn = '',
        $low_stock_threshold = null,
        $low_stock_alert = false,
        $mpn = null
    ) {
        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 = (int) $quantity;
        $combination->weight = (float) $weight;
        $combination->unit_price_impact = (float) $unit_impact;
        $combination->reference = pSQL($reference);
        $combination->location = pSQL($location);
        $combination->ean13 = pSQL($ean13);
        $combination->isbn = pSQL($isbn);
        $combination->upc = pSQL($upc);
        $combination->mpn = pSQL($mpn);
        $combination->default_on = (int) $default;
        $combination->minimal_quantity = (int) $minimal_quantity;
        $combination->low_stock_threshold = empty($low_stock_threshold) && '0' != $low_stock_threshold ? null : (int) $low_stock_threshold;
        $combination->low_stock_alert = !empty($low_stock_alert);
        $combination->available_date = $available_date;

        if (count($id_shop_list)) {
            $combination->id_shop_list = array_unique($id_shop_list);
        }

        $combination->add();

        if (!$combination->id) {
            return false;
        }

        $total_quantity = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
            '
            SELECT SUM(quantity) as quantity
            FROM ' . _DB_PREFIX_ . 'stock_available
            WHERE id_product = ' . (int) $this->id . '
            AND id_product_attribute <> 0 '
        );

        if (!$total_quantity) {
            Db::getInstance()->update('stock_available', ['quantity' => 0], '`id_product` = ' . $this->id);
        }

        $id_default_attribute = Product::updateDefaultAttribute($this->id);

        if ($id_default_attribute) {
            $this->cache_default_attribute = $id_default_attribute;
            if (!$combination->available_date) {
                $this->setAvailableDate();
            }
        }

        if (!empty($id_images)) {
            $combination->setImages($id_images);
        }

        Tools::clearColorListCache($this->id);

        if (Configuration::get('PS_DEFAULT_WAREHOUSE_NEW_PRODUCT') != 0 && Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')) {
            $warehouse_location_entity = new WarehouseProductLocation();
            $warehouse_location_entity->id_product = $this->id;
            $warehouse_location_entity->id_product_attribute = (int) $combination->id;
            $warehouse_location_entity->id_warehouse = Configuration::get('PS_DEFAULT_WAREHOUSE_NEW_PRODUCT');
            $warehouse_location_entity->location = pSQL('');
            $warehouse_location_entity->save();
        }

        return (int) $combination->id;
    }

    /**
     * @deprecated since 150
     */
    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', ['id_product_attribute' => 0, 'id_product' => (int) $this->id, 'deleteAllAttributes' => true]);

        $result = true;
        $combinations = new PrestaShopCollection('Combination');
        $combinations->where('id_product', '=', $this->id);
        foreach ($combinations as $combination) {
            $result &= $combination->delete();
        }
        SpecificPriceRule::applyAllRules([(int) $this->id]);
        Tools::clearColorListCache($this->id);

        return $result;
    }

    /**
     * Delete product attributes impacts.
     *
     * @return bool
     */
    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([(int) $this->id]);

        return $this->deleteFeatures();
    }

    public static function updateCacheAttachment($id_product)
    {
        $value = (bool) Db::getInstance()->getValue('
                                SELECT id_attachment
                                FROM ' . _DB_PREFIX_ . 'product_attachment
                                WHERE id_product=' . (int) $id_product);

        return Db::getInstance()->update(
                        'product',
                        ['cache_has_attachments' => (int) $value],
                        'id_product = ' . (int) $id_product
                    );
    }

    /**
     * Delete product attachments.
     *
     * @param bool $update_cache If set to true attachment cache will be updated
     *
     * @return array Deletion result
     */
    public function deleteAttachments($update_attachment_cache = true)
    {
        $res = Db::getInstance()->execute(
            '
            DELETE FROM `' . _DB_PREFIX_ . 'product_attachment`
            WHERE `id_product` = ' . (int) $this->id
        );

        if (isset($update_attachment_cache) && (bool) $update_attachment_cache === true) {
            Product::updateCacheAttachment((int) $this->id);
        }

        return $res;
    }

    /**
     * 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 `' . _DB_PREFIX_ . 'customization_field_lang` FROM `' . _DB_PREFIX_ . 'customization_field_lang` LEFT JOIN `' . _DB_PREFIX_ . 'customization_field`
                ON (' . _DB_PREFIX_ . 'customization_fieldid_customization_field = ' . _DB_PREFIX_ . 'customization_field_langid_customization_field)
                WHERE ' . _DB_PREFIX_ . 'customization_fieldid_customization_field IS NULL'
            );
    }

    /**
     * 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 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 sw FROM `' . _DB_PREFIX_ . 'search_word` sw
                LEFT JOIN `' . _DB_PREFIX_ . 'search_index` si ON (swid_word=siid_word)
                WHERE siid_word IS NULL;'
            );
    }

    /**
     * Add a product attributes combinaison.
     *
     * @param int $id_product_attribute Product attribute id
     * @param array $attributes Attributes to forge combinaison
     *
     * @return array Insertion result
     *
     * @deprecated since 1507
     */
    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);
    }

    /**
     * @deprecated 1550
     *
     * @param $id_attributes
     * @param $combinations
     *
     * @return bool
     *
     * @throws PrestaShopDatabaseException
     */
    public function addAttributeCombinationMultiple($id_attributes, $combinations)
    {
        Tools::displayAsDeprecated();
        $attributes_list = [];
        foreach ($id_attributes as $nb => $id_product_attribute) {
            if (isset($combinations[$nb])) {
                foreach ($combinations[$nb] as $id_attribute) {
                    $attributes_list[] = [
                        '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 int $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',
            [
                'id_product_attribute' => $id_product_attribute,
                'id_product' => $this->id,
                'deleteAllAttributes' => false,
            ]
        );

        $combination = new Combination($id_product_attribute);
        $res = $combination->delete();
        SpecificPriceRule::applyAllRules([(int) $this->id]);

        return $res;
    }

    /**
     * Delete features.
     */
    public function deleteFeatures()
    {
        $all_shops = Context::getContext()->shop->getContext() == Shop::CONTEXT_ALL ? true : false;

        // 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`)
            ' . (!$all_shops ? 'LEFT JOIN `' . _DB_PREFIX_ . 'feature_shop` fs ON (f.`id_feature` = fs.`id_feature`)' : null) . '
            WHERE `id_product` = ' . (int) $this->id
                . (!$all_shops ? ' AND fs.`id_shop` = ' . (int) Context::getContext()->shop->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 `' . _DB_PREFIX_ . 'feature_product` FROM `' . _DB_PREFIX_ . 'feature_product`
            WHERE `id_product` = ' . (int) $this->id . (!$all_shops ? '
                AND `id_feature` IN (
                    SELECT `id_feature`
                    FROM `' . _DB_PREFIX_ . 'feature_shop`
                    WHERE `id_shop` = ' . (int) Context::getContext()->shop->id . '
                )' : ''));

        SpecificPriceRule::applyAllRules([(int) $this->id]);

        return $result;
    }

    /**
     * Get all available product attributes resume.
     *
     * @param int $id_lang Language id
     *
     * @return array Product attributes combinations
     */
    public function getAttributesResume($id_lang, $attribute_value_separator = ' - ', $attribute_separator = ', ')
    {
        if (!Combination::isFeatureActive()) {
            return [];
        }

        $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`');

        if (!$combinations) {
            return false;
        }

        $product_attributes = [];
        foreach ($combinations as $combination) {
            $product_attributes[] = (int) $combination['id_product_attribute'];
        }

        $lang = Db::getInstance()->executeS('SELECT pacid_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 pacid_product_attribute IN (' . implode(',', $product_attributes) . ')
                GROUP BY pacid_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)) {
                $result = StockAvailable::getQuantityAvailableByProduct($row['id_product'], $row['id_product_attribute']);
                Cache::store(
                    $cache_key,
                    $result
                );
                $combinations[$key]['quantity'] = $result;
            } else {
                $combinations[$key]['quantity'] = Cache::retrieve($cache_key);
            }
        }

        return $combinations;
    }

    /**
     * Get all available product attributes combinations.
     *
     * @param int $id_lang Language id
     * @param bool $groupByIdAttributeGroup
     *
     * @return array Product attributes combinations
     */
    public function getAttributeCombinations($id_lang = null, $groupByIdAttributeGroup = true)
    {
        if (!Combination::isFeatureActive()) {
            return [];
        }
        if (null === $id_lang) {
            $id_lang = Context::getContext()->language->id;
        }

        $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`
                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`' . ($groupByIdAttributeGroup ? ',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 int $id_product_attribute
     * @param int $id_lang Language id
     *
     * @return array Product attribute combination by id_product_attribute
     */
    public function getAttributeCombinationsById($id_product_attribute, $id_lang, $groupByIdAttributeGroup = true)
    {
        if (!Combination::isFeatureActive()) {
            return [];
        }
        $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`, a.`position`
                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`' . ($groupByIdAttributeGroup ? ',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)) {
                $result = StockAvailable::getQuantityAvailableByProduct($row['id_product'], $row['id_product_attribute']);
                Cache::store(
                    $cache_key,
                    $result
                );
                $res[$key]['quantity'] = $result;
            } else {
                $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 = [];

        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 = [];

        foreach ($result as $row) {
            $images[$row['id_product_attribute']][] = $row;
        }

        return $images;
    }

    public static function getCombinationImageById($id_product_attribute, $id_lang)
    {
        if (!Combination::isFeatureActive() || !$id_product_attribute) {
            return false;
        }

        $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` = ' . (int) $id_product_attribute . ' AND il.`id_lang` = ' . (int) $id_lang . ' ORDER by i.`position` LIMIT 1'
        );

        if (!$result) {
            return false;
        }

        return "=";
    }

    /**
     * Check if product has attributes combinations.
     *
     * @return int 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 int $id_lang Language id
     * @param int $pageNumber Start from (optional)
     * @param int $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, 
        if (!$context) {
            $context = Context::getContext();
        }

        $front = true;
        if (!in_array($context->controller->controller_type, ['front', 'modulefront'])) {
            $front = false;
        }

        if ($page_number < 1) {
            $page_number = 1;
        }
        if ($nb_products < 1) {
            $nb_products = 10;
        }
        if (empty($order_by) || 
        }
        if (empty($order_way)) {
            $order_way = 'DESC';
        }
        if (
        } elseif (
        }
        if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay($order_way)) {
            die(Tools::displayError());
        }

        $sql_groups = '';
        if (Group::isFeatureActive()) {
            $groups = FrontController::getCurrentCustomerGroups();
            $sql_groups = ' AND EXISTS(SELECT 1 FROM `' . _DB_PREFIX_ . 'category_product` cp
            JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cpid_category = cgid_category AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id) . ')
            WHERE cp.`id_product` = p.`id_product`)';
        }

        if (strpos($order_by, ) > 0) {
            
            $order_by_prefix = "=";
            
        }

        $nb_days_new_product = (int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT');

        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 product_shop.`date_add` > "' . date('Y-m-d', strtotime('-' . $nb_days_new_product . ' DAY')) . '"
                    ' . ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . '
                    ' . $sql_groups;

            return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql);
        }
        $sql = new DbQuery();
        $sql->select(
            'p.*, product_shop.*, stockout_of_stock, IFNULL(stockquantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`link_rewrite`, pl.`meta_description`,
            pl.`meta_keywords`, pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`, image_shop.`id_image` id_image, il.`legend`, m.`name` AS manufacturer_name,
            (DATEDIFF(product_shop.`date_add`,
                DATE_SUB(
                    "' . $now . '",
                    INTERVAL ' . $nb_days_new_product . ' DAY
                )
            ) > 0) as new'
        );

        $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_langShop::addSqlRestrictionOnLang('pl')
        );
        $sql->leftJoin('image_shop', 'image_shop', 'image_shop.`id_product` = p.`id_product` AND image_shopcover=1 AND image_shopid_shop=' . (int) $context->shop->id);
        $sql->leftJoin('image_lang', 'il', 'image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $id_lang);
        $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('product_shop.`date_add` > "' . date('Y-m-d', strtotime('-' . $nb_days_new_product . ' DAY')) . '"');
        if (Group::isFeatureActive()) {
            $groups = FrontController::getCurrentCustomerGroups();
            $sql->where('EXISTS(SELECT 1 FROM `' . _DB_PREFIX_ . 'category_product` cp
            JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cpid_category = cgid_category AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id) . ')
            WHERE cp.`id_product` = p.`id_product`)');
        }

        $sql->orderBy((isset($order_by_prefix) ? pSQL($order_by_prefix) .  : '') . '`' . pSQL($order_by) . '` ' . pSQL($order_way));
        $sql->limit($nb_products, (int) (($page_number - 1) * $nb_products));

        if (Combination::isFeatureActive()) {
            $sql->select('product_attribute_shopminimal_quantity AS product_attribute_minimal_quantity, IFNULL(product_attribute_shopid_product_attribute,0) id_product_attribute');
            $sql->leftJoin('product_attribute_shop', 'product_attribute_shop', 'p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shopid_shop=' . (int) $context->shop->id);
        }
        $sql->join(Product::sqlStock('p', 0));

        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);

        if (!$result) {
            return false;
        }

        if (
        }
        $products_ids = [];
        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 = $ids['id_country'] ? (int) $ids['id_country'] : (int) 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 int $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, ['front', 'modulefront'])) {
            $front = false;
        }

        $current_date = date('Y-m-d H:i:00');
        $product_reductions = Product::_getProductIdByDate((!$beginning ? $current_date : $beginning), (!$ending ? $current_date : $ending), $context, true);

        if ($product_reductions) {
            $ids_products = '';
            foreach ($product_reductions as $product_reduction) {
                $ids_products .= '(' . (int) $product_reduction['id_product'] . ',' . ($product_reduction['id_product_attribute'] ? (int) $product_reduction['id_product_attribute'] : '0') . '),';
            }

            $ids_products = rtrim($ids_products, ',');
            Db::getInstance()->execute('CREATE TEMPORARY TABLE `' . _DB_PREFIX_ . 'product_reductions` (id_product INT UNSIGNED NOT NULL DEFAULT 0, id_product_attribute INT UNSIGNED NOT NULL DEFAULT 0) ENGINE=MEMORY', false);
            if ($ids_products) {
                Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'product_reductions` VALUES ' . $ids_products, false);
            }

            $groups = FrontController::getCurrentCustomerGroups();
            $sql_groups = ' AND EXISTS(SELECT 1 FROM `' . _DB_PREFIX_ . 'category_product` cp
            JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cpid_category = cgid_category AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id) . ')
            WHERE cp.`id_product` = p.`id_product`)';

            // Please keep 2 distinct queries because RAND() is an awful way to achieve this result
            $sql = 'SELECT product_shopid_product, IFNULL(product_attribute_shopid_product_attribute,0) id_product_attribute
                    FROM
                    `' . _DB_PREFIX_ . 'product_reductions` pr,
                    `' . _DB_PREFIX_ . 'product` p
                    ' . Shop::addSqlAssociation('product', 'p') . '
                    LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` product_attribute_shop
                        ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shopid_shop=' . (int) $context->shop->id . ')
                    WHERE pid_product=prid_product AND (prid_product_attribute = 0 OR product_attribute_shopid_product_attribute = prid_product_attribute) AND product_shop.`active` = 1
                        ' . $sql_groups . '
                    ' . ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . '
                    ORDER BY RAND()';

            $result = Db::getInstance()->getRow($sql);

            Db::getInstance()->execute('DROP TEMPORARY TABLE `' . _DB_PREFIX_ . 'product_reductions`', false);

            if (!$id_product = $result['id_product']) {
                return false;
            }

            // no group by needed : there's only one attribute with cover=1 for a given id_product + shop
            $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`, pl.`available_now`, pl.`available_later`,
                        p.`ean13`, p.`isbn`, p.`upc`, p.`mpn`, image_shop.`id_image` id_image, il.`legend`,
                        DATEDIFF(product_shop.`date_add`, DATE_SUB("' . date('Y-m-d') . ' 00:00:00",
                        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
                    LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (
                        p.`id_product` = pl.`id_product`
                        AND pl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl') . '
                    )
                    ' . Shop::addSqlAssociation('product', 'p') . '
                    LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
                        ON (image_shop.`id_product` = p.`id_product` AND image_shopcover=1 AND image_shopid_shop=' . (int) $context->shop->id . ')
                    LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $id_lang . ')
                    ' . Product::sqlStock('p', 0) . '
                    WHERE pid_product = ' . (int) $id_product;

            $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql);
            if (!$row) {
                return false;
            }

            $row['id_product_attribute'] = (int) $result['id_product_attribute'];

            return Product::getProductProperties($id_lang, $row);
        } else {
            return false;
        }
    }

    /**
     * Get prices drop.
     *
     * @param int $id_lang Language id
     * @param int $pageNumber Start from (optional)
     * @param int $nbProducts Number of products to return (optional)
     * @param bool $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,
        
        }

        if (!$context) {
            $context = Context::getContext();
        }
        if ($page_number < 1) {
            $page_number = 1;
        }
        if ($nb_products < 1) {
            $nb_products = 10;
        }
        if (empty($order_by) || 
        }
        if (empty($order_way)) {
            $order_way = 'DESC';
        }
        if (
        } elseif (
        }
        if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay($order_way)) {
            die(Tools::displayError());
        }
        $current_date = date('Y-m-d H:i:00');
        $ids_product = Product::_getProductIdByDate((!$beginning ? $current_date : $beginning), (!$ending ? $current_date : $ending), $context);

        $tab_id_product = [];
        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, ['front', 'modulefront'])) {
            $front = false;
        }

        $sql_groups = '';
        if (Group::isFeatureActive()) {
            $groups = FrontController::getCurrentCustomerGroups();
            $sql_groups = ' AND EXISTS(SELECT 1 FROM `' . _DB_PREFIX_ . 'category_product` cp
            JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cpid_category = cgid_category AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id) . ')
            WHERE cp.`id_product` = p.`id_product`)';
        }

        if ($count) {
            return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
            SELECT COUNT(DISTINCT p.`id_product`)
            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) . ')' : '') . '
            ' . $sql_groups);
        }

        if (strpos($order_by, ) > 0) {
            
            
        }

        $sql = '
        SELECT
            p.*, product_shop.*, stockout_of_stock, IFNULL(stockquantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`available_now`, pl.`available_later`,
            IFNULL(product_attribute_shopid_product_attribute, 0) id_product_attribute,
            pl.`link_rewrite`, pl.`meta_description`, pl.`meta_keywords`, pl.`meta_title`,
            pl.`name`, image_shop.`id_image` id_image, il.`legend`, m.`name` AS manufacturer_name,
            DATEDIFF(
                p.`date_add`,
                DATE_SUB(
                    "' . date('Y-m-d') . ' 00:00:00",
                    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') . '
        LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` product_attribute_shop
            ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shopid_shop=' . (int) $context->shop->id . ')
        ' . 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_langShop::addSqlRestrictionOnLang('pl') . '
        )
        LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
            ON (image_shop.`id_product` = p.`id_product` AND image_shopcover=1 AND image_shopid_shop=' . (int) $context->shop->id . ')
        LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $id_lang . ')
        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 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) . ')' : '') . '
        ' . $sql_groups . '
        ORDER BY ' . (isset($order_by_prefix) ? pSQL($order_by_prefix) .  : '') . pSQL($order_by) . ' ' . pSQL($order_way) . '
        LIMIT ' . (int) (($page_number - 1) * $nb_products) . ', ' . (int) $nb_products;

        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);

        if (!$result) {
            return false;
        }

        if (
        }

        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 = '')
    {
        $cache_id = 'Product::getProductCategories_' . (int) $id_product;
        if (!Cache::isStored($cache_id)) {
            $ret = [];

            $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'];
                }
            }
            Cache::store($cache_id, $ret);

            return $ret;
        }

        return Cache::retrieve($cache_id);
    }

    public static function getProductCategoriesFull($id_product = '', $id_lang = null)
    {
        if (!$id_lang) {
            $id_lang = Context::getContext()->language->id;
        }

        $ret = [];
        $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 (cid_category = cpid_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 = [];

        foreach ($carrier_list as $carrier) {
            $data[] = [
                '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
        );

        $unique_array = [];
        foreach ($data as $sub_array) {
            if (!in_array($sub_array, $unique_array)) {
                $unique_array[] = $sub_array;
            }
        }

        if (count($unique_array)) {
            Db::getInstance()->insert('product_carrier', $unique_array, false, true, Db::INSERT_IGNORE);
        }
    }

    /**
     * Get product images and legends.
     *
     * @param int $id_lang Language id for multilingual legends
     *
     * @return array Product images and legends
     */
    public function getImages($id_lang, Context $context = null)
    {
        return Db::getInstance()->executeS(
            '
            SELECT image_shop.`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`'
        );
    }

    /**
     * Get product cover image.
     *
     * @return array Product cover image
     */
    public static function getCover($id_product, Context $context = null)
    {
        if (!$context) {
            $context = Context::getContext();
        }
        $cache_id = 'Product::getCover_' . (int) $id_product . '-' . (int) $context->shop->id;
        if (!Cache::isStored($cache_id)) {
            $sql = 'SELECT image_shop.`id_image`
                    FROM `' . _DB_PREFIX_ . 'image` i
                    ' . Shop::addSqlAssociation('image', 'i') . '
                    WHERE i.`id_product` = ' . (int) $id_product . '
                    AND image_shop.`cover` = 1';
            $result = Db::getInstance()->getRow($sql);
            Cache::store($cache_id, $result);

            return $result;
        }

        return Cache::retrieve($cache_id);
    }

    /**
     * Returns product price.
     *
     * @param int $id_product Product id
     * @param bool $usetax With taxes or not (optional)
     * @param int|null $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 int $decimals Number of decimals (optional)
     * @param int|null $divisor Useful when paying many time without fees (optional)
     * @param bool $only_reduc Returns only the reduction amount
     * @param bool $usereduc Set if the returned amount will include reduction
     * @param int $quantity Required for quantity discount application (default value: 1)
     * @param bool $force_associated_tax DEPRECATED - NOT USED Force to apply the associated tax.
     *                                   Only works when the parameter $usetax is true
     * @param int|null $id_customer Customer ID (for customer group reduction)
     * @param int|null $id_cart Cart IDRequired when the cookie is not accessible
     *                          (eg., inside a payment module, a cron task...)
     * @param int|null $id_address Customer address IDRequired for price (tax included)
     *                             calculation regarding the guest localization
     * @param null $specific_price_output If a specific price applies regarding the previous parameters,
     *                                    this variable is filled with the corresponding SpecificPrice object
     * @param bool $with_ecotax insert ecotax in price output
     * @param bool $use_group_reduction
     * @param Context $context
     * @param bool $use_customer_price
     *
     * @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,
        $id_customization = null
    ) {
        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 = null;
        if ($id_customer) {
            $id_group = Customer::getDefaultGroupId((int) $id_customer);
        }
        if (!$id_group) {
            $id_group = (int) Group::getCurrent()->id;
        }

        // 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 (eg., guest, customer, Google...) is on PrestaShop, he has already its cart as the global (see /initphp)
            * When a non-user calls directly this method (eg., 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) {
            $cache_id = 'Product::getPriceStatic_' . (int) $id_product . '-' . (int) $id_cart;
            if (!Cache::isStored($cache_id) || ($cart_quantity = Cache::retrieve($cache_id) != (int) $quantity)) {
                $sql = 'SELECT SUM(`quantity`)
                FROM `' . _DB_PREFIX_ . 'cart_product`
                WHERE `id_product` = ' . (int) $id_product . '
                AND `id_cart` = ' . (int) $id_cart;
                $cart_quantity = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql);
                Cache::store($cache_id, $cart_quantity);
            } else {
                $cart_quantity = Cache::retrieve($cache_id);
            }
        }

        $id_currency = Validate::isLoadedObject($context->currency) ? (int) $context->currency->id : (int) Configuration::get('PS_CURRENCY_DEFAULT');

        if (!$id_address && Validate::isLoadedObject($cur_cart)) {
            $id_address = $cur_cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')};
        }

        // retrieve address informations
        $address = Address::initialize($id_address, true);
        $id_country = (int) $address->id_country;
        $id_state = (int) $address->id_state;
        $zipcode = $address->postcode;

        if (Tax::excludeTaxeOption()) {
            $usetax = false;
        }

        if ($usetax != false
            && !empty($address->vat_number)
            && $address->id_country != Configuration::get('VATNUMBER_COUNTRY')
            && Configuration::get('VATNUMBER_MANAGEMENT')) {
            $usetax = false;
        }

        if (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,
            $quantity,
            $usetax,
            $decimals,
            $only_reduc,
            $usereduc,
            $with_ecotax,
            $specific_price_output,
            $use_group_reduction,
            $id_customer,
            $use_customer_price,
            $id_cart,
            $cart_quantity,
            $id_customization
        );

        return $return;
    }

    /**
     * Price calculation / Get product price.
     *
     * @param int $id_shop Shop id
     * @param int $id_product Product id
     * @param int $id_product_attribute Product attribute id
     * @param int $id_country Country id
     * @param int $id_state State id
     * @param string $zipcode
     * @param int $id_currency Currency id
     * @param int $id_group Group id
     * @param int $quantity Quantity Required for Specific prices : quantity discount application
     * @param bool $use_tax with (1) or without (0) tax
     * @param int $decimals Number of decimals returned
     * @param bool $only_reduc Returns only the reduction amount
     * @param bool $use_reduc Set if the returned amount will include reduction
     * @param bool $with_ecotax insert ecotax in price output
     * @param null $specific_price If a specific price applies regarding the previous parameters,
     *                             this variable is filled with the corresponding SpecificPrice object
     * @param bool $use_group_reduction
     * @param int $id_customer
     * @param bool $use_customer_price
     * @param int $id_cart
     * @param int $real_quantity
     *
     * @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,
        $id_customization = 0
    ) {
        static $address = null;
        static $context = null;

        if ($context == null) {
            $context = Context::getContext()->cloneContext();
        }

        if ($address === null) {
            if (is_object($context->cart) && $context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')} != null) {
                $id_address = $context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')};
                $address = new Address($id_address);
            } else {
                $address = new Address();
            }
        }

        if ($id_shop !== null && $context->shop->id != (int) $id_shop) {
            $context->shop = new Shop((int) $id_shop);
        }

        if (!$use_customer_price) {
            $id_customer = 0;
        }

        if ($id_product_attribute === null) {
            $id_product_attribute = Product::getDefaultAttribute($id_product);
        }

        $cache_id = (int) $id_product . '-' . (int) $id_shop . '-' . (int) $id_currency . '-' . (int) $id_country . '-' . $id_state . '-' . $zipcode . '-' . (int) $id_group .
            '-' . (int) $quantity . '-' . (int) $id_product_attribute . '-' . (int) $id_customization .
            '-' . (int) $with_ecotax . '-' . (int) $id_customer . '-' . (int) $use_group_reduction . '-' . (int) $id_cart . '-' . (int) $real_quantity .
            '-' . ($only_reduc ? '1' : '0') . '-' . ($use_reduc ? '1' : '0') . '-' . ($use_tax ? '1' : '0') . '-' . (int) $decimals;

        // 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 . '-' . $id_shop;
        if (!isset(self::$_pricesLevel2[$cache_id_2])) {
            $sql = new DbQuery();
            $sql->select('product_shop.`price`, product_shop.`ecotax`');
            $sql->from('product', 'p');
            $sql->innerJoin('product_shop', 'product_shop', '(product_shopid_product=pid_product AND product_shopid_shop = ' . (int) $id_shop . ')');
            $sql->where('p.`id_product` = ' . (int) $id_product);
            if (Combination::isFeatureActive()) {
                $sql->select('IFNULL(product_attribute_shopid_product_attribute,0) id_product_attribute, product_attribute_shop.`price` AS attribute_price, product_attribute_shopdefault_on');
                $sql->leftJoin('product_attribute_shop', 'product_attribute_shop', '(product_attribute_shopid_product = pid_product AND product_attribute_shopid_shop = ' . (int) $id_shop . ')');
            } else {
                $sql->select('0 as id_product_attribute');
            }

            $res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);

            if (is_array($res) && count($res)) {
                foreach ($res as $row) {
                    $array_tmp = [
                        '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'] &&
                $id_currency !== $specific_price['id_currency']
            )
        ) {
            $price = Tools::convertPrice($price, $id_currency);

            if (isset($specific_price['price']) && $specific_price['price'] >= 0) {
                $specific_price['price'] = $price;
            }
        }

        // 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 you want the default combination, please use NULL value instead
            if ($id_product_attribute !== false) {
                $price += $attribute_price;
            }
        }

        // Customization price
        if ((int) $id_customization) {
            $price += Tools::convertPrice(Customization::getCustomizationPrice($id_customization), $id_currency);
        }

        // Tax
        $address->id_country = $id_country;
        $address->id_state = $id_state;
        $address->postcode = $zipcode;

        $tax_manager = TaxManagerFactory::getManager($address, Product::getIdTaxRulesGroupByIdProduct((int) $id_product, $context));
        $product_tax_calculator = $tax_manager->getTaxCalculator();

        // Add Tax
        if ($use_tax) {
            $price = $product_tax_calculator->addTaxes($price);
        }

        // 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) {
                static $psEcotaxTaxRulesGroupId = null;
                if ($psEcotaxTaxRulesGroupId === null) {
                    $psEcotaxTaxRulesGroupId = (int) Configuration::get('PS_ECOTAX_TAX_RULES_GROUP_ID');
                }
                // reinit the tax manager for ecotax handling
                $tax_manager = TaxManagerFactory::getManager(
                    $address,
                    $psEcotaxTaxRulesGroupId
                );
                $ecotax_tax_calculator = $tax_manager->getTaxCalculator();
                $price += $ecotax_tax_calculator->addTaxes($ecotax);
            } else {
                $price += $ecotax;
            }
        }

        // Reduction
        $specific_price_reduction = 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);
                }

                $specific_price_reduction = $reduction_amount;

                // Adjust taxes if required

                if (!$use_tax && $specific_price['reduction_tax']) {
                    $specific_price_reduction = $product_tax_calculator->removeTaxes($specific_price_reduction);
                }
                if ($use_tax && !$specific_price['reduction_tax']) {
                    $specific_price_reduction = $product_tax_calculator->addTaxes($specific_price_reduction);
                }
            } else {
                $specific_price_reduction = $price * $specific_price['reduction'];
            }
        }

        if ($use_reduc) {
            $price -= $specific_price_reduction;
        }

        // Group reduction
        if ($use_group_reduction) {
            $reduction_from_category = GroupReduction::getValueForProduct($id_product, $id_group);
            if ($reduction_from_category !== false) {
                $group_reduction = $price * (float) $reduction_from_category;
            } else { // apply group reduction if there is no group reduction for this category
                $group_reduction = (($reduc = Group::getReductionByIdGroup($id_group)) != 0) ? ($price * $reduc / 100) : 0;
            }

            $price -= $group_reduction;
        }

        if ($only_reduc) {
            return Tools::ps_round($specific_price_reduction, $decimals);
        }

        $price = Tools::ps_round($price, $decimals);

        if ($price < 0) {
            $price = 0;
        }

        self::$_prices[$cache_id] = $price;

        return self::$_prices[$cache_id];
    }

    /**
     * @param int $orderId
     * @param int $productId
     * @param int $combinationId
     * @param bool $withTaxes
     * @param bool $useReduction
     * @param bool $withEcoTax
     *
     * @return float|null
     *
     * @throws PrestaShopDatabaseException
     */
    public static function getPriceFromOrder(
        int $orderId,
        int $productId,
        int $combinationId,
        bool $withTaxes,
        bool $useReduction,
        bool $withEcoTax
    ): ?float {
        $sql = new DbQuery();
        $sql->select('od.*, trate AS tax_rate');
        $sql->from('order_detail', 'od');
        $sql->where('od.`id_order` = ' . $orderId);
        $sql->where('od.`product_id` = ' . $productId);
        if (Combination::isFeatureActive()) {
            $sql->where('od.`product_attribute_id` = ' . $combinationId);
        }
        $sql->leftJoin('order_detail_tax', 'odt', 'odtid_order_detail = odid_order_detail');
        $sql->leftJoin('tax', 't', 'tid_tax = odtid_tax');
        $res = Db::getInstance((bool) _PS_USE_SQL_SLAVE_)->executeS($sql);
        if (!is_array($res) || empty($res)) {
            return null;
        }

        $orderDetail = "=";
        if ($useReduction) {
            // If we want price with reduction it is already the one stored in OrderDetail
            $price = $withTaxes ? $orderDetail['unit_price_tax_incl'] : $orderDetail['unit_price_tax_excl'];
        } else {
            // Without reduction we use the original product price to compute the original price
            $tax_rate = $withTaxes ? (1 + ($orderDetail['tax_rate'] / 100)) : 1;
            $price = $orderDetail['original_product_price'] * $tax_rate;
        }
        $ecoTaxValue = 0;
        if ($withEcoTax) {
            $ecoTaxValue = $withTaxes ? $orderDetail['ecotax'] * (1 + $orderDetail['ecotax_tax_rate']) : $orderDetail['ecotax'];
        }
        $price += $ecoTaxValue;

        return $price;
    }

    public static function convertAndFormatPrice($price, $currency = false, Context $context = null)
    {
        if (!$context) {
            $context = Context::getContext();
        }
        if (!$currency) {
            $currency = $context->currency;
        }

        return $context->getCurrentLocale()->formatPrice(Tools::convertPrice($price, $currency), $currency->iso_code);
    }

    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 = $ids['id_country'] ? (int) $ids['id_country'] : (int) Configuration::get('PS_COUNTRY_DEFAULT');

        return (bool) SpecificPrice::getSpecificPrice((int) $id_product, $context->shop->id, $id_currency, $id_country, $id_group, $quantity, null, 0, 0, $quantity);
    }

    /**
     * Get product price
     * Same as static function getPriceStatic, no need to specify product id.
     *
     * @param bool $tax With taxes or not (optional)
     * @param int $id_product_attribute Product attribute id (optional)
     * @param int $decimals Number of decimals (optional)
     * @param int $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;
        }

        return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
        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');
    }

    public function getDefaultIdProductAttribute()
    {
        if (!Combination::isFeatureActive()) {
            return 0;
        }

        return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
            '
            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_shopdefault_on = 1'
        );
    }

    public function getPriceWithoutReduct($notax = false, $id_product_attribute = null, $decimals = 6)
    {
        return Product::getPriceStatic((int) $this->id, !$notax, $id_product_attribute, $decimals, 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 Context::getContext()->getCurrentLocale()->formatPrice($params['price'], Context::getContext()->currency->iso_code);
    }

    /**
     * Convert price with currency.
     *
     * @param array $params
     * @param object $smarty DEPRECATED
     *
     * @return string Ambigous <string, mixed, Ambigous <number, string>>
     */
    public static function convertPriceWithCurrency($params, &$smarty)
    {
        $currency = $params['currency'];
        $currency = is_object($currency) ? $currency->iso_code : Currency::getIsoCodeById((int) $currency);

        return Tools::getContextLocale(Context::getContext())->formatPrice($params['price'], $currency);
    }

    public static function displayWtPrice($params, &$smarty)
    {
        return Tools::getContextLocale(Context::getContext())->formatPrice($params['p'], Context::getContext()->currency->iso_code);
    }

    /**
     * Display WT price with currency.
     *
     * @param array $params
     * @param Smarty $smarty DEPRECATED
     *
     * @return string Ambigous <string, mixed, Ambigous <number, string>>
     */
    public static function displayWtPriceWithCurrency($params, &$smarty)
    {
        $currency = $params['currency'];
        $currency = is_object($currency) ? $currency->iso_code : Currency::getIsoCodeById((int) $currency);

        return !is_null($params['price']) ? Tools::getContextLocale(Context::getContext())->formatPrice($params['price'], $currency) : null;
    }

    /**
     * Get available product quantities (this method already have decreased products in cart).
     *
     * @param int $idProduct Product id
     * @param int $idProductAttribute Product attribute id (optional)
     * @param bool|null $cacheIsPack
     * @param Cart|null $cart
     * @param int $idCustomization Product customization id (optional)
     *
     * @return int Available quantities
     */
    public static function getQuantity(
        $idProduct,
        $idProductAttribute = null,
        $cacheIsPack = null,
        Cart $cart = null,
        $idCustomization = null
    ) {
        // pack usecase: Pack::getQuantity() returns the pack quantity after cart quantities have been removed from stock
        if (Pack::isPack((int) $idProduct)) {
            return Pack::getQuantity($idProduct, $idProductAttribute, $cacheIsPack, $cart, $idCustomization);
        }
        $availableQuantity = StockAvailable::getQuantityAvailableByProduct($idProduct, $idProductAttribute);
        $nbProductInCart = 0;

        // we don't substract products in cart if the cart is already attached to an order, since stock quantity
        // has already been updated, this is only useful when the order has not yet been created
        if (!empty($cart) && empty(Order::getByCartId($cart->id))) {
            $cartProduct = $cart->getProductQuantity($idProduct, $idProductAttribute, $idCustomization);

            if (!empty($cartProduct['deep_quantity'])) {
                $nbProductInCart = $cartProduct['deep_quantity'];
            }
        }

        // @since 150
        return $availableQuantity - $nbProductInCart;
    }

    /**
     * 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 = null, $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 (stockid_product = `' . bqSQL($product_alias) . '`.id_product';

        if (null !== $product_attribute) {
            if (!Combination::isFeatureActive()) {
                $sql .= ' AND stockid_product_attribute = 0';
            } elseif (is_numeric($product_attribute)) {
                $sql .= ' AND stockid_product_attribute = ' . $product_attribute;
            } elseif (is_string($product_attribute)) {
                $sql .= ' AND stockid_product_attribute = IFNULL(`' . bqSQL($product_attribute) . '`.id_product_attribute, 0)';
            }
        }

        $sql .= StockAvailable::addSqlShopRestriction(null, $id_shop, 'stock') . ' )';

        return $sql;
    }

    /**
     * @deprecated since 150
     *
     * 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)
     * @deprecated 1530
     *
     * @return false
     */
    public static function updateQuantity()
    {
        Tools::displayAsDeprecated();

        return false;
    }

    /**
     * @deprecated since 150
     *
     * It's not possible to use this method with new stockManager and stockAvailable features
     * Now this method do nothing
     * @deprecated 1530
     * @see StockManager if you want to manage real stock
     * @see StockAvailable if you want to manage available quantities for sale on your shop(s)
     *
     * @return false
     */
    public static function reinjectQuantities()
    {
        Tools::displayAsDeprecated();

        return false;
    }

    public static function isAvailableWhenOutOfStock($out_of_stock)
    {
        /** @TODO 150 Update of STOCK_MANAGEMENT & ORDER_OUT_OF_STOCK */
        $ps_stock_management = Configuration::get('PS_STOCK_MANAGEMENT');

        if (!$ps_stock_management) {
            return true;
        }

        $ps_order_out_of_stock = Configuration::get('PS_ORDER_OUT_OF_STOCK');

        return (int) $out_of_stock == 2 ? (int) $ps_order_out_of_stock : (int) $out_of_stock;
    }

    /**
     * Check product availability.
     *
     * @param int $qty Quantity desired
     *
     * @return bool True if product is available with this quantity, false otherwise
     */
    public function checkQty($qty)
    {
        if ($this->isAvailableWhenOutOfStock(StockAvailable::outOfStock($this->id))) {
            return true;
        }
        $id_product_attribute = isset($this->id_product_attribute) ? $this->id_product_attribute : null;
        $availableQuantity = StockAvailable::getQuantityAvailableByProduct($this->id, $id_product_attribute);

        return $qty <= $availableQuantity;
    }

    /**
     * Check if there is no default attribute and create it if not.
     */
    public function checkDefaultAttributes()
    {
        if (!$this->id) {
            return false;
        }

        if (Db::getInstance()->getValue('SELECT COUNT(*)
                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) > Shop::getTotalShops(true)) {
            Db::getInstance()->execute('UPDATE ' . _DB_PREFIX_ . 'product_attribute_shop product_attribute_shop, ' . _DB_PREFIX_ . 'product_attribute pa
                    SET product_attribute_shopdefault_on=NULL, padefault_on = NULL
                    WHERE product_attribute_shopid_product_attribute=paid_product_attribute AND paid_product=' . (int) $this->idShop::addSqlRestriction(false, 'product_attribute_shop'));
        }

        $row = Db::getInstance()->getRow(
            '
            SELECT paid_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(paid_product_attribute) as `id_attr`
        FROM `' . _DB_PREFIX_ . 'product_attribute` pa
            ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
            WHERE pa.`id_product` = ' . (int) $this->id
        );
        if (!$mini) {
            return false;
        }

        if (!ObjectModel::updateMultishopTable('Combination', ['default_on' => 1], 'aid_product_attribute = ' . (int) $mini['id_attr'])) {
            return false;
        }

        return true;
    }

    public static function getAttributesColorList(array $products, $have_stock = true)
    {
        if (!count($products)) {
            return [];
        }

        $id_lang = Context::getContext()->language->id;

        $check_stock = !Configuration::get('PS_DISP_UNAVAILABLE_ATTR');
        if (!$res = Db::getInstance()->executeS(
            '
            SELECT pa.`id_product`, a.`color`, pac.`id_product_attribute`, ' . ($check_stock ? 'SUM(IF(stock.`quantity` > 0, 1, 0))' : '0') . ' qty, a.`id_attribute`, al.`name`, IF(color = "", aid_attribute, color) group_by
            FROM `' . _DB_PREFIX_ . 'product_attribute` pa
            ' . Shop::addSqlAssociation('product_attribute', 'pa') .
            ($check_stock ? Product::sqlStock('pa', 'pa') : '') . '
            JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON (pac.`id_product_attribute` = product_attribute_shop.`id_product_attribute`)
            JOIN `' . _DB_PREFIX_ . 'attribute` a ON (a.`id_attribute` = pac.`id_attribute`)
            JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) $id_lang . ')
            JOIN `' . _DB_PREFIX_ . 'attribute_group` ag ON (aid_attribute_group = ag.`id_attribute_group`)
            WHERE pa.`id_product` IN (' . implode(',', array_map('intval', $products)) . ') AND ag.`is_color_group` = 1
            GROUP BY pa.`id_product`, a.`id_attribute`, `group_by`
            ' . ($check_stock ? 'HAVING qty > 0' : '') . '
            ORDER BY a.`position` ASC;'
            )
        ) {
            return false;
        }

        $colors = [];
        foreach ($res as $row) {
            $row['texture'] = '';

            if (@filemtime(_PS_COL_IMG_DIR_ . $row['id_attribute'] . '.jpg')) {
                $row['texture'] = _THEME_COL_DIR_ . $row['id_attribute'] . '.jpg';
            } elseif (Tools::isEmpty($row['color'])) {
                continue;
            }

            $colors[(int) $row['id_product']][] = ['id_product_attribute' => (int) $row['id_product_attribute'], 'color' => $row['color'], 'texture' => $row['texture'], 'id_product' => $row['id_product'], 'name' => $row['name'], 'id_attribute' => $row['id_attribute']];
        }

        return $colors;
    }

    /**
     * Get all available attribute groups.
     *
     * @param int $id_lang Language id
     *
     * @return array Attribute groups
     */
    public function getAttributesGroups($id_lang)
    {
        if (!Combination::isFeatureActive()) {
            return [];
        }
        $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, product_attribute_shop.`id_product_attribute`,
                    IFNULL(stockquantity, 0) as quantity, product_attribute_shop.`price`, product_attribute_shop.`ecotax`, product_attribute_shop.`weight`,
                    product_attribute_shop.`default_on`, pa.`reference`, product_attribute_shop.`unit_price_impact`,
                    product_attribute_shop.`minimal_quantity`, product_attribute_shop.`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, agl.`name` ASC';

        return Db::getInstance()->executeS($sql);
    }

    /**
     * Delete product accessories.
     * Wrapper to static method deleteAccessories($product_id).
     *
     * @return mixed Deletion result
     */
    public function deleteAccessories()
    {
        return Db::getInstance()->delete('accessory', 'id_product_1 = ' . (int) $this->id);
    }

    /**
     * Delete product from other products accessories.
     *
     * @return mixed Deletion result
     */
    public function deleteFromAccessories()
    {
        return Db::getInstance()->delete('accessory', 'id_product_2 = ' . (int) $this->id);
    }

    /**
     * Get product accessories (only names).
     *
     * @param int $id_lang Language id
     * @param int $id_product Product id
     *
     * @return array Product accessories
     */
    public static function getAccessoriesLight($id_lang, $id_product)
    {
        return Db::getInstance()->executeS(
            '
            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_langShop::addSqlRestrictionOnLang('pl') . '
            )
            WHERE `id_product_1` = ' . (int) $id_product
        );
    }

    /**
     * Get product accessories.
     *
     * @param int $id_lang Language id
     *
     * @return array Product accessories
     */
    public function getAccessories($id_lang, $active = true)
    {
        
        
/*        $sql = 'SELECT p.*, product_shop.*, stockout_of_stock, IFNULL(stockquantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`link_rewrite`,
                    pl.`meta_description`, pl.`meta_keywords`, pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`,
                    image_shop.`id_image` id_image, il.`legend`, m.`name` as manufacturer_name, cl.`name` AS category_default, IFNULL(product_attribute_shopid_product_attribute, 0) id_product_attribute,
                    DATEDIFF(
                        p.`date_add`,
                        DATE_SUB(
                            "' . date('Y-m-d') . ' 00:00:00",
                            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_attribute_shop` product_attribute_shop
                    ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shopid_shop=' . (int) $this->id_shop . ')
                LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (
                    p.`id_product` = pl.`id_product`
                    AND pl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl') . '
                )
                LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl ON (
                    product_shop.`id_category_default` = cl.`id_category`
                    AND cl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('cl') . '
                )
                LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
                    ON (image_shop.`id_product` = p.`id_product` AND image_shopcover=1 AND image_shopid_shop=' . (int) $this->id_shop . ')
                LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`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`)
                ' . Product::sqlStock('p', 0) . '
                WHERE `id_product_1` = ' . (int) $this->id .
                ($active ? ' AND product_shop.`active` = 1 AND product_shop.`visibility` != \'none\'' : '') . '
                GROUP BY product_shopid_product';*/
        

        $sql = 'SELECT p.*, product_shop.*, stockout_of_stock, IFNULL(stockquantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`link_rewrite`,
                    pl.`meta_description`, pl.`meta_keywords`, pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`,
                    image_shop.`id_image` id_image, il.`legend`, m.`name` as manufacturer_name, cl.`name` AS category_default, IFNULL(product_attribute_shopid_product_attribute, 0) id_product_attribute,
                    DATEDIFF(
                        p.`date_add`,
                        DATE_SUB(
                            "' . date('Y-m-d') . ' 00:00:00",
                            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_attribute_shop` product_attribute_shop
                    ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shopid_shop=' . (int) $this->id_shop . ')
                LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (
                    p.`id_product` = pl.`id_product`
                    AND pl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl') . '
                )
                LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl ON (
                    product_shop.`id_category_default` = cl.`id_category`
                    AND cl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('cl') . '
                )
                LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
                    ON (image_shop.`id_product` = p.`id_product` AND image_shopcover=1 AND image_shopid_shop=' . (int) $this->id_shop . ')
                LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`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`)
                ' . Product::sqlStock('p', 0) . '
LEFT JOIN `' . _DB_PREFIX_ . 'feature_product` fp ON (p.`id_product` = fp.`id_product` AND fp.`id_feature` = 208)
LEFT JOIN `' . _DB_PREFIX_ . 'feature_value_lang` fv ON (fp.`id_feature_value` = fv.`id_feature_value` AND fv.`id_lang` = 2)               
                WHERE `id_product_1` = ' . (int) $this->id .
                ($active ? ' AND product_shop.`active` = 1 AND product_shop.`visibility` != \'none\'' : '') . '
                GROUP BY product_shopid_product ORDER BY fvvalue + 0 ASC ';    
        
        
/*$sql = 'SELECT  cpposition, p.*, product_shop.*, stockout_of_stock, IFNULL(stockquantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`link_rewrite`,
                    pl.`meta_description`, pl.`meta_keywords`, pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`, 
                    image_shop.`id_image` id_image, il.`legend`, m.`name` as manufacturer_name, cl.`name` AS category_default, IFNULL(product_attribute_shopid_product_attribute, 0) id_product_attribute,
                    DATEDIFF(
                        p.`date_add`,
                        DATE_SUB(
                            "' . date('Y-m-d') . ' 00:00:00",
                            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_attribute_shop` product_attribute_shop
                    ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shopid_shop=' . (int) $this->id_shop . ')
                LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (
                    p.`id_product` = pl.`id_product`
                    AND pl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl') . '
                )
                LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl ON (
                    product_shop.`id_category_default` = cl.`id_category`
                    AND cl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('cl') . '
                )
                LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
                    ON (image_shop.`id_product` = p.`id_product` AND image_shopcover=1 AND image_shopid_shop=' . (int) $this->id_shop . ')
                LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`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`)
                ' . Product::sqlStock('p', 0) . '
LEFT JOIN `' . _DB_PREFIX_ . 'category_product` cp ON (p.`id_product` = cp.`id_product`)                 
                WHERE `id_product_1` = ' . (int) $this->id .
                ($active ? ' AND product_shop.`active` = 1 AND product_shop.`visibility` != \'none\'' : '') . '
                GROUP BY product_shopid_product ORDER BY cpposition ASC';   */     
//var_dump($sql);
  //      exit; 
        if (!$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql)) {
            return [];
        }

        foreach ($result as $k => &$row) {
            if (!Product::checkAccessStatic((int) $row['id_product'], false)) {
                unset($result[$k]);

                continue;
            } else {
                $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
     * Wrapper to static method changeAccessories($accessories_id, $product_id).
     *
     * @param array $accessories_id Accessories ids
     */
    public function changeAccessories($accessories_id)
    {
        self::changeAccessoriesForProduct($accessories_id, $this->id);
    }

    /**
     * Link accessories with productNo need to inflate a full Product (better performances).
     *
     * @param array $accessories_id Accessories ids
     * @param int the product ID to link accessories on
     */
    public static function changeAccessoriesForProduct($accessories_id, $product_id)
    {
        foreach ($accessories_id as $id_product_2) {
            Db::getInstance()->insert('accessory', [
                'id_product_1' => (int) $product_id,
                'id_product_2' => (int) $id_product_2,
            ]);
        }
    }

    /**
     * Add new feature to product.
     */
    public function addFeaturesCustomToDB($id_value, $lang, $cust)
    {
        $row = ['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, $position, $sortorder)
    {
        if ($cust) {
            $row = ['id_feature' => (int) $id_feature, 'custom' => 1];
            Db::getInstance()->insert('feature_value', $row);
            $id_value = Db::getInstance()->Insert_ID();
        }
        $row = ['id_feature' => (int) $id_feature, 'id_product' => (int) $this->id, 'id_feature_value' => (int) $id_value, 'position' => $position, 'sortorder' => $sortorder];
        Db::getInstance()->insert('feature_product', $row);
        SpecificPriceRule::applyAllRules([(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 [];
        }
        if (!array_key_exists($id_product, self::$_cacheFeatures)) {
            self::$_cacheFeatures[$id_product] = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
                '
                SELECT fpid_feature, fpid_product, fpid_feature_value, fpposition, fpsortorder, custom
                FROM `' . _DB_PREFIX_ . 'feature_product` fp
                LEFT JOIN `' . _DB_PREFIX_ . 'feature_value` fv ON (fpid_feature_value = fvid_feature_value)
                WHERE `id_product` = ' . (int) $id_product
            );
        }

        return self::$_cacheFeatures[$id_product];
    }

    public static function cacheProductsFeatures($product_ids)
    {
        if (!Feature::isFeatureActive()) {
            return;
        }

        $product_implode = [];
        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, position, sortorder
        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']] = [];
            }
            self::$_cacheFeatures[$row['id_product']][] = $row;
        }
    }

    public static function cacheFrontFeatures($product_ids, $id_lang)
    {
        if (!Feature::isFeatureActive()) {
            return;
        }

        $product_implode = [];
        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, pfid_feature, pfposition, pfsortorder
        FROM ' . _DB_PREFIX_ . 'feature_product pf
        LEFT JOIN ' . _DB_PREFIX_ . 'feature_lang fl ON (flid_feature = pfid_feature AND flid_lang = ' . (int) $id_lang . ')
        LEFT JOIN ' . _DB_PREFIX_ . 'feature_value_lang fvl ON (fvlid_feature_value = pfid_feature_value AND fvlid_lang = ' . (int) $id_lang . ')
        LEFT JOIN ' . _DB_PREFIX_ . 'feature f ON (fid_feature = pfid_feature)
        ' . Shop::addSqlAssociation('feature', 'f') . '
        WHERE `id_product` IN (' . implode(',', $product_implode) . ')
        ORDER BY fposition ASC');

        foreach ($result as $row) {
            if (!array_key_exists($row['id_product'] . '-' . $id_lang, self::$_frontFeaturesCache)) {
                self::$_frontFeaturesCache[$row['id_product'] . '-' . $id_lang] = [];
            }
            if (!isset(self::$_frontFeaturesCache[$row['id_product'] . '-' . $id_lang][$row['id_feature']])) {
                self::$_frontFeaturesCache[$row['id_product'] . '-' . $id_lang][$row['id_feature']] = $row;
            }
        }
    }

    /**
     * Admin panel product search.
     *
     * @param int $id_lang Language id
     * @param string $query Search query
     *
     * @return array Matching products
     */
    public static function searchByName($id_lang, $query, Context $context = null, $limit = null)
    {
        if (!$context) {
            $context = Context::getContext();
        }

        $sql = new DbQuery();
        $sql->select('p.`id_product`, pl.`name`, p.`ean13`, p.`isbn`, p.`upc`, p.`mpn`, p.`active`, p.`reference`, m.`name` AS manufacturer_name, stock.`quantity`, product_shopadvanced_stock_management, p.`customizable`');
        $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_langShop::addSqlRestrictionOnLang('pl')
        );
        $sql->leftJoin('manufacturer', 'm', 'm.`id_manufacturer` = p.`id_manufacturer`');

        $where = 'pl.`name` LIKE \'%' . pSQL($query) . '%\'
        OR p.`ean13` LIKE \'%' . pSQL($query) . '%\'
        OR p.`isbn` LIKE \'%' . pSQL($query) . '%\'
        OR p.`upc` LIKE \'%' . pSQL($query) . '%\'
        OR p.`mpn` LIKE \'%' . pSQL($query) . '%\'
        OR p.`reference` LIKE \'%' . pSQL($query) . '%\'
        OR p.`supplier_reference` LIKE \'%' . pSQL($query) . '%\'
        OR EXISTS(SELECT * FROM `' . _DB_PREFIX_ . 'product_supplier` sp WHERE sp.`id_product` = p.`id_product` AND `product_supplier_reference` LIKE \'%' . pSQL($query) . '%\')';

        $sql->orderBy('pl.`name` ASC');

        if ($limit) {
            $sql->limit($limit);
        }

        if (Combination::isFeatureActive()) {
            $where .= ' OR EXISTS(SELECT * FROM `' . _DB_PREFIX_ . 'product_attribute` `pa` WHERE pa.`id_product` = p.`id_product` AND (pa.`reference` LIKE \'%' . pSQL($query) . '%\'
            OR pa.`supplier_reference` LIKE \'%' . pSQL($query) . '%\'
            OR pa.`ean13` LIKE \'%' . pSQL($query) . '%\'
            OR pa.`isbn` LIKE \'%' . pSQL($query) . '%\'
            OR pa.`mpn` LIKE \'%' . pSQL($query) . '%\'
            OR pa.`upc` LIKE \'%' . pSQL($query) . '%\'))';
        }
        $sql->where($where);
        $sql->join(Product::sqlStock('p', 0));

        $result = Db::getInstance()->executeS($sql);

        if (!$result) {
            return false;
        }

        $results_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 int $id_product_old Old product id
     * @param int $id_product_new New product id
     */
    public static function duplicateAttributes($id_product_old, $id_product_new)
    {
        $return = true;
        $combination_images = [];

        $result = Db::getInstance()->executeS(
            '
        SELECT pa.*, product_attribute_shop.*
            FROM `' . _DB_PREFIX_ . 'product_attribute` pa
            ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
            WHERE pa.`id_product` = ' . (int) $id_product_old
        );
        $combinations = [];

        foreach ($result as $row) {
            $id_product_attribute_old = (int) $row['id_product_attribute'];
            if (!isset($combinations[$id_product_attribute_old])) {
                $id_combination = null;
                $id_shop = null;
                $result2 = Db::getInstance()->executeS(
                    '
                SELECT *
                FROM `' . _DB_PREFIX_ . 'product_attribute_combination`
                    WHERE `id_product_attribute` = ' . $id_product_attribute_old
                );
            } else {
                $id_combination = (int) $combinations[$id_product_attribute_old];
                $id_shop = (int) $row['id_shop'];
                $context_old = Shop::getContext();
                $context_shop_id_old = Shop::getContextShopID();
                Shop::setContext(Shop::CONTEXT_SHOP, $id_shop);
            }

            $row['id_product'] = $id_product_new;
            unset($row['id_product_attribute']);

            $combination = new Combination($id_combination, null, $id_shop);
            foreach ($row as $k => $v) {
                $combination->$k = $v;
            }
            $return &= $combination->save();

            $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;
            }

            if (!isset($combinations[$id_product_attribute_old])) {
                $combinations[$id_product_attribute_old] = (int) $id_product_attribute_new;
                foreach ($result2 as $row2) {
                    $row2['id_product_attribute'] = $id_product_attribute_new;
                    $return &= Db::getInstance()->insert('product_attribute_combination', $row2);
                }
            } else {
                Shop::setContext($context_old, $context_shop_id_old);
            }

            //Copy suppliers
            $result3 = Db::getInstance()->executeS('
            SELECT *
            FROM `' . _DB_PREFIX_ . 'product_supplier`
            WHERE `id_product_attribute` = ' . (int) $id_product_attribute_old . '
            AND `id_product` = ' . (int) $id_product_old);

            foreach ($result3 as $row3) {
                unset($row3['id_product_supplier']);
                $row3['id_product'] = $id_product_new;
                $row3['id_product_attribute'] = $id_product_attribute_new;
                $return &= Db::getInstance()->insert('product_supplier', $row3);
            }
        }

        $impacts = self::getAttributesImpacts($id_product_old);

        if (is_array($impacts) && count($impacts)) {
            $impact_sql = 'INSERT INTO `' . _DB_PREFIX_ . 'attribute_impact` (`id_product`, `id_attribute`, `weight`, `price`) VALUES ';

            foreach ($impacts as $id_attribute => $impact) {
                $impact_sql .= '(' . (int) $id_product_new . ', ' . (int) $id_attribute . ', ' . (float) $impacts[$id_attribute]['weight'] . ', '
                    . (float) $impacts[$id_attribute]['price'] . '),';
            }

            $impact_sql = substr_replace($impact_sql, '', -1);
            $impact_sql .= ' ON DUPLICATE KEY UPDATE `price` = VALUES(price), `weight` = VALUES(weight)';

            Db::getInstance()->execute($impact_sql);
        }

        return !$return ? false : $combination_images;
    }

    public static function getAttributesImpacts($id_product)
    {
        $return = [];
        $result = Db::getInstance()->executeS(
            'SELECT ai.`id_attribute`, ai.`price`, ai.`weight`
            FROM `' . _DB_PREFIX_ . 'attribute_impact` ai
            WHERE ai.`id_product` = ' . (int) $id_product
        );

        if (!$result) {
            return [];
        }
        foreach ($result as $impact) {
            $return[$impact['id_attribute']]['price'] = (float) $impact['price'];
            $return[$impact['id_attribute']]['weight'] = (float) $impact['weight'];
        }

        return $return;
    }

    /**
     * Get product attribute image associations.
     *
     * @param int $id_product_attribute
     *
     * @return array
     */
    public static function _getAttributeImageAssociations($id_product_attribute)
    {
        $combination_images = [];
        $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 = [
                '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`, `id_lang` FROM `' . _DB_PREFIX_ . 'product_tag` WHERE `id_product` = ' . (int) $id_product_old);
        if (!Db::getInstance()->numRows()) {
            return true;
        }

        $data = [];
        foreach ($tags as $tag) {
            $data[] = [
                'id_product' => (int) $id_product_new,
                'id_tag' => (int) $tag['id_tag'],
                'id_lang' => (int) $tag['id_lang'],
            ];
        }

        return Db::getInstance()->insert('product_tag', $data);
    }

    public static function duplicateTaxes($id_product_old, $id_product_new)
    {
        $query = new DbQuery();
        $query->select('id_tax_rules_group, id_shop');
        $query->from('product_shop');
        $query->where('`id_product` = ' . (int) $id_product_old);

        $results = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query->build());

        if (!empty($results)) {
            foreach ($results as $result) {
                if (!Db::getInstance()->update(
                    'product_shop',
                    ['id_tax_rules_group' => (int) $result['id_tax_rules_group']],
                    'id_product=' . (int) $id_product_new . ' AND id_shop = ' . (int) $result['id_shop']
                )) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Duplicate prices when duplicating a product.
     *
     * @param int $id_product_old Old product id
     * @param int $id_product_new New product id
     */
    public static function duplicatePrices($id_product_old, $id_product_new)
    {
        $query = new DbQuery();
        $query->select('price, unit_price_ratio, id_shop');
        $query->from('product_shop');
        $query->where('`id_product` = ' . (int) $id_product_old);
        $results = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query->build());
        if (!empty($results)) {
            foreach ($results as $result) {
                if (!Db::getInstance()->update(
                    'product_shop',
                    ['price' => pSQL($result['price']), 'unit_price_ratio' => pSQL($result['unit_price_ratio'])],
                    'id_product=' . (int) $id_product_new . ' AND id_shop = ' . (int) $result['id_shop']
                )) {
                    return false;
                }
            }
        }

        return true;
    }

    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 = [];
        foreach ($results as $row) {
            $new_filename = ProductDownload::getNewFilename();
            copy(_PS_DOWNLOAD_DIR_ . $row['filename'], _PS_DOWNLOAD_DIR_ . $new_filename);

            $data[] = [
                'id_product' => (int) $id_product_new,
                'display_filename' => pSQL($row['display_filename']),
                'filename' => pSQL($new_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'],
                'date_add' => date('Y-m-d H:i:s'),
            ];
        }

        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 = [];

        // Prepare data of table product_attachment
        foreach ($results as $row) {
            $data[] = [
                'id_product' => (int) $id_product_new,
                'id_attachment' => (int) $row['id_attachment'],
            ];
        }

        // Duplicate product attachement
        $res = Db::getInstance()->insert('product_attachment', $data);
        Product::updateCacheAttachment((int) $id_product_new);

        return $res;
    }

    /**
     * Duplicate features when duplicating a product.
     *
     * @param int $id_product_old Old product id
     * @param int $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'];

                foreach (Language::getIDs(false) as $id_lang) {
                    $result3 = Db::getInstance()->getRow('
                    SELECT *
                    FROM `' . _DB_PREFIX_ . 'feature_value_lang`
                    WHERE `id_feature_value` = ' . (int) $old_id_feature_value . '
                    AND `id_lang` = ' . (int) $id_lang);

                    if ($result3) {
                        $result3['id_feature_value'] = (int) $new_id_feature_value;
                        $result3['value'] = pSQL($result3['value']);
                        $return &= Db::getInstance()->insert('feature_value_lang', $result3);
                    }
                }
                $row['id_feature_value'] = $new_id_feature_value;
            }

            $row['id_product'] = (int) $id_product_new;
            $return &= Db::getInstance()->insert('feature_product', $row);
        }

        return $return;
    }

    protected static function _getCustomizationFieldsNLabels($product_id, $id_shop = null)
    {
        if (!Customization::isFeatureActive()) {
            return false;
        }

        if (Shop::isFeatureActive() && !$id_shop) {
            $id_shop = (int) Context::getContext()->shop->id;
        }

        $customizations = [];
        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 [];
        }

        $customization_field_ids = [];
        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`, `id_shop`, `name`
            FROM `' . _DB_PREFIX_ . 'customization_field_lang`
            WHERE `id_customization_field` IN (' . implode(', ', $customization_field_ids) . ')' . ($id_shop ? ' AND `id_shop` = ' . (int) $id_shop : '') . '
            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) {
            
            $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'])) {
                foreach ($customizations['labels'][$old_customization_field_id] as $customization_label) {
                    $data = [
                        'id_customization_field' => (int) $customization_field_id,
                        'id_lang' => (int) $customization_label['id_lang'],
                        'id_shop' => (int) $customization_label['id_shop'],
                        'name' => pSQL($customization_label['name']),
                    ];

                    if (!Db::getInstance()->insert('customization_field_lang', $data)) {
                        return false;
                    }
                }
            }
        }

        return true;
    }

    /**
     * Adds suppliers from old product onto a newly duplicated product.
     *
     * @param int $id_product_old
     * @param int $id_product_new
     */
    public static function duplicateSuppliers($id_product_old, $id_product_new)
    {
        $result = Db::getInstance()->executeS('
        SELECT *
        FROM `' . _DB_PREFIX_ . 'product_supplier`
        WHERE `id_product` = ' . (int) $id_product_old . ' AND `id_product_attribute` = 0');

        foreach ($result as $row) {
            unset($row['id_product_supplier']);
            $row['id_product'] = $id_product_new;
            if (!Db::getInstance()->insert('product_supplier', $row)) {
                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 && null === $this->tags) {
            $this->tags = Tag::getProductTags($this->id);
        }

        if (!($this->tags && array_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']) && $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)
    {
        Hook::exec('actionGetProductPropertiesBefore', [
            'id_lang' => $id_lang,
            'product' => &$row,
            'context' => $context,
        ]);

        if (!$row['id_product']) {
            return false;
        }

        if ($context == null) {
            $context = Context::getContext();
        }

        $id_product_attribute = $row['id_product_attribute'] = (!empty($row['id_product_attribute']) ? (int) $row['id_product_attribute'] : null);

        // 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() && $id_product_attribute === null
            && ((isset($row['cache_default_attribute']) && ($ipa_default = $row['cache_default_attribute']) !== null)
                || ($ipa_default = Product::getDefaultAttribute($row['id_product'], !$row['allow_oosp'])))) {
            $id_product_attribute = $row['id_product_attribute'] = $ipa_default;
        }
        if (!Combination::isFeatureActive() || !isset($row['id_product_attribute'])) {
            $id_product_attribute = $row['id_product_attribute'] = 0;
        }

        // Tax
        $usetax = !Tax::excludeTaxeOption();

        $cache_key = $row['id_product'] . '-' . $id_product_attribute . '-' . $id_lang . '-' . (int) $usetax;
        if (isset($row['id_product_pack'])) {
            $cache_key .= '-pack' . $row['id_product_pack'];
        }

        if (!isset($row['cover_image_id'])) {
            $cover = static::getCover($row['id_product']);
            if (isset($cover['id_image'])) {
                $row['cover_image_id'] = $cover['id_image'];
            }
        }

        if (isset($row['cover_image_id'])) {
            $cache_key .= '-cover' . (int) $row['cover_image_id'];
        }

        if (isset(self::$productPropertiesCache[$cache_key])) {
            return array_merge($row, self::$productPropertiesCache[$cache_key]);
        }

        // Datas
        $row['category'] = Category::getLinkRewrite((int) $row['id_category_default'], (int) $id_lang);
        $row['category_name'] = Db::getInstance()->getValue('SELECT name FROM ' . _DB_PREFIX_ . 'category_lang WHERE id_shop = ' . (int) $context->shop->id . ' AND id_lang = ' . (int) $id_lang . ' AND id_category = ' . (int) $row['id_category_default']);
        $row['link'] = $context->link->getProductLink((int) $row['id_product'], $row['link_rewrite'], $row['category'], $row['ean13']);

        $row['attribute_price'] = 0;
        if ($id_product_attribute) {
            $row['attribute_price'] = (float) Combination::getPrice($id_product_attribute);
        }

        if (isset($row['quantity_wanted'])) {
            // 'quantity_wanted' may very well be zero even if set
            $quantity = max((int) $row['minimal_quantity'], (int) $row['quantity_wanted']);
        } elseif (isset($row['cart_quantity'])) {
            $quantity = max((int) $row['minimal_quantity'], (int) $row['cart_quantity']);
        } else {
            $quantity = (int) $row['minimal_quantity'];
        }

        $row['price_tax_exc'] = Product::getPriceStatic(
            (int) $row['id_product'],
            false,
            $id_product_attribute,
            (self::$_taxCalculationMethod == PS_TAX_EXC ? 2 : 6),
            null,
            false,
            true,
            $quantity
        );

        if (self::$_taxCalculationMethod == PS_TAX_EXC) {
            $row['price_tax_exc'] = Tools::ps_round($row['price_tax_exc'], Context::getContext()->getComputingPrecision());
            $row['price'] = Product::getPriceStatic(
                (int) $row['id_product'],
                true,
                $id_product_attribute,
                6,
                null,
                false,
                true,
                $quantity
            );
            $row['price_without_reduction'] =
            $row['price_without_reduction_without_tax'] = Product::getPriceStatic(
                (int) $row['id_product'],
                false,
                $id_product_attribute,
                2,
                null,
                false,
                false,
                $quantity
            );
        } else {
            $row['price'] = Tools::ps_round(
                Product::getPriceStatic(
                    (int) $row['id_product'],
                    true,
                    $id_product_attribute,
                    6,
                    null,
                    false,
                    true,
                    $quantity
                ),
                Context::getContext()->getComputingPrecision()
            );
            $row['price_without_reduction'] = Product::getPriceStatic(
                (int) $row['id_product'],
                true,
                $id_product_attribute,
                6,
                null,
                false,
                false,
                $quantity
            );
            $row['price_without_reduction_without_tax'] = Product::getPriceStatic(
                (int) $row['id_product'],
                false,
                $id_product_attribute,
                6,
                null,
                false,
                false,
                $quantity
            );
        }

        $row['reduction'] = Product::getPriceStatic(
            (int) $row['id_product'],
            (bool) $usetax,
            $id_product_attribute,
            6,
            null,
            true,
            true,
            $quantity,
            true,
            null,
            null,
            null,
            $specific_prices
        );

        $row['reduction_without_tax'] = Product::getPriceStatic(
            (int) $row['id_product'],
            false,
            $id_product_attribute,
            6,
            null,
            true,
            true,
            $quantity,
            true,
            null,
            null,
            null,
            $specific_prices
        );

        $row['specific_prices'] = $specific_prices;

        $row['quantity'] = Product::getQuantity(
            (int) $row['id_product'],
            0,
            isset($row['cache_is_pack']) ? $row['cache_is_pack'] : null,
            $context->cart
        );

        $row['quantity_all_versions'] = $row['quantity'];

        if ($row['id_product_attribute']) {
            $row['quantity'] = Product::getQuantity(
                (int) $row['id_product'],
                $id_product_attribute,
                isset($row['cache_is_pack']) ? $row['cache_is_pack'] : null,
                $context->cart
            );

            $row['available_date'] = Product::getAvailableDate(
                (int) $row['id_product'],
                $id_product_attribute
            );
        }

        $row['id_image'] = Product::defineProductImage($row, $id_lang);
        $row['features'] = Product::getFrontFeaturesStatic((int) $id_lang, $row['id_product']);

        $row['attachments'] = [];
        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) : [];
        $row['nopackprice'] = $row['pack'] ? Pack::noPackPrice($row['id_product']) : 0;

        if ($row['pack'] && !Pack::isInStock($row['id_product'], $quantity, $context->cart)) {
            $row['quantity'] = 0;
        }

        $row['customization_required'] = false;
        if (isset($row['customizable']) && $row['customizable'] && Customization::isFeatureActive()) {
            if (count(Product::getRequiredCustomizableFieldsStatic((int) $row['id_product']))) {
                $row['customization_required'] = true;
            }
        }

        $attributes = Product::getAttributesParams($row['id_product'], $row['id_product_attribute']);

        foreach ($attributes as $attribute) {
            $row['attributes'][$attribute['id_attribute_group']] = $attribute;
        }

        $row = Product::getTaxesInformations($row, $context);

        $row['ecotax_rate'] = (float) Tax::getProductEcotaxRate($context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')});

        Hook::exec('actionGetProductPropertiesAfter', [
            'id_lang' => $id_lang,
            'product' => &$row,
            'context' => $context,
        ]);

        $combination = new Combination($id_product_attribute);

        if (0 != $combination->unit_price_impact && 0 != $row['unit_price_ratio']) {
            $unitPrice = ($row['price_tax_exc'] / $row['unit_price_ratio']) + $combination->unit_price_impact;
            $row['unit_price_ratio'] = $row['price_tax_exc'] / $unitPrice;
        }

        $row['unit_price'] = ($row['unit_price_ratio'] != 0 ? $row['price'] / $row['unit_price_ratio'] : 0);

        self::$productPropertiesCache[$cache_key] = $row;

        return self::$productPropertiesCache[$cache_key];
    }

    public static function getTaxesInformations($row, Context $context = null)
    {
        static $address = null;

        if ($context === null) {
            $context = Context::getContext();
        }
        if ($address === null) {
            $address = new Address();
        }

        $address->id_country = (int) $context->country->id;
        $address->id_state = 0;
        $address->postcode = 0;

        $tax_manager = TaxManagerFactory::getManager($address, Product::getIdTaxRulesGroupByIdProduct((int) $row['id_product'], $context));
        $row['rate'] = $tax_manager->getTaxCalculator()->getTotalRate();
        $row['tax_name'] = $tax_manager->getTaxCalculator()->getTaxesName();

        return $row;
    }

    public static function getProductsProperties($id_lang, $query_result)
    {
        $results_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 [];
        }
        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, pfid_feature, pfposition, pfsortorder
                FROM ' . _DB_PREFIX_ . 'feature_product pf
                LEFT JOIN ' . _DB_PREFIX_ . 'feature_lang fl ON (flid_feature = pfid_feature AND flid_lang = ' . (int) $id_lang . ')
                LEFT JOIN ' . _DB_PREFIX_ . 'feature_value_lang fvl ON (fvlid_feature_value = pfid_feature_value AND fvlid_lang = ' . (int) $id_lang . ')
                LEFT JOIN ' . _DB_PREFIX_ . 'feature f ON (fid_feature = pfid_feature AND flid_lang = ' . (int) $id_lang . ')
                ' . Shop::addSqlAssociation('feature', 'f') . '
                WHERE pfid_product = ' . (int) $id_product . '
                ORDER BY pfsortorder 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 aid_attachment = paid_attachment
        LEFT JOIN ' . _DB_PREFIX_ . 'attachment_lang al ON (aid_attachment = alid_attachment AND alid_lang = ' . (int) $id_lang . ')
        WHERE paid_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, $id_shop = null, $id_customization = null)
    {
        if (!Customization::isFeatureActive()) {
            return false;
        }

        // No need to query if there isn't any real cart!
        if (!$id_cart) {
            return false;
        }

        if ($id_customization === 0) {
            // Backward compatibility: check if there are no products in cart with specific `id_customization` before returning false
            $product_customizations = (int) Db::getInstance()->getValue('
                SELECT COUNT(`id_customization`) FROM `' . _DB_PREFIX_ . 'cart_product`
                WHERE `id_cart` = ' . (int) $id_cart .
                ' AND `id_customization` != 0');
            if ($product_customizations) {
                return false;
            }
        }

        if (!$id_lang) {
            $id_lang = Context::getContext()->language->id;
        }
        if (Shop::isFeatureActive() && !$id_shop) {
            $id_shop = (int) Context::getContext()->shop->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`, cd.`id_module`, cfl.`name`
            FROM `' . _DB_PREFIX_ . 'customized_data` cd
            NATURAL JOIN `' . _DB_PREFIX_ . 'customization` c
            LEFT JOIN `' . _DB_PREFIX_ . 'customization_field_lang` cfl ON (cflid_customization_field = cd.`index` AND id_lang = ' . (int) $id_lang .
                ($id_shop ? ' AND cfl.`id_shop` = ' . (int) $id_shop : '') . ')
            WHERE c.`id_cart` = ' . (int) $id_cart .
            ($only_in_cart ? ' AND c.`in_cart` = 1' : '') .
            ((int) $id_customization ? ' AND cd.`id_customization` = ' . (int) $id_customization : '') . '
            ORDER BY `id_product`, `id_product_attribute`, `type`, `index`')) {
            return false;
        }

        $customized_datas = [];

        foreach ($result as $row) {
            if ((int) $row['id_module'] && (int) $row['type'] == Product::CUSTOMIZE_TEXTFIELD) {
                // Hook displayCustomization: Call only the module in question
                // When a module saves a customization programmatically, it should add its ID in the `id_module` column
                $row['value'] = Hook::exec('displayCustomization', ['customization' => $row], (int) $row['id_module']);
            }
            $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 .
            ((int) $id_customization ? ' AND `id_customization` = ' . (int) $id_customization : '') .
            ($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'];
            $customized_datas[(int) $row['id_product']][(int) $row['id_product_attribute']][(int) $row['id_address_delivery']][(int) $row['id_customization']]['id_customization'] = (int) $row['id_customization'];
        }

        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;

                
                $product_id = isset($product_update['id_product']) ? (int) $product_update['id_product'] : (int) $product_update['product_id'];
                $product_attribute_id = isset($product_update['id_product_attribute']) ? (int) $product_update['id_product_attribute'] : (int) $product_update['product_attribute_id'];
                $id_address_delivery = (int) $product_update['id_address_delivery'];
                $product_quantity = isset($product_update['cart_quantity']) ? (int) $product_update['cart_quantity'] : (int) $product_update['product_quantity'];
                $price = isset($product_update['price']) ? $product_update['price'] : $product_update['product_price'];
                if (isset($product_update['price_wt']) && $product_update['price_wt']) {
                    $price_wt = $product_update['price_wt'];
                } else {
                    $price_wt = $price * (1 + ((isset($product_update['tax_rate']) ? $product_update['tax_rate'] : $product_update['rate']) * 001));
                }

                if (!isset($customized_datas[$product_id][$product_attribute_id][$id_address_delivery])) {
                    $id_address_delivery = 0;
                }
                if (isset($customized_datas[$product_id][$product_attribute_id][$id_address_delivery])) {
                    foreach ($customized_datas[$product_id][$product_attribute_id][$id_address_delivery] as $customization) {
                        if ((int) $product_update['id_customization'] && $customization['id_customization'] != $product_update['id_customization']) {
                            continue;
                        }
                        $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;
                }
            }
        }
    }

    /*
    ** Add customization price for a single product
    */
    public static function addProductCustomizationPrice(&$product, &$customized_datas)
    {
        if (!$customized_datas) {
            return;
        }

        $products = [$product];
        self::addCustomizationPrice($products, $customized_datas);
        $product = "=";
    }

    /*
    ** 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 = [
            Product::CUSTOMIZE_FILE => (int) $this->uploadable_files,
            Product::CUSTOMIZE_TEXTFIELD => (int) $this->text_fields,
        ];

        
        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 = [
            Product::CUSTOMIZE_FILE => [],
            Product::CUSTOMIZE_TEXTFIELD => [],
        ];

        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 ($extra_file > 0 && count($customization_fields[Product::CUSTOMIZE_FILE]) - $extra_file >= 0 &&
        (!Db::getInstance()->execute(
            'DELETE `' . _DB_PREFIX_ . 'customization_field`,`' . _DB_PREFIX_ . 'customization_field_lang`
            FROM `' . _DB_PREFIX_ . 'customization_field` JOIN `' . _DB_PREFIX_ . 'customization_field_lang`
            WHERE `' . _DB_PREFIX_ . 'customization_field`.`id_product` = ' . (int) $this->id . '
            AND `' . _DB_PREFIX_ . 'customization_field`.`type` = ' . Product::CUSTOMIZE_FILE . '
            AND `' . _DB_PREFIX_ . 'customization_field_lang`.`id_customization_field` = `' . _DB_PREFIX_ . 'customization_field`.`id_customization_field`
            AND `' . _DB_PREFIX_ . 'customization_field`.`id_customization_field` >= ' . (int) $customization_fields[Product::CUSTOMIZE_FILE][count($customization_fields[Product::CUSTOMIZE_FILE]) - $extra_file]
        ))) {
            return false;
        }

        if ($extra_text > 0 && count($customization_fields[Product::CUSTOMIZE_TEXTFIELD]) - $extra_text >= 0 &&
        (!Db::getInstance()->execute(
            'DELETE `' . _DB_PREFIX_ . 'customization_field`,`' . _DB_PREFIX_ . 'customization_field_lang`
            FROM `' . _DB_PREFIX_ . 'customization_field` JOIN `' . _DB_PREFIX_ . 'customization_field_lang`
            WHERE `' . _DB_PREFIX_ . 'customization_field`.`id_product` = ' . (int) $this->id . '
            AND `' . _DB_PREFIX_ . 'customization_field`.`type` = ' . Product::CUSTOMIZE_TEXTFIELD . '
            AND `' . _DB_PREFIX_ . 'customization_field_lang`.`id_customization_field` = `' . _DB_PREFIX_ . 'customization_field`.`id_customization_field`
            AND `' . _DB_PREFIX_ . 'customization_field`.`id_customization_field` >= ' . (int) $customization_fields[Product::CUSTOMIZE_TEXTFIELD][count($customization_fields[Product::CUSTOMIZE_TEXTFIELD]) - $extra_text]
        ))) {
            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) {
            foreach (Shop::getContextListShopID() as $id_shop) {
                $values .= '(' . (int) $id_customization_field . ', ' . (int) $language['id_lang'] . ', ' . (int) $id_shop . ',\'\'), ';
            }
        }

        $values = rtrim($values, ', ');
        if (!Db::getInstance()->execute('
            INSERT INTO `' . _DB_PREFIX_ . 'customization_field_lang` (`id_customization_field`, `id_lang`, `id_shop`, `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) {
            
            if (strncmp($field, 'label_', 6) == 0) {
                if (!$tmp = $this->_checkLabelField($field, $value)) {
                    return false;
                }
                
                if (Shop::isFeatureActive()) {
                    foreach (Shop::getContextListShopID() as $id_shop) {
                        if (!Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'customization_field_lang`
                        (`id_customization_field`, `id_lang`, `id_shop`, `name`) VALUES (' . (int) "', ' . (int) "i, ' . (int) $id_shop . ', \'' . pSQL($value) . '\')
                        ON DUPLICATE KEY UPDATE `name` = \'' . pSQL($value) . '\'')) {
                            return false;
                        }
                    }
                } elseif (!Db::getInstance()->execute('
                    INSERT INTO `' . _DB_PREFIX_ . 'customization_field_lang`
                    (`id_customization_field`, `id_lang`, `name`) VALUES (' . (int) "', ' . (int) "i, \'' . pSQL($value) . '\')
                    ON DUPLICATE KEY UPDATE `name` = \'' . pSQL($value) . '\'')) {
                    return false;
                }

                $is_required = isset($_POST['require_' . (int) " _' . (int) "'"]) ? 1 : 0;
                $has_required_fields |= $is_required;
                
                if (!Db::getInstance()->execute(
                    'UPDATE `' . _DB_PREFIX_ . 'customization_field`
                    SET `required` = ' . (int) $is_required . '
                    WHERE `id_customization_field` = ' . (int) "'"
                )) {
                    return false;
                }
            }
        }

        if ($has_required_fields && !ObjectModel::updateMultishopTable('product', ['customizable' => 2], 'aid_product = ' . (int) $this->id)) {
            return false;
        }

        if (!$this->_deleteOldLabels()) {
            return false;
        }

        return true;
    }

    public function getCustomizationFields($id_lang = false, $id_shop = null)
    {
        if (!Customization::isFeatureActive()) {
            return false;
        }

        if (Shop::isFeatureActive() && !$id_shop) {
            $id_shop = (int) Context::getContext()->shop->id;
        }

        // Hide the modules fields in the front-office
        // When a module adds a customization programmatically, it should set the `is_module` to 1
        $context = Context::getContext();
        $front = isset($context->controller->controller_type) && in_array($context->controller->controller_type, ['front']);

        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 : '') .
            ($id_shop ? ' AND cfl.`id_shop` = ' . (int) $id_shop : '') .
            ($front ? ' AND !cf.`is_module`' : '') . '
            AND cf.`is_deleted` = 0
            ORDER BY cf.`id_customization_field`')
        ) {
            return false;
        }

        if ($id_lang) {
            return $result;
        }

        $customization_fields = [];
        foreach ($result as $row) {
            $customization_fields[(int) $row['type']][(int) $row['id_customization_field']][(int) $row['id_lang']] = $row;
        }

        return $customization_fields;
    }

    /**
     * check if product has an activated and required customizationFields.
     *
     * @return bool
     *
     * @throws \PrestaShopDatabaseException
     */
    public function hasActivatedRequiredCustomizableFields()
    {
        if (!Customization::isFeatureActive()) {
            return false;
        }

        return (bool) Db::getInstance()->executeS(
            '
            SELECT 1
            FROM `' . _DB_PREFIX_ . 'customization_field`
            WHERE `id_product` = ' . (int) $this->id . '
            AND `required` = 1
            AND `is_deleted` = 0'
        );
    }

    public function getCustomizationFieldIds()
    {
        if (!Customization::isFeatureActive()) {
            return [];
        }

        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 [];
        }

        return Product::getRequiredCustomizableFieldsStatic($this->id);
    }

    public static function getRequiredCustomizableFieldsStatic($id)
    {
        if (!$id || !Customization::isFeatureActive()) {
            return [];
        }

        return Db::getInstance()->executeS(
            '
            SELECT `id_customization_field`, `type`
            FROM `' . _DB_PREFIX_ . 'customization_field`
            WHERE `id_product` = ' . (int) $id . '
            AND `required` = 1 AND `is_deleted` = 0'
        );
    }

    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 = [];
        foreach ($fields as $field) {
            $fields_present[] = ['id_customization_field' => $field['index'], 'type' => $field['type']];
        }

        if (is_array($required_fields) && count($required_fields)) {
            foreach ($required_fields as $required_field) {
                if (!in_array($required_field, $fields_present)) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Return the list of old temp products.
     *
     * @return array
     */
    public static function getOldTempProducts()
    {
        $sql = 'SELECT id_product FROM `' . _DB_PREFIX_ . 'product` WHERE state=' . \Product::STATE_TEMP . ' AND date_upd < NOW() - INTERVAL 1 DAY';

        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql, true, false);
    }

    /**
     * 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 bool 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()
    {
        $context = Context::getContext();

        return Tools::getContextLocale($context)->formatPrice(Pack::noPackPrice((int) $this->id), $context->currency->iso_code);
    }

    public function checkAccess($id_customer)
    {
        return Product::checkAccessStatic((int) $this->id, (int) $id_customer);
    }

    public static function checkAccessStatic($id_product, $id_customer)
    {
        if (!Group::isFeatureActive()) {
            return true;
        }

        $cache_id = 'Product::checkAccess_' . (int) $id_product . '-' . (int) $id_customer . (!$id_customer ? '-' . (int) Group::getCurrent()->id : '');
        if (!Cache::isStored($cache_id)) {
            if (!$id_customer) {
                $result = (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) $id_product . ' AND ctg.`id_group` = ' . (int) Group::getCurrent()->id);
            } else {
                $result = (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) $id_product . ' AND cg.`id_customer` = ' . (int) $id_customer);
            }

            Cache::store($cache_id, $result);

            return $result;
        }

        return Cache::retrieve($cache_id);
    }

    /**
     * Add a stock movement for current product.
     *
     * Since 15, 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 150
     *
     * @param int $quantity
     * @param int $id_reason - useless
     * @param int $id_product_attribute
     * @param int $id_order - DEPRECATED
     * @param int $id_employee - DEPRECATED
     *
     * @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 150
     */
    public function getStockMvts($id_lang)
    {
        Tools::displayAsDeprecated();

        return Db::getInstance()->executeS('
            SELECT smid_stock_mvt, smdate_add, smquantity, smid_order,
            CONCAT(plname, \' \', GROUP_CONCAT(IFNULL(alname, \'\'), \'\')) product_name, CONCAT(elastname, \' \', efirstname) employee, mrlname reason
            FROM `' . _DB_PREFIX_ . 'stock_mvt` sm
            LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (
                smid_product = plid_product
                AND plid_lang = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl') . '
            )
            LEFT JOIN `' . _DB_PREFIX_ . 'stock_mvt_reason_lang` mrl ON (
                smid_stock_mvt_reason = mrlid_stock_mvt_reason
                AND mrlid_lang = ' . (int) $id_lang . '
            )
            LEFT JOIN `' . _DB_PREFIX_ . 'employee` e ON (
                eid_employee = smid_employee
            )
            LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON (
                pacid_product_attribute = smid_product_attribute
            )
            LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ON (
                alid_attribute = pacid_attribute
                AND alid_lang = ' . (int) $id_lang . '
            )
            WHERE smid_product=' . (int) $this->id . '
            GROUP BY smid_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)) {
            $result = 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->shop->id);
            Cache::store($key, (int) $result);

            return (int) $result;
        }

        return Cache::retrieve($key);
    }

    /**
     * Returns tax rate.
     *
     * @param Address|null $address
     *
     * @return float 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'],
                    $rows[$keyrow]['custom']
                );
            }
            asort($rows[$keyrow]);
        }

        return $rows;
    }

    /**
     * Webservice setter : set product features association.
     *
     * @param $product_features Product Feature ids
     *
     * @return bool
     */
    public function setWsProductFeatures($product_features)
    {
        Db::getInstance()->execute(
            '
            DELETE FROM `' . _DB_PREFIX_ . 'feature_product`
            WHERE `id_product` = ' . (int) $this->id
        );
        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 int $id_combination id default combination
     *
     * @return bool
     */
    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 (cid_category = cpid_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 array $category_ids category ids
     *
     * @return bool
     */
    public function setWsCategories($category_ids)
    {
        $ids = [];
        foreach ($category_ids as $value) {
            if ($value instanceof Category) {
                $ids[] = (int) $value->id;
            } elseif (is_array($value) && array_key_exists('id', $value)) {
                $ids[] = (int) $value['id'];
            } else {
                $ids[] = (int) $value;
            }
        }
        $ids = array_unique($ids);

        $positions = Db::getInstance()->executeS(
                'SELECT `id_category`, `position`
                FROM `' . _DB_PREFIX_ . 'category_product`
                WHERE `id_product` = ' . (int) $this->id
        );

        $max_positions = Db::getInstance()->executeS(
                'SELECT `id_category`, max(`position`) as maximum
                FROM `' . _DB_PREFIX_ . 'category_product`
                GROUP BY id_category'
        );

        $positions_lookup = [];
        $max_position_lookup = [];

        foreach ($positions as $row) {
            $positions_lookup[(int) $row['id_category']] = (int) $row['position'];
        }
        foreach ($max_positions as $row) {
            $max_position_lookup[(int) $row['id_category']] = (int) $row['maximum'];
        }

        $return = true;
        if ($this->deleteCategories() && !empty($ids)) {
            $sql_values = [];
            foreach ($ids as $id) {
                $pos = 0;
                if (array_key_exists((int) $id, $positions_lookup)) {
                    $pos = (int) $positions_lookup[(int) $id] + 1;
                } elseif (array_key_exists((int) $id, $max_position_lookup)) {
                    $pos = (int) $max_position_lookup[(int) $id] + 1;
                }

                $sql_values[] = '(' . (int) $id . ', ' . (int) $this->id . ', ' . $pos . ')';
            }

            $return = Db::getInstance()->execute(
                '
                INSERT INTO `' . _DB_PREFIX_ . 'category_product` (`id_category`, `id_product`, `position`)
                VALUES ' . implode(',', $sql_values)
            );
        }

        Hook::exec('actionProductUpdate', ['id_product' => (int) $this->id]);

        return $return;
    }

    /**
     * Webservice getter : get product accessories ids of current product for association.
     *
     * @return array
     */
    public function getWsAccessories()
    {
        $result = Db::getInstance()->executeS(
            'SELECT p.`id_product` AS id
            FROM `' . _DB_PREFIX_ . 'accessory` a
            LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON (pid_product = aid_product_2)
            ' . Shop::addSqlAssociation('product', 'p') . '
            WHERE a.`id_product_1` = ' . (int) $this->id
        );

        return $result;
    }

    /**
     * Webservice setter : set product accessories ids of current product for association.
     *
     * @param $accessories product ids
     */
    public function setWsAccessories($accessories)
    {
        $this->deleteAccessories();
        foreach ($accessories as $accessory) {
            Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'accessory` (`id_product_1`, `id_product_2`) VALUES (' . (int) $this->id . ', ' . (int) $accessory['id'] . ')');
        }

        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 pa.`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 = [];
        foreach ($combinations as $combination) {
            $ids_new[] = (int) $combination['id'];
        }

        $ids_orig = [];
        $original = Db::getInstance()->executeS(
            'SELECT pa.`id_product_attribute` as id
            FROM `' . _DB_PREFIX_ . 'product_attribute` pa
            ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
            WHERE pa.`id_product` = ' . (int) $this->id
        );

        if (is_array($original)) {
            foreach ($original as $id) {
                $ids_orig[] = $id['id'];
            }
        }

        $all_ids = [];
        $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 = [];
        foreach ($ids_new as $id) {
            if (!in_array($id, $ids_orig)) {
                $to_add[] = $id;
            }
        }

        $to_delete = [];
        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 (' . (int) $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 pacid_attribute as id
            FROM `' . _DB_PREFIX_ . 'product_attribute` pa
            ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
            LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON (pacid_product_attribute = paid_product_attribute)
            WHERE paid_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 "="['position'];
        }

        return '';
    }

    /**
     * Webservice setter : set virtual field position in category.
     *
     * @return bool
     */
    public function setWsPositionInCategory($position)
    {
        if ($position < 0) {
            WebserviceRequest::getInstance()->setError(500, $this->trans('You cannot set a negative position, the minimum for a position is 0.', [], 'AdminCatalogNotification'), 134);
        }
        $result = Db::getInstance()->executeS('
            SELECT `id_product`
            FROM `' . _DB_PREFIX_ . 'category_product`
            WHERE `id_category` = ' . (int) $this->id_category_default . '
            ORDER BY `position`
        ');
        if (($position > 0) && ($position + 1 > count($result))) {
            WebserviceRequest::getInstance()->setError(500, $this->trans('You cannot set a position greater than the total number of products in the category, minus 1 (position numbering starts at 0).', [], 'AdminCatalogNotification'), 135);
        }

        foreach ($result as &$value) {
            $value = $value['id_product'];
        }
        $current_position = $this->getWsPositionInCategory();

        if ($current_position && isset($result[$current_position])) {
            $save = $result[$current_position];
            unset($result[$current_position]);
            array_splice($result, (int) $position, 0, $save);
        }

        foreach ($result as $position => $id_product) {
            Db::getInstance()->update('category_product', [
                'position' => $position,
            ], '`id_category` = ' . (int) $this->id_category_default . ' AND `id_product` = ' . (int) $id_product);
        }

        return true;
    }

    /**
     * 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_shop` image_shop, `' . _DB_PREFIX_ . 'image` i
            SET image_shop.`cover` = NULL
            WHERE i.`id_product` = ' . (int) $this->id . ' AND iid_image = image_shopid_image
            AND image_shopid_shop=' . (int) Context::getContext()->shop->id);

        Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'image_shop`
            SET `cover` = 1 WHERE `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 getWsStockAvailables()
    {
        return Db::getInstance()->executeS('SELECT `id_stock_available` id, `id_product_attribute`
            FROM `' . _DB_PREFIX_ . 'stock_available`
            WHERE `id_product`=' . (int) $this->idStockAvailable::addSqlShopRestriction());
    }

    public function getWsTags()
    {
        return Db::getInstance()->executeS('
            SELECT `id_tag` as id
            FROM `' . _DB_PREFIX_ . 'product_tag`
            WHERE `id_product` = ' . (int) $this->id);
    }

    /**
     * Webservice setter : set tag ids of current product for association.
     *
     * @param $tag_ids tag ids
     */
    public function setWsTags($tag_ids)
    {
        $ids = [];
        foreach ($tag_ids as $value) {
            $ids[] = $value['id'];
        }
        if ($this->deleteWsTags()) {
            if ($ids) {
                $sql_values = [];
                $ids = array_map('intval', $ids);
                foreach ($ids as $position => $id) {
                    $id_lang = Db::getInstance()->getValue('SELECT `id_lang` FROM `' . _DB_PREFIX_ . 'tag` WHERE `id_tag`=' . (int) $id);
                    $sql_values[] = '(' . (int) $this->id . ', ' . (int) $id . ', ' . (int) $id_lang . ')';
                }
                $result = Db::getInstance()->execute(
                    '
                    INSERT INTO `' . _DB_PREFIX_ . 'product_tag` (`id_product`, `id_tag`, `id_lang`)
                    VALUES ' . implode(',', $sql_values)
                );

                return $result;
            }
        }

        return true;
    }

    /**
     * Delete products tags entries without delete tags for webservice usage.
     *
     * @return array Deletion result
     */
    public function deleteWsTags()
    {
        return Db::getInstance()->delete('product_tag', 'id_product = ' . (int) $this->id);
    }

    public function getWsManufacturerName()
    {
        return Manufacturer::getNameById((int) $this->id_manufacturer);
    }

    public static function resetEcoTax()
    {
        return ObjectModel::updateMultishopTable('product', [
            'ecotax' => 0,
        ]);
    }

    /**
     * Set Group reduction if needed.
     */
    public function setGroupReduction()
    {
        return GroupReduction::setProductReduction($this->id);
    }

    /**
     * Checks if reference exists.
     *
     * @return bool
     */
    public function existsRefInDatabase($reference)
    {
        $row = Db::getInstance()->getRow('
        SELECT `reference`
        FROM `' . _DB_PREFIX_ . 'product` p
        WHERE preference = "' . pSQL($reference) . '"');

        return isset($row['reference']);
    }

    /**
     * Get all product attributes ids.
     *
     * @since 150
     *
     * @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 paid_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.
     *
     * @param int $id_product
     * @param int $product_attribute_id
     *
     * @return array
     */
    public static function getAttributesParams($id_product, $id_product_attribute)
    {
        if ($id_product_attribute == 0) {
            return [];
        }
        $id_lang = (int) Context::getContext()->language->id;
        $cache_id = 'Product::getAttributesParams_' . (int) $id_product . '-' . (int) $id_product_attribute . '-' . (int) $id_lang;

        if (!Cache::isStored($cache_id)) {
            $result = Db::getInstance()->executeS('
            SELECT a.`id_attribute`, a.`id_attribute_group`, al.`name`, agl.`name` as `group`, pa.`reference`, pa.`ean13`, pa.`isbn`, pa.`upc`, pa.`mpn`
            FROM `' . _DB_PREFIX_ . 'attribute` a
            LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al
                ON (al.`id_attribute` = a.`id_attribute` AND al.`id_lang` = ' . (int) $id_lang . ')
            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) $id_lang . ')
            WHERE pa.`id_product` = ' . (int) $id_product . '
                AND pac.`id_product_attribute` = ' . (int) $id_product_attribute . '
                AND agl.`id_lang` = ' . (int) $id_lang);
            Cache::store($cache_id, $result);
        } else {
            $result = Cache::retrieve($cache_id);
        }

        return $result;
    }

    /**
     * @param int $id_product
     */
    public static function getAttributesInformationsByProduct($id_product)
    {
        $result = Db::getInstance()->executeS('
        SELECT DISTINCT a.`id_attribute`, a.`id_attribute_group`, al.`name` as `attribute`, agl.`name` as `group`,pa.`reference`, pa.`ean13`, pa.`isbn`, pa.`upc`, pa.`mpn`
        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;
    }

    /**
     * @return bool
     */
    public function hasCombinations()
    {
        if (null === $this->id || 0 >= $this->id) {
            return false;
        }
        $attributes = self::getAttributesInformationsByProduct($this->id);

        return !empty($attributes);
    }

    /**
     * Get an id_product_attribute by an id_product and one or more
     * id_attribute.
     *
     * eg: id_product 8 with id_attribute 4 (size medium) and
     * id_attribute 5 (color blue) returns id_product_attribute 9 which
     * is the dress size medium and color blue.
     *
     * @param int $idProduct
     * @param int|int[] $idAttributes
     * @param bool $findBest
     *
     * @return int
     *
     * @throws PrestaShopException
     */
    public static function getIdProductAttributeByIdAttributes($idProduct, $idAttributes, $findBest = false)
    {
        $idProduct = (int) $idProduct;

        if (!is_array($idAttributes) && is_numeric($idAttributes)) {
            $idAttributes = [(int) $idAttributes];
        }

        if (!is_array($idAttributes) || empty($idAttributes)) {
            throw new PrestaShopException(sprintf('Invalid parameter $idAttributes with value: "%s"', print_r($idAttributes, true)));
        }

        $idAttributesImploded = implode(',', array_map('intval', $idAttributes));
        $idProductAttribute = Db::getInstance()->getValue(
            '
            SELECT
                pac.`id_product_attribute`
            FROM
                `' . _DB_PREFIX_ . 'product_attribute_combination` pac
                INNER JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON paid_product_attribute = pacid_product_attribute
            WHERE
                paid_product = ' . $idProduct . '
                AND pacid_attribute IN (' . $idAttributesImploded . ')
            GROUP BY
                pac.`id_product_attribute`
            HAVING
                COUNT(paid_product) = ' . count($idAttributes)
        );

        if ($idProductAttribute === false && $findBest) {
            //find the best possible combination
            //first we order $idAttributes by the group position
            $orderred = [];
            $result = Db::getInstance()->executeS(
                '
                SELECT
                    a.`id_attribute`
                FROM
                    `' . _DB_PREFIX_ . 'attribute` a
                    INNER JOIN `' . _DB_PREFIX_ . 'attribute_group` g ON a.`id_attribute_group` = g.`id_attribute_group`
                WHERE
                    a.`id_attribute` IN (' . $idAttributesImploded . ')
                ORDER BY
                    g.`position` ASC'
            );

            foreach ($result as $row) {
                $orderred[] = $row['id_attribute'];
            }

            while ($idProductAttribute === false && count($orderred) > 1) {
                array_pop($orderred);
                $idProductAttribute = Db::getInstance()->getValue(
                    '
                    SELECT
                        pac.`id_product_attribute`
                    FROM
                        `' . _DB_PREFIX_ . 'product_attribute_combination` pac
                        INNER JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON paid_product_attribute = pacid_product_attribute
                    WHERE
                        paid_product = ' . (int) $idProduct . '
                        AND pacid_attribute IN (' . implode(',', array_map('intval', $orderred)) . ')
                    GROUP BY
                        pacid_product_attribute
                    HAVING
                        COUNT(paid_product) = ' . count($orderred)
                );
            }
        }

        if (empty($idProductAttribute)) {
            throw new PrestaShopObjectNotFoundException('Can not retrieve the id_product_attribute');
        }

        return $idProductAttribute;
    }

    /**
     * @deprecated 1731
     * @see Product::getIdProductAttributeByIdAttributes()
     */
    public static function getIdProductAttributesByIdAttributes($id_product, $id_attributes, $find_best = false)
    {
        return self::getIdProductAttributeByIdAttributes($id_product, $id_attributes, $find_best);
    }

    /**
     * Get the combination url anchor of the product.
     *
     * @param int $id_product_attribute
     *
     * @return string
     */
    public function getAnchor($id_product_attribute, $with_id = false)
    {
        $attributes = Product::getAttributesParams($this->id, $id_product_attribute);
        $anchor = '#';
        $sep = Configuration::get('PS_ATTRIBUTE_ANCHOR_SEPARATOR');
        foreach ($attributes as &$a) {
            foreach ($a as &$b) {
                $b = str_replace($sep, '_', Tools::link_rewrite($b));
            }
            $anchor .= '/' . ($with_id && isset($a['id_attribute']) && $a['id_attribute'] ? (int) $a['id_attribute'] . $sep : '') . $a['group'] . $sep . $a['name'];
        }

        return $anchor;
    }

    /**
     * Gets the name of a given product, in the given lang.
     *
     * @since 150
     *
     * @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(plname, \' : \', GROUP_CONCAT(DISTINCT agl.`name`, \' - \', alname SEPARATOR \', \')),plname) as name');
        } else {
            $query->select('DISTINCT plname 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', 'plid_product = paid_product AND plid_lang = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl'));
            $query->leftJoin('product_attribute_combination', 'pac', 'pacid_product_attribute = paid_product_attribute');
            $query->leftJoin('attribute', 'atr', 'atrid_attribute = pacid_attribute');
            $query->leftJoin('attribute_lang', 'al', 'alid_attribute = atrid_attribute AND alid_lang = ' . (int) $id_lang);
            $query->leftJoin('attribute_group_lang', 'agl', 'aglid_attribute_group = atrid_attribute_group AND aglid_lang = ' . (int) $id_lang);
            $query->where('paid_product = ' . (int) $id_product . ' AND paid_product_attribute = ' . (int) $id_product_attribute);
        } else {
            // or just adds a 'where' clause for a simple product

            $query->from('product_lang', 'pl');
            $query->where('plid_product = ' . (int) $id_product);
            $query->where('plid_lang = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl'));
        }

        return Db::getInstance()->getValue($query);
    }

    public function addWs($autodate = true, $null_values = false)
    {
        $success = $this->add($autodate, $null_values);
        if ($success && Configuration::get('PS_SEARCH_INDEXATION')) {
            Search::indexation(false, $this->id);
        }

        return $success;
    }

    public function updateWs($null_values = false)
    {
        if (null === $this->price) {
            $this->price = Product::getPriceStatic((int) $this->id, false, null, 6, null, false, true, 1, false, null, null, null, $this->specificPrice);
        }

        if (null === $this->unit_price) {
            $this->unit_price = ($this->unit_price_ratio != 0 ? $this->price / $this->unit_price_ratio : 0);
        }

        $success = parent::update($null_values);
        if ($success && Configuration::get('PS_SEARCH_INDEXATION')) {
            Search::indexation(false, $this->id);
        }
        Hook::exec('actionProductUpdate', ['id_product' => (int) $this->id]);

        return $success;
    }

    /**
     * For a given product, returns its real quantity.
     *
     * @since 150
     *
     * @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') && 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 150
     *
     * @param int $id_product
     *
     * @return bool
     */
    public static function usesAdvancedStockManagement($id_product)
    {
        $query = new DbQuery();
        $query->select('product_shopadvanced_stock_management');
        $query->from('product', 'p');
        $query->join(Shop::addSqlAssociation('product', 'p'));
        $query->where('pid_product = ' . (int) $id_product);

        return (bool) Db::getInstance()->getValue($query);
    }

    /**
     * This method allows to flush price cache.
     *
     * @since 150
     */
    public static function flushPriceCache()
    {
        self::$_prices = [];
        self::$_pricesLevel2 = [];
    }

    /**
     * Get list of parent categories.
     *
     * @since 150
     *
     * @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', 'cid_category = clid_category AND id_lang = ' . (int) $id_langShop::addSqlRestrictionOnLang('cl'));
        $sql->where('cnleft <= ' . (int) $interval['nleft'] . ' AND cnright >= ' . (int) $interval['nright']);
        $sql->orderBy('cnleft');

        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
    }

    /**
     * Fill the variables used for stock management.
     */
    public function loadStockData()
    {
        if (false === Validate::isLoadedObject($this)) {
            return;
        }

        // Default product quantity is available quantity to sell in 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);
        $this->location = StockAvailable::getLocation($this->id);

        if (Context::getContext()->shop->getContext() == Shop::CONTEXT_GROUP && Context::getContext()->shop->getContextShopGroup()->share_stock == 1) {
            $this->advanced_stock_management = $this->useAdvancedStockManagement();
        }
    }

    public function useAdvancedStockManagement()
    {
        return Db::getInstance()->getValue(
            '
                    SELECT `advanced_stock_management`
                    FROM ' . _DB_PREFIX_ . 'product_shop
                    WHERE id_product=' . (int) $this->idShop::addSqlRestriction()
                );
    }

    public function setAdvancedStockManagement($value)
    {
        $this->advanced_stock_management = (int) $value;
        if (Context::getContext()->shop->getContext() == Shop::CONTEXT_GROUP && Context::getContext()->shop->getContextShopGroup()->share_stock == 1) {
            Db::getInstance()->execute(
                '
                UPDATE `' . _DB_PREFIX_ . 'product_shop`
                SET `advanced_stock_management`=' . (int) $value . '
                WHERE id_product=' . (int) $this->idShop::addSqlRestriction()
            );
        } else {
            $this->setFieldsToUpdate(['advanced_stock_management' => true]);
            $this->save();
        }
    }

    /**
     * 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 ['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 PrestaShopCollection('ProductDownload');
        $collection_download->where('id_product', '=', $this->id);
        foreach ($collection_download as $product_download) {
            
            $result &= $product_download->delete($product_download->checkFile());
        }

        return $result;
    }

    /**
     * @deprecated 15010
     * @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 15010
     * @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 150
     *
     * @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 paid_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
        );
    }

    public static function getIdTaxRulesGroupMostUsed()
    {
        return Db::getInstance()->getValue(
            '
                    SELECT id_tax_rules_group
                    FROM (
                        SELECT COUNT(*) n, product_shopid_tax_rules_group
                        FROM ' . _DB_PREFIX_ . 'product p
                        ' . Shop::addSqlAssociation('product', 'p') . '
                        JOIN ' . _DB_PREFIX_ . 'tax_rules_group trg ON (product_shopid_tax_rules_group = trgid_tax_rules_group)
                        WHERE trgactive = 1 AND trgdeleted = 0
                        GROUP BY product_shopid_tax_rules_group
                        ORDER BY n DESC
                        LIMIT 1
                    ) most_used'
                );
    }

    /**
     * For a given ean13 reference, returns the corresponding id.
     *
     * @param string $ean13
     *
     * @return int id
     */
    public static function getIdByEan13($ean13)
    {
        if (empty($ean13)) {
            return 0;
        }

        if (!Validate::isEan13($ean13)) {
            return 0;
        }

        $query = new DbQuery();
        $query->select('pid_product');
        $query->from('product', 'p');
        $query->where('pean13 = \'' . pSQL($ean13) . '\'');

        return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query);
    }

    /**
     * For a given reference, returns the corresponding id.
     *
     * @param string $reference
     *
     * @return int id
     */
    public static function getIdByReference($reference)
    {
        if (empty($reference)) {
            return 0;
        }

        if (!Validate::isReference($reference)) {
            return 0;
        }

        $query = new DbQuery();
        $query->select('pid_product');
        $query->from('product', 'p');
        $query->where('preference = \'' . pSQL($reference) . '\'');

        return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query);
    }

    public function getWsType()
    {
        $type_information = [
            Product::PTYPE_SIMPLE => 'simple',
            Product::PTYPE_PACK => 'pack',
            Product::PTYPE_VIRTUAL => 'virtual',
        ];

        return $type_information[$this->getType()];
    }

    
    public function modifierWsLinkRewrite()
    {
        foreach ($this->name as $id_lang => $name) {
            if (empty($this->link_rewrite[$id_lang])) {
                $this->link_rewrite[$id_lang] = Tools::link_rewrite($name);
            } elseif (!Validate::isLinkRewrite($this->link_rewrite[$id_lang])) {
                $this->link_rewrite[$id_lang] = Tools::link_rewrite($this->link_rewrite[$id_lang]);
            }
        }

        return true;
    }

    public function getWsProductBundle()
    {
        return Db::getInstance()->executeS('SELECT id_product_item as id, id_product_attribute_item as id_product_attribute, quantity FROM ' . _DB_PREFIX_ . 'pack WHERE id_product_pack = ' . (int) $this->id);
    }

    public function setWsType($type_str)
    {
        $reverse_type_information = [
            'simple' => Product::PTYPE_SIMPLE,
            'pack' => Product::PTYPE_PACK,
            'virtual' => Product::PTYPE_VIRTUAL,
        ];

        if (!isset($reverse_type_information[$type_str])) {
            return false;
        }

        $type = $reverse_type_information[$type_str];

        if (Pack::isPack((int) $this->id) && $type != Product::PTYPE_PACK) {
            Pack::deleteItems($this->id);
        }

        $this->cache_is_pack = ($type == Product::PTYPE_PACK);
        $this->is_virtual = ($type == Product::PTYPE_VIRTUAL);

        return true;
    }

    public function setWsProductBundle($items)
    {
        if ($this->is_virtual) {
            return false;
        }

        Pack::deleteItems($this->id);

        foreach ($items as $item) {
            // Combination of a product is optional, and can be omittedif (!isset($item['product_attribute_id'])) {
                $item['product_attribute_id'] = 0;
            }
            if ((int) $item['id'] > 0) {
                Pack::addItem($this->id, (int) $item['id'], (int) $item['quantity'], (int) $item['product_attribute_id']);
            }
        }

        return true;
    }

    public function isColorUnavailable($id_attribute, $id_shop)
    {
        return Db::getInstance()->getValue(
            '
            SELECT said_product_attribute
            FROM ' . _DB_PREFIX_ . 'stock_available sa
            WHERE id_product=' . (int) $this->id . ' AND quantity <= 0
            ' . StockAvailable::addSqlShopRestriction(null, $id_shop, 'sa') . '
            AND EXISTS (
                SELECT 1
                FROM ' . _DB_PREFIX_ . 'product_attribute pa
                JOIN ' . _DB_PREFIX_ . 'product_attribute_shop product_attribute_shop
                    ON (product_attribute_shopid_product_attribute = paid_product_attribute AND product_attribute_shopid_shop=' . (int) $id_shop . ')
                JOIN ' . _DB_PREFIX_ . 'product_attribute_combination pac
                    ON (pacid_product_attribute AND product_attribute_shopid_product_attribute)
                WHERE said_product_attribute = paid_product_attribute AND paid_product=' . (int) $this->id . ' AND pacid_attribute=' . (int) $id_attribute . '
            )'
        );
    }

    public static function getColorsListCacheId($id_product, $full = true)
    {
        $cache_id = 'productlist_colors';
        if ($id_product) {
            $cache_id .= '|' . (int) $id_product;
        }

        if ($full) {
            $cache_id .= '|' . (int) Context::getContext()->shop->id . '|' . (int) Context::getContext()->cookie->id_lang;
        }

        return $cache_id;
    }

    public static function setPackStockType($id_product, $pack_stock_type)
    {
        return Db::getInstance()->execute('UPDATE ' . _DB_PREFIX_ . 'product p
        ' . Shop::addSqlAssociation('product', 'p') . ' SET product_shoppack_stock_type = ' . (int) $pack_stock_type . ' WHERE p.`id_product` = ' . (int) $id_product);
    }

    /**
     * Gets a list of IDs from a list of IDs/RefsThe result will avoid duplicates, and checks if given IDs/Refs exists in DB.
     * Useful when a product list should be checked before a bulk operation on them (Only 1 query => performances).
     *
     * @return array the IDs list, whithout duplicate and only existing ones
     */
    public static function getExistingIdsFromIdsOrRefs($ids_or_refs)
    {
        // separate IDs and Refs
        $ids = [];
        $refs = [];
        $whereStatements = [];
        foreach ((is_array($ids_or_refs) ? $ids_or_refs : [$ids_or_refs]) as $id_or_ref) {
            if (is_numeric($id_or_ref)) {
                $ids[] = (int) $id_or_ref;
            } elseif (is_string($id_or_ref)) {
                $refs[] = '\'' . pSQL($id_or_ref) . '\'';
            }
        }

        // construct WHERE statement with OR combination
        if (count($ids) > 0) {
            $whereStatements[] = ' pid_product IN (' . implode(',', $ids) . ') ';
        }
        if (count($refs) > 0) {
            $whereStatements[] = ' preference IN (' . implode(',', $refs) . ') ';
        }
        if (!count($whereStatements)) {
            return false;
        }

        $results = Db::getInstance()->executeS('
        SELECT DISTINCT `id_product`
        FROM `' . _DB_PREFIX_ . 'product` p
        WHERE ' . implode(' OR ', $whereStatements));

        // simplify array since there is 1 useless dimension.
        // FIXME : find a better way to avoid this, directly in SQL?
        foreach ($results as $k => $v) {
            $results[$k] = (int) $v['id_product'];
        }

        return $results;
    }

    /**
     * Get object of redirect_type.
     *
     * @return bool|string
     */
    public function getRedirectType()
    {
        switch ($this->redirect_type) {
            case ProductInterface::REDIRECT_TYPE_CATEGORY_MOVED_PERMANENTLY:
            case ProductInterface::REDIRECT_TYPE_CATEGORY_FOUND:
                return 'category';

                break;

            case ProductInterface::REDIRECT_TYPE_PRODUCT_MOVED_PERMANENTLY:
            case ProductInterface::REDIRECT_TYPE_PRODUCT_FOUND:
                return 'product';

                break;
        }

        return false;
    }

    /**
     * Return an array of customization fields IDs.
     *
     * @return array|false
     */
    public function getUsedCustomizationFieldsIds()
    {
        return Db::getInstance()->executeS(
            'SELECT cd.`index` FROM `' . _DB_PREFIX_ . 'customized_data` cd
            LEFT JOIN `' . _DB_PREFIX_ . 'customization_field` cf ON cf.`id_customization_field` = cd.`index`
            WHERE cf.`id_product` = ' . (int) $this->id
        );
    }

    /**
     * Remove unused customization for the product.
     *
     * @param array $customizationIds - Array of customization fields IDs
     *
     * @return bool
     *
     * @throws PrestaShopDatabaseException
     */
    public function deleteUnusedCustomizationFields($customizationIds)
    {
        $return = true;
        if (is_array($customizationIds) && !empty($customizationIds)) {
            $toDeleteIds = implode(',', $customizationIds);
            $return &= Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'customization_field` WHERE
            `id_product` = ' . (int) $this->id . ' AND `id_customization_field` IN (' . $toDeleteIds . ')');

            $return &= Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'customization_field_lang` WHERE
            `id_customization_field` IN (' . $toDeleteIds . ')');
        }

        if (!$return) {
            throw new PrestaShopDatabaseException('An error occurred while deletion the customization fields');
        }

        return $return;
    }

    /**
     * Update the customization fields to be deleted if not used.
     *
     * @param array $customizationIds - Array of excluded customization fields IDs
     *
     * @return bool
     *
     * @throws PrestaShopDatabaseException
     */
    public function softDeleteCustomizationFields($customizationIds)
    {
        $return = true;
        $updateQuery = 'UPDATE `' . _DB_PREFIX_ . 'customization_field` cf
            SET cf.`is_deleted` = 1
            WHERE
            cf.`id_product` = ' . (int) $this->id . '
            AND cf.`is_deleted` = 0 ';

        if (is_array($customizationIds) && !empty($customizationIds)) {
            $updateQuery .= 'AND cf.`id_customization_field` NOT IN (' . implode(',', array_map('intval', $customizationIds)) . ')';
        }

        $return &= Db::getInstance()->execute($updateQuery);

        if (!$return) {
            throw new PrestaShopDatabaseException('An error occurred while soft deletion the customization fields');
        }

        return $return;
    }

    /**
     * Update default supplier data
     *
     * @param int $idSupplier
     * @param float $wholesalePrice
     * @param string $supplierReference
     *
     * @return bool
     */
    public function updateDefaultSupplierData(int $idSupplier, string $supplierReference, float $wholesalePrice): bool
    {
        if (!$this->id) {
            return false;
        }

        $sql = 'UPDATE `' . _DB_PREFIX_ . 'product` SET id_supplier = %d, supplier_reference = "%s", wholesale_price = "%s" WHERE id_product = %d';

        return Db::getInstance()->execute(
            sprintf(
                $sql,
                $idSupplier,
                pSQL($supplierReference),
                $wholesalePrice,
                $this->id
            )
        );
    }
}

/**
 * Copyright since 2007 PrestaShop SA and Contributors
 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 30)
 * that is bundled with this package in the file LICENSEmd.
 * It is also available through the world-wide-web at this URL:
 * https://opensourceorg/licenses/OSL-30
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashopcom so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
 * versions in the futureIf you wish to customize PrestaShop for your
 * needs please refer to https://devdocsprestashopcom/ for more information.
 *
 * @author    PrestaShop SA and Contributors <contact@prestashopcom>
 * @copyright Since 2007 PrestaShop SA and Contributors
 * @license   https://opensourceorg/licenses/OSL-30 Open Software License (OSL 30)
 */

/*
 * @deprecated 1501
 */
define('_CUSTOMIZE_FILE_', 0);
/*
 * @deprecated 1501
 */
define('_CUSTOMIZE_TEXTFIELD_', 1);

use PrestaShop\PrestaShop\Adapter\ServiceLocator;
use PrestaShop\PrestaShop\Core\Product\ProductInterface;

class ProductCore extends ObjectModel
{
    /** @var string Tax name */
    public $tax_name;

    /** @var string Tax rate */
    public $tax_rate;

    /** @var int Manufacturer id */
    public $id_manufacturer;

    /** @var int Supplier id */
    public $id_supplier;

    /** @var int default Category id */
    public $id_category_default;

    /** @var int 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 int Quantity available */
    public $quantity = 0;

    /** @var int Minimal quantity for add to cart */
    public $minimal_quantity = 1;

    /** @var int|null Low stock for mail alert */
    public $low_stock_threshold = null;

    /** @var bool Low stock mail alert activated */
    public $low_stock_alert = false;

    /** @var string available_now */
    public $available_now;

    /** @var string available_later */
    public $available_later;

    /** @var float Price in euros */
    public $price = 0;

    public $specificPrice = 0;

    /** @var float Additional shipping cost */
    public $additional_shipping_cost = 0;

    /** @var float Wholesale Price in euros */
    public $wholesale_price = 0;

    /** @var bool on_sale */
    public $on_sale = false;

    /** @var bool 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
     *
     * @deprecated since 1770
     */
    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 ISBN */
    public $isbn;

    /** @var string Upc barcode */
    public $upc;

    /** @var string MPN */
    public $mpn;

    /** @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 bool Product statuts */
    public $quantity_discount = 0;

    /** @var bool Product customization */
    public $customizable;

    /** @var bool Product is new */
    public $new = null;

    /** @var int Number of uploadable files (concerning customizable products) */
    public $uploadable_files;

    /** @var int Number of text fields */
    public $text_fields;

    /** @var bool Product statuts */
    public $active = true;

    /** @var bool Product statuts */
    public $redirect_type = '';

    /** @var bool Product statuts */
    public $id_type_redirected = 0;

    /** @var bool Product available for order */
    public $available_for_order = true;

    /** @var string Object available order date */
    public $available_date = '0000-00-00';

    /** @var bool Will the condition select should be visible for this product ? */
    public $show_condition = false;

    /** @var string Enumerated (enum) product condition (new, used, refurbished) */
    public $condition;

    /** @var bool Show price of Product */
    public $show_price = true;

    /** @var bool 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;

    /** @var int temporary or saved object */
    public $state = self::STATE_SAVED;

    /**
     * @var float Base price of the product
     *
     * @deprecated 16013
     */
    public $base_price;

    public $id_tax_rules_group = 1;

    /**
     * We keep this variable for retrocompatibility for themes.
     *
     * @deprecated 150
     */
    public $id_color_default = 0;

    /**
     * @since 150
     *
     * @var bool 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 $id_pack_product_attribute;
    public $cache_default_attribute;

    /**
     * @var string If product is populated, this property contain the rewrite link of the default category
     */
    public $category;

    /**
     * @var int tell the type of stock management to apply on the pack
     */
    public $pack_stock_type = Pack::STOCK_TYPE_DEFAULT;

    /**
     * Type of delivery time.
     *
     * Choose which parameters use for give information delivery.
     * 0 - none
     * 1 - use default information
     * 2 - use product information
     *
     * @var int
     */
    public $additional_delivery_times = 1;

    /**
     * Delivery in-stock information.
     *
     * Long description for delivery in-stock product information.
     *
     * @var string
     */
    public $delivery_in_stock;

    /**
     * Delivery out-stock information.
     *
     * Long description for delivery out-stock product information.
     *
     * @var string
     */
    public $delivery_out_stock;

    public static $_taxCalculationMethod = null;
    protected static $_prices = [];
    protected static $_pricesLevel2 = [];
    protected static $_incat = [];

    /**
     * @since 1561
     *
     * @var array is deprecated since 1561
     */
    protected static $_cart_quantity = [];

    protected static $_tax_rules_group = [];
    protected static $_cacheFeatures = [];
    protected static $_frontFeaturesCache = [];
    protected static $productPropertiesCache = [];

    /** @var array cache stock data in getStock() method */
    protected static $cacheStock = [];

    const STATE_TEMP = 0;
    const STATE_SAVED = 1;

    public static $definition = [
        'table' => 'product',
        'primary' => 'id_product',
        'multilang' => true,
        'multilang_shop' => true,
        'fields' => [
            
            'id_shop_default' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'id_manufacturer' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'id_supplier' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'reference' => ['type' => self::TYPE_STRING, 'validate' => 'isReference', 'size' => 64],
            'supplier_reference' => ['type' => self::TYPE_STRING, 'validate' => 'isReference', 'size' => 64],
            'location' => ['type' => self::TYPE_STRING, 'validate' => 'isReference', 'size' => 64],
            'width' => ['type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'],
            'height' => ['type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'],
            'depth' => ['type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'],
            'weight' => ['type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'],
            'quantity_discount' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
            'ean13' => ['type' => self::TYPE_STRING, 'validate' => 'isEan13', 'size' => 13],
            'isbn' => ['type' => self::TYPE_STRING, 'validate' => 'isIsbn', 'size' => 32],
            'upc' => ['type' => self::TYPE_STRING, 'validate' => 'isUpc', 'size' => 12],
            'mpn' => ['type' => self::TYPE_STRING, 'validate' => 'isMpn', 'size' => 40],
            'cache_is_pack' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
            'cache_has_attachments' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
            'is_virtual' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
            'state' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'additional_delivery_times' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId'],
            'delivery_in_stock' => [
                'type' => self::TYPE_STRING,
                'lang' => true,
                'validate' => 'isGenericName',
                'size' => 255,
            ],
            'delivery_out_stock' => [
                'type' => self::TYPE_STRING,
                'lang' => true,
                'validate' => 'isGenericName',
                'size' => 255,
            ],

            
            'id_category_default' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'],
            'id_tax_rules_group' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'],
            'on_sale' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'online_only' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'ecotax' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice'],
            'minimal_quantity' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'],
            'low_stock_threshold' => ['type' => self::TYPE_INT, 'shop' => true, 'allow_null' => true, 'validate' => 'isInt'],
            'low_stock_alert' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'price' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice', 'required' => true],
            'wholesale_price' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice'],
            'unity' => ['type' => self::TYPE_STRING, 'shop' => true, 'validate' => 'isString'],
            'unit_price_ratio' => ['type' => self::TYPE_FLOAT, 'shop' => true],
            'additional_shipping_cost' => ['type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice'],
            'customizable' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'],
            'text_fields' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'],
            'uploadable_files' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'],
            'active' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'redirect_type' => ['type' => self::TYPE_STRING, 'shop' => true, 'validate' => 'isString'],
            'id_type_redirected' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'],
            'available_for_order' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'available_date' => ['type' => self::TYPE_DATE, 'shop' => true, 'validate' => 'isDateFormat'],
            'show_condition' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'condition' => ['type' => self::TYPE_STRING, 'shop' => true, 'validate' => 'isGenericName', 'values' => ['new', 'used', 'refurbished'], 'default' => 'new'],
            'show_price' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'indexed' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'visibility' => ['type' => self::TYPE_STRING, 'shop' => true, 'validate' => 'isProductVisibility', 'values' => ['both', 'catalog', 'search', 'none'], 'default' => 'both'],
            'cache_default_attribute' => ['type' => self::TYPE_INT, 'shop' => true],
            'advanced_stock_management' => ['type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'],
            'date_add' => ['type' => self::TYPE_DATE, 'shop' => true, 'validate' => 'isDate'],
            'date_upd' => ['type' => self::TYPE_DATE, 'shop' => true, 'validate' => 'isDate'],
            'pack_stock_type' => ['type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'],

            
            'meta_description' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 512],
            'meta_keywords' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255],
            'meta_title' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255],
            'link_rewrite' => [
                'type' => self::TYPE_STRING,
                'lang' => true,
                'validate' => 'isLinkRewrite',
                'required' => false,
                'size' => 128,
                'ws_modifier' => [
                    'http_method' => WebserviceRequest::HTTP_POST,
                    'modifier' => 'modifierWsLinkRewrite',
                ],
            ],
            'name' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isCatalogName', 'required' => false, 'size' => 128],
            'description' => ['type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml'],
            'description_short' => ['type' => self::TYPE_HTML, 'lang' => true, 'validate' => 'isCleanHtml'],
            'available_now' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'size' => 255],
            'available_later' => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'IsGenericName', 'size' => 255],
        ],
        'associations' => [
            'manufacturer' => ['type' => self::HAS_ONE],
            'supplier' => ['type' => self::HAS_ONE],
            'default_category' => ['type' => self::HAS_ONE, 'field' => 'id_category_default', 'object' => 'Category'],
            'tax_rules_group' => ['type' => self::HAS_ONE],
            'categories' => ['type' => self::HAS_MANY, 'field' => 'id_category', 'object' => 'Category', 'association' => 'category_product'],
            'stock_availables' => ['type' => self::HAS_MANY, 'field' => 'id_stock_available', 'object' => 'StockAvailable', 'association' => 'stock_availables'],
        ],
    ];

    protected $webserviceParameters = [
        'objectMethods' => [
            'add' => 'addWs',
            'update' => 'updateWs',
        ],
        'objectNodeNames' => 'products',
        'fields' => [
            'id_manufacturer' => [
                'xlink_resource' => 'manufacturers',
            ],
            'id_supplier' => [
                'xlink_resource' => 'suppliers',
            ],
            'id_category_default' => [
                'xlink_resource' => 'categories',
            ],
            'new' => [],
            'cache_default_attribute' => [],
            'id_default_image' => [
                'getter' => 'getCoverWs',
                'setter' => 'setCoverWs',
                'xlink_resource' => [
                    'resourceName' => 'images',
                    'subResourceName' => 'products',
                ],
            ],
            'id_default_combination' => [
                'getter' => 'getWsDefaultCombination',
                'setter' => 'setWsDefaultCombination',
                'xlink_resource' => [
                    'resourceName' => 'combinations',
                ],
            ],
            'id_tax_rules_group' => [
                'xlink_resource' => [
                    'resourceName' => 'tax_rule_groups',
                ],
            ],
            'position_in_category' => [
                'getter' => 'getWsPositionInCategory',
                'setter' => 'setWsPositionInCategory',
            ],
            'manufacturer_name' => [
                'getter' => 'getWsManufacturerName',
                'setter' => false,
            ],
            'quantity' => [
                'getter' => false,
                'setter' => false,
            ],
            'type' => [
                'getter' => 'getWsType',
                'setter' => 'setWsType',
            ],
        ],
        'associations' => [
            'categories' => [
                'resource' => 'category',
                'fields' => [
                    'id' => ['required' => true],
                ],
            ],
            'images' => [
                'resource' => 'image',
                'fields' => ['id' => []],
            ],
            'combinations' => [
                'resource' => 'combination',
                'fields' => [
                    'id' => ['required' => true],
                ],
            ],
            'product_option_values' => [
                'resource' => 'product_option_value',
                'fields' => [
                    'id' => ['required' => true],
                ],
            ],
            'product_features' => [
                'resource' => 'product_feature',
                'fields' => [
                    'id' => ['required' => true],
                    'id_feature_value' => [
                        'required' => true,
                        'xlink_resource' => 'product_feature_values',
                    ],
                ],
            ],
            'tags' => ['resource' => 'tag',
                'fields' => [
                    'id' => ['required' => true],
                ], ],
            'stock_availables' => ['resource' => 'stock_available',
                'fields' => [
                    'id' => ['required' => true],
                    'id_product_attribute' => ['required' => true],
                ],
                'setter' => false,
            ],
            'accessories' => [
                'resource' => 'product',
                'api' => 'products',
                'fields' => [
                    'id' => [
                        'required' => true,
                        'xlink_resource' => 'product', ],
                ],
            ],
            'product_bundle' => [
                'resource' => 'product',
                'api' => 'products',
                'fields' => [
                    'id' => ['required' => true],
                    'id_product_attribute' => [],
                    'quantity' => [],
                ],
            ],
        ],
    ];

    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 ($full && $this->id) {
            if (!$context) {
                $context = Context::getContext();
            }

            $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();

            // Keep base price
            $this->base_price = $this->price;

            $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);
            $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 (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;
        }
        $fields['unity'] = pSQL($this->unity);

        return $fields;
    }

    public function add($autodate = true, $null_values = false)
    {
        if (!parent::add($autodate, $null_values)) {
            return false;
        }

        $id_shop_list = Shop::getContextListShopID();
        if ($this->getType() == Product::PTYPE_VIRTUAL) {
            foreach ($id_shop_list as $value) {
                StockAvailable::setProductOutOfStock((int) $this->id, 1, $value);
            }

            if ($this->active && !Configuration::get('PS_VIRTUAL_PROD_FEATURE_ACTIVE')) {
                Configuration::updateGlobalValue('PS_VIRTUAL_PROD_FEATURE_ACTIVE', '1');
            }
        } else {
            foreach ($id_shop_list as $value) {
                StockAvailable::setProductOutOfStock((int) $this->id, 2, $value);
            }
        }

        $this->setGroupReduction();
        Hook::exec('actionProductSave', ['id_product' => (int) $this->id, 'product' => $this]);

        return true;
    }

    public function update($null_values = false)
    {
        $return = parent::update($null_values);
        $this->setGroupReduction();

        // Sync stock Reference, EAN13, MPN and UPC
        if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && StockAvailable::dependsOnStock($this->id, Context::getContext()->shop->id)) {
            Db::getInstance()->update('stock', [
                'reference' => pSQL($this->reference),
                'ean13' => pSQL($this->ean13),
                'isbn' => pSQL($this->isbn),
                'upc' => pSQL($this->upc),
                'mpn' => pSQL($this->mpn),
            ], 'id_product = ' . (int) $this->id . ' AND id_product_attribute = 0');
        }

        Hook::exec('actionProductSave', ['id_product' => (int) $this->id, 'product' => $this]);
        Hook::exec('actionProductUpdate', ['id_product' => (int) $this->id, 'product' => $this]);
        if ($this->getType() == Product::PTYPE_VIRTUAL && $this->active && !Configuration::get('PS_VIRTUAL_PROD_FEATURE_ACTIVE')) {
            Configuration::updateGlobalValue('PS_VIRTUAL_PROD_FEATURE_ACTIVE', '1');
        }

        return $return;
    }

    /**
     * Init computation of price display method (ieprice should be including tax or not) for a customer.
     * If customer Id passed as null then this compute price display method with according of current group.
     * Otherwise a price display method will compute with according of a customer address (iecountry).
     *
     * @see Group::getPriceDisplayMethod()
     *
     * @param int|null $id_customer
     */
    public static function initPricesComputation($id_customer = null)
    {
        if ((int) $id_customer > 0) {
            $customer = new Customer((int) $id_customer);
            if (!Validate::isLoadedObject($customer)) {
                die(Tools::displayError());
            }
            self::$_taxCalculationMethod = Group::getPriceDisplayMethod((int) $customer->id_default_group);
            $cur_cart = Context::getContext()->cart;
            $id_address = 0;
            if (Validate::isLoadedObject($cur_cart)) {
                $id_address = (int) $cur_cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')};
            }
            $address_infos = Address::getCountryAndState($id_address);

            if (self::$_taxCalculationMethod != PS_TAX_EXC
                && !empty($address_infos['vat_number'])
                && $address_infos['id_country'] != Configuration::get('VATNUMBER_COUNTRY')
                && Configuration::get('VATNUMBER_MANAGEMENT')) {
                self::$_taxCalculationMethod = PS_TAX_EXC;
            }
        } else {
            self::$_taxCalculationMethod = Group::getPriceDisplayMethod(Group::getCurrent()->id);
        }
    }

    /**
     * Returns price display method for a customer (ieprice should be including tax or not).
     *
     * @see initPricesComputation()
     *
     * @param int|null $id_customer
     *
     * @return int Returns 0 (PS_TAX_INC) if tax should be included, otherwise 1 (PS_TAX_EXC) - tax should be excluded
     */
    public static function getTaxCalculationMethod($id_customer = null)
    {
        if (self::$_taxCalculationMethod === null || $id_customer !== null) {
            Product::initPricesComputation($id_customer);
        }

        return (int) self::$_taxCalculationMethod;
    }

    /**
     * Move a product inside its category.
     *
     * @param bool $way Up (1)  or Down (0)
     * @param int $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
        $result = (
            Db::getInstance()->execute('
            UPDATE `' . _DB_PREFIX_ . 'category_product` cp
            INNER JOIN `' . _DB_PREFIX_ . 'product` p ON (p.`id_product` = cp.`id_product`)
            ' . Shop::addSqlAssociation('product', 'p') . '
            SET cp.`position`= `position` ' . ($way ? '- 1' : '+ 1') . ',
            p.`date_upd` = "' . date('Y-m-d H:i:s') . '", product_shop.`date_upd` = "' . date('Y-m-d H:i:s') . '"
            WHERE cp.`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` cp
            INNER JOIN `' . _DB_PREFIX_ . 'product` p ON (p.`id_product` = cp.`id_product`)
            ' . Shop::addSqlAssociation('product', 'p') . '
            SET cp.`position` = ' . (int) $position . ',
            p.`date_upd` = "' . date('Y-m-d H:i:s') . '", product_shop.`date_upd` = "' . date('Y-m-d H:i:s') . '"
            WHERE cp.`id_product` = ' . (int) $moved_product['id_product'] . '
            AND cp.`id_category`=' . (int) $moved_product['id_category'])
        );
        Hook::exec('actionProductUpdate', ['id_product' => (int) $this->id, 'product' => $this]);

        return $result;
    }

    /**
     * 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, $position = 0)
    {
        $return = true;

        if (!(int) $position) {
            $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',
                    ['position' => $i],
                    '`id_category` = ' . (int) $id_category . ' AND `id_product` = ' . (int) $result[$i]['id_product']
                );
                $return &= Db::getInstance()->execute(
                    'UPDATE `' . _DB_PREFIX_ . 'product` p' . Shop::addSqlAssociation('product', 'p') . '
                    SET p.`date_upd` = "' . date('Y-m-d H:i:s') . '", product_shop.`date_upd` = "' . date('Y-m-d H:i:s') . '"
                    WHERE p.`id_product` = ' . (int) $result[$i]['id_product']
                );
            }
        } else {
            $result = Db::getInstance()->executeS('
                SELECT `id_product`
                FROM `' . _DB_PREFIX_ . 'category_product`
                WHERE `id_category` = ' . (int) $id_category . ' AND `position` > ' . (int) $position . '
                ORDER BY `position`
            ');
            $total = count($result);
            $return &= Db::getInstance()->update(
                'category_product',
                ['position' => ['type' => 'sql', 'value' => '`position`-1']],
                '`id_category` = ' . (int) $id_category . ' AND `position` > ' . (int) $position
            );

            for ($i = 0; $i < $total; ++$i) {
                $return &= Db::getInstance()->execute(
                    'UPDATE `' . _DB_PREFIX_ . 'product` p' . Shop::addSqlAssociation('product', 'p') . '
                    SET p.`date_upd` = "' . date('Y-m-d H:i:s') . '", product_shop.`date_upd` = "' . date('Y-m-d H:i:s') . '"
                    WHERE p.`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, $reset = false)
    {
        static $combinations = [];

        if (!Combination::isFeatureActive()) {
            return 0;
        }

        if ($reset && isset($combinations[$id_product])) {
            unset($combinations[$id_product]);
        }

        if (!isset($combinations[$id_product])) {
            $combinations[$id_product] = [];
        }
        if (isset($combinations[$id_product][$minimum_quantity])) {
            return $combinations[$id_product][$minimum_quantity];
        }

        'SELECT' = 'SELECT product_attribute_shopid_product_attribute
                FROM ' . _DB_PREFIX_ . 'product_attribute pa
                ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
                WHERE paid_product = ' . (int) $id_product;

        $result_no_filter = Db::getInstance()->getValue('SELECT');
        if (!$result_no_filter) {
            $combinations[$id_product][$minimum_quantity] = 0;

            return 0;
        }

        'SELECT' = 'SELECT product_attribute_shopid_product_attribute
                FROM ' . _DB_PREFIX_ . 'product_attribute pa
                ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
                ' . ($minimum_quantity > 0 ? Product::sqlStock('pa', 'pa') : '') .
                ' WHERE product_attribute_shopdefault_on = 1 '
                . ($minimum_quantity > 0 ? ' AND IFNULL(stockquantity, 0) >= ' . (int) $minimum_quantity : '') .
                ' AND paid_product = ' . (int) $id_product;
        $result = Db::getInstance()->getValue('SELECT');

        if (!$result) {
            'SELECT' = 'SELECT product_attribute_shopid_product_attribute
                    FROM ' . _DB_PREFIX_ . 'product_attribute pa
                    ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
                    ' . ($minimum_quantity > 0 ? Product::sqlStock('pa', 'pa') : '') .
                    ' WHERE paid_product = ' . (int) $id_product
                    . ($minimum_quantity > 0 ? ' AND IFNULL(stockquantity, 0) >= ' . (int) $minimum_quantity : '');

            $result = Db::getInstance()->getValue('SELECT');
        }

        if (!$result) {
            'SELECT' = 'SELECT product_attribute_shopid_product_attribute
                    FROM ' . _DB_PREFIX_ . 'product_attribute pa
                    ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
                    WHERE product_attribute_shop.`default_on` = 1
                    AND paid_product = ' . (int) $id_product;

            $result = Db::getInstance()->getValue('SELECT');
        }

        if (!$result) {
            $result = $result_no_filter;
        }

        $combinations[$id_product][$minimum_quantity] = $result;

        return $result;
    }

    public function setAvailableDate($available_date = '0000-00-00')
    {
        if (Validate::isDateFormat($available_date) && $this->available_date != $available_date) {
            $this->available_date = $available_date;

            return $this->update();
        }

        return false;
    }

    /**
     * For a given id_product and id_product_attribute, return available date.
     *
     * @param int $id_product
     * @param int $id_product_attribute Optional
     *
     * @return string/null
     */
    public static function getAvailableDate($id_product, $id_product_attribute = null)
    {
        

        if ($id_product_attribute === null) {
            'SELECT' .= ' p.`available_date`';
        } else {
            'SELECT' .= ' pa.`available_date`';
        }

        'SELECT' .= ' FROM `' . _DB_PREFIX_ . 'product` p';

        if ($id_product_attribute !== null) {
            'SELECT' .= ' LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON (pa.`id_product` = p.`id_product`)';
        }

        'SELECT' .= Shop::addSqlAssociation('product', 'p');

        if ($id_product_attribute !== null) {
            'SELECT' .= Shop::addSqlAssociation('product_attribute', 'pa');
        }

        'SELECT' .= ' WHERE p.`id_product` = ' . (int) $id_product;

        if ($id_product_attribute !== null) {
            'SELECT' .= ' AND pa.`id_product` = ' . (int) $id_product . ' AND pa.`id_product_attribute` = ' . (int) $id_product_attribute;
        }

        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('SELECT');

        if ($result == '0000-00-00') {
            $result = null;
        }

        return $result;
    }

    public static function updateIsVirtual($id_product, $is_virtual = true)
    {
        Db::getInstance()->update('product', [
            'is_virtual' => (bool) $is_virtual,
        ], 'id_product = ' . (int) $id_product);
    }

    /**
     * @see ObjectModel::resetStaticCache()
     *
     * reset static cache (eg unit testing purpose).
     */
    public static function resetStaticCache()
    {
        static::$loaded_classes = [];
        static::$productPropertiesCache = [];
        static::$_cacheFeatures = [];
        static::$_frontFeaturesCache = [];
        static::$_prices = [];
        static::$_pricesLevel2 = [];
        static::$_incat = [];
    }

    /**
     * @see ObjectModel::validateField()
     */
    public function validateField($field, $value, $id_lang = null, $skip = [], $human_errors = false)
    {
        if ($field == 'description_short') {
            $limit = (int) Configuration::get('PS_PRODUCT_SHORT_DESC_LIMIT');
            if ($limit <= 0) {
                $limit = 800;
            }

            $size_without_html = Tools::strlen(strip_tags($value));
            $size_with_html = Tools::strlen($value);
            $this->def['fields']['description_short']['size'] = $limit + $size_with_html - $size_without_html;
        }

        return parent::validateField($field, $value, $id_lang, $skip, $human_errors);
    }

    public function toggleStatus()
    {
        //test if the product is active and if redirect_type is empty string and set default value to id_type_redirected & redirect_type
        //  /!\ after parent::toggleStatus() active will be false, that why we set 404 by default :p
        if ($this->active) {
            //case where active will be false after parent::toggleStatus()
            $this->id_type_redirected = 0;
            $this->redirect_type = ProductInterface::REDIRECT_TYPE_CATEGORY_MOVED_PERMANENTLY;
        } else {
            //case where active will be true after parent::toggleStatus()
            $this->id_type_redirected = 0;
            $this->redirect_type = '';
        }

        return parent::toggleStatus();
    }

    public function delete()
    {
        /*
         * @since 150
         * 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;
            }

            $warehouse_product_locations = ServiceLocator::get('\\PrestaShop\\PrestaShop\\Core\\Foundation\\Database\\EntityManager')->getRepository('WarehouseProductLocation')->findByIdProduct($this->id);
            foreach ($warehouse_product_locations as $warehouse_product_location) {
                $warehouse_product_location->delete();
            }

            $stocks = ServiceLocator::get('\\PrestaShop\\PrestaShop\\Core\\Foundation\\Database\\EntityManager')->getRepository('Stock')->findByIdProduct($this->id);
            foreach ($stocks as $stock) {
                $stock->delete();
            }
        }
        $result = parent::delete();

        // Removes the product from StockAvailable, for the current shop
        StockAvailable::removeProductFromStockAvailable($this->id);
        $result &= ($this->deleteProductAttributes() && $this->deleteImages());
        // If there are still entries in product_shop, don't remove completely the product
        if ($this->hasMultishopEntries()) {
            return true;
        }

        Hook::exec('actionProductDelete', ['id_product' => (int) $this->id, 'product' => $this]);
        if (!$result ||
            !GroupReduction::deleteProductReduction($this->id) ||
            !$this->deleteCategories(true) ||
            !$this->deleteProductFeatures() ||
            !$this->deleteTags() ||
            !$this->deleteCartProducts() ||
            !$this->deleteAttributesImpacts() ||
            !$this->deleteAttachments(false) ||
            !$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 serverLet's say 15 seconds by product (I've seen it!).
            if ((int) (ini_get('max_execution_time')) < round($count * 15)) {
                ini_set('max_execution_time', round($count * 15));
            }

            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 bool true if succeed
     */
    public function addToCategories($categories = [])
    {
        if (empty($categories)) {
            return false;
        }

        if (!is_array($categories)) {
            $categories = [$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 = [];
        // The first position must be 1 instead of 0
        foreach ($categories as $id_category) {
            $new_categ_pos[$id_category] = isset($new_categories[$id_category]) ? $new_categories[$id_category] : 1;
        }

        $product_cats = [];

        foreach ($categories as $new_id_categ) {
            if (!in_array($new_id_categ, $current_categories)) {
                $product_cats[] = [
                    '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);

        Cache::clean('Product::getProductCategories_' . (int) $this->id);

        return true;
    }

    /**
     * Update categories to index product into.
     *
     * @param string $productCategories Categories list to index product into
     * @param bool $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, null, true) . '
            WHERE cp.`id_category` NOT IN (' . implode(',', array_map('intval', $categories)) . ')
            AND cpid_product = ' . (int) $this->id
        );

        // if none are found, it's an error
        if (!is_array($result)) {
            return false;
        }

        foreach ($result as $categ_to_delete) {
            $this->deleteCategory($categ_to_delete['id_category']);
        }

        if (!$this->addToCategories($categories)) {
            return false;
        }

        SpecificPriceRule::applyAllRules([(int) $this->id]);

        Cache::clean('Product::getProductCategories_' . (int) $this->id);

        return true;
    }

    /**
     * deleteCategory delete this product from the category $id_category.
     *
     * @param mixed $id_category
     * @param mixed $clean_positions
     *
     * @return bool
     */
    public function deleteCategory($id_category, $clean_positions = true)
    {
        $result = Db::getInstance()->executeS(
            'SELECT `id_category`, `position`
            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) {
                self::cleanPositions((int) $row['id_category'], (int) $row['position']);
            }
        }

        SpecificPriceRule::applyAllRules([(int) $this->id]);

        Cache::clean('Product::getProductCategories_' . (int) $this->id);

        return $return;
    }

    /**
     * Delete all association to category where product is indexed.
     *
     * @param bool $clean_positions clean category positions after deletion
     *
     * @return array Deletion result
     */
    public function deleteCategories($clean_positions = false)
    {
        if ($clean_positions === true) {
            $result = Db::getInstance()->executeS(
                'SELECT `id_category`, `position`
                FROM `' . _DB_PREFIX_ . 'category_product`
                WHERE `id_product` = ' . (int) $this->id
            );
        }

        $return = Db::getInstance()->delete('category_product', 'id_product = ' . (int) $this->id);
        if ($clean_positions === true && is_array($result)) {
            foreach ($result as $row) {
                $return &= self::cleanPositions((int) $row['id_category'], (int) $row['position']);
            }
        }

        Cache::clean('Product::getProductCategories_' . (int) $this->id);

        return $return;
    }

    /**
     * Delete products tags entries.
     *
     * @return array Deletion result
     */
    public function deleteTags()
    {
        return Tag::deleteTagsForProduct((int) $this->id);
    }

    /**
     * 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;
    }

    /**
     * Get all available products.
     *
     * @param int $id_lang Language id
     * @param int $start Start number
     * @param int $limit Number of products to return
     * @param string $order_by Field for ordering
     * @param string 'DESC' Way for ordering (ASC or DESC)
     *
     * @return array Products details
     */
    public static function getProducts(
        $id_lang,
        $start,
        $limit,
        $order_by,
        'DESC',
        $id_category = false,
        $only_active = false,
        Context $context = null
    ) {
        if (!$context) {
            $context = Context::getContext();
        }

        $front = true;
        if (!in_array($context->controller->controller_type, ['front', 'modulefront'])) {
            $front = false;
        }

        if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay('DESC')) {
            die(Tools::displayError());
        }
        if (
        } elseif (
        } elseif (
        }

        if (strpos($order_by, ) > 0) {
            
            $order_by_prefix = "=";
            
        }
        'SELECT' = 'SELECT p.*, product_shop.*, pl.* , 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_ . '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('DESC') .
                ($limit > 0 ? ' LIMIT ' . (int) $start . ',' . (int) $limit : '');
        $rq = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT');
        if (
        }

        foreach ($rq as &$row) {
            $row = Product::getTaxesInformations($row);
        }

        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, ['front', 'modulefront'])) {
            $front = false;
        }

        'SELECT' = '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('SELECT');
    }

    public function isNew()
    {
        $result = Db::getInstance()->executeS('
            SELECT pid_product
            FROM `' . _DB_PREFIX_ . 'product` p
            ' . Shop::addSqlAssociation('product', 'p') . '
            WHERE pid_product = ' . (int) $this->id . '
            AND DATEDIFF(
                product_shop.`date_add`,
                DATE_SUB(
                    "' . date('Y-m-d') . ' 00:00:00",
                    INTERVAL ' . (Validate::isUnsignedInt(Configuration::get('PS_NB_DAYS_NEW_PRODUCT')) ? Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20) . ' DAY
                )
            ) > 0
        ', true, false);

        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 (pasid_product_attribute = paid_product_attribute)
            LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON (pac.`id_product_attribute` = pa.`id_product_attribute`)
            WHERE 1 ' . (!$all_shops ? ' AND pasid_shop =' . (int) $context->shop->id : '') . ' AND pa.`id_product` = ' . (int) $this->id .
            ($all_shops ? ' GROUP BY pacid_attribute, pacid_product_attribute ' : '')
        );

        
        if (!$result || empty($result)) {
            return false;
        }
        
        $product_attributes = [];
        foreach ($result as $product_attribute) {
            $product_attributes[$product_attribute['id_product_attribute']][] = $product_attribute['id_attribute'];
        }
        
        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 150
     */
    public function addProductAttribute(
        $price,
        $weight,
        $unit_impact,
        $ecotax,
        $quantity,
        $id_images,
        $reference,
        $id_supplier,
        $ean13,
        $default,
        $location,
        $upc,
        $minimal_quantity,
        $isbn,
        $low_stock_threshold = null,
        $low_stock_alert = false,
        $mpn = null
    ) {
        Tools::displayAsDeprecated();

        $id_product_attribute = $this->addAttribute(
            $price,
            $weight,
            $unit_impact,
            $ecotax,
            $id_images,
            $reference,
            $ean13,
            $default,
            $location,
            $upc,
            $minimal_quantity,
            [],
            null,
            0,
            $isbn,
            $low_stock_threshold,
            $low_stock_alert,
            $mpn
        );

        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, $resetExistingCombination = true)
    {
        $res = true;
        foreach ($combinations as $key => $combination) {
            $id_combination = (int) $this->productAttributeExists($attributes[$key], false, null, true, true);
            if ($id_combination && !$resetExistingCombination) {
                continue;
            }

            $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->default_on = 0;
            $this->setAvailableDate();

            $obj->save();

            if (!$id_combination) {
                $attribute_list = [];
                foreach ($attributes[$key] as $id_attribute) {
                    $attribute_list[] = [
                        'id_product_attribute' => (int) $obj->id,
                        'id_attribute' => (int) $id_attribute,
                    ];
                }
                $res &= Db::getInstance()->insert('product_attribute_combination', $attribute_list);
            }
        }

        return $res;
    }

    public function sortCombinationByAttributePosition($combinations, $langId)
    {
        $attributes = [];
        foreach ($combinations as $combinationId) {
            $attributeCombination = $this->getAttributeCombinationsById($combinationId, $langId);
            $attributes["="['position']][$combinationId] = "=";
        }

        ksort($attributes);

        return $attributes;
    }

    /**
     * @param int $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 = [],
        $available_date = null,
        $isbn = '',
        $low_stock_threshold = null,
        $low_stock_alert = false,
        $mpn = null
    ) {
        $id_product_attribute = $this->addAttribute(
            $price,
            $weight,
            $unit_impact,
            $ecotax,
            $id_images,
            $reference,
            $ean13,
            $default,
            $location,
            $upc,
            $minimal_quantity,
            $id_shop_list,
            $available_date,
            0,
            $isbn,
            $low_stock_threshold,
            $low_stock_alert,
            $mpn
        );
        $this->addSupplierReference($id_supplier, $id_product_attribute);
        $result = ObjectModel::updateMultishopTable('Combination', [
            'wholesale_price' => (float) $wholesale_price,
        ], 'aid_product_attribute = ' . (int) $id_product_attribute);

        if (!$id_product_attribute || !$result) {
            return false;
        }

        return $id_product_attribute;
    }

    /**
     * @deprecated 1550
     *
     * @param $attributes
     * @param bool $set_default
     *
     * @return array
     */
    public function addProductAttributeMultiple($attributes, $set_default = true)
    {
        Tools::displayAsDeprecated();
        $return = [];
        $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 = [];
                    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', [
            'default_on' => null,
        ], 'a.`id_product` = ' . (int) $this->id);
    }

    public function setDefaultAttribute($id_product_attribute)
    {
        $result = ObjectModel::updateMultishopTable('Combination', [
            'default_on' => 1,
        ], 'a.`id_product` = ' . (int) $this->id . ' AND a.`id_product_attribute` = ' . (int) $id_product_attribute);

        $result &= ObjectModel::updateMultishopTable('product', [
            'cache_default_attribute' => (int) $id_product_attribute,
        ], 'a.`id_product` = ' . (int) $this->id);
        $this->cache_default_attribute = (int) $id_product_attribute;

        return $result;
    }

    public static function updateDefaultAttribute($id_product)
    {
        $id_default_attribute = (int) Product::getDefaultAttribute($id_product, 0, true);

        $result = Db::getInstance()->update('product_shop', [
            'cache_default_attribute' => $id_default_attribute,
        ], 'id_product = ' . (int) $id_productShop::addSqlRestriction());

        $result &= Db::getInstance()->update('product', [
            'cache_default_attribute' => $id_default_attribute,
        ], 'id_product = ' . (int) $id_product);

        if ($result && $id_default_attribute) {
            return $id_default_attribute;
        } else {
            return $result;
        }
    }

    /**
     * Update a product attribute.
     *
     * @deprecated since 15
     * @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,
        $ean13,
        $default,
        $location,
        $upc,
        $minimal_quantity,
        $available_date,
        $isbn = '',
        $low_stock_threshold = null,
        $low_stock_alert = false,
        $mpn = null
    ) {
        Tools::displayAsDeprecated('Use updateAttribute() instead');

        $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,
            true,
            [],
            $isbn,
            $low_stock_threshold,
            $low_stock_alert,
            $mpn = null
        );
        $this->addSupplierReference($id_supplier, $id_product_attribute);

        return $return;
    }

    /**
     * Sets or updates 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) && ($this->id > 0)) {
            $id_product_supplier = (int) ProductSupplier::getIdByProductAndSupplier($this->id, $id_product_attribute, $id_supplier);

            $product_supplier = new ProductSupplier($id_product_supplier);

            if (!$id_product_supplier) {
                $product_supplier->id_product = (int) $this->id;
                $product_supplier->id_product_attribute = (int) $id_product_attribute;
                $product_supplier->id_supplier = (int) $id_supplier;
            }

            $product_supplier->product_supplier_reference = pSQL($supplier_reference);
            $product_supplier->product_supplier_price_te = null !== $price ? (float) $price : (float) $product_supplier->product_supplier_price_te;
            $product_supplier->id_currency = null !== $id_currency ? (int) $id_currency : (int) $product_supplier->id_currency;
            $product_supplier->save();
        }
    }

    /**
     * Update a product attribute.
     *
     * @param int $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 int $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
     * @param string $isbn ISBN reference
     * @param int|null $low_stock_threshold Low stock alert
     * @param bool $low_stock_alert send email on low stock
     * @param string $mpn MPN
     *
     * @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 = [],
        $isbn = '',
        $low_stock_threshold = null,
        $low_stock_alert = false,
        $mpn = null
    ) {
        $combination = new Combination($id_product_attribute);

        if (!$update_all_fields) {
            $combination->setFieldsToUpdate([
                'price' => null !== $price,
                'wholesale_price' => null !== $wholesale_price,
                'ecotax' => null !== $ecotax,
                'weight' => null !== $weight,
                'unit_price_impact' => null !== $unit,
                'default_on' => null !== $default,
                'minimal_quantity' => null !== $minimal_quantity,
                'available_date' => 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->isbn = pSQL($isbn);
        $combination->upc = pSQL($upc);
        $combination->mpn = pSQL($mpn);
        $combination->default_on = (int) $default;
        $combination->minimal_quantity = (int) $minimal_quantity;
        $combination->low_stock_threshold = empty($low_stock_threshold) && '0' != $low_stock_threshold ? null : (int) $low_stock_threshold;
        $combination->low_stock_alert = !empty($low_stock_alert);
        $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 (is_array($id_images) && count($id_images)) {
            $combination->setImages($id_images);
        }

        $id_default_attribute = (int) Product::updateDefaultAttribute($this->id);
        if ($id_default_attribute) {
            $this->cache_default_attribute = $id_default_attribute;
        }

        // Sync stock Reference, EAN13, ISBN, MPN and UPC for this attribute
        if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && StockAvailable::dependsOnStock($this->id, Context::getContext()->shop->id)) {
            Db::getInstance()->update('stock', [
                'reference' => pSQL($reference),
                'ean13' => pSQL($ean13),
                'isbn' => pSQL($isbn),
                'upc' => pSQL($upc),
                'mpn' => pSQL($mpn),
            ], 'id_product = ' . $this->id . ' AND id_product_attribute = ' . (int) $id_product_attribute);
        }

        Hook::exec('actionProductAttributeUpdate', ['id_product_attribute' => (int) $id_product_attribute]);
        Tools::clearColorListCache($this->id);

        return true;
    }

    /**
     * Add a product attribute.
     *
     * @since 1501
     *
     * @param float $price Additional price
     * @param float $weight Additional weight
     * @param float $ecotax Additional ecotax
     * @param int $id_images Image ids
     * @param string $reference Reference
     * @param string $location Location
     * @param string $ean13 Ean-13 barcode
     * @param bool $default Is default attribute for product
     * @param int $minimal_quantity Minimal quantity to add to cart
     * @param string $isbn ISBN reference
     * @param int|null $low_stock Low stock alert
     *
     * @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 = [],
        $available_date = null,
        $quantity = 0,
        $isbn = '',
        $low_stock_threshold = null,
        $low_stock_alert = false,
        $mpn = null
    ) {
        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 = (int) $quantity;
        $combination->weight = (float) $weight;
        $combination->unit_price_impact = (float) $unit_impact;
        $combination->reference = pSQL($reference);
        $combination->location = pSQL($location);
        $combination->ean13 = pSQL($ean13);
        $combination->isbn = pSQL($isbn);
        $combination->upc = pSQL($upc);
        $combination->mpn = pSQL($mpn);
        $combination->default_on = (int) $default;
        $combination->minimal_quantity = (int) $minimal_quantity;
        $combination->low_stock_threshold = empty($low_stock_threshold) && '0' != $low_stock_threshold ? null : (int) $low_stock_threshold;
        $combination->low_stock_alert = !empty($low_stock_alert);
        $combination->available_date = $available_date;

        if (count($id_shop_list)) {
            $combination->id_shop_list = array_unique($id_shop_list);
        }

        $combination->add();

        if (!$combination->id) {
            return false;
        }

        $total_quantity = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
            '
            SELECT SUM(quantity) as quantity
            FROM ' . _DB_PREFIX_ . 'stock_available
            WHERE id_product = ' . (int) $this->id . '
            AND id_product_attribute <> 0 '
        );

        if (!$total_quantity) {
            Db::getInstance()->update('stock_available', ['quantity' => 0], '`id_product` = ' . $this->id);
        }

        $id_default_attribute = Product::updateDefaultAttribute($this->id);

        if ($id_default_attribute) {
            $this->cache_default_attribute = $id_default_attribute;
            if (!$combination->available_date) {
                $this->setAvailableDate();
            }
        }

        if (!empty($id_images)) {
            $combination->setImages($id_images);
        }

        Tools::clearColorListCache($this->id);

        if (Configuration::get('PS_DEFAULT_WAREHOUSE_NEW_PRODUCT') != 0 && Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')) {
            $warehouse_location_entity = new WarehouseProductLocation();
            $warehouse_location_entity->id_product = $this->id;
            $warehouse_location_entity->id_product_attribute = (int) $combination->id;
            $warehouse_location_entity->id_warehouse = Configuration::get('PS_DEFAULT_WAREHOUSE_NEW_PRODUCT');
            $warehouse_location_entity->location = pSQL('');
            $warehouse_location_entity->save();
        }

        return (int) $combination->id;
    }

    /**
     * @deprecated since 150
     */
    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', ['id_product_attribute' => 0, 'id_product' => (int) $this->id, 'deleteAllAttributes' => true]);

        $result = true;
        $combinations = new PrestaShopCollection('Combination');
        $combinations->where('id_product', '=', $this->id);
        foreach ($combinations as $combination) {
            $result &= $combination->delete();
        }
        SpecificPriceRule::applyAllRules([(int) $this->id]);
        Tools::clearColorListCache($this->id);

        return $result;
    }

    /**
     * Delete product attributes impacts.
     *
     * @return bool
     */
    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([(int) $this->id]);

        return $this->deleteFeatures();
    }

    public static function updateCacheAttachment($id_product)
    {
        $value = (bool) Db::getInstance()->getValue('
                                SELECT id_attachment
                                FROM ' . _DB_PREFIX_ . 'product_attachment
                                WHERE id_product=' . (int) $id_product);

        return Db::getInstance()->update(
                        'product',
                        ['cache_has_attachments' => (int) $value],
                        'id_product = ' . (int) $id_product
                    );
    }

    /**
     * Delete product attachments.
     *
     * @param bool $update_cache If set to true attachment cache will be updated
     *
     * @return array Deletion result
     */
    public function deleteAttachments($update_attachment_cache = true)
    {
        $res = Db::getInstance()->execute(
            '
            DELETE FROM `' . _DB_PREFIX_ . 'product_attachment`
            WHERE `id_product` = ' . (int) $this->id
        );

        if (isset($update_attachment_cache) && (bool) $update_attachment_cache === true) {
            Product::updateCacheAttachment((int) $this->id);
        }

        return $res;
    }

    /**
     * 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 `' . _DB_PREFIX_ . 'customization_field_lang` FROM `' . _DB_PREFIX_ . 'customization_field_lang` LEFT JOIN `' . _DB_PREFIX_ . 'customization_field`
                ON (' . _DB_PREFIX_ . 'customization_fieldid_customization_field = ' . _DB_PREFIX_ . 'customization_field_langid_customization_field)
                WHERE ' . _DB_PREFIX_ . 'customization_fieldid_customization_field IS NULL'
            );
    }

    /**
     * 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 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 sw FROM `' . _DB_PREFIX_ . 'search_word` sw
                LEFT JOIN `' . _DB_PREFIX_ . 'search_index` si ON (swid_word=siid_word)
                WHERE siid_word IS NULL;'
            );
    }

    /**
     * Add a product attributes combinaison.
     *
     * @param int $id_product_attribute Product attribute id
     * @param array $attributes Attributes to forge combinaison
     *
     * @return array Insertion result
     *
     * @deprecated since 1507
     */
    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);
    }

    /**
     * @deprecated 1550
     *
     * @param $id_attributes
     * @param $combinations
     *
     * @return bool
     *
     * @throws PrestaShopDatabaseException
     */
    public function addAttributeCombinationMultiple($id_attributes, $combinations)
    {
        Tools::displayAsDeprecated();
        $attributes_list = [];
        foreach ($id_attributes as $nb => $id_product_attribute) {
            if (isset($combinations[$nb])) {
                foreach ($combinations[$nb] as $id_attribute) {
                    $attributes_list[] = [
                        '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 int $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',
            [
                'id_product_attribute' => $id_product_attribute,
                'id_product' => $this->id,
                'deleteAllAttributes' => false,
            ]
        );

        $combination = new Combination($id_product_attribute);
        $res = $combination->delete();
        SpecificPriceRule::applyAllRules([(int) $this->id]);

        return $res;
    }

    /**
     * Delete features.
     */
    public function deleteFeatures()
    {
        $all_shops = Context::getContext()->shop->getContext() == Shop::CONTEXT_ALL ? true : false;

        // 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`)
            ' . (!$all_shops ? 'LEFT JOIN `' . _DB_PREFIX_ . 'feature_shop` fs ON (f.`id_feature` = fs.`id_feature`)' : null) . '
            WHERE `id_product` = ' . (int) $this->id
                . (!$all_shops ? ' AND fs.`id_shop` = ' . (int) Context::getContext()->shop->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 `' . _DB_PREFIX_ . 'feature_product` FROM `' . _DB_PREFIX_ . 'feature_product`
            WHERE `id_product` = ' . (int) $this->id . (!$all_shops ? '
                AND `id_feature` IN (
                    SELECT `id_feature`
                    FROM `' . _DB_PREFIX_ . 'feature_shop`
                    WHERE `id_shop` = ' . (int) Context::getContext()->shop->id . '
                )' : ''));

        SpecificPriceRule::applyAllRules([(int) $this->id]);

        return $result;
    }

    /**
     * Get all available product attributes resume.
     *
     * @param int $id_lang Language id
     *
     * @return array Product attributes combinations
     */
    public function getAttributesResume($id_lang, $attribute_value_separator = ' - ', $attribute_separator = ', ')
    {
        if (!Combination::isFeatureActive()) {
            return [];
        }

        $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`');

        if (!$combinations) {
            return false;
        }

        $product_attributes = [];
        foreach ($combinations as $combination) {
            $product_attributes[] = (int) $combination['id_product_attribute'];
        }

        $lang = Db::getInstance()->executeS('SELECT pacid_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 pacid_product_attribute IN (' . implode(',', $product_attributes) . ')
                GROUP BY pacid_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)) {
                $result = StockAvailable::getQuantityAvailableByProduct($row['id_product'], $row['id_product_attribute']);
                Cache::store(
                    $cache_key,
                    $result
                );
                $combinations[$key]['quantity'] = $result;
            } else {
                $combinations[$key]['quantity'] = Cache::retrieve($cache_key);
            }
        }

        return $combinations;
    }

    /**
     * Get all available product attributes combinations.
     *
     * @param int $id_lang Language id
     * @param bool $groupByIdAttributeGroup
     *
     * @return array Product attributes combinations
     */
    public function getAttributeCombinations($id_lang = null, $groupByIdAttributeGroup = true)
    {
        if (!Combination::isFeatureActive()) {
            return [];
        }
        if (null === $id_lang) {
            $id_lang = Context::getContext()->language->id;
        }

        'SELECT' = '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`
                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`' . ($groupByIdAttributeGroup ? ',ag.`id_attribute_group`' : '') . '
                ORDER BY pa.`id_product_attribute`';

        $res = Db::getInstance()->executeS('SELECT');

        //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 int $id_product_attribute
     * @param int $id_lang Language id
     *
     * @return array Product attribute combination by id_product_attribute
     */
    public function getAttributeCombinationsById($id_product_attribute, $id_lang, $groupByIdAttributeGroup = true)
    {
        if (!Combination::isFeatureActive()) {
            return [];
        }
        'SELECT' = '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`, a.`position`
                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`' . ($groupByIdAttributeGroup ? ',ag.`id_attribute_group`' : '') . '
                ORDER BY pa.`id_product_attribute`';

        $res = Db::getInstance()->executeS('SELECT');

        //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)) {
                $result = StockAvailable::getQuantityAvailableByProduct($row['id_product'], $row['id_product_attribute']);
                Cache::store(
                    $cache_key,
                    $result
                );
                $res[$key]['quantity'] = $result;
            } else {
                $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 = [];

        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 = [];

        foreach ($result as $row) {
            $images[$row['id_product_attribute']][] = $row;
        }

        return $images;
    }

    public static function getCombinationImageById($id_product_attribute, $id_lang)
    {
        if (!Combination::isFeatureActive() || !$id_product_attribute) {
            return false;
        }

        $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` = ' . (int) $id_product_attribute . ' AND il.`id_lang` = ' . (int) $id_lang . ' ORDER by i.`position` LIMIT 1'
        );

        if (!$result) {
            return false;
        }

        return "=";
    }

    /**
     * Check if product has attributes combinations.
     *
     * @return int 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 int $id_lang Language id
     * @param int $pageNumber Start from (optional)
     * @param int $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, 
        if (!$context) {
            $context = Context::getContext();
        }

        $front = true;
        if (!in_array($context->controller->controller_type, ['front', 'modulefront'])) {
            $front = false;
        }

        if ($page_number < 1) {
            $page_number = 1;
        }
        if ($nb_products < 1) {
            $nb_products = 10;
        }
        if (empty($order_by) || 
        }
        if (empty('DESC')) {
            
        }
        if (
        } elseif (
        }
        if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay('DESC')) {
            die(Tools::displayError());
        }

        'SELECT'_groups = '';
        if (Group::isFeatureActive()) {
            $groups = FrontController::getCurrentCustomerGroups();
            'SELECT'_groups = ' AND EXISTS(SELECT 1 FROM `' . _DB_PREFIX_ . 'category_product` cp
            JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cpid_category = cgid_category AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id) . ')
            WHERE cp.`id_product` = p.`id_product`)';
        }

        if (strpos($order_by, ) > 0) {
            
            $order_by_prefix = "=";
            
        }

        $nb_days_new_product = (int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT');

        if ($count) {
            'SELECT' = 'SELECT COUNT(p.`id_product`) AS nb
                    FROM `' . _DB_PREFIX_ . 'product` p
                    ' . Shop::addSqlAssociation('product', 'p') . '
                    WHERE product_shop.`active` = 1
                    AND product_shop.`date_add` > "' . date('Y-m-d', strtotime('-' . $nb_days_new_product . ' DAY')) . '"
                    ' . ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . '
                    SELECT'_groups;

            return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('SELECT');
        }
        'SELECT' = new DbQuery();
        'SELECT'->select(
            'p.*, product_shop.*, stockout_of_stock, IFNULL(stockquantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`link_rewrite`, pl.`meta_description`,
            pl.`meta_keywords`, pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`, image_shop.`id_image` id_image, il.`legend`, m.`name` AS manufacturer_name,
            (DATEDIFF(product_shop.`date_add`,
                DATE_SUB(
                    "' . $now . '",
                    INTERVAL ' . $nb_days_new_product . ' DAY
                )
            ) > 0) as new'
        );

        'SELECT'->from('product', 'p');
        'SELECT'->join(Shop::addSqlAssociation('product', 'p'));
        'SELECT'->leftJoin(
            'product_lang',
            'pl',
            '
            p.`id_product` = pl.`id_product`
            AND pl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl')
        );
        'SELECT'->leftJoin('image_shop', 'image_shop', 'image_shop.`id_product` = p.`id_product` AND image_shopcover=1 AND image_shopid_shop=' . (int) $context->shop->id);
        'SELECT'->leftJoin('image_lang', 'il', 'image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $id_lang);
        'SELECT'->leftJoin('manufacturer', 'm', 'm.`id_manufacturer` = p.`id_manufacturer`');

        'SELECT'->where('product_shop.`active` = 1');
        if ($front) {
            'SELECT'->where('product_shop.`visibility` IN ("both", "catalog")');
        }
        'SELECT'->where('product_shop.`date_add` > "' . date('Y-m-d', strtotime('-' . $nb_days_new_product . ' DAY')) . '"');
        if (Group::isFeatureActive()) {
            $groups = FrontController::getCurrentCustomerGroups();
            'SELECT'->where('EXISTS(SELECT 1 FROM `' . _DB_PREFIX_ . 'category_product` cp
            JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cpid_category = cgid_category AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id) . ')
            WHERE cp.`id_product` = p.`id_product`)');
        }

        'SELECT'->orderBy((isset($order_by_prefix) ? pSQL($order_by_prefix) .  : '') . '`' . pSQL($order_by) . '` ' . pSQL('DESC'));
        'SELECT'->limit($nb_products, (int) (($page_number - 1) * $nb_products));

        if (Combination::isFeatureActive()) {
            'SELECT'->select('product_attribute_shopminimal_quantity AS product_attribute_minimal_quantity, IFNULL(product_attribute_shopid_product_attribute,0) id_product_attribute');
            'SELECT'->leftJoin('product_attribute_shop', 'product_attribute_shop', 'p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shopid_shop=' . (int) $context->shop->id);
        }
        'SELECT'->join(Product::sqlStock('p', 0));

        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT');

        if (!$result) {
            return false;
        }

        if (
        }
        $products_ids = [];
        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 = $ids['id_country'] ? (int) $ids['id_country'] : (int) 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 int $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, ['front', 'modulefront'])) {
            $front = false;
        }

        $current_date = date('Y-m-d H:i:00');
        $product_reductions = Product::_getProductIdByDate((!$beginning ? $current_date : $beginning), (!$ending ? $current_date : $ending), $context, true);

        if ($product_reductions) {
            $ids_products = '';
            foreach ($product_reductions as $product_reduction) {
                $ids_products .= '(' . (int) $product_reduction['id_product'] . ',' . ($product_reduction['id_product_attribute'] ? (int) $product_reduction['id_product_attribute'] : '0') . '),';
            }

            $ids_products = rtrim($ids_products, ',');
            Db::getInstance()->execute('CREATE TEMPORARY TABLE `' . _DB_PREFIX_ . 'product_reductions` (id_product INT UNSIGNED NOT NULL DEFAULT 0, id_product_attribute INT UNSIGNED NOT NULL DEFAULT 0) ENGINE=MEMORY', false);
            if ($ids_products) {
                Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'product_reductions` VALUES ' . $ids_products, false);
            }

            $groups = FrontController::getCurrentCustomerGroups();
            'SELECT'_groups = ' AND EXISTS(SELECT 1 FROM `' . _DB_PREFIX_ . 'category_product` cp
            JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cpid_category = cgid_category AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id) . ')
            WHERE cp.`id_product` = p.`id_product`)';

            // Please keep 2 distinct queries because RAND() is an awful way to achieve this result
            'SELECT' = 'SELECT product_shopid_product, IFNULL(product_attribute_shopid_product_attribute,0) id_product_attribute
                    FROM
                    `' . _DB_PREFIX_ . 'product_reductions` pr,
                    `' . _DB_PREFIX_ . 'product` p
                    ' . Shop::addSqlAssociation('product', 'p') . '
                    LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` product_attribute_shop
                        ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shopid_shop=' . (int) $context->shop->id . ')
                    WHERE pid_product=prid_product AND (prid_product_attribute = 0 OR product_attribute_shopid_product_attribute = prid_product_attribute) AND product_shop.`active` = 1
                        SELECT'_groups . '
                    ' . ($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '') . '
                    ORDER BY RAND()';

            $result = Db::getInstance()->getRow('SELECT');

            Db::getInstance()->execute('DROP TEMPORARY TABLE `' . _DB_PREFIX_ . 'product_reductions`', false);

            if (!$id_product = $result['id_product']) {
                return false;
            }

            // no group by needed : there's only one attribute with cover=1 for a given id_product + shop
            'SELECT' = '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`, pl.`available_now`, pl.`available_later`,
                        p.`ean13`, p.`isbn`, p.`upc`, p.`mpn`, image_shop.`id_image` id_image, il.`legend`,
                        DATEDIFF(product_shop.`date_add`, DATE_SUB("' . date('Y-m-d') . ' 00:00:00",
                        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
                    LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (
                        p.`id_product` = pl.`id_product`
                        AND pl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl') . '
                    )
                    ' . Shop::addSqlAssociation('product', 'p') . '
                    LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
                        ON (image_shop.`id_product` = p.`id_product` AND image_shopcover=1 AND image_shopid_shop=' . (int) $context->shop->id . ')
                    LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $id_lang . ')
                    ' . Product::sqlStock('p', 0) . '
                    WHERE pid_product = ' . (int) $id_product;

            $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow('SELECT');
            if (!$row) {
                return false;
            }

            $row['id_product_attribute'] = (int) $result['id_product_attribute'];

            return Product::getProductProperties($id_lang, $row);
        } else {
            return false;
        }
    }

    /**
     * Get prices drop.
     *
     * @param int $id_lang Language id
     * @param int $pageNumber Start from (optional)
     * @param int $nbProducts Number of products to return (optional)
     * @param bool $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,
        
        }

        if (!$context) {
            $context = Context::getContext();
        }
        if ($page_number < 1) {
            $page_number = 1;
        }
        if ($nb_products < 1) {
            $nb_products = 10;
        }
        if (empty($order_by) || 
        }
        if (empty('DESC')) {
            
        }
        if (
        } elseif (
        }
        if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay('DESC')) {
            die(Tools::displayError());
        }
        $current_date = date('Y-m-d H:i:00');
        $ids_product = Product::_getProductIdByDate((!$beginning ? $current_date : $beginning), (!$ending ? $current_date : $ending), $context);

        $tab_id_product = [];
        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, ['front', 'modulefront'])) {
            $front = false;
        }

        'SELECT'_groups = '';
        if (Group::isFeatureActive()) {
            $groups = FrontController::getCurrentCustomerGroups();
            'SELECT'_groups = ' AND EXISTS(SELECT 1 FROM `' . _DB_PREFIX_ . 'category_product` cp
            JOIN `' . _DB_PREFIX_ . 'category_group` cg ON (cpid_category = cgid_category AND cg.`id_group` ' . (count($groups) ? 'IN (' . implode(',', $groups) . ')' : '=' . (int) Group::getCurrent()->id) . ')
            WHERE cp.`id_product` = p.`id_product`)';
        }

        if ($count) {
            return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
            SELECT COUNT(DISTINCT p.`id_product`)
            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) . ')' : '') . '
            SELECT'_groups);
        }

        if (strpos($order_by, ) > 0) {
            
            
        }

        'SELECT' = '
        SELECT
            p.*, product_shop.*, stockout_of_stock, IFNULL(stockquantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`available_now`, pl.`available_later`,
            IFNULL(product_attribute_shopid_product_attribute, 0) id_product_attribute,
            pl.`link_rewrite`, pl.`meta_description`, pl.`meta_keywords`, pl.`meta_title`,
            pl.`name`, image_shop.`id_image` id_image, il.`legend`, m.`name` AS manufacturer_name,
            DATEDIFF(
                p.`date_add`,
                DATE_SUB(
                    "' . date('Y-m-d') . ' 00:00:00",
                    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') . '
        LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_shop` product_attribute_shop
            ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shopid_shop=' . (int) $context->shop->id . ')
        ' . 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_langShop::addSqlRestrictionOnLang('pl') . '
        )
        LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
            ON (image_shop.`id_product` = p.`id_product` AND image_shopcover=1 AND image_shopid_shop=' . (int) $context->shop->id . ')
        LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $id_lang . ')
        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 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) . ')' : '') . '
        SELECT'_groups . '
        ORDER BY ' . (isset($order_by_prefix) ? pSQL($order_by_prefix) .  : '') . pSQL($order_by) . ' ' . pSQL('DESC') . '
        LIMIT ' . (int) (($page_number - 1) * $nb_products) . ', ' . (int) $nb_products;

        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT');

        if (!$result) {
            return false;
        }

        if (
        }

        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 = '')
    {
        'productlist_colors' = 'Product::getProductCategories_' . (int) $id_product;
        if (!Cache::isStored('productlist_colors')) {
            $ret = [];

            $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'];
                }
            }
            Cache::store('productlist_colors', $ret);

            return $ret;
        }

        return Cache::retrieve('productlist_colors');
    }

    public static function getProductCategoriesFull($id_product = '', $id_lang = null)
    {
        if (!$id_lang) {
            $id_lang = Context::getContext()->language->id;
        }

        $ret = [];
        $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 (cid_category = cpid_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 = [];

        foreach ($carrier_list as $carrier) {
            $data[] = [
                '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
        );

        $unique_array = [];
        foreach ($data as $sub_array) {
            if (!in_array($sub_array, $unique_array)) {
                $unique_array[] = $sub_array;
            }
        }

        if (count($unique_array)) {
            Db::getInstance()->insert('product_carrier', $unique_array, false, true, Db::INSERT_IGNORE);
        }
    }

    /**
     * Get product images and legends.
     *
     * @param int $id_lang Language id for multilingual legends
     *
     * @return array Product images and legends
     */
    public function getImages($id_lang, Context $context = null)
    {
        return Db::getInstance()->executeS(
            '
            SELECT image_shop.`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`'
        );
    }

    /**
     * Get product cover image.
     *
     * @return array Product cover image
     */
    public static function getCover($id_product, Context $context = null)
    {
        if (!$context) {
            $context = Context::getContext();
        }
        'productlist_colors' = 'Product::getCover_' . (int) $id_product . '-' . (int) $context->shop->id;
        if (!Cache::isStored('productlist_colors')) {
            'SELECT' = 'SELECT image_shop.`id_image`
                    FROM `' . _DB_PREFIX_ . 'image` i
                    ' . Shop::addSqlAssociation('image', 'i') . '
                    WHERE i.`id_product` = ' . (int) $id_product . '
                    AND image_shop.`cover` = 1';
            $result = Db::getInstance()->getRow('SELECT');
            Cache::store('productlist_colors', $result);

            return $result;
        }

        return Cache::retrieve('productlist_colors');
    }

    /**
     * Returns product price.
     *
     * @param int $id_product Product id
     * @param bool $usetax With taxes or not (optional)
     * @param int|null $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 int $decimals Number of decimals (optional)
     * @param int|null $divisor Useful when paying many time without fees (optional)
     * @param bool $only_reduc Returns only the reduction amount
     * @param bool $usereduc Set if the returned amount will include reduction
     * @param int $quantity Required for quantity discount application (default value: 1)
     * @param bool $force_associated_tax DEPRECATED - NOT USED Force to apply the associated tax.
     *                                   Only works when the parameter $usetax is true
     * @param int|null $id_customer Customer ID (for customer group reduction)
     * @param int|null $id_cart Cart IDRequired when the cookie is not accessible
     *                          (eg., inside a payment module, a cron task...)
     * @param int|null $id_address Customer address IDRequired for price (tax included)
     *                             calculation regarding the guest localization
     * @param null $specific_price_output If a specific price applies regarding the previous parameters,
     *                                    this variable is filled with the corresponding SpecificPrice object
     * @param bool $with_ecotax insert ecotax in price output
     * @param bool $use_group_reduction
     * @param Context $context
     * @param bool $use_customer_price
     *
     * @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,
        $id_customization = null
    ) {
        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 = null;
        if ($id_customer) {
            $id_group = Customer::getDefaultGroupId((int) $id_customer);
        }
        if (!$id_group) {
            $id_group = (int) Group::getCurrent()->id;
        }

        // 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 (eg., guest, customer, Google...) is on PrestaShop, he has already its cart as the global (see /initphp)
            * When a non-user calls directly this method (eg., 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) {
            'productlist_colors' = 'Product::getPriceStatic_' . (int) $id_product . '-' . (int) $id_cart;
            if (!Cache::isStored('productlist_colors') || ($cart_quantity = Cache::retrieve('productlist_colors') != (int) $quantity)) {
                'SELECT' = 'SELECT SUM(`quantity`)
                FROM `' . _DB_PREFIX_ . 'cart_product`
                WHERE `id_product` = ' . (int) $id_product . '
                AND `id_cart` = ' . (int) $id_cart;
                $cart_quantity = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('SELECT');
                Cache::store('productlist_colors', $cart_quantity);
            } else {
                $cart_quantity = Cache::retrieve('productlist_colors');
            }
        }

        $id_currency = Validate::isLoadedObject($context->currency) ? (int) $context->currency->id : (int) Configuration::get('PS_CURRENCY_DEFAULT');

        if (!$id_address && Validate::isLoadedObject($cur_cart)) {
            $id_address = $cur_cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')};
        }

        // retrieve address informations
        $address = Address::initialize($id_address, true);
        $id_country = (int) $address->id_country;
        $id_state = (int) $address->id_state;
        $zipcode = $address->postcode;

        if (Tax::excludeTaxeOption()) {
            $usetax = false;
        }

        if ($usetax != false
            && !empty($address->vat_number)
            && $address->id_country != Configuration::get('VATNUMBER_COUNTRY')
            && Configuration::get('VATNUMBER_MANAGEMENT')) {
            $usetax = false;
        }

        if (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,
            $quantity,
            $usetax,
            $decimals,
            $only_reduc,
            $usereduc,
            $with_ecotax,
            $specific_price_output,
            $use_group_reduction,
            $id_customer,
            $use_customer_price,
            $id_cart,
            $cart_quantity,
            $id_customization
        );

        return $return;
    }

    /**
     * Price calculation / Get product price.
     *
     * @param int $id_shop Shop id
     * @param int $id_product Product id
     * @param int $id_product_attribute Product attribute id
     * @param int $id_country Country id
     * @param int $id_state State id
     * @param string $zipcode
     * @param int $id_currency Currency id
     * @param int $id_group Group id
     * @param int $quantity Quantity Required for Specific prices : quantity discount application
     * @param bool $use_tax with (1) or without (0) tax
     * @param int $decimals Number of decimals returned
     * @param bool $only_reduc Returns only the reduction amount
     * @param bool $use_reduc Set if the returned amount will include reduction
     * @param bool $with_ecotax insert ecotax in price output
     * @param null $specific_price If a specific price applies regarding the previous parameters,
     *                             this variable is filled with the corresponding SpecificPrice object
     * @param bool $use_group_reduction
     * @param int $id_customer
     * @param bool $use_customer_price
     * @param int $id_cart
     * @param int $real_quantity
     *
     * @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,
        $id_customization = 0
    ) {
        static $address = null;
        static $context = null;

        if ($context == null) {
            $context = Context::getContext()->cloneContext();
        }

        if ($address === null) {
            if (is_object($context->cart) && $context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')} != null) {
                $id_address = $context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')};
                $address = new Address($id_address);
            } else {
                $address = new Address();
            }
        }

        if ($id_shop !== null && $context->shop->id != (int) $id_shop) {
            $context->shop = new Shop((int) $id_shop);
        }

        if (!$use_customer_price) {
            $id_customer = 0;
        }

        if ($id_product_attribute === null) {
            $id_product_attribute = Product::getDefaultAttribute($id_product);
        }

        'productlist_colors' = (int) $id_product . '-' . (int) $id_shop . '-' . (int) $id_currency . '-' . (int) $id_country . '-' . $id_state . '-' . $zipcode . '-' . (int) $id_group .
            '-' . (int) $quantity . '-' . (int) $id_product_attribute . '-' . (int) $id_customization .
            '-' . (int) $with_ecotax . '-' . (int) $id_customer . '-' . (int) $use_group_reduction . '-' . (int) $id_cart . '-' . (int) $real_quantity .
            '-' . ($only_reduc ? '1' : '0') . '-' . ($use_reduc ? '1' : '0') . '-' . ($use_tax ? '1' : '0') . '-' . (int) $decimals;

        // 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['productlist_colors'])) {
            return self::$_prices['productlist_colors'];
        }

        // fetch price & attribute price
        'productlist_colors'_2 = $id_product . '-' . $id_shop;
        if (!isset(self::$_pricesLevel2['productlist_colors'_2])) {
            'SELECT' = new DbQuery();
            'SELECT'->select('product_shop.`price`, product_shop.`ecotax`');
            'SELECT'->from('product', 'p');
            'SELECT'->innerJoin('product_shop', 'product_shop', '(product_shopid_product=pid_product AND product_shopid_shop = ' . (int) $id_shop . ')');
            'SELECT'->where('p.`id_product` = ' . (int) $id_product);
            if (Combination::isFeatureActive()) {
                'SELECT'->select('IFNULL(product_attribute_shopid_product_attribute,0) id_product_attribute, product_attribute_shop.`price` AS attribute_price, product_attribute_shopdefault_on');
                'SELECT'->leftJoin('product_attribute_shop', 'product_attribute_shop', '(product_attribute_shopid_product = pid_product AND product_attribute_shopid_shop = ' . (int) $id_shop . ')');
            } else {
                'SELECT'->select('0 as id_product_attribute');
            }

            $res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT');

            if (is_array($res) && count($res)) {
                foreach ($res as $row) {
                    $array_tmp = [
                        'price' => $row['price'],
                        'ecotax' => $row['ecotax'],
                        'attribute_price' => (isset($row['attribute_price']) ? $row['attribute_price'] : null),
                    ];
                    self::$_pricesLevel2['productlist_colors'_2][(int) $row['id_product_attribute']] = $array_tmp;

                    if (isset($row['default_on']) && $row['default_on'] == 1) {
                        self::$_pricesLevel2['productlist_colors'_2][0] = $array_tmp;
                    }
                }
            }
        }

        if (!isset(self::$_pricesLevel2['productlist_colors'_2][(int) $id_product_attribute])) {
            return;
        }

        $result = self::$_pricesLevel2['productlist_colors'_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'] &&
                $id_currency !== $specific_price['id_currency']
            )
        ) {
            $price = Tools::convertPrice($price, $id_currency);

            if (isset($specific_price['price']) && $specific_price['price'] >= 0) {
                $specific_price['price'] = $price;
            }
        }

        // 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 you want the default combination, please use NULL value instead
            if ($id_product_attribute !== false) {
                $price += $attribute_price;
            }
        }

        // Customization price
        if ((int) $id_customization) {
            $price += Tools::convertPrice(Customization::getCustomizationPrice($id_customization), $id_currency);
        }

        // Tax
        $address->id_country = $id_country;
        $address->id_state = $id_state;
        $address->postcode = $zipcode;

        $tax_manager = TaxManagerFactory::getManager($address, Product::getIdTaxRulesGroupByIdProduct((int) $id_product, $context));
        $product_tax_calculator = $tax_manager->getTaxCalculator();

        // Add Tax
        if ($use_tax) {
            $price = $product_tax_calculator->addTaxes($price);
        }

        // 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) {
                static $psEcotaxTaxRulesGroupId = null;
                if ($psEcotaxTaxRulesGroupId === null) {
                    $psEcotaxTaxRulesGroupId = (int) Configuration::get('PS_ECOTAX_TAX_RULES_GROUP_ID');
                }
                // reinit the tax manager for ecotax handling
                $tax_manager = TaxManagerFactory::getManager(
                    $address,
                    $psEcotaxTaxRulesGroupId
                );
                $ecotax_tax_calculator = $tax_manager->getTaxCalculator();
                $price += $ecotax_tax_calculator->addTaxes($ecotax);
            } else {
                $price += $ecotax;
            }
        }

        // Reduction
        $specific_price_reduction = 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);
                }

                $specific_price_reduction = $reduction_amount;

                // Adjust taxes if required

                if (!$use_tax && $specific_price['reduction_tax']) {
                    $specific_price_reduction = $product_tax_calculator->removeTaxes($specific_price_reduction);
                }
                if ($use_tax && !$specific_price['reduction_tax']) {
                    $specific_price_reduction = $product_tax_calculator->addTaxes($specific_price_reduction);
                }
            } else {
                $specific_price_reduction = $price * $specific_price['reduction'];
            }
        }

        if ($use_reduc) {
            $price -= $specific_price_reduction;
        }

        // Group reduction
        if ($use_group_reduction) {
            $reduction_from_category = GroupReduction::getValueForProduct($id_product, $id_group);
            if ($reduction_from_category !== false) {
                $group_reduction = $price * (float) $reduction_from_category;
            } else { // apply group reduction if there is no group reduction for this category
                $group_reduction = (($reduc = Group::getReductionByIdGroup($id_group)) != 0) ? ($price * $reduc / 100) : 0;
            }

            $price -= $group_reduction;
        }

        if ($only_reduc) {
            return Tools::ps_round($specific_price_reduction, $decimals);
        }

        $price = Tools::ps_round($price, $decimals);

        if ($price < 0) {
            $price = 0;
        }

        self::$_prices['productlist_colors'] = $price;

        return self::$_prices['productlist_colors'];
    }

    /**
     * @param int $orderId
     * @param int $productId
     * @param int $combinationId
     * @param bool $withTaxes
     * @param bool $useReduction
     * @param bool $withEcoTax
     *
     * @return float|null
     *
     * @throws PrestaShopDatabaseException
     */
    public static function getPriceFromOrder(
        int $orderId,
        int $productId,
        int $combinationId,
        bool $withTaxes,
        bool $useReduction,
        bool $withEcoTax
    ): ?float {
        'SELECT' = new DbQuery();
        'SELECT'->select('od.*, trate AS tax_rate');
        'SELECT'->from('order_detail', 'od');
        'SELECT'->where('od.`id_order` = ' . $orderId);
        'SELECT'->where('od.`product_id` = ' . $productId);
        if (Combination::isFeatureActive()) {
            'SELECT'->where('od.`product_attribute_id` = ' . $combinationId);
        }
        'SELECT'->leftJoin('order_detail_tax', 'odt', 'odtid_order_detail = odid_order_detail');
        'SELECT'->leftJoin('tax', 't', 'tid_tax = odtid_tax');
        $res = Db::getInstance((bool) _PS_USE_SQL_SLAVE_)->executeS('SELECT');
        if (!is_array($res) || empty($res)) {
            return null;
        }

        $orderDetail = "=";
        if ($useReduction) {
            // If we want price with reduction it is already the one stored in OrderDetail
            $price = $withTaxes ? $orderDetail['unit_price_tax_incl'] : $orderDetail['unit_price_tax_excl'];
        } else {
            // Without reduction we use the original product price to compute the original price
            $tax_rate = $withTaxes ? (1 + ($orderDetail['tax_rate'] / 100)) : 1;
            $price = $orderDetail['original_product_price'] * $tax_rate;
        }
        $ecoTaxValue = 0;
        if ($withEcoTax) {
            $ecoTaxValue = $withTaxes ? $orderDetail['ecotax'] * (1 + $orderDetail['ecotax_tax_rate']) : $orderDetail['ecotax'];
        }
        $price += $ecoTaxValue;

        return $price;
    }

    public static function convertAndFormatPrice($price, $currency = false, Context $context = null)
    {
        if (!$context) {
            $context = Context::getContext();
        }
        if (!$currency) {
            $currency = $context->currency;
        }

        return $context->getCurrentLocale()->formatPrice(Tools::convertPrice($price, $currency), $currency->iso_code);
    }

    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 = $ids['id_country'] ? (int) $ids['id_country'] : (int) Configuration::get('PS_COUNTRY_DEFAULT');

        return (bool) SpecificPrice::getSpecificPrice((int) $id_product, $context->shop->id, $id_currency, $id_country, $id_group, $quantity, null, 0, 0, $quantity);
    }

    /**
     * Get product price
     * Same as static function getPriceStatic, no need to specify product id.
     *
     * @param bool $tax With taxes or not (optional)
     * @param int $id_product_attribute Product attribute id (optional)
     * @param int $decimals Number of decimals (optional)
     * @param int $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;
        }

        return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('
        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');
    }

    public function getDefaultIdProductAttribute()
    {
        if (!Combination::isFeatureActive()) {
            return 0;
        }

        return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
            '
            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_shopdefault_on = 1'
        );
    }

    public function getPriceWithoutReduct($notax = false, $id_product_attribute = null, $decimals = 6)
    {
        return Product::getPriceStatic((int) $this->id, !$notax, $id_product_attribute, $decimals, 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 Context::getContext()->getCurrentLocale()->formatPrice($params['price'], Context::getContext()->currency->iso_code);
    }

    /**
     * Convert price with currency.
     *
     * @param array $params
     * @param object $smarty DEPRECATED
     *
     * @return string Ambigous <string, mixed, Ambigous <number, string>>
     */
    public static function convertPriceWithCurrency($params, &$smarty)
    {
        $currency = $params['currency'];
        $currency = is_object($currency) ? $currency->iso_code : Currency::getIsoCodeById((int) $currency);

        return Tools::getContextLocale(Context::getContext())->formatPrice($params['price'], $currency);
    }

    public static function displayWtPrice($params, &$smarty)
    {
        return Tools::getContextLocale(Context::getContext())->formatPrice($params['p'], Context::getContext()->currency->iso_code);
    }

    /**
     * Display WT price with currency.
     *
     * @param array $params
     * @param Smarty $smarty DEPRECATED
     *
     * @return string Ambigous <string, mixed, Ambigous <number, string>>
     */
    public static function displayWtPriceWithCurrency($params, &$smarty)
    {
        $currency = $params['currency'];
        $currency = is_object($currency) ? $currency->iso_code : Currency::getIsoCodeById((int) $currency);

        return !is_null($params['price']) ? Tools::getContextLocale(Context::getContext())->formatPrice($params['price'], $currency) : null;
    }

    /**
     * Get available product quantities (this method already have decreased products in cart).
     *
     * @param int $idProduct Product id
     * @param int $idProductAttribute Product attribute id (optional)
     * @param bool|null $cacheIsPack
     * @param Cart|null $cart
     * @param int $idCustomization Product customization id (optional)
     *
     * @return int Available quantities
     */
    public static function getQuantity(
        $idProduct,
        $idProductAttribute = null,
        $cacheIsPack = null,
        Cart $cart = null,
        $idCustomization = null
    ) {
        // pack usecase: Pack::getQuantity() returns the pack quantity after cart quantities have been removed from stock
        if (Pack::isPack((int) $idProduct)) {
            return Pack::getQuantity($idProduct, $idProductAttribute, $cacheIsPack, $cart, $idCustomization);
        }
        $availableQuantity = StockAvailable::getQuantityAvailableByProduct($idProduct, $idProductAttribute);
        $nbProductInCart = 0;

        // we don't substract products in cart if the cart is already attached to an order, since stock quantity
        // has already been updated, this is only useful when the order has not yet been created
        if (!empty($cart) && empty(Order::getByCartId($cart->id))) {
            $cartProduct = $cart->getProductQuantity($idProduct, $idProductAttribute, $idCustomization);

            if (!empty($cartProduct['deep_quantity'])) {
                $nbProductInCart = $cartProduct['deep_quantity'];
            }
        }

        // @since 150
        return $availableQuantity - $nbProductInCart;
    }

    /**
     * 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 = null, $inner_join = false, Shop $shop = null)
    {
        $id_shop = ($shop !== null ? (int) $shop->id : null);
        'SELECT' = (($inner_join) ? ' INNER ' : ' LEFT ')
            . 'JOIN ' . _DB_PREFIX_ . 'stock_available stock
            ON (stockid_product = `' . bqSQL($product_alias) . '`.id_product';

        if (null !== $product_attribute) {
            if (!Combination::isFeatureActive()) {
                'SELECT' .= ' AND stockid_product_attribute = 0';
            } elseif (is_numeric($product_attribute)) {
                'SELECT' .= ' AND stockid_product_attribute = ' . $product_attribute;
            } elseif (is_string($product_attribute)) {
                'SELECT' .= ' AND stockid_product_attribute = IFNULL(`' . bqSQL($product_attribute) . '`.id_product_attribute, 0)';
            }
        }

        'SELECT' .= StockAvailable::addSqlShopRestriction(null, $id_shop, 'stock') . ' )';

        return 'SELECT';
    }

    /**
     * @deprecated since 150
     *
     * 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)
     * @deprecated 1530
     *
     * @return false
     */
    public static function updateQuantity()
    {
        Tools::displayAsDeprecated();

        return false;
    }

    /**
     * @deprecated since 150
     *
     * It's not possible to use this method with new stockManager and stockAvailable features
     * Now this method do nothing
     * @deprecated 1530
     * @see StockManager if you want to manage real stock
     * @see StockAvailable if you want to manage available quantities for sale on your shop(s)
     *
     * @return false
     */
    public static function reinjectQuantities()
    {
        Tools::displayAsDeprecated();

        return false;
    }

    public static function isAvailableWhenOutOfStock($out_of_stock)
    {
        /** @TODO 150 Update of STOCK_MANAGEMENT & ORDER_OUT_OF_STOCK */
        $ps_stock_management = Configuration::get('PS_STOCK_MANAGEMENT');

        if (!$ps_stock_management) {
            return true;
        }

        $ps_order_out_of_stock = Configuration::get('PS_ORDER_OUT_OF_STOCK');

        return (int) $out_of_stock == 2 ? (int) $ps_order_out_of_stock : (int) $out_of_stock;
    }

    /**
     * Check product availability.
     *
     * @param int $qty Quantity desired
     *
     * @return bool True if product is available with this quantity, false otherwise
     */
    public function checkQty($qty)
    {
        if ($this->isAvailableWhenOutOfStock(StockAvailable::outOfStock($this->id))) {
            return true;
        }
        $id_product_attribute = isset($this->id_product_attribute) ? $this->id_product_attribute : null;
        $availableQuantity = StockAvailable::getQuantityAvailableByProduct($this->id, $id_product_attribute);

        return $qty <= $availableQuantity;
    }

    /**
     * Check if there is no default attribute and create it if not.
     */
    public function checkDefaultAttributes()
    {
        if (!$this->id) {
            return false;
        }

        if (Db::getInstance()->getValue('SELECT COUNT(*)
                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) > Shop::getTotalShops(true)) {
            Db::getInstance()->execute('UPDATE ' . _DB_PREFIX_ . 'product_attribute_shop product_attribute_shop, ' . _DB_PREFIX_ . 'product_attribute pa
                    SET product_attribute_shopdefault_on=NULL, padefault_on = NULL
                    WHERE product_attribute_shopid_product_attribute=paid_product_attribute AND paid_product=' . (int) $this->idShop::addSqlRestriction(false, 'product_attribute_shop'));
        }

        $row = Db::getInstance()->getRow(
            '
            SELECT paid_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(paid_product_attribute) as `id_attr`
        FROM `' . _DB_PREFIX_ . 'product_attribute` pa
            ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
            WHERE pa.`id_product` = ' . (int) $this->id
        );
        if (!$mini) {
            return false;
        }

        if (!ObjectModel::updateMultishopTable('Combination', ['default_on' => 1], 'aid_product_attribute = ' . (int) $mini['id_attr'])) {
            return false;
        }

        return true;
    }

    public static function getAttributesColorList(array $products, $have_stock = true)
    {
        if (!count($products)) {
            return [];
        }

        $id_lang = Context::getContext()->language->id;

        $check_stock = !Configuration::get('PS_DISP_UNAVAILABLE_ATTR');
        if (!$res = Db::getInstance()->executeS(
            '
            SELECT pa.`id_product`, a.`color`, pac.`id_product_attribute`, ' . ($check_stock ? 'SUM(IF(stock.`quantity` > 0, 1, 0))' : '0') . ' qty, a.`id_attribute`, al.`name`, IF(color = "", aid_attribute, color) group_by
            FROM `' . _DB_PREFIX_ . 'product_attribute` pa
            ' . Shop::addSqlAssociation('product_attribute', 'pa') .
            ($check_stock ? Product::sqlStock('pa', 'pa') : '') . '
            JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON (pac.`id_product_attribute` = product_attribute_shop.`id_product_attribute`)
            JOIN `' . _DB_PREFIX_ . 'attribute` a ON (a.`id_attribute` = pac.`id_attribute`)
            JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) $id_lang . ')
            JOIN `' . _DB_PREFIX_ . 'attribute_group` ag ON (aid_attribute_group = ag.`id_attribute_group`)
            WHERE pa.`id_product` IN (' . implode(',', array_map('intval', $products)) . ') AND ag.`is_color_group` = 1
            GROUP BY pa.`id_product`, a.`id_attribute`, `group_by`
            ' . ($check_stock ? 'HAVING qty > 0' : '') . '
            ORDER BY a.`position` ASC;'
            )
        ) {
            return false;
        }

        $colors = [];
        foreach ($res as $row) {
            $row['texture'] = '';

            if (@filemtime(_PS_COL_IMG_DIR_ . $row['id_attribute'] . '.jpg')) {
                $row['texture'] = _THEME_COL_DIR_ . $row['id_attribute'] . '.jpg';
            } elseif (Tools::isEmpty($row['color'])) {
                continue;
            }

            $colors[(int) $row['id_product']][] = ['id_product_attribute' => (int) $row['id_product_attribute'], 'color' => $row['color'], 'texture' => $row['texture'], 'id_product' => $row['id_product'], 'name' => $row['name'], 'id_attribute' => $row['id_attribute']];
        }

        return $colors;
    }

    /**
     * Get all available attribute groups.
     *
     * @param int $id_lang Language id
     *
     * @return array Attribute groups
     */
    public function getAttributesGroups($id_lang)
    {
        if (!Combination::isFeatureActive()) {
            return [];
        }
        'SELECT' = '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, product_attribute_shop.`id_product_attribute`,
                    IFNULL(stockquantity, 0) as quantity, product_attribute_shop.`price`, product_attribute_shop.`ecotax`, product_attribute_shop.`weight`,
                    product_attribute_shop.`default_on`, pa.`reference`, product_attribute_shop.`unit_price_impact`,
                    product_attribute_shop.`minimal_quantity`, product_attribute_shop.`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, agl.`name` ASC';

        return Db::getInstance()->executeS('SELECT');
    }

    /**
     * Delete product accessories.
     * Wrapper to static method deleteAccessories($product_id).
     *
     * @return mixed Deletion result
     */
    public function deleteAccessories()
    {
        return Db::getInstance()->delete('accessory', 'id_product_1 = ' . (int) $this->id);
    }

    /**
     * Delete product from other products accessories.
     *
     * @return mixed Deletion result
     */
    public function deleteFromAccessories()
    {
        return Db::getInstance()->delete('accessory', 'id_product_2 = ' . (int) $this->id);
    }

    /**
     * Get product accessories (only names).
     *
     * @param int $id_lang Language id
     * @param int $id_product Product id
     *
     * @return array Product accessories
     */
    public static function getAccessoriesLight($id_lang, $id_product)
    {
        return Db::getInstance()->executeS(
            '
            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_langShop::addSqlRestrictionOnLang('pl') . '
            )
            WHERE `id_product_1` = ' . (int) $id_product
        );
    }

    /**
     * Get product accessories.
     *
     * @param int $id_lang Language id
     *
     * @return array Product accessories
     */
    public function getAccessories($id_lang, $active = true)
    {
        
        
/*        'SELECT' = 'SELECT p.*, product_shop.*, stockout_of_stock, IFNULL(stockquantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`link_rewrite`,
                    pl.`meta_description`, pl.`meta_keywords`, pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`,
                    image_shop.`id_image` id_image, il.`legend`, m.`name` as manufacturer_name, cl.`name` AS category_default, IFNULL(product_attribute_shopid_product_attribute, 0) id_product_attribute,
                    DATEDIFF(
                        p.`date_add`,
                        DATE_SUB(
                            "' . date('Y-m-d') . ' 00:00:00",
                            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_attribute_shop` product_attribute_shop
                    ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shopid_shop=' . (int) $this->id_shop . ')
                LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (
                    p.`id_product` = pl.`id_product`
                    AND pl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl') . '
                )
                LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl ON (
                    product_shop.`id_category_default` = cl.`id_category`
                    AND cl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('cl') . '
                )
                LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
                    ON (image_shop.`id_product` = p.`id_product` AND image_shopcover=1 AND image_shopid_shop=' . (int) $this->id_shop . ')
                LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`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`)
                ' . Product::sqlStock('p', 0) . '
                WHERE `id_product_1` = ' . (int) $this->id .
                ($active ? ' AND product_shop.`active` = 1 AND product_shop.`visibility` != \'none\'' : '') . '
                GROUP BY product_shopid_product';*/
        

        'SELECT' = 'SELECT p.*, product_shop.*, stockout_of_stock, IFNULL(stockquantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`link_rewrite`,
                    pl.`meta_description`, pl.`meta_keywords`, pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`,
                    image_shop.`id_image` id_image, il.`legend`, m.`name` as manufacturer_name, cl.`name` AS category_default, IFNULL(product_attribute_shopid_product_attribute, 0) id_product_attribute,
                    DATEDIFF(
                        p.`date_add`,
                        DATE_SUB(
                            "' . date('Y-m-d') . ' 00:00:00",
                            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_attribute_shop` product_attribute_shop
                    ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shopid_shop=' . (int) $this->id_shop . ')
                LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (
                    p.`id_product` = pl.`id_product`
                    AND pl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl') . '
                )
                LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl ON (
                    product_shop.`id_category_default` = cl.`id_category`
                    AND cl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('cl') . '
                )
                LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
                    ON (image_shop.`id_product` = p.`id_product` AND image_shopcover=1 AND image_shopid_shop=' . (int) $this->id_shop . ')
                LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`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`)
                ' . Product::sqlStock('p', 0) . '
LEFT JOIN `' . _DB_PREFIX_ . 'feature_product` fp ON (p.`id_product` = fp.`id_product` AND fp.`id_feature` = 208)
LEFT JOIN `' . _DB_PREFIX_ . 'feature_value_lang` fv ON (fp.`id_feature_value` = fv.`id_feature_value` AND fv.`id_lang` = 2)               
                WHERE `id_product_1` = ' . (int) $this->id .
                ($active ? ' AND product_shop.`active` = 1 AND product_shop.`visibility` != \'none\'' : '') . '
                GROUP BY product_shopid_product ORDER BY fvvalue + 0 ASC ';    
        
        
/*'SELECT' = 'SELECT  cpposition, p.*, product_shop.*, stockout_of_stock, IFNULL(stockquantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`link_rewrite`,
                    pl.`meta_description`, pl.`meta_keywords`, pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`, 
                    image_shop.`id_image` id_image, il.`legend`, m.`name` as manufacturer_name, cl.`name` AS category_default, IFNULL(product_attribute_shopid_product_attribute, 0) id_product_attribute,
                    DATEDIFF(
                        p.`date_add`,
                        DATE_SUB(
                            "' . date('Y-m-d') . ' 00:00:00",
                            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_attribute_shop` product_attribute_shop
                    ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shopid_shop=' . (int) $this->id_shop . ')
                LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (
                    p.`id_product` = pl.`id_product`
                    AND pl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl') . '
                )
                LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl ON (
                    product_shop.`id_category_default` = cl.`id_category`
                    AND cl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('cl') . '
                )
                LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
                    ON (image_shop.`id_product` = p.`id_product` AND image_shopcover=1 AND image_shopid_shop=' . (int) $this->id_shop . ')
                LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`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`)
                ' . Product::sqlStock('p', 0) . '
LEFT JOIN `' . _DB_PREFIX_ . 'category_product` cp ON (p.`id_product` = cp.`id_product`)                 
                WHERE `id_product_1` = ' . (int) $this->id .
                ($active ? ' AND product_shop.`active` = 1 AND product_shop.`visibility` != \'none\'' : '') . '
                GROUP BY product_shopid_product ORDER BY cpposition ASC';   */     
//var_dump('SELECT');
  //      exit; 
        if (!$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT')) {
            return [];
        }

        foreach ($result as $k => &$row) {
            if (!Product::checkAccessStatic((int) $row['id_product'], false)) {
                unset($result[$k]);

                continue;
            } else {
                $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
     * Wrapper to static method changeAccessories($accessories_id, $product_id).
     *
     * @param array $accessories_id Accessories ids
     */
    public function changeAccessories($accessories_id)
    {
        self::changeAccessoriesForProduct($accessories_id, $this->id);
    }

    /**
     * Link accessories with productNo need to inflate a full Product (better performances).
     *
     * @param array $accessories_id Accessories ids
     * @param int the product ID to link accessories on
     */
    public static function changeAccessoriesForProduct($accessories_id, $product_id)
    {
        foreach ($accessories_id as $id_product_2) {
            Db::getInstance()->insert('accessory', [
                'id_product_1' => (int) $product_id,
                'id_product_2' => (int) $id_product_2,
            ]);
        }
    }

    /**
     * Add new feature to product.
     */
    public function addFeaturesCustomToDB($id_value, $lang, $cust)
    {
        $row = ['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, $position, $sortorder)
    {
        if ($cust) {
            $row = ['id_feature' => (int) $id_feature, 'custom' => 1];
            Db::getInstance()->insert('feature_value', $row);
            $id_value = Db::getInstance()->Insert_ID();
        }
        $row = ['id_feature' => (int) $id_feature, 'id_product' => (int) $this->id, 'id_feature_value' => (int) $id_value, 'position' => $position, 'sortorder' => $sortorder];
        Db::getInstance()->insert('feature_product', $row);
        SpecificPriceRule::applyAllRules([(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 [];
        }
        if (!array_key_exists($id_product, self::$_cacheFeatures)) {
            self::$_cacheFeatures[$id_product] = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
                '
                SELECT fpid_feature, fpid_product, fpid_feature_value, fpposition, fpsortorder, custom
                FROM `' . _DB_PREFIX_ . 'feature_product` fp
                LEFT JOIN `' . _DB_PREFIX_ . 'feature_value` fv ON (fpid_feature_value = fvid_feature_value)
                WHERE `id_product` = ' . (int) $id_product
            );
        }

        return self::$_cacheFeatures[$id_product];
    }

    public static function cacheProductsFeatures($product_ids)
    {
        if (!Feature::isFeatureActive()) {
            return;
        }

        $product_implode = [];
        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, position, sortorder
        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']] = [];
            }
            self::$_cacheFeatures[$row['id_product']][] = $row;
        }
    }

    public static function cacheFrontFeatures($product_ids, $id_lang)
    {
        if (!Feature::isFeatureActive()) {
            return;
        }

        $product_implode = [];
        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, pfid_feature, pfposition, pfsortorder
        FROM ' . _DB_PREFIX_ . 'feature_product pf
        LEFT JOIN ' . _DB_PREFIX_ . 'feature_lang fl ON (flid_feature = pfid_feature AND flid_lang = ' . (int) $id_lang . ')
        LEFT JOIN ' . _DB_PREFIX_ . 'feature_value_lang fvl ON (fvlid_feature_value = pfid_feature_value AND fvlid_lang = ' . (int) $id_lang . ')
        LEFT JOIN ' . _DB_PREFIX_ . 'feature f ON (fid_feature = pfid_feature)
        ' . Shop::addSqlAssociation('feature', 'f') . '
        WHERE `id_product` IN (' . implode(',', $product_implode) . ')
        ORDER BY fposition ASC');

        foreach ($result as $row) {
            if (!array_key_exists($row['id_product'] . '-' . $id_lang, self::$_frontFeaturesCache)) {
                self::$_frontFeaturesCache[$row['id_product'] . '-' . $id_lang] = [];
            }
            if (!isset(self::$_frontFeaturesCache[$row['id_product'] . '-' . $id_lang][$row['id_feature']])) {
                self::$_frontFeaturesCache[$row['id_product'] . '-' . $id_lang][$row['id_feature']] = $row;
            }
        }
    }

    /**
     * Admin panel product search.
     *
     * @param int $id_lang Language id
     * @param string $query Search query
     *
     * @return array Matching products
     */
    public static function searchByName($id_lang, $query, Context $context = null, $limit = null)
    {
        if (!$context) {
            $context = Context::getContext();
        }

        'SELECT' = new DbQuery();
        'SELECT'->select('p.`id_product`, pl.`name`, p.`ean13`, p.`isbn`, p.`upc`, p.`mpn`, p.`active`, p.`reference`, m.`name` AS manufacturer_name, stock.`quantity`, product_shopadvanced_stock_management, p.`customizable`');
        'SELECT'->from('product', 'p');
        'SELECT'->join(Shop::addSqlAssociation('product', 'p'));
        'SELECT'->leftJoin(
            'product_lang',
            'pl',
            'p.`id_product` = pl.`id_product`
            AND pl.`id_lang` = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl')
        );
        'SELECT'->leftJoin('manufacturer', 'm', 'm.`id_manufacturer` = p.`id_manufacturer`');

        $where = 'pl.`name` LIKE \'%' . pSQL($query) . '%\'
        OR p.`ean13` LIKE \'%' . pSQL($query) . '%\'
        OR p.`isbn` LIKE \'%' . pSQL($query) . '%\'
        OR p.`upc` LIKE \'%' . pSQL($query) . '%\'
        OR p.`mpn` LIKE \'%' . pSQL($query) . '%\'
        OR p.`reference` LIKE \'%' . pSQL($query) . '%\'
        OR p.`supplier_reference` LIKE \'%' . pSQL($query) . '%\'
        OR EXISTS(SELECT * FROM `' . _DB_PREFIX_ . 'product_supplier` sp WHERE sp.`id_product` = p.`id_product` AND `product_supplier_reference` LIKE \'%' . pSQL($query) . '%\')';

        'SELECT'->orderBy('pl.`name` ASC');

        if ($limit) {
            'SELECT'->limit($limit);
        }

        if (Combination::isFeatureActive()) {
            $where .= ' OR EXISTS(SELECT * FROM `' . _DB_PREFIX_ . 'product_attribute` `pa` WHERE pa.`id_product` = p.`id_product` AND (pa.`reference` LIKE \'%' . pSQL($query) . '%\'
            OR pa.`supplier_reference` LIKE \'%' . pSQL($query) . '%\'
            OR pa.`ean13` LIKE \'%' . pSQL($query) . '%\'
            OR pa.`isbn` LIKE \'%' . pSQL($query) . '%\'
            OR pa.`mpn` LIKE \'%' . pSQL($query) . '%\'
            OR pa.`upc` LIKE \'%' . pSQL($query) . '%\'))';
        }
        'SELECT'->where($where);
        'SELECT'->join(Product::sqlStock('p', 0));

        $result = Db::getInstance()->executeS('SELECT');

        if (!$result) {
            return false;
        }

        $results_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 int $id_product_old Old product id
     * @param int $id_product_new New product id
     */
    public static function duplicateAttributes($id_product_old, $id_product_new)
    {
        $return = true;
        $combination_images = [];

        $result = Db::getInstance()->executeS(
            '
        SELECT pa.*, product_attribute_shop.*
            FROM `' . _DB_PREFIX_ . 'product_attribute` pa
            ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
            WHERE pa.`id_product` = ' . (int) $id_product_old
        );
        $combinations = [];

        foreach ($result as $row) {
            $id_product_attribute_old = (int) $row['id_product_attribute'];
            if (!isset($combinations[$id_product_attribute_old])) {
                $id_combination = null;
                $id_shop = null;
                $result2 = Db::getInstance()->executeS(
                    '
                SELECT *
                FROM `' . _DB_PREFIX_ . 'product_attribute_combination`
                    WHERE `id_product_attribute` = ' . $id_product_attribute_old
                );
            } else {
                $id_combination = (int) $combinations[$id_product_attribute_old];
                $id_shop = (int) $row['id_shop'];
                $context_old = Shop::getContext();
                $context_shop_id_old = Shop::getContextShopID();
                Shop::setContext(Shop::CONTEXT_SHOP, $id_shop);
            }

            $row['id_product'] = $id_product_new;
            unset($row['id_product_attribute']);

            $combination = new Combination($id_combination, null, $id_shop);
            foreach ($row as $k => $v) {
                $combination->$k = $v;
            }
            $return &= $combination->save();

            $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;
            }

            if (!isset($combinations[$id_product_attribute_old])) {
                $combinations[$id_product_attribute_old] = (int) $id_product_attribute_new;
                foreach ($result2 as $row2) {
                    $row2['id_product_attribute'] = $id_product_attribute_new;
                    $return &= Db::getInstance()->insert('product_attribute_combination', $row2);
                }
            } else {
                Shop::setContext($context_old, $context_shop_id_old);
            }

            //Copy suppliers
            $result3 = Db::getInstance()->executeS('
            SELECT *
            FROM `' . _DB_PREFIX_ . 'product_supplier`
            WHERE `id_product_attribute` = ' . (int) $id_product_attribute_old . '
            AND `id_product` = ' . (int) $id_product_old);

            foreach ($result3 as $row3) {
                unset($row3['id_product_supplier']);
                $row3['id_product'] = $id_product_new;
                $row3['id_product_attribute'] = $id_product_attribute_new;
                $return &= Db::getInstance()->insert('product_supplier', $row3);
            }
        }

        $impacts = self::getAttributesImpacts($id_product_old);

        if (is_array($impacts) && count($impacts)) {
            $impact_sql = 'INSERT INTO `' . _DB_PREFIX_ . 'attribute_impact` (`id_product`, `id_attribute`, `weight`, `price`) VALUES ';

            foreach ($impacts as $id_attribute => $impact) {
                $impact_sql .= '(' . (int) $id_product_new . ', ' . (int) $id_attribute . ', ' . (float) $impacts[$id_attribute]['weight'] . ', '
                    . (float) $impacts[$id_attribute]['price'] . '),';
            }

            $impact_sql = substr_replace($impact_sql, '', -1);
            $impact_sql .= ' ON DUPLICATE KEY UPDATE `price` = VALUES(price), `weight` = VALUES(weight)';

            Db::getInstance()->execute($impact_sql);
        }

        return !$return ? false : $combination_images;
    }

    public static function getAttributesImpacts($id_product)
    {
        $return = [];
        $result = Db::getInstance()->executeS(
            'SELECT ai.`id_attribute`, ai.`price`, ai.`weight`
            FROM `' . _DB_PREFIX_ . 'attribute_impact` ai
            WHERE ai.`id_product` = ' . (int) $id_product
        );

        if (!$result) {
            return [];
        }
        foreach ($result as $impact) {
            $return[$impact['id_attribute']]['price'] = (float) $impact['price'];
            $return[$impact['id_attribute']]['weight'] = (float) $impact['weight'];
        }

        return $return;
    }

    /**
     * Get product attribute image associations.
     *
     * @param int $id_product_attribute
     *
     * @return array
     */
    public static function _getAttributeImageAssociations($id_product_attribute)
    {
        $combination_images = [];
        $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 = [
                '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`, `id_lang` FROM `' . _DB_PREFIX_ . 'product_tag` WHERE `id_product` = ' . (int) $id_product_old);
        if (!Db::getInstance()->numRows()) {
            return true;
        }

        $data = [];
        foreach ($tags as $tag) {
            $data[] = [
                'id_product' => (int) $id_product_new,
                'id_tag' => (int) $tag['id_tag'],
                'id_lang' => (int) $tag['id_lang'],
            ];
        }

        return Db::getInstance()->insert('product_tag', $data);
    }

    public static function duplicateTaxes($id_product_old, $id_product_new)
    {
        $query = new DbQuery();
        $query->select('id_tax_rules_group, id_shop');
        $query->from('product_shop');
        $query->where('`id_product` = ' . (int) $id_product_old);

        $results = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query->build());

        if (!empty($results)) {
            foreach ($results as $result) {
                if (!Db::getInstance()->update(
                    'product_shop',
                    ['id_tax_rules_group' => (int) $result['id_tax_rules_group']],
                    'id_product=' . (int) $id_product_new . ' AND id_shop = ' . (int) $result['id_shop']
                )) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Duplicate prices when duplicating a product.
     *
     * @param int $id_product_old Old product id
     * @param int $id_product_new New product id
     */
    public static function duplicatePrices($id_product_old, $id_product_new)
    {
        $query = new DbQuery();
        $query->select('price, unit_price_ratio, id_shop');
        $query->from('product_shop');
        $query->where('`id_product` = ' . (int) $id_product_old);
        $results = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query->build());
        if (!empty($results)) {
            foreach ($results as $result) {
                if (!Db::getInstance()->update(
                    'product_shop',
                    ['price' => pSQL($result['price']), 'unit_price_ratio' => pSQL($result['unit_price_ratio'])],
                    'id_product=' . (int) $id_product_new . ' AND id_shop = ' . (int) $result['id_shop']
                )) {
                    return false;
                }
            }
        }

        return true;
    }

    public static function duplicateDownload($id_product_old, $id_product_new)
    {
        'SELECT' = '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('SELECT');
        if (!$results) {
            return true;
        }

        $data = [];
        foreach ($results as $row) {
            $new_filename = ProductDownload::getNewFilename();
            copy(_PS_DOWNLOAD_DIR_ . $row['filename'], _PS_DOWNLOAD_DIR_ . $new_filename);

            $data[] = [
                'id_product' => (int) $id_product_new,
                'display_filename' => pSQL($row['display_filename']),
                'filename' => pSQL($new_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'],
                'date_add' => date('Y-m-d H:i:s'),
            ];
        }

        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
        'SELECT' = 'SELECT `id_attachment` FROM `' . _DB_PREFIX_ . 'product_attachment` WHERE `id_product` = ' . (int) $id_product_old;
        $results = Db::getInstance()->executeS('SELECT');

        if (!$results) {
            return true;
        }

        $data = [];

        // Prepare data of table product_attachment
        foreach ($results as $row) {
            $data[] = [
                'id_product' => (int) $id_product_new,
                'id_attachment' => (int) $row['id_attachment'],
            ];
        }

        // Duplicate product attachement
        $res = Db::getInstance()->insert('product_attachment', $data);
        Product::updateCacheAttachment((int) $id_product_new);

        return $res;
    }

    /**
     * Duplicate features when duplicating a product.
     *
     * @param int $id_product_old Old product id
     * @param int $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'];

                foreach (Language::getIDs(false) as $id_lang) {
                    $result3 = Db::getInstance()->getRow('
                    SELECT *
                    FROM `' . _DB_PREFIX_ . 'feature_value_lang`
                    WHERE `id_feature_value` = ' . (int) $old_id_feature_value . '
                    AND `id_lang` = ' . (int) $id_lang);

                    if ($result3) {
                        $result3['id_feature_value'] = (int) $new_id_feature_value;
                        $result3['value'] = pSQL($result3['value']);
                        $return &= Db::getInstance()->insert('feature_value_lang', $result3);
                    }
                }
                $row['id_feature_value'] = $new_id_feature_value;
            }

            $row['id_product'] = (int) $id_product_new;
            $return &= Db::getInstance()->insert('feature_product', $row);
        }

        return $return;
    }

    protected static function _getCustomizationFieldsNLabels($product_id, $id_shop = null)
    {
        if (!Customization::isFeatureActive()) {
            return false;
        }

        if (Shop::isFeatureActive() && !$id_shop) {
            $id_shop = (int) Context::getContext()->shop->id;
        }

        $customizations = [];
        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 [];
        }

        $customization_field_ids = [];
        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`, `id_shop`, `name`
            FROM `' . _DB_PREFIX_ . 'customization_field_lang`
            WHERE `id_customization_field` IN (' . implode(', ', $customization_field_ids) . ')' . ($id_shop ? ' AND `id_shop` = ' . (int) $id_shop : '') . '
            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) {
            
            $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'])) {
                foreach ($customizations['labels'][$old_customization_field_id] as $customization_label) {
                    $data = [
                        'id_customization_field' => (int) $customization_field_id,
                        'id_lang' => (int) $customization_label['id_lang'],
                        'id_shop' => (int) $customization_label['id_shop'],
                        'name' => pSQL($customization_label['name']),
                    ];

                    if (!Db::getInstance()->insert('customization_field_lang', $data)) {
                        return false;
                    }
                }
            }
        }

        return true;
    }

    /**
     * Adds suppliers from old product onto a newly duplicated product.
     *
     * @param int $id_product_old
     * @param int $id_product_new
     */
    public static function duplicateSuppliers($id_product_old, $id_product_new)
    {
        $result = Db::getInstance()->executeS('
        SELECT *
        FROM `' . _DB_PREFIX_ . 'product_supplier`
        WHERE `id_product` = ' . (int) $id_product_old . ' AND `id_product_attribute` = 0');

        foreach ($result as $row) {
            unset($row['id_product_supplier']);
            $row['id_product'] = $id_product_new;
            if (!Db::getInstance()->insert('product_supplier', $row)) {
                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 && null === $this->tags) {
            $this->tags = Tag::getProductTags($this->id);
        }

        if (!($this->tags && array_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']) && $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)
    {
        Hook::exec('actionGetProductPropertiesBefore', [
            'id_lang' => $id_lang,
            'product' => &$row,
            'context' => $context,
        ]);

        if (!$row['id_product']) {
            return false;
        }

        if ($context == null) {
            $context = Context::getContext();
        }

        $id_product_attribute = $row['id_product_attribute'] = (!empty($row['id_product_attribute']) ? (int) $row['id_product_attribute'] : null);

        // 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() && $id_product_attribute === null
            && ((isset($row['cache_default_attribute']) && ($ipa_default = $row['cache_default_attribute']) !== null)
                || ($ipa_default = Product::getDefaultAttribute($row['id_product'], !$row['allow_oosp'])))) {
            $id_product_attribute = $row['id_product_attribute'] = $ipa_default;
        }
        if (!Combination::isFeatureActive() || !isset($row['id_product_attribute'])) {
            $id_product_attribute = $row['id_product_attribute'] = 0;
        }

        // Tax
        $usetax = !Tax::excludeTaxeOption();

        $cache_key = $row['id_product'] . '-' . $id_product_attribute . '-' . $id_lang . '-' . (int) $usetax;
        if (isset($row['id_product_pack'])) {
            $cache_key .= '-pack' . $row['id_product_pack'];
        }

        if (!isset($row['cover_image_id'])) {
            $cover = static::getCover($row['id_product']);
            if (isset($cover['id_image'])) {
                $row['cover_image_id'] = $cover['id_image'];
            }
        }

        if (isset($row['cover_image_id'])) {
            $cache_key .= '-cover' . (int) $row['cover_image_id'];
        }

        if (isset(self::$productPropertiesCache[$cache_key])) {
            return array_merge($row, self::$productPropertiesCache[$cache_key]);
        }

        // Datas
        $row['category'] = Category::getLinkRewrite((int) $row['id_category_default'], (int) $id_lang);
        $row['category_name'] = Db::getInstance()->getValue('SELECT name FROM ' . _DB_PREFIX_ . 'category_lang WHERE id_shop = ' . (int) $context->shop->id . ' AND id_lang = ' . (int) $id_lang . ' AND id_category = ' . (int) $row['id_category_default']);
        $row['link'] = $context->link->getProductLink((int) $row['id_product'], $row['link_rewrite'], $row['category'], $row['ean13']);

        $row['attribute_price'] = 0;
        if ($id_product_attribute) {
            $row['attribute_price'] = (float) Combination::getPrice($id_product_attribute);
        }

        if (isset($row['quantity_wanted'])) {
            // 'quantity_wanted' may very well be zero even if set
            $quantity = max((int) $row['minimal_quantity'], (int) $row['quantity_wanted']);
        } elseif (isset($row['cart_quantity'])) {
            $quantity = max((int) $row['minimal_quantity'], (int) $row['cart_quantity']);
        } else {
            $quantity = (int) $row['minimal_quantity'];
        }

        $row['price_tax_exc'] = Product::getPriceStatic(
            (int) $row['id_product'],
            false,
            $id_product_attribute,
            (self::$_taxCalculationMethod == PS_TAX_EXC ? 2 : 6),
            null,
            false,
            true,
            $quantity
        );

        if (self::$_taxCalculationMethod == PS_TAX_EXC) {
            $row['price_tax_exc'] = Tools::ps_round($row['price_tax_exc'], Context::getContext()->getComputingPrecision());
            $row['price'] = Product::getPriceStatic(
                (int) $row['id_product'],
                true,
                $id_product_attribute,
                6,
                null,
                false,
                true,
                $quantity
            );
            $row['price_without_reduction'] =
            $row['price_without_reduction_without_tax'] = Product::getPriceStatic(
                (int) $row['id_product'],
                false,
                $id_product_attribute,
                2,
                null,
                false,
                false,
                $quantity
            );
        } else {
            $row['price'] = Tools::ps_round(
                Product::getPriceStatic(
                    (int) $row['id_product'],
                    true,
                    $id_product_attribute,
                    6,
                    null,
                    false,
                    true,
                    $quantity
                ),
                Context::getContext()->getComputingPrecision()
            );
            $row['price_without_reduction'] = Product::getPriceStatic(
                (int) $row['id_product'],
                true,
                $id_product_attribute,
                6,
                null,
                false,
                false,
                $quantity
            );
            $row['price_without_reduction_without_tax'] = Product::getPriceStatic(
                (int) $row['id_product'],
                false,
                $id_product_attribute,
                6,
                null,
                false,
                false,
                $quantity
            );
        }

        $row['reduction'] = Product::getPriceStatic(
            (int) $row['id_product'],
            (bool) $usetax,
            $id_product_attribute,
            6,
            null,
            true,
            true,
            $quantity,
            true,
            null,
            null,
            null,
            $specific_prices
        );

        $row['reduction_without_tax'] = Product::getPriceStatic(
            (int) $row['id_product'],
            false,
            $id_product_attribute,
            6,
            null,
            true,
            true,
            $quantity,
            true,
            null,
            null,
            null,
            $specific_prices
        );

        $row['specific_prices'] = $specific_prices;

        $row['quantity'] = Product::getQuantity(
            (int) $row['id_product'],
            0,
            isset($row['cache_is_pack']) ? $row['cache_is_pack'] : null,
            $context->cart
        );

        $row['quantity_all_versions'] = $row['quantity'];

        if ($row['id_product_attribute']) {
            $row['quantity'] = Product::getQuantity(
                (int) $row['id_product'],
                $id_product_attribute,
                isset($row['cache_is_pack']) ? $row['cache_is_pack'] : null,
                $context->cart
            );

            $row['available_date'] = Product::getAvailableDate(
                (int) $row['id_product'],
                $id_product_attribute
            );
        }

        $row['id_image'] = Product::defineProductImage($row, $id_lang);
        $row['features'] = Product::getFrontFeaturesStatic((int) $id_lang, $row['id_product']);

        $row['attachments'] = [];
        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) : [];
        $row['nopackprice'] = $row['pack'] ? Pack::noPackPrice($row['id_product']) : 0;

        if ($row['pack'] && !Pack::isInStock($row['id_product'], $quantity, $context->cart)) {
            $row['quantity'] = 0;
        }

        $row['customization_required'] = false;
        if (isset($row['customizable']) && $row['customizable'] && Customization::isFeatureActive()) {
            if (count(Product::getRequiredCustomizableFieldsStatic((int) $row['id_product']))) {
                $row['customization_required'] = true;
            }
        }

        $attributes = Product::getAttributesParams($row['id_product'], $row['id_product_attribute']);

        foreach ($attributes as $attribute) {
            $row['attributes'][$attribute['id_attribute_group']] = $attribute;
        }

        $row = Product::getTaxesInformations($row, $context);

        $row['ecotax_rate'] = (float) Tax::getProductEcotaxRate($context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')});

        Hook::exec('actionGetProductPropertiesAfter', [
            'id_lang' => $id_lang,
            'product' => &$row,
            'context' => $context,
        ]);

        $combination = new Combination($id_product_attribute);

        if (0 != $combination->unit_price_impact && 0 != $row['unit_price_ratio']) {
            $unitPrice = ($row['price_tax_exc'] / $row['unit_price_ratio']) + $combination->unit_price_impact;
            $row['unit_price_ratio'] = $row['price_tax_exc'] / $unitPrice;
        }

        $row['unit_price'] = ($row['unit_price_ratio'] != 0 ? $row['price'] / $row['unit_price_ratio'] : 0);

        self::$productPropertiesCache[$cache_key] = $row;

        return self::$productPropertiesCache[$cache_key];
    }

    public static function getTaxesInformations($row, Context $context = null)
    {
        static $address = null;

        if ($context === null) {
            $context = Context::getContext();
        }
        if ($address === null) {
            $address = new Address();
        }

        $address->id_country = (int) $context->country->id;
        $address->id_state = 0;
        $address->postcode = 0;

        $tax_manager = TaxManagerFactory::getManager($address, Product::getIdTaxRulesGroupByIdProduct((int) $row['id_product'], $context));
        $row['rate'] = $tax_manager->getTaxCalculator()->getTotalRate();
        $row['tax_name'] = $tax_manager->getTaxCalculator()->getTaxesName();

        return $row;
    }

    public static function getProductsProperties($id_lang, $query_result)
    {
        $results_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 [];
        }
        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, pfid_feature, pfposition, pfsortorder
                FROM ' . _DB_PREFIX_ . 'feature_product pf
                LEFT JOIN ' . _DB_PREFIX_ . 'feature_lang fl ON (flid_feature = pfid_feature AND flid_lang = ' . (int) $id_lang . ')
                LEFT JOIN ' . _DB_PREFIX_ . 'feature_value_lang fvl ON (fvlid_feature_value = pfid_feature_value AND fvlid_lang = ' . (int) $id_lang . ')
                LEFT JOIN ' . _DB_PREFIX_ . 'feature f ON (fid_feature = pfid_feature AND flid_lang = ' . (int) $id_lang . ')
                ' . Shop::addSqlAssociation('feature', 'f') . '
                WHERE pfid_product = ' . (int) $id_product . '
                ORDER BY pfsortorder 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 aid_attachment = paid_attachment
        LEFT JOIN ' . _DB_PREFIX_ . 'attachment_lang al ON (aid_attachment = alid_attachment AND alid_lang = ' . (int) $id_lang . ')
        WHERE paid_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, $id_shop = null, $id_customization = null)
    {
        if (!Customization::isFeatureActive()) {
            return false;
        }

        // No need to query if there isn't any real cart!
        if (!$id_cart) {
            return false;
        }

        if ($id_customization === 0) {
            // Backward compatibility: check if there are no products in cart with specific `id_customization` before returning false
            $product_customizations = (int) Db::getInstance()->getValue('
                SELECT COUNT(`id_customization`) FROM `' . _DB_PREFIX_ . 'cart_product`
                WHERE `id_cart` = ' . (int) $id_cart .
                ' AND `id_customization` != 0');
            if ($product_customizations) {
                return false;
            }
        }

        if (!$id_lang) {
            $id_lang = Context::getContext()->language->id;
        }
        if (Shop::isFeatureActive() && !$id_shop) {
            $id_shop = (int) Context::getContext()->shop->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`, cd.`id_module`, cfl.`name`
            FROM `' . _DB_PREFIX_ . 'customized_data` cd
            NATURAL JOIN `' . _DB_PREFIX_ . 'customization` c
            LEFT JOIN `' . _DB_PREFIX_ . 'customization_field_lang` cfl ON (cflid_customization_field = cd.`index` AND id_lang = ' . (int) $id_lang .
                ($id_shop ? ' AND cfl.`id_shop` = ' . (int) $id_shop : '') . ')
            WHERE c.`id_cart` = ' . (int) $id_cart .
            ($only_in_cart ? ' AND c.`in_cart` = 1' : '') .
            ((int) $id_customization ? ' AND cd.`id_customization` = ' . (int) $id_customization : '') . '
            ORDER BY `id_product`, `id_product_attribute`, `type`, `index`')) {
            return false;
        }

        $customized_datas = [];

        foreach ($result as $row) {
            if ((int) $row['id_module'] && (int) $row['type'] == Product::CUSTOMIZE_TEXTFIELD) {
                // Hook displayCustomization: Call only the module in question
                // When a module saves a customization programmatically, it should add its ID in the `id_module` column
                $row['value'] = Hook::exec('displayCustomization', ['customization' => $row], (int) $row['id_module']);
            }
            $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 .
            ((int) $id_customization ? ' AND `id_customization` = ' . (int) $id_customization : '') .
            ($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'];
            $customized_datas[(int) $row['id_product']][(int) $row['id_product_attribute']][(int) $row['id_address_delivery']][(int) $row['id_customization']]['id_customization'] = (int) $row['id_customization'];
        }

        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;

                
                $product_id = isset($product_update['id_product']) ? (int) $product_update['id_product'] : (int) $product_update['product_id'];
                $product_attribute_id = isset($product_update['id_product_attribute']) ? (int) $product_update['id_product_attribute'] : (int) $product_update['product_attribute_id'];
                $id_address_delivery = (int) $product_update['id_address_delivery'];
                $product_quantity = isset($product_update['cart_quantity']) ? (int) $product_update['cart_quantity'] : (int) $product_update['product_quantity'];
                $price = isset($product_update['price']) ? $product_update['price'] : $product_update['product_price'];
                if (isset($product_update['price_wt']) && $product_update['price_wt']) {
                    $price_wt = $product_update['price_wt'];
                } else {
                    $price_wt = $price * (1 + ((isset($product_update['tax_rate']) ? $product_update['tax_rate'] : $product_update['rate']) * 001));
                }

                if (!isset($customized_datas[$product_id][$product_attribute_id][$id_address_delivery])) {
                    $id_address_delivery = 0;
                }
                if (isset($customized_datas[$product_id][$product_attribute_id][$id_address_delivery])) {
                    foreach ($customized_datas[$product_id][$product_attribute_id][$id_address_delivery] as $customization) {
                        if ((int) $product_update['id_customization'] && $customization['id_customization'] != $product_update['id_customization']) {
                            continue;
                        }
                        $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;
                }
            }
        }
    }

    /*
    ** Add customization price for a single product
    */
    public static function addProductCustomizationPrice(&$product, &$customized_datas)
    {
        if (!$customized_datas) {
            return;
        }

        $products = [$product];
        self::addCustomizationPrice($products, $customized_datas);
        $product = "=";
    }

    /*
    ** 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 = [
            Product::CUSTOMIZE_FILE => (int) $this->uploadable_files,
            Product::CUSTOMIZE_TEXTFIELD => (int) $this->text_fields,
        ];

        
        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 = [
            Product::CUSTOMIZE_FILE => [],
            Product::CUSTOMIZE_TEXTFIELD => [],
        ];

        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 ($extra_file > 0 && count($customization_fields[Product::CUSTOMIZE_FILE]) - $extra_file >= 0 &&
        (!Db::getInstance()->execute(
            'DELETE `' . _DB_PREFIX_ . 'customization_field`,`' . _DB_PREFIX_ . 'customization_field_lang`
            FROM `' . _DB_PREFIX_ . 'customization_field` JOIN `' . _DB_PREFIX_ . 'customization_field_lang`
            WHERE `' . _DB_PREFIX_ . 'customization_field`.`id_product` = ' . (int) $this->id . '
            AND `' . _DB_PREFIX_ . 'customization_field`.`type` = ' . Product::CUSTOMIZE_FILE . '
            AND `' . _DB_PREFIX_ . 'customization_field_lang`.`id_customization_field` = `' . _DB_PREFIX_ . 'customization_field`.`id_customization_field`
            AND `' . _DB_PREFIX_ . 'customization_field`.`id_customization_field` >= ' . (int) $customization_fields[Product::CUSTOMIZE_FILE][count($customization_fields[Product::CUSTOMIZE_FILE]) - $extra_file]
        ))) {
            return false;
        }

        if ($extra_text > 0 && count($customization_fields[Product::CUSTOMIZE_TEXTFIELD]) - $extra_text >= 0 &&
        (!Db::getInstance()->execute(
            'DELETE `' . _DB_PREFIX_ . 'customization_field`,`' . _DB_PREFIX_ . 'customization_field_lang`
            FROM `' . _DB_PREFIX_ . 'customization_field` JOIN `' . _DB_PREFIX_ . 'customization_field_lang`
            WHERE `' . _DB_PREFIX_ . 'customization_field`.`id_product` = ' . (int) $this->id . '
            AND `' . _DB_PREFIX_ . 'customization_field`.`type` = ' . Product::CUSTOMIZE_TEXTFIELD . '
            AND `' . _DB_PREFIX_ . 'customization_field_lang`.`id_customization_field` = `' . _DB_PREFIX_ . 'customization_field`.`id_customization_field`
            AND `' . _DB_PREFIX_ . 'customization_field`.`id_customization_field` >= ' . (int) $customization_fields[Product::CUSTOMIZE_TEXTFIELD][count($customization_fields[Product::CUSTOMIZE_TEXTFIELD]) - $extra_text]
        ))) {
            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) {
            foreach (Shop::getContextListShopID() as $id_shop) {
                $values .= '(' . (int) $id_customization_field . ', ' . (int) $language['id_lang'] . ', ' . (int) $id_shop . ',\'\'), ';
            }
        }

        $values = rtrim($values, ', ');
        if (!Db::getInstance()->execute('
            INSERT INTO `' . _DB_PREFIX_ . 'customization_field_lang` (`id_customization_field`, `id_lang`, `id_shop`, `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) {
            
            if (strncmp($field, 'label_', 6) == 0) {
                if (!$tmp = $this->_checkLabelField($field, $value)) {
                    return false;
                }
                
                if (Shop::isFeatureActive()) {
                    foreach (Shop::getContextListShopID() as $id_shop) {
                        if (!Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'customization_field_lang`
                        (`id_customization_field`, `id_lang`, `id_shop`, `name`) VALUES (' . (int) "', ' . (int) "i, ' . (int) $id_shop . ', \'' . pSQL($value) . '\')
                        ON DUPLICATE KEY UPDATE `name` = \'' . pSQL($value) . '\'')) {
                            return false;
                        }
                    }
                } elseif (!Db::getInstance()->execute('
                    INSERT INTO `' . _DB_PREFIX_ . 'customization_field_lang`
                    (`id_customization_field`, `id_lang`, `name`) VALUES (' . (int) "', ' . (int) "i, \'' . pSQL($value) . '\')
                    ON DUPLICATE KEY UPDATE `name` = \'' . pSQL($value) . '\'')) {
                    return false;
                }

                $is_required = isset($_POST['require_' . (int) " _' . (int) "'"]) ? 1 : 0;
                $has_required_fields |= $is_required;
                
                if (!Db::getInstance()->execute(
                    'UPDATE `' . _DB_PREFIX_ . 'customization_field`
                    SET `required` = ' . (int) $is_required . '
                    WHERE `id_customization_field` = ' . (int) "'"
                )) {
                    return false;
                }
            }
        }

        if ($has_required_fields && !ObjectModel::updateMultishopTable('product', ['customizable' => 2], 'aid_product = ' . (int) $this->id)) {
            return false;
        }

        if (!$this->_deleteOldLabels()) {
            return false;
        }

        return true;
    }

    public function getCustomizationFields($id_lang = false, $id_shop = null)
    {
        if (!Customization::isFeatureActive()) {
            return false;
        }

        if (Shop::isFeatureActive() && !$id_shop) {
            $id_shop = (int) Context::getContext()->shop->id;
        }

        // Hide the modules fields in the front-office
        // When a module adds a customization programmatically, it should set the `is_module` to 1
        $context = Context::getContext();
        $front = isset($context->controller->controller_type) && in_array($context->controller->controller_type, ['front']);

        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 : '') .
            ($id_shop ? ' AND cfl.`id_shop` = ' . (int) $id_shop : '') .
            ($front ? ' AND !cf.`is_module`' : '') . '
            AND cf.`is_deleted` = 0
            ORDER BY cf.`id_customization_field`')
        ) {
            return false;
        }

        if ($id_lang) {
            return $result;
        }

        $customization_fields = [];
        foreach ($result as $row) {
            $customization_fields[(int) $row['type']][(int) $row['id_customization_field']][(int) $row['id_lang']] = $row;
        }

        return $customization_fields;
    }

    /**
     * check if product has an activated and required customizationFields.
     *
     * @return bool
     *
     * @throws \PrestaShopDatabaseException
     */
    public function hasActivatedRequiredCustomizableFields()
    {
        if (!Customization::isFeatureActive()) {
            return false;
        }

        return (bool) Db::getInstance()->executeS(
            '
            SELECT 1
            FROM `' . _DB_PREFIX_ . 'customization_field`
            WHERE `id_product` = ' . (int) $this->id . '
            AND `required` = 1
            AND `is_deleted` = 0'
        );
    }

    public function getCustomizationFieldIds()
    {
        if (!Customization::isFeatureActive()) {
            return [];
        }

        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 [];
        }

        return Product::getRequiredCustomizableFieldsStatic($this->id);
    }

    public static function getRequiredCustomizableFieldsStatic($id)
    {
        if (!$id || !Customization::isFeatureActive()) {
            return [];
        }

        return Db::getInstance()->executeS(
            '
            SELECT `id_customization_field`, `type`
            FROM `' . _DB_PREFIX_ . 'customization_field`
            WHERE `id_product` = ' . (int) $id . '
            AND `required` = 1 AND `is_deleted` = 0'
        );
    }

    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 = [];
        foreach ($fields as $field) {
            $fields_present[] = ['id_customization_field' => $field['index'], 'type' => $field['type']];
        }

        if (is_array($required_fields) && count($required_fields)) {
            foreach ($required_fields as $required_field) {
                if (!in_array($required_field, $fields_present)) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Return the list of old temp products.
     *
     * @return array
     */
    public static function getOldTempProducts()
    {
        'SELECT' = 'SELECT id_product FROM `' . _DB_PREFIX_ . 'product` WHERE state=' . \Product::STATE_TEMP . ' AND date_upd < NOW() - INTERVAL 1 DAY';

        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT', true, false);
    }

    /**
     * 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 bool 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;
        }
        'SELECT' = 'SELECT id_product FROM `' . _DB_PREFIX_ . 'category_product` WHERE `id_product` = ' . (int) $id_product . ' AND `id_category` IN (';
        foreach ($categories as $category) {
            'SELECT' .= (int) $category['id_category'] . ',';
        }
        'SELECT' = rtrim('SELECT', ',') . ')';

        $hash = md5('SELECT');
        if (!isset(self::$_incat[$hash])) {
            if (!Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('SELECT')) {
                return false;
            }
            self::$_incat[$hash] = (Db::getInstance(_PS_USE_SQL_SLAVE_)->numRows() > 0 ? true : false);
        }

        return self::$_incat[$hash];
    }

    public function getNoPackPrice()
    {
        $context = Context::getContext();

        return Tools::getContextLocale($context)->formatPrice(Pack::noPackPrice((int) $this->id), $context->currency->iso_code);
    }

    public function checkAccess($id_customer)
    {
        return Product::checkAccessStatic((int) $this->id, (int) $id_customer);
    }

    public static function checkAccessStatic($id_product, $id_customer)
    {
        if (!Group::isFeatureActive()) {
            return true;
        }

        'productlist_colors' = 'Product::checkAccess_' . (int) $id_product . '-' . (int) $id_customer . (!$id_customer ? '-' . (int) Group::getCurrent()->id : '');
        if (!Cache::isStored('productlist_colors')) {
            if (!$id_customer) {
                $result = (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) $id_product . ' AND ctg.`id_group` = ' . (int) Group::getCurrent()->id);
            } else {
                $result = (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) $id_product . ' AND cg.`id_customer` = ' . (int) $id_customer);
            }

            Cache::store('productlist_colors', $result);

            return $result;
        }

        return Cache::retrieve('productlist_colors');
    }

    /**
     * Add a stock movement for current product.
     *
     * Since 15, 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 150
     *
     * @param int $quantity
     * @param int $id_reason - useless
     * @param int $id_product_attribute
     * @param int $id_order - DEPRECATED
     * @param int $id_employee - DEPRECATED
     *
     * @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 150
     */
    public function getStockMvts($id_lang)
    {
        Tools::displayAsDeprecated();

        return Db::getInstance()->executeS('
            SELECT smid_stock_mvt, smdate_add, smquantity, smid_order,
            CONCAT(plname, \' \', GROUP_CONCAT(IFNULL(alname, \'\'), \'\')) product_name, CONCAT(elastname, \' \', efirstname) employee, mrlname reason
            FROM `' . _DB_PREFIX_ . 'stock_mvt` sm
            LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (
                smid_product = plid_product
                AND plid_lang = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl') . '
            )
            LEFT JOIN `' . _DB_PREFIX_ . 'stock_mvt_reason_lang` mrl ON (
                smid_stock_mvt_reason = mrlid_stock_mvt_reason
                AND mrlid_lang = ' . (int) $id_lang . '
            )
            LEFT JOIN `' . _DB_PREFIX_ . 'employee` e ON (
                eid_employee = smid_employee
            )
            LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON (
                pacid_product_attribute = smid_product_attribute
            )
            LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ON (
                alid_attribute = pacid_attribute
                AND alid_lang = ' . (int) $id_lang . '
            )
            WHERE smid_product=' . (int) $this->id . '
            GROUP BY smid_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)) {
            $result = 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->shop->id);
            Cache::store($key, (int) $result);

            return (int) $result;
        }

        return Cache::retrieve($key);
    }

    /**
     * Returns tax rate.
     *
     * @param Address|null $address
     *
     * @return float 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'],
                    $rows[$keyrow]['custom']
                );
            }
            asort($rows[$keyrow]);
        }

        return $rows;
    }

    /**
     * Webservice setter : set product features association.
     *
     * @param $product_features Product Feature ids
     *
     * @return bool
     */
    public function setWsProductFeatures($product_features)
    {
        Db::getInstance()->execute(
            '
            DELETE FROM `' . _DB_PREFIX_ . 'feature_product`
            WHERE `id_product` = ' . (int) $this->id
        );
        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 int $id_combination id default combination
     *
     * @return bool
     */
    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 (cid_category = cpid_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 array $category_ids category ids
     *
     * @return bool
     */
    public function setWsCategories($category_ids)
    {
        $ids = [];
        foreach ($category_ids as $value) {
            if ($value instanceof Category) {
                $ids[] = (int) $value->id;
            } elseif (is_array($value) && array_key_exists('id', $value)) {
                $ids[] = (int) $value['id'];
            } else {
                $ids[] = (int) $value;
            }
        }
        $ids = array_unique($ids);

        $positions = Db::getInstance()->executeS(
                'SELECT `id_category`, `position`
                FROM `' . _DB_PREFIX_ . 'category_product`
                WHERE `id_product` = ' . (int) $this->id
        );

        $max_positions = Db::getInstance()->executeS(
                'SELECT `id_category`, max(`position`) as maximum
                FROM `' . _DB_PREFIX_ . 'category_product`
                GROUP BY id_category'
        );

        $positions_lookup = [];
        $max_position_lookup = [];

        foreach ($positions as $row) {
            $positions_lookup[(int) $row['id_category']] = (int) $row['position'];
        }
        foreach ($max_positions as $row) {
            $max_position_lookup[(int) $row['id_category']] = (int) $row['maximum'];
        }

        $return = true;
        if ($this->deleteCategories() && !empty($ids)) {
            'SELECT'_values = [];
            foreach ($ids as $id) {
                $pos = 0;
                if (array_key_exists((int) $id, $positions_lookup)) {
                    $pos = (int) $positions_lookup[(int) $id] + 1;
                } elseif (array_key_exists((int) $id, $max_position_lookup)) {
                    $pos = (int) $max_position_lookup[(int) $id] + 1;
                }

                'SELECT'_values[] = '(' . (int) $id . ', ' . (int) $this->id . ', ' . $pos . ')';
            }

            $return = Db::getInstance()->execute(
                '
                INSERT INTO `' . _DB_PREFIX_ . 'category_product` (`id_category`, `id_product`, `position`)
                VALUES ' . implode(',', 'SELECT'_values)
            );
        }

        Hook::exec('actionProductUpdate', ['id_product' => (int) $this->id]);

        return $return;
    }

    /**
     * Webservice getter : get product accessories ids of current product for association.
     *
     * @return array
     */
    public function getWsAccessories()
    {
        $result = Db::getInstance()->executeS(
            'SELECT p.`id_product` AS id
            FROM `' . _DB_PREFIX_ . 'accessory` a
            LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON (pid_product = aid_product_2)
            ' . Shop::addSqlAssociation('product', 'p') . '
            WHERE a.`id_product_1` = ' . (int) $this->id
        );

        return $result;
    }

    /**
     * Webservice setter : set product accessories ids of current product for association.
     *
     * @param $accessories product ids
     */
    public function setWsAccessories($accessories)
    {
        $this->deleteAccessories();
        foreach ($accessories as $accessory) {
            Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'accessory` (`id_product_1`, `id_product_2`) VALUES (' . (int) $this->id . ', ' . (int) $accessory['id'] . ')');
        }

        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 pa.`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 = [];
        foreach ($combinations as $combination) {
            $ids_new[] = (int) $combination['id'];
        }

        $ids_orig = [];
        $original = Db::getInstance()->executeS(
            'SELECT pa.`id_product_attribute` as id
            FROM `' . _DB_PREFIX_ . 'product_attribute` pa
            ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
            WHERE pa.`id_product` = ' . (int) $this->id
        );

        if (is_array($original)) {
            foreach ($original as $id) {
                $ids_orig[] = $id['id'];
            }
        }

        $all_ids = [];
        $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 = [];
        foreach ($ids_new as $id) {
            if (!in_array($id, $ids_orig)) {
                $to_add[] = $id;
            }
        }

        $to_delete = [];
        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 (' . (int) $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 pacid_attribute as id
            FROM `' . _DB_PREFIX_ . 'product_attribute` pa
            ' . Shop::addSqlAssociation('product_attribute', 'pa') . '
            LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute_combination` pac ON (pacid_product_attribute = paid_product_attribute)
            WHERE paid_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 "="['position'];
        }

        return '';
    }

    /**
     * Webservice setter : set virtual field position in category.
     *
     * @return bool
     */
    public function setWsPositionInCategory($position)
    {
        if ($position < 0) {
            WebserviceRequest::getInstance()->setError(500, $this->trans('You cannot set a negative position, the minimum for a position is 0.', [], 'AdminCatalogNotification'), 134);
        }
        $result = Db::getInstance()->executeS('
            SELECT `id_product`
            FROM `' . _DB_PREFIX_ . 'category_product`
            WHERE `id_category` = ' . (int) $this->id_category_default . '
            ORDER BY `position`
        ');
        if (($position > 0) && ($position + 1 > count($result))) {
            WebserviceRequest::getInstance()->setError(500, $this->trans('You cannot set a position greater than the total number of products in the category, minus 1 (position numbering starts at 0).', [], 'AdminCatalogNotification'), 135);
        }

        foreach ($result as &$value) {
            $value = $value['id_product'];
        }
        $current_position = $this->getWsPositionInCategory();

        if ($current_position && isset($result[$current_position])) {
            $save = $result[$current_position];
            unset($result[$current_position]);
            array_splice($result, (int) $position, 0, $save);
        }

        foreach ($result as $position => $id_product) {
            Db::getInstance()->update('category_product', [
                'position' => $position,
            ], '`id_category` = ' . (int) $this->id_category_default . ' AND `id_product` = ' . (int) $id_product);
        }

        return true;
    }

    /**
     * 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_shop` image_shop, `' . _DB_PREFIX_ . 'image` i
            SET image_shop.`cover` = NULL
            WHERE i.`id_product` = ' . (int) $this->id . ' AND iid_image = image_shopid_image
            AND image_shopid_shop=' . (int) Context::getContext()->shop->id);

        Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'image_shop`
            SET `cover` = 1 WHERE `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 getWsStockAvailables()
    {
        return Db::getInstance()->executeS('SELECT `id_stock_available` id, `id_product_attribute`
            FROM `' . _DB_PREFIX_ . 'stock_available`
            WHERE `id_product`=' . (int) $this->idStockAvailable::addSqlShopRestriction());
    }

    public function getWsTags()
    {
        return Db::getInstance()->executeS('
            SELECT `id_tag` as id
            FROM `' . _DB_PREFIX_ . 'product_tag`
            WHERE `id_product` = ' . (int) $this->id);
    }

    /**
     * Webservice setter : set tag ids of current product for association.
     *
     * @param $tag_ids tag ids
     */
    public function setWsTags($tag_ids)
    {
        $ids = [];
        foreach ($tag_ids as $value) {
            $ids[] = $value['id'];
        }
        if ($this->deleteWsTags()) {
            if ($ids) {
                'SELECT'_values = [];
                $ids = array_map('intval', $ids);
                foreach ($ids as $position => $id) {
                    $id_lang = Db::getInstance()->getValue('SELECT `id_lang` FROM `' . _DB_PREFIX_ . 'tag` WHERE `id_tag`=' . (int) $id);
                    'SELECT'_values[] = '(' . (int) $this->id . ', ' . (int) $id . ', ' . (int) $id_lang . ')';
                }
                $result = Db::getInstance()->execute(
                    '
                    INSERT INTO `' . _DB_PREFIX_ . 'product_tag` (`id_product`, `id_tag`, `id_lang`)
                    VALUES ' . implode(',', 'SELECT'_values)
                );

                return $result;
            }
        }

        return true;
    }

    /**
     * Delete products tags entries without delete tags for webservice usage.
     *
     * @return array Deletion result
     */
    public function deleteWsTags()
    {
        return Db::getInstance()->delete('product_tag', 'id_product = ' . (int) $this->id);
    }

    public function getWsManufacturerName()
    {
        return Manufacturer::getNameById((int) $this->id_manufacturer);
    }

    public static function resetEcoTax()
    {
        return ObjectModel::updateMultishopTable('product', [
            'ecotax' => 0,
        ]);
    }

    /**
     * Set Group reduction if needed.
     */
    public function setGroupReduction()
    {
        return GroupReduction::setProductReduction($this->id);
    }

    /**
     * Checks if reference exists.
     *
     * @return bool
     */
    public function existsRefInDatabase($reference)
    {
        $row = Db::getInstance()->getRow('
        SELECT `reference`
        FROM `' . _DB_PREFIX_ . 'product` p
        WHERE preference = "' . pSQL($reference) . '"');

        return isset($row['reference']);
    }

    /**
     * Get all product attributes ids.
     *
     * @since 150
     *
     * @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 paid_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.
     *
     * @param int $id_product
     * @param int $product_attribute_id
     *
     * @return array
     */
    public static function getAttributesParams($id_product, $id_product_attribute)
    {
        if ($id_product_attribute == 0) {
            return [];
        }
        $id_lang = (int) Context::getContext()->language->id;
        'productlist_colors' = 'Product::getAttributesParams_' . (int) $id_product . '-' . (int) $id_product_attribute . '-' . (int) $id_lang;

        if (!Cache::isStored('productlist_colors')) {
            $result = Db::getInstance()->executeS('
            SELECT a.`id_attribute`, a.`id_attribute_group`, al.`name`, agl.`name` as `group`, pa.`reference`, pa.`ean13`, pa.`isbn`, pa.`upc`, pa.`mpn`
            FROM `' . _DB_PREFIX_ . 'attribute` a
            LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al
                ON (al.`id_attribute` = a.`id_attribute` AND al.`id_lang` = ' . (int) $id_lang . ')
            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) $id_lang . ')
            WHERE pa.`id_product` = ' . (int) $id_product . '
                AND pac.`id_product_attribute` = ' . (int) $id_product_attribute . '
                AND agl.`id_lang` = ' . (int) $id_lang);
            Cache::store('productlist_colors', $result);
        } else {
            $result = Cache::retrieve('productlist_colors');
        }

        return $result;
    }

    /**
     * @param int $id_product
     */
    public static function getAttributesInformationsByProduct($id_product)
    {
        $result = Db::getInstance()->executeS('
        SELECT DISTINCT a.`id_attribute`, a.`id_attribute_group`, al.`name` as `attribute`, agl.`name` as `group`,pa.`reference`, pa.`ean13`, pa.`isbn`, pa.`upc`, pa.`mpn`
        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;
    }

    /**
     * @return bool
     */
    public function hasCombinations()
    {
        if (null === $this->id || 0 >= $this->id) {
            return false;
        }
        $attributes = self::getAttributesInformationsByProduct($this->id);

        return !empty($attributes);
    }

    /**
     * Get an id_product_attribute by an id_product and one or more
     * id_attribute.
     *
     * eg: id_product 8 with id_attribute 4 (size medium) and
     * id_attribute 5 (color blue) returns id_product_attribute 9 which
     * is the dress size medium and color blue.
     *
     * @param int $idProduct
     * @param int|int[] $idAttributes
     * @param bool $findBest
     *
     * @return int
     *
     * @throws PrestaShopException
     */
    public static function getIdProductAttributeByIdAttributes($idProduct, $idAttributes, $findBest = false)
    {
        $idProduct = (int) $idProduct;

        if (!is_array($idAttributes) && is_numeric($idAttributes)) {
            $idAttributes = [(int) $idAttributes];
        }

        if (!is_array($idAttributes) || empty($idAttributes)) {
            throw new PrestaShopException(sprintf('Invalid parameter $idAttributes with value: "%s"', print_r($idAttributes, true)));
        }

        $idAttributesImploded = implode(',', array_map('intval', $idAttributes));
        $idProductAttribute = Db::getInstance()->getValue(
            '
            SELECT
                pac.`id_product_attribute`
            FROM
                `' . _DB_PREFIX_ . 'product_attribute_combination` pac
                INNER JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON paid_product_attribute = pacid_product_attribute
            WHERE
                paid_product = ' . $idProduct . '
                AND pacid_attribute IN (' . $idAttributesImploded . ')
            GROUP BY
                pac.`id_product_attribute`
            HAVING
                COUNT(paid_product) = ' . count($idAttributes)
        );

        if ($idProductAttribute === false && $findBest) {
            //find the best possible combination
            //first we order $idAttributes by the group position
            $orderred = [];
            $result = Db::getInstance()->executeS(
                '
                SELECT
                    a.`id_attribute`
                FROM
                    `' . _DB_PREFIX_ . 'attribute` a
                    INNER JOIN `' . _DB_PREFIX_ . 'attribute_group` g ON a.`id_attribute_group` = g.`id_attribute_group`
                WHERE
                    a.`id_attribute` IN (' . $idAttributesImploded . ')
                ORDER BY
                    g.`position` ASC'
            );

            foreach ($result as $row) {
                $orderred[] = $row['id_attribute'];
            }

            while ($idProductAttribute === false && count($orderred) > 1) {
                array_pop($orderred);
                $idProductAttribute = Db::getInstance()->getValue(
                    '
                    SELECT
                        pac.`id_product_attribute`
                    FROM
                        `' . _DB_PREFIX_ . 'product_attribute_combination` pac
                        INNER JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON paid_product_attribute = pacid_product_attribute
                    WHERE
                        paid_product = ' . (int) $idProduct . '
                        AND pacid_attribute IN (' . implode(',', array_map('intval', $orderred)) . ')
                    GROUP BY
                        pacid_product_attribute
                    HAVING
                        COUNT(paid_product) = ' . count($orderred)
                );
            }
        }

        if (empty($idProductAttribute)) {
            throw new PrestaShopObjectNotFoundException('Can not retrieve the id_product_attribute');
        }

        return $idProductAttribute;
    }

    /**
     * @deprecated 1731
     * @see Product::getIdProductAttributeByIdAttributes()
     */
    public static function getIdProductAttributesByIdAttributes($id_product, $id_attributes, $find_best = false)
    {
        return self::getIdProductAttributeByIdAttributes($id_product, $id_attributes, $find_best);
    }

    /**
     * Get the combination url anchor of the product.
     *
     * @param int $id_product_attribute
     *
     * @return string
     */
    public function getAnchor($id_product_attribute, $with_id = false)
    {
        $attributes = Product::getAttributesParams($this->id, $id_product_attribute);
        $anchor = '#';
        $sep = Configuration::get('PS_ATTRIBUTE_ANCHOR_SEPARATOR');
        foreach ($attributes as &$a) {
            foreach ($a as &$b) {
                $b = str_replace($sep, '_', Tools::link_rewrite($b));
            }
            $anchor .= '/' . ($with_id && isset($a['id_attribute']) && $a['id_attribute'] ? (int) $a['id_attribute'] . $sep : '') . $a['group'] . $sep . $a['name'];
        }

        return $anchor;
    }

    /**
     * Gets the name of a given product, in the given lang.
     *
     * @since 150
     *
     * @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(plname, \' : \', GROUP_CONCAT(DISTINCT agl.`name`, \' - \', alname SEPARATOR \', \')),plname) as name');
        } else {
            $query->select('DISTINCT plname 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', 'plid_product = paid_product AND plid_lang = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl'));
            $query->leftJoin('product_attribute_combination', 'pac', 'pacid_product_attribute = paid_product_attribute');
            $query->leftJoin('attribute', 'atr', 'atrid_attribute = pacid_attribute');
            $query->leftJoin('attribute_lang', 'al', 'alid_attribute = atrid_attribute AND alid_lang = ' . (int) $id_lang);
            $query->leftJoin('attribute_group_lang', 'agl', 'aglid_attribute_group = atrid_attribute_group AND aglid_lang = ' . (int) $id_lang);
            $query->where('paid_product = ' . (int) $id_product . ' AND paid_product_attribute = ' . (int) $id_product_attribute);
        } else {
            // or just adds a 'where' clause for a simple product

            $query->from('product_lang', 'pl');
            $query->where('plid_product = ' . (int) $id_product);
            $query->where('plid_lang = ' . (int) $id_langShop::addSqlRestrictionOnLang('pl'));
        }

        return Db::getInstance()->getValue($query);
    }

    public function addWs($autodate = true, $null_values = false)
    {
        $success = $this->add($autodate, $null_values);
        if ($success && Configuration::get('PS_SEARCH_INDEXATION')) {
            Search::indexation(false, $this->id);
        }

        return $success;
    }

    public function updateWs($null_values = false)
    {
        if (null === $this->price) {
            $this->price = Product::getPriceStatic((int) $this->id, false, null, 6, null, false, true, 1, false, null, null, null, $this->specificPrice);
        }

        if (null === $this->unit_price) {
            $this->unit_price = ($this->unit_price_ratio != 0 ? $this->price / $this->unit_price_ratio : 0);
        }

        $success = parent::update($null_values);
        if ($success && Configuration::get('PS_SEARCH_INDEXATION')) {
            Search::indexation(false, $this->id);
        }
        Hook::exec('actionProductUpdate', ['id_product' => (int) $this->id]);

        return $success;
    }

    /**
     * For a given product, returns its real quantity.
     *
     * @since 150
     *
     * @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') && 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 150
     *
     * @param int $id_product
     *
     * @return bool
     */
    public static function usesAdvancedStockManagement($id_product)
    {
        $query = new DbQuery();
        $query->select('product_shopadvanced_stock_management');
        $query->from('product', 'p');
        $query->join(Shop::addSqlAssociation('product', 'p'));
        $query->where('pid_product = ' . (int) $id_product);

        return (bool) Db::getInstance()->getValue($query);
    }

    /**
     * This method allows to flush price cache.
     *
     * @since 150
     */
    public static function flushPriceCache()
    {
        self::$_prices = [];
        self::$_pricesLevel2 = [];
    }

    /**
     * Get list of parent categories.
     *
     * @since 150
     *
     * @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);
        'SELECT' = new DbQuery();
        'SELECT'->from('category', 'c');
        'SELECT'->leftJoin('category_lang', 'cl', 'cid_category = clid_category AND id_lang = ' . (int) $id_langShop::addSqlRestrictionOnLang('cl'));
        'SELECT'->where('cnleft <= ' . (int) $interval['nleft'] . ' AND cnright >= ' . (int) $interval['nright']);
        'SELECT'->orderBy('cnleft');

        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT');
    }

    /**
     * Fill the variables used for stock management.
     */
    public function loadStockData()
    {
        if (false === Validate::isLoadedObject($this)) {
            return;
        }

        // Default product quantity is available quantity to sell in 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);
        $this->location = StockAvailable::getLocation($this->id);

        if (Context::getContext()->shop->getContext() == Shop::CONTEXT_GROUP && Context::getContext()->shop->getContextShopGroup()->share_stock == 1) {
            $this->advanced_stock_management = $this->useAdvancedStockManagement();
        }
    }

    public function useAdvancedStockManagement()
    {
        return Db::getInstance()->getValue(
            '
                    SELECT `advanced_stock_management`
                    FROM ' . _DB_PREFIX_ . 'product_shop
                    WHERE id_product=' . (int) $this->idShop::addSqlRestriction()
                );
    }

    public function setAdvancedStockManagement($value)
    {
        $this->advanced_stock_management = (int) $value;
        if (Context::getContext()->shop->getContext() == Shop::CONTEXT_GROUP && Context::getContext()->shop->getContextShopGroup()->share_stock == 1) {
            Db::getInstance()->execute(
                '
                UPDATE `' . _DB_PREFIX_ . 'product_shop`
                SET `advanced_stock_management`=' . (int) $value . '
                WHERE id_product=' . (int) $this->idShop::addSqlRestriction()
            );
        } else {
            $this->setFieldsToUpdate(['advanced_stock_management' => true]);
            $this->save();
        }
    }

    /**
     * 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 ['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 PrestaShopCollection('ProductDownload');
        $collection_download->where('id_product', '=', $this->id);
        foreach ($collection_download as $product_download) {
            
            $result &= $product_download->delete($product_download->checkFile());
        }

        return $result;
    }

    /**
     * @deprecated 15010
     * @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 15010
     * @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 150
     *
     * @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 paid_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
        );
    }

    public static function getIdTaxRulesGroupMostUsed()
    {
        return Db::getInstance()->getValue(
            '
                    SELECT id_tax_rules_group
                    FROM (
                        SELECT COUNT(*) n, product_shopid_tax_rules_group
                        FROM ' . _DB_PREFIX_ . 'product p
                        ' . Shop::addSqlAssociation('product', 'p') . '
                        JOIN ' . _DB_PREFIX_ . 'tax_rules_group trg ON (product_shopid_tax_rules_group = trgid_tax_rules_group)
                        WHERE trgactive = 1 AND trgdeleted = 0
                        GROUP BY product_shopid_tax_rules_group
                        ORDER BY n DESC
                        LIMIT 1
                    ) most_used'
                );
    }

    /**
     * For a given ean13 reference, returns the corresponding id.
     *
     * @param string $ean13
     *
     * @return int id
     */
    public static function getIdByEan13($ean13)
    {
        if (empty($ean13)) {
            return 0;
        }

        if (!Validate::isEan13($ean13)) {
            return 0;
        }

        $query = new DbQuery();
        $query->select('pid_product');
        $query->from('product', 'p');
        $query->where('pean13 = \'' . pSQL($ean13) . '\'');

        return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query);
    }

    /**
     * For a given reference, returns the corresponding id.
     *
     * @param string $reference
     *
     * @return int id
     */
    public static function getIdByReference($reference)
    {
        if (empty($reference)) {
            return 0;
        }

        if (!Validate::isReference($reference)) {
            return 0;
        }

        $query = new DbQuery();
        $query->select('pid_product');
        $query->from('product', 'p');
        $query->where('preference = \'' . pSQL($reference) . '\'');

        return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query);
    }

    public function getWsType()
    {
        $type_information = [
            Product::PTYPE_SIMPLE => 'simple',
            Product::PTYPE_PACK => 'pack',
            Product::PTYPE_VIRTUAL => 'virtual',
        ];

        return $type_information[$this->getType()];
    }

    
    public function modifierWsLinkRewrite()
    {
        foreach ($this->name as $id_lang => $name) {
            if (empty($this->link_rewrite[$id_lang])) {
                $this->link_rewrite[$id_lang] = Tools::link_rewrite($name);
            } elseif (!Validate::isLinkRewrite($this->link_rewrite[$id_lang])) {
                $this->link_rewrite[$id_lang] = Tools::link_rewrite($this->link_rewrite[$id_lang]);
            }
        }

        return true;
    }

    public function getWsProductBundle()
    {
        return Db::getInstance()->executeS('SELECT id_product_item as id, id_product_attribute_item as id_product_attribute, quantity FROM ' . _DB_PREFIX_ . 'pack WHERE id_product_pack = ' . (int) $this->id);
    }

    public function setWsType($type_str)
    {
        $reverse_type_information = [
            'simple' => Product::PTYPE_SIMPLE,
            'pack' => Product::PTYPE_PACK,
            'virtual' => Product::PTYPE_VIRTUAL,
        ];

        if (!isset($reverse_type_information[$type_str])) {
            return false;
        }

        $type = $reverse_type_information[$type_str];

        if (Pack::isPack((int) $this->id) && $type != Product::PTYPE_PACK) {
            Pack::deleteItems($this->id);
        }

        $this->cache_is_pack = ($type == Product::PTYPE_PACK);
        $this->is_virtual = ($type == Product::PTYPE_VIRTUAL);

        return true;
    }

    public function setWsProductBundle($items)
    {
        if ($this->is_virtual) {
            return false;
        }

        Pack::deleteItems($this->id);

        foreach ($items as $item) {
            // Combination of a product is optional, and can be omittedif (!isset($item['product_attribute_id'])) {
                $item['product_attribute_id'] = 0;
            }
            if ((int) $item['id'] > 0) {
                Pack::addItem($this->id, (int) $item['id'], (int) $item['quantity'], (int) $item['product_attribute_id']);
            }
        }

        return true;
    }

    public function isColorUnavailable($id_attribute, $id_shop)
    {
        return Db::getInstance()->getValue(
            '
            SELECT said_product_attribute
            FROM ' . _DB_PREFIX_ . 'stock_available sa
            WHERE id_product=' . (int) $this->id . ' AND quantity <= 0
            ' . StockAvailable::addSqlShopRestriction(null, $id_shop, 'sa') . '
            AND EXISTS (
                SELECT 1
                FROM ' . _DB_PREFIX_ . 'product_attribute pa
                JOIN ' . _DB_PREFIX_ . 'product_attribute_shop product_attribute_shop
                    ON (product_attribute_shopid_product_attribute = paid_product_attribute AND product_attribute_shopid_shop=' . (int) $id_shop . ')
                JOIN ' . _DB_PREFIX_ . 'product_attribute_combination pac
                    ON (pacid_product_attribute AND product_attribute_shopid_product_attribute)
                WHERE said_product_attribute = paid_product_attribute AND paid_product=' . (int) $this->id . ' AND pacid_attribute=' . (int) $id_attribute . '
            )'
        );
    }

    public static function getColorsListCacheId($id_product, $full = true)
    {
        
        if ($id_product) {
            'productlist_colors' .= '|' . (int) $id_product;
        }

        if ($full) {
            'productlist_colors' .= '|' . (int) Context::getContext()->shop->id . '|' . (int) Context::getContext()->cookie->id_lang;
        }

        return 'productlist_colors';
    }

    public static function setPackStockType($id_product, $pack_stock_type)
    {
        return Db::getInstance()->execute('UPDATE ' . _DB_PREFIX_ . 'product p
        ' . Shop::addSqlAssociation('product', 'p') . ' SET product_shoppack_stock_type = ' . (int) $pack_stock_type . ' WHERE p.`id_product` = ' . (int) $id_product);
    }

    /**
     * Gets a list of IDs from a list of IDs/RefsThe result will avoid duplicates, and checks if given IDs/Refs exists in DB.
     * Useful when a product list should be checked before a bulk operation on them (Only 1 query => performances).
     *
     * @return array the IDs list, whithout duplicate and only existing ones
     */
    public static function getExistingIdsFromIdsOrRefs($ids_or_refs)
    {
        // separate IDs and Refs
        $ids = [];
        $refs = [];
        $whereStatements = [];
        foreach ((is_array($ids_or_refs) ? $ids_or_refs : [$ids_or_refs]) as $id_or_ref) {
            if (is_numeric($id_or_ref)) {
                $ids[] = (int) $id_or_ref;
            } elseif (is_string($id_or_ref)) {
                $refs[] = '\'' . pSQL($id_or_ref) . '\'';
            }
        }

        // construct WHERE statement with OR combination
        if (count($ids) > 0) {
            $whereStatements[] = ' pid_product IN (' . implode(',', $ids) . ') ';
        }
        if (count($refs) > 0) {
            $whereStatements[] = ' preference IN (' . implode(',', $refs) . ') ';
        }
        if (!count($whereStatements)) {
            return false;
        }

        $results = Db::getInstance()->executeS('
        SELECT DISTINCT `id_product`
        FROM `' . _DB_PREFIX_ . 'product` p
        WHERE ' . implode(' OR ', $whereStatements));

        // simplify array since there is 1 useless dimension.
        // FIXME : find a better way to avoid this, directly in SQL?
        foreach ($results as $k => $v) {
            $results[$k] = (int) $v['id_product'];
        }

        return $results;
    }

    /**
     * Get object of redirect_type.
     *
     * @return bool|string
     */
    public function getRedirectType()
    {
        switch ($this->redirect_type) {
            case ProductInterface::REDIRECT_TYPE_CATEGORY_MOVED_PERMANENTLY:
            case ProductInterface::REDIRECT_TYPE_CATEGORY_FOUND:
                return 'category';

                break;

            case ProductInterface::REDIRECT_TYPE_PRODUCT_MOVED_PERMANENTLY:
            case ProductInterface::REDIRECT_TYPE_PRODUCT_FOUND:
                return 'product';

                break;
        }

        return false;
    }

    /**
     * Return an array of customization fields IDs.
     *
     * @return array|false
     */
    public function getUsedCustomizationFieldsIds()
    {
        return Db::getInstance()->executeS(
            'SELECT cd.`index` FROM `' . _DB_PREFIX_ . 'customized_data` cd
            LEFT JOIN `' . _DB_PREFIX_ . 'customization_field` cf ON cf.`id_customization_field` = cd.`index`
            WHERE cf.`id_product` = ' . (int) $this->id
        );
    }

    /**
     * Remove unused customization for the product.
     *
     * @param array $customizationIds - Array of customization fields IDs
     *
     * @return bool
     *
     * @throws PrestaShopDatabaseException
     */
    public function deleteUnusedCustomizationFields($customizationIds)
    {
        $return = true;
        if (is_array($customizationIds) && !empty($customizationIds)) {
            $toDeleteIds = implode(',', $customizationIds);
            $return &= Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'customization_field` WHERE
            `id_product` = ' . (int) $this->id . ' AND `id_customization_field` IN (' . $toDeleteIds . ')');

            $return &= Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'customization_field_lang` WHERE
            `id_customization_field` IN (' . $toDeleteIds . ')');
        }

        if (!$return) {
            throw new PrestaShopDatabaseException('An error occurred while deletion the customization fields');
        }

        return $return;
    }

    /**
     * Update the customization fields to be deleted if not used.
     *
     * @param array $customizationIds - Array of excluded customization fields IDs
     *
     * @return bool
     *
     * @throws PrestaShopDatabaseException
     */
    public function softDeleteCustomizationFields($customizationIds)
    {
        $return = true;
        $updateQuery = 'UPDATE `' . _DB_PREFIX_ . 'customization_field` cf
            SET cf.`is_deleted` = 1
            WHERE
            cf.`id_product` = ' . (int) $this->id . '
            AND cf.`is_deleted` = 0 ';

        if (is_array($customizationIds) && !empty($customizationIds)) {
            $updateQuery .= 'AND cf.`id_customization_field` NOT IN (' . implode(',', array_map('intval', $customizationIds)) . ')';
        }

        $return &= Db::getInstance()->execute($updateQuery);

        if (!$return) {
            throw new PrestaShopDatabaseException('An error occurred while soft deletion the customization fields');
        }

        return $return;
    }

    /**
     * Update default supplier data
     *
     * @param int $idSupplier
     * @param float $wholesalePrice
     * @param string $supplierReference
     *
     * @return bool
     */
    public function updateDefaultSupplierData(int $idSupplier, string $supplierReference, float $wholesalePrice): bool
    {
        if (!$this->id) {
            return false;
        }

        'SELECT' = 'UPDATE `' . _DB_PREFIX_ . 'product` SET id_supplier = %d, supplier_reference = "%s", wholesale_price = "%s" WHERE id_product = %d';

        return Db::getInstance()->execute(
            sprintf(
                'SELECT',
                $idSupplier,
                pSQL($supplierReference),
                $wholesalePrice,
                $this->id
            )
        );
    }
}



© 2023 Quttera Ltd. All rights reserved.