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