1<?php
2/**
3 * Copyright since 2007 PrestaShop SA and Contributors
4 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
5 *
6 * NOTICE OF LICENSE
7 *
8 * This source file is subject to the Open Software License (OSL 3.0)
9 * that is bundled with this package in the file LICENSE.md.
10 * It is also available through the world-wide-web at this URL:
11 * https://opensource.org/licenses/OSL-3.0
12 * If you did not receive a copy of the license and are unable to
13 * obtain it through the world-wide-web, please send an email
14 * to license@prestashop.com so we can send you a copy immediately.
15 *
16 * DISCLAIMER
17 *
18 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
19 * versions in the future. If you wish to customize PrestaShop for your
20 * needs please refer to https://devdocs.prestashop.com/ for more information.
21 *
22 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
23 * @copyright Since 2007 PrestaShop SA and Contributors
24 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
25 */
26
27/**
28 * StockManager : implementation of StockManagerInterface.
29 *
30 * @since 1.5.0
31 */
32class StockManagerCore implements StockManagerInterface
33{
34    /**
35     * @see StockManagerInterface::isAvailable()
36     */
37    public static function isAvailable()
38    {
39        // Default Manager : always available
40        return true;
41    }
42
43    /**
44     * @see StockManagerInterface::addProduct()
45     *
46     * @param int $id_product
47     * @param int $id_product_attribute
48     * @param Warehouse $warehouse
49     * @param int $quantity
50     * @param int $id_stock_mvt_reason
51     * @param float $price_te
52     * @param bool $is_usable
53     * @param int|null $id_supply_order
54     * @param Employee|null $employee
55     *
56     * @return bool
57     *
58     * @throws PrestaShopException
59     */
60    public function addProduct(
61        $id_product,
62        $id_product_attribute,
63        Warehouse $warehouse,
64        $quantity,
65        $id_stock_mvt_reason,
66        $price_te,
67        $is_usable = true,
68        $id_supply_order = null,
69        $employee = null
70    ) {
71        if (!Validate::isLoadedObject($warehouse) || !$quantity || !$id_product) {
72            return false;
73        }
74
75        $price_te = round((float) $price_te, 6);
76        if ($price_te <= 0.0) {
77            return false;
78        }
79
80        if (!StockMvtReason::exists($id_stock_mvt_reason)) {
81            $id_stock_mvt_reason = Configuration::get('PS_STOCK_MVT_INC_REASON_DEFAULT');
82        }
83
84        $context = Context::getContext();
85
86        $mvt_params = [
87            'id_stock' => null,
88            'physical_quantity' => $quantity,
89            'id_stock_mvt_reason' => $id_stock_mvt_reason,
90            'id_supply_order' => $id_supply_order,
91            'price_te' => $price_te,
92            'last_wa' => null,
93            'current_wa' => null,
94            'id_employee' => (int) $context->employee->id ? (int) $context->employee->id : $employee->id,
95            'employee_firstname' => $context->employee->firstname ? $context->employee->firstname : $employee->firstname,
96            'employee_lastname' => $context->employee->lastname ? $context->employee->lastname : $employee->lastname,
97            'sign' => 1,
98        ];
99
100        $stock_exists = false;
101
102        // switch on MANAGEMENT_TYPE
103        switch ($warehouse->management_type) {
104            // case CUMP mode
105            case 'WA':
106                $stock_collection = $this->getStockCollection($id_product, $id_product_attribute, $warehouse->id);
107
108                // if this product is already in stock
109                if (count($stock_collection) > 0) {
110                    $stock_exists = true;
111
112                    /** @var Stock $stock */
113                    // for a warehouse using WA, there is one and only one stock for a given product
114                    $stock = $stock_collection->current();
115
116                    // calculates WA price
117                    $last_wa = $stock->price_te;
118                    $current_wa = $this->calculateWA($stock, $quantity, $price_te);
119
120                    $mvt_params['id_stock'] = $stock->id;
121                    $mvt_params['last_wa'] = $last_wa;
122                    $mvt_params['current_wa'] = $current_wa;
123
124                    $stock_params = [
125                        'physical_quantity' => ($stock->physical_quantity + $quantity),
126                        'price_te' => $current_wa,
127                        'usable_quantity' => ($is_usable ? ($stock->usable_quantity + $quantity) : $stock->usable_quantity),
128                        'id_warehouse' => $warehouse->id,
129                    ];
130
131                    // saves stock in warehouse
132                    $stock->hydrate($stock_params);
133                    $stock->update();
134                } else {
135                    // else, the product is not in sock
136
137                    $mvt_params['last_wa'] = 0;
138                    $mvt_params['current_wa'] = $price_te;
139                }
140
141                break;
142
143            // case FIFO / LIFO mode
144            case 'FIFO':
145            case 'LIFO':
146                $stock_collection = $this->getStockCollection($id_product, $id_product_attribute, $warehouse->id, $price_te);
147
148                // if this product is already in stock
149                if (count($stock_collection) > 0) {
150                    $stock_exists = true;
151
152                    /** @var Stock $stock */
153                    // there is one and only one stock for a given product in a warehouse and at the current unit price
154                    $stock = $stock_collection->current();
155
156                    $stock_params = [
157                        'physical_quantity' => ($stock->physical_quantity + $quantity),
158                        'usable_quantity' => ($is_usable ? ($stock->usable_quantity + $quantity) : $stock->usable_quantity),
159                    ];
160
161                    // updates stock in warehouse
162                    $stock->hydrate($stock_params);
163                    $stock->update();
164
165                    // sets mvt_params
166                    $mvt_params['id_stock'] = $stock->id;
167                }
168
169                break;
170
171            default:
172                return false;
173
174                break;
175        }
176
177        if (!$stock_exists) {
178            $stock = new Stock();
179
180            $stock_params = [
181                'id_product_attribute' => $id_product_attribute,
182                'id_product' => $id_product,
183                'physical_quantity' => $quantity,
184                'price_te' => $price_te,
185                'usable_quantity' => ($is_usable ? $quantity : 0),
186                'id_warehouse' => $warehouse->id,
187            ];
188
189            // saves stock in warehouse
190            $stock->hydrate($stock_params);
191            $stock->add();
192            $mvt_params['id_stock'] = $stock->id;
193        }
194
195        // saves stock mvt
196        $stock_mvt = new StockMvt();
197        $stock_mvt->hydrate($mvt_params);
198        $stock_mvt->add();
199
200        return true;
201    }
202
203    /**
204     * @see StockManagerInterface::removeProduct()
205     *
206     * @param int $id_product
207     * @param int|null $id_product_attribute
208     * @param Warehouse $warehouse
209     * @param int $quantity
210     * @param int $id_stock_mvt_reason
211     * @param bool $is_usable
212     * @param int|null $id_order
213     * @param int $ignore_pack
214     * @param Employee|null $employee
215     *
216     * @return array
217     *
218     * @throws PrestaShopException
219     */
220    public function removeProduct(
221        $id_product,
222        $id_product_attribute,
223        Warehouse $warehouse,
224        $quantity,
225        $id_stock_mvt_reason,
226        $is_usable = true,
227        $id_order = null,
228        $ignore_pack = 0,
229        $employee = null
230    ) {
231        $return = [];
232
233        if (!Validate::isLoadedObject($warehouse) || !$quantity || !$id_product) {
234            return $return;
235        }
236
237        if (!StockMvtReason::exists($id_stock_mvt_reason)) {
238            $id_stock_mvt_reason = Configuration::get('PS_STOCK_MVT_DEC_REASON_DEFAULT');
239        }
240
241        $context = Context::getContext();
242
243        // Special case of a pack
244        if (Pack::isPack((int) $id_product) && !$ignore_pack) {
245            if (Validate::isLoadedObject($product = new Product((int) $id_product))) {
246                // Gets items
247                if ($product->pack_stock_type == Pack::STOCK_TYPE_PRODUCTS_ONLY
248                    || $product->pack_stock_type == Pack::STOCK_TYPE_PACK_BOTH
249                    || ($product->pack_stock_type == Pack::STOCK_TYPE_DEFAULT
250                        && Configuration::get('PS_PACK_STOCK_TYPE') > 0)
251                ) {
252                    $products_pack = Pack::getItems((int) $id_product, (int) Configuration::get('PS_LANG_DEFAULT'));
253                    // Foreach item
254                    foreach ($products_pack as $product_pack) {
255                        if ($product_pack->advanced_stock_management == 1) {
256                            $product_warehouses = Warehouse::getProductWarehouseList($product_pack->id, $product_pack->id_pack_product_attribute);
257                            $warehouse_stock_found = false;
258                            foreach ($product_warehouses as $product_warehouse) {
259                                if (!$warehouse_stock_found) {
260                                    if (Warehouse::exists($product_warehouse['id_warehouse'])) {
261                                        $current_warehouse = new Warehouse($product_warehouse['id_warehouse']);
262                                        $return[] = $this->removeProduct($product_pack->id, $product_pack->id_pack_product_attribute, $current_warehouse, $product_pack->pack_quantity * $quantity, $id_stock_mvt_reason, $is_usable, $id_order, $ignore_pack, $employee);
263
264                                        // The product was found on this warehouse. Stop the stock searching.
265                                        $warehouse_stock_found = !empty($return[count($return) - 1]);
266                                    }
267                                }
268                            }
269                        }
270                    }
271                }
272
273                if ($product->pack_stock_type == Pack::STOCK_TYPE_PACK_ONLY
274                    || $product->pack_stock_type == Pack::STOCK_TYPE_PACK_BOTH
275                    || (
276                        $product->pack_stock_type == Pack::STOCK_TYPE_DEFAULT
277                        && (Configuration::get('PS_PACK_STOCK_TYPE') == Pack::STOCK_TYPE_PACK_ONLY
278                            || Configuration::get('PS_PACK_STOCK_TYPE') == Pack::STOCK_TYPE_PACK_BOTH)
279                    )
280                ) {
281                    $return = array_merge($return, $this->removeProduct($id_product, $id_product_attribute, $warehouse, $quantity, $id_stock_mvt_reason, $is_usable, $id_order, 1, $employee));
282                }
283            } else {
284                return false;
285            }
286        } else {
287            // gets total quantities in stock for the current product
288            $physical_quantity_in_stock = (int) $this->getProductPhysicalQuantities($id_product, $id_product_attribute, [$warehouse->id], false);
289            $usable_quantity_in_stock = (int) $this->getProductPhysicalQuantities($id_product, $id_product_attribute, [$warehouse->id], true);
290
291            // check quantity if we want to decrement unusable quantity
292            if (!$is_usable) {
293                $quantity_in_stock = $physical_quantity_in_stock - $usable_quantity_in_stock;
294            } else {
295                $quantity_in_stock = $usable_quantity_in_stock;
296            }
297
298            // checks if it's possible to remove the given quantity
299            if ($quantity_in_stock < $quantity) {
300                return $return;
301            }
302
303            $stock_collection = $this->getStockCollection($id_product, $id_product_attribute, $warehouse->id);
304            $stock_collection->getAll();
305
306            // check if the collection is loaded
307            if (count($stock_collection) <= 0) {
308                return $return;
309            }
310
311            $stock_history_qty_available = [];
312            $mvt_params = [];
313            $stock_params = [];
314            $quantity_to_decrement_by_stock = [];
315            $global_quantity_to_decrement = $quantity;
316
317            // switch on MANAGEMENT_TYPE
318            switch ($warehouse->management_type) {
319                // case CUMP mode
320                case 'WA':
321                    /** @var Stock $stock */
322                    // There is one and only one stock for a given product in a warehouse in this mode
323                    $stock = $stock_collection->current();
324
325                    $mvt_params = [
326                        'id_stock' => $stock->id,
327                        'physical_quantity' => $quantity,
328                        'id_stock_mvt_reason' => $id_stock_mvt_reason,
329                        'id_order' => $id_order,
330                        'price_te' => $stock->price_te,
331                        'last_wa' => $stock->price_te,
332                        'current_wa' => $stock->price_te,
333                        'id_employee' => (int) $context->employee->id ? (int) $context->employee->id : $employee->id,
334                        'employee_firstname' => $context->employee->firstname ? $context->employee->firstname : $employee->firstname,
335                        'employee_lastname' => $context->employee->lastname ? $context->employee->lastname : $employee->lastname,
336                        'sign' => -1,
337                    ];
338                    $stock_params = [
339                        'physical_quantity' => ($stock->physical_quantity - $quantity),
340                        'usable_quantity' => ($is_usable ? ($stock->usable_quantity - $quantity) : $stock->usable_quantity),
341                    ];
342
343                    // saves stock in warehouse
344                    $stock->hydrate($stock_params);
345                    $stock->update();
346
347                    // saves stock mvt
348                    $stock_mvt = new StockMvt();
349                    $stock_mvt->hydrate($mvt_params);
350                    $stock_mvt->save();
351
352                    $return[$stock->id]['quantity'] = $quantity;
353                    $return[$stock->id]['price_te'] = $stock->price_te;
354
355                    break;
356
357                case 'LIFO':
358                case 'FIFO':
359                    // for each stock, parse its mvts history to calculate the quantities left for each positive mvt,
360                    // according to the instant available quantities for this stock
361                    foreach ($stock_collection as $stock) {
362                        /** @var Stock $stock */
363                        $left_quantity_to_check = $stock->physical_quantity;
364                        if ($left_quantity_to_check <= 0) {
365                            continue;
366                        }
367
368                        $resource = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
369                            '
370							SELECT sm.`id_stock_mvt`, sm.`date_add`, sm.`physical_quantity`,
371								IF ((sm2.`physical_quantity` is null), sm.`physical_quantity`, (sm.`physical_quantity` - SUM(sm2.`physical_quantity`))) as qty
372							FROM `' . _DB_PREFIX_ . 'stock_mvt` sm
373							LEFT JOIN `' . _DB_PREFIX_ . 'stock_mvt` sm2 ON sm2.`referer` = sm.`id_stock_mvt`
374							WHERE sm.`sign` = 1
375							AND sm.`id_stock` = ' . (int) $stock->id . '
376							GROUP BY sm.`id_stock_mvt`
377							ORDER BY sm.`date_add` DESC',
378                            false
379                        );
380
381                        while ($row = Db::getInstance()->nextRow($resource)) {
382                            // continue - in FIFO mode, we have to retreive the oldest positive mvts for which there are left quantities
383                            if ($warehouse->management_type == 'FIFO') {
384                                if ($row['qty'] == 0) {
385                                    continue;
386                                }
387                            }
388
389                            // converts date to timestamp
390                            $date = new DateTime($row['date_add']);
391                            $timestamp = $date->format('U');
392
393                            // history of the mvt
394                            $stock_history_qty_available[$timestamp] = [
395                                'id_stock' => $stock->id,
396                                'id_stock_mvt' => (int) $row['id_stock_mvt'],
397                                'qty' => (int) $row['qty'],
398                            ];
399
400                            // break - in LIFO mode, checks only the necessary history to handle the global quantity for the current stock
401                            if ($warehouse->management_type == 'LIFO') {
402                                $left_quantity_to_check -= (int) $row['qty'];
403                                if ($left_quantity_to_check <= 0) {
404                                    break;
405                                }
406                            }
407                        }
408                    }
409
410                    if ($warehouse->management_type == 'LIFO') {
411                        // orders stock history by timestamp to get newest history first
412                        krsort($stock_history_qty_available);
413                    } else {
414                        // orders stock history by timestamp to get oldest history first
415                        ksort($stock_history_qty_available);
416                    }
417
418                    // checks each stock to manage the real quantity to decrement for each of them
419                    foreach ($stock_history_qty_available as $entry) {
420                        if ($entry['qty'] >= $global_quantity_to_decrement) {
421                            $quantity_to_decrement_by_stock[$entry['id_stock']][$entry['id_stock_mvt']] = $global_quantity_to_decrement;
422                            $global_quantity_to_decrement = 0;
423                        } else {
424                            $quantity_to_decrement_by_stock[$entry['id_stock']][$entry['id_stock_mvt']] = $entry['qty'];
425                            $global_quantity_to_decrement -= $entry['qty'];
426                        }
427
428                        if ($global_quantity_to_decrement <= 0) {
429                            break;
430                        }
431                    }
432
433                    // for each stock, decrements it and logs the mvts
434                    foreach ($stock_collection as $stock) {
435                        if (array_key_exists($stock->id, $quantity_to_decrement_by_stock) && is_array($quantity_to_decrement_by_stock[$stock->id])) {
436                            $total_quantity_for_current_stock = 0;
437
438                            foreach ($quantity_to_decrement_by_stock[$stock->id] as $id_mvt_referrer => $qte) {
439                                $mvt_params = [
440                                    'id_stock' => $stock->id,
441                                    'physical_quantity' => $qte,
442                                    'id_stock_mvt_reason' => $id_stock_mvt_reason,
443                                    'id_order' => $id_order,
444                                    'price_te' => $stock->price_te,
445                                    'sign' => -1,
446                                    'referer' => $id_mvt_referrer,
447                                    'id_employee' => (int) $context->employee->id ? (int) $context->employee->id : $employee->id,
448                                ];
449
450                                // saves stock mvt
451                                $stock_mvt = new StockMvt();
452                                $stock_mvt->hydrate($mvt_params);
453                                $stock_mvt->save();
454
455                                $total_quantity_for_current_stock += $qte;
456                            }
457
458                            $stock_params = [
459                                'physical_quantity' => ($stock->physical_quantity - $total_quantity_for_current_stock),
460                                'usable_quantity' => ($is_usable ? ($stock->usable_quantity - $total_quantity_for_current_stock) : $stock->usable_quantity),
461                            ];
462
463                            $return[$stock->id]['quantity'] = $total_quantity_for_current_stock;
464                            $return[$stock->id]['price_te'] = $stock->price_te;
465
466                            // saves stock in warehouse
467                            $stock->hydrate($stock_params);
468                            $stock->update();
469                        }
470                    }
471
472                    break;
473            }
474
475            if (Pack::isPacked($id_product, $id_product_attribute)) {
476                $packs = Pack::getPacksContainingItem($id_product, $id_product_attribute, (int) Configuration::get('PS_LANG_DEFAULT'));
477                foreach ($packs as $pack) {
478                    // Decrease stocks of the pack only if pack is in linked stock mode (option called 'Decrement both')
479                    if (!((int) $pack->pack_stock_type == Pack::STOCK_TYPE_PACK_BOTH)
480                        && !((int) $pack->pack_stock_type == Pack::STOCK_TYPE_DEFAULT
481                            && (int) Configuration::get('PS_PACK_STOCK_TYPE') == Pack::STOCK_TYPE_PACK_BOTH)
482                    ) {
483                        continue;
484                    }
485
486                    // Decrease stocks of the pack only if there is not enough items to constituate the actual pack stocks.
487
488                    // How many packs can be constituated with the remaining product stocks
489                    $quantity_by_pack = $pack->pack_item_quantity;
490                    $stock_available_quantity = $quantity_in_stock - $quantity;
491                    $max_pack_quantity = max([0, floor($stock_available_quantity / $quantity_by_pack)]);
492                    $quantity_delta = Pack::getQuantity($pack->id) - $max_pack_quantity;
493
494                    if ($pack->advanced_stock_management == 1 && $quantity_delta > 0) {
495                        $product_warehouses = Warehouse::getPackWarehouses($pack->id);
496                        $warehouse_stock_found = false;
497                        foreach ($product_warehouses as $product_warehouse) {
498                            if (!$warehouse_stock_found) {
499                                if (Warehouse::exists($product_warehouse)) {
500                                    $current_warehouse = new Warehouse($product_warehouse);
501                                    $return[] = $this->removeProduct($pack->id, null, $current_warehouse, $quantity_delta, $id_stock_mvt_reason, $is_usable, $id_order, 1);
502                                    // The product was found on this warehouse. Stop the stock searching.
503                                    $warehouse_stock_found = !empty($return[count($return) - 1]);
504                                }
505                            }
506                        }
507                    }
508                }
509            }
510        }
511
512        // if we remove a usable quantity, exec hook
513        if ($is_usable) {
514            Hook::exec(
515                'actionProductCoverage',
516                    [
517                        'id_product' => $id_product,
518                        'id_product_attribute' => $id_product_attribute,
519                        'warehouse' => $warehouse,
520                    ]
521            );
522        }
523
524        return $return;
525    }
526
527    /**
528     * @see StockManagerInterface::getProductPhysicalQuantities()
529     */
530    public function getProductPhysicalQuantities($id_product, $id_product_attribute, $ids_warehouse = null, $usable = false)
531    {
532        if (null !== $ids_warehouse) {
533            // in case $ids_warehouse is not an array
534            if (!is_array($ids_warehouse)) {
535                $ids_warehouse = [$ids_warehouse];
536            }
537
538            // casts for security reason
539            $ids_warehouse = array_map('intval', $ids_warehouse);
540            if (!count($ids_warehouse)) {
541                return 0;
542            }
543        } else {
544            $ids_warehouse = [];
545        }
546
547        $query = new DbQuery();
548        $query->select('SUM(' . ($usable ? 's.usable_quantity' : 's.physical_quantity') . ')');
549        $query->from('stock', 's');
550        $query->where('s.id_product = ' . (int) $id_product);
551        if (0 != $id_product_attribute) {
552            $query->where('s.id_product_attribute = ' . (int) $id_product_attribute);
553        }
554
555        if (count($ids_warehouse)) {
556            $query->where('s.id_warehouse IN(' . implode(', ', $ids_warehouse) . ')');
557        }
558
559        return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query);
560    }
561
562    /**
563     * @see StockManagerInterface::getProductRealQuantities()
564     */
565    public function getProductRealQuantities($id_product, $id_product_attribute, $ids_warehouse = null, $usable = false)
566    {
567        if (null !== $ids_warehouse) {
568            // in case $ids_warehouse is not an array
569            if (!is_array($ids_warehouse)) {
570                $ids_warehouse = [$ids_warehouse];
571            }
572
573            // casts for security reason
574            $ids_warehouse = array_map('intval', $ids_warehouse);
575        }
576
577        $client_orders_qty = 0;
578
579        // check if product is present in a pack
580        if (!Pack::isPack($id_product) && $in_pack = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
581            'SELECT id_product_pack, quantity FROM ' . _DB_PREFIX_ . 'pack
582			WHERE id_product_item = ' . (int) $id_product . '
583			AND id_product_attribute_item = ' . ($id_product_attribute ? (int) $id_product_attribute : '0')
584        )) {
585            foreach ($in_pack as $value) {
586                if (Validate::isLoadedObject($product = new Product((int) $value['id_product_pack'])) &&
587                    ($product->pack_stock_type == Pack::STOCK_TYPE_PRODUCTS_ONLY || $product->pack_stock_type == Pack::STOCK_TYPE_PACK_BOTH || ($product->pack_stock_type == Pack::STOCK_TYPE_DEFAULT && Configuration::get('PS_PACK_STOCK_TYPE') > 0))) {
588                    $query = new DbQuery();
589                    $query->select('od.product_quantity, od.product_quantity_refunded, pk.quantity');
590                    $query->from('order_detail', 'od');
591                    $query->leftjoin('orders', 'o', 'o.id_order = od.id_order');
592                    $query->where('od.product_id = ' . (int) $value['id_product_pack']);
593                    $query->leftJoin('order_history', 'oh', 'oh.id_order = o.id_order AND oh.id_order_state = o.current_state');
594                    $query->leftJoin('order_state', 'os', 'os.id_order_state = oh.id_order_state');
595                    $query->leftJoin('pack', 'pk', 'pk.id_product_item = ' . (int) $id_product . ' AND pk.id_product_attribute_item = ' . ($id_product_attribute ? (int) $id_product_attribute : '0') . ' AND id_product_pack = od.product_id');
596                    $query->where('os.shipped != 1');
597                    $query->where('o.valid = 1 OR (os.id_order_state != ' . (int) Configuration::get('PS_OS_ERROR') . '
598								   AND os.id_order_state != ' . (int) Configuration::get('PS_OS_CANCELED') . ')');
599                    $query->groupBy('od.id_order_detail');
600                    if (count($ids_warehouse)) {
601                        $query->where('od.id_warehouse IN(' . implode(', ', $ids_warehouse) . ')');
602                    }
603                    $res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query);
604                    if (count($res)) {
605                        foreach ($res as $row) {
606                            $client_orders_qty += ($row['product_quantity'] - $row['product_quantity_refunded']) * $row['quantity'];
607                        }
608                    }
609                }
610            }
611        }
612
613        // skip if product is a pack without
614        if (!Pack::isPack($id_product) || (Pack::isPack($id_product) && Validate::isLoadedObject($product = new Product((int) $id_product))
615            && $product->pack_stock_type == Pack::STOCK_TYPE_PACK_ONLY || $product->pack_stock_type == Pack::STOCK_TYPE_PACK_BOTH ||
616                    ($product->pack_stock_type == Pack::STOCK_TYPE_DEFAULT && (Configuration::get('PS_PACK_STOCK_TYPE') == Pack::STOCK_TYPE_PACK_ONLY || Configuration::get('PS_PACK_STOCK_TYPE') == Pack::STOCK_TYPE_PACK_BOTH)))) {
617            // Gets client_orders_qty
618            $query = new DbQuery();
619            $query->select('od.product_quantity, od.product_quantity_refunded');
620            $query->from('order_detail', 'od');
621            $query->leftjoin('orders', 'o', 'o.id_order = od.id_order');
622            $query->where('od.product_id = ' . (int) $id_product);
623            if (0 != $id_product_attribute) {
624                $query->where('od.product_attribute_id = ' . (int) $id_product_attribute);
625            }
626            $query->leftJoin('order_history', 'oh', 'oh.id_order = o.id_order AND oh.id_order_state = o.current_state');
627            $query->leftJoin('order_state', 'os', 'os.id_order_state = oh.id_order_state');
628            $query->where('os.shipped != 1');
629            $query->where('o.valid = 1 OR (os.id_order_state != ' . (int) Configuration::get('PS_OS_ERROR') . '
630						   AND os.id_order_state != ' . (int) Configuration::get('PS_OS_CANCELED') . ')');
631            $query->groupBy('od.id_order_detail');
632            if (count($ids_warehouse)) {
633                $query->where('od.id_warehouse IN(' . implode(', ', $ids_warehouse) . ')');
634            }
635            $res = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query);
636            if (count($res)) {
637                foreach ($res as $row) {
638                    $client_orders_qty += ($row['product_quantity'] - $row['product_quantity_refunded']);
639                }
640            }
641        }
642        // Gets supply_orders_qty
643        $query = new DbQuery();
644
645        $query->select('sod.quantity_expected, sod.quantity_received');
646        $query->from('supply_order', 'so');
647        $query->leftjoin('supply_order_detail', 'sod', 'sod.id_supply_order = so.id_supply_order');
648        $query->leftjoin('supply_order_state', 'sos', 'sos.id_supply_order_state = so.id_supply_order_state');
649        $query->where('sos.pending_receipt = 1');
650        $query->where('sod.id_product = ' . (int) $id_product . ' AND sod.id_product_attribute = ' . (int) $id_product_attribute);
651        if (null !== $ids_warehouse && count($ids_warehouse)) {
652            $query->where('so.id_warehouse IN(' . implode(', ', $ids_warehouse) . ')');
653        }
654
655        $supply_orders_qties = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($query);
656
657        $supply_orders_qty = 0;
658        foreach ($supply_orders_qties as $qty) {
659            if ($qty['quantity_expected'] > $qty['quantity_received']) {
660                $supply_orders_qty += ($qty['quantity_expected'] - $qty['quantity_received']);
661            }
662        }
663
664        // Gets {physical OR usable}_qty
665        $qty = $this->getProductPhysicalQuantities($id_product, $id_product_attribute, $ids_warehouse, $usable);
666
667        //real qty = actual qty in stock - current client orders + current supply orders
668        return $qty - $client_orders_qty + $supply_orders_qty;
669    }
670
671    /**
672     * @see StockManagerInterface::transferBetweenWarehouses()
673     */
674    public function transferBetweenWarehouses(
675        $id_product,
676        $id_product_attribute,
677        $quantity,
678        $id_warehouse_from,
679        $id_warehouse_to,
680        $usable_from = true,
681        $usable_to = true
682    ) {
683        // Checks if this transfer is possible
684        if ($this->getProductPhysicalQuantities($id_product, $id_product_attribute, [$id_warehouse_from], $usable_from) < $quantity) {
685            return false;
686        }
687
688        if ($id_warehouse_from == $id_warehouse_to && $usable_from == $usable_to) {
689            return false;
690        }
691
692        // Checks if the given warehouses are available
693        $warehouse_from = new Warehouse($id_warehouse_from);
694        $warehouse_to = new Warehouse($id_warehouse_to);
695        if (!Validate::isLoadedObject($warehouse_from) ||
696            !Validate::isLoadedObject($warehouse_to)) {
697            return false;
698        }
699
700        // Removes from warehouse_from
701        $stocks = $this->removeProduct(
702            $id_product,
703                                       $id_product_attribute,
704                                       $warehouse_from,
705                                       $quantity,
706                                       Configuration::get('PS_STOCK_MVT_TRANSFER_FROM'),
707                                       $usable_from
708        );
709        if (!count($stocks)) {
710            return false;
711        }
712
713        // Adds in warehouse_to
714        foreach ($stocks as $stock) {
715            $price = $stock['price_te'];
716
717            // convert product price to destination warehouse currency if needed
718            if ($warehouse_from->id_currency != $warehouse_to->id_currency) {
719                // First convert price to the default currency
720                $price_converted_to_default_currency = Tools::convertPrice($price, $warehouse_from->id_currency, false);
721
722                // Convert the new price from default currency to needed currency
723                $price = Tools::convertPrice($price_converted_to_default_currency, $warehouse_to->id_currency, true);
724            }
725
726            if (!$this->addProduct(
727                $id_product,
728                                   $id_product_attribute,
729                                   $warehouse_to,
730                                   $stock['quantity'],
731                                   Configuration::get('PS_STOCK_MVT_TRANSFER_TO'),
732                                   $price,
733                                   $usable_to
734            )) {
735                return false;
736            }
737        }
738
739        return true;
740    }
741
742    /**
743     * @see StockManagerInterface::getProductCoverage()
744     * Here, $coverage is a number of days
745     *
746     * @return int number of days left (-1 if infinite)
747     */
748    public function getProductCoverage($id_product, $id_product_attribute, $coverage, $id_warehouse = null)
749    {
750        if (!$id_product_attribute) {
751            $id_product_attribute = 0;
752        }
753
754        if ($coverage == 0 || !$coverage) {
755            $coverage = 7;
756        } // Week by default
757
758        // gets all stock_mvt for the given coverage period
759        $query = '
760			SELECT SUM(view.quantity) as quantity_out
761			FROM
762			(	SELECT sm.`physical_quantity` as quantity
763				FROM `' . _DB_PREFIX_ . 'stock_mvt` sm
764				LEFT JOIN `' . _DB_PREFIX_ . 'stock` s ON (sm.`id_stock` = s.`id_stock`)
765				LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON (p.`id_product` = s.`id_product`)
766				' . Shop::addSqlAssociation('product', 'p') . '
767				LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON (p.`id_product` = pa.`id_product`)
768				' . Shop::addSqlAssociation('product_attribute', 'pa', false) . '
769				WHERE sm.`sign` = -1
770				AND sm.`id_stock_mvt_reason` != ' . Configuration::get('PS_STOCK_MVT_TRANSFER_FROM') . '
771				AND TO_DAYS("' . date('Y-m-d') . ' 00:00:00") - TO_DAYS(sm.`date_add`) <= ' . (int) $coverage . '
772				AND s.`id_product` = ' . (int) $id_product . '
773				AND s.`id_product_attribute` = ' . (int) $id_product_attribute .
774                ($id_warehouse ? ' AND s.`id_warehouse` = ' . (int) $id_warehouse : '') . '
775				GROUP BY sm.`id_stock_mvt`
776			) as view';
777
778        $quantity_out = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query);
779        if (!$quantity_out) {
780            return -1;
781        }
782
783        $quantity_per_day = Tools::ps_round($quantity_out / $coverage);
784        $physical_quantity = $this->getProductPhysicalQuantities(
785            $id_product,
786                                                                 $id_product_attribute,
787                                                                 ($id_warehouse ? [$id_warehouse] : null),
788                                                                 true
789        );
790        $time_left = ($quantity_per_day == 0) ? (-1) : Tools::ps_round($physical_quantity / $quantity_per_day);
791
792        return $time_left;
793    }
794
795    /**
796     * For a given stock, calculates its new WA(Weighted Average) price based on the new quantities and price
797     * Formula : (physicalStock * lastCump + quantityToAdd * unitPrice) / (physicalStock + quantityToAdd).
798     *
799     * @param Stock|PrestaShopCollection $stock
800     * @param int $quantity
801     * @param float $price_te
802     *
803     * @return int WA
804     */
805    protected function calculateWA(Stock $stock, $quantity, $price_te)
806    {
807        return (float) Tools::ps_round(((($stock->physical_quantity * $stock->price_te) + ($quantity * $price_te)) / ($stock->physical_quantity + $quantity)), 6);
808    }
809
810    /**
811     * For a given product, retrieves the stock collection.
812     *
813     * @param int $id_product
814     * @param int $id_product_attribute
815     * @param int $id_warehouse Optional
816     * @param int $price_te Optional
817     *
818     * @return PrestaShopCollection Collection of Stock
819     */
820    protected function getStockCollection($id_product, $id_product_attribute, $id_warehouse = null, $price_te = null)
821    {
822        $stocks = new PrestaShopCollection('Stock');
823        $stocks->where('id_product', '=', $id_product);
824        $stocks->where('id_product_attribute', '=', $id_product_attribute);
825        if ($id_warehouse) {
826            $stocks->where('id_warehouse', '=', $id_warehouse);
827        }
828        if ($price_te) {
829            $stocks->where('price_te', '=', $price_te);
830        }
831
832        return $stocks;
833    }
834
835    /**
836     * For a given product, retrieves the stock in function of the delivery option.
837     *
838     * @param int $id_product
839     * @param int $id_product_attribute optional
840     * @param array $delivery_option
841     *
842     * @return int quantity
843     */
844    public static function getStockByCarrier($id_product = 0, $id_product_attribute = 0, $delivery_option = null)
845    {
846        if (!(int) $id_product || !is_array($delivery_option) || !is_int($id_product_attribute)) {
847            return false;
848        }
849
850        $results = Warehouse::getWarehousesByProductId($id_product, $id_product_attribute);
851        $stock_quantity = 0;
852
853        foreach ($results as $result) {
854            if (isset($result['id_warehouse']) && (int) $result['id_warehouse']) {
855                $ws = new Warehouse((int) $result['id_warehouse']);
856                $carriers = $ws->getWsCarriers();
857
858                if (is_array($carriers) && !empty($carriers)) {
859                    $stock_quantity += Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('SELECT SUM(s.`usable_quantity`) as quantity
860						FROM ' . _DB_PREFIX_ . 'stock s
861						LEFT JOIN ' . _DB_PREFIX_ . 'warehouse_carrier wc ON wc.`id_warehouse` = s.`id_warehouse`
862						LEFT JOIN ' . _DB_PREFIX_ . 'carrier c ON wc.`id_carrier` = c.`id_reference`
863						WHERE s.`id_product` = ' . (int) $id_product . ' AND s.`id_product_attribute` = ' . (int) $id_product_attribute . ' AND s.`id_warehouse` = ' . $result['id_warehouse'] . ' AND c.`id_carrier` IN (' . rtrim($delivery_option[(int) Context::getContext()->cart->id_address_delivery], ',') . ') GROUP BY s.`id_product`');
864                } else {
865                    $stock_quantity += Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue('SELECT SUM(s.`usable_quantity`) as quantity
866						FROM ' . _DB_PREFIX_ . 'stock s
867						WHERE s.`id_product` = ' . (int) $id_product . ' AND s.`id_product_attribute` = ' . (int) $id_product_attribute . ' AND s.`id_warehouse` = ' . $result['id_warehouse'] . ' GROUP BY s.`id_product`');
868                }
869            }
870        }
871
872        return $stock_quantity;
873    }
874}
875