dolibarr  x.y.z
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->statut = $obj->fk_statut;
567  $this->user_author_id = $obj->fk_user_author;
568  $this->date_creation = $this->db->jdate($obj->date_creation);
569  $this->date_valid = $this->db->jdate($obj->date_valid);
570  $this->date = $this->db->jdate($obj->date_expedition); // TODO deprecated
571  $this->date_expedition = $this->db->jdate($obj->date_expedition); // TODO deprecated
572  $this->date_shipping = $this->db->jdate($obj->date_expedition); // Date real
573  $this->date_delivery = $this->db->jdate($obj->date_delivery); // Date planed
574  $this->fk_delivery_address = $obj->fk_address;
575  $this->model_pdf = $obj->model_pdf;
576  $this->modelpdf = $obj->model_pdf; // deprecated
577  $this->shipping_method_id = $obj->fk_shipping_method;
578  $this->shipping_method = $obj->shipping_method;
579  $this->tracking_number = $obj->tracking_number;
580  $this->origin = ($obj->origin ? $obj->origin : 'commande'); // For compatibility
581  $this->origin_id = $obj->origin_id;
582  $this->billed = $obj->billed;
583  $this->fk_project = $obj->fk_project;
584 
585  $this->trueWeight = $obj->weight;
586  $this->weight_units = $obj->weight_units;
587 
588  $this->trueWidth = $obj->width;
589  $this->width_units = $obj->size_units;
590  $this->trueHeight = $obj->height;
591  $this->height_units = $obj->size_units;
592  $this->trueDepth = $obj->size;
593  $this->depth_units = $obj->size_units;
594 
595  $this->note_public = $obj->note_public;
596  $this->note_private = $obj->note_private;
597 
598  // A denormalized value
599  $this->trueSize = $obj->size."x".$obj->width."x".$obj->height;
600  $this->size_units = $obj->size_units;
601 
602  //Incoterms
603  $this->fk_incoterms = $obj->fk_incoterms;
604  $this->location_incoterms = $obj->location_incoterms;
605  $this->label_incoterms = $obj->label_incoterms;
606 
607  $this->db->free($result);
608 
609  if ($this->statut == self::STATUS_DRAFT) {
610  $this->brouillon = 1;
611  }
612 
613  // Tracking url
614  $this->getUrlTrackingStatus($obj->tracking_number);
615 
616  // Thirdparty
617  $result = $this->fetch_thirdparty(); // TODO Remove this
618 
619  // Retrieve extrafields
620  $this->fetch_optionals();
621 
622  // Fix Get multicurrency param for transmited
623  if (isModEnabled('multicurrency')) {
624  if (!empty($this->multicurrency_code)) {
625  $this->multicurrency_code = $this->thirdparty->multicurrency_code;
626  }
627  if (!empty($conf->global->MULTICURRENCY_USE_ORIGIN_TX) && !empty($this->thirdparty->multicurrency_tx)) {
628  $this->multicurrency_tx = $this->thirdparty->multicurrency_tx;
629  }
630  }
631 
632  /*
633  * Lines
634  */
635  $result = $this->fetch_lines();
636  if ($result < 0) {
637  return -3;
638  }
639 
640  return 1;
641  } else {
642  dol_syslog(get_class($this).'::Fetch no expedition found', LOG_ERR);
643  $this->error = 'Delivery with id '.$id.' not found';
644  return 0;
645  }
646  } else {
647  $this->error = $this->db->error();
648  return -1;
649  }
650  }
651 
659  public function valid($user, $notrigger = 0)
660  {
661  global $conf, $langs;
662 
663  require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
664 
665  dol_syslog(get_class($this)."::valid");
666 
667  // Protection
668  if ($this->statut) {
669  dol_syslog(get_class($this)."::valid not in draft status", LOG_WARNING);
670  return 0;
671  }
672 
673  if (!((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && !empty($user->rights->expedition->creer))
674  || (!empty($conf->global->MAIN_USE_ADVANCED_PERMS) && !empty($user->rights->expedition->shipping_advance->validate)))) {
675  $this->error = 'Permission denied';
676  dol_syslog(get_class($this)."::valid ".$this->error, LOG_ERR);
677  return -1;
678  }
679 
680  $this->db->begin();
681 
682  $error = 0;
683 
684  // Define new ref
685  $soc = new Societe($this->db);
686  $soc->fetch($this->socid);
687 
688  // Class of company linked to order
689  $result = $soc->set_as_client();
690 
691  // Define new ref
692  if (!$error && (preg_match('/^[\‍(]?PROV/i', $this->ref) || empty($this->ref))) { // empty should not happened, but when it occurs, the test save life
693  $numref = $this->getNextNumRef($soc);
694  } else {
695  $numref = "EXP".$this->id;
696  }
697  $this->newref = dol_sanitizeFileName($numref);
698 
699  $now = dol_now();
700 
701  // Validate
702  $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET";
703  $sql .= " ref='".$this->db->escape($numref)."'";
704  $sql .= ", fk_statut = 1";
705  $sql .= ", date_valid = '".$this->db->idate($now)."'";
706  $sql .= ", fk_user_valid = ".$user->id;
707  $sql .= " WHERE rowid = ".((int) $this->id);
708 
709  dol_syslog(get_class($this)."::valid update expedition", LOG_DEBUG);
710  $resql = $this->db->query($sql);
711  if (!$resql) {
712  $this->error = $this->db->lasterror();
713  $error++;
714  }
715 
716  // If stock increment is done on sending (recommanded choice)
717  if (!$error && isModEnabled('stock') && !empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT)) {
718  require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
719 
720  $langs->load("agenda");
721 
722  // Loop on each product line to add a stock movement
723  $sql = "SELECT cd.fk_product, cd.subprice,";
724  $sql .= " ed.rowid, ed.qty, ed.fk_entrepot,";
725  $sql .= " edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock";
726  $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,";
727  $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed";
728  $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid";
729  $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
730  $sql .= " AND cd.rowid = ed.fk_origin_line";
731 
732  dol_syslog(get_class($this)."::valid select details", LOG_DEBUG);
733  $resql = $this->db->query($sql);
734  if ($resql) {
735  $cpt = $this->db->num_rows($resql);
736  for ($i = 0; $i < $cpt; $i++) {
737  $obj = $this->db->fetch_object($resql);
738  if (empty($obj->edbrowid)) {
739  $qty = $obj->qty;
740  } else {
741  $qty = $obj->edbqty;
742  }
743 
744  if ($qty == 0 || ($qty < 0 && !getDolGlobalInt('SHIPMENT_ALLOW_NEGATIVE_QTY'))) {
745  continue;
746  }
747  dol_syslog(get_class($this)."::valid movement index ".$i." ed.rowid=".$obj->rowid." edb.rowid=".$obj->edbrowid);
748 
749  //var_dump($this->lines[$i]);
750  $mouvS = new MouvementStock($this->db);
751 
752  $mouvS->setOrigin($this->element, $this->id);
753 
754  if (empty($obj->edbrowid)) {
755  // line without batch detail
756 
757  // 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.
758  $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, $qty, $obj->subprice, $langs->trans("ShipmentValidatedInDolibarr", $numref), '', '', '', '', 0, '', 1);
759 
760  if ($result < 0) {
761  $error++;
762  $this->error = $mouvS->error;
763  $this->errors = array_merge($this->errors, $mouvS->errors);
764  break;
765  }
766  } else {
767  // line with batch detail
768 
769  // 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.
770  // Note: ->fk_origin_stock = id into table llx_product_batch (may be renamed into llx_product_stock_batch in another version)
771  $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);
772  if ($result < 0) {
773  $error++;
774  $this->error = $mouvS->error;
775  $this->errors = array_merge($this->errors, $mouvS->errors);
776  break;
777  }
778  }
779  }
780 
781  // 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
782  // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot.
783  $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)";
784  $resql = $this->db->query($sql);
785  // We do not test error, it can fails if there is child in batch details
786  } else {
787  $this->db->rollback();
788  $this->error = $this->db->error();
789  return -2;
790  }
791  }
792 
793  // Change status of order to "shipment in process"
794  $ret = $this->setStatut(Commande::STATUS_SHIPMENTONPROCESS, $this->origin_id, $this->origin);
795  if (!$ret) {
796  $error++;
797  }
798 
799  if (!$error && !$notrigger) {
800  // Call trigger
801  $result = $this->call_trigger('SHIPPING_VALIDATE', $user);
802  if ($result < 0) {
803  $error++;
804  }
805  // End call triggers
806  }
807 
808  if (!$error) {
809  $this->oldref = $this->ref;
810 
811  // Rename directory if dir was a temporary ref
812  if (preg_match('/^[\‍(]?PROV/i', $this->ref)) {
813  // Now we rename also files into index
814  $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)."'";
815  $sql .= " WHERE filename LIKE '".$this->db->escape($this->ref)."%' AND filepath = 'expedition/sending/".$this->db->escape($this->ref)."' and entity = ".((int) $conf->entity);
816  $resql = $this->db->query($sql);
817  if (!$resql) {
818  $error++; $this->error = $this->db->lasterror();
819  }
820 
821  // We rename directory ($this->ref = old ref, $num = new ref) in order not to lose the attachments
822  $oldref = dol_sanitizeFileName($this->ref);
823  $newref = dol_sanitizeFileName($numref);
824  $dirsource = $conf->expedition->dir_output.'/sending/'.$oldref;
825  $dirdest = $conf->expedition->dir_output.'/sending/'.$newref;
826  if (!$error && file_exists($dirsource)) {
827  dol_syslog(get_class($this)."::valid rename dir ".$dirsource." into ".$dirdest);
828 
829  if (@rename($dirsource, $dirdest)) {
830  dol_syslog("Rename ok");
831  // Rename docs starting with $oldref with $newref
832  $listoffiles = dol_dir_list($conf->expedition->dir_output.'/sending/'.$newref, 'files', 1, '^'.preg_quote($oldref, '/'));
833  foreach ($listoffiles as $fileentry) {
834  $dirsource = $fileentry['name'];
835  $dirdest = preg_replace('/^'.preg_quote($oldref, '/').'/', $newref, $dirsource);
836  $dirsource = $fileentry['path'].'/'.$dirsource;
837  $dirdest = $fileentry['path'].'/'.$dirdest;
838  @rename($dirsource, $dirdest);
839  }
840  }
841  }
842  }
843  }
844 
845  // Set new ref and current status
846  if (!$error) {
847  $this->ref = $numref;
848  $this->statut = self::STATUS_VALIDATED;
849  }
850 
851  if (!$error) {
852  $this->db->commit();
853  return 1;
854  } else {
855  $this->db->rollback();
856  return -1 * $error;
857  }
858  }
859 
860 
861  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
868  public function create_delivery($user)
869  {
870  // phpcs:enable
871  global $conf;
872 
873  if ($conf->delivery_note->enabled) {
874  if ($this->statut == self::STATUS_VALIDATED || $this->statut == self::STATUS_CLOSED) {
875  // Expedition validee
876  include_once DOL_DOCUMENT_ROOT.'/delivery/class/delivery.class.php';
877  $delivery = new Delivery($this->db);
878  $result = $delivery->create_from_sending($user, $this->id);
879  if ($result > 0) {
880  return $result;
881  } else {
882  $this->error = $delivery->error;
883  return $result;
884  }
885  } else {
886  return 0;
887  }
888  } else {
889  return 0;
890  }
891  }
892 
905  public function addline($entrepot_id, $id, $qty, $array_options = 0)
906  {
907  global $conf, $langs;
908 
909  $num = count($this->lines);
910  $line = new ExpeditionLigne($this->db);
911 
912  $line->entrepot_id = $entrepot_id;
913  $line->origin_line_id = $id;
914  $line->fk_origin_line = $id;
915  $line->qty = $qty;
916 
917  $orderline = new OrderLine($this->db);
918  $orderline->fetch($id);
919 
920  // Copy the rang of the order line to the expedition line
921  $line->rang = $orderline->rang;
922  $line->product_type = $orderline->product_type;
923 
924  if (isModEnabled('stock') && !empty($orderline->fk_product)) {
925  $fk_product = $orderline->fk_product;
926 
927  if (!($entrepot_id > 0) && empty($conf->global->STOCK_WAREHOUSE_NOT_REQUIRED_FOR_SHIPMENTS)) {
928  $langs->load("errors");
929  $this->error = $langs->trans("ErrorWarehouseRequiredIntoShipmentLine");
930  return -1;
931  }
932 
933  if (!empty($conf->global->STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT)) {
934  $product = new Product($this->db);
935  $product->fetch($fk_product);
936 
937  // Check must be done for stock of product into warehouse if $entrepot_id defined
938  if ($entrepot_id > 0) {
939  $product->load_stock('warehouseopen');
940  $product_stock = $product->stock_warehouse[$entrepot_id]->real;
941  } else {
942  $product_stock = $product->stock_reel;
943  }
944 
945  $product_type = $product->type;
946  if ($product_type == 0 || !empty($conf->global->STOCK_SUPPORTS_SERVICES)) {
947  $isavirtualproduct = ($product->hasFatherOrChild(1) > 0);
948  // The product is qualified for a check of quantity (must be enough in stock to be added into shipment).
949  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.
950  if ($product_stock < $qty) {
951  $langs->load("errors");
952  $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $product->ref);
953  $this->errorhidden = 'ErrorStockIsNotEnoughToAddProductOnShipment';
954 
955  $this->db->rollback();
956  return -3;
957  }
958  }
959  }
960  }
961  }
962 
963  // If product need a batch number, we should not have called this function but addline_batch instead.
964  // If this happen, we may have a bug in card.php page
965  if (isModEnabled('productbatch') && !empty($orderline->fk_product) && !empty($orderline->product_tobatch)) {
966  $this->error = 'ADDLINE_WAS_CALLED_INSTEAD_OF_ADDLINEBATCH '.$orderline->id.' '.$orderline->fk_product; //
967  return -4;
968  }
969 
970  // extrafields
971  if (empty($conf->global->MAIN_EXTRAFIELDS_DISABLED) && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used
972  $line->array_options = $array_options;
973  }
974 
975  $this->lines[$num] = $line;
976 
977  return 1;
978  }
979 
980  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
988  public function addline_batch($dbatch, $array_options = 0)
989  {
990  // phpcs:enable
991  global $conf, $langs;
992 
993  $num = count($this->lines);
994  if ($dbatch['qty'] > 0) {
995  $line = new ExpeditionLigne($this->db);
996  $tab = array();
997  foreach ($dbatch['detail'] as $key => $value) {
998  if ($value['q'] > 0) {
999  // $value['q']=qty to move
1000  // $value['id_batch']=id into llx_product_batch of record to move
1001  //var_dump($value);
1002 
1003  $linebatch = new ExpeditionLineBatch($this->db);
1004  $ret = $linebatch->fetchFromStock($value['id_batch']); // load serial, sellby, eatby
1005  if ($ret < 0) {
1006  $this->error = $linebatch->error;
1007  return -1;
1008  }
1009  $linebatch->qty = $value['q'];
1010  $tab[] = $linebatch;
1011 
1012  if ($conf->global->STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT) {
1013  require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
1014  $prod_batch = new Productbatch($this->db);
1015  $prod_batch->fetch($value['id_batch']);
1016 
1017  if ($prod_batch->qty < $linebatch->qty) {
1018  $langs->load("errors");
1019  $this->errors[] = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $prod_batch->fk_product);
1020  dol_syslog(get_class($this)."::addline_batch error=Product ".$prod_batch->batch.": ".$this->errorsToString(), LOG_ERR);
1021  $this->db->rollback();
1022  return -1;
1023  }
1024  }
1025 
1026  //var_dump($linebatch);
1027  }
1028  }
1029  $line->entrepot_id = $linebatch->entrepot_id;
1030  $line->origin_line_id = $dbatch['ix_l']; // deprecated
1031  $line->fk_origin_line = $dbatch['ix_l'];
1032  $line->qty = $dbatch['qty'];
1033  $line->detail_batch = $tab;
1034 
1035  // extrafields
1036  if (empty($conf->global->MAIN_EXTRAFIELDS_DISABLED) && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used
1037  $line->array_options = $array_options;
1038  }
1039 
1040  //var_dump($line);
1041  $this->lines[$num] = $line;
1042  return 1;
1043  }
1044  }
1045 
1053  public function update($user = null, $notrigger = 0)
1054  {
1055  global $conf;
1056  $error = 0;
1057 
1058  // Clean parameters
1059 
1060  if (isset($this->ref)) {
1061  $this->ref = trim($this->ref);
1062  }
1063  if (isset($this->entity)) {
1064  $this->entity = (int) $this->entity;
1065  }
1066  if (isset($this->ref_customer)) {
1067  $this->ref_customer = trim($this->ref_customer);
1068  }
1069  if (isset($this->socid)) {
1070  $this->socid = (int) $this->socid;
1071  }
1072  if (isset($this->fk_user_author)) {
1073  $this->fk_user_author = (int) $this->fk_user_author;
1074  }
1075  if (isset($this->fk_user_valid)) {
1076  $this->fk_user_valid = (int) $this->fk_user_valid;
1077  }
1078  if (isset($this->fk_delivery_address)) {
1079  $this->fk_delivery_address = (int) $this->fk_delivery_address;
1080  }
1081  if (isset($this->shipping_method_id)) {
1082  $this->shipping_method_id = (int) $this->shipping_method_id;
1083  }
1084  if (isset($this->tracking_number)) {
1085  $this->tracking_number = trim($this->tracking_number);
1086  }
1087  if (isset($this->statut)) {
1088  $this->statut = (int) $this->statut;
1089  }
1090  if (isset($this->trueDepth)) {
1091  $this->trueDepth = trim($this->trueDepth);
1092  }
1093  if (isset($this->trueWidth)) {
1094  $this->trueWidth = trim($this->trueWidth);
1095  }
1096  if (isset($this->trueHeight)) {
1097  $this->trueHeight = trim($this->trueHeight);
1098  }
1099  if (isset($this->size_units)) {
1100  $this->size_units = trim($this->size_units);
1101  }
1102  if (isset($this->weight_units)) {
1103  $this->weight_units = trim($this->weight_units);
1104  }
1105  if (isset($this->trueWeight)) {
1106  $this->weight = trim($this->trueWeight);
1107  }
1108  if (isset($this->note_private)) {
1109  $this->note_private = trim($this->note_private);
1110  }
1111  if (isset($this->note_public)) {
1112  $this->note_public = trim($this->note_public);
1113  }
1114  if (isset($this->model_pdf)) {
1115  $this->model_pdf = trim($this->model_pdf);
1116  }
1117 
1118 
1119 
1120  // Check parameters
1121  // Put here code to add control on parameters values
1122 
1123  // Update request
1124  $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET";
1125 
1126  $sql .= " ref=".(isset($this->ref) ? "'".$this->db->escape($this->ref)."'" : "null").",";
1127  $sql .= " ref_ext=".(isset($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null").",";
1128  $sql .= " ref_customer=".(isset($this->ref_customer) ? "'".$this->db->escape($this->ref_customer)."'" : "null").",";
1129  $sql .= " fk_soc=".(isset($this->socid) ? $this->socid : "null").",";
1130  $sql .= " date_creation=".(dol_strlen($this->date_creation) != 0 ? "'".$this->db->idate($this->date_creation)."'" : 'null').",";
1131  $sql .= " fk_user_author=".(isset($this->fk_user_author) ? $this->fk_user_author : "null").",";
1132  $sql .= " date_valid=".(dol_strlen($this->date_valid) != 0 ? "'".$this->db->idate($this->date_valid)."'" : 'null').",";
1133  $sql .= " fk_user_valid=".(isset($this->fk_user_valid) ? $this->fk_user_valid : "null").",";
1134  $sql .= " date_expedition=".(dol_strlen($this->date_expedition) != 0 ? "'".$this->db->idate($this->date_expedition)."'" : 'null').",";
1135  $sql .= " date_delivery=".(dol_strlen($this->date_delivery) != 0 ? "'".$this->db->idate($this->date_delivery)."'" : 'null').",";
1136  $sql .= " fk_address=".(isset($this->fk_delivery_address) ? $this->fk_delivery_address : "null").",";
1137  $sql .= " fk_shipping_method=".((isset($this->shipping_method_id) && $this->shipping_method_id > 0) ? $this->shipping_method_id : "null").",";
1138  $sql .= " tracking_number=".(isset($this->tracking_number) ? "'".$this->db->escape($this->tracking_number)."'" : "null").",";
1139  $sql .= " fk_statut=".(isset($this->statut) ? $this->statut : "null").",";
1140  $sql .= " fk_projet=".(isset($this->fk_project) ? $this->fk_project : "null").",";
1141  $sql .= " height=".(($this->trueHeight != '') ? $this->trueHeight : "null").",";
1142  $sql .= " width=".(($this->trueWidth != '') ? $this->trueWidth : "null").",";
1143  $sql .= " size_units=".(isset($this->size_units) ? $this->size_units : "null").",";
1144  $sql .= " size=".(($this->trueDepth != '') ? $this->trueDepth : "null").",";
1145  $sql .= " weight_units=".(isset($this->weight_units) ? $this->weight_units : "null").",";
1146  $sql .= " weight=".(($this->trueWeight != '') ? $this->trueWeight : "null").",";
1147  $sql .= " note_private=".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : "null").",";
1148  $sql .= " note_public=".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : "null").",";
1149  $sql .= " model_pdf=".(isset($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null").",";
1150  $sql .= " entity=".$conf->entity;
1151 
1152  $sql .= " WHERE rowid=".((int) $this->id);
1153 
1154  $this->db->begin();
1155 
1156  dol_syslog(get_class($this)."::update", LOG_DEBUG);
1157  $resql = $this->db->query($sql);
1158  if (!$resql) {
1159  $error++; $this->errors[] = "Error ".$this->db->lasterror();
1160  }
1161 
1162  if (!$error && !$notrigger) {
1163  // Call trigger
1164  $result = $this->call_trigger('SHIPPING_MODIFY', $user);
1165  if ($result < 0) {
1166  $error++;
1167  }
1168  // End call triggers
1169  }
1170 
1171  // Commit or rollback
1172  if ($error) {
1173  foreach ($this->errors as $errmsg) {
1174  dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR);
1175  $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
1176  }
1177  $this->db->rollback();
1178  return -1 * $error;
1179  } else {
1180  $this->db->commit();
1181  return 1;
1182  }
1183  }
1184 
1185 
1193  public function cancel($notrigger = 0, $also_update_stock = false)
1194  {
1195  global $conf, $langs, $user;
1196 
1197  require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1198 
1199  $error = 0;
1200  $this->error = '';
1201 
1202  $this->db->begin();
1203 
1204  // Add a protection to refuse deleting if shipment has at least one delivery
1205  $this->fetchObjectLinked($this->id, 'shipping', 0, 'delivery'); // Get deliveries linked to this shipment
1206  if (count($this->linkedObjectsIds) > 0) {
1207  $this->error = 'ErrorThereIsSomeDeliveries';
1208  $error++;
1209  }
1210 
1211  if (!$error && !$notrigger) {
1212  // Call trigger
1213  $result = $this->call_trigger('SHIPPING_CANCEL', $user);
1214  if ($result < 0) {
1215  $error++;
1216  }
1217  // End call triggers
1218  }
1219 
1220  // Stock control
1221  if (!$error && isModEnabled('stock') &&
1222  (($conf->global->STOCK_CALCULATE_ON_SHIPMENT && $this->statut > self::STATUS_DRAFT) ||
1223  ($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE && $this->statut == self::STATUS_CLOSED && $also_update_stock))) {
1224  require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php";
1225 
1226  $langs->load("agenda");
1227 
1228  // Loop on each product line to add a stock movement and delete features
1229  $sql = "SELECT cd.fk_product, cd.subprice, ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id";
1230  $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,";
1231  $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed";
1232  $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
1233  $sql .= " AND cd.rowid = ed.fk_origin_line";
1234 
1235  dol_syslog(get_class($this)."::delete select details", LOG_DEBUG);
1236  $resql = $this->db->query($sql);
1237  if ($resql) {
1238  $cpt = $this->db->num_rows($resql);
1239 
1240  $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1241 
1242  for ($i = 0; $i < $cpt; $i++) {
1243  dol_syslog(get_class($this)."::delete movement index ".$i);
1244  $obj = $this->db->fetch_object($resql);
1245 
1246  $mouvS = new MouvementStock($this->db);
1247  // we do not log origin because it will be deleted
1248  $mouvS->origin = null;
1249  // get lot/serial
1250  $lotArray = null;
1251  if (isModEnabled('productbatch')) {
1252  $lotArray = $shipmentlinebatch->fetchAll($obj->expeditiondet_id);
1253  if (!is_array($lotArray)) {
1254  $error++;
1255  $this->errors[] = "Error ".$this->db->lasterror();
1256  }
1257  }
1258 
1259  if (empty($lotArray)) {
1260  // no lot/serial
1261  // We increment stock of product (and sub-products)
1262  // We use warehouse selected for each line
1263  $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
1264  if ($result < 0) {
1265  $error++;
1266  $this->errors = array_merge($this->errors, $mouvS->errors);
1267  break;
1268  }
1269  } else {
1270  // We increment stock of batches
1271  // We use warehouse selected for each line
1272  foreach ($lotArray as $lot) {
1273  $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
1274  if ($result < 0) {
1275  $error++;
1276  $this->errors = array_merge($this->errors, $mouvS->errors);
1277  break;
1278  }
1279  }
1280  if ($error) {
1281  break; // break for loop incase of error
1282  }
1283  }
1284  }
1285  } else {
1286  $error++; $this->errors[] = "Error ".$this->db->lasterror();
1287  }
1288  }
1289 
1290  // delete batch expedition line
1291  if (!$error && isModEnabled('productbatch')) {
1292  $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1293  if ($shipmentlinebatch->deleteFromShipment($this->id) < 0) {
1294  $error++; $this->errors[] = "Error ".$this->db->lasterror();
1295  }
1296  }
1297 
1298 
1299  if (!$error) {
1300  $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet";
1301  $sql .= " WHERE fk_expedition = ".((int) $this->id);
1302 
1303  if ($this->db->query($sql)) {
1304  // Delete linked object
1305  $res = $this->deleteObjectLinked();
1306  if ($res < 0) {
1307  $error++;
1308  }
1309 
1310  // No delete expedition
1311  if (!$error) {
1312  $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."expedition";
1313  $sql .= " WHERE rowid = ".((int) $this->id);
1314 
1315  if ($this->db->query($sql)) {
1316  if (!empty($this->origin) && $this->origin_id > 0) {
1317  $this->fetch_origin();
1318  $origin = $this->origin;
1319  if ($this->$origin->statut == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress"
1320  // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress"
1321  $this->$origin->loadExpeditions();
1322  //var_dump($this->$origin->expeditions);exit;
1323  if (count($this->$origin->expeditions) <= 0) {
1324  $this->$origin->setStatut(Commande::STATUS_VALIDATED);
1325  }
1326  }
1327  }
1328 
1329  if (!$error) {
1330  $this->db->commit();
1331 
1332  // We delete PDFs
1333  $ref = dol_sanitizeFileName($this->ref);
1334  if (!empty($conf->expedition->dir_output)) {
1335  $dir = $conf->expedition->dir_output.'/sending/'.$ref;
1336  $file = $dir.'/'.$ref.'.pdf';
1337  if (file_exists($file)) {
1338  if (!dol_delete_file($file)) {
1339  return 0;
1340  }
1341  }
1342  if (file_exists($dir)) {
1343  if (!dol_delete_dir_recursive($dir)) {
1344  $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir);
1345  return 0;
1346  }
1347  }
1348  }
1349 
1350  return 1;
1351  } else {
1352  $this->db->rollback();
1353  return -1;
1354  }
1355  } else {
1356  $this->error = $this->db->lasterror()." - sql=$sql";
1357  $this->db->rollback();
1358  return -3;
1359  }
1360  } else {
1361  $this->error = $this->db->lasterror()." - sql=$sql";
1362  $this->db->rollback();
1363  return -2;
1364  }//*/
1365  } else {
1366  $this->error = $this->db->lasterror()." - sql=$sql";
1367  $this->db->rollback();
1368  return -1;
1369  }
1370  } else {
1371  $this->db->rollback();
1372  return -1;
1373  }
1374  }
1375 
1384  public function delete($notrigger = 0, $also_update_stock = false)
1385  {
1386  global $conf, $langs, $user;
1387 
1388  require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1389 
1390  $error = 0;
1391  $this->error = '';
1392 
1393  $this->db->begin();
1394 
1395  // Add a protection to refuse deleting if shipment has at least one delivery
1396  $this->fetchObjectLinked($this->id, 'shipping', 0, 'delivery'); // Get deliveries linked to this shipment
1397  if (count($this->linkedObjectsIds) > 0) {
1398  $this->error = 'ErrorThereIsSomeDeliveries';
1399  $error++;
1400  }
1401 
1402  if (!$error && !$notrigger) {
1403  // Call trigger
1404  $result = $this->call_trigger('SHIPPING_DELETE', $user);
1405  if ($result < 0) {
1406  $error++;
1407  }
1408  // End call triggers
1409  }
1410 
1411  // Stock control
1412  if (!$error && isModEnabled('stock') &&
1413  (($conf->global->STOCK_CALCULATE_ON_SHIPMENT && $this->statut > self::STATUS_DRAFT) ||
1414  ($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE && $this->statut == self::STATUS_CLOSED && $also_update_stock))) {
1415  require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php";
1416 
1417  $langs->load("agenda");
1418 
1419  // we try deletion of batch line even if module batch not enabled in case of the module were enabled and disabled previously
1420  $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1421 
1422  // Loop on each product line to add a stock movement
1423  $sql = "SELECT cd.fk_product, cd.subprice, ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id";
1424  $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,";
1425  $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed";
1426  $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
1427  $sql .= " AND cd.rowid = ed.fk_origin_line";
1428 
1429  dol_syslog(get_class($this)."::delete select details", LOG_DEBUG);
1430  $resql = $this->db->query($sql);
1431  if ($resql) {
1432  $cpt = $this->db->num_rows($resql);
1433  for ($i = 0; $i < $cpt; $i++) {
1434  dol_syslog(get_class($this)."::delete movement index ".$i);
1435  $obj = $this->db->fetch_object($resql);
1436 
1437  $mouvS = new MouvementStock($this->db);
1438  // we do not log origin because it will be deleted
1439  $mouvS->origin = null;
1440  // get lot/serial
1441  $lotArray = $shipmentlinebatch->fetchAll($obj->expeditiondet_id);
1442  if (!is_array($lotArray)) {
1443  $error++; $this->errors[] = "Error ".$this->db->lasterror();
1444  }
1445  if (empty($lotArray)) {
1446  // no lot/serial
1447  // We increment stock of product (and sub-products)
1448  // We use warehouse selected for each line
1449  $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
1450  if ($result < 0) {
1451  $error++;
1452  $this->errors = array_merge($this->errors, $mouvS->errors);
1453  break;
1454  }
1455  } else {
1456  // We increment stock of batches
1457  // We use warehouse selected for each line
1458  foreach ($lotArray as $lot) {
1459  $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
1460  if ($result < 0) {
1461  $error++;
1462  $this->errors = array_merge($this->errors, $mouvS->errors);
1463  break;
1464  }
1465  }
1466  if ($error) {
1467  break; // break for loop incase of error
1468  }
1469  }
1470  }
1471  } else {
1472  $error++; $this->errors[] = "Error ".$this->db->lasterror();
1473  }
1474  }
1475 
1476  // delete batch expedition line
1477  if (!$error) {
1478  $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1479  if ($shipmentlinebatch->deleteFromShipment($this->id) < 0) {
1480  $error++; $this->errors[] = "Error ".$this->db->lasterror();
1481  }
1482  }
1483 
1484  if (!$error) {
1485  $main = MAIN_DB_PREFIX.'expeditiondet';
1486  $ef = $main."_extrafields";
1487  $sqlef = "DELETE FROM $ef WHERE fk_object IN (SELECT rowid FROM $main WHERE fk_expedition = ".((int) $this->id).")";
1488 
1489  $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet";
1490  $sql .= " WHERE fk_expedition = ".((int) $this->id);
1491 
1492  if ($this->db->query($sqlef) && $this->db->query($sql)) {
1493  // Delete linked object
1494  $res = $this->deleteObjectLinked();
1495  if ($res < 0) {
1496  $error++;
1497  }
1498 
1499  // delete extrafields
1500  $res = $this->deleteExtraFields();
1501  if ($res < 0) {
1502  $error++;
1503  }
1504 
1505  if (!$error) {
1506  $sql = "DELETE FROM ".MAIN_DB_PREFIX."expedition";
1507  $sql .= " WHERE rowid = ".((int) $this->id);
1508 
1509  if ($this->db->query($sql)) {
1510  if (!empty($this->origin) && $this->origin_id > 0) {
1511  $this->fetch_origin();
1512  $origin = $this->origin;
1513  if ($this->$origin->statut == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress"
1514  // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress"
1515  $this->$origin->loadExpeditions();
1516  //var_dump($this->$origin->expeditions);exit;
1517  if (count($this->$origin->expeditions) <= 0) {
1518  $this->$origin->setStatut(Commande::STATUS_VALIDATED);
1519  }
1520  }
1521  }
1522 
1523  if (!$error) {
1524  $this->db->commit();
1525 
1526  // Delete record into ECM index (Note that delete is also done when deleting files with the dol_delete_dir_recursive
1527  $this->deleteEcmFiles();
1528 
1529  // We delete PDFs
1530  $ref = dol_sanitizeFileName($this->ref);
1531  if (!empty($conf->expedition->dir_output)) {
1532  $dir = $conf->expedition->dir_output.'/sending/'.$ref;
1533  $file = $dir.'/'.$ref.'.pdf';
1534  if (file_exists($file)) {
1535  if (!dol_delete_file($file)) {
1536  return 0;
1537  }
1538  }
1539  if (file_exists($dir)) {
1540  if (!dol_delete_dir_recursive($dir)) {
1541  $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir);
1542  return 0;
1543  }
1544  }
1545  }
1546 
1547  return 1;
1548  } else {
1549  $this->db->rollback();
1550  return -1;
1551  }
1552  } else {
1553  $this->error = $this->db->lasterror()." - sql=$sql";
1554  $this->db->rollback();
1555  return -3;
1556  }
1557  } else {
1558  $this->error = $this->db->lasterror()." - sql=$sql";
1559  $this->db->rollback();
1560  return -2;
1561  }
1562  } else {
1563  $this->error = $this->db->lasterror()." - sql=$sql";
1564  $this->db->rollback();
1565  return -1;
1566  }
1567  } else {
1568  $this->db->rollback();
1569  return -1;
1570  }
1571  }
1572 
1573  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1579  public function fetch_lines()
1580  {
1581  // phpcs:enable
1582  global $conf, $mysoc;
1583 
1584  $this->lines = array();
1585 
1586  // NOTE: This fetch_lines is special because it groups all lines with the same origin_line_id into one line.
1587  // TODO: See if we can restore a common fetch_lines (one line = one record)
1588 
1589  $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";
1590  $sql .= ", cd.total_ht, cd.total_localtax1, cd.total_localtax2, cd.total_ttc, cd.total_tva";
1591  $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";
1592  $sql .= ", cd.fk_multicurrency, cd.multicurrency_code, cd.multicurrency_subprice, cd.multicurrency_total_ht, cd.multicurrency_total_tva, cd.multicurrency_total_ttc, cd.rang";
1593  $sql .= ", ed.rowid as line_id, ed.qty as qty_shipped, ed.fk_origin_line, ed.fk_entrepot";
1594  $sql .= ", p.ref as product_ref, p.label as product_label, p.fk_product_type";
1595  $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";
1596  $sql .= " FROM ".MAIN_DB_PREFIX."expeditiondet as ed, ".MAIN_DB_PREFIX."commandedet as cd";
1597  $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product";
1598  $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
1599  $sql .= " AND ed.fk_origin_line = cd.rowid";
1600  $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.
1601 
1602  dol_syslog(get_class($this)."::fetch_lines", LOG_DEBUG);
1603  $resql = $this->db->query($sql);
1604  if ($resql) {
1605  include_once DOL_DOCUMENT_ROOT.'/core/lib/price.lib.php';
1606 
1607  $num = $this->db->num_rows($resql);
1608  $i = 0;
1609  $lineindex = 0;
1610  $originline = 0;
1611 
1612  $this->total_ht = 0;
1613  $this->total_tva = 0;
1614  $this->total_ttc = 0;
1615  $this->total_localtax1 = 0;
1616  $this->total_localtax2 = 0;
1617 
1618  $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1619 
1620  while ($i < $num) {
1621  $obj = $this->db->fetch_object($resql);
1622 
1623  if ($originline > 0 && $originline == $obj->fk_origin_line) {
1624  $line->entrepot_id = 0; // entrepod_id in details_entrepot
1625  $line->qty_shipped += $obj->qty_shipped;
1626  } else {
1627  $line = new ExpeditionLigne($this->db);
1628  $line->entrepot_id = $obj->fk_entrepot; // this is a property of a shipment line
1629  $line->qty_shipped = $obj->qty_shipped; // this is a property of a shipment line
1630  }
1631 
1632  $detail_entrepot = new stdClass();
1633  $detail_entrepot->entrepot_id = $obj->fk_entrepot;
1634  $detail_entrepot->qty_shipped = $obj->qty_shipped;
1635  $detail_entrepot->line_id = $obj->line_id;
1636  $line->details_entrepot[] = $detail_entrepot;
1637 
1638  $line->line_id = $obj->line_id;
1639  $line->rowid = $obj->line_id; // TODO deprecated
1640  $line->id = $obj->line_id;
1641 
1642  $line->fk_origin = 'orderline';
1643  $line->fk_origin_line = $obj->fk_origin_line;
1644  $line->origin_line_id = $obj->fk_origin_line; // TODO deprecated
1645 
1646  $line->fk_expedition = $this->id; // id of parent
1647 
1648  $line->product_type = $obj->product_type;
1649  $line->fk_product = $obj->fk_product;
1650  $line->fk_product_type = $obj->fk_product_type;
1651  $line->ref = $obj->product_ref; // TODO deprecated
1652  $line->product_ref = $obj->product_ref;
1653  $line->product_label = $obj->product_label;
1654  $line->libelle = $obj->product_label; // TODO deprecated
1655  $line->product_tosell = $obj->product_tosell;
1656  $line->product_tobuy = $obj->product_tobuy;
1657  $line->product_tobatch = $obj->product_tobatch;
1658  $line->label = $obj->custom_label;
1659  $line->description = $obj->description;
1660  $line->qty_asked = $obj->qty_asked;
1661  $line->rang = $obj->rang;
1662  $line->weight = $obj->weight;
1663  $line->weight_units = $obj->weight_units;
1664  $line->length = $obj->length;
1665  $line->length_units = $obj->length_units;
1666  $line->surface = $obj->surface;
1667  $line->surface_units = $obj->surface_units;
1668  $line->volume = $obj->volume;
1669  $line->volume_units = $obj->volume_units;
1670  $line->fk_unit = $obj->fk_unit;
1671 
1672  $line->pa_ht = $obj->pa_ht;
1673 
1674  // Local taxes
1675  $localtax_array = array(0=>$obj->localtax1_type, 1=>$obj->localtax1_tx, 2=>$obj->localtax2_type, 3=>$obj->localtax2_tx);
1676  $localtax1_tx = get_localtax($obj->tva_tx, 1, $this->thirdparty);
1677  $localtax2_tx = get_localtax($obj->tva_tx, 2, $this->thirdparty);
1678 
1679  // For invoicing
1680  $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
1681  $line->desc = $obj->description; // We need ->desc because some code into CommonObject use desc (property defined for other elements)
1682  $line->qty = $line->qty_shipped;
1683  $line->total_ht = $tabprice[0];
1684  $line->total_localtax1 = $tabprice[9];
1685  $line->total_localtax2 = $tabprice[10];
1686  $line->total_ttc = $tabprice[2];
1687  $line->total_tva = $tabprice[1];
1688  $line->vat_src_code = $obj->vat_src_code;
1689  $line->tva_tx = $obj->tva_tx;
1690  $line->localtax1_tx = $obj->localtax1_tx;
1691  $line->localtax2_tx = $obj->localtax2_tx;
1692  $line->info_bits = $obj->info_bits;
1693  $line->price = $obj->price;
1694  $line->subprice = $obj->subprice;
1695  $line->remise_percent = $obj->remise_percent;
1696 
1697  $this->total_ht += $tabprice[0];
1698  $this->total_tva += $tabprice[1];
1699  $this->total_ttc += $tabprice[2];
1700  $this->total_localtax1 += $tabprice[9];
1701  $this->total_localtax2 += $tabprice[10];
1702 
1703  // Multicurrency
1704  $this->fk_multicurrency = $obj->fk_multicurrency;
1705  $this->multicurrency_code = $obj->multicurrency_code;
1706  $this->multicurrency_subprice = $obj->multicurrency_subprice;
1707  $this->multicurrency_total_ht = $obj->multicurrency_total_ht;
1708  $this->multicurrency_total_tva = $obj->multicurrency_total_tva;
1709  $this->multicurrency_total_ttc = $obj->multicurrency_total_ttc;
1710 
1711  if ($originline != $obj->fk_origin_line) {
1712  $line->detail_batch = array();
1713  }
1714 
1715  // Detail of batch
1716  if (isModEnabled('productbatch') && $obj->line_id > 0 && $obj->product_tobatch > 0) {
1717  $newdetailbatch = $shipmentlinebatch->fetchAll($obj->line_id, $obj->fk_product);
1718 
1719  if (is_array($newdetailbatch)) {
1720  if ($originline != $obj->fk_origin_line) {
1721  $line->detail_batch = $newdetailbatch;
1722  } else {
1723  $line->detail_batch = array_merge($line->detail_batch, $newdetailbatch);
1724  }
1725  }
1726  }
1727 
1728  $line->fetch_optionals();
1729 
1730  if ($originline != $obj->fk_origin_line) {
1731  $this->lines[$lineindex] = $line;
1732  $lineindex++;
1733  } else {
1734  $line->total_ht += $tabprice[0];
1735  $line->total_localtax1 += $tabprice[9];
1736  $line->total_localtax2 += $tabprice[10];
1737  $line->total_ttc += $tabprice[2];
1738  $line->total_tva += $tabprice[1];
1739  }
1740 
1741  $i++;
1742  $originline = $obj->fk_origin_line;
1743  }
1744  $this->db->free($resql);
1745  return 1;
1746  } else {
1747  $this->error = $this->db->error();
1748  return -3;
1749  }
1750  }
1751 
1759  public function deleteline($user, $lineid)
1760  {
1761  global $user;
1762 
1763  if ($this->statut == self::STATUS_DRAFT) {
1764  $this->db->begin();
1765 
1766  $line = new ExpeditionLigne($this->db);
1767 
1768  // For triggers
1769  $line->fetch($lineid);
1770 
1771  if ($line->delete($user) > 0) {
1772  //$this->update_price(1);
1773 
1774  $this->db->commit();
1775  return 1;
1776  } else {
1777  $this->db->rollback();
1778  return -1;
1779  }
1780  } else {
1781  $this->error = 'ErrorDeleteLineNotAllowedByObjectStatus';
1782  return -2;
1783  }
1784  }
1785 
1786 
1798  public function getNomUrl($withpicto = 0, $option = '', $max = 0, $short = 0, $notooltip = 0, $save_lastsearch_value = -1)
1799  {
1800  global $langs, $conf, $hookmanager;
1801 
1802  $result = '';
1803  $label = '<u>'.$langs->trans("Shipment").'</u>';
1804  $label .= '<br><b>'.$langs->trans('Ref').':</b> '.$this->ref;
1805  $label .= '<br><b>'.$langs->trans('RefCustomer').':</b> '.($this->ref_customer ? $this->ref_customer : $this->ref_client);
1806 
1807  $url = DOL_URL_ROOT.'/expedition/card.php?id='.$this->id;
1808 
1809  if ($short) {
1810  return $url;
1811  }
1812 
1813  if ($option !== 'nolink') {
1814  // Add param to save lastsearch_values or not
1815  $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
1816  if ($save_lastsearch_value == -1 && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
1817  $add_save_lastsearch_values = 1;
1818  }
1819  if ($add_save_lastsearch_values) {
1820  $url .= '&save_lastsearch_values=1';
1821  }
1822  }
1823 
1824  $linkclose = '';
1825  if (empty($notooltip)) {
1826  if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER)) {
1827  $label = $langs->trans("Shipment");
1828  $linkclose .= ' alt="'.dol_escape_htmltag($label, 1).'"';
1829  }
1830  $linkclose .= ' title="'.dol_escape_htmltag($label, 1).'"';
1831  $linkclose .= ' class="classfortooltip"';
1832  }
1833 
1834  $linkstart = '<a href="'.$url.'"';
1835  $linkstart .= $linkclose.'>';
1836  $linkend = '</a>';
1837 
1838  $result .= $linkstart;
1839  if ($withpicto) {
1840  $result .= img_object(($notooltip ? '' : $label), $this->picto, ($notooltip ? (($withpicto != 2) ? 'class="paddingright"' : '') : 'class="'.(($withpicto != 2) ? 'paddingright ' : '').'classfortooltip"'), 0, 0, $notooltip ? 0 : 1);
1841  }
1842  if ($withpicto != 2) {
1843  $result .= $this->ref;
1844  }
1845  $result .= $linkend;
1846  global $action;
1847  $hookmanager->initHooks(array($this->element . 'dao'));
1848  $parameters = array('id'=>$this->id, 'getnomurl' => &$result);
1849  $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
1850  if ($reshook > 0) {
1851  $result = $hookmanager->resPrint;
1852  } else {
1853  $result .= $hookmanager->resPrint;
1854  }
1855  return $result;
1856  }
1857 
1864  public function getLibStatut($mode = 0)
1865  {
1866  return $this->LibStatut($this->statut, $mode);
1867  }
1868 
1869  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1877  public function LibStatut($status, $mode)
1878  {
1879  // phpcs:enable
1880  global $langs;
1881 
1882  $labelStatus = $langs->transnoentitiesnoconv($this->statuts[$status]);
1883  $labelStatusShort = $langs->transnoentitiesnoconv($this->statuts_short[$status]);
1884 
1885  $statusType = 'status'.$status;
1886  if ($status == self::STATUS_VALIDATED) {
1887  $statusType = 'status4';
1888  }
1889  if ($status == self::STATUS_CLOSED) {
1890  $statusType = 'status6';
1891  }
1892  if ($status == self::STATUS_CANCELED) {
1893  $statusType = 'status9';
1894  }
1895 
1896  return dolGetStatus($labelStatus, $labelStatusShort, '', $statusType, $mode);
1897  }
1898 
1906  public function initAsSpecimen()
1907  {
1908  global $langs;
1909 
1910  $now = dol_now();
1911 
1912  dol_syslog(get_class($this)."::initAsSpecimen");
1913 
1914  $order = new Commande($this->db);
1915  $order->initAsSpecimen();
1916 
1917  // Initialise parametres
1918  $this->id = 0;
1919  $this->ref = 'SPECIMEN';
1920  $this->specimen = 1;
1921  $this->statut = self::STATUS_VALIDATED;
1922  $this->livraison_id = 0;
1923  $this->date = $now;
1924  $this->date_creation = $now;
1925  $this->date_valid = $now;
1926  $this->date_delivery = $now + 24 * 3600;
1927  $this->date_expedition = $now + 24 * 3600;
1928 
1929  $this->entrepot_id = 0;
1930  $this->fk_delivery_address = 0;
1931  $this->socid = 1;
1932 
1933  $this->commande_id = 0;
1934  $this->commande = $order;
1935 
1936  $this->origin_id = 1;
1937  $this->origin = 'commande';
1938 
1939  $this->note_private = 'Private note';
1940  $this->note_public = 'Public note';
1941 
1942  $nbp = 5;
1943  $xnbp = 0;
1944  while ($xnbp < $nbp) {
1945  $line = new ExpeditionLigne($this->db);
1946  $line->desc = $langs->trans("Description")." ".$xnbp;
1947  $line->libelle = $langs->trans("Description")." ".$xnbp; // deprecated
1948  $line->label = $langs->trans("Description")." ".$xnbp;
1949  $line->qty = 10;
1950  $line->qty_asked = 5;
1951  $line->qty_shipped = 4;
1952  $line->fk_product = $this->commande->lines[$xnbp]->fk_product;
1953 
1954  $this->lines[] = $line;
1955  $xnbp++;
1956  }
1957  }
1958 
1959  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1968  public function set_date_livraison($user, $delivery_date)
1969  {
1970  // phpcs:enable
1971  return $this->setDeliveryDate($user, $delivery_date);
1972  }
1973 
1981  public function setDeliveryDate($user, $delivery_date)
1982  {
1983  if ($user->rights->expedition->creer) {
1984  $sql = "UPDATE ".MAIN_DB_PREFIX."expedition";
1985  $sql .= " SET date_delivery = ".($delivery_date ? "'".$this->db->idate($delivery_date)."'" : 'null');
1986  $sql .= " WHERE rowid = ".((int) $this->id);
1987 
1988  dol_syslog(get_class($this)."::setDeliveryDate", LOG_DEBUG);
1989  $resql = $this->db->query($sql);
1990  if ($resql) {
1991  $this->date_delivery = $delivery_date;
1992  return 1;
1993  } else {
1994  $this->error = $this->db->error();
1995  return -1;
1996  }
1997  } else {
1998  return -2;
1999  }
2000  }
2001 
2002  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2008  public function fetch_delivery_methods()
2009  {
2010  // phpcs:enable
2011  global $langs;
2012  $this->meths = array();
2013 
2014  $sql = "SELECT em.rowid, em.code, em.libelle as label";
2015  $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2016  $sql .= " WHERE em.active = 1";
2017  $sql .= " ORDER BY em.libelle ASC";
2018 
2019  $resql = $this->db->query($sql);
2020  if ($resql) {
2021  while ($obj = $this->db->fetch_object($resql)) {
2022  $label = $langs->trans('SendingMethod'.$obj->code);
2023  $this->meths[$obj->rowid] = ($label != 'SendingMethod'.$obj->code ? $label : $obj->label);
2024  }
2025  }
2026  }
2027 
2028  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2035  public function list_delivery_methods($id = '')
2036  {
2037  // phpcs:enable
2038  global $langs;
2039 
2040  $this->listmeths = array();
2041  $i = 0;
2042 
2043  $sql = "SELECT em.rowid, em.code, em.libelle as label, em.description, em.tracking, em.active";
2044  $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2045  if ($id != '') {
2046  $sql .= " WHERE em.rowid=".((int) $id);
2047  }
2048 
2049  $resql = $this->db->query($sql);
2050  if ($resql) {
2051  while ($obj = $this->db->fetch_object($resql)) {
2052  $this->listmeths[$i]['rowid'] = $obj->rowid;
2053  $this->listmeths[$i]['code'] = $obj->code;
2054  $label = $langs->trans('SendingMethod'.$obj->code);
2055  $this->listmeths[$i]['libelle'] = ($label != 'SendingMethod'.$obj->code ? $label : $obj->label);
2056  $this->listmeths[$i]['description'] = $obj->description;
2057  $this->listmeths[$i]['tracking'] = $obj->tracking;
2058  $this->listmeths[$i]['active'] = $obj->active;
2059  $i++;
2060  }
2061  }
2062  }
2063 
2070  public function getUrlTrackingStatus($value = '')
2071  {
2072  if (!empty($this->shipping_method_id)) {
2073  $sql = "SELECT em.code, em.tracking";
2074  $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2075  $sql .= " WHERE em.rowid = ".((int) $this->shipping_method_id);
2076 
2077  $resql = $this->db->query($sql);
2078  if ($resql) {
2079  if ($obj = $this->db->fetch_object($resql)) {
2080  $tracking = $obj->tracking;
2081  }
2082  }
2083  }
2084 
2085  if (!empty($tracking) && !empty($value)) {
2086  $url = str_replace('{TRACKID}', $value, $tracking);
2087  $this->tracking_url = sprintf('<a target="_blank" rel="noopener noreferrer" href="%s">'.($value ? $value : 'url').'</a>', $url, $url);
2088  } else {
2089  $this->tracking_url = $value;
2090  }
2091  }
2092 
2098  public function setClosed()
2099  {
2100  global $conf, $langs, $user;
2101 
2102  $error = 0;
2103 
2104  // Protection. This avoid to move stock later when we should not
2105  if ($this->statut == self::STATUS_CLOSED) {
2106  return 0;
2107  }
2108 
2109  $this->db->begin();
2110 
2111  $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET fk_statut = ".self::STATUS_CLOSED;
2112  $sql .= " WHERE rowid = ".((int) $this->id)." AND fk_statut > 0";
2113 
2114  $resql = $this->db->query($sql);
2115  if ($resql) {
2116  // Set order billed if 100% of order is shipped (qty in shipment lines match qty in order lines)
2117  if ($this->origin == 'commande' && $this->origin_id > 0) {
2118  $order = new Commande($this->db);
2119  $order->fetch($this->origin_id);
2120 
2121  $order->loadExpeditions(self::STATUS_CLOSED); // Fill $order->expeditions = array(orderlineid => qty)
2122 
2123  $shipments_match_order = 1;
2124  foreach ($order->lines as $line) {
2125  $lineid = $line->id;
2126  $qty = $line->qty;
2127  if (($line->product_type == 0 || !empty($conf->global->STOCK_SUPPORTS_SERVICES)) && $order->expeditions[$lineid] != $qty) {
2128  $shipments_match_order = 0;
2129  $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';
2130  dol_syslog($text);
2131  break;
2132  }
2133  }
2134  if ($shipments_match_order) {
2135  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');
2136  // We close the order
2137  $order->cloture($user); // Note this may also create an invoice if module workflow ask it
2138  }
2139  }
2140 
2141  $this->statut = self::STATUS_CLOSED; // Will be revert to STATUS_VALIDATED at end if there is a rollback
2142  $this->status = self::STATUS_CLOSED; // Will be revert to STATUS_VALIDATED at end if there is a rollback
2143 
2144  // If stock increment is done on closing
2145  if (!$error && isModEnabled('stock') && !empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE)) {
2146  require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
2147 
2148  $langs->load("agenda");
2149 
2150  // Loop on each product line to add a stock movement
2151  // TODO possibilite d'expedier a partir d'une propale ou autre origine ?
2152  $sql = "SELECT cd.fk_product, cd.subprice,";
2153  $sql .= " ed.rowid, ed.qty, ed.fk_entrepot,";
2154  $sql .= " e.ref,";
2155  $sql .= " edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock";
2156  $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,";
2157  $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed";
2158  $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid";
2159  $sql .= " INNER JOIN ".MAIN_DB_PREFIX."expedition as e ON ed.fk_expedition = e.rowid";
2160  $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
2161  $sql .= " AND cd.rowid = ed.fk_origin_line";
2162 
2163  dol_syslog(get_class($this)."::valid select details", LOG_DEBUG);
2164  $resql = $this->db->query($sql);
2165  if ($resql) {
2166  $cpt = $this->db->num_rows($resql);
2167  for ($i = 0; $i < $cpt; $i++) {
2168  $obj = $this->db->fetch_object($resql);
2169  if (empty($obj->edbrowid)) {
2170  $qty = $obj->qty;
2171  } else {
2172  $qty = $obj->edbqty;
2173  }
2174  if ($qty <= 0) {
2175  continue;
2176  }
2177  dol_syslog(get_class($this)."::valid movement index ".$i." ed.rowid=".$obj->rowid." edb.rowid=".$obj->edbrowid);
2178 
2179  $mouvS = new MouvementStock($this->db);
2180  $mouvS->origin = &$this;
2181  $mouvS->setOrigin($this->element, $this->id);
2182 
2183  if (empty($obj->edbrowid)) {
2184  // line without batch detail
2185 
2186  // 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
2187  $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, $qty, $obj->subprice, $langs->trans("ShipmentClassifyClosedInDolibarr", $obj->ref));
2188  if ($result < 0) {
2189  $this->error = $mouvS->error;
2190  $this->errors = $mouvS->errors;
2191  $error++;
2192  break;
2193  }
2194  } else {
2195  // line with batch detail
2196 
2197  // 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
2198  $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);
2199  if ($result < 0) {
2200  $this->error = $mouvS->error;
2201  $this->errors = $mouvS->errors;
2202  $error++;
2203  break;
2204  }
2205  }
2206  }
2207  } else {
2208  $this->error = $this->db->lasterror();
2209  $error++;
2210  }
2211  }
2212 
2213  // Call trigger
2214  if (!$error) {
2215  $result = $this->call_trigger('SHIPPING_CLOSED', $user);
2216  if ($result < 0) {
2217  $error++;
2218  }
2219  }
2220  } else {
2221  dol_print_error($this->db);
2222  $error++;
2223  }
2224 
2225  if (!$error) {
2226  $this->db->commit();
2227  return 1;
2228  } else {
2229  $this->statut = self::STATUS_VALIDATED;
2230  $this->status = self::STATUS_VALIDATED;
2231 
2232  $this->db->rollback();
2233  return -1;
2234  }
2235  }
2236 
2242  public function setBilled()
2243  {
2244  global $user;
2245  $error = 0;
2246 
2247  $this->db->begin();
2248 
2249  $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET fk_statut=2, billed=1'; // TODO Update only billed
2250  $sql .= " WHERE rowid = ".((int) $this->id).' AND fk_statut > 0';
2251 
2252  $resql = $this->db->query($sql);
2253  if ($resql) {
2254  $this->statut = self::STATUS_CLOSED;
2255  $this->billed = 1;
2256 
2257  // Call trigger
2258  $result = $this->call_trigger('SHIPPING_BILLED', $user);
2259  if ($result < 0) {
2260  $error++;
2261  }
2262  } else {
2263  $error++;
2264  $this->errors[] = $this->db->lasterror;
2265  }
2266 
2267  if (empty($error)) {
2268  $this->db->commit();
2269  return 1;
2270  } else {
2271  $this->statut = self::STATUS_VALIDATED;
2272  $this->billed = 0;
2273  $this->db->rollback();
2274  return -1;
2275  }
2276  }
2277 
2283  public function reOpen()
2284  {
2285  global $conf, $langs, $user;
2286 
2287  $error = 0;
2288 
2289  // Protection. This avoid to move stock later when we should not
2290  if ($this->statut == self::STATUS_VALIDATED) {
2291  return 0;
2292  }
2293 
2294  $this->db->begin();
2295 
2296  $oldbilled = $this->billed;
2297 
2298  $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET fk_statut=1';
2299  $sql .= " WHERE rowid = ".((int) $this->id).' AND fk_statut > 0';
2300 
2301  $resql = $this->db->query($sql);
2302  if ($resql) {
2303  $this->statut = self::STATUS_VALIDATED;
2304  $this->billed = 0;
2305 
2306  // If stock increment is done on closing
2307  if (!$error && isModEnabled('stock') && !empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE)) {
2308  require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
2309 
2310  $langs->load("agenda");
2311 
2312  // Loop on each product line to add a stock movement
2313  // TODO possibilite d'expedier a partir d'une propale ou autre origine
2314  $sql = "SELECT cd.fk_product, cd.subprice,";
2315  $sql .= " ed.rowid, ed.qty, ed.fk_entrepot,";
2316  $sql .= " edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock";
2317  $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,";
2318  $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed";
2319  $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid";
2320  $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
2321  $sql .= " AND cd.rowid = ed.fk_origin_line";
2322 
2323  dol_syslog(get_class($this)."::valid select details", LOG_DEBUG);
2324  $resql = $this->db->query($sql);
2325  if ($resql) {
2326  $cpt = $this->db->num_rows($resql);
2327  for ($i = 0; $i < $cpt; $i++) {
2328  $obj = $this->db->fetch_object($resql);
2329  if (empty($obj->edbrowid)) {
2330  $qty = $obj->qty;
2331  } else {
2332  $qty = $obj->edbqty;
2333  }
2334  if ($qty <= 0) {
2335  continue;
2336  }
2337  dol_syslog(get_class($this)."::reopen expedition movement index ".$i." ed.rowid=".$obj->rowid." edb.rowid=".$obj->edbrowid);
2338 
2339  //var_dump($this->lines[$i]);
2340  $mouvS = new MouvementStock($this->db);
2341  $mouvS->origin = &$this;
2342  $mouvS->setOrigin($this->element, $this->id);
2343 
2344  if (empty($obj->edbrowid)) {
2345  // line without batch detail
2346 
2347  // 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
2348  $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, -$qty, $obj->subprice, $langs->trans("ShipmentUnClassifyCloseddInDolibarr", $numref));
2349  if ($result < 0) {
2350  $this->error = $mouvS->error;
2351  $this->errors = $mouvS->errors;
2352  $error++;
2353  break;
2354  }
2355  } else {
2356  // line with batch detail
2357 
2358  // 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
2359  $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);
2360  if ($result < 0) {
2361  $this->error = $mouvS->error;
2362  $this->errors = $mouvS->errors;
2363  $error++;
2364  break;
2365  }
2366  }
2367  }
2368  } else {
2369  $this->error = $this->db->lasterror();
2370  $error++;
2371  }
2372  }
2373 
2374  if (!$error) {
2375  // Call trigger
2376  $result = $this->call_trigger('SHIPPING_REOPEN', $user);
2377  if ($result < 0) {
2378  $error++;
2379  }
2380  }
2381  } else {
2382  $error++;
2383  $this->errors[] = $this->db->lasterror();
2384  }
2385 
2386  if (!$error) {
2387  $this->db->commit();
2388  return 1;
2389  } else {
2390  $this->statut = self::STATUS_CLOSED;
2391  $this->billed = $oldbilled;
2392  $this->db->rollback();
2393  return -1;
2394  }
2395  }
2396 
2408  public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null)
2409  {
2410  global $conf;
2411 
2412  $outputlangs->load("products");
2413 
2414  if (!dol_strlen($modele)) {
2415  $modele = 'rouget';
2416 
2417  if (!empty($this->model_pdf)) {
2418  $modele = $this->model_pdf;
2419  } elseif (!empty($this->modelpdf)) { // deprecated
2420  $modele = $this->modelpdf;
2421  } elseif (!empty($conf->global->EXPEDITION_ADDON_PDF)) {
2422  $modele = $conf->global->EXPEDITION_ADDON_PDF;
2423  }
2424  }
2425 
2426  $modelpath = "core/modules/expedition/doc/";
2427 
2428  $this->fetch_origin();
2429 
2430  return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
2431  }
2432 
2441  public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
2442  {
2443  $tables = array(
2444  'expedition'
2445  );
2446 
2447  return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
2448  }
2449 }
2450 
2451 
2456 {
2460  public $element = 'expeditiondet';
2461 
2465  public $table_element = 'expeditiondet';
2466 
2467 
2474  public $line_id; // deprecated
2475 
2481 
2487  public $fk_origin; // Example: 'orderline'
2488 
2492  public $fk_origin_line;
2493 
2497  public $fk_expedition;
2498 
2502  public $db;
2503 
2507  public $qty;
2508 
2512  public $qty_shipped;
2513 
2517  public $fk_product;
2518 
2519  // detail of lot and qty = array(id in llx_expeditiondet_batch, fk_expeditiondet, batch, qty, fk_origin_stock)
2520  // We can use this to know warehouse planned to be used for each lot.
2521  public $detail_batch;
2522 
2523  // detail of warehouses and qty
2524  // We can use this to know warehouse when there is no lot.
2525  public $details_entrepot;
2526 
2527 
2531  public $entrepot_id;
2532 
2533 
2537  public $qty_asked;
2538 
2543  public $ref;
2544 
2548  public $product_ref;
2549 
2554  public $libelle;
2555 
2559  public $product_label;
2560 
2566  public $desc;
2567 
2571  public $product_desc;
2572 
2577  public $product_type = 0;
2578 
2582  public $rang;
2583 
2587  public $weight;
2588  public $weight_units;
2589 
2593  public $length;
2594  public $length_units;
2595 
2599  public $surface;
2600  public $surface_units;
2601 
2605  public $volume;
2606  public $volume_units;
2607 
2608  // Invoicing
2609  public $remise_percent;
2610  public $tva_tx;
2611 
2615  public $total_ht;
2616 
2620  public $total_ttc;
2621 
2625  public $total_tva;
2626 
2630  public $total_localtax1;
2631 
2635  public $total_localtax2;
2636 
2637 
2643  public function __construct($db)
2644  {
2645  $this->db = $db;
2646  }
2647 
2654  public function fetch($rowid)
2655  {
2656  $sql = 'SELECT ed.rowid, ed.fk_expedition, ed.fk_entrepot, ed.fk_origin_line, ed.qty, ed.rang';
2657  $sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element.' as ed';
2658  $sql .= ' WHERE ed.rowid = '.((int) $rowid);
2659  $result = $this->db->query($sql);
2660  if ($result) {
2661  $objp = $this->db->fetch_object($result);
2662  $this->id = $objp->rowid;
2663  $this->fk_expedition = $objp->fk_expedition;
2664  $this->entrepot_id = $objp->fk_entrepot;
2665  $this->fk_origin_line = $objp->fk_origin_line;
2666  $this->qty = $objp->qty;
2667  $this->rang = $objp->rang;
2668 
2669  $this->db->free($result);
2670 
2671  return 1;
2672  } else {
2673  $this->errors[] = $this->db->lasterror();
2674  $this->error = $this->db->lasterror();
2675  return -1;
2676  }
2677  }
2678 
2686  public function insert($user, $notrigger = 0)
2687  {
2688  global $langs, $conf;
2689 
2690  $error = 0;
2691 
2692  // Check parameters
2693  if (empty($this->fk_expedition) || empty($this->fk_origin_line) || !is_numeric($this->qty)) {
2694  $this->error = 'ErrorMandatoryParametersNotProvided';
2695  return -1;
2696  }
2697 
2698  $this->db->begin();
2699 
2700  if (empty($this->rang)) {
2701  $this->rang = 0;
2702  }
2703 
2704  // Rank to use
2705  $ranktouse = $this->rang;
2706  if ($ranktouse == -1) {
2707  $rangmax = $this->line_max($this->fk_expedition);
2708  $ranktouse = $rangmax + 1;
2709  }
2710 
2711  $sql = "INSERT INTO ".MAIN_DB_PREFIX."expeditiondet (";
2712  $sql .= "fk_expedition";
2713  $sql .= ", fk_entrepot";
2714  $sql .= ", fk_origin_line";
2715  $sql .= ", qty";
2716  $sql .= ", rang";
2717  $sql .= ") VALUES (";
2718  $sql .= $this->fk_expedition;
2719  $sql .= ", ".(empty($this->entrepot_id) ? 'NULL' : $this->entrepot_id);
2720  $sql .= ", ".((int) $this->fk_origin_line);
2721  $sql .= ", ".price2num($this->qty, 'MS');
2722  $sql .= ", ".((int) $ranktouse);
2723  $sql .= ")";
2724 
2725  dol_syslog(get_class($this)."::insert", LOG_DEBUG);
2726  $resql = $this->db->query($sql);
2727  if ($resql) {
2728  $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX."expeditiondet");
2729 
2730  if (!$error) {
2731  $result = $this->insertExtraFields();
2732  if ($result < 0) {
2733  $error++;
2734  }
2735  }
2736 
2737  if (!$error && !$notrigger) {
2738  // Call trigger
2739  $result = $this->call_trigger('LINESHIPPING_INSERT', $user);
2740  if ($result < 0) {
2741  $error++;
2742  }
2743  // End call triggers
2744  }
2745 
2746  if ($error) {
2747  foreach ($this->errors as $errmsg) {
2748  dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
2749  $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
2750  }
2751  }
2752  } else {
2753  $error++;
2754  }
2755 
2756  if ($error) {
2757  $this->db->rollback();
2758  return -1;
2759  } else {
2760  $this->db->commit();
2761  return $this->id;
2762  }
2763  }
2764 
2772  public function delete($user = null, $notrigger = 0)
2773  {
2774  global $conf;
2775 
2776  $error = 0;
2777 
2778  $this->db->begin();
2779 
2780  // delete batch expedition line
2781  if (isModEnabled('productbatch')) {
2782  $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet_batch";
2783  $sql .= " WHERE fk_expeditiondet = ".((int) $this->id);
2784 
2785  if (!$this->db->query($sql)) {
2786  $this->errors[] = $this->db->lasterror()." - sql=$sql";
2787  $error++;
2788  }
2789  }
2790 
2791  $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet";
2792  $sql .= " WHERE rowid = ".((int) $this->id);
2793 
2794  if (!$error && $this->db->query($sql)) {
2795  // Remove extrafields
2796  if (!$error) {
2797  $result = $this->deleteExtraFields();
2798  if ($result < 0) {
2799  $this->errors[] = $this->error;
2800  $error++;
2801  }
2802  }
2803  if (!$error && !$notrigger) {
2804  // Call trigger
2805  $result = $this->call_trigger('LINESHIPPING_DELETE', $user);
2806  if ($result < 0) {
2807  $this->errors[] = $this->error;
2808  $error++;
2809  }
2810  // End call triggers
2811  }
2812  } else {
2813  $this->errors[] = $this->db->lasterror()." - sql=$sql";
2814  $error++;
2815  }
2816 
2817  if (!$error) {
2818  $this->db->commit();
2819  return 1;
2820  } else {
2821  foreach ($this->errors as $errmsg) {
2822  dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
2823  $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
2824  }
2825  $this->db->rollback();
2826  return -1 * $error;
2827  }
2828  }
2829 
2837  public function update($user = null, $notrigger = 0)
2838  {
2839  global $conf;
2840 
2841  $error = 0;
2842 
2843  dol_syslog(get_class($this)."::update id=$this->id, entrepot_id=$this->entrepot_id, product_id=$this->fk_product, qty=$this->qty");
2844 
2845  $this->db->begin();
2846 
2847  // Clean parameters
2848  if (empty($this->qty)) {
2849  $this->qty = 0;
2850  }
2851  $qty = price2num($this->qty);
2852  $remainingQty = 0;
2853  $batch = null;
2854  $batch_id = null;
2855  $expedition_batch_id = null;
2856  if (is_array($this->detail_batch)) { // array of ExpeditionLineBatch
2857  if (count($this->detail_batch) > 1) {
2858  dol_syslog(get_class($this).'::update only possible for one batch', LOG_ERR);
2859  $this->errors[] = 'ErrorBadParameters';
2860  $error++;
2861  } else {
2862  $batch = $this->detail_batch[0]->batch;
2863  $batch_id = $this->detail_batch[0]->fk_origin_stock;
2864  $expedition_batch_id = $this->detail_batch[0]->id;
2865  if ($this->entrepot_id != $this->detail_batch[0]->entrepot_id) {
2866  dol_syslog(get_class($this).'::update only possible for batch of same warehouse', LOG_ERR);
2867  $this->errors[] = 'ErrorBadParameters';
2868  $error++;
2869  }
2870  $qty = price2num($this->detail_batch[0]->qty);
2871  }
2872  } elseif (!empty($this->detail_batch)) {
2873  $batch = $this->detail_batch->batch;
2874  $batch_id = $this->detail_batch->fk_origin_stock;
2875  $expedition_batch_id = $this->detail_batch->id;
2876  if ($this->entrepot_id != $this->detail_batch->entrepot_id) {
2877  dol_syslog(get_class($this).'::update only possible for batch of same warehouse', LOG_ERR);
2878  $this->errors[] = 'ErrorBadParameters';
2879  $error++;
2880  }
2881  $qty = price2num($this->detail_batch->qty);
2882  }
2883 
2884  // check parameters
2885  if (!isset($this->id) || !isset($this->entrepot_id)) {
2886  dol_syslog(get_class($this).'::update missing line id and/or warehouse id', LOG_ERR);
2887  $this->errors[] = 'ErrorMandatoryParametersNotProvided';
2888  $error++;
2889  return -1;
2890  }
2891 
2892  // update lot
2893 
2894  if (!empty($batch) && isModEnabled('productbatch')) {
2895  dol_syslog(get_class($this)."::update expedition batch id=$expedition_batch_id, batch_id=$batch_id, batch=$batch");
2896 
2897  if (empty($batch_id) || empty($this->fk_product)) {
2898  dol_syslog(get_class($this).'::update missing fk_origin_stock (batch_id) and/or fk_product', LOG_ERR);
2899  $this->errors[] = 'ErrorMandatoryParametersNotProvided';
2900  $error++;
2901  }
2902 
2903  // fetch remaining lot qty
2904  $shipmentlinebatch = new ExpeditionLineBatch($this->db);
2905 
2906  if (!$error && ($lotArray = $shipmentlinebatch->fetchAll($this->id)) < 0) {
2907  $this->errors[] = $this->db->lasterror()." - ExpeditionLineBatch::fetchAll";
2908  $error++;
2909  } else {
2910  // caculate new total line qty
2911  foreach ($lotArray as $lot) {
2912  if ($expedition_batch_id != $lot->id) {
2913  $remainingQty += $lot->qty;
2914  }
2915  }
2916  $qty += $remainingQty;
2917 
2918  //fetch lot details
2919 
2920  // fetch from product_lot
2921  require_once DOL_DOCUMENT_ROOT.'/product/stock/class/productlot.class.php';
2922  $lot = new Productlot($this->db);
2923  if ($lot->fetch(0, $this->fk_product, $batch) < 0) {
2924  $this->errors[] = $lot->errors;
2925  $error++;
2926  }
2927  if (!$error && !empty($expedition_batch_id)) {
2928  // delete lot expedition line
2929  $sql = "DELETE FROM ".MAIN_DB_PREFIX."expeditiondet_batch";
2930  $sql .= " WHERE fk_expeditiondet = ".((int) $this->id);
2931  $sql .= " AND rowid = ".((int) $expedition_batch_id);
2932 
2933  if (!$this->db->query($sql)) {
2934  $this->errors[] = $this->db->lasterror()." - sql=$sql";
2935  $error++;
2936  }
2937  }
2938  if (!$error && $this->detail_batch->qty > 0) {
2939  // create lot expedition line
2940  if (isset($lot->id)) {
2941  $shipmentLot = new ExpeditionLineBatch($this->db);
2942  $shipmentLot->batch = $lot->batch;
2943  $shipmentLot->eatby = $lot->eatby;
2944  $shipmentLot->sellby = $lot->sellby;
2945  $shipmentLot->entrepot_id = $this->detail_batch->entrepot_id;
2946  $shipmentLot->qty = $this->detail_batch->qty;
2947  $shipmentLot->fk_origin_stock = $batch_id;
2948  if ($shipmentLot->create($this->id) < 0) {
2949  $this->errors[] = $shipmentLot->errors;
2950  $error++;
2951  }
2952  }
2953  }
2954  }
2955  }
2956  if (!$error) {
2957  // update line
2958  $sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
2959  $sql .= " fk_entrepot = ".($this->entrepot_id > 0 ? $this->entrepot_id : 'null');
2960  $sql .= " , qty = ".((float) price2num($qty, 'MS'));
2961  $sql .= " WHERE rowid = ".((int) $this->id);
2962 
2963  if (!$this->db->query($sql)) {
2964  $this->errors[] = $this->db->lasterror()." - sql=$sql";
2965  $error++;
2966  }
2967  }
2968 
2969  if (!$error) {
2970  if (!$error) {
2971  $result = $this->insertExtraFields();
2972  if ($result < 0) {
2973  $this->errors[] = $this->error;
2974  $error++;
2975  }
2976  }
2977  }
2978 
2979  if (!$error && !$notrigger) {
2980  // Call trigger
2981  $result = $this->call_trigger('LINESHIPPING_MODIFY', $user);
2982  if ($result < 0) {
2983  $this->errors[] = $this->error;
2984  $error++;
2985  }
2986  // End call triggers
2987  }
2988  if (!$error) {
2989  $this->db->commit();
2990  return 1;
2991  } else {
2992  foreach ($this->errors as $errmsg) {
2993  dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR);
2994  $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
2995  }
2996  $this->db->rollback();
2997  return -1 * $error;
2998  }
2999  }
3000 }
$object ref
Definition: info.php:78
Class to manage customers orders.
const STATUS_SHIPMENTONPROCESS
Shipment on process.
const STATUS_VALIDATED
Validated status.
Parent class of all other business classes (invoices, contracts, proposals, orders,...
fetch_optionals($rowid=null, $optionsArray=null)
Function to get extra fields of an object into $this->array_options This method is in most cases call...
deleteEcmFiles($mode=0)
Delete related files of object in database.
add_object_linked($origin=null, $origin_id=null, $f_user=null, $notrigger=0)
Add an object link into llx_element_element.
commonGenerateDocument($modelspath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams=null)
Common function for all objects extending CommonObject for generating documents.
fetch_thirdparty($force_thirdparty_id=0)
Load the third party of object, from id $this->socid or $this->fk_soc, into this->thirdparty.
deleteObjectLinked($sourceid=null, $sourcetype='', $targetid=null, $targettype='', $rowid='', $f_user=null, $notrigger=0)
Delete all links between an object $this.
setStatut($status, $elementId=null, $elementType='', $trigkey='', $fieldstatus='fk_statut')
Set status of an object.
deleteExtraFields()
Delete all extra fields values for the current object.
static commonReplaceThirdparty(DoliDB $dbs, $origin_id, $dest_id, array $tables, $ignoreerrors=0)
Function used to replace a thirdparty id with another one.
fetchObjectLinked($sourceid=null, $sourcetype='', $targetid=null, $targettype='', $clause='OR', $alsosametype=1, $orderby='sourcetype', $loadalsoobjects=1)
Fetch array of objects linked to current object (object of enabled modules only).
line_max($fk_parent_line=0)
Get max value used for position of line (rang)
fetch_origin()
Read linked origin object.
insertExtraFields($trigger='', $userused=null)
Add/Update all extra fields values for the current object.
call_trigger($triggerName, $user)
Call trigger based on this instance.
Parent class for class inheritance lines of business objects This class is useless for the moment so ...
Class to manage receptions.
Class to manage Dolibarr database access.
Class to manage shipments.
getNomUrl($withpicto=0, $option='', $max=0, $short=0, $notooltip=0, $save_lastsearch_value=-1)
Return clicable link of object (with eventually picto)
create_delivery($user)
Create a delivery receipt from a shipment.
deleteline($user, $lineid)
Delete detail line.
getUrlTrackingStatus($value='')
Forge an set tracking url.
setClosed()
Classify the shipping as closed.
__construct($db)
Constructor.
fetch_lines()
Load lines.
list_delivery_methods($id='')
Fetch all deliveries method and return an array.
create($user, $notrigger=0)
Create expedition en base.
LibStatut($status, $mode)
Return label of a status.
setBilled()
Classify the shipping as invoiced (used when WORKFLOW_BILL_ON_SHIPMENT is on)
addline_batch($dbatch, $array_options=0)
Add a shipment line with batch record.
generateDocument($modele, $outputlangs, $hidedetails=0, $hidedesc=0, $hideref=0, $moreparams=null)
Create a document onto disk according to template module.
setDeliveryDate($user, $delivery_date)
Set the planned delivery date.
const STATUS_DRAFT
Draft status.
const STATUS_CANCELED
Canceled status.
getLibStatut($mode=0)
Return status label.
set_date_livraison($user, $delivery_date)
Set delivery date.
initAsSpecimen()
Initialise an instance with random values.
getNextNumRef($soc)
Return next expedition ref.
const STATUS_CLOSED
Closed status.
const STATUS_VALIDATED
Validated status.
create_line_batch($line_ext, $array_options=0)
Create the detail of the expedition line.
valid($user, $notrigger=0)
Validate object and update stock if option enabled.
update($user=null, $notrigger=0)
Update database.
cancel($notrigger=0, $also_update_stock=false)
Cancel shipment.
fetch_delivery_methods()
Fetch deliveries method and return an array.
addline($entrepot_id, $id, $qty, $array_options=0)
Add an expedition line.
fetch($id, $ref='', $ref_ext='', $notused='')
Get object and lines from database.
reOpen()
Classify the shipping as validated/opened.
static replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
Function used to replace a thirdparty id with another one.
create_line($entrepot_id, $origin_line_id, $qty, $rang=0, $array_options=null)
Create a expedition line.
Classe to manage lines of shipment.
fetch($rowid)
Load line expedition.
__construct($db)
Constructor.
insert($user, $notrigger=0)
Insert line into database.
update($user=null, $notrigger=0)
Update a line in database.
CRUD class for batch number management within shipment.
Class to manage stock movements.
Class to manage order lines.
Class to manage products or services.
Manage record for batch number management.
Class with list of lots and properties.
Class to manage third parties objects (customers, suppliers, prospects...)
trait CommonIncoterm
Superclass for incoterm classes.
if(isModEnabled('facture') &&!empty($user->rights->facture->lire)) if((isModEnabled('fournisseur') &&empty($conf->global->MAIN_USE_NEW_SUPPLIERMOD) && $user->hasRight("fournisseur", "facture", "lire"))||(isModEnabled('supplier_invoice') && $user->hasRight("supplier_invoice", "lire"))) if(isModEnabled('don') &&!empty($user->rights->don->lire)) if(isModEnabled('tax') &&!empty($user->rights->tax->charges->lire)) if(isModEnabled('facture') &&isModEnabled('commande') && $user->hasRight("commande", "lire") &&empty($conf->global->WORKFLOW_DISABLE_CREATE_INVOICE_FROM_ORDER)) $resql
Social contributions to pay.
Definition: index.php:745
dol_delete_dir_recursive($dir, $count=0, $nophperrors=0, $onlysub=0, &$countdeleted=0, $indexdatabase=1, $nolog=0)
Remove a directory $dir and its subdirectories (or only files and subdirectories)
Definition: files.lib.php:1401
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:1250
dol_dir_list($path, $types="all", $recursive=0, $filter="", $excludefilter=null, $sortcriteria="name", $sortorder=SORT_ASC, $mode=0, $nohook=0, $relativename="", $donotfollowsymlinks=0, $nbsecondsold=0)
Scan a directory and return a list of files/directories.
Definition: files.lib.php:61
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
dol_print_error($db='', $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
img_object($titlealt, $picto, $moreatt='', $pictoisfullpath=false, $srconly=0, $notitle=0)
Show a picto called object_picto (generic function)
dol_strlen($string, $stringencoding='UTF-8')
Make a strlen call.
dol_now($mode='auto')
Return date for now.
getDolGlobalInt($key, $default=0)
Return dolibarr global constant int value.
dolGetStatus($statusLabel='', $statusLabelShort='', $html='', $statusType='status0', $displayMode=0, $url='', $params=array())
Output the badge of a status.
dol_buildpath($path, $type=0, $returnemptyifnotfound=0)
Return path of url or filesystem.
get_localtax($vatrate, $local, $thirdparty_buyer="", $thirdparty_seller="", $vatnpr=0)
Return localtax rate for a particular vat, when selling a product with vat $vatrate,...
dol_sanitizeFileName($str, $newstr='_', $unaccent=1)
Clean a string to use it as a file name.
isModEnabled($module)
Is Dolibarr module enabled.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
calcul_price_total($qty, $pu, $remise_percent_ligne, $txtva, $uselocaltax1_rate, $uselocaltax2_rate, $remise_percent_global, $price_base_type, $info_bits, $type, $seller='', $localtaxes_array='', $progress=100, $multicurrency_tx=1, $pu_devise=0, $multicurrency_code='')
Calculate totals (net, vat, ...) of a line.
Definition: price.lib.php:86
$conf db user
Definition: repair.php:123
$conf db
API class for accounts.
Definition: inc.php:41