dolibarr  17.0.2
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  $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1620 
1621  while ($i < $num) {
1622  $obj = $this->db->fetch_object($resql);
1623 
1624  if ($originline > 0 && $originline == $obj->fk_origin_line) {
1625  $line->entrepot_id = 0; // entrepod_id in details_entrepot
1626  $line->qty_shipped += $obj->qty_shipped;
1627  } else {
1628  $line = new ExpeditionLigne($this->db);
1629  $line->entrepot_id = $obj->fk_entrepot; // this is a property of a shipment line
1630  $line->qty_shipped = $obj->qty_shipped; // this is a property of a shipment line
1631  }
1632 
1633  $detail_entrepot = new stdClass();
1634  $detail_entrepot->entrepot_id = $obj->fk_entrepot;
1635  $detail_entrepot->qty_shipped = $obj->qty_shipped;
1636  $detail_entrepot->line_id = $obj->line_id;
1637  $line->details_entrepot[] = $detail_entrepot;
1638 
1639  $line->line_id = $obj->line_id;
1640  $line->rowid = $obj->line_id; // TODO deprecated
1641  $line->id = $obj->line_id;
1642 
1643  $line->fk_origin = 'orderline';
1644  $line->fk_origin_line = $obj->fk_origin_line;
1645  $line->origin_line_id = $obj->fk_origin_line; // TODO deprecated
1646 
1647  $line->fk_expedition = $this->id; // id of parent
1648 
1649  $line->product_type = $obj->product_type;
1650  $line->fk_product = $obj->fk_product;
1651  $line->fk_product_type = $obj->fk_product_type;
1652  $line->ref = $obj->product_ref; // TODO deprecated
1653  $line->product_ref = $obj->product_ref;
1654  $line->product_label = $obj->product_label;
1655  $line->libelle = $obj->product_label; // TODO deprecated
1656  $line->product_tosell = $obj->product_tosell;
1657  $line->product_tobuy = $obj->product_tobuy;
1658  $line->product_tobatch = $obj->product_tobatch;
1659  $line->label = $obj->custom_label;
1660  $line->description = $obj->description;
1661  $line->qty_asked = $obj->qty_asked;
1662  $line->rang = $obj->rang;
1663  $line->weight = $obj->weight;
1664  $line->weight_units = $obj->weight_units;
1665  $line->length = $obj->length;
1666  $line->length_units = $obj->length_units;
1667  $line->surface = $obj->surface;
1668  $line->surface_units = $obj->surface_units;
1669  $line->volume = $obj->volume;
1670  $line->volume_units = $obj->volume_units;
1671  $line->fk_unit = $obj->fk_unit;
1672 
1673  $line->pa_ht = $obj->pa_ht;
1674 
1675  // Local taxes
1676  $localtax_array = array(0=>$obj->localtax1_type, 1=>$obj->localtax1_tx, 2=>$obj->localtax2_type, 3=>$obj->localtax2_tx);
1677  $localtax1_tx = get_localtax($obj->tva_tx, 1, $this->thirdparty);
1678  $localtax2_tx = get_localtax($obj->tva_tx, 2, $this->thirdparty);
1679 
1680  // For invoicing
1681  $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
1682  $line->desc = $obj->description; // We need ->desc because some code into CommonObject use desc (property defined for other elements)
1683  $line->qty = $line->qty_shipped;
1684  $line->total_ht = $tabprice[0];
1685  $line->total_localtax1 = $tabprice[9];
1686  $line->total_localtax2 = $tabprice[10];
1687  $line->total_ttc = $tabprice[2];
1688  $line->total_tva = $tabprice[1];
1689  $line->vat_src_code = $obj->vat_src_code;
1690  $line->tva_tx = $obj->tva_tx;
1691  $line->localtax1_tx = $obj->localtax1_tx;
1692  $line->localtax2_tx = $obj->localtax2_tx;
1693  $line->info_bits = $obj->info_bits;
1694  $line->price = $obj->price;
1695  $line->subprice = $obj->subprice;
1696  $line->remise_percent = $obj->remise_percent;
1697 
1698  $this->total_ht += $tabprice[0];
1699  $this->total_tva += $tabprice[1];
1700  $this->total_ttc += $tabprice[2];
1701  $this->total_localtax1 += $tabprice[9];
1702  $this->total_localtax2 += $tabprice[10];
1703 
1704  // Multicurrency
1705  $this->fk_multicurrency = $obj->fk_multicurrency;
1706  $this->multicurrency_code = $obj->multicurrency_code;
1707  $this->multicurrency_subprice = $obj->multicurrency_subprice;
1708  $this->multicurrency_total_ht = $obj->multicurrency_total_ht;
1709  $this->multicurrency_total_tva = $obj->multicurrency_total_tva;
1710  $this->multicurrency_total_ttc = $obj->multicurrency_total_ttc;
1711 
1712  if ($originline != $obj->fk_origin_line) {
1713  $line->detail_batch = array();
1714  }
1715 
1716  // Detail of batch
1717  if (isModEnabled('productbatch') && $obj->line_id > 0 && $obj->product_tobatch > 0) {
1718  $newdetailbatch = $shipmentlinebatch->fetchAll($obj->line_id, $obj->fk_product);
1719 
1720  if (is_array($newdetailbatch)) {
1721  if ($originline != $obj->fk_origin_line) {
1722  $line->detail_batch = $newdetailbatch;
1723  } else {
1724  $line->detail_batch = array_merge($line->detail_batch, $newdetailbatch);
1725  }
1726  }
1727  }
1728 
1729  $line->fetch_optionals();
1730 
1731  if ($originline != $obj->fk_origin_line) {
1732  $this->lines[$lineindex] = $line;
1733  $lineindex++;
1734  } else {
1735  $line->total_ht += $tabprice[0];
1736  $line->total_localtax1 += $tabprice[9];
1737  $line->total_localtax2 += $tabprice[10];
1738  $line->total_ttc += $tabprice[2];
1739  $line->total_tva += $tabprice[1];
1740  }
1741 
1742  $i++;
1743  $originline = $obj->fk_origin_line;
1744  }
1745  $this->db->free($resql);
1746  return 1;
1747  } else {
1748  $this->error = $this->db->error();
1749  return -3;
1750  }
1751  }
1752 
1760  public function deleteline($user, $lineid)
1761  {
1762  global $user;
1763 
1764  if ($this->statut == self::STATUS_DRAFT) {
1765  $this->db->begin();
1766 
1767  $line = new ExpeditionLigne($this->db);
1768 
1769  // For triggers
1770  $line->fetch($lineid);
1771 
1772  if ($line->delete($user) > 0) {
1773  //$this->update_price(1);
1774 
1775  $this->db->commit();
1776  return 1;
1777  } else {
1778  $this->db->rollback();
1779  return -1;
1780  }
1781  } else {
1782  $this->error = 'ErrorDeleteLineNotAllowedByObjectStatus';
1783  return -2;
1784  }
1785  }
1786 
1787 
1799  public function getNomUrl($withpicto = 0, $option = '', $max = 0, $short = 0, $notooltip = 0, $save_lastsearch_value = -1)
1800  {
1801  global $langs, $conf, $hookmanager;
1802 
1803  $result = '';
1804  $label = '<u>'.$langs->trans("Shipment").'</u>';
1805  $label .= '<br><b>'.$langs->trans('Ref').':</b> '.$this->ref;
1806  $label .= '<br><b>'.$langs->trans('RefCustomer').':</b> '.($this->ref_customer ? $this->ref_customer : $this->ref_client);
1807 
1808  $url = DOL_URL_ROOT.'/expedition/card.php?id='.$this->id;
1809 
1810  if ($short) {
1811  return $url;
1812  }
1813 
1814  if ($option !== 'nolink') {
1815  // Add param to save lastsearch_values or not
1816  $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
1817  if ($save_lastsearch_value == -1 && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
1818  $add_save_lastsearch_values = 1;
1819  }
1820  if ($add_save_lastsearch_values) {
1821  $url .= '&save_lastsearch_values=1';
1822  }
1823  }
1824 
1825  $linkclose = '';
1826  if (empty($notooltip)) {
1827  if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER)) {
1828  $label = $langs->trans("Shipment");
1829  $linkclose .= ' alt="'.dol_escape_htmltag($label, 1).'"';
1830  }
1831  $linkclose .= ' title="'.dol_escape_htmltag($label, 1).'"';
1832  $linkclose .= ' class="classfortooltip"';
1833  }
1834 
1835  $linkstart = '<a href="'.$url.'"';
1836  $linkstart .= $linkclose.'>';
1837  $linkend = '</a>';
1838 
1839  $result .= $linkstart;
1840  if ($withpicto) {
1841  $result .= img_object(($notooltip ? '' : $label), $this->picto, ($notooltip ? (($withpicto != 2) ? 'class="paddingright"' : '') : 'class="'.(($withpicto != 2) ? 'paddingright ' : '').'classfortooltip"'), 0, 0, $notooltip ? 0 : 1);
1842  }
1843  if ($withpicto != 2) {
1844  $result .= $this->ref;
1845  }
1846  $result .= $linkend;
1847  global $action;
1848  $hookmanager->initHooks(array($this->element . 'dao'));
1849  $parameters = array('id'=>$this->id, 'getnomurl' => &$result);
1850  $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
1851  if ($reshook > 0) {
1852  $result = $hookmanager->resPrint;
1853  } else {
1854  $result .= $hookmanager->resPrint;
1855  }
1856  return $result;
1857  }
1858 
1865  public function getLibStatut($mode = 0)
1866  {
1867  return $this->LibStatut($this->statut, $mode);
1868  }
1869 
1870  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1878  public function LibStatut($status, $mode)
1879  {
1880  // phpcs:enable
1881  global $langs;
1882 
1883  $labelStatus = $langs->transnoentitiesnoconv($this->statuts[$status]);
1884  $labelStatusShort = $langs->transnoentitiesnoconv($this->statuts_short[$status]);
1885 
1886  $statusType = 'status'.$status;
1887  if ($status == self::STATUS_VALIDATED) {
1888  $statusType = 'status4';
1889  }
1890  if ($status == self::STATUS_CLOSED) {
1891  $statusType = 'status6';
1892  }
1893  if ($status == self::STATUS_CANCELED) {
1894  $statusType = 'status9';
1895  }
1896 
1897  return dolGetStatus($labelStatus, $labelStatusShort, '', $statusType, $mode);
1898  }
1899 
1907  public function initAsSpecimen()
1908  {
1909  global $langs;
1910 
1911  $now = dol_now();
1912 
1913  dol_syslog(get_class($this)."::initAsSpecimen");
1914 
1915  // Load array of products prodids
1916  $num_prods = 0;
1917  $prodids = array();
1918  $sql = "SELECT rowid";
1919  $sql .= " FROM ".MAIN_DB_PREFIX."product";
1920  $sql .= " WHERE entity IN (".getEntity('product').")";
1921  $resql = $this->db->query($sql);
1922  if ($resql) {
1923  $num_prods = $this->db->num_rows($resql);
1924  $i = 0;
1925  while ($i < $num_prods) {
1926  $i++;
1927  $row = $this->db->fetch_row($resql);
1928  $prodids[$i] = $row[0];
1929  }
1930  }
1931 
1932  $order = new Commande($this->db);
1933  $order->initAsSpecimen();
1934 
1935  // Initialise parametres
1936  $this->id = 0;
1937  $this->ref = 'SPECIMEN';
1938  $this->specimen = 1;
1939  $this->statut = self::STATUS_VALIDATED;
1940  $this->livraison_id = 0;
1941  $this->date = $now;
1942  $this->date_creation = $now;
1943  $this->date_valid = $now;
1944  $this->date_delivery = $now;
1945  $this->date_expedition = $now + 24 * 3600;
1946 
1947  $this->entrepot_id = 0;
1948  $this->fk_delivery_address = 0;
1949  $this->socid = 1;
1950 
1951  $this->commande_id = 0;
1952  $this->commande = $order;
1953 
1954  $this->origin_id = 1;
1955  $this->origin = 'commande';
1956 
1957  $this->note_private = 'Private note';
1958  $this->note_public = 'Public note';
1959 
1960  $nbp = 5;
1961  $xnbp = 0;
1962  while ($xnbp < $nbp) {
1963  $line = new ExpeditionLigne($this->db);
1964  $line->desc = $langs->trans("Description")." ".$xnbp;
1965  $line->libelle = $langs->trans("Description")." ".$xnbp; // deprecated
1966  $line->label = $langs->trans("Description")." ".$xnbp;
1967  $line->qty = 10;
1968  $line->qty_asked = 5;
1969  $line->qty_shipped = 4;
1970  $line->fk_product = $this->commande->lines[$xnbp]->fk_product;
1971 
1972  $this->lines[] = $line;
1973  $xnbp++;
1974  }
1975  }
1976 
1977  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1986  public function set_date_livraison($user, $delivery_date)
1987  {
1988  // phpcs:enable
1989  return $this->setDeliveryDate($user, $delivery_date);
1990  }
1991 
1999  public function setDeliveryDate($user, $delivery_date)
2000  {
2001  if ($user->rights->expedition->creer) {
2002  $sql = "UPDATE ".MAIN_DB_PREFIX."expedition";
2003  $sql .= " SET date_delivery = ".($delivery_date ? "'".$this->db->idate($delivery_date)."'" : 'null');
2004  $sql .= " WHERE rowid = ".((int) $this->id);
2005 
2006  dol_syslog(get_class($this)."::setDeliveryDate", LOG_DEBUG);
2007  $resql = $this->db->query($sql);
2008  if ($resql) {
2009  $this->date_delivery = $delivery_date;
2010  return 1;
2011  } else {
2012  $this->error = $this->db->error();
2013  return -1;
2014  }
2015  } else {
2016  return -2;
2017  }
2018  }
2019 
2020  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2026  public function fetch_delivery_methods()
2027  {
2028  // phpcs:enable
2029  global $langs;
2030  $this->meths = array();
2031 
2032  $sql = "SELECT em.rowid, em.code, em.libelle as label";
2033  $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2034  $sql .= " WHERE em.active = 1";
2035  $sql .= " ORDER BY em.libelle ASC";
2036 
2037  $resql = $this->db->query($sql);
2038  if ($resql) {
2039  while ($obj = $this->db->fetch_object($resql)) {
2040  $label = $langs->trans('SendingMethod'.$obj->code);
2041  $this->meths[$obj->rowid] = ($label != 'SendingMethod'.$obj->code ? $label : $obj->label);
2042  }
2043  }
2044  }
2045 
2046  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2053  public function list_delivery_methods($id = '')
2054  {
2055  // phpcs:enable
2056  global $langs;
2057 
2058  $this->listmeths = array();
2059  $i = 0;
2060 
2061  $sql = "SELECT em.rowid, em.code, em.libelle as label, em.description, em.tracking, em.active";
2062  $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2063  if ($id != '') {
2064  $sql .= " WHERE em.rowid=".((int) $id);
2065  }
2066 
2067  $resql = $this->db->query($sql);
2068  if ($resql) {
2069  while ($obj = $this->db->fetch_object($resql)) {
2070  $this->listmeths[$i]['rowid'] = $obj->rowid;
2071  $this->listmeths[$i]['code'] = $obj->code;
2072  $label = $langs->trans('SendingMethod'.$obj->code);
2073  $this->listmeths[$i]['libelle'] = ($label != 'SendingMethod'.$obj->code ? $label : $obj->label);
2074  $this->listmeths[$i]['description'] = $obj->description;
2075  $this->listmeths[$i]['tracking'] = $obj->tracking;
2076  $this->listmeths[$i]['active'] = $obj->active;
2077  $i++;
2078  }
2079  }
2080  }
2081 
2088  public function getUrlTrackingStatus($value = '')
2089  {
2090  if (!empty($this->shipping_method_id)) {
2091  $sql = "SELECT em.code, em.tracking";
2092  $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2093  $sql .= " WHERE em.rowid = ".((int) $this->shipping_method_id);
2094 
2095  $resql = $this->db->query($sql);
2096  if ($resql) {
2097  if ($obj = $this->db->fetch_object($resql)) {
2098  $tracking = $obj->tracking;
2099  }
2100  }
2101  }
2102 
2103  if (!empty($tracking) && !empty($value)) {
2104  $url = str_replace('{TRACKID}', $value, $tracking);
2105  $this->tracking_url = sprintf('<a target="_blank" rel="noopener noreferrer" href="%s">'.($value ? $value : 'url').'</a>', $url, $url);
2106  } else {
2107  $this->tracking_url = $value;
2108  }
2109  }
2110 
2116  public function setClosed()
2117  {
2118  global $conf, $langs, $user;
2119 
2120  $error = 0;
2121 
2122  // Protection. This avoid to move stock later when we should not
2123  if ($this->statut == self::STATUS_CLOSED) {
2124  return 0;
2125  }
2126 
2127  $this->db->begin();
2128 
2129  $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET fk_statut = ".self::STATUS_CLOSED;
2130  $sql .= " WHERE rowid = ".((int) $this->id)." AND fk_statut > 0";
2131 
2132  $resql = $this->db->query($sql);
2133  if ($resql) {
2134  // Set order billed if 100% of order is shipped (qty in shipment lines match qty in order lines)
2135  if ($this->origin == 'commande' && $this->origin_id > 0) {
2136  $order = new Commande($this->db);
2137  $order->fetch($this->origin_id);
2138 
2139  $order->loadExpeditions(self::STATUS_CLOSED); // Fill $order->expeditions = array(orderlineid => qty)
2140 
2141  $shipments_match_order = 1;
2142  foreach ($order->lines as $line) {
2143  $lineid = $line->id;
2144  $qty = $line->qty;
2145  if (($line->product_type == 0 || !empty($conf->global->STOCK_SUPPORTS_SERVICES)) && $order->expeditions[$lineid] != $qty) {
2146  $shipments_match_order = 0;
2147  $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';
2148  dol_syslog($text);
2149  break;
2150  }
2151  }
2152  if ($shipments_match_order) {
2153  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');
2154  // We close the order
2155  $order->cloture($user); // Note this may also create an invoice if module workflow ask it
2156  }
2157  }
2158 
2159  $this->statut = self::STATUS_CLOSED; // Will be revert to STATUS_VALIDATED at end if there is a rollback
2160  $this->status = self::STATUS_CLOSED; // Will be revert to STATUS_VALIDATED at end if there is a rollback
2161 
2162  // If stock increment is done on closing
2163  if (!$error && isModEnabled('stock') && !empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE)) {
2164  require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
2165 
2166  $langs->load("agenda");
2167 
2168  // Loop on each product line to add a stock movement
2169  // TODO possibilite d'expedier a partir d'une propale ou autre origine ?
2170  $sql = "SELECT cd.fk_product, cd.subprice,";
2171  $sql .= " ed.rowid, ed.qty, ed.fk_entrepot,";
2172  $sql .= " e.ref,";
2173  $sql .= " edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock";
2174  $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,";
2175  $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed";
2176  $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid";
2177  $sql .= " INNER JOIN ".MAIN_DB_PREFIX."expedition as e ON ed.fk_expedition = e.rowid";
2178  $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
2179  $sql .= " AND cd.rowid = ed.fk_origin_line";
2180 
2181  dol_syslog(get_class($this)."::valid select details", LOG_DEBUG);
2182  $resql = $this->db->query($sql);
2183  if ($resql) {
2184  $cpt = $this->db->num_rows($resql);
2185  for ($i = 0; $i < $cpt; $i++) {
2186  $obj = $this->db->fetch_object($resql);
2187  if (empty($obj->edbrowid)) {
2188  $qty = $obj->qty;
2189  } else {
2190  $qty = $obj->edbqty;
2191  }
2192  if ($qty <= 0) {
2193  continue;
2194  }
2195  dol_syslog(get_class($this)."::valid movement index ".$i." ed.rowid=".$obj->rowid." edb.rowid=".$obj->edbrowid);
2196 
2197  $mouvS = new MouvementStock($this->db);
2198  $mouvS->origin = &$this;
2199  $mouvS->setOrigin($this->element, $this->id);
2200 
2201  if (empty($obj->edbrowid)) {
2202  // line without batch detail
2203 
2204  // 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
2205  $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, $qty, $obj->subprice, $langs->trans("ShipmentClassifyClosedInDolibarr", $obj->ref));
2206  if ($result < 0) {
2207  $this->error = $mouvS->error;
2208  $this->errors = $mouvS->errors;
2209  $error++;
2210  break;
2211  }
2212  } else {
2213  // line with batch detail
2214 
2215  // 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
2216  $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);
2217  if ($result < 0) {
2218  $this->error = $mouvS->error;
2219  $this->errors = $mouvS->errors;
2220  $error++;
2221  break;
2222  }
2223  }
2224  }
2225  } else {
2226  $this->error = $this->db->lasterror();
2227  $error++;
2228  }
2229  }
2230 
2231  // Call trigger
2232  if (!$error) {
2233  $result = $this->call_trigger('SHIPPING_CLOSED', $user);
2234  if ($result < 0) {
2235  $error++;
2236  }
2237  }
2238  } else {
2239  dol_print_error($this->db);
2240  $error++;
2241  }
2242 
2243  if (!$error) {
2244  $this->db->commit();
2245  return 1;
2246  } else {
2247  $this->statut = self::STATUS_VALIDATED;
2248  $this->status = self::STATUS_VALIDATED;
2249 
2250  $this->db->rollback();
2251  return -1;
2252  }
2253  }
2254 
2260  public function setBilled()
2261  {
2262  global $user;
2263  $error = 0;
2264 
2265  $this->db->begin();
2266 
2267  $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET fk_statut=2, billed=1'; // TODO Update only billed
2268  $sql .= " WHERE rowid = ".((int) $this->id).' AND fk_statut > 0';
2269 
2270  $resql = $this->db->query($sql);
2271  if ($resql) {
2272  $this->statut = self::STATUS_CLOSED;
2273  $this->billed = 1;
2274 
2275  // Call trigger
2276  $result = $this->call_trigger('SHIPPING_BILLED', $user);
2277  if ($result < 0) {
2278  $error++;
2279  }
2280  } else {
2281  $error++;
2282  $this->errors[] = $this->db->lasterror;
2283  }
2284 
2285  if (empty($error)) {
2286  $this->db->commit();
2287  return 1;
2288  } else {
2289  $this->statut = self::STATUS_VALIDATED;
2290  $this->billed = 0;
2291  $this->db->rollback();
2292  return -1;
2293  }
2294  }
2295 
2301  public function reOpen()
2302  {
2303  global $conf, $langs, $user;
2304 
2305  $error = 0;
2306 
2307  // Protection. This avoid to move stock later when we should not
2308  if ($this->statut == self::STATUS_VALIDATED) {
2309  return 0;
2310  }
2311 
2312  $this->db->begin();
2313 
2314  $oldbilled = $this->billed;
2315 
2316  $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET fk_statut=1';
2317  $sql .= " WHERE rowid = ".((int) $this->id).' AND fk_statut > 0';
2318 
2319  $resql = $this->db->query($sql);
2320  if ($resql) {
2321  $this->statut = self::STATUS_VALIDATED;
2322  $this->billed = 0;
2323 
2324  // If stock increment is done on closing
2325  if (!$error && isModEnabled('stock') && !empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE)) {
2326  require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
2327 
2328  $langs->load("agenda");
2329 
2330  // Loop on each product line to add a stock movement
2331  // TODO possibilite d'expedier a partir d'une propale ou autre origine
2332  $sql = "SELECT cd.fk_product, cd.subprice,";
2333  $sql .= " ed.rowid, ed.qty, ed.fk_entrepot,";
2334  $sql .= " edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock";
2335  $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,";
2336  $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed";
2337  $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid";
2338  $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
2339  $sql .= " AND cd.rowid = ed.fk_origin_line";
2340 
2341  dol_syslog(get_class($this)."::valid select details", LOG_DEBUG);
2342  $resql = $this->db->query($sql);
2343  if ($resql) {
2344  $cpt = $this->db->num_rows($resql);
2345  for ($i = 0; $i < $cpt; $i++) {
2346  $obj = $this->db->fetch_object($resql);
2347  if (empty($obj->edbrowid)) {
2348  $qty = $obj->qty;
2349  } else {
2350  $qty = $obj->edbqty;
2351  }
2352  if ($qty <= 0) {
2353  continue;
2354  }
2355  dol_syslog(get_class($this)."::reopen expedition movement index ".$i." ed.rowid=".$obj->rowid." edb.rowid=".$obj->edbrowid);
2356 
2357  //var_dump($this->lines[$i]);
2358  $mouvS = new MouvementStock($this->db);
2359  $mouvS->origin = &$this;
2360  $mouvS->setOrigin($this->element, $this->id);
2361 
2362  if (empty($obj->edbrowid)) {
2363  // line without batch detail
2364 
2365  // 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
2366  $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, -$qty, $obj->subprice, $langs->trans("ShipmentUnClassifyCloseddInDolibarr", $numref));
2367  if ($result < 0) {
2368  $this->error = $mouvS->error;
2369  $this->errors = $mouvS->errors;
2370  $error++;
2371  break;
2372  }
2373  } else {
2374  // line with batch detail
2375 
2376  // 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
2377  $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);
2378  if ($result < 0) {
2379  $this->error = $mouvS->error;
2380  $this->errors = $mouvS->errors;
2381  $error++;
2382  break;
2383  }
2384  }
2385  }
2386  } else {
2387  $this->error = $this->db->lasterror();
2388  $error++;
2389  }
2390  }
2391 
2392  if (!$error) {
2393  // Call trigger
2394  $result = $this->call_trigger('SHIPPING_REOPEN', $user);
2395  if ($result < 0) {
2396  $error++;
2397  }
2398  }
2399  } else {
2400  $error++;
2401  $this->errors[] = $this->db->lasterror();
2402  }
2403 
2404  if (!$error) {
2405  $this->db->commit();
2406  return 1;
2407  } else {
2408  $this->statut = self::STATUS_CLOSED;
2409  $this->billed = $oldbilled;
2410  $this->db->rollback();
2411  return -1;
2412  }
2413  }
2414 
2426  public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null)
2427  {
2428  global $conf;
2429 
2430  $outputlangs->load("products");
2431 
2432  if (!dol_strlen($modele)) {
2433  $modele = 'rouget';
2434 
2435  if (!empty($this->model_pdf)) {
2436  $modele = $this->model_pdf;
2437  } elseif (!empty($this->modelpdf)) { // deprecated
2438  $modele = $this->modelpdf;
2439  } elseif (!empty($conf->global->EXPEDITION_ADDON_PDF)) {
2440  $modele = $conf->global->EXPEDITION_ADDON_PDF;
2441  }
2442  }
2443 
2444  $modelpath = "core/modules/expedition/doc/";
2445 
2446  $this->fetch_origin();
2447 
2448  return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
2449  }
2450 
2459  public static function replaceThirdparty(DoliDB $db, $origin_id, $dest_id)
2460  {
2461  $tables = array(
2462  'expedition'
2463  );
2464 
2465  return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables);
2466  }
2467 }
2468 
2469 
2474 {
2478  public $element = 'expeditiondet';
2479 
2483  public $table_element = 'expeditiondet';
2484 
2485 
2492  public $line_id; // deprecated
2493 
2499 
2505  public $fk_origin; // Example: 'orderline'
2506 
2510  public $fk_origin_line;
2511 
2515  public $fk_expedition;
2516 
2520  public $db;
2521 
2525  public $qty;
2526 
2530  public $qty_shipped;
2531 
2535  public $fk_product;
2536 
2537  // detail of lot and qty = array(id in llx_expeditiondet_batch, fk_expeditiondet, batch, qty, fk_origin_stock)
2538  // We can use this to know warehouse planned to be used for each lot.
2539  public $detail_batch;
2540 
2541  // detail of warehouses and qty
2542  // We can use this to know warehouse when there is no lot.
2543  public $details_entrepot;
2544 
2545 
2549  public $entrepot_id;
2550 
2551 
2555  public $qty_asked;
2556 
2561  public $ref;
2562 
2566  public $product_ref;
2567 
2572  public $libelle;
2573 
2577  public $product_label;
2578 
2584  public $desc;
2585 
2589  public $product_desc;
2590 
2595  public $product_type = 0;
2596 
2600  public $rang;
2601 
2605  public $weight;
2606  public $weight_units;
2607 
2611  public $length;
2612  public $length_units;
2613 
2617  public $surface;
2618  public $surface_units;
2619 
2623  public $volume;
2624  public $volume_units;
2625 
2626  // Invoicing
2627  public $remise_percent;
2628  public $tva_tx;
2629 
2633  public $total_ht;
2634 
2638  public $total_ttc;
2639 
2643  public $total_tva;
2644 
2648  public $total_localtax1;
2649 
2653  public $total_localtax2;
2654 
2655 
2661  public function __construct($db)
2662  {
2663  $this->db = $db;
2664  }
2665 
2672  public function fetch($rowid)
2673  {
2674  $sql = 'SELECT ed.rowid, ed.fk_expedition, ed.fk_entrepot, ed.fk_origin_line, ed.qty, ed.rang';
2675  $sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element.' as ed';
2676  $sql .= ' WHERE ed.rowid = '.((int) $rowid);
2677  $result = $this->db->query($sql);
2678  if ($result) {
2679  $objp = $this->db->fetch_object($result);
2680  $this->id = $objp->rowid;
2681  $this->fk_expedition = $objp->fk_expedition;
2682  $this->entrepot_id = $objp->fk_entrepot;
2683  $this->fk_origin_line = $objp->fk_origin_line;
2684  $this->qty = $objp->qty;
2685  $this->rang = $objp->rang;
2686 
2687  $this->db->free($result);
2688 
2689  return 1;
2690  } else {
2691  $this->errors[] = $this->db->lasterror();
2692  $this->error = $this->db->lasterror();
2693  return -1;
2694  }
2695  }
2696 
2704  public function insert($user, $notrigger = 0)
2705  {
2706  global $langs, $conf;
2707 
2708  $error = 0;
2709 
2710  // Check parameters
2711  if (empty($this->fk_expedition) || empty($this->fk_origin_line) || !is_numeric($this->qty)) {
2712  $this->error = 'ErrorMandatoryParametersNotProvided';
2713  return -1;
2714  }
2715 
2716  $this->db->begin();
2717 
2718  if (empty($this->rang)) {
2719  $this->rang = 0;
2720  }
2721 
2722  // Rank to use
2723  $ranktouse = $this->rang;
2724  if ($ranktouse == -1) {
2725  $rangmax = $this->line_max($this->fk_expedition);
2726  $ranktouse = $rangmax + 1;
2727  }
2728 
2729  $sql = "INSERT INTO ".MAIN_DB_PREFIX."expeditiondet (";
2730  $sql .= "fk_expedition";
2731  $sql .= ", fk_entrepot";
2732  $sql .= ", fk_origin_line";
2733  $sql .= ", qty";
2734  $sql .= ", rang";
2735  $sql .= ") VALUES (";
2736  $sql .= $this->fk_expedition;
2737  $sql .= ", ".(empty($this->entrepot_id) ? 'NULL' : $this->entrepot_id);
2738  $sql .= ", ".((int) $this->fk_origin_line);
2739  $sql .= ", ".price2num($this->qty, 'MS');
2740  $sql .= ", ".((int) $ranktouse);
2741  $sql .= ")";
2742 
2743  dol_syslog(get_class($this)."::insert", LOG_DEBUG);
2744  $resql = $this->db->query($sql);
2745  if ($resql) {
2746  $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX."expeditiondet");
2747 
2748  if (!$error) {
2749  $result = $this->insertExtraFields();
2750  if ($result < 0) {
2751  $error++;
2752  }
2753  }
2754 
2755  if (!$error && !$notrigger) {
2756  // Call trigger
2757  $result = $this->call_trigger('LINESHIPPING_INSERT', $user);
2758  if ($result < 0) {
2759  $error++;
2760  }
2761  // End call triggers
2762  }
2763 
2764  if ($error) {
2765  foreach ($this->errors as $errmsg) {
2766  dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
2767  $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
2768  }
2769  }
2770  } else {
2771  $error++;
2772  }
2773 
2774  if ($error) {
2775  $this->db->rollback();
2776  return -1;
2777  } else {
2778  $this->db->commit();
2779  return $this->id;
2780  }
2781  }
2782 
2790  public function delete($user = null, $notrigger = 0)
2791  {
2792  global $conf;
2793 
2794  $error = 0;
2795 
2796  $this->db->begin();
2797 
2798  // delete batch expedition line
2799  if (isModEnabled('productbatch')) {
2800  $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet_batch";
2801  $sql .= " WHERE fk_expeditiondet = ".((int) $this->id);
2802 
2803  if (!$this->db->query($sql)) {
2804  $this->errors[] = $this->db->lasterror()." - sql=$sql";
2805  $error++;
2806  }
2807  }
2808 
2809  $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet";
2810  $sql .= " WHERE rowid = ".((int) $this->id);
2811 
2812  if (!$error && $this->db->query($sql)) {
2813  // Remove extrafields
2814  if (!$error) {
2815  $result = $this->deleteExtraFields();
2816  if ($result < 0) {
2817  $this->errors[] = $this->error;
2818  $error++;
2819  }
2820  }
2821  if (!$error && !$notrigger) {
2822  // Call trigger
2823  $result = $this->call_trigger('LINESHIPPING_DELETE', $user);
2824  if ($result < 0) {
2825  $this->errors[] = $this->error;
2826  $error++;
2827  }
2828  // End call triggers
2829  }
2830  } else {
2831  $this->errors[] = $this->db->lasterror()." - sql=$sql";
2832  $error++;
2833  }
2834 
2835  if (!$error) {
2836  $this->db->commit();
2837  return 1;
2838  } else {
2839  foreach ($this->errors as $errmsg) {
2840  dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
2841  $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
2842  }
2843  $this->db->rollback();
2844  return -1 * $error;
2845  }
2846  }
2847 
2855  public function update($user = null, $notrigger = 0)
2856  {
2857  global $conf;
2858 
2859  $error = 0;
2860 
2861  dol_syslog(get_class($this)."::update id=$this->id, entrepot_id=$this->entrepot_id, product_id=$this->fk_product, qty=$this->qty");
2862 
2863  $this->db->begin();
2864 
2865  // Clean parameters
2866  if (empty($this->qty)) {
2867  $this->qty = 0;
2868  }
2869  $qty = price2num($this->qty);
2870  $remainingQty = 0;
2871  $batch = null;
2872  $batch_id = null;
2873  $expedition_batch_id = null;
2874  if (is_array($this->detail_batch)) { // array of ExpeditionLineBatch
2875  if (count($this->detail_batch) > 1) {
2876  dol_syslog(get_class($this).'::update only possible for one batch', LOG_ERR);
2877  $this->errors[] = 'ErrorBadParameters';
2878  $error++;
2879  } else {
2880  $batch = $this->detail_batch[0]->batch;
2881  $batch_id = $this->detail_batch[0]->fk_origin_stock;
2882  $expedition_batch_id = $this->detail_batch[0]->id;
2883  if ($this->entrepot_id != $this->detail_batch[0]->entrepot_id) {
2884  dol_syslog(get_class($this).'::update only possible for batch of same warehouse', LOG_ERR);
2885  $this->errors[] = 'ErrorBadParameters';
2886  $error++;
2887  }
2888  $qty = price2num($this->detail_batch[0]->qty);
2889  }
2890  } elseif (!empty($this->detail_batch)) {
2891  $batch = $this->detail_batch->batch;
2892  $batch_id = $this->detail_batch->fk_origin_stock;
2893  $expedition_batch_id = $this->detail_batch->id;
2894  if ($this->entrepot_id != $this->detail_batch->entrepot_id) {
2895  dol_syslog(get_class($this).'::update only possible for batch of same warehouse', LOG_ERR);
2896  $this->errors[] = 'ErrorBadParameters';
2897  $error++;
2898  }
2899  $qty = price2num($this->detail_batch->qty);
2900  }
2901 
2902  // check parameters
2903  if (!isset($this->id) || !isset($this->entrepot_id)) {
2904  dol_syslog(get_class($this).'::update missing line id and/or warehouse id', LOG_ERR);
2905  $this->errors[] = 'ErrorMandatoryParametersNotProvided';
2906  $error++;
2907  return -1;
2908  }
2909 
2910  // update lot
2911 
2912  if (!empty($batch) && isModEnabled('productbatch')) {
2913  dol_syslog(get_class($this)."::update expedition batch id=$expedition_batch_id, batch_id=$batch_id, batch=$batch");
2914 
2915  if (empty($batch_id) || empty($this->fk_product)) {
2916  dol_syslog(get_class($this).'::update missing fk_origin_stock (batch_id) and/or fk_product', LOG_ERR);
2917  $this->errors[] = 'ErrorMandatoryParametersNotProvided';
2918  $error++;
2919  }
2920 
2921  // fetch remaining lot qty
2922  $shipmentlinebatch = new ExpeditionLineBatch($this->db);
2923 
2924  if (!$error && ($lotArray = $shipmentlinebatch->fetchAll($this->id)) < 0) {
2925  $this->errors[] = $this->db->lasterror()." - ExpeditionLineBatch::fetchAll";
2926  $error++;
2927  } else {
2928  // caculate new total line qty
2929  foreach ($lotArray as $lot) {
2930  if ($expedition_batch_id != $lot->id) {
2931  $remainingQty += $lot->qty;
2932  }
2933  }
2934  $qty += $remainingQty;
2935 
2936  //fetch lot details
2937 
2938  // fetch from product_lot
2939  require_once DOL_DOCUMENT_ROOT.'/product/stock/class/productlot.class.php';
2940  $lot = new Productlot($this->db);
2941  if ($lot->fetch(0, $this->fk_product, $batch) < 0) {
2942  $this->errors[] = $lot->errors;
2943  $error++;
2944  }
2945  if (!$error && !empty($expedition_batch_id)) {
2946  // delete lot expedition line
2947  $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet_batch";
2948  $sql .= " WHERE fk_expeditiondet = ".((int) $this->id);
2949  $sql .= " AND rowid = ".((int) $expedition_batch_id);
2950 
2951  if (!$this->db->query($sql)) {
2952  $this->errors[] = $this->db->lasterror()." - sql=$sql";
2953  $error++;
2954  }
2955  }
2956  if (!$error && $this->detail_batch->qty > 0) {
2957  // create lot expedition line
2958  if (isset($lot->id)) {
2959  $shipmentLot = new ExpeditionLineBatch($this->db);
2960  $shipmentLot->batch = $lot->batch;
2961  $shipmentLot->eatby = $lot->eatby;
2962  $shipmentLot->sellby = $lot->sellby;
2963  $shipmentLot->entrepot_id = $this->detail_batch->entrepot_id;
2964  $shipmentLot->qty = $this->detail_batch->qty;
2965  $shipmentLot->fk_origin_stock = $batch_id;
2966  if ($shipmentLot->create($this->id) < 0) {
2967  $this->errors[] = $shipmentLot->errors;
2968  $error++;
2969  }
2970  }
2971  }
2972  }
2973  }
2974  if (!$error) {
2975  // update line
2976  $sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
2977  $sql .= " fk_entrepot = ".($this->entrepot_id > 0 ? $this->entrepot_id : 'null');
2978  $sql .= " , qty = ".((float) price2num($qty, 'MS'));
2979  $sql .= " WHERE rowid = ".((int) $this->id);
2980 
2981  if (!$this->db->query($sql)) {
2982  $this->errors[] = $this->db->lasterror()." - sql=$sql";
2983  $error++;
2984  }
2985  }
2986 
2987  if (!$error) {
2988  if (!$error) {
2989  $result = $this->insertExtraFields();
2990  if ($result < 0) {
2991  $this->errors[] = $this->error;
2992  $error++;
2993  }
2994  }
2995  }
2996 
2997  if (!$error && !$notrigger) {
2998  // Call trigger
2999  $result = $this->call_trigger('LINESHIPPING_MODIFY', $user);
3000  if ($result < 0) {
3001  $this->errors[] = $this->error;
3002  $error++;
3003  }
3004  // End call triggers
3005  }
3006  if (!$error) {
3007  $this->db->commit();
3008  return 1;
3009  } else {
3010  foreach ($this->errors as $errmsg) {
3011  dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR);
3012  $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
3013  }
3014  $this->db->rollback();
3015  return -1 * $error;
3016  }
3017  }
3018 }
Expedition\$date_expedition
$date_expedition
Definition: expedition.class.php:157
ExpeditionLigne\insert
insert($user, $notrigger=0)
Insert line into database.
Definition: expedition.class.php:2704
Societe
Class to manage third parties objects (customers, suppliers, prospects...)
Definition: societe.class.php:49
Expedition\setClosed
setClosed()
Classify the shipping as closed.
Definition: expedition.class.php:2116
db
$conf db
API class for accounts.
Definition: inc.php:41
Productbatch
Manage record for batch number management.
Definition: productbatch.class.php:31
Expedition\update
update($user=null, $notrigger=0)
Update database.
Definition: expedition.class.php:1054
Expedition\list_delivery_methods
list_delivery_methods($id='')
Fetch all deliveries method and return an array.
Definition: expedition.class.php:2053
dol_sanitizeFileName
dol_sanitizeFileName($str, $newstr='_', $unaccent=1)
Clean a string to use it as a file name.
Definition: functions.lib.php:1225
Expedition
Class to manage shipments.
Definition: expedition.class.php:52
Expedition\addline_batch
addline_batch($dbatch, $array_options=0)
Add a shipment line with batch record.
Definition: expedition.class.php:989
Expedition\setDeliveryDate
setDeliveryDate($user, $delivery_date)
Set the planned delivery date.
Definition: expedition.class.php:1999
Expedition\__construct
__construct($db)
Constructor.
Definition: expedition.class.php:207
Expedition\reOpen
reOpen()
Classify the shipping as validated/opened.
Definition: expedition.class.php:2301
dol_delete_dir_recursive
dol_delete_dir_recursive($dir, $count=0, $nophperrors=0, $onlysub=0, &$countdeleted=0, $indexdatabase=1, $nolog=0)
Remove a directory $dir and its subdirectories (or only files and subdirectories)
Definition: files.lib.php:1402
DoliDB
Class to manage Dolibarr database access.
Definition: DoliDB.class.php:30
Expedition\replaceThirdparty
static replaceThirdparty(DoliDB $db, $origin_id, $dest_id)
Function used to replace a thirdparty id with another one.
Definition: expedition.class.php:2459
CommonObject\fetchObjectLinked
fetchObjectLinked($sourceid=null, $sourcetype='', $targetid=null, $targettype='', $clause='OR', $alsosametype=1, $orderby='sourcetype', $loadalsoobjects=1)
Fetch array of objects linked to current object (object of enabled modules only).
Definition: commonobject.class.php:3920
dol_print_error
dol_print_error($db='', $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
Definition: functions.lib.php:4993
ExpeditionLigne
Classe to manage lines of shipment.
Definition: expedition.class.php:2473
Expedition\STATUS_DRAFT
const STATUS_DRAFT
Draft status.
Definition: expedition.class.php:184
ref
$object ref
Definition: info.php:78
dol_buildpath
dol_buildpath($path, $type=0, $returnemptyifnotfound=0)
Return path of url or filesystem.
Definition: functions.lib.php:1061
dol_dir_list
dol_dir_list($path, $types="all", $recursive=0, $filter="", $excludefilter=null, $sortcriteria="name", $sortorder=SORT_ASC, $mode=0, $nohook=0, $relativename="", $donotfollowsymlinks=0, $nbsecondsold=0)
Scan a directory and return a list of files/directories.
Definition: files.lib.php:61
Expedition\create_line_batch
create_line_batch($line_ext, $array_options=0)
Create the detail of the expedition line.
Definition: expedition.class.php:474
Commande\STATUS_SHIPMENTONPROCESS
const STATUS_SHIPMENTONPROCESS
Shipment on process.
Definition: commande.class.php:382
CommonIncoterm
trait CommonIncoterm
Superclass for incoterm classes.
Definition: commonincoterm.class.php:29
CommonObject\deleteObjectLinked
deleteObjectLinked($sourceid=null, $sourcetype='', $targetid=null, $targettype='', $rowid='', $f_user=null, $notrigger=0)
Delete all links between an object $this.
Definition: commonobject.class.php:4232
Expedition\addline
addline($entrepot_id, $id, $qty, $array_options=0)
Add an expedition line.
Definition: expedition.class.php:906
ExpeditionLineBatch
CRUD class for batch number management within shipment.
Definition: expeditionlinebatch.class.php:29
CommonObject\setStatut
setStatut($status, $elementId=null, $elementType='', $trigkey='', $fieldstatus='fk_statut')
Set status of an object.
Definition: commonobject.class.php:4368
Expedition\$date
$date
Definition: expedition.class.php:151
Expedition\deleteline
deleteline($user, $lineid)
Delete detail line.
Definition: expedition.class.php:1760
CommonObjectLine
Parent class for class inheritance lines of business objects This class is useless for the moment so ...
Definition: commonobjectline.class.php:32
CommonObject
Parent class of all other business classes (invoices, contracts, proposals, orders,...
Definition: commonobject.class.php:44
Expedition\generateDocument
generateDocument($modele, $outputlangs, $hidedetails=0, $hidedesc=0, $hideref=0, $moreparams=null)
Create a document onto disk according to template module.
Definition: expedition.class.php:2426
Expedition\STATUS_CLOSED
const STATUS_CLOSED
Closed status.
Definition: expedition.class.php:194
CommonObject\fetch_origin
fetch_origin()
Read linked origin object.
Definition: commonobject.class.php:1943
price2num
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
Definition: functions.lib.php:5823
Productlot
Class with list of lots and properties.
Definition: productlot.class.php:36
CommonObject\fetch_thirdparty
fetch_thirdparty($force_thirdparty_id=0)
Load the third party of object, from id $this->socid or $this->fk_soc, into this->thirdparty.
Definition: commonobject.class.php:1750
Expedition\setBilled
setBilled()
Classify the shipping as invoiced (used when WORKFLOW_BILL_ON_SHIPMENT is on)
Definition: expedition.class.php:2260
Expedition\getUrlTrackingStatus
getUrlTrackingStatus($value='')
Forge an set tracking url.
Definition: expedition.class.php:2088
Commande\STATUS_VALIDATED
const STATUS_VALIDATED
Validated status.
Definition: commande.class.php:378
CommonObject\commonGenerateDocument
commonGenerateDocument($modelspath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams=null)
Common function for all objects extending CommonObject for generating documents.
Definition: commonobject.class.php:5415
calcul_price_total
calcul_price_total($qty, $pu, $remise_percent_ligne, $txtva, $uselocaltax1_rate, $uselocaltax2_rate, $remise_percent_global, $price_base_type, $info_bits, $type, $seller='', $localtaxes_array='', $progress=100, $multicurrency_tx=1, $pu_devise=0, $multicurrency_code='')
Calculate totals (net, vat, ...) of a line.
Definition: price.lib.php:86
dol_delete_file
dol_delete_file($file, $disableglob=0, $nophperrors=0, $nohook=0, $object=null, $allowdotdot=false, $indexdatabase=1, $nolog=0)
Remove a file or several files with a mask.
Definition: files.lib.php:1251
Expedition\valid
valid($user, $notrigger=0)
Validate object and update stock if option enabled.
Definition: expedition.class.php:660
OrderLine
Class to manage order lines.
Definition: commande.class.php:4150
Expedition\fetch_lines
fetch_lines()
Load lines.
Definition: expedition.class.php:1580
get_localtax
get_localtax($vatrate, $local, $thirdparty_buyer="", $thirdparty_seller="", $vatnpr=0)
Return localtax rate for a particular vat, when selling a product with vat $vatrate,...
Definition: functions.lib.php:6004
Expedition\initAsSpecimen
initAsSpecimen()
Initialise an instance with random values.
Definition: expedition.class.php:1907
MouvementStock
Class to manage stock movements.
Definition: mouvementstock.class.php:31
CommonObject\insertExtraFields
insertExtraFields($trigger='', $userused=null)
Add/Update all extra fields values for the current object.
Definition: commonobject.class.php:6173
$resql
if(isModEnabled('facture') &&!empty($user->rights->facture->lire)) if((isModEnabled('fournisseur') &&empty($conf->global->MAIN_USE_NEW_SUPPLIERMOD) && $user->hasRight("fournisseur", "facture", "lire"))||(isModEnabled('supplier_invoice') && $user->hasRight("supplier_invoice", "lire"))) if(isModEnabled('don') &&!empty($user->rights->don->lire)) if(isModEnabled('tax') &&!empty($user->rights->tax->charges->lire)) if(isModEnabled('facture') &&isModEnabled('commande') && $user->hasRight("commande", "lire") &&empty($conf->global->WORKFLOW_DISABLE_CREATE_INVOICE_FROM_ORDER)) $resql
Social contributions to pay.
Definition: index.php:745
ExpeditionLigne\update
update($user=null, $notrigger=0)
Update a line in database.
Definition: expedition.class.php:2855
Expedition\create
create($user, $notrigger=0)
Create expedition en base.
Definition: expedition.class.php:283
ExpeditionLigne\fetch
fetch($rowid)
Load line expedition.
Definition: expedition.class.php:2672
Commande
Class to manage customers orders.
Definition: commande.class.php:46
CommonObject\deleteEcmFiles
deleteEcmFiles($mode=0)
Delete related files of object in database.
Definition: commonobject.class.php:10109
CommonObject\commonReplaceThirdparty
static commonReplaceThirdparty(DoliDB $db, $origin_id, $dest_id, array $tables, $ignoreerrors=0)
Function used to replace a thirdparty id with another one.
Definition: commonobject.class.php:8479
dol_syslog
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
Definition: functions.lib.php:1628
CommonObject\line_max
line_max($fk_parent_line=0)
Get max value used for position of line (rang)
Definition: commonobject.class.php:3387
Expedition\getNomUrl
getNomUrl($withpicto=0, $option='', $max=0, $short=0, $notooltip=0, $save_lastsearch_value=-1)
Return clicable link of object (with eventually picto)
Definition: expedition.class.php:1799
Expedition\LibStatut
LibStatut($status, $mode)
Return label of a status.
Definition: expedition.class.php:1878
CommonObject\fetch_optionals
fetch_optionals($rowid=null, $optionsArray=null)
Function to get extra fields of an object into $this->array_options This method is in most cases call...
Definition: commonobject.class.php:6022
Expedition\fetch
fetch($id, $ref='', $ref_ext='', $notused='')
Get object and lines from database.
Definition: expedition.class.php:520
dol_strlen
dol_strlen($string, $stringencoding='UTF-8')
Make a strlen call.
Definition: functions.lib.php:3887
Expedition\cancel
cancel($notrigger=0, $also_update_stock=false)
Cancel shipment.
Definition: expedition.class.php:1194
Expedition\set_date_livraison
set_date_livraison($user, $delivery_date)
Set delivery date.
Definition: expedition.class.php:1986
Expedition\getNextNumRef
getNextNumRef($soc)
Return next expedition ref.
Definition: expedition.class.php:234
Expedition\create_delivery
create_delivery($user)
Create a delivery receipt from a shipment.
Definition: expedition.class.php:869
ExpeditionLigne\$libelle
$libelle
Definition: expedition.class.php:2572
isModEnabled
isModEnabled($module)
Is Dolibarr module enabled.
Definition: functions.lib.php:137
Expedition\STATUS_CANCELED
const STATUS_CANCELED
Canceled status.
Definition: expedition.class.php:199
Expedition\STATUS_VALIDATED
const STATUS_VALIDATED
Validated status.
Definition: expedition.class.php:189
CommonObject\deleteExtraFields
deleteExtraFields()
Delete all extra fields values for the current object.
Definition: commonobject.class.php:6133
Product
Class to manage products or services.
Definition: product.class.php:46
CommonObject\add_object_linked
add_object_linked($origin=null, $origin_id=null, $f_user=null, $notrigger=0)
Add an object link into llx_element_element.
Definition: commonobject.class.php:3822
dolGetStatus
dolGetStatus($statusLabel='', $statusLabelShort='', $html='', $statusType='status0', $displayMode=0, $url='', $params=array())
Output the badge of a status.
Definition: functions.lib.php:10740
ExpeditionLigne\__construct
__construct($db)
Constructor.
Definition: expedition.class.php:2661
img_object
img_object($titlealt, $picto, $moreatt='', $pictoisfullpath=false, $srconly=0, $notitle=0)
Show a picto called object_picto (generic function)
Definition: functions.lib.php:4360
ExpeditionLigne\$ref
$ref
Definition: expedition.class.php:2561
dol_now
dol_now($mode='auto')
Return date for now.
Definition: functions.lib.php:2951
Expedition\create_line
create_line($entrepot_id, $origin_line_id, $qty, $rang=0, $array_options=null)
Create a expedition line.
Definition: expedition.class.php:446
Expedition\getLibStatut
getLibStatut($mode=0)
Return status label.
Definition: expedition.class.php:1865
Delivery
Class to manage receptions.
Definition: delivery.class.php:45
CommonObject\call_trigger
call_trigger($triggerName, $user)
Call trigger based on this instance.
Definition: commonobject.class.php:5806
user
$conf db user
Definition: repair.php:123
ExpeditionLigne\$origin_line_id
$origin_line_id
Definition: expedition.class.php:2498
getDolGlobalInt
getDolGlobalInt($key, $default=0)
Return dolibarr global constant int value.
Definition: functions.lib.php:96
Expedition\fetch_delivery_methods
fetch_delivery_methods()
Fetch deliveries method and return an array.
Definition: expedition.class.php:2026