1<?php
2/**
3 * 2007-2016 PrestaShop
4 *
5 * thirty bees is an extension to the PrestaShop e-commerce software developed by PrestaShop SA
6 * Copyright (C) 2017-2018 thirty bees
7 *
8 * NOTICE OF LICENSE
9 *
10 * This source file is subject to the Open Software License (OSL 3.0)
11 * that is bundled with this package in the file LICENSE.txt.
12 * It is also available through the world-wide-web at this URL:
13 * http://opensource.org/licenses/osl-3.0.php
14 * If you did not receive a copy of the license and are unable to
15 * obtain it through the world-wide-web, please send an email
16 * to license@thirtybees.com so we can send you a copy immediately.
17 *
18 * DISCLAIMER
19 *
20 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
21 * versions in the future. If you wish to customize PrestaShop for your
22 * needs please refer to https://www.thirtybees.com for more information.
23 *
24 * @author    thirty bees <contact@thirtybees.com>
25 * @author    PrestaShop SA <contact@prestashop.com>
26 * @copyright 2017-2018 thirty bees
27 * @copyright 2007-2016 PrestaShop SA
28 * @license   http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
29 *  PrestaShop is an internationally registered trademark & property of PrestaShop SA
30 */
31
32/**
33 * Class PackCore
34 */
35class PackCore extends Product
36{
37    protected static $cachePackItems = [];
38    protected static $cacheIsPack = [];
39    protected static $cacheIsPacked = [];
40
41    /**
42     * @param int $idProduct
43     *
44     * @return float|int
45     *
46     * @throws PrestaShopDatabaseException
47     * @throws PrestaShopException
48     * @since 1.0.0
49     */
50    public static function noPackPrice($idProduct)
51    {
52        $sum = 0;
53        $priceDisplayMethod = !static::$_taxCalculationMethod;
54        $items = Pack::getItems($idProduct, Configuration::get('PS_LANG_DEFAULT'));
55        foreach ($items as $item) {
56            /** @var Product $item */
57            $sum += $item->getPrice($priceDisplayMethod, ($item->id_pack_product_attribute ? $item->id_pack_product_attribute : null)) * $item->pack_quantity;
58        }
59
60        return $sum;
61    }
62
63    /**
64     * @param int $idProduct
65     * @param int $idLang
66     *
67     * @return array|mixed
68     * @throws PrestaShopDatabaseException
69     * @throws PrestaShopException
70     */
71    public static function getItems($idProduct, $idLang)
72    {
73        if (!Pack::isFeatureActive()) {
74            return [];
75        }
76
77        if (array_key_exists($idProduct, static::$cachePackItems)) {
78            return static::$cachePackItems[$idProduct];
79        }
80        $result = Db::getInstance()->executeS('SELECT id_product_item, id_product_attribute_item, quantity FROM `'._DB_PREFIX_.'pack` where id_product_pack = '.(int) $idProduct);
81        $arrayResult = [];
82        foreach ($result as $row) {
83            $p = new Product($row['id_product_item'], false, $idLang);
84            $p->loadStockData();
85            $p->pack_quantity = $row['quantity'];
86            $p->id_pack_product_attribute = (isset($row['id_product_attribute_item']) && $row['id_product_attribute_item'] ? $row['id_product_attribute_item'] : 0);
87            if (isset($row['id_product_attribute_item']) && $row['id_product_attribute_item']) {
88                $sql = 'SELECT agl.`name` AS group_name, al.`name` AS attribute_name
89					FROM `'._DB_PREFIX_.'product_attribute` pa
90					'.Shop::addSqlAssociation('product_attribute', 'pa').'
91					LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac ON pac.`id_product_attribute` = pa.`id_product_attribute`
92					LEFT JOIN `'._DB_PREFIX_.'attribute` a ON a.`id_attribute` = pac.`id_attribute`
93					LEFT JOIN `'._DB_PREFIX_.'attribute_group` ag ON ag.`id_attribute_group` = a.`id_attribute_group`
94					LEFT JOIN `'._DB_PREFIX_.'attribute_lang` al ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = '.(int) Context::getContext()->language->id.')
95					LEFT JOIN `'._DB_PREFIX_.'attribute_group_lang` agl ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = '.(int) Context::getContext()->language->id.')
96					WHERE pa.`id_product_attribute` = '.$row['id_product_attribute_item'].'
97					GROUP BY pa.`id_product_attribute`, ag.`id_attribute_group`
98					ORDER BY pa.`id_product_attribute`';
99
100                $combinations = Db::getInstance()->executeS($sql);
101                foreach ($combinations as $k => $combination) {
102                    $p->name .= ' '.$combination['group_name'].'-'.$combination['attribute_name'];
103                }
104            }
105            $arrayResult[] = $p;
106        }
107        static::$cachePackItems[$idProduct] = $arrayResult;
108
109        return static::$cachePackItems[$idProduct];
110    }
111
112    /**
113     * This method is allow to know if a feature is used or active
114     *
115     * @since 1.5.0.1
116     * @return bool
117     * @throws PrestaShopException
118     */
119    public static function isFeatureActive()
120    {
121        return Configuration::get('PS_PACK_FEATURE_ACTIVE');
122    }
123
124    /**
125     * @param int $idProduct
126     *
127     * @return int
128     * @throws PrestaShopDatabaseException
129     * @throws PrestaShopException
130     */
131    public static function noPackWholesalePrice($idProduct)
132    {
133        $sum = 0;
134        $items = Pack::getItems($idProduct, Configuration::get('PS_LANG_DEFAULT'));
135        foreach ($items as $item) {
136            $sum += $item->wholesale_price * $item->pack_quantity;
137        }
138
139        return $sum;
140    }
141
142    /**
143     * @param int $idProduct
144     *
145     * @return bool
146     * @throws PrestaShopDatabaseException
147     * @throws PrestaShopException
148     */
149    public static function isInStock($idProduct)
150    {
151        if (!Pack::isFeatureActive()) {
152            return true;
153        }
154
155        $items = Pack::getItems((int) $idProduct, Configuration::get('PS_LANG_DEFAULT'));
156
157        foreach ($items as $item) {
158            /** @var Product $item */
159            // Updated for 1.5.0
160            if (Product::getQuantity($item->id) < $item->pack_quantity && !$item->isAvailableWhenOutOfStock((int) $item->out_of_stock)) {
161                return false;
162            }
163        }
164
165        return true;
166    }
167
168    /**
169     * @param int  $idProduct
170     * @param int  $idLang
171     * @param bool $full
172     *
173     * @return array|false|null|PDOStatement
174     * @throws PrestaShopDatabaseException
175     * @throws PrestaShopException
176     */
177    public static function getItemTable($idProduct, $idLang, $full = false)
178    {
179        if (!Pack::isFeatureActive()) {
180            return [];
181        }
182
183        $context = Context::getContext();
184
185        $sql = 'SELECT p.*, product_shop.*, pl.*, image_shop.`id_image` id_image, il.`legend`, cl.`name` AS category_default, a.quantity AS pack_quantity, product_shop.`id_category_default`, a.id_product_pack, a.id_product_attribute_item
186				FROM `'._DB_PREFIX_.'pack` a
187				LEFT JOIN `'._DB_PREFIX_.'product` p ON p.id_product = a.id_product_item
188				LEFT JOIN `'._DB_PREFIX_.'product_lang` pl
189					ON p.id_product = pl.id_product
190					AND pl.`id_lang` = '.(int) $idLang.Shop::addSqlRestrictionOnLang('pl').'
191				LEFT JOIN `'._DB_PREFIX_.'image_shop` image_shop
192					ON (image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop='.(int) $context->shop->id.')
193				LEFT JOIN `'._DB_PREFIX_.'image_lang` il ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = '.(int) $idLang.')
194				'.Shop::addSqlAssociation('product', 'p').'
195				LEFT JOIN `'._DB_PREFIX_.'category_lang` cl
196					ON product_shop.`id_category_default` = cl.`id_category`
197					AND cl.`id_lang` = '.(int) $idLang.Shop::addSqlRestrictionOnLang('cl').'
198				WHERE product_shop.`id_shop` = '.(int) $context->shop->id.'
199				AND a.`id_product_pack` = '.(int) $idProduct.'
200				GROUP BY a.`id_product_item`, a.`id_product_attribute_item`';
201
202        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
203
204        foreach ($result as &$line) {
205            if (Combination::isFeatureActive() && isset($line['id_product_attribute_item']) && $line['id_product_attribute_item']) {
206                $line['cache_default_attribute'] = $line['id_product_attribute'] = $line['id_product_attribute_item'];
207
208                $sql = 'SELECT agl.`name` AS group_name, al.`name` AS attribute_name,  pai.`id_image` AS id_product_attribute_image
209				FROM `'._DB_PREFIX_.'product_attribute` pa
210				'.Shop::addSqlAssociation('product_attribute', 'pa').'
211				LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac ON pac.`id_product_attribute` = '.$line['id_product_attribute_item'].'
212				LEFT JOIN `'._DB_PREFIX_.'attribute` a ON a.`id_attribute` = pac.`id_attribute`
213				LEFT JOIN `'._DB_PREFIX_.'attribute_group` ag ON ag.`id_attribute_group` = a.`id_attribute_group`
214				LEFT JOIN `'._DB_PREFIX_.'attribute_lang` al ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = '.(int) Context::getContext()->language->id.')
215				LEFT JOIN `'._DB_PREFIX_.'attribute_group_lang` agl ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = '.(int) Context::getContext()->language->id.')
216				LEFT JOIN `'._DB_PREFIX_.'product_attribute_image` pai ON ('.$line['id_product_attribute_item'].' = pai.`id_product_attribute`)
217				WHERE pa.`id_product` = '.(int) $line['id_product'].' AND pa.`id_product_attribute` = '.$line['id_product_attribute_item'].'
218				GROUP BY pa.`id_product_attribute`, ag.`id_attribute_group`
219				ORDER BY pa.`id_product_attribute`';
220
221                $attrName = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
222
223                if (isset($attrName[0]['id_product_attribute_image']) && $attrName[0]['id_product_attribute_image']) {
224                    $line['id_image'] = $attrName[0]['id_product_attribute_image'];
225                }
226                $line['name'] .= "\n";
227                foreach ($attrName as $value) {
228                    $line['name'] .= ' '.$value['group_name'].'-'.$value['attribute_name'];
229                }
230            }
231            $line = Product::getTaxesInformations($line);
232        }
233
234        if (!$full) {
235            return $result;
236        }
237
238        $arrayResult = [];
239        foreach ($result as $prow) {
240            if (!Pack::isPack($prow['id_product'])) {
241                $prow['id_product_attribute'] = (int) $prow['id_product_attribute_item'];
242                $arrayResult[] = Product::getProductProperties($idLang, $prow);
243            }
244        }
245
246        return $arrayResult;
247    }
248
249    /**
250     * Is product a pack?
251     *
252     * @param int $idProduct
253     *
254     * @return bool
255     * @throws PrestaShopException
256     */
257    public static function isPack($idProduct)
258    {
259        if (!Pack::isFeatureActive()) {
260            return false;
261        }
262
263        if (!$idProduct) {
264            return false;
265        }
266
267        if (!array_key_exists($idProduct, static::$cacheIsPack)) {
268            $result = Db::getInstance()->getValue('SELECT COUNT(*) FROM `'._DB_PREFIX_.'pack` WHERE id_product_pack = '.(int) $idProduct);
269            static::$cacheIsPack[$idProduct] = ($result > 0);
270        }
271
272        return static::$cacheIsPack[$idProduct];
273    }
274
275    /**
276     * @param int  $idProduct
277     * @param int  $idLang
278     * @param bool $full
279     * @param null $limit
280     *
281     * @return array|false|null|PDOStatement
282     * @throws PrestaShopException
283     */
284    public static function getPacksTable($idProduct, $idLang, $full = false, $limit = null)
285    {
286        if (!Pack::isFeatureActive()) {
287            return [];
288        }
289
290        $packs = Db::getInstance()->getValue(
291            '
292		SELECT GROUP_CONCAT(a.`id_product_pack`)
293		FROM `'._DB_PREFIX_.'pack` a
294		WHERE a.`id_product_item` = '.(int) $idProduct
295        );
296
297        if (!(int) $packs) {
298            return [];
299        }
300
301        $context = Context::getContext();
302
303        $sql = '
304		SELECT p.*, product_shop.*, pl.*, image_shop.`id_image` id_image, il.`legend`, IFNULL(product_attribute_shop.id_product_attribute, 0) id_product_attribute
305		FROM `'._DB_PREFIX_.'product` p
306		NATURAL LEFT JOIN `'._DB_PREFIX_.'product_lang` pl
307		'.Shop::addSqlAssociation('product', 'p').'
308		LEFT JOIN `'._DB_PREFIX_.'product_attribute_shop` product_attribute_shop
309	   		ON (p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shop.id_shop='.(int) $context->shop->id.')
310		LEFT JOIN `'._DB_PREFIX_.'image_shop` image_shop
311			ON (image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop='.(int) $context->shop->id.')
312		LEFT JOIN `'._DB_PREFIX_.'image_lang` il ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = '.(int) $idLang.')
313		WHERE pl.`id_lang` = '.(int) $idLang.'
314			'.Shop::addSqlRestrictionOnLang('pl').'
315			AND p.`id_product` IN ('.$packs.')
316		GROUP BY p.id_product';
317        if ($limit) {
318            $sql .= ' LIMIT '.(int) $limit;
319        }
320        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
321        if (!$full) {
322            return $result;
323        }
324
325        $arrayResult = [];
326        foreach ($result as $row) {
327            if (!Pack::isPacked($row['id_product'])) {
328                $arrayResult[] = Product::getProductProperties($idLang, $row);
329            }
330        }
331
332        return $arrayResult;
333    }
334
335    /**
336     * Is product in a pack?
337     * If $id_product_attribute specified, then will restrict search on the given combination,
338     * else this method will match a product if at least one of all its combination is in a pack.
339     *
340     * @param int      $idProduct
341     * @param bool|int $idProductAttribute Optional combination of the product
342     *
343     * @return bool
344     * @throws PrestaShopException
345     * @throws PrestaShopException
346     */
347    public static function isPacked($idProduct, $idProductAttribute = false)
348    {
349
350        if (!Pack::isFeatureActive()) {
351            return false;
352        }
353        if ($idProductAttribute === false) {
354            $cacheKey = $idProduct.'-0';
355            if (!array_key_exists($cacheKey, static::$cacheIsPacked)) {
356                $result = Db::getInstance()->getValue('SELECT COUNT(*) FROM `'._DB_PREFIX_.'pack` WHERE id_product_item = '.(int) $idProduct);
357                static::$cacheIsPacked[$cacheKey] = ($result > 0);
358            }
359
360            return static::$cacheIsPacked[$cacheKey];
361        } else {
362            $cacheKey = $idProduct.'-'.$idProductAttribute;
363            if (!array_key_exists($cacheKey, static::$cacheIsPacked)) {
364                $result = Db::getInstance()->getValue(
365                    'SELECT COUNT(*) FROM `'._DB_PREFIX_.'pack` WHERE id_product_item = '.((int) $idProduct).' AND
366					id_product_attribute_item = '.((int) $idProductAttribute)
367                );
368                static::$cacheIsPacked[$cacheKey] = ($result > 0);
369            }
370
371            return static::$cacheIsPacked[$cacheKey];
372        }
373    }
374
375    /**
376     * @param int $idProduct
377     *
378     * @return bool
379     *
380     * @since 1.0.0
381     * @throws PrestaShopException
382     */
383    public static function deleteItems($idProduct)
384    {
385        return Db::getInstance()->update('product', ['cache_is_pack' => 0], 'id_product = '.(int) $idProduct) &&
386            Db::getInstance()->execute('DELETE FROM `'._DB_PREFIX_.'pack` WHERE `id_product_pack` = '.(int) $idProduct) &&
387            Configuration::updateGlobalValue('PS_PACK_FEATURE_ACTIVE', Pack::isCurrentlyUsed());
388    }
389
390    /**
391     * This method is allow to know if a Pack entity is currently used
392     *
393     * @since 1.5.0
394     *
395     * @param string $table
396     * @param bool   $hasActiveColumn
397     *
398     * @return bool
399     * @throws PrestaShopException
400     */
401    public static function isCurrentlyUsed($table = null, $hasActiveColumn = false)
402    {
403        // We dont't use the parent method because the identifier isn't id_pack
404        return (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
405            '
406			SELECT `id_product_pack`
407			FROM `'._DB_PREFIX_.'pack`
408		'
409        );
410    }
411
412    /**
413     * Add an item to the pack
414     *
415     * @param int $idProduct
416     * @param int $idItem
417     * @param int $qty
418     * @param int $idAttributeItem
419     *
420     * @return bool true if everything was fine
421     * @throws PrestaShopDatabaseException
422     * @throws PrestaShopException
423     * @throws PrestaShopException
424     */
425    public static function addItem($idProduct, $idItem, $qty, $idAttributeItem = 0)
426    {
427        $idAttributeItem = (int) $idAttributeItem ? (int) $idAttributeItem : Product::getDefaultAttribute((int) $idItem);
428
429        return Db::getInstance()->update('product', ['cache_is_pack' => 1], 'id_product = '.(int) $idProduct) &&
430            Db::getInstance()->insert(
431                'pack',
432                [
433                    'id_product_pack'           => (int) $idProduct,
434                    'id_product_item'           => (int) $idItem,
435                    'id_product_attribute_item' => (int) $idAttributeItem,
436                    'quantity'                  => (int) $qty,
437                ]
438            )
439            && Configuration::updateGlobalValue('PS_PACK_FEATURE_ACTIVE', '1');
440    }
441
442    /**
443     * @param int $idProductOld
444     * @param int $idProductNew
445     *
446     * @return bool
447     *
448     * @since 1.0.0
449     * @throws PrestaShopException
450     */
451    public static function duplicate($idProductOld, $idProductNew)
452    {
453        Db::getInstance()->execute(
454            'INSERT INTO `'._DB_PREFIX_.'pack` (`id_product_pack`, `id_product_item`, `id_product_attribute_item`, `quantity`)
455		(SELECT '.(int) $idProductNew.', `id_product_item`, `id_product_attribute_item`, `quantity` FROM `'._DB_PREFIX_.'pack` WHERE `id_product_pack` = '.(int) $idProductOld.')'
456        );
457
458        // If return query result, a non-pack product will return false
459        return true;
460    }
461
462    /**
463     * For a given pack, tells if it has at least one product using the advanced stock management
464     *
465     * @param int $idProduct id_pack
466     *
467     * @return bool
468     */
469    public static function usesAdvancedStockManagement($idProduct)
470    {
471        if (!Pack::isPack($idProduct)) {
472            return false;
473        }
474
475        $products = Pack::getItems($idProduct, Configuration::get('PS_LANG_DEFAULT'));
476        foreach ($products as $product) {
477            // if one product uses the advanced stock management
478            if ($product->advanced_stock_management == 1) {
479                return true;
480            }
481        }
482
483        // not used
484        return false;
485    }
486
487    /**
488     * For a given pack, tells if all products using the advanced stock management
489     *
490     * @param int $idProduct id_pack
491     *
492     * @return bool
493     */
494    public static function allUsesAdvancedStockManagement($idProduct)
495    {
496        if (!Pack::isPack($idProduct)) {
497            return false;
498        }
499
500        $products = Pack::getItems($idProduct, Configuration::get('PS_LANG_DEFAULT'));
501        foreach ($products as $product) {
502            // if one product uses the advanced stock management
503            if ($product->advanced_stock_management == 0) {
504                return false;
505            }
506        }
507
508        // not used
509        return true;
510    }
511
512    /**
513     * Returns Packs that conatins the given product in the right declinaison.
514     *
515     * @param integer $idItem          Product item id that could be contained in a|many pack(s)
516     * @param integer $idAttributeItem The declinaison of the product
517     * @param integer $idLang
518     *
519     * @return array[Product] Packs that contains the given product
520     */
521    public static function getPacksContainingItem($idItem, $idAttributeItem, $idLang)
522    {
523        if (!Pack::isFeatureActive() || !$idItem) {
524            return [];
525        }
526
527        $query = 'SELECT `id_product_pack`, `quantity` FROM `'._DB_PREFIX_.'pack`
528			WHERE `id_product_item` = '.((int) $idItem);
529        if (Combination::isFeatureActive()) {
530            $query .= ' AND `id_product_attribute_item` = '.((int) $idAttributeItem);
531        }
532        $result = Db::getInstance()->executeS($query);
533        $arrayResult = [];
534        foreach ($result as $row) {
535            $p = new Product($row['id_product_pack'], true, $idLang);
536            $p->loadStockData();
537            $p->pack_item_quantity = $row['quantity']; // Specific need from StockAvailable::updateQuantity()
538            $arrayResult[] = $p;
539        }
540
541        return $arrayResult;
542    }
543}
544