dolibarr  20.0.0-beta
expedition.class.php
Go to the documentation of this file.
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-2024 Ferran Marcet <fmarcet@2byte.es>
13  * Copyright (C) 2018 Nicolas ZABOURI <info@inovea-conseil.com>
14  * Copyright (C) 2018-2024 Frédéric France <frederic.france@free.fr>
15  * Copyright (C) 2020 Lenin Rivas <lenin@leninrivas.com>
16  * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
17  *
18  * This program is free software; you can redistribute it and/or modify
19  * it under the terms of the GNU General Public License as published by
20  * the Free Software Foundation; either version 3 of the License, or
21  * (at your option) any later version.
22  *
23  * This program is distributed in the hope that it will be useful,
24  * but WITHOUT ANY WARRANTY; without even the implied warranty of
25  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26  * GNU General Public License for more details.
27  *
28  * You should have received a copy of the GNU General Public License
29  * along with this program. If not, see <https://www.gnu.org/licenses/>.
30  */
31 
38 require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
39 require_once DOL_DOCUMENT_ROOT."/core/class/commonobjectline.class.php";
40 require_once DOL_DOCUMENT_ROOT.'/core/class/commonincoterm.class.php';
41 if (isModEnabled("propal")) {
42  require_once DOL_DOCUMENT_ROOT.'/comm/propal/class/propal.class.php';
43 }
44 if (isModEnabled('order')) {
45  require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
46 }
47 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expeditionlinebatch.class.php';
48 
49 
53 class Expedition extends CommonObject
54 {
55  use CommonIncoterm;
56 
60  public $element = "shipping";
61 
65  public $fk_element = "fk_expedition";
66 
70  public $table_element = "expedition";
71 
75  public $table_element_line = "expeditiondet";
76 
80  public $picto = 'dolly';
81 
82 
86  public $fields = array();
87 
91  public $user_author_id;
92 
96  public $fk_user_author;
97 
98  public $socid;
99 
105  public $ref_client;
106 
110  public $ref_customer;
111 
115  public $entrepot_id;
116 
120  public $tracking_number;
121 
125  public $tracking_url;
126  public $billed;
127 
131  public $model_pdf;
132 
133  public $trueWeight;
134  public $weight_units;
135  public $trueWidth;
136  public $width_units;
137  public $trueHeight;
138  public $height_units;
139  public $trueDepth;
140  public $depth_units;
141  // A denormalized value
142  public $trueSize;
143 
144  public $livraison_id;
145 
149  public $multicurrency_subprice;
150 
151  public $size_units;
152 
153  public $sizeH;
154 
155  public $sizeS;
156 
157  public $sizeW;
158 
159  public $weight;
160 
164  public $date_delivery;
165 
170  public $date;
171 
177 
182  public $date_shipping;
183 
187  public $date_creation;
188 
192  public $date_valid;
193 
194  public $meths;
195  public $listmeths; // List of carriers
196 
200  public $commande_id;
201 
205  public $commande;
206 
210  public $lines = array();
211 
212  // Multicurrency
216  public $fk_multicurrency;
217 
221  public $multicurrency_code;
222  public $multicurrency_tx;
223  public $multicurrency_total_ht;
224  public $multicurrency_total_tva;
225  public $multicurrency_total_ttc;
226 
230  public $signed_status = 0;
231 
235  const STATUS_DRAFT = 0;
236 
243  const STATUS_VALIDATED = 1;
244 
251  const STATUS_CLOSED = 2;
252 
256  const STATUS_CANCELED = -1;
257 
266 
267 
272 
276  const STATUS_SIGNED = 1;
277 
278 
279 
285  public function __construct($db)
286  {
287  global $conf;
288 
289  $this->db = $db;
290 
291  $this->ismultientitymanaged = 1;
292  $this->isextrafieldmanaged = 1;
293 
294  // List of long language codes for status
295  $this->labelStatus = array();
296  $this->labelStatus[-1] = 'StatusSendingCanceled';
297  $this->labelStatus[0] = 'StatusSendingDraft';
298  $this->labelStatus[1] = 'StatusSendingValidated';
299  $this->labelStatus[2] = 'StatusSendingProcessed';
300 
301  // List of short language codes for status
302  $this->labelStatusShort = array();
303  $this->labelStatusShort[-1] = 'StatusSendingCanceledShort';
304  $this->labelStatusShort[0] = 'StatusSendingDraftShort';
305  $this->labelStatusShort[1] = 'StatusSendingValidatedShort';
306  $this->labelStatusShort[2] = 'StatusSendingProcessedShort';
307  }
308 
315  public function getNextNumRef($soc)
316  {
317  global $langs, $conf;
318  $langs->load("sendings");
319 
320  if (getDolGlobalString('EXPEDITION_ADDON_NUMBER')) {
321  $mybool = false;
322 
323  $file = getDolGlobalString('EXPEDITION_ADDON_NUMBER') . ".php";
324  $classname = getDolGlobalString('EXPEDITION_ADDON_NUMBER');
325 
326  // Include file with class
327  $dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']);
328 
329  foreach ($dirmodels as $reldir) {
330  $dir = dol_buildpath($reldir."core/modules/expedition/");
331 
332  // Load file with numbering class (if found)
333  $mybool = ((bool) @include_once $dir.$file) || $mybool;
334  }
335 
336  if (!$mybool) {
337  dol_print_error(null, "Failed to include file ".$file);
338  return '';
339  }
340 
341  $obj = new $classname();
342  $numref = "";
343  $numref = $obj->getNextValue($soc, $this);
344 
345  if ($numref != "") {
346  return $numref;
347  } else {
348  dol_print_error($this->db, get_class($this)."::getNextNumRef ".$obj->error);
349  return "";
350  }
351  } else {
352  print $langs->trans("Error")." ".$langs->trans("Error_EXPEDITION_ADDON_NUMBER_NotDefined");
353  return "";
354  }
355  }
356 
364  public function create($user, $notrigger = 0)
365  {
366  global $conf, $hookmanager;
367 
368  $now = dol_now();
369 
370  require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
371  $error = 0;
372 
373  // Clean parameters
374  $this->tracking_number = dol_sanitizeFileName($this->tracking_number);
375  if (empty($this->fk_project)) {
376  $this->fk_project = 0;
377  }
378  if (empty($this->date_shipping) && !empty($this->date_expedition)) {
379  $this->date_shipping = $this->date_expedition;
380  }
381 
382  $this->user = $user;
383 
384  $this->db->begin();
385 
386  $sql = "INSERT INTO ".MAIN_DB_PREFIX."expedition (";
387  $sql .= "ref";
388  $sql .= ", entity";
389  $sql .= ", ref_customer";
390  $sql .= ", ref_ext";
391  $sql .= ", date_creation";
392  $sql .= ", fk_user_author";
393  $sql .= ", date_expedition";
394  $sql .= ", date_delivery";
395  $sql .= ", fk_soc";
396  $sql .= ", fk_projet";
397  $sql .= ", fk_address";
398  $sql .= ", fk_shipping_method";
399  $sql .= ", tracking_number";
400  $sql .= ", weight";
401  $sql .= ", size";
402  $sql .= ", width";
403  $sql .= ", height";
404  $sql .= ", weight_units";
405  $sql .= ", size_units";
406  $sql .= ", note_private";
407  $sql .= ", note_public";
408  $sql .= ", model_pdf";
409  $sql .= ", fk_incoterms, location_incoterms";
410  $sql .= ") VALUES (";
411  $sql .= "'(PROV)'";
412  $sql .= ", ".((int) $conf->entity);
413  $sql .= ", ".($this->ref_customer ? "'".$this->db->escape($this->ref_customer)."'" : "null");
414  $sql .= ", ".($this->ref_ext ? "'".$this->db->escape($this->ref_ext)."'" : "null");
415  $sql .= ", '".$this->db->idate($now)."'";
416  $sql .= ", ".((int) $user->id);
417  $sql .= ", ".($this->date_shipping > 0 ? "'".$this->db->idate($this->date_shipping)."'" : "null");
418  $sql .= ", ".($this->date_delivery > 0 ? "'".$this->db->idate($this->date_delivery)."'" : "null");
419  $sql .= ", ".($this->socid > 0 ? ((int) $this->socid) : "null");
420  $sql .= ", ".($this->fk_project > 0 ? ((int) $this->fk_project) : "null");
421  $sql .= ", ".($this->fk_delivery_address > 0 ? $this->fk_delivery_address : "null");
422  $sql .= ", ".($this->shipping_method_id > 0 ? ((int) $this->shipping_method_id) : "null");
423  $sql .= ", '".$this->db->escape($this->tracking_number)."'";
424  $sql .= ", ".(is_numeric($this->weight) ? $this->weight : 'NULL');
425  $sql .= ", ".(is_numeric($this->sizeS) ? $this->sizeS : 'NULL'); // TODO Should use this->trueDepth
426  $sql .= ", ".(is_numeric($this->sizeW) ? $this->sizeW : 'NULL'); // TODO Should use this->trueWidth
427  $sql .= ", ".(is_numeric($this->sizeH) ? $this->sizeH : 'NULL'); // TODO Should use this->trueHeight
428  $sql .= ", ".($this->weight_units != '' ? (int) $this->weight_units : 'NULL');
429  $sql .= ", ".($this->size_units != '' ? (int) $this->size_units : 'NULL');
430  $sql .= ", ".(!empty($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : "null");
431  $sql .= ", ".(!empty($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : "null");
432  $sql .= ", ".(!empty($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null");
433  $sql .= ", ".(int) $this->fk_incoterms;
434  $sql .= ", '".$this->db->escape($this->location_incoterms)."'";
435  $sql .= ")";
436 
437  dol_syslog(get_class($this)."::create", LOG_DEBUG);
438  $resql = $this->db->query($sql);
439  if ($resql) {
440  $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX."expedition");
441 
442  $sql = "UPDATE ".MAIN_DB_PREFIX."expedition";
443  $sql .= " SET ref = '(PROV".$this->id.")'";
444  $sql .= " WHERE rowid = ".((int) $this->id);
445 
446  dol_syslog(get_class($this)."::create", LOG_DEBUG);
447  if ($this->db->query($sql)) {
448  // Insert of lines
449  $num = count($this->lines);
450  for ($i = 0; $i < $num; $i++) {
451  if (empty($this->lines[$i]->product_type) || getDolGlobalString('STOCK_SUPPORTS_SERVICES') || getDolGlobalString('SHIPMENT_SUPPORTS_SERVICES')) {
452  if (!isset($this->lines[$i]->detail_batch)) { // no batch management
453  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) {
454  $error++;
455  }
456  } else { // with batch management
457  if ($this->create_line_batch($this->lines[$i], $this->lines[$i]->array_options) <= 0) {
458  $error++;
459  }
460  }
461  }
462  }
463 
464  if (!$error && $this->id && $this->origin_id) {
465  $ret = $this->add_object_linked();
466  if (!$ret) {
467  $error++;
468  }
469  }
470 
471  // Actions on extra fields
472  if (!$error) {
473  $result = $this->insertExtraFields();
474  if ($result < 0) {
475  $error++;
476  }
477  }
478 
479  if (!$error && !$notrigger) {
480  // Call trigger
481  $result = $this->call_trigger('SHIPPING_CREATE', $user);
482  if ($result < 0) {
483  $error++;
484  }
485  // End call triggers
486 
487  if (!$error) {
488  $this->db->commit();
489  return $this->id;
490  } else {
491  foreach ($this->errors as $errmsg) {
492  dol_syslog(get_class($this)."::create ".$errmsg, LOG_ERR);
493  $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
494  }
495  $this->db->rollback();
496  return -1 * $error;
497  }
498  } else {
499  $error++;
500  $this->db->rollback();
501  return -3;
502  }
503  } else {
504  $error++;
505  $this->error = $this->db->lasterror()." - sql=$sql";
506  $this->db->rollback();
507  return -2;
508  }
509  } else {
510  $error++;
511  $this->error = $this->db->error()." - sql=$sql";
512  $this->db->rollback();
513  return -1;
514  }
515  }
516 
517  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
528  public function create_line($entrepot_id, $origin_line_id, $qty, $rang = 0, $array_options = [])
529  {
530  //phpcs:enable
531  global $user;
532 
533  $expeditionline = new ExpeditionLigne($this->db);
534  $expeditionline->fk_expedition = $this->id;
535  $expeditionline->entrepot_id = $entrepot_id;
536  $expeditionline->fk_elementdet = $origin_line_id;
537  $expeditionline->element_type = $this->origin;
538  $expeditionline->qty = $qty;
539  $expeditionline->rang = $rang;
540  $expeditionline->array_options = $array_options;
541 
542  if (($lineId = $expeditionline->insert($user)) < 0) {
543  $this->errors[] = $expeditionline->error;
544  }
545  return $lineId;
546  }
547 
548 
549  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
557  public function create_line_batch($line_ext, $array_options = [])
558  {
559  // phpcs:enable
560  $error = 0;
561  $stockLocationQty = array(); // associated array with batch qty in stock location
562 
563  $tab = $line_ext->detail_batch;
564  // create stockLocation Qty array
565  foreach ($tab as $detbatch) {
566  if (!empty($detbatch->entrepot_id)) {
567  if (empty($stockLocationQty[$detbatch->entrepot_id])) {
568  $stockLocationQty[$detbatch->entrepot_id] = 0;
569  }
570  $stockLocationQty[$detbatch->entrepot_id] += $detbatch->qty;
571  }
572  }
573  // create shipment lines
574  foreach ($stockLocationQty as $stockLocation => $qty) {
575  $line_id = $this->create_line($stockLocation, $line_ext->origin_line_id, $qty, $line_ext->rang, $array_options);
576  if ($line_id < 0) {
577  $error++;
578  } else {
579  // create shipment batch lines for stockLocation
580  foreach ($tab as $detbatch) {
581  if ($detbatch->entrepot_id == $stockLocation) {
582  if (!($detbatch->create($line_id) > 0)) { // Create an ExpeditionLineBatch
583  $this->errors = $detbatch->errors;
584  $error++;
585  }
586  }
587  }
588  }
589  }
590 
591  if (!$error) {
592  return 1;
593  } else {
594  return -1;
595  }
596  }
597 
607  public function fetch($id, $ref = '', $ref_ext = '', $notused = '')
608  {
609  global $conf;
610 
611  // Check parameters
612  if (empty($id) && empty($ref) && empty($ref_ext)) {
613  return -1;
614  }
615 
616  $sql = "SELECT e.rowid, e.entity, e.ref, e.fk_soc as socid, e.date_creation, e.ref_customer, e.ref_ext, e.fk_user_author, e.fk_statut, e.fk_projet as fk_project, e.billed";
617  $sql .= ", e.date_valid";
618  $sql .= ", e.weight, e.weight_units, e.size, e.size_units, e.width, e.height";
619  $sql .= ", e.date_expedition as date_expedition, e.model_pdf, e.fk_address, e.date_delivery";
620  $sql .= ", e.fk_shipping_method, e.tracking_number";
621  $sql .= ", e.note_private, e.note_public";
622  $sql .= ', e.fk_incoterms, e.location_incoterms';
623  $sql .= ', e.signed_status';
624  $sql .= ', i.libelle as label_incoterms';
625  $sql .= ', s.libelle as shipping_method';
626  $sql .= ", el.fk_source as origin_id, el.sourcetype as origin_type";
627  $sql .= " FROM ".MAIN_DB_PREFIX."expedition as e";
628  $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."element_element as el ON el.fk_target = e.rowid AND el.targettype = '".$this->db->escape($this->element)."'";
629  $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_incoterms as i ON e.fk_incoterms = i.rowid';
630  $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_shipment_mode as s ON e.fk_shipping_method = s.rowid';
631  $sql .= " WHERE e.entity IN (".getEntity('expedition').")";
632  if ($id) {
633  $sql .= " AND e.rowid = ".((int) $id);
634  }
635  if ($ref) {
636  $sql .= " AND e.ref='".$this->db->escape($ref)."'";
637  }
638  if ($ref_ext) {
639  $sql .= " AND e.ref_ext='".$this->db->escape($ref_ext)."'";
640  }
641 
642  dol_syslog(get_class($this)."::fetch", LOG_DEBUG);
643  $result = $this->db->query($sql);
644  if ($result) {
645  if ($this->db->num_rows($result)) {
646  $obj = $this->db->fetch_object($result);
647 
648  $this->id = $obj->rowid;
649  $this->entity = $obj->entity;
650  $this->ref = $obj->ref;
651  $this->socid = $obj->socid;
652  $this->ref_customer = $obj->ref_customer;
653  $this->ref_ext = $obj->ref_ext;
654  $this->status = $obj->fk_statut;
655  $this->statut = $this->status; // Deprecated
656  $this->user_author_id = $obj->fk_user_author;
657  $this->fk_user_author = $obj->fk_user_author;
658  $this->date_creation = $this->db->jdate($obj->date_creation);
659  $this->date_valid = $this->db->jdate($obj->date_valid);
660  $this->date = $this->db->jdate($obj->date_expedition); // TODO deprecated
661  $this->date_expedition = $this->db->jdate($obj->date_expedition); // TODO deprecated
662  $this->date_shipping = $this->db->jdate($obj->date_expedition); // Date real
663  $this->date_delivery = $this->db->jdate($obj->date_delivery); // Date planned
664  $this->fk_delivery_address = $obj->fk_address;
665  $this->model_pdf = $obj->model_pdf;
666  $this->shipping_method_id = $obj->fk_shipping_method;
667  $this->shipping_method = $obj->shipping_method;
668  $this->tracking_number = $obj->tracking_number;
669  $this->origin = ($obj->origin_type ? $obj->origin_type : 'commande'); // For compatibility
670  $this->origin_type = ($obj->origin_type ? $obj->origin_type : 'commande');
671  $this->origin_id = $obj->origin_id;
672  $this->billed = $obj->billed;
673  $this->fk_project = $obj->fk_project;
674  $this->signed_status = $obj->signed_status;
675  $this->trueWeight = $obj->weight;
676  $this->weight_units = $obj->weight_units;
677 
678  $this->trueWidth = $obj->width;
679  $this->width_units = $obj->size_units;
680  $this->trueHeight = $obj->height;
681  $this->height_units = $obj->size_units;
682  $this->trueDepth = $obj->size;
683  $this->depth_units = $obj->size_units;
684 
685  $this->note_public = $obj->note_public;
686  $this->note_private = $obj->note_private;
687 
688  // A denormalized value
689  $this->trueSize = $obj->size."x".$obj->width."x".$obj->height;
690  $this->size_units = $obj->size_units;
691 
692  //Incoterms
693  $this->fk_incoterms = $obj->fk_incoterms;
694  $this->location_incoterms = $obj->location_incoterms;
695  $this->label_incoterms = $obj->label_incoterms;
696 
697  $this->db->free($result);
698 
699  // Tracking url
700  $this->getUrlTrackingStatus($obj->tracking_number);
701 
702  // Thirdparty
703  $result = $this->fetch_thirdparty(); // TODO Remove this
704 
705  // Retrieve extrafields
706  $this->fetch_optionals();
707 
708  // Fix Get multicurrency param for transmitted
709  if (isModEnabled('multicurrency')) {
710  if (!empty($this->multicurrency_code)) {
711  $this->multicurrency_code = $this->thirdparty->multicurrency_code;
712  }
713  if (getDolGlobalString('MULTICURRENCY_USE_ORIGIN_TX') && !empty($this->thirdparty->multicurrency_tx)) {
714  $this->multicurrency_tx = $this->thirdparty->multicurrency_tx;
715  }
716  }
717 
718  /*
719  * Lines
720  */
721  $result = $this->fetch_lines();
722  if ($result < 0) {
723  return -3;
724  }
725 
726  return 1;
727  } else {
728  dol_syslog(get_class($this).'::Fetch no expedition found', LOG_ERR);
729  $this->error = 'Shipment with id '.$id.' not found';
730  return 0;
731  }
732  } else {
733  $this->error = $this->db->error();
734  return -1;
735  }
736  }
737 
745  public function valid($user, $notrigger = 0)
746  {
747  global $conf;
748 
749  require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
750 
751  dol_syslog(get_class($this)."::valid");
752 
753  // Protection
754  if ($this->status) {
755  dol_syslog(get_class($this)."::valid not in draft status", LOG_WARNING);
756  return 0;
757  }
758 
759  if (!((!getDolGlobalString('MAIN_USE_ADVANCED_PERMS') && $user->hasRight('expedition', 'creer'))
760  || (getDolGlobalString('MAIN_USE_ADVANCED_PERMS') && $user->hasRight('expedition', 'shipping_advance', 'validate')))) {
761  $this->error = 'Permission denied';
762  dol_syslog(get_class($this)."::valid ".$this->error, LOG_ERR);
763  return -1;
764  }
765 
766  $this->db->begin();
767 
768  $error = 0;
769 
770  // Define new ref
771  $soc = new Societe($this->db);
772  $soc->fetch($this->socid);
773 
774  // Class of company linked to order
775  $result = $soc->setAsCustomer();
776 
777  // Define new ref
778  if (!$error && (preg_match('/^[\‍(]?PROV/i', $this->ref) || empty($this->ref))) { // empty should not happened, but when it occurs, the test save life
779  $numref = $this->getNextNumRef($soc);
780  } elseif (!empty($this->ref)) {
781  $numref = $this->ref;
782  } else {
783  $numref = "EXP".$this->id;
784  }
785  $this->newref = dol_sanitizeFileName($numref);
786 
787  $now = dol_now();
788 
789  // Validate
790  $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET";
791  $sql .= " ref='".$this->db->escape($numref)."'";
792  $sql .= ", fk_statut = 1";
793  $sql .= ", date_valid = '".$this->db->idate($now)."'";
794  $sql .= ", fk_user_valid = ".$user->id;
795  $sql .= " WHERE rowid = ".((int) $this->id);
796 
797  dol_syslog(get_class($this)."::valid update expedition", LOG_DEBUG);
798  $resql = $this->db->query($sql);
799  if (!$resql) {
800  $this->error = $this->db->lasterror();
801  $error++;
802  }
803 
804  // If stock increment is done on sending (recommended choice)
805  if (!$error && isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
806  $result = $this->manageStockMvtOnEvt($user, "ShipmentValidatedInDolibarr");
807  if ($result < 0) {
808  return -2;
809  }
810  }
811 
812  // Change status of order to "shipment in process"
813  $ret = $this->setStatut(Commande::STATUS_SHIPMENTONPROCESS, $this->origin_id, $this->origin);
814  if (!$ret) {
815  $error++;
816  }
817 
818  if (!$error && !$notrigger) {
819  // Call trigger
820  $result = $this->call_trigger('SHIPPING_VALIDATE', $user);
821  if ($result < 0) {
822  $error++;
823  }
824  // End call triggers
825  }
826 
827  if (!$error) {
828  $this->oldref = $this->ref;
829 
830  // Rename directory if dir was a temporary ref
831  if (preg_match('/^[\‍(]?PROV/i', $this->ref)) {
832  // Now we rename also files into index
833  $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)."'";
834  $sql .= " WHERE filename LIKE '".$this->db->escape($this->ref)."%' AND filepath = 'expedition/sending/".$this->db->escape($this->ref)."' and entity = ".((int) $conf->entity);
835  $resql = $this->db->query($sql);
836  if (!$resql) {
837  $error++;
838  $this->error = $this->db->lasterror();
839  }
840  $sql = 'UPDATE '.MAIN_DB_PREFIX."ecm_files set filepath = 'expedition/sending/".$this->db->escape($this->newref)."'";
841  $sql .= " WHERE filepath = 'expedition/sending/".$this->db->escape($this->ref)."' and entity = ".$conf->entity;
842  $resql = $this->db->query($sql);
843  if (!$resql) {
844  $error++;
845  $this->error = $this->db->lasterror();
846  }
847 
848  // We rename directory ($this->ref = old ref, $num = new ref) in order not to lose the attachments
849  $oldref = dol_sanitizeFileName($this->ref);
850  $newref = dol_sanitizeFileName($numref);
851  $dirsource = $conf->expedition->dir_output.'/sending/'.$oldref;
852  $dirdest = $conf->expedition->dir_output.'/sending/'.$newref;
853  if (!$error && file_exists($dirsource)) {
854  dol_syslog(get_class($this)."::valid rename dir ".$dirsource." into ".$dirdest);
855 
856  if (@rename($dirsource, $dirdest)) {
857  dol_syslog("Rename ok");
858  // Rename docs starting with $oldref with $newref
859  $listoffiles = dol_dir_list($conf->expedition->dir_output.'/sending/'.$newref, 'files', 1, '^'.preg_quote($oldref, '/'));
860  foreach ($listoffiles as $fileentry) {
861  $dirsource = $fileentry['name'];
862  $dirdest = preg_replace('/^'.preg_quote($oldref, '/').'/', $newref, $dirsource);
863  $dirsource = $fileentry['path'].'/'.$dirsource;
864  $dirdest = $fileentry['path'].'/'.$dirdest;
865  @rename($dirsource, $dirdest);
866  }
867  }
868  }
869  }
870  }
871 
872  // Set new ref and current status
873  if (!$error) {
874  $this->ref = $numref;
875  $this->statut = self::STATUS_VALIDATED;
877  }
878 
879  if (!$error) {
880  $this->db->commit();
881  return 1;
882  } else {
883  $this->db->rollback();
884  return -1 * $error;
885  }
886  }
887 
888 
889  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
896  public function create_delivery($user)
897  {
898  // phpcs:enable
899  global $conf;
900 
901  if (getDolGlobalInt('MAIN_SUBMODULE_DELIVERY')) {
902  if ($this->statut == self::STATUS_VALIDATED || $this->statut == self::STATUS_CLOSED) {
903  // Expedition validee
904  include_once DOL_DOCUMENT_ROOT.'/delivery/class/delivery.class.php';
905  $delivery = new Delivery($this->db);
906  $result = $delivery->create_from_sending($user, $this->id);
907  if ($result > 0) {
908  return $result;
909  } else {
910  $this->error = $delivery->error;
911  return $result;
912  }
913  } else {
914  return 0;
915  }
916  } else {
917  return 0;
918  }
919  }
920 
933  public function addline($entrepot_id, $id, $qty, $array_options = [])
934  {
935  global $conf, $langs;
936 
937  $num = count($this->lines);
938  $line = new ExpeditionLigne($this->db);
939 
940  $line->entrepot_id = $entrepot_id;
941  $line->origin_line_id = $id;
942  $line->fk_elementdet = $id;
943  $line->element_type = 'order';
944  $line->qty = $qty;
945 
946  $orderline = new OrderLine($this->db);
947  $orderline->fetch($id);
948 
949  // Copy the rang of the order line to the expedition line
950  $line->rang = $orderline->rang;
951  $line->product_type = $orderline->product_type;
952 
953  if (isModEnabled('stock') && !empty($orderline->fk_product)) {
954  $fk_product = $orderline->fk_product;
955 
956  if (!($entrepot_id > 0) && !getDolGlobalString('STOCK_WAREHOUSE_NOT_REQUIRED_FOR_SHIPMENTS') && !(getDolGlobalString('SHIPMENT_SUPPORTS_SERVICES') && $line->product_type == Product::TYPE_SERVICE)) {
957  $langs->load("errors");
958  $this->error = $langs->trans("ErrorWarehouseRequiredIntoShipmentLine");
959  return -1;
960  }
961 
962  if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT')) {
963  $product = new Product($this->db);
964  $product->fetch($fk_product);
965 
966  // Check must be done for stock of product into warehouse if $entrepot_id defined
967  if ($entrepot_id > 0) {
968  $product->load_stock('warehouseopen');
969  $product_stock = $product->stock_warehouse[$entrepot_id]->real;
970  } else {
971  $product_stock = $product->stock_reel;
972  }
973 
974  $product_type = $product->type;
975  if ($product_type == 0 || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
976  $isavirtualproduct = ($product->hasFatherOrChild(1) > 0);
977  // The product is qualified for a check of quantity (must be enough in stock to be added into shipment).
978  if (!$isavirtualproduct || !getDolGlobalString('PRODUIT_SOUSPRODUITS') || ($isavirtualproduct && !getDolGlobalString('STOCK_EXCLUDE_VIRTUAL_PRODUCTS'))) { // If STOCK_EXCLUDE_VIRTUAL_PRODUCTS is set, we do not manage stock for kits/virtual products.
979  if ($product_stock < $qty) {
980  $langs->load("errors");
981  $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $product->ref);
982  $this->errorhidden = 'ErrorStockIsNotEnoughToAddProductOnShipment';
983 
984  $this->db->rollback();
985  return -3;
986  }
987  }
988  }
989  }
990  }
991 
992  // If product need a batch number, we should not have called this function but addline_batch instead.
993  // If this happen, we may have a bug in card.php page
994  if (isModEnabled('productbatch') && !empty($orderline->fk_product) && !empty($orderline->product_tobatch)) {
995  $this->error = 'ADDLINE_WAS_CALLED_INSTEAD_OF_ADDLINEBATCH '.$orderline->id.' '.$orderline->fk_product; //
996  return -4;
997  }
998 
999  // extrafields
1000  if (!getDolGlobalString('MAIN_EXTRAFIELDS_DISABLED') && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used
1001  $line->array_options = $array_options;
1002  }
1003 
1004  $this->lines[$num] = $line;
1005 
1006  return 1;
1007  }
1008 
1009  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1017  public function addline_batch($dbatch, $array_options = [])
1018  {
1019  // phpcs:enable
1020  global $conf, $langs;
1021 
1022  $num = count($this->lines);
1023  if ($dbatch['qty'] > 0 || ($dbatch['qty'] == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1024  $line = new ExpeditionLigne($this->db);
1025  $tab = array();
1026  foreach ($dbatch['detail'] as $key => $value) {
1027  if ($value['q'] > 0 || ($value['q'] == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1028  // $value['q']=qty to move
1029  // $value['id_batch']=id into llx_product_batch of record to move
1030  //var_dump($value);
1031 
1032  $linebatch = new ExpeditionLineBatch($this->db);
1033  $ret = $linebatch->fetchFromStock($value['id_batch']); // load serial, sellby, eatby
1034  if ($ret < 0) {
1035  $this->setErrorsFromObject($linebatch);
1036  return -1;
1037  }
1038  $linebatch->qty = $value['q'];
1039  if ($linebatch->qty == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS')) {
1040  $linebatch->batch = null;
1041  }
1042  $tab[] = $linebatch;
1043 
1044  if (getDolGlobalString("STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT", '0')) {
1045  require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
1046  $prod_batch = new Productbatch($this->db);
1047  $prod_batch->fetch($value['id_batch']);
1048 
1049  if ($prod_batch->qty < $linebatch->qty) {
1050  $langs->load("errors");
1051  $this->errors[] = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $prod_batch->fk_product);
1052  dol_syslog(get_class($this)."::addline_batch error=Product ".$prod_batch->batch.": ".$this->errorsToString(), LOG_ERR);
1053  $this->db->rollback();
1054  return -1;
1055  }
1056  }
1057 
1058  //var_dump($linebatch);
1059  }
1060  }
1061  $line->entrepot_id = $linebatch->entrepot_id;
1062  $line->origin_line_id = $dbatch['ix_l']; // deprecated
1063  $line->fk_elementdet = $dbatch['ix_l'];
1064  $line->qty = $dbatch['qty'];
1065  $line->detail_batch = $tab;
1066 
1067  // extrafields
1068  if (!getDolGlobalString('MAIN_EXTRAFIELDS_DISABLED') && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used
1069  $line->array_options = $array_options;
1070  }
1071 
1072  //var_dump($line);
1073  $this->lines[$num] = $line;
1074  return 1;
1075  }
1076  return 0;
1077  }
1078 
1086  public function update($user = null, $notrigger = 0)
1087  {
1088  global $conf;
1089  $error = 0;
1090 
1091  // Clean parameters
1092 
1093  if (isset($this->ref)) {
1094  $this->ref = trim($this->ref);
1095  }
1096  if (isset($this->entity)) {
1097  $this->entity = (int) $this->entity;
1098  }
1099  if (isset($this->ref_customer)) {
1100  $this->ref_customer = trim($this->ref_customer);
1101  }
1102  if (isset($this->socid)) {
1103  $this->socid = (int) $this->socid;
1104  }
1105  if (isset($this->fk_user_author)) {
1106  $this->fk_user_author = (int) $this->fk_user_author;
1107  }
1108  if (isset($this->fk_user_valid)) {
1109  $this->fk_user_valid = (int) $this->fk_user_valid;
1110  }
1111  if (isset($this->fk_delivery_address)) {
1112  $this->fk_delivery_address = (int) $this->fk_delivery_address;
1113  }
1114  if (isset($this->shipping_method_id)) {
1115  $this->shipping_method_id = (int) $this->shipping_method_id;
1116  }
1117  if (isset($this->tracking_number)) {
1118  $this->tracking_number = trim($this->tracking_number);
1119  }
1120  if (isset($this->statut)) {
1121  $this->statut = (int) $this->statut;
1122  }
1123  if (isset($this->trueDepth)) {
1124  $this->trueDepth = trim($this->trueDepth);
1125  }
1126  if (isset($this->trueWidth)) {
1127  $this->trueWidth = trim($this->trueWidth);
1128  }
1129  if (isset($this->trueHeight)) {
1130  $this->trueHeight = trim($this->trueHeight);
1131  }
1132  if (isset($this->size_units)) {
1133  $this->size_units = trim($this->size_units);
1134  }
1135  if (isset($this->weight_units)) {
1136  $this->weight_units = trim($this->weight_units);
1137  }
1138  if (isset($this->trueWeight)) {
1139  $this->weight = trim((string) $this->trueWeight);
1140  }
1141  if (isset($this->note_private)) {
1142  $this->note_private = trim($this->note_private);
1143  }
1144  if (isset($this->note_public)) {
1145  $this->note_public = trim($this->note_public);
1146  }
1147  if (isset($this->model_pdf)) {
1148  $this->model_pdf = trim($this->model_pdf);
1149  }
1150 
1151  // Check parameters
1152  // Put here code to add control on parameters values
1153 
1154  // Update request
1155  $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET";
1156  $sql .= " ref=".(isset($this->ref) ? "'".$this->db->escape($this->ref)."'" : "null").",";
1157  $sql .= " ref_ext=".(isset($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null").",";
1158  $sql .= " ref_customer=".(isset($this->ref_customer) ? "'".$this->db->escape($this->ref_customer)."'" : "null").",";
1159  $sql .= " fk_soc=".(isset($this->socid) ? $this->socid : "null").",";
1160  $sql .= " date_creation=".(dol_strlen($this->date_creation) != 0 ? "'".$this->db->idate($this->date_creation)."'" : 'null').",";
1161  $sql .= " fk_user_author=".(isset($this->fk_user_author) ? $this->fk_user_author : "null").",";
1162  $sql .= " date_valid=".(dol_strlen($this->date_valid) != 0 ? "'".$this->db->idate($this->date_valid)."'" : 'null').",";
1163  $sql .= " fk_user_valid=".(isset($this->fk_user_valid) ? $this->fk_user_valid : "null").",";
1164  $sql .= " date_expedition=".(dol_strlen($this->date_expedition) != 0 ? "'".$this->db->idate($this->date_expedition)."'" : 'null').",";
1165  $sql .= " date_delivery=".(dol_strlen($this->date_delivery) != 0 ? "'".$this->db->idate($this->date_delivery)."'" : 'null').",";
1166  $sql .= " fk_address=".(isset($this->fk_delivery_address) ? $this->fk_delivery_address : "null").",";
1167  $sql .= " fk_shipping_method=".((isset($this->shipping_method_id) && $this->shipping_method_id > 0) ? $this->shipping_method_id : "null").",";
1168  $sql .= " tracking_number=".(isset($this->tracking_number) ? "'".$this->db->escape($this->tracking_number)."'" : "null").",";
1169  $sql .= " fk_statut=".(isset($this->statut) ? $this->statut : "null").",";
1170  $sql .= " fk_projet=".(isset($this->fk_project) ? $this->fk_project : "null").",";
1171  $sql .= " height=".(($this->trueHeight != '') ? $this->trueHeight : "null").",";
1172  $sql .= " width=".(($this->trueWidth != '') ? $this->trueWidth : "null").",";
1173  $sql .= " size_units=".(isset($this->size_units) ? $this->size_units : "null").",";
1174  $sql .= " size=".(($this->trueDepth != '') ? $this->trueDepth : "null").",";
1175  $sql .= " weight_units=".(isset($this->weight_units) ? $this->weight_units : "null").",";
1176  $sql .= " weight=".(($this->trueWeight != '') ? $this->trueWeight : "null").",";
1177  $sql .= " note_private=".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : "null").",";
1178  $sql .= " note_public=".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : "null").",";
1179  $sql .= " model_pdf=".(isset($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null").",";
1180  $sql .= " entity=".$conf->entity;
1181  $sql .= " WHERE rowid=".((int) $this->id);
1182 
1183  $this->db->begin();
1184 
1185  dol_syslog(get_class($this)."::update", LOG_DEBUG);
1186  $resql = $this->db->query($sql);
1187  if (!$resql) {
1188  $error++;
1189  $this->errors[] = "Error ".$this->db->lasterror();
1190  }
1191 
1192  if (!$error && !$notrigger) {
1193  // Call trigger
1194  $result = $this->call_trigger('SHIPPING_MODIFY', $user);
1195  if ($result < 0) {
1196  $error++;
1197  }
1198  // End call triggers
1199  }
1200 
1201  // Commit or rollback
1202  if ($error) {
1203  foreach ($this->errors as $errmsg) {
1204  dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR);
1205  $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
1206  }
1207  $this->db->rollback();
1208  return -1 * $error;
1209  } else {
1210  $this->db->commit();
1211  return 1;
1212  }
1213  }
1214 
1215 
1223  public function cancel($notrigger = 0, $also_update_stock = false)
1224  {
1225  global $conf, $langs, $user;
1226 
1227  require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1228 
1229  $error = 0;
1230  $this->error = '';
1231 
1232  $this->db->begin();
1233 
1234  // Add a protection to refuse deleting if shipment has at least one delivery
1235  $this->fetchObjectLinked($this->id, 'shipping', 0, 'delivery'); // Get deliveries linked to this shipment
1236  if (count($this->linkedObjectsIds) > 0) {
1237  $this->error = 'ErrorThereIsSomeDeliveries';
1238  $error++;
1239  }
1240 
1241  if (!$error && !$notrigger) {
1242  // Call trigger
1243  $result = $this->call_trigger('SHIPPING_CANCEL', $user);
1244  if ($result < 0) {
1245  $error++;
1246  }
1247  // End call triggers
1248  }
1249 
1250  // Stock control
1251  if (!$error && isModEnabled('stock') &&
1252  ((getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') && $this->statut > self::STATUS_DRAFT) ||
1253  (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE') && $this->statut == self::STATUS_CLOSED && $also_update_stock))) {
1254  require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php";
1255 
1256  $langs->load("agenda");
1257 
1258  // Loop on each product line to add a stock movement and delete features
1259  $sql = "SELECT cd.fk_product, cd.subprice, ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id";
1260  $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,";
1261  $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed";
1262  $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
1263  $sql .= " AND cd.rowid = ed.fk_elementdet";
1264 
1265  dol_syslog(get_class($this)."::delete select details", LOG_DEBUG);
1266  $resql = $this->db->query($sql);
1267  if ($resql) {
1268  $cpt = $this->db->num_rows($resql);
1269 
1270  $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1271 
1272  for ($i = 0; $i < $cpt; $i++) {
1273  dol_syslog(get_class($this)."::delete movement index ".$i);
1274  $obj = $this->db->fetch_object($resql);
1275 
1276  $mouvS = new MouvementStock($this->db);
1277  // we do not log origin because it will be deleted
1278  $mouvS->origin = '';
1279  // get lot/serial
1280  $lotArray = null;
1281  if (isModEnabled('productbatch')) {
1282  $lotArray = $shipmentlinebatch->fetchAll($obj->expeditiondet_id);
1283  if (!is_array($lotArray)) {
1284  $error++;
1285  $this->errors[] = "Error ".$this->db->lasterror();
1286  }
1287  }
1288 
1289  if (empty($lotArray)) {
1290  // no lot/serial
1291  // We increment stock of product (and sub-products)
1292  // We use warehouse selected for each line
1293  $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
1294  if ($result < 0) {
1295  $error++;
1296  $this->errors = array_merge($this->errors, $mouvS->errors);
1297  break;
1298  }
1299  } else {
1300  // We increment stock of batches
1301  // We use warehouse selected for each line
1302  foreach ($lotArray as $lot) {
1303  $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
1304  if ($result < 0) {
1305  $error++;
1306  $this->errors = array_merge($this->errors, $mouvS->errors);
1307  break;
1308  }
1309  }
1310  if ($error) {
1311  break; // break for loop in case of error
1312  }
1313  }
1314  }
1315  } else {
1316  $error++;
1317  $this->errors[] = "Error ".$this->db->lasterror();
1318  }
1319  }
1320 
1321  // delete batch expedition line
1322  if (!$error && isModEnabled('productbatch')) {
1323  $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1324  if ($shipmentlinebatch->deleteFromShipment($this->id) < 0) {
1325  $error++;
1326  $this->errors[] = "Error ".$this->db->lasterror();
1327  }
1328  }
1329 
1330 
1331  if (!$error) {
1332  $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet";
1333  $sql .= " WHERE fk_expedition = ".((int) $this->id);
1334 
1335  if ($this->db->query($sql)) {
1336  // Delete linked object
1337  $res = $this->deleteObjectLinked();
1338  if ($res < 0) {
1339  $error++;
1340  }
1341 
1342  // No delete expedition
1343  if (!$error) {
1344  $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."expedition";
1345  $sql .= " WHERE rowid = ".((int) $this->id);
1346 
1347  if ($this->db->query($sql)) {
1348  if (!empty($this->origin) && $this->origin_id > 0) {
1349  $this->fetch_origin();
1350  if ($this->origin_object->statut == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress"
1351  // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress"
1352  $this->origin_object->loadExpeditions();
1353  //var_dump($this->$origin->expeditions);exit;
1354  if (count($this->origin_object->expeditions) <= 0) {
1355  $this->origin_object->setStatut(Commande::STATUS_VALIDATED);
1356  }
1357  }
1358  }
1359 
1360  if (!$error) {
1361  $this->db->commit();
1362 
1363  // We delete PDFs
1364  $ref = dol_sanitizeFileName($this->ref);
1365  if (!empty($conf->expedition->dir_output)) {
1366  $dir = $conf->expedition->dir_output.'/sending/'.$ref;
1367  $file = $dir.'/'.$ref.'.pdf';
1368  if (file_exists($file)) {
1369  if (!dol_delete_file($file)) {
1370  return 0;
1371  }
1372  }
1373  if (file_exists($dir)) {
1374  if (!dol_delete_dir_recursive($dir)) {
1375  $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir);
1376  return 0;
1377  }
1378  }
1379  }
1380 
1381  return 1;
1382  } else {
1383  $this->db->rollback();
1384  return -1;
1385  }
1386  } else {
1387  $this->error = $this->db->lasterror()." - sql=$sql";
1388  $this->db->rollback();
1389  return -3;
1390  }
1391  } else {
1392  $this->error = $this->db->lasterror()." - sql=$sql";
1393  $this->db->rollback();
1394  return -2;
1395  }//*/
1396  } else {
1397  $this->error = $this->db->lasterror()." - sql=$sql";
1398  $this->db->rollback();
1399  return -1;
1400  }
1401  } else {
1402  $this->db->rollback();
1403  return -1;
1404  }
1405  }
1406 
1416  public function delete($user = null, $notrigger = 0, $also_update_stock = false)
1417  {
1418  global $conf, $langs;
1419 
1420  if (empty($user)) {
1421  global $user;
1422  }
1423 
1424  require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1425 
1426  $error = 0;
1427  $this->error = '';
1428 
1429  $this->db->begin();
1430 
1431  // Add a protection to refuse deleting if shipment has at least one delivery
1432  $this->fetchObjectLinked($this->id, 'shipping', 0, 'delivery'); // Get deliveries linked to this shipment
1433  if (count($this->linkedObjectsIds) > 0) {
1434  $this->error = 'ErrorThereIsSomeDeliveries';
1435  $error++;
1436  }
1437 
1438  if (!$error && !$notrigger) {
1439  // Call trigger
1440  $result = $this->call_trigger('SHIPPING_DELETE', $user);
1441  if ($result < 0) {
1442  $error++;
1443  }
1444  // End call triggers
1445  }
1446 
1447  // Stock control
1448  if (!$error && isModEnabled('stock') &&
1449  ((getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') && $this->statut > self::STATUS_DRAFT) ||
1450  (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE') && $this->statut == self::STATUS_CLOSED && $also_update_stock))) {
1451  require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php";
1452 
1453  $langs->load("agenda");
1454 
1455  // we try deletion of batch line even if module batch not enabled in case of the module were enabled and disabled previously
1456  $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1457 
1458  // Loop on each product line to add a stock movement
1459  $sql = "SELECT cd.fk_product, cd.subprice, ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id";
1460  $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,";
1461  $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed";
1462  $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
1463  $sql .= " AND cd.rowid = ed.fk_elementdet";
1464 
1465  dol_syslog(get_class($this)."::delete select details", LOG_DEBUG);
1466  $resql = $this->db->query($sql);
1467  if ($resql) {
1468  $cpt = $this->db->num_rows($resql);
1469  for ($i = 0; $i < $cpt; $i++) {
1470  dol_syslog(get_class($this)."::delete movement index ".$i);
1471  $obj = $this->db->fetch_object($resql);
1472 
1473  $mouvS = new MouvementStock($this->db);
1474  // we do not log origin because it will be deleted
1475  $mouvS->origin = '';
1476  // get lot/serial
1477  $lotArray = $shipmentlinebatch->fetchAll($obj->expeditiondet_id);
1478  if (!is_array($lotArray)) {
1479  $error++;
1480  $this->errors[] = "Error ".$this->db->lasterror();
1481  }
1482  if (empty($lotArray)) {
1483  // no lot/serial
1484  // We increment stock of product (and sub-products)
1485  // We use warehouse selected for each line
1486  $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
1487  if ($result < 0) {
1488  $error++;
1489  $this->errors = array_merge($this->errors, $mouvS->errors);
1490  break;
1491  }
1492  } else {
1493  // We increment stock of batches
1494  // We use warehouse selected for each line
1495  foreach ($lotArray as $lot) {
1496  $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
1497  if ($result < 0) {
1498  $error++;
1499  $this->errors = array_merge($this->errors, $mouvS->errors);
1500  break;
1501  }
1502  }
1503  if ($error) {
1504  break; // break for loop in case of error
1505  }
1506  }
1507  }
1508  } else {
1509  $error++;
1510  $this->errors[] = "Error ".$this->db->lasterror();
1511  }
1512  }
1513 
1514  // delete batch expedition line
1515  if (!$error) {
1516  $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1517  if ($shipmentlinebatch->deleteFromShipment($this->id) < 0) {
1518  $error++;
1519  $this->errors[] = "Error ".$this->db->lasterror();
1520  }
1521  }
1522 
1523  if (!$error) {
1524  $main = MAIN_DB_PREFIX.'expeditiondet';
1525  $ef = $main."_extrafields";
1526  $sqlef = "DELETE FROM $ef WHERE fk_object IN (SELECT rowid FROM $main WHERE fk_expedition = ".((int) $this->id).")";
1527 
1528  $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet";
1529  $sql .= " WHERE fk_expedition = ".((int) $this->id);
1530 
1531  if ($this->db->query($sqlef) && $this->db->query($sql)) {
1532  // Delete linked object
1533  $res = $this->deleteObjectLinked();
1534  if ($res < 0) {
1535  $error++;
1536  }
1537 
1538  // delete extrafields
1539  $res = $this->deleteExtraFields();
1540  if ($res < 0) {
1541  $error++;
1542  }
1543 
1544  if (!$error) {
1545  $sql = "DELETE FROM ".MAIN_DB_PREFIX."expedition";
1546  $sql .= " WHERE rowid = ".((int) $this->id);
1547 
1548  if ($this->db->query($sql)) {
1549  if (!empty($this->origin) && $this->origin_id > 0) {
1550  $this->fetch_origin();
1551  if ($this->origin_object->statut == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress"
1552  // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress"
1553  $this->origin_object->loadExpeditions();
1554  //var_dump($this->$origin->expeditions);exit;
1555  if (count($this->origin_object->expeditions) <= 0) {
1556  $this->origin_object->setStatut(Commande::STATUS_VALIDATED);
1557  }
1558  }
1559  }
1560 
1561  if (!$error) {
1562  $this->db->commit();
1563 
1564  // Delete record into ECM index (Note that delete is also done when deleting files with the dol_delete_dir_recursive
1565  $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
1566  $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
1567 
1568  // We delete PDFs
1569  $ref = dol_sanitizeFileName($this->ref);
1570  if (!empty($conf->expedition->dir_output)) {
1571  $dir = $conf->expedition->dir_output.'/sending/'.$ref;
1572  $file = $dir.'/'.$ref.'.pdf';
1573  if (file_exists($file)) {
1574  if (!dol_delete_file($file)) {
1575  return 0;
1576  }
1577  }
1578  if (file_exists($dir)) {
1579  if (!dol_delete_dir_recursive($dir)) {
1580  $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir);
1581  return 0;
1582  }
1583  }
1584  }
1585 
1586  return 1;
1587  } else {
1588  $this->db->rollback();
1589  return -1;
1590  }
1591  } else {
1592  $this->error = $this->db->lasterror()." - sql=$sql";
1593  $this->db->rollback();
1594  return -3;
1595  }
1596  } else {
1597  $this->error = $this->db->lasterror()." - sql=$sql";
1598  $this->db->rollback();
1599  return -2;
1600  }
1601  } else {
1602  $this->error = $this->db->lasterror()." - sql=$sql";
1603  $this->db->rollback();
1604  return -1;
1605  }
1606  } else {
1607  $this->db->rollback();
1608  return -1;
1609  }
1610  }
1611 
1612  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1618  public function fetch_lines()
1619  {
1620  // phpcs:enable
1621  global $mysoc;
1622 
1623  $this->lines = array();
1624 
1625  // NOTE: This fetch_lines is special because it groups all lines with the same origin_line_id into one line.
1626  // TODO: See if we can restore a common fetch_lines (one line = one record)
1627 
1628  $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";
1629  $sql .= ", cd.total_ht, cd.total_localtax1, cd.total_localtax2, cd.total_ttc, cd.total_tva";
1630  $sql .= ", cd.fk_remise_except, cd.fk_product_fournisseur_price as fk_fournprice";
1631  $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";
1632  $sql .= ", cd.fk_multicurrency, cd.multicurrency_code, cd.multicurrency_subprice, cd.multicurrency_total_ht, cd.multicurrency_total_tva, cd.multicurrency_total_ttc, cd.rang, cd.date_start, cd.date_end";
1633  $sql .= ", ed.rowid as line_id, ed.qty as qty_shipped, ed.fk_element, ed.fk_elementdet, ed.element_type, ed.fk_entrepot";
1634  $sql .= ", p.ref as product_ref, p.label as product_label, p.fk_product_type, p.barcode as product_barcode";
1635  $sql .= ", p.weight, p.weight_units, p.length, p.length_units, p.width, p.width_units, p.height, p.height_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";
1636  $sql .= " FROM ".MAIN_DB_PREFIX."expeditiondet as ed, ".MAIN_DB_PREFIX."commandedet as cd";
1637  $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product";
1638  $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
1639  $sql .= " AND ed.fk_elementdet = cd.rowid";
1640  $sql .= " ORDER BY cd.rang, ed.fk_elementdet"; // We need after a break on fk_elementdet but when there is no break on fk_elementdet, cd.rang is same so we can add it as first order criteria.
1641 
1642  dol_syslog(get_class($this)."::fetch_lines", LOG_DEBUG);
1643  $resql = $this->db->query($sql);
1644  if ($resql) {
1645  include_once DOL_DOCUMENT_ROOT.'/core/lib/price.lib.php';
1646 
1647  $num = $this->db->num_rows($resql);
1648  $i = 0;
1649  $lineindex = 0;
1650  $originline = 0;
1651 
1652  $this->total_ht = 0;
1653  $this->total_tva = 0;
1654  $this->total_ttc = 0;
1655  $this->total_localtax1 = 0;
1656  $this->total_localtax2 = 0;
1657 
1658  $this->multicurrency_total_ht = 0;
1659  $this->multicurrency_total_tva = 0;
1660  $this->multicurrency_total_ttc = 0;
1661 
1662  $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1663 
1664  while ($i < $num) {
1665  $obj = $this->db->fetch_object($resql);
1666 
1667 
1668  if ($originline > 0 && $originline == $obj->fk_elementdet) {
1669  '@phan-var-force ExpeditionLigne $line'; // $line from previous loop
1670  $line->entrepot_id = 0; // entrepod_id in details_entrepot
1671  $line->qty_shipped += $obj->qty_shipped;
1672  } else {
1673  $line = new ExpeditionLigne($this->db); // new group to start
1674  $line->entrepot_id = $obj->fk_entrepot; // this is a property of a shipment line
1675  $line->qty_shipped = $obj->qty_shipped; // this is a property of a shipment line
1676  }
1677 
1678  $detail_entrepot = new stdClass();
1679  $detail_entrepot->entrepot_id = $obj->fk_entrepot;
1680  $detail_entrepot->qty_shipped = $obj->qty_shipped;
1681  $detail_entrepot->line_id = $obj->line_id;
1682  $line->details_entrepot[] = $detail_entrepot;
1683 
1684  $line->line_id = $obj->line_id; // TODO deprecated
1685  $line->rowid = $obj->line_id; // TODO deprecated
1686  $line->id = $obj->line_id;
1687 
1688  $line->fk_origin = 'orderline'; // TODO deprecated, we already have element_type that can be use to guess type of line
1689 
1690  $line->fk_element = $obj->fk_element;
1691  $line->origin_id = $obj->fk_element;
1692  $line->fk_elementdet = $obj->fk_elementdet;
1693  $line->origin_line_id = $obj->fk_elementdet;
1694  $line->element_type = $obj->element_type;
1695 
1696  $line->fk_expedition = $this->id; // id of parent
1697 
1698  $line->product_type = $obj->product_type;
1699  $line->fk_product = $obj->fk_product;
1700  $line->fk_product_type = $obj->fk_product_type;
1701  $line->ref = $obj->product_ref; // TODO deprecated
1702  $line->product_ref = $obj->product_ref;
1703  $line->product_label = $obj->product_label;
1704  $line->libelle = $obj->product_label; // TODO deprecated
1705  $line->product_barcode = $obj->product_barcode; // Barcode number product
1706  $line->product_tosell = $obj->product_tosell;
1707  $line->product_tobuy = $obj->product_tobuy;
1708  $line->product_tobatch = $obj->product_tobatch;
1709  $line->fk_fournprice = $obj->fk_fournprice;
1710  $line->label = $obj->custom_label;
1711  $line->description = $obj->description;
1712  $line->qty_asked = $obj->qty_asked;
1713  $line->rang = $obj->rang;
1714  $line->weight = $obj->weight;
1715  $line->weight_units = $obj->weight_units;
1716  $line->length = $obj->length;
1717  $line->length_units = $obj->length_units;
1718  $line->width = $obj->width;
1719  $line->width_units = $obj->width_units;
1720  $line->height = $obj->height;
1721  $line->height_units = $obj->height_units;
1722  $line->surface = $obj->surface;
1723  $line->surface_units = $obj->surface_units;
1724  $line->volume = $obj->volume;
1725  $line->volume_units = $obj->volume_units;
1726  $line->fk_unit = $obj->fk_unit;
1727 
1728  $line->pa_ht = $obj->pa_ht;
1729 
1730  // Local taxes
1731  $localtax_array = array(0 => $obj->localtax1_type, 1 => $obj->localtax1_tx, 2 => $obj->localtax2_type, 3 => $obj->localtax2_tx);
1732  $localtax1_tx = get_localtax($obj->tva_tx, 1, $this->thirdparty);
1733  $localtax2_tx = get_localtax($obj->tva_tx, 2, $this->thirdparty);
1734 
1735  // For invoicing
1736  $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
1737  $line->desc = $obj->description; // We need ->desc because some code into CommonObject use desc (property defined for other elements)
1738  $line->qty = $line->qty_shipped;
1739  $line->total_ht = $tabprice[0];
1740  $line->total_localtax1 = $tabprice[9];
1741  $line->total_localtax2 = $tabprice[10];
1742  $line->total_ttc = $tabprice[2];
1743  $line->total_tva = $tabprice[1];
1744  $line->vat_src_code = $obj->vat_src_code;
1745  $line->tva_tx = $obj->tva_tx;
1746  $line->localtax1_tx = $obj->localtax1_tx;
1747  $line->localtax2_tx = $obj->localtax2_tx;
1748  $line->info_bits = $obj->info_bits;
1749  $line->price = $obj->price;
1750  $line->subprice = $obj->subprice;
1751  $line->fk_remise_except = $obj->fk_remise_except;
1752  $line->remise_percent = $obj->remise_percent;
1753 
1754  $this->total_ht += $tabprice[0];
1755  $this->total_tva += $tabprice[1];
1756  $this->total_ttc += $tabprice[2];
1757  $this->total_localtax1 += $tabprice[9];
1758  $this->total_localtax2 += $tabprice[10];
1759 
1760  $line->date_start = $this->db->jdate($obj->date_start);
1761  $line->date_end = $this->db->jdate($obj->date_end);
1762 
1763  // Multicurrency
1764  $this->fk_multicurrency = $obj->fk_multicurrency;
1765  $this->multicurrency_code = $obj->multicurrency_code;
1766  $line->multicurrency_subprice = $obj->multicurrency_subprice;
1767  $line->multicurrency_total_ht = $obj->multicurrency_total_ht;
1768  $line->multicurrency_total_tva = $obj->multicurrency_total_tva;
1769  $line->multicurrency_total_ttc = $obj->multicurrency_total_ttc;
1770 
1771  $this->multicurrency_total_ht += $obj->multicurrency_total_ht;
1772  $this->multicurrency_total_tva += $obj->multicurrency_total_tva;
1773  $this->multicurrency_total_ttc += $obj->multicurrency_total_ttc;
1774 
1775  if ($originline != $obj->fk_elementdet) {
1776  $line->detail_batch = array();
1777  }
1778 
1779  // Detail of batch
1780  if (isModEnabled('productbatch') && $obj->line_id > 0 && $obj->product_tobatch > 0) {
1781  $newdetailbatch = $shipmentlinebatch->fetchAll($obj->line_id, $obj->fk_product);
1782 
1783  if (is_array($newdetailbatch)) {
1784  if ($originline != $obj->fk_elementdet) {
1785  $line->detail_batch = $newdetailbatch;
1786  } else {
1787  $line->detail_batch = array_merge($line->detail_batch, $newdetailbatch);
1788  }
1789  }
1790  }
1791 
1792  $line->fetch_optionals();
1793 
1794  if ($originline != $obj->fk_elementdet) {
1795  $this->lines[$lineindex] = $line;
1796  $lineindex++;
1797  } else {
1798  $line->total_ht += $tabprice[0];
1799  $line->total_localtax1 += $tabprice[9];
1800  $line->total_localtax2 += $tabprice[10];
1801  $line->total_ttc += $tabprice[2];
1802  $line->total_tva += $tabprice[1];
1803  }
1804 
1805  $i++;
1806  $originline = $obj->fk_elementdet;
1807  }
1808  $this->db->free($resql);
1809  return 1;
1810  } else {
1811  $this->error = $this->db->error();
1812  return -3;
1813  }
1814  }
1815 
1823  public function deleteLine($user, $lineid)
1824  {
1825  global $user;
1826 
1827  if ($this->statut == self::STATUS_DRAFT) {
1828  $this->db->begin();
1829 
1830  $line = new ExpeditionLigne($this->db);
1831 
1832  // For triggers
1833  $line->fetch($lineid);
1834 
1835  if ($line->delete($user) > 0) {
1836  //$this->update_price(1);
1837 
1838  $this->db->commit();
1839  return 1;
1840  } else {
1841  $this->db->rollback();
1842  return -1;
1843  }
1844  } else {
1845  $this->error = 'ErrorDeleteLineNotAllowedByObjectStatus';
1846  return -2;
1847  }
1848  }
1849 
1850 
1858  public function getTooltipContentArray($params)
1859  {
1860  global $conf, $langs;
1861 
1862  $langs->load('sendings');
1863 
1864  $nofetch = !empty($params['nofetch']);
1865 
1866  $datas = array();
1867  $datas['picto'] = img_picto('', $this->picto).' <u class="paddingrightonly">'.$langs->trans("Shipment").'</u>';
1868  if (isset($this->statut)) {
1869  $datas['picto'] .= ' '.$this->getLibStatut(5);
1870  }
1871  $datas['ref'] = '<br><b>'.$langs->trans('Ref').':</b> '.$this->ref;
1872  $datas['refcustomer'] = '<br><b>'.$langs->trans('RefCustomer').':</b> '.($this->ref_customer ? $this->ref_customer : $this->ref_client);
1873  if (!$nofetch) {
1874  $langs->load('companies');
1875  if (empty($this->thirdparty)) {
1876  $this->fetch_thirdparty();
1877  }
1878  $datas['customer'] = '<br><b>'.$langs->trans('Customer').':</b> '.$this->thirdparty->getNomUrl(1, '', 0, 1);
1879  }
1880 
1881  return $datas;
1882  }
1883 
1895  public function getNomUrl($withpicto = 0, $option = '', $max = 0, $short = 0, $notooltip = 0, $save_lastsearch_value = -1)
1896  {
1897  global $langs, $hookmanager;
1898 
1899  $result = '';
1900  $params = [
1901  'id' => $this->id,
1902  'objecttype' => $this->element,
1903  'option' => $option,
1904  'nofetch' => 1,
1905  ];
1906  $classfortooltip = 'classfortooltip';
1907  $dataparams = '';
1908  if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
1909  $classfortooltip = 'classforajaxtooltip';
1910  $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
1911  $label = '';
1912  } else {
1913  $label = implode($this->getTooltipContentArray($params));
1914  }
1915 
1916  $url = DOL_URL_ROOT.'/expedition/card.php?id='.$this->id;
1917 
1918  if ($short) {
1919  return $url;
1920  }
1921 
1922  if ($option !== 'nolink') {
1923  // Add param to save lastsearch_values or not
1924  $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
1925  if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
1926  $add_save_lastsearch_values = 1;
1927  }
1928  if ($add_save_lastsearch_values) {
1929  $url .= '&save_lastsearch_values=1';
1930  }
1931  }
1932 
1933  $linkclose = '';
1934  if (empty($notooltip)) {
1935  if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
1936  $label = $langs->trans("Shipment");
1937  $linkclose .= ' alt="'.dol_escape_htmltag($label, 1).'"';
1938  }
1939  $linkclose .= ($label ? ' title="'.dol_escape_htmltag($label, 1).'"' : ' title="tocomplete"');
1940  $linkclose .= $dataparams.' class="'.$classfortooltip.'"';
1941  }
1942 
1943  $linkstart = '<a href="'.$url.'"';
1944  $linkstart .= $linkclose.'>';
1945  $linkend = '</a>';
1946 
1947  $result .= $linkstart;
1948  if ($withpicto) {
1949  $result .= img_object(($notooltip ? '' : $label), ($this->picto ? $this->picto : 'generic'), ($notooltip ? (($withpicto != 2) ? 'class="paddingright"' : '') : 'class="'.(($withpicto != 2) ? 'paddingright ' : '').'"'), 0, 0, $notooltip ? 0 : 1);
1950  }
1951  if ($withpicto != 2) {
1952  $result .= $this->ref;
1953  }
1954  $result .= $linkend;
1955  global $action;
1956  $hookmanager->initHooks(array($this->element . 'dao'));
1957  $parameters = array('id' => $this->id, 'getnomurl' => &$result);
1958  $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
1959  if ($reshook > 0) {
1960  $result = $hookmanager->resPrint;
1961  } else {
1962  $result .= $hookmanager->resPrint;
1963  }
1964  return $result;
1965  }
1966 
1973  public function getLibStatut($mode = 0)
1974  {
1975  return $this->LibStatut($this->statut, $mode);
1976  }
1977 
1978  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1986  public function LibStatut($status, $mode)
1987  {
1988  // phpcs:enable
1989  global $langs;
1990 
1991  $labelStatus = $langs->transnoentitiesnoconv($this->labelStatus[$status]);
1992  $labelStatusShort = $langs->transnoentitiesnoconv($this->labelStatusShort[$status]);
1993 
1994  $statusType = 'status'.$status;
1995  if ($status == self::STATUS_VALIDATED) {
1996  $statusType = 'status4';
1997  }
1998  if ($status == self::STATUS_CLOSED) {
1999  $statusType = 'status6';
2000  }
2001  if ($status == self::STATUS_CANCELED) {
2002  $statusType = 'status9';
2003  }
2004 
2005  return dolGetStatus($labelStatus, $labelStatusShort, '', $statusType, $mode);
2006  }
2007 
2015  public function getKanbanView($option = '', $arraydata = null)
2016  {
2017  global $langs, $conf;
2018 
2019  $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
2020 
2021  $return = '<div class="box-flex-item box-flex-grow-zero">';
2022  $return .= '<div class="info-box info-box-sm">';
2023  $return .= '<div class="info-box-icon bg-infobox-action">';
2024  $return .= img_picto('', 'order');
2025  $return .= '</div>';
2026  $return .= '<div class="info-box-content">';
2027  $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">'.(method_exists($this, 'getNomUrl') ? $this->getNomUrl() : $this->ref).'</span>';
2028  if ($selected >= 0) {
2029  $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
2030  }
2031  if (property_exists($this, 'thirdparty') && is_object($this->thirdparty)) {
2032  $return .= '<br><div class="info-box-ref tdoverflowmax150">'.$this->thirdparty->getNomUrl(1).'</div>';
2033  }
2034  if (property_exists($this, 'total_ht')) {
2035  $return .= '<div class="info-box-ref amount">'.price($this->total_ht, 0, $langs, 0, -1, -1, $conf->currency).' '.$langs->trans('HT').'</div>';
2036  }
2037  if (method_exists($this, 'getLibStatut')) {
2038  $return .= '<div class="info-box-status">'.$this->getLibStatut(3).'</div>';
2039  }
2040  $return .= '</div>';
2041  $return .= '</div>';
2042  $return .= '</div>';
2043 
2044  return $return;
2045  }
2046 
2054  public function initAsSpecimen()
2055  {
2056  global $langs;
2057 
2058  $now = dol_now();
2059 
2060  dol_syslog(get_class($this)."::initAsSpecimen");
2061 
2062  $order = new Commande($this->db);
2063  $order->initAsSpecimen();
2064 
2065  // Initialise parameters
2066  $this->id = 0;
2067  $this->ref = 'SPECIMEN';
2068  $this->specimen = 1;
2069  $this->statut = self::STATUS_VALIDATED;
2070  $this->livraison_id = 0;
2071  $this->date = $now;
2072  $this->date_creation = $now;
2073  $this->date_valid = $now;
2074  $this->date_delivery = $now + 24 * 3600;
2075  $this->date_expedition = $now + 24 * 3600;
2076 
2077  $this->entrepot_id = 0;
2078  $this->fk_delivery_address = 0;
2079  $this->socid = 1;
2080 
2081  $this->commande_id = 0;
2082  $this->commande = $order;
2083 
2084  $this->origin_id = 1;
2085  $this->origin = 'commande';
2086 
2087  $this->note_private = 'Private note';
2088  $this->note_public = 'Public note';
2089 
2090  $nbp = 5;
2091  $xnbp = 0;
2092  while ($xnbp < $nbp) {
2093  $line = new ExpeditionLigne($this->db);
2094  $line->product_desc = $langs->trans("Description")." ".$xnbp;
2095  $line->product_label = $langs->trans("Description")." ".$xnbp;
2096  $line->qty = 10;
2097  $line->qty_asked = 5;
2098  $line->qty_shipped = 4;
2099  $line->fk_product = $this->commande->lines[$xnbp]->fk_product;
2100 
2101  $this->lines[] = $line;
2102  $xnbp++;
2103  }
2104 
2105  return 1;
2106  }
2107 
2108  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2117  public function set_date_livraison($user, $delivery_date)
2118  {
2119  // phpcs:enable
2120  return $this->setDeliveryDate($user, $delivery_date);
2121  }
2122 
2130  public function setDeliveryDate($user, $delivery_date)
2131  {
2132  if ($user->hasRight('expedition', 'creer')) {
2133  $sql = "UPDATE ".MAIN_DB_PREFIX."expedition";
2134  $sql .= " SET date_delivery = ".($delivery_date ? "'".$this->db->idate($delivery_date)."'" : 'null');
2135  $sql .= " WHERE rowid = ".((int) $this->id);
2136 
2137  dol_syslog(get_class($this)."::setDeliveryDate", LOG_DEBUG);
2138  $resql = $this->db->query($sql);
2139  if ($resql) {
2140  $this->date_delivery = $delivery_date;
2141  return 1;
2142  } else {
2143  $this->error = $this->db->error();
2144  return -1;
2145  }
2146  } else {
2147  return -2;
2148  }
2149  }
2150 
2151  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2157  public function fetch_delivery_methods()
2158  {
2159  // phpcs:enable
2160  global $langs;
2161  $this->meths = array();
2162 
2163  $sql = "SELECT em.rowid, em.code, em.libelle as label";
2164  $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2165  $sql .= " WHERE em.active = 1";
2166  $sql .= " ORDER BY em.libelle ASC";
2167 
2168  $resql = $this->db->query($sql);
2169  if ($resql) {
2170  while ($obj = $this->db->fetch_object($resql)) {
2171  $label = $langs->trans('SendingMethod'.$obj->code);
2172  $this->meths[$obj->rowid] = ($label != 'SendingMethod'.$obj->code ? $label : $obj->label);
2173  }
2174  }
2175  }
2176 
2177  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2184  public function list_delivery_methods($id = 0)
2185  {
2186  // phpcs:enable
2187  global $langs;
2188 
2189  $this->listmeths = array();
2190  $i = 0;
2191 
2192  $sql = "SELECT em.rowid, em.code, em.libelle as label, em.description, em.tracking, em.active";
2193  $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2194  if (!empty($id)) {
2195  $sql .= " WHERE em.rowid=".((int) $id);
2196  }
2197 
2198  $resql = $this->db->query($sql);
2199  if ($resql) {
2200  while ($obj = $this->db->fetch_object($resql)) {
2201  $this->listmeths[$i]['rowid'] = $obj->rowid;
2202  $this->listmeths[$i]['code'] = $obj->code;
2203  $label = $langs->trans('SendingMethod'.$obj->code);
2204  $this->listmeths[$i]['libelle'] = ($label != 'SendingMethod'.$obj->code ? $label : $obj->label);
2205  $this->listmeths[$i]['description'] = $obj->description;
2206  $this->listmeths[$i]['tracking'] = $obj->tracking;
2207  $this->listmeths[$i]['active'] = $obj->active;
2208  $i++;
2209  }
2210  }
2211  }
2212 
2219  public function getUrlTrackingStatus($value = '')
2220  {
2221  if (!empty($this->shipping_method_id)) {
2222  $sql = "SELECT em.code, em.tracking";
2223  $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2224  $sql .= " WHERE em.rowid = ".((int) $this->shipping_method_id);
2225 
2226  $resql = $this->db->query($sql);
2227  if ($resql) {
2228  if ($obj = $this->db->fetch_object($resql)) {
2229  $tracking = $obj->tracking;
2230  }
2231  }
2232  }
2233 
2234  if (!empty($tracking) && !empty($value)) {
2235  $url = str_replace('{TRACKID}', $value, $tracking);
2236  $this->tracking_url = sprintf('<a target="_blank" rel="noopener noreferrer" href="%s">%s</a>', $url, ($value ? $value : 'url'));
2237  } else {
2238  $this->tracking_url = $value;
2239  }
2240  }
2241 
2247  public function setClosed()
2248  {
2249  global $user;
2250 
2251  $error = 0;
2252 
2253  // Protection. This avoid to move stock later when we should not
2254  if ($this->statut == self::STATUS_CLOSED) {
2255  return 0;
2256  }
2257 
2258  $this->db->begin();
2259 
2260  $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET fk_statut = ".self::STATUS_CLOSED;
2261  $sql .= " WHERE rowid = ".((int) $this->id)." AND fk_statut > 0";
2262 
2263  $resql = $this->db->query($sql);
2264  if ($resql) {
2265  // Set order billed if 100% of order is shipped (qty in shipment lines match qty in order lines)
2266  if ($this->origin == 'commande' && $this->origin_id > 0) {
2267  $order = new Commande($this->db);
2268  $order->fetch($this->origin_id);
2269 
2270  $order->loadExpeditions(self::STATUS_CLOSED); // Fill $order->expeditions = array(orderlineid => qty)
2271 
2272  $shipments_match_order = 1;
2273  foreach ($order->lines as $line) {
2274  $lineid = $line->id;
2275  $qty = $line->qty;
2276  if (($line->product_type == 0 || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) && $order->expeditions[$lineid] != $qty) {
2277  $shipments_match_order = 0;
2278  $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';
2279  dol_syslog($text);
2280  break;
2281  }
2282  }
2283  if ($shipments_match_order) {
2284  dol_syslog("Qty for the ".count($order->lines)." lines of the origin order is same than qty for lines in the shipment we close (shipments_match_order is true), with new status Expedition::STATUS_CLOSED=".self::STATUS_CLOSED.', so we close order');
2285  // We close the order
2286  $order->cloture($user); // Note this may also create an invoice if module workflow ask it
2287  }
2288  }
2289 
2290  $this->statut = self::STATUS_CLOSED; // Will be revert to STATUS_VALIDATED at end if there is a rollback
2291  $this->status = self::STATUS_CLOSED; // Will be revert to STATUS_VALIDATED at end if there is a rollback
2292 
2293  // If stock increment is done on closing
2294  if (!$error && isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
2295  $result = $this->manageStockMvtOnEvt($user);
2296  if ($result < 0) {
2297  $error++;
2298  }
2299  }
2300 
2301  // Call trigger
2302  if (!$error) {
2303  $result = $this->call_trigger('SHIPPING_CLOSED', $user);
2304  if ($result < 0) {
2305  $error++;
2306  }
2307  }
2308  } else {
2309  dol_print_error($this->db);
2310  $error++;
2311  }
2312 
2313  if (!$error) {
2314  $this->db->commit();
2315  return 1;
2316  } else {
2317  $this->statut = self::STATUS_VALIDATED;
2318  $this->status = self::STATUS_VALIDATED;
2319 
2320  $this->db->rollback();
2321  return -1;
2322  }
2323  }
2324 
2334  private function manageStockMvtOnEvt($user, $labelmovement = 'ShipmentClassifyClosedInDolibarr')
2335  {
2336  global $langs;
2337 
2338  $error = 0;
2339 
2340  require_once DOL_DOCUMENT_ROOT . '/product/stock/class/mouvementstock.class.php';
2341 
2342  $langs->load("agenda");
2343 
2344  // Loop on each product line to add a stock movement
2345  $sql = "SELECT cd.fk_product, cd.subprice,";
2346  $sql .= " ed.rowid, ed.qty, ed.fk_entrepot,";
2347  $sql .= " e.ref,";
2348  $sql .= " edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock,";
2349  $sql .= " cd.rowid as cdid, ed.rowid as edid";
2350  $sql .= " FROM " . MAIN_DB_PREFIX . "commandedet as cd,";
2351  $sql .= " " . MAIN_DB_PREFIX . "expeditiondet as ed";
2352  $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid";
2353  $sql .= " INNER JOIN " . MAIN_DB_PREFIX . "expedition as e ON ed.fk_expedition = e.rowid";
2354  $sql .= " WHERE ed.fk_expedition = " . ((int) $this->id);
2355  $sql .= " AND cd.rowid = ed.fk_elementdet";
2356 
2357  dol_syslog(get_class($this) . "::valid select details", LOG_DEBUG);
2358  $resql = $this->db->query($sql);
2359  if ($resql) {
2360  $cpt = $this->db->num_rows($resql);
2361  for ($i = 0; $i < $cpt; $i++) {
2362  $obj = $this->db->fetch_object($resql);
2363  if (empty($obj->edbrowid)) {
2364  $qty = $obj->qty;
2365  } else {
2366  $qty = $obj->edbqty;
2367  }
2368  if ($qty <= 0 || ($qty < 0 && !getDolGlobalInt('SHIPMENT_ALLOW_NEGATIVE_QTY'))) {
2369  continue;
2370  }
2371  dol_syslog(get_class($this) . "::valid movement index " . $i . " ed.rowid=" . $obj->rowid . " edb.rowid=" . $obj->edbrowid);
2372 
2373  $mouvS = new MouvementStock($this->db);
2374  $mouvS->origin = &$this;
2375  $mouvS->setOrigin($this->element, $this->id, $obj->cdid, $obj->edid);
2376 
2377  if (empty($obj->edbrowid)) {
2378  // line without batch detail
2379 
2380  // 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
2381  $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, $qty, $obj->subprice, $langs->trans($labelmovement, $obj->ref));
2382  if ($result < 0) {
2383  $this->error = $mouvS->error;
2384  $this->errors = $mouvS->errors;
2385  $error++;
2386  break;
2387  }
2388  } else {
2389  // line with batch detail
2390 
2391  // 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
2392  $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, $qty, $obj->subprice, $langs->trans($labelmovement, $obj->ref), '', $this->db->jdate($obj->eatby), $this->db->jdate($obj->sellby), $obj->batch, $obj->fk_origin_stock);
2393  if ($result < 0) {
2394  $this->error = $mouvS->error;
2395  $this->errors = $mouvS->errors;
2396  $error++;
2397  break;
2398  }
2399  }
2400 
2401  // If some stock lines are now 0, we can remove entry into llx_product_stock, but only if there is no child lines into llx_product_batch (detail of batch, because we can imagine
2402  // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot.
2403  $sqldelete = "DELETE FROM ".MAIN_DB_PREFIX."product_stock WHERE reel = 0 AND rowid NOT IN (SELECT fk_product_stock FROM ".MAIN_DB_PREFIX."product_batch as pb)";
2404  $resqldelete = $this->db->query($sqldelete);
2405  // We do not test error, it can fails if there is child in batch details
2406  }
2407  } else {
2408  $this->error = $this->db->lasterror();
2409  $this->errors[] = $this->db->lasterror();
2410  $error++;
2411  }
2412 
2413  return $error;
2414  }
2415 
2421  public function setBilled()
2422  {
2423  global $user;
2424  $error = 0;
2425 
2426  $this->db->begin();
2427 
2428  $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET billed = 1';
2429  $sql .= " WHERE rowid = ".((int) $this->id).' AND fk_statut > 0';
2430 
2431  $resql = $this->db->query($sql);
2432  if ($resql) {
2433  $this->billed = 1;
2434 
2435  // Call trigger
2436  $result = $this->call_trigger('SHIPPING_BILLED', $user);
2437  if ($result < 0) {
2438  $this->billed = 0;
2439  $error++;
2440  }
2441  } else {
2442  $error++;
2443  $this->errors[] = $this->db->lasterror;
2444  }
2445 
2446  if (empty($error)) {
2447  $this->db->commit();
2448  return 1;
2449  } else {
2450  $this->db->rollback();
2451  return -1;
2452  }
2453  }
2454 
2462  public function setDraft($user, $notrigger = 0)
2463  {
2464  // Protection
2465  if ($this->statut <= self::STATUS_DRAFT) {
2466  return 0;
2467  }
2468 
2469  return $this->setStatusCommon($user, self::STATUS_DRAFT, $notrigger, 'SHIPMENT_UNVALIDATE');
2470  }
2471 
2477  public function reOpen()
2478  {
2479  global $langs, $user;
2480 
2481  $error = 0;
2482 
2483  // Protection. This avoid to move stock later when we should not
2484  if ($this->statut == self::STATUS_VALIDATED) {
2485  return 0;
2486  }
2487 
2488  $this->db->begin();
2489 
2490  $oldbilled = $this->billed;
2491 
2492  $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET fk_statut = 1';
2493  $sql .= " WHERE rowid = ".((int) $this->id).' AND fk_statut > 0';
2494 
2495  $resql = $this->db->query($sql);
2496  if ($resql) {
2497  $this->statut = self::STATUS_VALIDATED;
2498  $this->status = self::STATUS_VALIDATED;
2499  $this->billed = 0;
2500 
2501  // If stock increment is done on closing
2502  if (!$error && isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
2503  require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
2504 
2505  $langs->load("agenda");
2506 
2507  // Loop on each product line to add a stock movement
2508  // TODO possibilite d'expedier a partir d'une propale ou autre origine
2509  $sql = "SELECT cd.fk_product, cd.subprice,";
2510  $sql .= " ed.rowid, ed.qty, ed.fk_entrepot,";
2511  $sql .= " edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock";
2512  $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,";
2513  $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed";
2514  $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid";
2515  $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
2516  $sql .= " AND cd.rowid = ed.fk_elementdet";
2517 
2518  dol_syslog(get_class($this)."::valid select details", LOG_DEBUG);
2519  $resql = $this->db->query($sql);
2520  if ($resql) {
2521  $cpt = $this->db->num_rows($resql);
2522  for ($i = 0; $i < $cpt; $i++) {
2523  $obj = $this->db->fetch_object($resql);
2524  if (empty($obj->edbrowid)) {
2525  $qty = $obj->qty;
2526  } else {
2527  $qty = $obj->edbqty;
2528  }
2529  if ($qty <= 0) {
2530  continue;
2531  }
2532  dol_syslog(get_class($this)."::reopen expedition movement index ".$i." ed.rowid=".$obj->rowid." edb.rowid=".$obj->edbrowid);
2533 
2534  //var_dump($this->lines[$i]);
2535  $mouvS = new MouvementStock($this->db);
2536  $mouvS->origin = &$this;
2537  $mouvS->setOrigin($this->element, $this->id);
2538 
2539  if (empty($obj->edbrowid)) {
2540  // line without batch detail
2541 
2542  // 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
2543  $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, -$qty, $obj->subprice, $langs->trans("ShipmentUnClassifyCloseddInDolibarr", $this->ref));
2544  if ($result < 0) {
2545  $this->error = $mouvS->error;
2546  $this->errors = $mouvS->errors;
2547  $error++;
2548  break;
2549  }
2550  } else {
2551  // line with batch detail
2552 
2553  // 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
2554  $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, -$qty, $obj->subprice, $langs->trans("ShipmentUnClassifyCloseddInDolibarr", $this->ref), '', $this->db->jdate($obj->eatby), $this->db->jdate($obj->sellby), $obj->batch, $obj->fk_origin_stock);
2555  if ($result < 0) {
2556  $this->error = $mouvS->error;
2557  $this->errors = $mouvS->errors;
2558  $error++;
2559  break;
2560  }
2561  }
2562  }
2563  } else {
2564  $this->error = $this->db->lasterror();
2565  $error++;
2566  }
2567  }
2568 
2569  if (!$error) {
2570  // Call trigger
2571  $result = $this->call_trigger('SHIPPING_REOPEN', $user);
2572  if ($result < 0) {
2573  $error++;
2574  }
2575  }
2576  } else {
2577  $error++;
2578  $this->errors[] = $this->db->lasterror();
2579  }
2580 
2581  if (!$error) {
2582  $this->db->commit();
2583  return 1;
2584  } else {
2585  $this->statut = self::STATUS_CLOSED;
2586  $this->status = self::STATUS_CLOSED;
2587  $this->billed = $oldbilled;
2588  $this->db->rollback();
2589  return -1;
2590  }
2591  }
2592 
2604  public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null)
2605  {
2606  $outputlangs->load("products");
2607 
2608  if (!dol_strlen($modele)) {
2609  $modele = 'rouget';
2610 
2611  if (!empty($this->model_pdf)) {
2612  $modele = $this->model_pdf;
2613  } elseif (getDolGlobalString('EXPEDITION_ADDON_PDF')) {
2614  $modele = getDolGlobalString('EXPEDITION_ADDON_PDF');
2615  }
2616  }
2617 
2618  $modelpath = "core/modules/expedition/doc/";
2619 
2620  $this->fetch_origin();
2621 
2622  return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
2623  }
2624 
2633  public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
2634  {
2635  $tables = array(
2636  'expedition'
2637  );
2638 
2639  return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
2640  }
2641 }
2642 
2643 
2648 {
2652  public $element = 'expeditiondet';
2653 
2657  public $table_element = 'expeditiondet';
2658 
2659 
2666  public $line_id; // deprecated
2667 
2671  public $fk_element;
2672 
2676  public $origin_id;
2677 
2681  public $fk_elementdet;
2682 
2686  public $origin_line_id;
2687 
2691  public $element_type;
2692 
2693 
2700  public $fk_origin; // Example: 'orderline'
2701 
2705  public $fk_expedition;
2706 
2710  public $db;
2711 
2715  public $qty;
2716 
2720  public $qty_shipped;
2721 
2725  public $fk_product;
2726 
2727  // detail of lot and qty = array(id in llx_expeditiondet_batch, fk_expeditiondet, batch, qty, fk_origin_stock)
2728  // We can use this to know warehouse planned to be used for each lot.
2729  public $detail_batch;
2730 
2731  // detail of warehouses and qty
2732  // We can use this to know warehouse when there is no lot.
2733  public $details_entrepot;
2734 
2735 
2739  public $entrepot_id;
2740 
2741 
2745  public $qty_asked;
2746 
2751  public $ref;
2752 
2756  public $product_ref;
2757 
2762  public $libelle;
2763 
2767  public $product_label;
2768 
2774  public $desc;
2775 
2779  public $product_desc;
2780 
2785  public $product_type = 0;
2786 
2790  public $rang;
2791 
2795  public $weight;
2796  public $weight_units;
2797 
2801  public $length;
2802  public $length_units;
2803 
2807  public $width;
2808  public $width_units;
2809 
2813  public $height;
2814  public $height_units;
2815 
2819  public $surface;
2820  public $surface_units;
2821 
2825  public $volume;
2826  public $volume_units;
2827 
2828  // Invoicing
2829  public $remise_percent;
2830  public $tva_tx;
2831 
2835  public $total_ht;
2836 
2840  public $total_ttc;
2841 
2845  public $total_tva;
2846 
2850  public $total_localtax1;
2851 
2855  public $total_localtax2;
2856 
2857 
2863  public function __construct($db)
2864  {
2865  $this->db = $db;
2866  }
2867 
2874  public function fetch($rowid)
2875  {
2876  $sql = 'SELECT ed.rowid, ed.fk_expedition, ed.fk_entrepot, ed.fk_elementdet, ed.element_type, ed.qty, ed.rang';
2877  $sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element.' as ed';
2878  $sql .= ' WHERE ed.rowid = '.((int) $rowid);
2879  $result = $this->db->query($sql);
2880  if ($result) {
2881  $objp = $this->db->fetch_object($result);
2882  $this->id = $objp->rowid;
2883  $this->fk_expedition = $objp->fk_expedition;
2884  $this->entrepot_id = $objp->fk_entrepot;
2885  $this->fk_elementdet = $objp->fk_elementdet;
2886  $this->element_type = $objp->element_type;
2887  $this->qty = $objp->qty;
2888  $this->rang = $objp->rang;
2889 
2890  $this->db->free($result);
2891 
2892  return 1;
2893  } else {
2894  $this->errors[] = $this->db->lasterror();
2895  $this->error = $this->db->lasterror();
2896  return -1;
2897  }
2898  }
2899 
2907  public function insert($user, $notrigger = 0)
2908  {
2909  $error = 0;
2910 
2911  // Check parameters
2912  if (empty($this->fk_expedition) || empty($this->fk_elementdet) || !is_numeric($this->qty)) {
2913  $this->error = 'ErrorMandatoryParametersNotProvided';
2914  return -1;
2915  }
2916 
2917  $this->db->begin();
2918 
2919  if (empty($this->rang)) {
2920  $this->rang = 0;
2921  }
2922 
2923  // Rank to use
2924  $ranktouse = $this->rang;
2925  if ($ranktouse == -1) {
2926  $rangmax = $this->line_max($this->fk_expedition);
2927  $ranktouse = $rangmax + 1;
2928  }
2929 
2930  $sql = "INSERT INTO ".MAIN_DB_PREFIX."expeditiondet (";
2931  $sql .= "fk_expedition";
2932  $sql .= ", fk_entrepot";
2933  $sql .= ", fk_elementdet";
2934  $sql .= ", element_type";
2935  $sql .= ", qty";
2936  $sql .= ", rang";
2937  $sql .= ") VALUES (";
2938  $sql .= $this->fk_expedition;
2939  $sql .= ", ".(empty($this->entrepot_id) ? 'NULL' : $this->entrepot_id);
2940  $sql .= ", ".((int) $this->fk_elementdet);
2941  $sql .= ", '".(empty($this->element_type) ? 'order' : $this->db->escape($this->element_type))."'";
2942  $sql .= ", ".price2num($this->qty, 'MS');
2943  $sql .= ", ".((int) $ranktouse);
2944  $sql .= ")";
2945 
2946  dol_syslog(get_class($this)."::insert", LOG_DEBUG);
2947  $resql = $this->db->query($sql);
2948  if ($resql) {
2949  $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX."expeditiondet");
2950 
2951  if (!$error) {
2952  $result = $this->insertExtraFields();
2953  if ($result < 0) {
2954  $error++;
2955  }
2956  }
2957 
2958  if (!$error && !$notrigger) {
2959  // Call trigger
2960  $result = $this->call_trigger('LINESHIPPING_INSERT', $user);
2961  if ($result < 0) {
2962  $error++;
2963  }
2964  // End call triggers
2965  }
2966 
2967  if ($error) {
2968  foreach ($this->errors as $errmsg) {
2969  dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
2970  $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
2971  }
2972  }
2973  } else {
2974  $error++;
2975  }
2976 
2977  if ($error) {
2978  $this->db->rollback();
2979  return -1;
2980  } else {
2981  $this->db->commit();
2982  return $this->id;
2983  }
2984  }
2985 
2993  public function delete($user = null, $notrigger = 0)
2994  {
2995  $error = 0;
2996 
2997  $this->db->begin();
2998 
2999  // delete batch expedition line
3000  if (isModEnabled('productbatch')) {
3001  $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet_batch";
3002  $sql .= " WHERE fk_expeditiondet = ".((int) $this->id);
3003 
3004  if (!$this->db->query($sql)) {
3005  $this->errors[] = $this->db->lasterror()." - sql=$sql";
3006  $error++;
3007  }
3008  }
3009 
3010  $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet";
3011  $sql .= " WHERE rowid = ".((int) $this->id);
3012 
3013  if (!$error && $this->db->query($sql)) {
3014  // Remove extrafields
3015  if (!$error) {
3016  $result = $this->deleteExtraFields();
3017  if ($result < 0) {
3018  $this->errors[] = $this->error;
3019  $error++;
3020  }
3021  }
3022  if (!$error && !$notrigger) {
3023  // Call trigger
3024  $result = $this->call_trigger('LINESHIPPING_DELETE', $user);
3025  if ($result < 0) {
3026  $this->errors[] = $this->error;
3027  $error++;
3028  }
3029  // End call triggers
3030  }
3031  } else {
3032  $this->errors[] = $this->db->lasterror()." - sql=$sql";
3033  $error++;
3034  }
3035 
3036  if (!$error) {
3037  $this->db->commit();
3038  return 1;
3039  } else {
3040  foreach ($this->errors as $errmsg) {
3041  dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
3042  $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
3043  }
3044  $this->db->rollback();
3045  return -1 * $error;
3046  }
3047  }
3048 
3056  public function update($user = null, $notrigger = 0)
3057  {
3058  $error = 0;
3059 
3060  dol_syslog(get_class($this)."::update id=$this->id, entrepot_id=$this->entrepot_id, product_id=$this->fk_product, qty=$this->qty");
3061 
3062  $this->db->begin();
3063 
3064  // Clean parameters
3065  if (empty($this->qty)) {
3066  $this->qty = 0;
3067  }
3068  $qty = price2num($this->qty);
3069  $remainingQty = 0;
3070  $batch = null;
3071  $batch_id = null;
3072  $expedition_batch_id = null;
3073  if (is_array($this->detail_batch)) { // array of ExpeditionLineBatch
3074  if (count($this->detail_batch) > 1) {
3075  dol_syslog(get_class($this).'::update only possible for one batch', LOG_ERR);
3076  $this->errors[] = 'ErrorBadParameters';
3077  $error++;
3078  } else {
3079  $batch = $this->detail_batch[0]->batch;
3080  $batch_id = $this->detail_batch[0]->fk_origin_stock;
3081  $expedition_batch_id = $this->detail_batch[0]->id;
3082  if ($this->entrepot_id != $this->detail_batch[0]->entrepot_id) {
3083  dol_syslog(get_class($this).'::update only possible for batch of same warehouse', LOG_ERR);
3084  $this->errors[] = 'ErrorBadParameters';
3085  $error++;
3086  }
3087  $qty = price2num($this->detail_batch[0]->qty);
3088  }
3089  } elseif (!empty($this->detail_batch)) {
3090  $batch = $this->detail_batch->batch;
3091  $batch_id = $this->detail_batch->fk_origin_stock;
3092  $expedition_batch_id = $this->detail_batch->id;
3093  if ($this->entrepot_id != $this->detail_batch->entrepot_id) {
3094  dol_syslog(get_class($this).'::update only possible for batch of same warehouse', LOG_ERR);
3095  $this->errors[] = 'ErrorBadParameters';
3096  $error++;
3097  }
3098  $qty = price2num($this->detail_batch->qty);
3099  }
3100 
3101  // check parameters
3102  if (!isset($this->id) || !isset($this->entrepot_id)) {
3103  dol_syslog(get_class($this).'::update missing line id and/or warehouse id', LOG_ERR);
3104  $this->errors[] = 'ErrorMandatoryParametersNotProvided';
3105  $error++;
3106  return -1;
3107  }
3108 
3109  // update lot
3110 
3111  if (!empty($batch) && isModEnabled('productbatch')) {
3112  $batch_id_str = $batch_id ?? 'null';
3113  dol_syslog(get_class($this)."::update expedition batch id=$expedition_batch_id, batch_id=$batch_id_str, batch=$batch");
3114 
3115  if (empty($batch_id) || empty($this->fk_product)) {
3116  dol_syslog(get_class($this).'::update missing fk_origin_stock (batch_id) and/or fk_product', LOG_ERR);
3117  $this->errors[] = 'ErrorMandatoryParametersNotProvided';
3118  $error++;
3119  }
3120 
3121  // fetch remaining lot qty
3122  $shipmentlinebatch = new ExpeditionLineBatch($this->db);
3123 
3124  if (!$error && ($lotArray = $shipmentlinebatch->fetchAll($this->id)) < 0) {
3125  $this->errors[] = $this->db->lasterror()." - ExpeditionLineBatch::fetchAll";
3126  $error++;
3127  } else {
3128  // calculate new total line qty
3129  foreach ($lotArray as $lot) {
3130  if ($expedition_batch_id != $lot->id) {
3131  $remainingQty += $lot->qty;
3132  }
3133  }
3134  $qty += $remainingQty;
3135 
3136  //fetch lot details
3137 
3138  // fetch from product_lot
3139  require_once DOL_DOCUMENT_ROOT.'/product/stock/class/productlot.class.php';
3140  $lot = new Productlot($this->db);
3141  if ($lot->fetch(0, $this->fk_product, $batch) < 0) {
3142  $this->errors[] = $lot->errors;
3143  $error++;
3144  }
3145  if (!$error && !empty($expedition_batch_id)) {
3146  // delete lot expedition line
3147  $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet_batch";
3148  $sql .= " WHERE fk_expeditiondet = ".((int) $this->id);
3149  $sql .= " AND rowid = ".((int) $expedition_batch_id);
3150 
3151  if (!$this->db->query($sql)) {
3152  $this->errors[] = $this->db->lasterror()." - sql=$sql";
3153  $error++;
3154  }
3155  }
3156  if (!$error && $this->detail_batch->qty > 0) {
3157  // create lot expedition line
3158  if (isset($lot->id)) {
3159  $shipmentLot = new ExpeditionLineBatch($this->db);
3160  $shipmentLot->batch = $lot->batch;
3161  $shipmentLot->eatby = $lot->eatby;
3162  $shipmentLot->sellby = $lot->sellby;
3163  $shipmentLot->entrepot_id = $this->detail_batch->entrepot_id;
3164  $shipmentLot->qty = $this->detail_batch->qty;
3165  $shipmentLot->fk_origin_stock = $batch_id;
3166  if ($shipmentLot->create($this->id) < 0) {
3167  $this->errors = $shipmentLot->errors;
3168  $error++;
3169  }
3170  }
3171  }
3172  }
3173  }
3174  if (!$error) {
3175  // update line
3176  $sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
3177  $sql .= " fk_entrepot = ".($this->entrepot_id > 0 ? $this->entrepot_id : 'null');
3178  $sql .= " , qty = ".((float) price2num($qty, 'MS'));
3179  $sql .= " WHERE rowid = ".((int) $this->id);
3180 
3181  if (!$this->db->query($sql)) {
3182  $this->errors[] = $this->db->lasterror()." - sql=$sql";
3183  $error++;
3184  }
3185  }
3186 
3187  if (!$error) {
3188  $result = $this->insertExtraFields();
3189  if ($result < 0) {
3190  $this->errors[] = $this->error;
3191  $error++;
3192  }
3193  }
3194 
3195  if (!$error && !$notrigger) {
3196  // Call trigger
3197  $result = $this->call_trigger('LINESHIPPING_MODIFY', $user);
3198  if ($result < 0) {
3199  $this->errors[] = $this->error;
3200  $error++;
3201  }
3202  // End call triggers
3203  }
3204  if (!$error) {
3205  $this->db->commit();
3206  return 1;
3207  } else {
3208  foreach ($this->errors as $errmsg) {
3209  dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR);
3210  $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
3211  }
3212  $this->db->rollback();
3213  return -1 * $error;
3214  }
3215  }
3216 }
print $langs trans("AuditedSecurityEvents").'</strong >< span class="opacitymedium"></span >< br > status
Or an array listing all the potential status of the object: array: int of the status => translated la...
Definition: security.php:607
$object ref
Definition: info.php:79
Class to manage customers orders.
const STATUS_SHIPMENTONPROCESS
Shipment on process.
const STATUS_VALIDATED
Validated status.
Parent class of all other business classes (invoices, contracts, proposals, orders,...
fetch_optionals($rowid=null, $optionsArray=null)
Function to get extra fields of an object into $this->array_options This method is in most cases call...
deleteEcmFiles($mode=0)
Delete related files of object in database.
add_object_linked($origin=null, $origin_id=null, $f_user=null, $notrigger=0)
Add an object link into llx_element_element.
commonGenerateDocument($modelspath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams=null)
Common function for all objects extending CommonObject for generating documents.
fetch_thirdparty($force_thirdparty_id=0)
Load the third party of object, from id $this->socid or $this->fk_soc, into this->thirdparty.
deleteObjectLinked($sourceid=null, $sourcetype='', $targetid=null, $targettype='', $rowid=0, $f_user=null, $notrigger=0)
Delete all links between an object $this.
setErrorsFromObject($object)
setErrorsFromObject
setStatut($status, $elementId=null, $elementType='', $trigkey='', $fieldstatus='fk_statut')
Set status of an object.
fetch_origin()
Read linked origin object.
deleteExtraFields()
Delete all extra fields values for the current object.
setStatusCommon($user, $status, $notrigger=0, $triggercode='')
Set to a status.
static commonReplaceThirdparty(DoliDB $dbs, $origin_id, $dest_id, array $tables, $ignoreerrors=0)
Function used to replace a thirdparty id with another one.
fetchObjectLinked($sourceid=null, $sourcetype='', $targetid=null, $targettype='', $clause='OR', $alsosametype=1, $orderby='sourcetype', $loadalsoobjects=1)
Fetch array of objects linked to current object (object of enabled modules only).
line_max($fk_parent_line=0)
Get max value used for position of line (rang)
insertExtraFields($trigger='', $userused=null)
Add/Update all extra fields values for the current object.
call_trigger($triggerName, $user)
Call trigger based on this instance.
Parent class for class inheritance lines of business objects This class is useless for the moment so ...
Class to manage receptions.
Class to manage Dolibarr database access.
Class to manage shipments.
getNomUrl($withpicto=0, $option='', $max=0, $short=0, $notooltip=0, $save_lastsearch_value=-1)
Return clicable link of object (with eventually picto)
create_delivery($user)
Create a delivery receipt from a shipment.
const STATUS_NO_SIGNATURE
No signature.
setDraft($user, $notrigger=0)
Set draft status.
const STATUS_SHIPMENT_IN_PROGRESS
Expedition in progress -> package exit the warehouse and is now in the truck or into the hand of the ...
getUrlTrackingStatus($value='')
Forge an set tracking url.
setClosed()
Classify the shipping as closed (this records also the stock movement)
__construct($db)
Constructor.
fetch_lines()
Load lines.
create($user, $notrigger=0)
Create expedition en base.
LibStatut($status, $mode)
Return label of a status.
setBilled()
Classify the shipping as invoiced (used for example by trigger when WORKFLOW_SHIPPING_CLASSIFY_BILLED...
addline($entrepot_id, $id, $qty, $array_options=[])
Add an expedition line.
generateDocument($modele, $outputlangs, $hidedetails=0, $hidedesc=0, $hideref=0, $moreparams=null)
Create a document onto disk according to template module.
getTooltipContentArray($params)
getTooltipContentArray
setDeliveryDate($user, $delivery_date)
Set the planned delivery date.
create_line($entrepot_id, $origin_line_id, $qty, $rang=0, $array_options=[])
Create a expedition line.
getKanbanView($option='', $arraydata=null)
Return clicable link of object (with eventually picto)
const STATUS_DRAFT
Draft status.
const STATUS_CANCELED
Canceled status.
getLibStatut($mode=0)
Return status label.
set_date_livraison($user, $delivery_date)
Set delivery date.
initAsSpecimen()
Initialise an instance with random values.
addline_batch($dbatch, $array_options=[])
Add a shipment line with batch record.
getNextNumRef($soc)
Return next expedition ref.
const STATUS_CLOSED
Closed status -> parcel was received by customer / end of process prev status : validated or shipment...
const STATUS_VALIDATED
Validated status -> parcel is ready to be sent prev status : draft next status : closed or shipment_i...
create_line_batch($line_ext, $array_options=[])
Create the detail of the expedition line.
manageStockMvtOnEvt($user, $labelmovement='ShipmentClassifyClosedInDolibarr')
Manage Stock MVt onb Close or valid Shipment.
valid($user, $notrigger=0)
Validate object and update stock if option enabled.
const STATUS_SIGNED
Signed status.
update($user=null, $notrigger=0)
Update database.
cancel($notrigger=0, $also_update_stock=false)
Cancel shipment.
fetch_delivery_methods()
Fetch deliveries method and return an array.
fetch($id, $ref='', $ref_ext='', $notused='')
Get object and lines from database.
reOpen()
Classify the shipping as validated/opened.
deleteLine($user, $lineid)
Delete detail line.
static replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
Function used to replace a thirdparty id with another one.
list_delivery_methods($id=0)
Fetch all deliveries method and return an array.
Class to manage lines of shipment.
fetch($rowid)
Load line expedition.
__construct($db)
Constructor.
insert($user, $notrigger=0)
Insert line into database.
update($user=null, $notrigger=0)
Update a line in database.
CRUD class for batch number management within shipment.
Class to manage stock movements.
Class to manage order lines.
Class to manage products or services.
const TYPE_SERVICE
Service.
Manage record for batch number management.
Class with list of lots and properties.
Class to manage third parties objects (customers, suppliers, prospects...)
trait CommonIncoterm
Superclass for incoterm classes.
if(isModEnabled('invoice') && $user->hasRight('facture', 'lire')) if((isModEnabled('fournisseur') &&!getDolGlobalString('MAIN_USE_NEW_SUPPLIERMOD') && $user->hasRight("fournisseur", "facture", "lire"))||(isModEnabled('supplier_invoice') && $user->hasRight("supplier_invoice", "lire"))) if(isModEnabled('don') && $user->hasRight('don', 'lire')) if(isModEnabled('tax') && $user->hasRight('tax', 'charges', 'lire')) if(isModEnabled('invoice') &&isModEnabled('order') && $user->hasRight("commande", "lire") &&!getDolGlobalString('WORKFLOW_DISABLE_CREATE_INVOICE_FROM_ORDER')) $sql
Social contributions to pay.
Definition: index.php:745
dol_delete_dir_recursive($dir, $count=0, $nophperrors=0, $onlysub=0, &$countdeleted=0, $indexdatabase=1, $nolog=0)
Remove a directory $dir and its subdirectories (or only files and subdirectories)
Definition: files.lib.php:1609
dol_delete_file($file, $disableglob=0, $nophperrors=0, $nohook=0, $object=null, $allowdotdot=false, $indexdatabase=1, $nolog=0)
Remove a file or several files with a mask.
Definition: files.lib.php:1458
dol_dir_list($utf8_path, $types="all", $recursive=0, $filter="", $excludefilter=null, $sortcriteria="name", $sortorder=SORT_ASC, $mode=0, $nohook=0, $relativename="", $donotfollowsymlinks=0, $nbsecondsold=0)
Scan a directory and return a list of files/directories.
Definition: files.lib.php:63
img_object($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0)
Show a picto called object_picto (generic function)
img_picto($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $alt='', $morecss='', $marginleftonlyshort=2)
Show picto whatever it's its name (generic function)
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
dol_strlen($string, $stringencoding='UTF-8')
Make a strlen call.
dol_now($mode='auto')
Return date for now.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dolGetStatus($statusLabel='', $statusLabelShort='', $html='', $statusType='status0', $displayMode=0, $url='', $params=array())
Output the badge of a status.
dol_buildpath($path, $type=0, $returnemptyifnotfound=0)
Return path of url or filesystem.
dol_sanitizeFileName($str, $newstr='_', $unaccent=1)
Clean a string to use it as a file name.
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
getDolGlobalString($key, $default='')
Return dolibarr global constant string value.
isModEnabled($module)
Is Dolibarr module enabled.
get_localtax($vatrate, $local, $thirdparty_buyer=null, $thirdparty_seller=null, $vatnpr=0)
Return localtax rate for a particular vat, when selling a product with vat $vatrate,...
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
calcul_price_total($qty, $pu, $remise_percent_ligne, $txtva, $uselocaltax1_rate, $uselocaltax2_rate, $remise_percent_global, $price_base_type, $info_bits, $type, $seller='', $localtaxes_array=[], $progress=100, $multicurrency_tx=1, $pu_devise=0, $multicurrency_code='')
Calculate totals (net, vat, ...) of a line.
Definition: price.lib.php:88
$conf db user
Active Directory does not allow anonymous connections.
Definition: repair.php:127