dolibarr  17.0.4
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 }
$object ref
Definition: info.php:78
Class to manage customers orders.
const STATUS_SHIPMENTONPROCESS
Shipment on process.
const STATUS_VALIDATED
Validated status.
Parent class of all other business classes (invoices, contracts, proposals, orders,...
fetch_optionals($rowid=null, $optionsArray=null)
Function to get extra fields of an object into $this->array_options This method is in most cases call...
deleteEcmFiles($mode=0)
Delete related files of object in database.
add_object_linked($origin=null, $origin_id=null, $f_user=null, $notrigger=0)
Add an object link into llx_element_element.
commonGenerateDocument($modelspath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams=null)
Common function for all objects extending CommonObject for generating documents.
fetch_thirdparty($force_thirdparty_id=0)
Load the third party of object, from id $this->socid or $this->fk_soc, into this->thirdparty.
deleteObjectLinked($sourceid=null, $sourcetype='', $targetid=null, $targettype='', $rowid='', $f_user=null, $notrigger=0)
Delete all links between an object $this.
setStatut($status, $elementId=null, $elementType='', $trigkey='', $fieldstatus='fk_statut')
Set status of an object.
deleteExtraFields()
Delete all extra fields values for the current object.
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).
static commonReplaceThirdparty(DoliDB $db, $origin_id, $dest_id, array $tables, $ignoreerrors=0)
Function used to replace a thirdparty id with another one.
line_max($fk_parent_line=0)
Get max value used for position of line (rang)
fetch_origin()
Read linked origin object.
insertExtraFields($trigger='', $userused=null)
Add/Update all extra fields values for the current object.
call_trigger($triggerName, $user)
Call trigger based on this instance.
Parent class for class inheritance lines of business objects This class is useless for the moment so ...
Class to manage receptions.
Class to manage Dolibarr database access.
Class to manage shipments.
getNomUrl($withpicto=0, $option='', $max=0, $short=0, $notooltip=0, $save_lastsearch_value=-1)
Return clicable link of object (with eventually picto)
create_delivery($user)
Create a delivery receipt from a shipment.
deleteline($user, $lineid)
Delete detail line.
getUrlTrackingStatus($value='')
Forge an set tracking url.
setClosed()
Classify the shipping as closed.
__construct($db)
Constructor.
fetch_lines()
Load lines.
list_delivery_methods($id='')
Fetch all deliveries method and return an array.
create($user, $notrigger=0)
Create expedition en base.
LibStatut($status, $mode)
Return label of a status.
setBilled()
Classify the shipping as invoiced (used when WORKFLOW_BILL_ON_SHIPMENT is on)
addline_batch($dbatch, $array_options=0)
Add a shipment line with batch record.
generateDocument($modele, $outputlangs, $hidedetails=0, $hidedesc=0, $hideref=0, $moreparams=null)
Create a document onto disk according to template module.
setDeliveryDate($user, $delivery_date)
Set the planned delivery date.
const STATUS_DRAFT
Draft status.
const STATUS_CANCELED
Canceled status.
getLibStatut($mode=0)
Return status label.
set_date_livraison($user, $delivery_date)
Set delivery date.
initAsSpecimen()
Initialise an instance with random values.
getNextNumRef($soc)
Return next expedition ref.
const STATUS_CLOSED
Closed status.
const STATUS_VALIDATED
Validated status.
create_line_batch($line_ext, $array_options=0)
Create the detail of the expedition line.
valid($user, $notrigger=0)
Validate object and update stock if option enabled.
update($user=null, $notrigger=0)
Update database.
cancel($notrigger=0, $also_update_stock=false)
Cancel shipment.
fetch_delivery_methods()
Fetch deliveries method and return an array.
addline($entrepot_id, $id, $qty, $array_options=0)
Add an expedition line.
fetch($id, $ref='', $ref_ext='', $notused='')
Get object and lines from database.
reOpen()
Classify the shipping as validated/opened.
static replaceThirdparty(DoliDB $db, $origin_id, $dest_id)
Function used to replace a thirdparty id with another one.
create_line($entrepot_id, $origin_line_id, $qty, $rang=0, $array_options=null)
Create a expedition line.
Classe to manage lines of shipment.
fetch($rowid)
Load line expedition.
__construct($db)
Constructor.
insert($user, $notrigger=0)
Insert line into database.
update($user=null, $notrigger=0)
Update a line in database.
CRUD class for batch number management within shipment.
Class to manage stock movements.
Class to manage order lines.
Class to manage products or services.
Manage record for batch number management.
Class with list of lots and properties.
Class to manage third parties objects (customers, suppliers, prospects...)
trait CommonIncoterm
Superclass for incoterm classes.
if(isModEnabled('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
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
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
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
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
dol_print_error($db='', $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
img_object($titlealt, $picto, $moreatt='', $pictoisfullpath=false, $srconly=0, $notitle=0)
Show a picto called object_picto (generic function)
dol_strlen($string, $stringencoding='UTF-8')
Make a strlen call.
dol_now($mode='auto')
Return date for now.
getDolGlobalInt($key, $default=0)
Return dolibarr global constant int value.
dolGetStatus($statusLabel='', $statusLabelShort='', $html='', $statusType='status0', $displayMode=0, $url='', $params=array())
Output the badge of a status.
dol_buildpath($path, $type=0, $returnemptyifnotfound=0)
Return path of url or filesystem.
get_localtax($vatrate, $local, $thirdparty_buyer="", $thirdparty_seller="", $vatnpr=0)
Return localtax rate for a particular vat, when selling a product with vat $vatrate,...
dol_sanitizeFileName($str, $newstr='_', $unaccent=1)
Clean a string to use it as a file name.
isModEnabled($module)
Is Dolibarr module enabled.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
calcul_price_total($qty, $pu, $remise_percent_ligne, $txtva, $uselocaltax1_rate, $uselocaltax2_rate, $remise_percent_global, $price_base_type, $info_bits, $type, $seller='', $localtaxes_array='', $progress=100, $multicurrency_tx=1, $pu_devise=0, $multicurrency_code='')
Calculate totals (net, vat, ...) of a line.
Definition: price.lib.php:86
$conf db user
Definition: repair.php:123
$conf db
API class for accounts.
Definition: inc.php:41