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