1<?php 2/* Copyright (C) 2003-2008 Rodolphe Quiedeville <rodolphe@quiedeville.org> 3 * Copyright (C) 2005-2012 Regis Houssin <regis.houssin@inodbox.com> 4 * Copyright (C) 2007 Franky Van Liedekerke <franky.van.liedekerke@telenet.be> 5 * Copyright (C) 2006-2012 Laurent Destailleur <eldy@users.sourceforge.net> 6 * Copyright (C) 2011-2020 Juanjo Menent <jmenent@2byte.es> 7 * Copyright (C) 2013 Florian Henry <florian.henry@open-concept.pro> 8 * Copyright (C) 2014 Cedric GROSS <c.gross@kreiz-it.fr> 9 * Copyright (C) 2014-2015 Marcos García <marcosgdf@gmail.com> 10 * Copyright (C) 2014-2017 Francis Appels <francis.appels@yahoo.com> 11 * Copyright (C) 2015 Claudio Aschieri <c.aschieri@19.coop> 12 * Copyright (C) 2016 Ferran Marcet <fmarcet@2byte.es> 13 * Copyright (C) 2018 Nicolas ZABOURI <info@inovea-conseil.com> 14 * Copyright (C) 2018-2020 Frédéric France <frederic.france@netlogic.fr> 15 * Copyright (C) 2020 Lenin Rivas <lenin@leninrivas.com> 16 * 17 * This program is free software; you can redistribute it and/or modify 18 * it under the terms of the GNU General Public License as published by 19 * the Free Software Foundation; either version 3 of the License, or 20 * (at your option) any later version. 21 * 22 * This program is distributed in the hope that it will be useful, 23 * but WITHOUT ANY WARRANTY; without even the implied warranty of 24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 * GNU General Public License for more details. 26 * 27 * You should have received a copy of the GNU General Public License 28 * along with this program. If not, see <https://www.gnu.org/licenses/>. 29 */ 30 31/** 32 * \file htdocs/expedition/class/expedition.class.php 33 * \ingroup expedition 34 * \brief Fichier de la classe de gestion des expeditions 35 */ 36 37require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php'; 38require_once DOL_DOCUMENT_ROOT."/core/class/commonobjectline.class.php"; 39require_once DOL_DOCUMENT_ROOT.'/core/class/commonincoterm.class.php'; 40if (!empty($conf->propal->enabled)) { 41 require_once DOL_DOCUMENT_ROOT.'/comm/propal/class/propal.class.php'; 42} 43if (!empty($conf->commande->enabled)) { 44 require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php'; 45} 46if (!empty($conf->productbatch->enabled)) { 47 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expeditionbatch.class.php'; 48} 49 50 51/** 52 * Class to manage shipments 53 */ 54class Expedition extends CommonObject 55{ 56 use CommonIncoterm; 57 58 /** 59 * @var string ID to identify managed object 60 */ 61 public $element = "shipping"; 62 63 /** 64 * @var string Field with ID of parent key if this field has a parent 65 */ 66 public $fk_element = "fk_expedition"; 67 68 /** 69 * @var string Name of table without prefix where object is stored 70 */ 71 public $table_element = "expedition"; 72 73 /** 74 * @var string Name of subtable line 75 */ 76 public $table_element_line = "expeditiondet"; 77 78 /** 79 * 0=No test on entity, 1=Test with field entity, 2=Test with link by societe 80 * @var int 81 */ 82 public $ismultientitymanaged = 1; 83 84 /** 85 * @var string String with name of icon for myobject. Must be the part after the 'object_' into object_myobject.png 86 */ 87 public $picto = 'dolly'; 88 89 public $socid; 90 91 /** 92 * @var string Customer ref 93 * @deprecated 94 * @see $ref_customer 95 */ 96 public $ref_client; 97 98 /** 99 * @var string Customer ref 100 */ 101 public $ref_customer; 102 103 /** 104 * @var string internal ref 105 * @deprecated 106 */ 107 public $ref_int; 108 109 public $brouillon; 110 111 /** 112 * @var int warehouse id 113 */ 114 public $entrepot_id; 115 116 /** 117 * @var string Tracking number 118 */ 119 public $tracking_number; 120 121 /** 122 * @var string Tracking url 123 */ 124 public $tracking_url; 125 public $billed; 126 127 /** 128 * @var string name of pdf model 129 */ 130 public $model_pdf; 131 132 public $trueWeight; 133 public $weight_units; 134 public $trueWidth; 135 public $width_units; 136 public $trueHeight; 137 public $height_units; 138 public $trueDepth; 139 public $depth_units; 140 // A denormalized value 141 public $trueSize; 142 143 /** 144 * @var integer|string Date delivery planed 145 */ 146 public $date_delivery; 147 148 /** 149 * @deprecated 150 * @see $date_shipping 151 */ 152 public $date; 153 154 /** 155 * @deprecated 156 * @see $date_shipping 157 */ 158 public $date_expedition; 159 160 /** 161 * Effective delivery date 162 * @var integer|string 163 */ 164 public $date_shipping; 165 166 /** 167 * @var integer|string date_creation 168 */ 169 public $date_creation; 170 171 /** 172 * @var integer|string date_valid 173 */ 174 public $date_valid; 175 176 public $meths; 177 public $listmeths; // List of carriers 178 179 public $lines = array(); 180 181 182 /** 183 * Draft status 184 */ 185 const STATUS_DRAFT = 0; 186 187 /** 188 * Validated status 189 */ 190 const STATUS_VALIDATED = 1; 191 192 /** 193 * Closed status 194 */ 195 const STATUS_CLOSED = 2; 196 197 /** 198 * Canceled status 199 */ 200 const STATUS_CANCELED = -1; 201 202 203 /** 204 * Constructor 205 * 206 * @param DoliDB $db Database handler 207 */ 208 public function __construct($db) 209 { 210 global $conf; 211 212 $this->db = $db; 213 214 // List of long language codes for status 215 $this->statuts = array(); 216 $this->statuts[-1] = 'StatusSendingCanceled'; 217 $this->statuts[0] = 'StatusSendingDraft'; 218 $this->statuts[1] = 'StatusSendingValidated'; 219 $this->statuts[2] = 'StatusSendingProcessed'; 220 221 // List of short language codes for status 222 $this->statutshorts = array(); 223 $this->statutshorts[-1] = 'StatusSendingCanceledShort'; 224 $this->statutshorts[0] = 'StatusSendingDraftShort'; 225 $this->statutshorts[1] = 'StatusSendingValidatedShort'; 226 $this->statutshorts[2] = 'StatusSendingProcessedShort'; 227 } 228 229 /** 230 * Return next contract ref 231 * 232 * @param Societe $soc Thirdparty object 233 * @return string Free reference for contract 234 */ 235 public function getNextNumRef($soc) 236 { 237 global $langs, $conf; 238 $langs->load("sendings"); 239 240 if (!empty($conf->global->EXPEDITION_ADDON_NUMBER)) { 241 $mybool = false; 242 243 $file = $conf->global->EXPEDITION_ADDON_NUMBER.".php"; 244 $classname = $conf->global->EXPEDITION_ADDON_NUMBER; 245 246 // Include file with class 247 $dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']); 248 249 foreach ($dirmodels as $reldir) { 250 $dir = dol_buildpath($reldir."core/modules/expedition/"); 251 252 // Load file with numbering class (if found) 253 $mybool |= @include_once $dir.$file; 254 } 255 256 if (!$mybool) { 257 dol_print_error('', "Failed to include file ".$file); 258 return ''; 259 } 260 261 $obj = new $classname(); 262 $numref = ""; 263 $numref = $obj->getNextValue($soc, $this); 264 265 if ($numref != "") { 266 return $numref; 267 } else { 268 dol_print_error($this->db, get_class($this)."::getNextNumRef ".$obj->error); 269 return ""; 270 } 271 } else { 272 print $langs->trans("Error")." ".$langs->trans("Error_EXPEDITION_ADDON_NUMBER_NotDefined"); 273 return ""; 274 } 275 } 276 277 /** 278 * Create expedition en base 279 * 280 * @param User $user Objet du user qui cree 281 * @param int $notrigger 1=Does not execute triggers, 0= execute triggers 282 * @return int <0 si erreur, id expedition creee si ok 283 */ 284 public function create($user, $notrigger = 0) 285 { 286 global $conf, $hookmanager; 287 288 $now = dol_now(); 289 290 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php'; 291 $error = 0; 292 293 // Clean parameters 294 $this->brouillon = 1; 295 $this->tracking_number = dol_sanitizeFileName($this->tracking_number); 296 if (empty($this->fk_project)) { 297 $this->fk_project = 0; 298 } 299 300 $this->user = $user; 301 302 303 $this->db->begin(); 304 305 $sql = "INSERT INTO ".MAIN_DB_PREFIX."expedition ("; 306 307 $sql .= "ref"; 308 $sql .= ", entity"; 309 $sql .= ", ref_customer"; 310 $sql .= ", ref_int"; 311 $sql .= ", ref_ext"; 312 $sql .= ", date_creation"; 313 $sql .= ", fk_user_author"; 314 $sql .= ", date_expedition"; 315 $sql .= ", date_delivery"; 316 $sql .= ", fk_soc"; 317 $sql .= ", fk_projet"; 318 $sql .= ", fk_address"; 319 $sql .= ", fk_shipping_method"; 320 $sql .= ", tracking_number"; 321 $sql .= ", weight"; 322 $sql .= ", size"; 323 $sql .= ", width"; 324 $sql .= ", height"; 325 $sql .= ", weight_units"; 326 $sql .= ", size_units"; 327 $sql .= ", note_private"; 328 $sql .= ", note_public"; 329 $sql .= ", model_pdf"; 330 $sql .= ", fk_incoterms, location_incoterms"; 331 $sql .= ") VALUES ("; 332 $sql .= "'(PROV)'"; 333 $sql .= ", ".$conf->entity; 334 $sql .= ", ".($this->ref_customer ? "'".$this->db->escape($this->ref_customer)."'" : "null"); 335 $sql .= ", ".($this->ref_int ? "'".$this->db->escape($this->ref_int)."'" : "null"); 336 $sql .= ", ".($this->ref_ext ? "'".$this->db->escape($this->ref_ext)."'" : "null"); 337 $sql .= ", '".$this->db->idate($now)."'"; 338 $sql .= ", ".$user->id; 339 $sql .= ", ".($this->date_expedition > 0 ? "'".$this->db->idate($this->date_expedition)."'" : "null"); 340 $sql .= ", ".($this->date_delivery > 0 ? "'".$this->db->idate($this->date_delivery)."'" : "null"); 341 $sql .= ", ".$this->socid; 342 $sql .= ", ".$this->fk_project; 343 $sql .= ", ".($this->fk_delivery_address > 0 ? $this->fk_delivery_address : "null"); 344 $sql .= ", ".($this->shipping_method_id > 0 ? $this->shipping_method_id : "null"); 345 $sql .= ", '".$this->db->escape($this->tracking_number)."'"; 346 $sql .= ", ".(is_numeric($this->weight) ? $this->weight : 'NULL'); 347 $sql .= ", ".(is_numeric($this->sizeS) ? $this->sizeS : 'NULL'); // TODO Should use this->trueDepth 348 $sql .= ", ".(is_numeric($this->sizeW) ? $this->sizeW : 'NULL'); // TODO Should use this->trueWidth 349 $sql .= ", ".(is_numeric($this->sizeH) ? $this->sizeH : 'NULL'); // TODO Should use this->trueHeight 350 $sql .= ", ".($this->weight_units != '' ? (int) $this->weight_units : 'NULL'); 351 $sql .= ", ".($this->size_units != '' ? (int) $this->size_units : 'NULL'); 352 $sql .= ", ".(!empty($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : "null"); 353 $sql .= ", ".(!empty($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : "null"); 354 $sql .= ", ".(!empty($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null"); 355 $sql .= ", ".(int) $this->fk_incoterms; 356 $sql .= ", '".$this->db->escape($this->location_incoterms)."'"; 357 $sql .= ")"; 358 359 dol_syslog(get_class($this)."::create", LOG_DEBUG); 360 $resql = $this->db->query($sql); 361 if ($resql) { 362 $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX."expedition"); 363 364 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition"; 365 $sql .= " SET ref = '(PROV".$this->id.")'"; 366 $sql .= " WHERE rowid = ".$this->id; 367 368 dol_syslog(get_class($this)."::create", LOG_DEBUG); 369 if ($this->db->query($sql)) { 370 // Insert of lines 371 $num = count($this->lines); 372 for ($i = 0; $i < $num; $i++) { 373 if (!isset($this->lines[$i]->detail_batch)) { // no batch management 374 if (!$this->create_line($this->lines[$i]->entrepot_id, $this->lines[$i]->origin_line_id, $this->lines[$i]->qty, $this->lines[$i]->rang, $this->lines[$i]->array_options) > 0) { 375 $error++; 376 } 377 } else { // with batch management 378 if (!$this->create_line_batch($this->lines[$i], $this->lines[$i]->array_options) > 0) { 379 $error++; 380 } 381 } 382 } 383 384 if (!$error && $this->id && $this->origin_id) { 385 $ret = $this->add_object_linked(); 386 if (!$ret) { 387 $error++; 388 } 389 } 390 391 // Actions on extra fields 392 if (!$error) { 393 $result = $this->insertExtraFields(); 394 if ($result < 0) { 395 $error++; 396 } 397 } 398 399 if (!$error && !$notrigger) { 400 // Call trigger 401 $result = $this->call_trigger('SHIPPING_CREATE', $user); 402 if ($result < 0) { 403 $error++; 404 } 405 // End call triggers 406 407 if (!$error) { 408 $this->db->commit(); 409 return $this->id; 410 } else { 411 foreach ($this->errors as $errmsg) { 412 dol_syslog(get_class($this)."::create ".$errmsg, LOG_ERR); 413 $this->error .= ($this->error ? ', '.$errmsg : $errmsg); 414 } 415 $this->db->rollback(); 416 return -1 * $error; 417 } 418 } else { 419 $error++; 420 $this->error = $this->db->lasterror()." - sql=$sql"; 421 $this->db->rollback(); 422 return -3; 423 } 424 } else { 425 $error++; 426 $this->error = $this->db->lasterror()." - sql=$sql"; 427 $this->db->rollback(); 428 return -2; 429 } 430 } else { 431 $error++; 432 $this->error = $this->db->error()." - sql=$sql"; 433 $this->db->rollback(); 434 return -1; 435 } 436 } 437 438 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 439 /** 440 * Create a expedition line 441 * 442 * @param int $entrepot_id Id of warehouse 443 * @param int $origin_line_id Id of source line 444 * @param int $qty Quantity 445 * @param int $rang Rang 446 * @param array $array_options extrafields array 447 * @return int <0 if KO, line_id if OK 448 */ 449 public function create_line($entrepot_id, $origin_line_id, $qty, $rang = 0, $array_options = 0) 450 { 451 //phpcs:enable 452 global $user; 453 454 $expeditionline = new ExpeditionLigne($this->db); 455 $expeditionline->fk_expedition = $this->id; 456 $expeditionline->entrepot_id = $entrepot_id; 457 $expeditionline->fk_origin_line = $origin_line_id; 458 $expeditionline->qty = $qty; 459 $expeditionline->rang = $rang; 460 $expeditionline->array_options = $array_options; 461 462 if (($lineId = $expeditionline->insert($user)) < 0) { 463 $this->errors[] = $expeditionline->error; 464 } 465 return $lineId; 466 } 467 468 469 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 470 /** 471 * Create the detail of the expedition line. Create 1 record into expeditiondet for each warehouse and n record for each lot in this warehouse into expeditiondet_batch. 472 * 473 * @param object $line_ext Objet with full information of line. $line_ext->detail_batch must be an array of ExpeditionLineBatch 474 * @param array $array_options extrafields array 475 * @return int <0 if KO, >0 if OK 476 */ 477 public function create_line_batch($line_ext, $array_options = 0) 478 { 479 // phpcs:enable 480 $error = 0; 481 $stockLocationQty = array(); // associated array with batch qty in stock location 482 483 $tab = $line_ext->detail_batch; 484 // create stockLocation Qty array 485 foreach ($tab as $detbatch) { 486 if ($detbatch->entrepot_id) { 487 $stockLocationQty[$detbatch->entrepot_id] += $detbatch->qty; 488 } 489 } 490 // create shipment lines 491 foreach ($stockLocationQty as $stockLocation => $qty) { 492 $line_id = $this->create_line($stockLocation, $line_ext->origin_line_id, $qty, $line_ext->rang, $array_options); 493 if ($line_id < 0) { 494 $error++; 495 } else { 496 // create shipment batch lines for stockLocation 497 foreach ($tab as $detbatch) { 498 if ($detbatch->entrepot_id == $stockLocation) { 499 if (!($detbatch->create($line_id) > 0)) { // Create an ExpeditionLineBatch 500 $error++; 501 } 502 } 503 } 504 } 505 } 506 507 if (!$error) { 508 return 1; 509 } else { 510 return -1; 511 } 512 } 513 514 /** 515 * Get object and lines from database 516 * 517 * @param int $id Id of object to load 518 * @param string $ref Ref of object 519 * @param string $ref_ext External reference of object 520 * @param string $notused Internal reference of other object 521 * @return int >0 if OK, 0 if not found, <0 if KO 522 */ 523 public function fetch($id, $ref = '', $ref_ext = '', $notused = '') 524 { 525 global $conf; 526 527 // Check parameters 528 if (empty($id) && empty($ref) && empty($ref_ext)) { 529 return -1; 530 } 531 532 $sql = "SELECT e.rowid, e.entity, e.ref, e.fk_soc as socid, e.date_creation, e.ref_customer, e.ref_ext, e.ref_int, e.fk_user_author, e.fk_statut, e.fk_projet as fk_project, e.billed"; 533 $sql .= ", e.date_valid"; 534 $sql .= ", e.weight, e.weight_units, e.size, e.size_units, e.width, e.height"; 535 $sql .= ", e.date_expedition as date_expedition, e.model_pdf, e.fk_address, e.date_delivery"; 536 $sql .= ", e.fk_shipping_method, e.tracking_number"; 537 $sql .= ", e.note_private, e.note_public"; 538 $sql .= ', e.fk_incoterms, e.location_incoterms'; 539 $sql .= ', i.libelle as label_incoterms'; 540 $sql .= ', s.libelle as shipping_method'; 541 $sql .= ", el.fk_source as origin_id, el.sourcetype as origin"; 542 $sql .= " FROM ".MAIN_DB_PREFIX."expedition as e"; 543 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."element_element as el ON el.fk_target = e.rowid AND el.targettype = '".$this->db->escape($this->element)."'"; 544 $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_incoterms as i ON e.fk_incoterms = i.rowid'; 545 $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_shipment_mode as s ON e.fk_shipping_method = s.rowid'; 546 $sql .= " WHERE e.entity IN (".getEntity('expedition').")"; 547 if ($id) { 548 $sql .= " AND e.rowid = ".((int) $id); 549 } 550 if ($ref) { 551 $sql .= " AND e.ref='".$this->db->escape($ref)."'"; 552 } 553 if ($ref_ext) { 554 $sql .= " AND e.ref_ext='".$this->db->escape($ref_ext)."'"; 555 } 556 if ($notused) { 557 $sql .= " AND e.ref_int='".$this->db->escape($notused)."'"; 558 } 559 560 dol_syslog(get_class($this)."::fetch", LOG_DEBUG); 561 $result = $this->db->query($sql); 562 if ($result) { 563 if ($this->db->num_rows($result)) { 564 $obj = $this->db->fetch_object($result); 565 566 $this->id = $obj->rowid; 567 $this->entity = $obj->entity; 568 $this->ref = $obj->ref; 569 $this->socid = $obj->socid; 570 $this->ref_customer = $obj->ref_customer; 571 $this->ref_ext = $obj->ref_ext; 572 $this->ref_int = $obj->ref_int; 573 $this->statut = $obj->fk_statut; 574 $this->user_author_id = $obj->fk_user_author; 575 $this->date_creation = $this->db->jdate($obj->date_creation); 576 $this->date_valid = $this->db->jdate($obj->date_valid); 577 $this->date = $this->db->jdate($obj->date_expedition); // TODO deprecated 578 $this->date_expedition = $this->db->jdate($obj->date_expedition); // TODO deprecated 579 $this->date_shipping = $this->db->jdate($obj->date_expedition); // Date real 580 $this->date_delivery = $this->db->jdate($obj->date_delivery); // Date planed 581 $this->fk_delivery_address = $obj->fk_address; 582 $this->model_pdf = $obj->model_pdf; 583 $this->modelpdf = $obj->model_pdf; // deprecated 584 $this->shipping_method_id = $obj->fk_shipping_method; 585 $this->shipping_method = $obj->shipping_method; 586 $this->tracking_number = $obj->tracking_number; 587 $this->origin = ($obj->origin ? $obj->origin : 'commande'); // For compatibility 588 $this->origin_id = $obj->origin_id; 589 $this->billed = $obj->billed; 590 $this->fk_project = $obj->fk_project; 591 592 $this->trueWeight = $obj->weight; 593 $this->weight_units = $obj->weight_units; 594 595 $this->trueWidth = $obj->width; 596 $this->width_units = $obj->size_units; 597 $this->trueHeight = $obj->height; 598 $this->height_units = $obj->size_units; 599 $this->trueDepth = $obj->size; 600 $this->depth_units = $obj->size_units; 601 602 $this->note_public = $obj->note_public; 603 $this->note_private = $obj->note_private; 604 605 // A denormalized value 606 $this->trueSize = $obj->size."x".$obj->width."x".$obj->height; 607 $this->size_units = $obj->size_units; 608 609 //Incoterms 610 $this->fk_incoterms = $obj->fk_incoterms; 611 $this->location_incoterms = $obj->location_incoterms; 612 $this->label_incoterms = $obj->label_incoterms; 613 614 $this->db->free($result); 615 616 if ($this->statut == self::STATUS_DRAFT) { 617 $this->brouillon = 1; 618 } 619 620 // Tracking url 621 $this->getUrlTrackingStatus($obj->tracking_number); 622 623 // Thirdparty 624 $result = $this->fetch_thirdparty(); // TODO Remove this 625 626 // Retrieve extrafields 627 $this->fetch_optionals(); 628 629 // Fix Get multicurrency param for transmited 630 if (!empty($conf->multicurrency->enabled)) { 631 if (!empty($this->multicurrency_code)) { 632 $this->multicurrency_code = $this->thirdparty->multicurrency_code; 633 } 634 if (!empty($conf->global->MULTICURRENCY_USE_ORIGIN_TX) && !empty($this->thirdparty->multicurrency_tx)) { 635 $this->multicurrency_tx = $this->thirdparty->multicurrency_tx; 636 } 637 } 638 639 /* 640 * Lines 641 */ 642 $result = $this->fetch_lines(); 643 if ($result < 0) { 644 return -3; 645 } 646 647 return 1; 648 } else { 649 dol_syslog(get_class($this).'::Fetch no expedition found', LOG_ERR); 650 $this->error = 'Delivery with id '.$id.' not found'; 651 return 0; 652 } 653 } else { 654 $this->error = $this->db->error(); 655 return -1; 656 } 657 } 658 659 /** 660 * Validate object and update stock if option enabled 661 * 662 * @param User $user Object user that validate 663 * @param int $notrigger 1=Does not execute triggers, 0= execute triggers 664 * @return int <0 if OK, >0 if KO 665 */ 666 public function valid($user, $notrigger = 0) 667 { 668 global $conf, $langs; 669 670 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; 671 672 dol_syslog(get_class($this)."::valid"); 673 674 // Protection 675 if ($this->statut) { 676 dol_syslog(get_class($this)."::valid not in draft status", LOG_WARNING); 677 return 0; 678 } 679 680 if (!((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && !empty($user->rights->expedition->creer)) 681 || (!empty($conf->global->MAIN_USE_ADVANCED_PERMS) && !empty($user->rights->expedition->shipping_advance->validate)))) { 682 $this->error = 'Permission denied'; 683 dol_syslog(get_class($this)."::valid ".$this->error, LOG_ERR); 684 return -1; 685 } 686 687 $this->db->begin(); 688 689 $error = 0; 690 691 // Define new ref 692 $soc = new Societe($this->db); 693 $soc->fetch($this->socid); 694 695 // Class of company linked to order 696 $result = $soc->set_as_client(); 697 698 // Define new ref 699 if (!$error && (preg_match('/^[\(]?PROV/i', $this->ref) || empty($this->ref))) { // empty should not happened, but when it occurs, the test save life 700 $numref = $this->getNextNumRef($soc); 701 } else { 702 $numref = "EXP".$this->id; 703 } 704 $this->newref = dol_sanitizeFileName($numref); 705 706 $now = dol_now(); 707 708 // Validate 709 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET"; 710 $sql .= " ref='".$this->db->escape($numref)."'"; 711 $sql .= ", fk_statut = 1"; 712 $sql .= ", date_valid = '".$this->db->idate($now)."'"; 713 $sql .= ", fk_user_valid = ".$user->id; 714 $sql .= " WHERE rowid = ".$this->id; 715 716 dol_syslog(get_class($this)."::valid update expedition", LOG_DEBUG); 717 $resql = $this->db->query($sql); 718 if (!$resql) { 719 $this->error = $this->db->lasterror(); 720 $error++; 721 } 722 723 // If stock increment is done on sending (recommanded choice) 724 if (!$error && !empty($conf->stock->enabled) && !empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT)) { 725 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php'; 726 727 $langs->load("agenda"); 728 729 // Loop on each product line to add a stock movement 730 $sql = "SELECT cd.fk_product, cd.subprice,"; 731 $sql .= " ed.rowid, ed.qty, ed.fk_entrepot,"; 732 $sql .= " edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock"; 733 $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,"; 734 $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed"; 735 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid"; 736 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id); 737 $sql .= " AND cd.rowid = ed.fk_origin_line"; 738 739 dol_syslog(get_class($this)."::valid select details", LOG_DEBUG); 740 $resql = $this->db->query($sql); 741 if ($resql) { 742 $cpt = $this->db->num_rows($resql); 743 for ($i = 0; $i < $cpt; $i++) { 744 $obj = $this->db->fetch_object($resql); 745 if (empty($obj->edbrowid)) { 746 $qty = $obj->qty; 747 } else { 748 $qty = $obj->edbqty; 749 } 750 if ($qty <= 0) { 751 continue; 752 } 753 dol_syslog(get_class($this)."::valid movement index ".$i." ed.rowid=".$obj->rowid." edb.rowid=".$obj->edbrowid); 754 755 //var_dump($this->lines[$i]); 756 $mouvS = new MouvementStock($this->db); 757 $mouvS->origin = dol_clone($this, 1); 758 759 if (empty($obj->edbrowid)) { 760 // line without batch detail 761 762 // We decrement stock of product (and sub-products) -> update table llx_product_stock (key of this table is fk_product+fk_entrepot) and add a movement record. 763 $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, $qty, $obj->subprice, $langs->trans("ShipmentValidatedInDolibarr", $numref)); 764 765 if ($result < 0) { 766 $error++; 767 $this->error = $mouvS->error; 768 $this->errors = array_merge($this->errors, $mouvS->errors); 769 break; 770 } 771 } else { 772 // line with batch detail 773 774 // We decrement stock of product (and sub-products) -> update table llx_product_stock (key of this table is fk_product+fk_entrepot) and add a movement record. 775 // Note: ->fk_origin_stock = id into table llx_product_batch (may be rename into llx_product_stock_batch in another version) 776 $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, $qty, $obj->subprice, $langs->trans("ShipmentValidatedInDolibarr", $numref), '', $this->db->jdate($obj->eatby), $this->db->jdate($obj->sellby), $obj->batch, $obj->fk_origin_stock); 777 if ($result < 0) { 778 $error++; 779 $this->error = $mouvS->error; 780 $this->errors = array_merge($this->errors, $mouvS->errors); 781 break; 782 } 783 } 784 } 785 } else { 786 $this->db->rollback(); 787 $this->error = $this->db->error(); 788 return -2; 789 } 790 } 791 792 // Change status of order to "shipment in process" 793 $ret = $this->setStatut(Commande::STATUS_SHIPMENTONPROCESS, $this->origin_id, $this->origin); 794 if (!$ret) { 795 $error++; 796 } 797 798 if (!$error && !$notrigger) { 799 // Call trigger 800 $result = $this->call_trigger('SHIPPING_VALIDATE', $user); 801 if ($result < 0) { 802 $error++; 803 } 804 // End call triggers 805 } 806 807 if (!$error) { 808 $this->oldref = $this->ref; 809 810 // Rename directory if dir was a temporary ref 811 if (preg_match('/^[\(]?PROV/i', $this->ref)) { 812 // Now we rename also files into index 813 $sql = 'UPDATE '.MAIN_DB_PREFIX."ecm_files set filename = CONCAT('".$this->db->escape($this->newref)."', SUBSTR(filename, ".(strlen($this->ref) + 1).")), filepath = 'expedition/sending/".$this->db->escape($this->newref)."'"; 814 $sql .= " WHERE filename LIKE '".$this->db->escape($this->ref)."%' AND filepath = 'expedition/sending/".$this->db->escape($this->ref)."' and entity = ".((int) $conf->entity); 815 $resql = $this->db->query($sql); 816 if (!$resql) { 817 $error++; $this->error = $this->db->lasterror(); 818 } 819 820 // We rename directory ($this->ref = old ref, $num = new ref) in order not to lose the attachments 821 $oldref = dol_sanitizeFileName($this->ref); 822 $newref = dol_sanitizeFileName($numref); 823 $dirsource = $conf->expedition->dir_output.'/sending/'.$oldref; 824 $dirdest = $conf->expedition->dir_output.'/sending/'.$newref; 825 if (!$error && file_exists($dirsource)) { 826 dol_syslog(get_class($this)."::valid rename dir ".$dirsource." into ".$dirdest); 827 828 if (@rename($dirsource, $dirdest)) { 829 dol_syslog("Rename ok"); 830 // Rename docs starting with $oldref with $newref 831 $listoffiles = dol_dir_list($conf->expedition->dir_output.'/sending/'.$newref, 'files', 1, '^'.preg_quote($oldref, '/')); 832 foreach ($listoffiles as $fileentry) { 833 $dirsource = $fileentry['name']; 834 $dirdest = preg_replace('/^'.preg_quote($oldref, '/').'/', $newref, $dirsource); 835 $dirsource = $fileentry['path'].'/'.$dirsource; 836 $dirdest = $fileentry['path'].'/'.$dirdest; 837 @rename($dirsource, $dirdest); 838 } 839 } 840 } 841 } 842 } 843 844 // Set new ref and current status 845 if (!$error) { 846 $this->ref = $numref; 847 $this->statut = self::STATUS_VALIDATED; 848 } 849 850 if (!$error) { 851 $this->db->commit(); 852 return 1; 853 } else { 854 $this->db->rollback(); 855 return -1 * $error; 856 } 857 } 858 859 860 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 861 /** 862 * Create a delivery receipt from a shipment 863 * 864 * @param User $user User 865 * @return int <0 if KO, >=0 if OK 866 */ 867 public function create_delivery($user) 868 { 869 // phpcs:enable 870 global $conf; 871 872 if ($conf->delivery_note->enabled) { 873 if ($this->statut == self::STATUS_VALIDATED || $this->statut == self::STATUS_CLOSED) { 874 // Expedition validee 875 include_once DOL_DOCUMENT_ROOT.'/delivery/class/delivery.class.php'; 876 $delivery = new Delivery($this->db); 877 $result = $delivery->create_from_sending($user, $this->id); 878 if ($result > 0) { 879 return $result; 880 } else { 881 $this->error = $delivery->error; 882 return $result; 883 } 884 } else { 885 return 0; 886 } 887 } else { 888 return 0; 889 } 890 } 891 892 /** 893 * Add an expedition line. 894 * If STOCK_WAREHOUSE_NOT_REQUIRED_FOR_SHIPMENTS is set, you can add a shipment line, with no stock source defined 895 * If STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT is not set, you can add a shipment line, even if not enough into stock 896 * 897 * @param int $entrepot_id Id of warehouse 898 * @param int $id Id of source line (order line) 899 * @param int $qty Quantity 900 * @param array $array_options extrafields array 901 * @return int <0 if KO, >0 if OK 902 */ 903 public function addline($entrepot_id, $id, $qty, $array_options = 0) 904 { 905 global $conf, $langs; 906 907 $num = count($this->lines); 908 $line = new ExpeditionLigne($this->db); 909 910 $line->entrepot_id = $entrepot_id; 911 $line->origin_line_id = $id; 912 $line->fk_origin_line = $id; 913 $line->qty = $qty; 914 915 $orderline = new OrderLine($this->db); 916 $orderline->fetch($id); 917 918 // Copy the rang of the order line to the expedition line 919 $line->rang = $orderline->rang; 920 921 if (!empty($conf->stock->enabled) && !empty($orderline->fk_product)) { 922 $fk_product = $orderline->fk_product; 923 924 if (!($entrepot_id > 0) && empty($conf->global->STOCK_WAREHOUSE_NOT_REQUIRED_FOR_SHIPMENTS)) { 925 $langs->load("errors"); 926 $this->error = $langs->trans("ErrorWarehouseRequiredIntoShipmentLine"); 927 return -1; 928 } 929 930 if ($conf->global->STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT) { 931 $product = new Product($this->db); 932 $product->fetch($fk_product); 933 934 // Check must be done for stock of product into warehouse if $entrepot_id defined 935 if ($entrepot_id > 0) { 936 $product->load_stock('warehouseopen'); 937 $product_stock = $product->stock_warehouse[$entrepot_id]->real; 938 } else { 939 $product_stock = $product->stock_reel; 940 } 941 942 $product_type = $product->type; 943 if ($product_type == 0 || !empty($conf->global->STOCK_SUPPORTS_SERVICES)) { 944 $isavirtualproduct = ($product->hasFatherOrChild(1) > 0); 945 // The product is qualified for a check of quantity (must be enough in stock to be added into shipment). 946 if (!$isavirtualproduct || empty($conf->global->PRODUIT_SOUSPRODUITS) || ($isavirtualproduct && empty($conf->global->STOCK_EXCLUDE_VIRTUAL_PRODUCTS))) { // If STOCK_EXCLUDE_VIRTUAL_PRODUCTS is set, we do not manage stock for kits/virtual products. 947 if ($product_stock < $qty) { 948 $langs->load("errors"); 949 $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $product->ref); 950 $this->errorhidden = 'ErrorStockIsNotEnoughToAddProductOnShipment'; 951 952 $this->db->rollback(); 953 return -3; 954 } 955 } 956 } 957 } 958 } 959 960 // If product need a batch number, we should not have called this function but addline_batch instead. 961 if (!empty($conf->productbatch->enabled) && !empty($orderline->fk_product) && !empty($orderline->product_tobatch)) { 962 $this->error = 'ADDLINE_WAS_CALLED_INSTEAD_OF_ADDLINEBATCH'; 963 return -4; 964 } 965 966 // extrafields 967 if (empty($conf->global->MAIN_EXTRAFIELDS_DISABLED) && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used 968 $line->array_options = $array_options; 969 } 970 971 $this->lines[$num] = $line; 972 } 973 974 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 975 /** 976 * Add a shipment line with batch record 977 * 978 * @param array $dbatch Array of value (key 'detail' -> Array, key 'qty' total quantity for line, key ix_l : original line index) 979 * @param array $array_options extrafields array 980 * @return int <0 if KO, >0 if OK 981 */ 982 public function addline_batch($dbatch, $array_options = 0) 983 { 984 // phpcs:enable 985 global $conf, $langs; 986 987 $num = count($this->lines); 988 if ($dbatch['qty'] > 0) { 989 $line = new ExpeditionLigne($this->db); 990 $tab = array(); 991 foreach ($dbatch['detail'] as $key => $value) { 992 if ($value['q'] > 0) { 993 // $value['q']=qty to move 994 // $value['id_batch']=id into llx_product_batch of record to move 995 //var_dump($value); 996 997 $linebatch = new ExpeditionLineBatch($this->db); 998 $ret = $linebatch->fetchFromStock($value['id_batch']); // load serial, sellby, eatby 999 if ($ret < 0) { 1000 $this->error = $linebatch->error; 1001 return -1; 1002 } 1003 $linebatch->qty = $value['q']; 1004 $tab[] = $linebatch; 1005 1006 if ($conf->global->STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT) { 1007 require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php'; 1008 $prod_batch = new Productbatch($this->db); 1009 $prod_batch->fetch($value['id_batch']); 1010 1011 if ($prod_batch->qty < $linebatch->qty) { 1012 $langs->load("errors"); 1013 $this->errors[] = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $prod_batch->fk_product); 1014 dol_syslog(get_class($this)."::addline_batch error=Product ".$prod_batch->batch.": ".$this->errorsToString(), LOG_ERR); 1015 $this->db->rollback(); 1016 return -1; 1017 } 1018 } 1019 1020 //var_dump($linebatch); 1021 } 1022 } 1023 $line->entrepot_id = $linebatch->entrepot_id; 1024 $line->origin_line_id = $dbatch['ix_l']; // deprecated 1025 $line->fk_origin_line = $dbatch['ix_l']; 1026 $line->qty = $dbatch['qty']; 1027 $line->detail_batch = $tab; 1028 1029 // extrafields 1030 if (empty($conf->global->MAIN_EXTRAFIELDS_DISABLED) && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used 1031 $line->array_options = $array_options; 1032 } 1033 1034 //var_dump($line); 1035 $this->lines[$num] = $line; 1036 return 1; 1037 } 1038 } 1039 1040 /** 1041 * Update database 1042 * 1043 * @param User $user User that modify 1044 * @param int $notrigger 0=launch triggers after, 1=disable triggers 1045 * @return int <0 if KO, >0 if OK 1046 */ 1047 public function update($user = null, $notrigger = 0) 1048 { 1049 global $conf; 1050 $error = 0; 1051 1052 // Clean parameters 1053 1054 if (isset($this->ref)) { 1055 $this->ref = trim($this->ref); 1056 } 1057 if (isset($this->entity)) { 1058 $this->entity = (int) $this->entity; 1059 } 1060 if (isset($this->ref_customer)) { 1061 $this->ref_customer = trim($this->ref_customer); 1062 } 1063 if (isset($this->socid)) { 1064 $this->socid = (int) $this->socid; 1065 } 1066 if (isset($this->fk_user_author)) { 1067 $this->fk_user_author = (int) $this->fk_user_author; 1068 } 1069 if (isset($this->fk_user_valid)) { 1070 $this->fk_user_valid = (int) $this->fk_user_valid; 1071 } 1072 if (isset($this->fk_delivery_address)) { 1073 $this->fk_delivery_address = (int) $this->fk_delivery_address; 1074 } 1075 if (isset($this->shipping_method_id)) { 1076 $this->shipping_method_id = (int) $this->shipping_method_id; 1077 } 1078 if (isset($this->tracking_number)) { 1079 $this->tracking_number = trim($this->tracking_number); 1080 } 1081 if (isset($this->statut)) { 1082 $this->statut = (int) $this->statut; 1083 } 1084 if (isset($this->trueDepth)) { 1085 $this->trueDepth = trim($this->trueDepth); 1086 } 1087 if (isset($this->trueWidth)) { 1088 $this->trueWidth = trim($this->trueWidth); 1089 } 1090 if (isset($this->trueHeight)) { 1091 $this->trueHeight = trim($this->trueHeight); 1092 } 1093 if (isset($this->size_units)) { 1094 $this->size_units = trim($this->size_units); 1095 } 1096 if (isset($this->weight_units)) { 1097 $this->weight_units = trim($this->weight_units); 1098 } 1099 if (isset($this->trueWeight)) { 1100 $this->weight = trim($this->trueWeight); 1101 } 1102 if (isset($this->note_private)) { 1103 $this->note_private = trim($this->note_private); 1104 } 1105 if (isset($this->note_public)) { 1106 $this->note_public = trim($this->note_public); 1107 } 1108 if (isset($this->model_pdf)) { 1109 $this->model_pdf = trim($this->model_pdf); 1110 } 1111 1112 1113 1114 // Check parameters 1115 // Put here code to add control on parameters values 1116 1117 // Update request 1118 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET"; 1119 1120 $sql .= " ref=".(isset($this->ref) ? "'".$this->db->escape($this->ref)."'" : "null").","; 1121 $sql .= " ref_ext=".(isset($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null").","; 1122 $sql .= " ref_customer=".(isset($this->ref_customer) ? "'".$this->db->escape($this->ref_customer)."'" : "null").","; 1123 $sql .= " fk_soc=".(isset($this->socid) ? $this->socid : "null").","; 1124 $sql .= " date_creation=".(dol_strlen($this->date_creation) != 0 ? "'".$this->db->idate($this->date_creation)."'" : 'null').","; 1125 $sql .= " fk_user_author=".(isset($this->fk_user_author) ? $this->fk_user_author : "null").","; 1126 $sql .= " date_valid=".(dol_strlen($this->date_valid) != 0 ? "'".$this->db->idate($this->date_valid)."'" : 'null').","; 1127 $sql .= " fk_user_valid=".(isset($this->fk_user_valid) ? $this->fk_user_valid : "null").","; 1128 $sql .= " date_expedition=".(dol_strlen($this->date_expedition) != 0 ? "'".$this->db->idate($this->date_expedition)."'" : 'null').","; 1129 $sql .= " date_delivery=".(dol_strlen($this->date_delivery) != 0 ? "'".$this->db->idate($this->date_delivery)."'" : 'null').","; 1130 $sql .= " fk_address=".(isset($this->fk_delivery_address) ? $this->fk_delivery_address : "null").","; 1131 $sql .= " fk_shipping_method=".((isset($this->shipping_method_id) && $this->shipping_method_id > 0) ? $this->shipping_method_id : "null").","; 1132 $sql .= " tracking_number=".(isset($this->tracking_number) ? "'".$this->db->escape($this->tracking_number)."'" : "null").","; 1133 $sql .= " fk_statut=".(isset($this->statut) ? $this->statut : "null").","; 1134 $sql .= " fk_projet=".(isset($this->fk_project) ? $this->fk_project : "null").","; 1135 $sql .= " height=".(($this->trueHeight != '') ? $this->trueHeight : "null").","; 1136 $sql .= " width=".(($this->trueWidth != '') ? $this->trueWidth : "null").","; 1137 $sql .= " size_units=".(isset($this->size_units) ? $this->size_units : "null").","; 1138 $sql .= " size=".(($this->trueDepth != '') ? $this->trueDepth : "null").","; 1139 $sql .= " weight_units=".(isset($this->weight_units) ? $this->weight_units : "null").","; 1140 $sql .= " weight=".(($this->trueWeight != '') ? $this->trueWeight : "null").","; 1141 $sql .= " note_private=".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : "null").","; 1142 $sql .= " note_public=".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : "null").","; 1143 $sql .= " model_pdf=".(isset($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null").","; 1144 $sql .= " entity=".$conf->entity; 1145 1146 $sql .= " WHERE rowid=".((int) $this->id); 1147 1148 $this->db->begin(); 1149 1150 dol_syslog(get_class($this)."::update", LOG_DEBUG); 1151 $resql = $this->db->query($sql); 1152 if (!$resql) { 1153 $error++; $this->errors[] = "Error ".$this->db->lasterror(); 1154 } 1155 1156 if (!$error && !$notrigger) { 1157 // Call trigger 1158 $result = $this->call_trigger('SHIPPING_MODIFY', $user); 1159 if ($result < 0) { 1160 $error++; 1161 } 1162 // End call triggers 1163 } 1164 1165 // Commit or rollback 1166 if ($error) { 1167 foreach ($this->errors as $errmsg) { 1168 dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR); 1169 $this->error .= ($this->error ? ', '.$errmsg : $errmsg); 1170 } 1171 $this->db->rollback(); 1172 return -1 * $error; 1173 } else { 1174 $this->db->commit(); 1175 return 1; 1176 } 1177 } 1178 1179 1180 /** 1181 * Cancel shipment. 1182 * 1183 * @param int $notrigger Disable triggers 1184 * @param bool $also_update_stock true if the stock should be increased back (false by default) 1185 * @return int >0 if OK, 0 if deletion done but failed to delete files, <0 if KO 1186 */ 1187 public function cancel($notrigger = 0, $also_update_stock = false) 1188 { 1189 global $conf, $langs, $user; 1190 1191 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; 1192 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expeditionbatch.class.php'; 1193 1194 $error = 0; 1195 $this->error = ''; 1196 1197 $this->db->begin(); 1198 1199 // Add a protection to refuse deleting if shipment has at least one delivery 1200 $this->fetchObjectLinked($this->id, 'shipping', 0, 'delivery'); // Get deliveries linked to this shipment 1201 if (count($this->linkedObjectsIds) > 0) { 1202 $this->error = 'ErrorThereIsSomeDeliveries'; 1203 $error++; 1204 } 1205 1206 if (!$error && !$notrigger) { 1207 // Call trigger 1208 $result = $this->call_trigger('SHIPPING_CANCEL', $user); 1209 if ($result < 0) { 1210 $error++; 1211 } 1212 // End call triggers 1213 } 1214 1215 // Stock control 1216 if (!$error && $conf->stock->enabled && 1217 (($conf->global->STOCK_CALCULATE_ON_SHIPMENT && $this->statut > self::STATUS_DRAFT) || 1218 ($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE && $this->statut == self::STATUS_CLOSED && $also_update_stock))) { 1219 require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php"; 1220 1221 $langs->load("agenda"); 1222 1223 // Loop on each product line to add a stock movement and delete features 1224 $sql = "SELECT cd.fk_product, cd.subprice, ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id"; 1225 $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,"; 1226 $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed"; 1227 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id); 1228 $sql .= " AND cd.rowid = ed.fk_origin_line"; 1229 1230 dol_syslog(get_class($this)."::delete select details", LOG_DEBUG); 1231 $resql = $this->db->query($sql); 1232 if ($resql) { 1233 $cpt = $this->db->num_rows($resql); 1234 for ($i = 0; $i < $cpt; $i++) { 1235 dol_syslog(get_class($this)."::delete movement index ".$i); 1236 $obj = $this->db->fetch_object($resql); 1237 1238 $mouvS = new MouvementStock($this->db); 1239 // we do not log origin because it will be deleted 1240 $mouvS->origin = null; 1241 // get lot/serial 1242 $lotArray = null; 1243 if ($conf->productbatch->enabled) { 1244 $lotArray = ExpeditionLineBatch::fetchAll($this->db, $obj->expeditiondet_id); 1245 if (!is_array($lotArray)) { 1246 $error++; $this->errors[] = "Error ".$this->db->lasterror(); 1247 } 1248 } 1249 if (empty($lotArray)) { 1250 // no lot/serial 1251 // We increment stock of product (and sub-products) 1252 // We use warehouse selected for each line 1253 $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $obj->qty, 0, $langs->trans("ShipmentCanceledInDolibarr", $this->ref)); // Price is set to 0, because we don't want to see WAP changed 1254 if ($result < 0) { 1255 $error++; $this->errors = $this->errors + $mouvS->errors; 1256 break; 1257 } 1258 } else { 1259 // We increment stock of batches 1260 // We use warehouse selected for each line 1261 foreach ($lotArray as $lot) { 1262 $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $lot->qty, 0, $langs->trans("ShipmentCanceledInDolibarr", $this->ref), $lot->eatby, $lot->sellby, $lot->batch); // Price is set to 0, because we don't want to see WAP changed 1263 if ($result < 0) { 1264 $error++; $this->errors = $this->errors + $mouvS->errors; 1265 break; 1266 } 1267 } 1268 if ($error) { 1269 break; // break for loop incase of error 1270 } 1271 } 1272 } 1273 } else { 1274 $error++; $this->errors[] = "Error ".$this->db->lasterror(); 1275 } 1276 } 1277 1278 // delete batch expedition line 1279 if (!$error && $conf->productbatch->enabled) { 1280 if (ExpeditionLineBatch::deletefromexp($this->db, $this->id) < 0) { 1281 $error++; $this->errors[] = "Error ".$this->db->lasterror(); 1282 } 1283 } 1284 1285 1286 if (!$error) { 1287 $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet"; 1288 $sql .= " WHERE fk_expedition = ".((int) $this->id); 1289 1290 if ($this->db->query($sql)) { 1291 // Delete linked object 1292 $res = $this->deleteObjectLinked(); 1293 if ($res < 0) { 1294 $error++; 1295 } 1296 1297 // No delete expedition 1298 if (!$error) { 1299 $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."expedition"; 1300 $sql .= " WHERE rowid = ".$this->id; 1301 1302 if ($this->db->query($sql)) { 1303 if (!empty($this->origin) && $this->origin_id > 0) { 1304 $this->fetch_origin(); 1305 $origin = $this->origin; 1306 if ($this->$origin->statut == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress" 1307 // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress" 1308 $this->$origin->loadExpeditions(); 1309 //var_dump($this->$origin->expeditions);exit; 1310 if (count($this->$origin->expeditions) <= 0) { 1311 $this->$origin->setStatut(Commande::STATUS_VALIDATED); 1312 } 1313 } 1314 } 1315 1316 if (!$error) { 1317 $this->db->commit(); 1318 1319 // We delete PDFs 1320 $ref = dol_sanitizeFileName($this->ref); 1321 if (!empty($conf->expedition->dir_output)) { 1322 $dir = $conf->expedition->dir_output.'/sending/'.$ref; 1323 $file = $dir.'/'.$ref.'.pdf'; 1324 if (file_exists($file)) { 1325 if (!dol_delete_file($file)) { 1326 return 0; 1327 } 1328 } 1329 if (file_exists($dir)) { 1330 if (!dol_delete_dir_recursive($dir)) { 1331 $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir); 1332 return 0; 1333 } 1334 } 1335 } 1336 1337 return 1; 1338 } else { 1339 $this->db->rollback(); 1340 return -1; 1341 } 1342 } else { 1343 $this->error = $this->db->lasterror()." - sql=$sql"; 1344 $this->db->rollback(); 1345 return -3; 1346 } 1347 } else { 1348 $this->error = $this->db->lasterror()." - sql=$sql"; 1349 $this->db->rollback(); 1350 return -2; 1351 }//*/ 1352 } else { 1353 $this->error = $this->db->lasterror()." - sql=$sql"; 1354 $this->db->rollback(); 1355 return -1; 1356 } 1357 } else { 1358 $this->db->rollback(); 1359 return -1; 1360 } 1361 } 1362 1363 /** 1364 * Delete shipment. 1365 * Warning, do not delete a shipment if a delivery is linked to (with table llx_element_element) 1366 * 1367 * @param int $notrigger Disable triggers 1368 * @param bool $also_update_stock true if the stock should be increased back (false by default) 1369 * @return int >0 if OK, 0 if deletion done but failed to delete files, <0 if KO 1370 */ 1371 public function delete($notrigger = 0, $also_update_stock = false) 1372 { 1373 global $conf, $langs, $user; 1374 1375 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; 1376 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expeditionbatch.class.php'; 1377 1378 $error = 0; 1379 $this->error = ''; 1380 1381 $this->db->begin(); 1382 1383 // Add a protection to refuse deleting if shipment has at least one delivery 1384 $this->fetchObjectLinked($this->id, 'shipping', 0, 'delivery'); // Get deliveries linked to this shipment 1385 if (count($this->linkedObjectsIds) > 0) { 1386 $this->error = 'ErrorThereIsSomeDeliveries'; 1387 $error++; 1388 } 1389 1390 if (!$error && !$notrigger) { 1391 // Call trigger 1392 $result = $this->call_trigger('SHIPPING_DELETE', $user); 1393 if ($result < 0) { 1394 $error++; 1395 } 1396 // End call triggers 1397 } 1398 1399 // Stock control 1400 if (!$error && $conf->stock->enabled && 1401 (($conf->global->STOCK_CALCULATE_ON_SHIPMENT && $this->statut > self::STATUS_DRAFT) || 1402 ($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE && $this->statut == self::STATUS_CLOSED && $also_update_stock))) { 1403 require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php"; 1404 1405 $langs->load("agenda"); 1406 1407 // Loop on each product line to add a stock movement 1408 $sql = "SELECT cd.fk_product, cd.subprice, ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id"; 1409 $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,"; 1410 $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed"; 1411 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id); 1412 $sql .= " AND cd.rowid = ed.fk_origin_line"; 1413 1414 dol_syslog(get_class($this)."::delete select details", LOG_DEBUG); 1415 $resql = $this->db->query($sql); 1416 if ($resql) { 1417 $cpt = $this->db->num_rows($resql); 1418 for ($i = 0; $i < $cpt; $i++) { 1419 dol_syslog(get_class($this)."::delete movement index ".$i); 1420 $obj = $this->db->fetch_object($resql); 1421 1422 $mouvS = new MouvementStock($this->db); 1423 // we do not log origin because it will be deleted 1424 $mouvS->origin = null; 1425 // get lot/serial 1426 $lotArray = null; 1427 if ($conf->productbatch->enabled) { 1428 $lotArray = ExpeditionLineBatch::fetchAll($this->db, $obj->expeditiondet_id); 1429 if (!is_array($lotArray)) { 1430 $error++; $this->errors[] = "Error ".$this->db->lasterror(); 1431 } 1432 } 1433 if (empty($lotArray)) { 1434 // no lot/serial 1435 // We increment stock of product (and sub-products) 1436 // We use warehouse selected for each line 1437 $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $obj->qty, 0, $langs->trans("ShipmentDeletedInDolibarr", $this->ref)); // Price is set to 0, because we don't want to see WAP changed 1438 if ($result < 0) { 1439 $error++; $this->errors = $this->errors + $mouvS->errors; 1440 break; 1441 } 1442 } else { 1443 // We increment stock of batches 1444 // We use warehouse selected for each line 1445 foreach ($lotArray as $lot) { 1446 $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $lot->qty, 0, $langs->trans("ShipmentDeletedInDolibarr", $this->ref), $lot->eatby, $lot->sellby, $lot->batch); // Price is set to 0, because we don't want to see WAP changed 1447 if ($result < 0) { 1448 $error++; $this->errors = $this->errors + $mouvS->errors; 1449 break; 1450 } 1451 } 1452 if ($error) { 1453 break; // break for loop incase of error 1454 } 1455 } 1456 } 1457 } else { 1458 $error++; $this->errors[] = "Error ".$this->db->lasterror(); 1459 } 1460 } 1461 1462 // delete batch expedition line (we try deletion even if module not enabled in case of the module were enabled and disabled previously) 1463 if (!$error) { 1464 if (ExpeditionLineBatch::deletefromexp($this->db, $this->id) < 0) { 1465 $error++; $this->errors[] = "Error ".$this->db->lasterror(); 1466 } 1467 } 1468 1469 if (!$error) { 1470 $main = MAIN_DB_PREFIX.'expeditiondet'; 1471 $ef = $main."_extrafields"; 1472 $sqlef = "DELETE FROM $ef WHERE fk_object IN (SELECT rowid FROM $main WHERE fk_expedition = ".((int) $this->id).")"; 1473 1474 $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet"; 1475 $sql .= " WHERE fk_expedition = ".((int) $this->id); 1476 1477 if ($this->db->query($sqlef) && $this->db->query($sql)) { 1478 // Delete linked object 1479 $res = $this->deleteObjectLinked(); 1480 if ($res < 0) { 1481 $error++; 1482 } 1483 1484 // delete extrafields 1485 $res = $this->deleteExtraFields(); 1486 if ($res < 0) { 1487 $error++; 1488 } 1489 1490 if (!$error) { 1491 $sql = "DELETE FROM ".MAIN_DB_PREFIX."expedition"; 1492 $sql .= " WHERE rowid = ".$this->id; 1493 1494 if ($this->db->query($sql)) { 1495 if (!empty($this->origin) && $this->origin_id > 0) { 1496 $this->fetch_origin(); 1497 $origin = $this->origin; 1498 if ($this->$origin->statut == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress" 1499 // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress" 1500 $this->$origin->loadExpeditions(); 1501 //var_dump($this->$origin->expeditions);exit; 1502 if (count($this->$origin->expeditions) <= 0) { 1503 $this->$origin->setStatut(Commande::STATUS_VALIDATED); 1504 } 1505 } 1506 } 1507 1508 if (!$error) { 1509 $this->db->commit(); 1510 1511 // Delete record into ECM index (Note that delete is also done when deleting files with the dol_delete_dir_recursive 1512 $this->deleteEcmFiles(); 1513 1514 // We delete PDFs 1515 $ref = dol_sanitizeFileName($this->ref); 1516 if (!empty($conf->expedition->dir_output)) { 1517 $dir = $conf->expedition->dir_output.'/sending/'.$ref; 1518 $file = $dir.'/'.$ref.'.pdf'; 1519 if (file_exists($file)) { 1520 if (!dol_delete_file($file)) { 1521 return 0; 1522 } 1523 } 1524 if (file_exists($dir)) { 1525 if (!dol_delete_dir_recursive($dir)) { 1526 $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir); 1527 return 0; 1528 } 1529 } 1530 } 1531 1532 return 1; 1533 } else { 1534 $this->db->rollback(); 1535 return -1; 1536 } 1537 } else { 1538 $this->error = $this->db->lasterror()." - sql=$sql"; 1539 $this->db->rollback(); 1540 return -3; 1541 } 1542 } else { 1543 $this->error = $this->db->lasterror()." - sql=$sql"; 1544 $this->db->rollback(); 1545 return -2; 1546 } 1547 } else { 1548 $this->error = $this->db->lasterror()." - sql=$sql"; 1549 $this->db->rollback(); 1550 return -1; 1551 } 1552 } else { 1553 $this->db->rollback(); 1554 return -1; 1555 } 1556 } 1557 1558 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 1559 /** 1560 * Load lines 1561 * 1562 * @return int >0 if OK, Otherwise if KO 1563 */ 1564 public function fetch_lines() 1565 { 1566 // phpcs:enable 1567 global $conf, $mysoc; 1568 // TODO: recuperer les champs du document associe a part 1569 $this->lines = array(); 1570 1571 $sql = "SELECT cd.rowid, cd.fk_product, cd.label as custom_label, cd.description, cd.qty as qty_asked, cd.product_type, cd.fk_unit"; 1572 $sql .= ", cd.total_ht, cd.total_localtax1, cd.total_localtax2, cd.total_ttc, cd.total_tva"; 1573 $sql .= ", cd.vat_src_code, cd.tva_tx, cd.localtax1_tx, cd.localtax2_tx, cd.localtax1_type, cd.localtax2_type, cd.info_bits, cd.price, cd.subprice, cd.remise_percent,cd.buy_price_ht as pa_ht"; 1574 $sql .= ", cd.fk_multicurrency, cd.multicurrency_code, cd.multicurrency_subprice, cd.multicurrency_total_ht, cd.multicurrency_total_tva, cd.multicurrency_total_ttc, cd.rang"; 1575 $sql .= ", ed.rowid as line_id, ed.qty as qty_shipped, ed.fk_origin_line, ed.fk_entrepot"; 1576 $sql .= ", p.ref as product_ref, p.label as product_label, p.fk_product_type"; 1577 $sql .= ", p.weight, p.weight_units, p.length, p.length_units, p.surface, p.surface_units, p.volume, p.volume_units, p.tosell as product_tosell, p.tobuy as product_tobuy, p.tobatch as product_tobatch"; 1578 $sql .= " FROM ".MAIN_DB_PREFIX."expeditiondet as ed, ".MAIN_DB_PREFIX."commandedet as cd"; 1579 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product"; 1580 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id); 1581 $sql .= " AND ed.fk_origin_line = cd.rowid"; 1582 $sql .= " ORDER BY cd.rang, ed.fk_origin_line"; 1583 1584 dol_syslog(get_class($this)."::fetch_lines", LOG_DEBUG); 1585 $resql = $this->db->query($sql); 1586 if ($resql) { 1587 include_once DOL_DOCUMENT_ROOT.'/core/lib/price.lib.php'; 1588 1589 $num = $this->db->num_rows($resql); 1590 $i = 0; 1591 $lineindex = 0; 1592 $originline = 0; 1593 1594 $this->total_ht = 0; 1595 $this->total_tva = 0; 1596 $this->total_ttc = 0; 1597 $this->total_localtax1 = 0; 1598 $this->total_localtax2 = 0; 1599 1600 $line = new ExpeditionLigne($this->db); 1601 1602 while ($i < $num) { 1603 $obj = $this->db->fetch_object($resql); 1604 1605 if ($originline == $obj->fk_origin_line) { 1606 $line->entrepot_id = 0; // entrepod_id in details_entrepot 1607 $line->qty_shipped += $obj->qty_shipped; 1608 } else { 1609 $line = new ExpeditionLigne($this->db); 1610 $line->entrepot_id = $obj->fk_entrepot; 1611 $line->qty_shipped = $obj->qty_shipped; 1612 } 1613 1614 $detail_entrepot = new stdClass; 1615 $detail_entrepot->entrepot_id = $obj->fk_entrepot; 1616 $detail_entrepot->qty_shipped = $obj->qty_shipped; 1617 $detail_entrepot->line_id = $obj->line_id; 1618 $line->details_entrepot[] = $detail_entrepot; 1619 1620 $line->line_id = $obj->line_id; 1621 $line->rowid = $obj->line_id; // TODO deprecated 1622 $line->id = $obj->line_id; 1623 1624 $line->fk_origin = 'orderline'; 1625 $line->fk_origin_line = $obj->fk_origin_line; 1626 $line->origin_line_id = $obj->fk_origin_line; // TODO deprecated 1627 1628 $line->fk_expedition = $this->id; // id of parent 1629 1630 $line->product_type = $obj->product_type; 1631 $line->fk_product = $obj->fk_product; 1632 $line->fk_product_type = $obj->fk_product_type; 1633 $line->ref = $obj->product_ref; // TODO deprecated 1634 $line->product_ref = $obj->product_ref; 1635 $line->product_label = $obj->product_label; 1636 $line->libelle = $obj->product_label; // TODO deprecated 1637 $line->product_tosell = $obj->product_tosell; 1638 $line->product_tobuy = $obj->product_tobuy; 1639 $line->product_tobatch = $obj->product_tobatch; 1640 $line->label = $obj->custom_label; 1641 $line->description = $obj->description; 1642 $line->qty_asked = $obj->qty_asked; 1643 $line->rang = $obj->rang; 1644 $line->weight = $obj->weight; 1645 $line->weight_units = $obj->weight_units; 1646 $line->length = $obj->length; 1647 $line->length_units = $obj->length_units; 1648 $line->surface = $obj->surface; 1649 $line->surface_units = $obj->surface_units; 1650 $line->volume = $obj->volume; 1651 $line->volume_units = $obj->volume_units; 1652 $line->fk_unit = $obj->fk_unit; 1653 1654 $line->pa_ht = $obj->pa_ht; 1655 1656 // Local taxes 1657 $localtax_array = array(0=>$obj->localtax1_type, 1=>$obj->localtax1_tx, 2=>$obj->localtax2_type, 3=>$obj->localtax2_tx); 1658 $localtax1_tx = get_localtax($obj->tva_tx, 1, $this->thirdparty); 1659 $localtax2_tx = get_localtax($obj->tva_tx, 2, $this->thirdparty); 1660 1661 // For invoicing 1662 $tabprice = calcul_price_total($obj->qty_shipped, $obj->subprice, $obj->remise_percent, $obj->tva_tx, $localtax1_tx, $localtax2_tx, 0, 'HT', $obj->info_bits, $obj->fk_product_type, $mysoc, $localtax_array); // We force type to 0 1663 $line->desc = $obj->description; // We need ->desc because some code into CommonObject use desc (property defined for other elements) 1664 $line->qty = $line->qty_shipped; 1665 $line->total_ht = $tabprice[0]; 1666 $line->total_localtax1 = $tabprice[9]; 1667 $line->total_localtax2 = $tabprice[10]; 1668 $line->total_ttc = $tabprice[2]; 1669 $line->total_tva = $tabprice[1]; 1670 $line->vat_src_code = $obj->vat_src_code; 1671 $line->tva_tx = $obj->tva_tx; 1672 $line->localtax1_tx = $obj->localtax1_tx; 1673 $line->localtax2_tx = $obj->localtax2_tx; 1674 $line->info_bits = $obj->info_bits; 1675 $line->price = $obj->price; 1676 $line->subprice = $obj->subprice; 1677 $line->remise_percent = $obj->remise_percent; 1678 1679 $this->total_ht += $tabprice[0]; 1680 $this->total_tva += $tabprice[1]; 1681 $this->total_ttc += $tabprice[2]; 1682 $this->total_localtax1 += $tabprice[9]; 1683 $this->total_localtax2 += $tabprice[10]; 1684 1685 // Multicurrency 1686 $this->fk_multicurrency = $obj->fk_multicurrency; 1687 $this->multicurrency_code = $obj->multicurrency_code; 1688 $this->multicurrency_subprice = $obj->multicurrency_subprice; 1689 $this->multicurrency_total_ht = $obj->multicurrency_total_ht; 1690 $this->multicurrency_total_tva = $obj->multicurrency_total_tva; 1691 $this->multicurrency_total_ttc = $obj->multicurrency_total_ttc; 1692 1693 if ($originline != $obj->fk_origin_line) { 1694 $line->detail_batch = array(); 1695 } 1696 1697 // Detail of batch 1698 if (!empty($conf->productbatch->enabled) && $obj->line_id > 0 && $obj->product_tobatch > 0) { 1699 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expeditionbatch.class.php'; 1700 1701 $newdetailbatch = ExpeditionLineBatch::fetchAll($this->db, $obj->line_id, $obj->fk_product); 1702 if (is_array($newdetailbatch)) { 1703 if ($originline != $obj->fk_origin_line) { 1704 $line->detail_batch = $newdetailbatch; 1705 } else { 1706 $line->detail_batch = array_merge($line->detail_batch, $newdetailbatch); 1707 } 1708 } 1709 } 1710 1711 if ($originline != $obj->fk_origin_line) { 1712 $this->lines[$lineindex] = $line; 1713 $lineindex++; 1714 } else { 1715 $line->total_ht += $tabprice[0]; 1716 $line->total_localtax1 += $tabprice[9]; 1717 $line->total_localtax2 += $tabprice[10]; 1718 $line->total_ttc += $tabprice[2]; 1719 $line->total_tva += $tabprice[1]; 1720 } 1721 $line->fetch_optionals(); 1722 $i++; 1723 $originline = $obj->fk_origin_line; 1724 } 1725 $this->db->free($resql); 1726 return 1; 1727 } else { 1728 $this->error = $this->db->error(); 1729 return -3; 1730 } 1731 } 1732 1733 /** 1734 * Delete detail line 1735 * 1736 * @param User $user User making deletion 1737 * @param int $lineid Id of line to delete 1738 * @return int >0 if OK, <0 if KO 1739 */ 1740 public function deleteline($user, $lineid) 1741 { 1742 global $user; 1743 1744 if ($this->statut == self::STATUS_DRAFT) { 1745 $this->db->begin(); 1746 1747 $line = new ExpeditionLigne($this->db); 1748 1749 // For triggers 1750 $line->fetch($lineid); 1751 1752 if ($line->delete($user) > 0) { 1753 //$this->update_price(1); 1754 1755 $this->db->commit(); 1756 return 1; 1757 } else { 1758 $this->db->rollback(); 1759 return -1; 1760 } 1761 } else { 1762 $this->error = 'ErrorDeleteLineNotAllowedByObjectStatus'; 1763 return -2; 1764 } 1765 } 1766 1767 1768 /** 1769 * Return clicable link of object (with eventually picto) 1770 * 1771 * @param int $withpicto Add picto into link 1772 * @param string $option Where the link point to 1773 * @param int $max Max length to show 1774 * @param int $short Use short labels 1775 * @param int $notooltip 1=No tooltip 1776 * @param int $save_lastsearch_value -1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values whenclicking 1777 * @return string String with URL 1778 */ 1779 public function getNomUrl($withpicto = 0, $option = '', $max = 0, $short = 0, $notooltip = 0, $save_lastsearch_value = -1) 1780 { 1781 global $langs, $conf; 1782 1783 $result = ''; 1784 $label = '<u>'.$langs->trans("Shipment").'</u>'; 1785 $label .= '<br><b>'.$langs->trans('Ref').':</b> '.$this->ref; 1786 $label .= '<br><b>'.$langs->trans('RefCustomer').':</b> '.($this->ref_customer ? $this->ref_customer : $this->ref_client); 1787 1788 $url = DOL_URL_ROOT.'/expedition/card.php?id='.$this->id; 1789 1790 if ($short) { 1791 return $url; 1792 } 1793 1794 if ($option !== 'nolink') { 1795 // Add param to save lastsearch_values or not 1796 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0); 1797 if ($save_lastsearch_value == -1 && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) { 1798 $add_save_lastsearch_values = 1; 1799 } 1800 if ($add_save_lastsearch_values) { 1801 $url .= '&save_lastsearch_values=1'; 1802 } 1803 } 1804 1805 $linkclose = ''; 1806 if (empty($notooltip)) { 1807 if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER)) { 1808 $label = $langs->trans("Shipment"); 1809 $linkclose .= ' alt="'.dol_escape_htmltag($label, 1).'"'; 1810 } 1811 $linkclose .= ' title="'.dol_escape_htmltag($label, 1).'"'; 1812 $linkclose .= ' class="classfortooltip"'; 1813 } 1814 1815 $linkstart = '<a href="'.$url.'"'; 1816 $linkstart .= $linkclose.'>'; 1817 $linkend = '</a>'; 1818 1819 $result .= $linkstart; 1820 if ($withpicto) { 1821 $result .= img_object(($notooltip ? '' : $label), $this->picto, ($notooltip ? (($withpicto != 2) ? 'class="paddingright"' : '') : 'class="'.(($withpicto != 2) ? 'paddingright ' : '').'classfortooltip"'), 0, 0, $notooltip ? 0 : 1); 1822 } 1823 if ($withpicto != 2) { 1824 $result .= $this->ref; 1825 } 1826 $result .= $linkend; 1827 1828 return $result; 1829 } 1830 1831 /** 1832 * Return status label 1833 * 1834 * @param int $mode 0=Long label, 1=Short label, 2=Picto + Short label, 3=Picto, 4=Picto + Long label, 5=Short label + Picto 1835 * @return string Libelle 1836 */ 1837 public function getLibStatut($mode = 0) 1838 { 1839 return $this->LibStatut($this->statut, $mode); 1840 } 1841 1842 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 1843 /** 1844 * Return label of a status 1845 * 1846 * @param int $status Id statut 1847 * @param int $mode 0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto, 6=Long label + Picto 1848 * @return string Label of status 1849 */ 1850 public function LibStatut($status, $mode) 1851 { 1852 // phpcs:enable 1853 global $langs; 1854 1855 $labelStatus = $langs->trans($this->statuts[$status]); 1856 $labelStatusShort = $langs->trans($this->statutshorts[$status]); 1857 1858 $statusType = 'status'.$status; 1859 if ($status == self::STATUS_VALIDATED) { 1860 $statusType = 'status4'; 1861 } 1862 if ($status == self::STATUS_CLOSED) { 1863 $statusType = 'status6'; 1864 } 1865 if ($status == self::STATUS_CANCELED) { 1866 $statusType = 'status9'; 1867 } 1868 1869 return dolGetStatus($labelStatus, $labelStatusShort, '', $statusType, $mode); 1870 } 1871 1872 /** 1873 * Initialise an instance with random values. 1874 * Used to build previews or test instances. 1875 * id must be 0 if object instance is a specimen. 1876 * 1877 * @return void 1878 */ 1879 public function initAsSpecimen() 1880 { 1881 global $langs; 1882 1883 $now = dol_now(); 1884 1885 dol_syslog(get_class($this)."::initAsSpecimen"); 1886 1887 // Load array of products prodids 1888 $num_prods = 0; 1889 $prodids = array(); 1890 $sql = "SELECT rowid"; 1891 $sql .= " FROM ".MAIN_DB_PREFIX."product"; 1892 $sql .= " WHERE entity IN (".getEntity('product').")"; 1893 $resql = $this->db->query($sql); 1894 if ($resql) { 1895 $num_prods = $this->db->num_rows($resql); 1896 $i = 0; 1897 while ($i < $num_prods) { 1898 $i++; 1899 $row = $this->db->fetch_row($resql); 1900 $prodids[$i] = $row[0]; 1901 } 1902 } 1903 1904 $order = new Commande($this->db); 1905 $order->initAsSpecimen(); 1906 1907 // Initialise parametres 1908 $this->id = 0; 1909 $this->ref = 'SPECIMEN'; 1910 $this->specimen = 1; 1911 $this->statut = self::STATUS_VALIDATED; 1912 $this->livraison_id = 0; 1913 $this->date = $now; 1914 $this->date_creation = $now; 1915 $this->date_valid = $now; 1916 $this->date_delivery = $now; 1917 $this->date_expedition = $now + 24 * 3600; 1918 1919 $this->entrepot_id = 0; 1920 $this->fk_delivery_address = 0; 1921 $this->socid = 1; 1922 1923 $this->commande_id = 0; 1924 $this->commande = $order; 1925 1926 $this->origin_id = 1; 1927 $this->origin = 'commande'; 1928 1929 $this->note_private = 'Private note'; 1930 $this->note_public = 'Public note'; 1931 1932 $nbp = 5; 1933 $xnbp = 0; 1934 while ($xnbp < $nbp) { 1935 $line = new ExpeditionLigne($this->db); 1936 $line->desc = $langs->trans("Description")." ".$xnbp; 1937 $line->libelle = $langs->trans("Description")." ".$xnbp; // deprecated 1938 $line->label = $langs->trans("Description")." ".$xnbp; 1939 $line->qty = 10; 1940 $line->qty_asked = 5; 1941 $line->qty_shipped = 4; 1942 $line->fk_product = $this->commande->lines[$xnbp]->fk_product; 1943 1944 $this->lines[] = $line; 1945 $xnbp++; 1946 } 1947 } 1948 1949 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 1950 /** 1951 * Set delivery date 1952 * 1953 * @param User $user Object user that modify 1954 * @param int $delivery_date Delivery date 1955 * @return int <0 if ko, >0 if ok 1956 * @deprecated Use setDeliveryDate 1957 */ 1958 public function set_date_livraison($user, $delivery_date) 1959 { 1960 // phpcs:enable 1961 return $this->setDeliveryDate($user, $delivery_date); 1962 } 1963 1964 /** 1965 * Set the planned delivery date 1966 * 1967 * @param User $user Objet user that modify 1968 * @param integer $delivery_date Date of delivery 1969 * @return int <0 if KO, >0 if OK 1970 */ 1971 public function setDeliveryDate($user, $delivery_date) 1972 { 1973 if ($user->rights->expedition->creer) { 1974 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition"; 1975 $sql .= " SET date_delivery = ".($delivery_date ? "'".$this->db->idate($delivery_date)."'" : 'null'); 1976 $sql .= " WHERE rowid = ".$this->id; 1977 1978 dol_syslog(get_class($this)."::setDeliveryDate", LOG_DEBUG); 1979 $resql = $this->db->query($sql); 1980 if ($resql) { 1981 $this->date_delivery = $delivery_date; 1982 return 1; 1983 } else { 1984 $this->error = $this->db->error(); 1985 return -1; 1986 } 1987 } else { 1988 return -2; 1989 } 1990 } 1991 1992 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 1993 /** 1994 * Fetch deliveries method and return an array. Load array this->meths(rowid=>label). 1995 * 1996 * @return void 1997 */ 1998 public function fetch_delivery_methods() 1999 { 2000 // phpcs:enable 2001 global $langs; 2002 $this->meths = array(); 2003 2004 $sql = "SELECT em.rowid, em.code, em.libelle as label"; 2005 $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em"; 2006 $sql .= " WHERE em.active = 1"; 2007 $sql .= " ORDER BY em.libelle ASC"; 2008 2009 $resql = $this->db->query($sql); 2010 if ($resql) { 2011 while ($obj = $this->db->fetch_object($resql)) { 2012 $label = $langs->trans('SendingMethod'.$obj->code); 2013 $this->meths[$obj->rowid] = ($label != 'SendingMethod'.$obj->code ? $label : $obj->label); 2014 } 2015 } 2016 } 2017 2018 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 2019 /** 2020 * Fetch all deliveries method and return an array. Load array this->listmeths. 2021 * 2022 * @param int $id only this carrier, all if none 2023 * @return void 2024 */ 2025 public function list_delivery_methods($id = '') 2026 { 2027 // phpcs:enable 2028 global $langs; 2029 2030 $this->listmeths = array(); 2031 $i = 0; 2032 2033 $sql = "SELECT em.rowid, em.code, em.libelle as label, em.description, em.tracking, em.active"; 2034 $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em"; 2035 if ($id != '') { 2036 $sql .= " WHERE em.rowid=".((int) $id); 2037 } 2038 2039 $resql = $this->db->query($sql); 2040 if ($resql) { 2041 while ($obj = $this->db->fetch_object($resql)) { 2042 $this->listmeths[$i]['rowid'] = $obj->rowid; 2043 $this->listmeths[$i]['code'] = $obj->code; 2044 $label = $langs->trans('SendingMethod'.$obj->code); 2045 $this->listmeths[$i]['libelle'] = ($label != 'SendingMethod'.$obj->code ? $label : $obj->label); 2046 $this->listmeths[$i]['description'] = $obj->description; 2047 $this->listmeths[$i]['tracking'] = $obj->tracking; 2048 $this->listmeths[$i]['active'] = $obj->active; 2049 $i++; 2050 } 2051 } 2052 } 2053 2054 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 2055 /** 2056 * Update/create delivery method. 2057 * 2058 * @param string $id id method to activate 2059 * 2060 * @return void 2061 */ 2062 public function update_delivery_method($id = '') 2063 { 2064 // phpcs:enable 2065 if ($id == '') { 2066 $sql = "INSERT INTO ".MAIN_DB_PREFIX."c_shipment_mode (code, libelle, description, tracking)"; 2067 $sql .= " VALUES ('".$this->db->escape($this->update['code'])."','".$this->db->escape($this->update['libelle'])."','".$this->db->escape($this->update['description'])."','".$this->db->escape($this->update['tracking'])."')"; 2068 $resql = $this->db->query($sql); 2069 } else { 2070 $sql = "UPDATE ".MAIN_DB_PREFIX."c_shipment_mode SET"; 2071 $sql .= " code='".$this->db->escape($this->update['code'])."'"; 2072 $sql .= ",libelle='".$this->db->escape($this->update['libelle'])."'"; 2073 $sql .= ",description='".$this->db->escape($this->update['description'])."'"; 2074 $sql .= ",tracking='".$this->db->escape($this->update['tracking'])."'"; 2075 $sql .= " WHERE rowid=".((int) $id); 2076 $resql = $this->db->query($sql); 2077 } 2078 if ($resql < 0) { 2079 dol_print_error($this->db, ''); 2080 } 2081 } 2082 2083 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 2084 /** 2085 * Activate delivery method. 2086 * 2087 * @param int $id id method to activate 2088 * @return void 2089 */ 2090 public function activ_delivery_method($id) 2091 { 2092 // phpcs:enable 2093 $sql = 'UPDATE '.MAIN_DB_PREFIX.'c_shipment_mode SET active=1'; 2094 $sql .= ' WHERE rowid='.$id; 2095 2096 $resql = $this->db->query($sql); 2097 } 2098 2099 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 2100 /** 2101 * DesActivate delivery method. 2102 * 2103 * @param int $id id method to desactivate 2104 * 2105 * @return void 2106 */ 2107 public function disable_delivery_method($id) 2108 { 2109 // phpcs:enable 2110 $sql = 'UPDATE '.MAIN_DB_PREFIX.'c_shipment_mode SET active=0'; 2111 $sql .= ' WHERE rowid='.$id; 2112 2113 $resql = $this->db->query($sql); 2114 } 2115 2116 2117 /** 2118 * Forge an set tracking url 2119 * 2120 * @param string $value Value 2121 * @return void 2122 */ 2123 public function getUrlTrackingStatus($value = '') 2124 { 2125 if (!empty($this->shipping_method_id)) { 2126 $sql = "SELECT em.code, em.tracking"; 2127 $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em"; 2128 $sql .= " WHERE em.rowid = ".((int) $this->shipping_method_id); 2129 2130 $resql = $this->db->query($sql); 2131 if ($resql) { 2132 if ($obj = $this->db->fetch_object($resql)) { 2133 $tracking = $obj->tracking; 2134 } 2135 } 2136 } 2137 2138 if (!empty($tracking) && !empty($value)) { 2139 $url = str_replace('{TRACKID}', $value, $tracking); 2140 $this->tracking_url = sprintf('<a target="_blank" href="%s">'.($value ? $value : 'url').'</a>', $url, $url); 2141 } else { 2142 $this->tracking_url = $value; 2143 } 2144 } 2145 2146 /** 2147 * Classify the shipping as closed. 2148 * 2149 * @return int <0 if KO, >0 if OK 2150 */ 2151 public function setClosed() 2152 { 2153 global $conf, $langs, $user; 2154 2155 $error = 0; 2156 2157 // Protection. This avoid to move stock later when we should not 2158 if ($this->statut == self::STATUS_CLOSED) { 2159 return 0; 2160 } 2161 2162 $this->db->begin(); 2163 2164 $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET fk_statut='.self::STATUS_CLOSED; 2165 $sql .= ' WHERE rowid = '.$this->id.' AND fk_statut > 0'; 2166 2167 $resql = $this->db->query($sql); 2168 if ($resql) { 2169 // Set order billed if 100% of order is shipped (qty in shipment lines match qty in order lines) 2170 if ($this->origin == 'commande' && $this->origin_id > 0) { 2171 $order = new Commande($this->db); 2172 $order->fetch($this->origin_id); 2173 2174 $order->loadExpeditions(self::STATUS_CLOSED); // Fill $order->expeditions = array(orderlineid => qty) 2175 2176 $shipments_match_order = 1; 2177 foreach ($order->lines as $line) { 2178 $lineid = $line->id; 2179 $qty = $line->qty; 2180 if (($line->product_type == 0 || !empty($conf->global->STOCK_SUPPORTS_SERVICES)) && $order->expeditions[$lineid] != $qty) { 2181 $shipments_match_order = 0; 2182 $text = 'Qty for order line id '.$lineid.' is '.$qty.'. However in the shipments with status Expedition::STATUS_CLOSED='.self::STATUS_CLOSED.' we have qty = '.$order->expeditions[$lineid].', so we can t close order'; 2183 dol_syslog($text); 2184 break; 2185 } 2186 } 2187 if ($shipments_match_order) { 2188 dol_syslog("Qty for the ".count($order->lines)." lines of order have same value for shipments with status Expedition::STATUS_CLOSED=".self::STATUS_CLOSED.', so we close order'); 2189 $order->cloture($user); 2190 } 2191 } 2192 2193 $this->statut = self::STATUS_CLOSED; 2194 2195 2196 // If stock increment is done on closing 2197 if (!$error && !empty($conf->stock->enabled) && !empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE)) { 2198 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php'; 2199 2200 $langs->load("agenda"); 2201 2202 // Loop on each product line to add a stock movement 2203 // TODO possibilite d'expedier a partir d'une propale ou autre origine ? 2204 $sql = "SELECT cd.fk_product, cd.subprice,"; 2205 $sql .= " ed.rowid, ed.qty, ed.fk_entrepot,"; 2206 $sql .= " edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock"; 2207 $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,"; 2208 $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed"; 2209 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid"; 2210 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id); 2211 $sql .= " AND cd.rowid = ed.fk_origin_line"; 2212 2213 dol_syslog(get_class($this)."::valid select details", LOG_DEBUG); 2214 $resql = $this->db->query($sql); 2215 if ($resql) { 2216 $cpt = $this->db->num_rows($resql); 2217 for ($i = 0; $i < $cpt; $i++) { 2218 $obj = $this->db->fetch_object($resql); 2219 if (empty($obj->edbrowid)) { 2220 $qty = $obj->qty; 2221 } else { 2222 $qty = $obj->edbqty; 2223 } 2224 if ($qty <= 0) { 2225 continue; 2226 } 2227 dol_syslog(get_class($this)."::valid movement index ".$i." ed.rowid=".$obj->rowid." edb.rowid=".$obj->edbrowid); 2228 2229 $mouvS = new MouvementStock($this->db); 2230 $mouvS->origin = &$this; 2231 2232 if (empty($obj->edbrowid)) { 2233 // line without batch detail 2234 2235 // We decrement stock of product (and sub-products) -> update table llx_product_stock (key of this table is fk_product+fk_entrepot) and add a movement record 2236 $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, $qty, $obj->subprice, $langs->trans("ShipmentClassifyClosedInDolibarr", $numref)); 2237 if ($result < 0) { 2238 $this->error = $mouvS->error; 2239 $this->errors = $mouvS->errors; 2240 $error++; break; 2241 } 2242 } else { 2243 // line with batch detail 2244 2245 // We decrement stock of product (and sub-products) -> update table llx_product_stock (key of this table is fk_product+fk_entrepot) and add a movement record 2246 $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, $qty, $obj->subprice, $langs->trans("ShipmentClassifyClosedInDolibarr", $numref), '', $this->db->jdate($obj->eatby), $this->db->jdate($obj->sellby), $obj->batch, $obj->fk_origin_stock); 2247 if ($result < 0) { 2248 $this->error = $mouvS->error; 2249 $this->errors = $mouvS->errors; 2250 $error++; break; 2251 } 2252 } 2253 } 2254 } else { 2255 $this->error = $this->db->lasterror(); 2256 $error++; 2257 } 2258 } 2259 2260 // Call trigger 2261 if (!$error) { 2262 $result = $this->call_trigger('SHIPPING_CLOSED', $user); 2263 if ($result < 0) { 2264 $error++; 2265 } 2266 } 2267 } else { 2268 dol_print_error($this->db); 2269 $error++; 2270 } 2271 2272 if (!$error) { 2273 $this->db->commit(); 2274 return 1; 2275 } else { 2276 $this->statut = self::STATUS_VALIDATED; 2277 $this->db->rollback(); 2278 return -1; 2279 } 2280 } 2281 2282 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 2283 /** 2284 * Classify the shipping as invoiced (used when WORKFLOW_BILL_ON_SHIPMENT is on) 2285 * 2286 * @deprecated 2287 * @see setBilled() 2288 * @return int <0 if ko, >0 if ok 2289 */ 2290 public function set_billed() 2291 { 2292 // phpcs:enable 2293 dol_syslog(get_class($this)."::set_billed is deprecated, use setBilled instead", LOG_NOTICE); 2294 return $this->setBilled(); 2295 } 2296 2297 /** 2298 * Classify the shipping as invoiced (used when WORKFLOW_BILL_ON_SHIPMENT is on) 2299 * 2300 * @return int <0 if ko, >0 if ok 2301 */ 2302 public function setBilled() 2303 { 2304 global $user; 2305 $error = 0; 2306 2307 $this->db->begin(); 2308 2309 $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET fk_statut=2, billed=1'; // TODO Update only billed 2310 $sql .= ' WHERE rowid = '.$this->id.' AND fk_statut > 0'; 2311 2312 $resql = $this->db->query($sql); 2313 if ($resql) { 2314 $this->statut = self::STATUS_CLOSED; 2315 $this->billed = 1; 2316 2317 // Call trigger 2318 $result = $this->call_trigger('SHIPPING_BILLED', $user); 2319 if ($result < 0) { 2320 $error++; 2321 } 2322 } else { 2323 $error++; 2324 $this->errors[] = $this->db->lasterror; 2325 } 2326 2327 if (empty($error)) { 2328 $this->db->commit(); 2329 return 1; 2330 } else { 2331 $this->statut = self::STATUS_VALIDATED; 2332 $this->billed = 0; 2333 $this->db->rollback(); 2334 return -1; 2335 } 2336 } 2337 2338 /** 2339 * Classify the shipping as validated/opened 2340 * 2341 * @return int <0 if KO, 0 if already open, >0 if OK 2342 */ 2343 public function reOpen() 2344 { 2345 global $conf, $langs, $user; 2346 2347 $error = 0; 2348 2349 // Protection. This avoid to move stock later when we should not 2350 if ($this->statut == self::STATUS_VALIDATED) { 2351 return 0; 2352 } 2353 2354 $this->db->begin(); 2355 2356 $oldbilled = $this->billed; 2357 2358 $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET fk_statut=1'; 2359 $sql .= ' WHERE rowid = '.$this->id.' AND fk_statut > 0'; 2360 2361 $resql = $this->db->query($sql); 2362 if ($resql) { 2363 $this->statut = self::STATUS_VALIDATED; 2364 $this->billed = 0; 2365 2366 // If stock increment is done on closing 2367 if (!$error && !empty($conf->stock->enabled) && !empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE)) { 2368 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php'; 2369 2370 $langs->load("agenda"); 2371 2372 // Loop on each product line to add a stock movement 2373 // TODO possibilite d'expedier a partir d'une propale ou autre origine 2374 $sql = "SELECT cd.fk_product, cd.subprice,"; 2375 $sql .= " ed.rowid, ed.qty, ed.fk_entrepot,"; 2376 $sql .= " edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock"; 2377 $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,"; 2378 $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed"; 2379 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid"; 2380 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id); 2381 $sql .= " AND cd.rowid = ed.fk_origin_line"; 2382 2383 dol_syslog(get_class($this)."::valid select details", LOG_DEBUG); 2384 $resql = $this->db->query($sql); 2385 if ($resql) { 2386 $cpt = $this->db->num_rows($resql); 2387 for ($i = 0; $i < $cpt; $i++) { 2388 $obj = $this->db->fetch_object($resql); 2389 if (empty($obj->edbrowid)) { 2390 $qty = $obj->qty; 2391 } else { 2392 $qty = $obj->edbqty; 2393 } 2394 if ($qty <= 0) { 2395 continue; 2396 } 2397 dol_syslog(get_class($this)."::reopen expedition movement index ".$i." ed.rowid=".$obj->rowid." edb.rowid=".$obj->edbrowid); 2398 2399 //var_dump($this->lines[$i]); 2400 $mouvS = new MouvementStock($this->db); 2401 $mouvS->origin = &$this; 2402 2403 if (empty($obj->edbrowid)) { 2404 // line without batch detail 2405 2406 // We decrement stock of product (and sub-products) -> update table llx_product_stock (key of this table is fk_product+fk_entrepot) and add a movement record 2407 $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, -$qty, $obj->subprice, $langs->trans("ShipmentUnClassifyCloseddInDolibarr", $numref)); 2408 if ($result < 0) { 2409 $this->error = $mouvS->error; 2410 $this->errors = $mouvS->errors; 2411 $error++; break; 2412 } 2413 } else { 2414 // line with batch detail 2415 2416 // We decrement stock of product (and sub-products) -> update table llx_product_stock (key of this table is fk_product+fk_entrepot) and add a movement record 2417 $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, -$qty, $obj->subprice, $langs->trans("ShipmentUnClassifyCloseddInDolibarr", $numref), '', $this->db->jdate($obj->eatby), $this->db->jdate($obj->sellby), $obj->batch, $obj->fk_origin_stock); 2418 if ($result < 0) { 2419 $this->error = $mouvS->error; 2420 $this->errors = $mouvS->errors; 2421 $error++; break; 2422 } 2423 } 2424 } 2425 } else { 2426 $this->error = $this->db->lasterror(); 2427 $error++; 2428 } 2429 } 2430 2431 if (!$error) { 2432 // Call trigger 2433 $result = $this->call_trigger('SHIPPING_REOPEN', $user); 2434 if ($result < 0) { 2435 $error++; 2436 } 2437 } 2438 } else { 2439 $error++; 2440 $this->errors[] = $this->db->lasterror(); 2441 } 2442 2443 if (!$error) { 2444 $this->db->commit(); 2445 return 1; 2446 } else { 2447 $this->statut = self::STATUS_CLOSED; 2448 $this->billed = $oldbilled; 2449 $this->db->rollback(); 2450 return -1; 2451 } 2452 } 2453 2454 /** 2455 * Create a document onto disk according to template module. 2456 * 2457 * @param string $modele Force the model to using ('' to not force) 2458 * @param Translate $outputlangs object lang to use for translations 2459 * @param int $hidedetails Hide details of lines 2460 * @param int $hidedesc Hide description 2461 * @param int $hideref Hide ref 2462 * @param null|array $moreparams Array to provide more information 2463 * @return int 0 if KO, 1 if OK 2464 */ 2465 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null) 2466 { 2467 global $conf; 2468 2469 $outputlangs->load("products"); 2470 2471 if (!dol_strlen($modele)) { 2472 $modele = 'rouget'; 2473 2474 if (!empty($this->model_pdf)) { 2475 $modele = $this->model_pdf; 2476 } elseif (!empty($this->modelpdf)) { // deprecated 2477 $modele = $this->modelpdf; 2478 } elseif (!empty($conf->global->EXPEDITION_ADDON_PDF)) { 2479 $modele = $conf->global->EXPEDITION_ADDON_PDF; 2480 } 2481 } 2482 2483 $modelpath = "core/modules/expedition/doc/"; 2484 2485 $this->fetch_origin(); 2486 2487 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams); 2488 } 2489 2490 /** 2491 * Function used to replace a thirdparty id with another one. 2492 * 2493 * @param DoliDB $db Database handler 2494 * @param int $origin_id Old thirdparty id 2495 * @param int $dest_id New thirdparty id 2496 * @return bool 2497 */ 2498 public static function replaceThirdparty(DoliDB $db, $origin_id, $dest_id) 2499 { 2500 $tables = array( 2501 'expedition' 2502 ); 2503 2504 return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables); 2505 } 2506} 2507 2508 2509/** 2510 * Classe to manage lines of shipment 2511 */ 2512class ExpeditionLigne extends CommonObjectLine 2513{ 2514 /** 2515 * @var string ID to identify managed object 2516 */ 2517 public $element = 'expeditiondet'; 2518 2519 /** 2520 * @var string Name of table without prefix where object is stored 2521 */ 2522 public $table_element = 'expeditiondet'; 2523 2524 /** 2525 * @deprecated 2526 * @see $fk_origin_line 2527 */ 2528 public $origin_line_id; 2529 2530 /** 2531 * @var int ID 2532 */ 2533 public $fk_origin_line; 2534 2535 /** 2536 * @var int Id of shipment 2537 */ 2538 public $fk_expedition; 2539 2540 /** 2541 * @var DoliDB Database handler. 2542 */ 2543 public $db; 2544 2545 /** 2546 * @var float qty asked From llx_expeditiondet 2547 */ 2548 public $qty; 2549 2550 /** 2551 * @var float qty shipped 2552 */ 2553 public $qty_shipped; 2554 2555 /** 2556 * @var int Id of product 2557 */ 2558 public $fk_product; 2559 public $detail_batch; 2560 2561 /** 2562 * @var int Id of warehouse 2563 */ 2564 public $entrepot_id; 2565 2566 2567 /** 2568 * @var float qty asked From llx_commandedet or llx_propaldet 2569 */ 2570 public $qty_asked; 2571 2572 /** 2573 * @deprecated 2574 * @see $product_ref 2575 */ 2576 public $ref; 2577 2578 /** 2579 * @var string product ref 2580 */ 2581 public $product_ref; 2582 2583 /** 2584 * @deprecated 2585 * @see $product_label 2586 */ 2587 public $libelle; 2588 2589 /** 2590 * @var string product label 2591 */ 2592 public $product_label; 2593 2594 /** 2595 * @var string product description 2596 * @deprecated 2597 * @see $product_desc 2598 */ 2599 public $desc; 2600 2601 /** 2602 * @var string product description 2603 */ 2604 public $product_desc; 2605 2606 /** 2607 * @var int rang of line 2608 */ 2609 public $rang; 2610 2611 /** 2612 * @var float weight 2613 */ 2614 public $weight; 2615 public $weight_units; 2616 2617 /** 2618 * @var float weight 2619 */ 2620 public $length; 2621 public $length_units; 2622 2623 /** 2624 * @var float weight 2625 */ 2626 public $surface; 2627 public $surface_units; 2628 2629 /** 2630 * @var float weight 2631 */ 2632 public $volume; 2633 public $volume_units; 2634 2635 // Invoicing 2636 public $remise_percent; 2637 public $tva_tx; 2638 2639 /** 2640 * @var float total without tax 2641 */ 2642 public $total_ht; 2643 2644 /** 2645 * @var float total with tax 2646 */ 2647 public $total_ttc; 2648 2649 /** 2650 * @var float total vat 2651 */ 2652 public $total_tva; 2653 2654 /** 2655 * @var float total localtax 1 2656 */ 2657 public $total_localtax1; 2658 2659 /** 2660 * @var float total localtax 2 2661 */ 2662 public $total_localtax2; 2663 2664 2665 /** 2666 * Constructor 2667 * 2668 * @param DoliDB $db Database handler 2669 */ 2670 public function __construct($db) 2671 { 2672 $this->db = $db; 2673 } 2674 2675 /** 2676 * Load line expedition 2677 * 2678 * @param int $rowid Id line order 2679 * @return int <0 if KO, >0 if OK 2680 */ 2681 public function fetch($rowid) 2682 { 2683 $sql = 'SELECT ed.rowid, ed.fk_expedition, ed.fk_entrepot, ed.fk_origin_line, ed.qty, ed.rang'; 2684 $sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element.' as ed'; 2685 $sql .= ' WHERE ed.rowid = '.((int) $rowid); 2686 $result = $this->db->query($sql); 2687 if ($result) { 2688 $objp = $this->db->fetch_object($result); 2689 $this->id = $objp->rowid; 2690 $this->fk_expedition = $objp->fk_expedition; 2691 $this->entrepot_id = $objp->fk_entrepot; 2692 $this->fk_origin_line = $objp->fk_origin_line; 2693 $this->qty = $objp->qty; 2694 $this->rang = $objp->rang; 2695 2696 $this->db->free($result); 2697 2698 return 1; 2699 } else { 2700 $this->errors[] = $this->db->lasterror(); 2701 $this->error = $this->db->lasterror(); 2702 return -1; 2703 } 2704 } 2705 2706 /** 2707 * Insert line into database 2708 * 2709 * @param User $user User that modify 2710 * @param int $notrigger 1 = disable triggers 2711 * @return int <0 if KO, line id >0 if OK 2712 */ 2713 public function insert($user, $notrigger = 0) 2714 { 2715 global $langs, $conf; 2716 2717 $error = 0; 2718 2719 // Check parameters 2720 if (empty($this->fk_expedition) || empty($this->fk_origin_line) || !is_numeric($this->qty)) { 2721 $this->error = 'ErrorMandatoryParametersNotProvided'; 2722 return -1; 2723 } 2724 2725 $this->db->begin(); 2726 2727 if (empty($this->rang)) { 2728 $this->rang = 0; 2729 } 2730 2731 // Rank to use 2732 $ranktouse = $this->rang; 2733 if ($ranktouse == -1) { 2734 $rangmax = $this->line_max($this->fk_expedition); 2735 $ranktouse = $rangmax + 1; 2736 } 2737 2738 $sql = "INSERT INTO ".MAIN_DB_PREFIX."expeditiondet ("; 2739 $sql .= "fk_expedition"; 2740 $sql .= ", fk_entrepot"; 2741 $sql .= ", fk_origin_line"; 2742 $sql .= ", qty"; 2743 $sql .= ", rang"; 2744 $sql .= ") VALUES ("; 2745 $sql .= $this->fk_expedition; 2746 $sql .= ", ".(empty($this->entrepot_id) ? 'NULL' : $this->entrepot_id); 2747 $sql .= ", ".$this->fk_origin_line; 2748 $sql .= ", ".$this->qty; 2749 $sql .= ", ".$ranktouse; 2750 $sql .= ")"; 2751 2752 dol_syslog(get_class($this)."::insert", LOG_DEBUG); 2753 $resql = $this->db->query($sql); 2754 if ($resql) { 2755 $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX."expeditiondet"); 2756 2757 if (!$error) { 2758 $result = $this->insertExtraFields(); 2759 if ($result < 0) { 2760 $error++; 2761 } 2762 } 2763 2764 if (!$error && !$notrigger) { 2765 // Call trigger 2766 $result = $this->call_trigger('LINESHIPPING_INSERT', $user); 2767 if ($result < 0) { 2768 $error++; 2769 } 2770 // End call triggers 2771 } 2772 2773 if (!$error) { 2774 $this->db->commit(); 2775 return $this->id; 2776 } 2777 2778 foreach ($this->errors as $errmsg) { 2779 dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR); 2780 $this->error .= ($this->error ? ', '.$errmsg : $errmsg); 2781 } 2782 2783 $this->db->rollback(); 2784 return -1 * $error; 2785 } else { 2786 $error++; 2787 } 2788 } 2789 2790 /** 2791 * Delete shipment line. 2792 * 2793 * @param User $user User that modify 2794 * @param int $notrigger 0=launch triggers after, 1=disable triggers 2795 * @return int >0 if OK, <0 if KO 2796 */ 2797 public function delete($user = null, $notrigger = 0) 2798 { 2799 global $conf; 2800 2801 $error = 0; 2802 2803 $this->db->begin(); 2804 2805 // delete batch expedition line 2806 if ($conf->productbatch->enabled) { 2807 $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet_batch"; 2808 $sql .= " WHERE fk_expeditiondet = ".$this->id; 2809 2810 if (!$this->db->query($sql)) { 2811 $this->errors[] = $this->db->lasterror()." - sql=$sql"; 2812 $error++; 2813 } 2814 } 2815 2816 $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet"; 2817 $sql .= " WHERE rowid = ".$this->id; 2818 2819 if (!$error && $this->db->query($sql)) { 2820 // Remove extrafields 2821 if (!$error) { 2822 $result = $this->deleteExtraFields(); 2823 if ($result < 0) { 2824 $this->errors[] = $this->error; 2825 $error++; 2826 } 2827 } 2828 if (!$error && !$notrigger) { 2829 // Call trigger 2830 $result = $this->call_trigger('LINESHIPPING_DELETE', $user); 2831 if ($result < 0) { 2832 $this->errors[] = $this->error; 2833 $error++; 2834 } 2835 // End call triggers 2836 } 2837 } else { 2838 $this->errors[] = $this->db->lasterror()." - sql=$sql"; 2839 $error++; 2840 } 2841 2842 if (!$error) { 2843 $this->db->commit(); 2844 return 1; 2845 } else { 2846 foreach ($this->errors as $errmsg) { 2847 dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR); 2848 $this->error .= ($this->error ? ', '.$errmsg : $errmsg); 2849 } 2850 $this->db->rollback(); 2851 return -1 * $error; 2852 } 2853 } 2854 2855 /** 2856 * Update a line in database 2857 * 2858 * @param User $user User that modify 2859 * @param int $notrigger 1 = disable triggers 2860 * @return int < 0 if KO, > 0 if OK 2861 */ 2862 public function update($user = null, $notrigger = 0) 2863 { 2864 global $conf; 2865 2866 $error = 0; 2867 2868 dol_syslog(get_class($this)."::update id=$this->id, entrepot_id=$this->entrepot_id, product_id=$this->fk_product, qty=$this->qty"); 2869 2870 $this->db->begin(); 2871 2872 // Clean parameters 2873 if (empty($this->qty)) { 2874 $this->qty = 0; 2875 } 2876 $qty = price2num($this->qty); 2877 $remainingQty = 0; 2878 $batch = null; 2879 $batch_id = null; 2880 $expedition_batch_id = null; 2881 if (is_array($this->detail_batch)) { // array of ExpeditionLineBatch 2882 if (count($this->detail_batch) > 1) { 2883 dol_syslog(get_class($this).'::update only possible for one batch', LOG_ERR); 2884 $this->errors[] = 'ErrorBadParameters'; 2885 $error++; 2886 } else { 2887 $batch = $this->detail_batch[0]->batch; 2888 $batch_id = $this->detail_batch[0]->fk_origin_stock; 2889 $expedition_batch_id = $this->detail_batch[0]->id; 2890 if ($this->entrepot_id != $this->detail_batch[0]->entrepot_id) { 2891 dol_syslog(get_class($this).'::update only possible for batch of same warehouse', LOG_ERR); 2892 $this->errors[] = 'ErrorBadParameters'; 2893 $error++; 2894 } 2895 $qty = price2num($this->detail_batch[0]->qty); 2896 } 2897 } elseif (!empty($this->detail_batch)) { 2898 $batch = $this->detail_batch->batch; 2899 $batch_id = $this->detail_batch->fk_origin_stock; 2900 $expedition_batch_id = $this->detail_batch->id; 2901 if ($this->entrepot_id != $this->detail_batch->entrepot_id) { 2902 dol_syslog(get_class($this).'::update only possible for batch of same warehouse', LOG_ERR); 2903 $this->errors[] = 'ErrorBadParameters'; 2904 $error++; 2905 } 2906 $qty = price2num($this->detail_batch->qty); 2907 } 2908 2909 // check parameters 2910 if (!isset($this->id) || !isset($this->entrepot_id)) { 2911 dol_syslog(get_class($this).'::update missing line id and/or warehouse id', LOG_ERR); 2912 $this->errors[] = 'ErrorMandatoryParametersNotProvided'; 2913 $error++; 2914 return -1; 2915 } 2916 2917 // update lot 2918 2919 if (!empty($batch) && $conf->productbatch->enabled) { 2920 dol_syslog(get_class($this)."::update expedition batch id=$expedition_batch_id, batch_id=$batch_id, batch=$batch"); 2921 2922 if (empty($batch_id) || empty($this->fk_product)) { 2923 dol_syslog(get_class($this).'::update missing fk_origin_stock (batch_id) and/or fk_product', LOG_ERR); 2924 $this->errors[] = 'ErrorMandatoryParametersNotProvided'; 2925 $error++; 2926 } 2927 2928 // fetch remaining lot qty 2929 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expeditionbatch.class.php'; 2930 if (!$error && ($lotArray = ExpeditionLineBatch::fetchAll($this->db, $this->id)) < 0) { 2931 $this->errors[] = $this->db->lasterror()." - ExpeditionLineBatch::fetchAll"; 2932 $error++; 2933 } else { 2934 // caculate new total line qty 2935 foreach ($lotArray as $lot) { 2936 if ($expedition_batch_id != $lot->id) { 2937 $remainingQty += $lot->qty; 2938 } 2939 } 2940 $qty += $remainingQty; 2941 2942 //fetch lot details 2943 2944 // fetch from product_lot 2945 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/productlot.class.php'; 2946 $lot = new Productlot($this->db); 2947 if ($lot->fetch(0, $this->fk_product, $batch) < 0) { 2948 $this->errors[] = $lot->errors; 2949 $error++; 2950 } 2951 if (!$error && !empty($expedition_batch_id)) { 2952 // delete lot expedition line 2953 $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet_batch"; 2954 $sql .= " WHERE fk_expeditiondet = ".$this->id; 2955 $sql .= " AND rowid = ".((int) $expedition_batch_id); 2956 2957 if (!$this->db->query($sql)) { 2958 $this->errors[] = $this->db->lasterror()." - sql=$sql"; 2959 $error++; 2960 } 2961 } 2962 if (!$error && $this->detail_batch->qty > 0) { 2963 // create lot expedition line 2964 if (isset($lot->id)) { 2965 $shipmentLot = new ExpeditionLineBatch($this->db); 2966 $shipmentLot->batch = $lot->batch; 2967 $shipmentLot->eatby = $lot->eatby; 2968 $shipmentLot->sellby = $lot->sellby; 2969 $shipmentLot->entrepot_id = $this->detail_batch->entrepot_id; 2970 $shipmentLot->qty = $this->detail_batch->qty; 2971 $shipmentLot->fk_origin_stock = $batch_id; 2972 if ($shipmentLot->create($this->id) < 0) { 2973 $this->errors[] = $shipmentLot->errors; 2974 $error++; 2975 } 2976 } 2977 } 2978 } 2979 } 2980 if (!$error) { 2981 // update line 2982 $sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET"; 2983 $sql .= " fk_entrepot = ".($this->entrepot_id > 0 ? $this->entrepot_id : 'null'); 2984 $sql .= " , qty = ".((float) price2num($qty, 'MS')); 2985 $sql .= " WHERE rowid = ".((int) $this->id); 2986 2987 if (!$this->db->query($sql)) { 2988 $this->errors[] = $this->db->lasterror()." - sql=$sql"; 2989 $error++; 2990 } 2991 } 2992 2993 if (!$error) { 2994 if (!$error) { 2995 $result = $this->insertExtraFields(); 2996 if ($result < 0) { 2997 $this->errors[] = $this->error; 2998 $error++; 2999 } 3000 } 3001 } 3002 3003 if (!$error && !$notrigger) { 3004 // Call trigger 3005 $result = $this->call_trigger('LINESHIPPING_UPDATE', $user); 3006 if ($result < 0) { 3007 $this->errors[] = $this->error; 3008 $error++; 3009 } 3010 // End call triggers 3011 } 3012 if (!$error) { 3013 $this->db->commit(); 3014 return 1; 3015 } else { 3016 foreach ($this->errors as $errmsg) { 3017 dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR); 3018 $this->error .= ($this->error ? ', '.$errmsg : $errmsg); 3019 } 3020 $this->db->rollback(); 3021 return -1 * $error; 3022 } 3023 } 3024} 3025