Passed
Push — EXTRACT_CLASSES ( a2ff75...ae6b5c )
by Rafael
34:15
created

Expedition   F

Complexity

Total Complexity 412

Size/Duplication

Total Lines 2593
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 1398
dl 0
loc 2593
rs 0.8
c 0
b 0
f 0
wmc 412

How to fix   Complexity   

Complex Class

Complex classes like Expedition often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Expedition, and based on these observations, apply Extract Interface, too.

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