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