dolibarr 22.0.5
expedition.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2003-2008 Rodolphe Quiedeville <rodolphe@quiedeville.org>
3 * Copyright (C) 2005-2012 Regis Houssin <regis.houssin@inodbox.com>
4 * Copyright (C) 2007 Franky Van Liedekerke <franky.van.liedekerke@telenet.be>
5 * Copyright (C) 2006-2012 Laurent Destailleur <eldy@users.sourceforge.net>
6 * Copyright (C) 2011-2020 Juanjo Menent <jmenent@2byte.es>
7 * Copyright (C) 2013 Florian Henry <florian.henry@open-concept.pro>
8 * Copyright (C) 2014 Cedric GROSS <c.gross@kreiz-it.fr>
9 * Copyright (C) 2014-2015 Marcos García <marcosgdf@gmail.com>
10 * Copyright (C) 2014-2017 Francis Appels <francis.appels@yahoo.com>
11 * Copyright (C) 2015 Claudio Aschieri <c.aschieri@19.coop>
12 * Copyright (C) 2016-2024 Ferran Marcet <fmarcet@2byte.es>
13 * Copyright (C) 2018 Nicolas ZABOURI <info@inovea-conseil.com>
14 * Copyright (C) 2018-2025 Frédéric France <frederic.france@free.fr>
15 * Copyright (C) 2020 Lenin Rivas <lenin@leninrivas.com>
16 * Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
17 * Copyright (C) 2024 William Mead <william.mead@manchenumerique.fr>
18 *
19 * This program is free software; you can redistribute it and/or modify
20 * it under the terms of the GNU General Public License as published by
21 * the Free Software Foundation; either version 3 of the License, or
22 * (at your option) any later version.
23 *
24 * This program is distributed in the hope that it will be useful,
25 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27 * GNU General Public License for more details.
28 *
29 * You should have received a copy of the GNU General Public License
30 * along with this program. If not, see <https://www.gnu.org/licenses/>.
31 */
32
39require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
40require_once DOL_DOCUMENT_ROOT."/expedition/class/expeditionligne.class.php";
41require_once DOL_DOCUMENT_ROOT.'/core/class/commonincoterm.class.php';
42if (isModEnabled("propal")) {
43 require_once DOL_DOCUMENT_ROOT.'/comm/propal/class/propal.class.php';
44}
45if (isModEnabled('order')) {
46 require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
47}
48require_once DOL_DOCUMENT_ROOT.'/expedition/class/expeditionlinebatch.class.php';
49require_once DOL_DOCUMENT_ROOT.'/core/class/commonsignedobject.class.php';
50require_once DOL_DOCUMENT_ROOT.'/subtotals/class/commonsubtotal.class.php';
51
58{
59 use CommonIncoterm;
60 use CommonSignedObject;
61 use CommonSubtotal;
62
66 public $element = "shipping";
67
71 public $fk_element = "fk_expedition";
72
76 public $table_element = "expedition";
77
81 public $table_element_line = "expeditiondet";
82
86 public $picto = 'dolly';
87
88
92 public $fields = array();
93
97 public $user_author_id;
98
102 public $fk_user_author;
103
107 public $socid;
108
114 public $ref_client;
115
119 public $ref_customer;
120
124 public $entrepot_id;
125
129 public $tracking_number;
130
134 public $tracking_url;
138 public $billed;
139
143 public $trueWeight;
147 public $weight_units;
151 public $trueWidth;
155 public $width_units;
159 public $trueHeight;
163 public $height_units;
167 public $trueDepth;
171 public $depth_units;
175 public $trueSize;
176
180 public $livraison_id;
181
185 public $multicurrency_subprice;
186
190 public $size_units;
191
195 public $sizeH;
196
200 public $sizeS;
201
205 public $sizeW;
206
210 public $weight;
211
215 public $date_delivery;
216
222 public $date;
223
229 public $date_expedition;
230
235 public $date_shipping;
236
240 public $date_valid;
241
245 public $meths;
249 public $listmeths; // List of carriers
250
254 public $commande_id;
255
259 public $commande;
260
264 public $lines = array();
265
266 // Multicurrency
270 public $fk_multicurrency;
271
275 public $multicurrency_code;
279 public $multicurrency_tx;
283 public $multicurrency_total_ht;
287 public $multicurrency_total_tva;
291 public $multicurrency_total_ttc;
292
296 const STATUS_DRAFT = 0;
297
305
311 const STATUS_CLOSED = 2;
312
316 const STATUS_CANCELED = -1;
317
326
332 public function __construct($db)
333 {
334 global $conf;
335
336 $this->db = $db;
337
338 $this->ismultientitymanaged = 1;
339 $this->isextrafieldmanaged = 1;
340
341 // List of long language codes for status
342 $this->labelStatus = array();
343 $this->labelStatus[-1] = 'StatusSendingCanceled';
344 $this->labelStatus[0] = 'StatusSendingDraft';
345 $this->labelStatus[1] = 'StatusSendingValidated';
346 $this->labelStatus[2] = 'StatusSendingProcessed';
347
348 // List of short language codes for status
349 $this->labelStatusShort = array();
350 $this->labelStatusShort[-1] = 'StatusSendingCanceledShort';
351 $this->labelStatusShort[0] = 'StatusSendingDraftShort';
352 $this->labelStatusShort[1] = 'StatusSendingValidatedShort';
353 $this->labelStatusShort[2] = 'StatusSendingProcessedShort';
354 }
355
362 public function getNextNumRef($soc)
363 {
364 global $langs, $conf;
365 $langs->load("sendings");
366
367 if (getDolGlobalString('EXPEDITION_ADDON_NUMBER')) {
368 $mybool = false;
369
370 $file = getDolGlobalString('EXPEDITION_ADDON_NUMBER') . ".php";
371 $classname = getDolGlobalString('EXPEDITION_ADDON_NUMBER');
372
373 // Include file with class
374 $dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']);
375
376 foreach ($dirmodels as $reldir) {
377 $dir = dol_buildpath($reldir."core/modules/expedition/");
378
379 // Load file with numbering class (if found)
380 $mybool = ((bool) @include_once $dir.$file) || $mybool;
381 }
382
383 if (!$mybool) {
384 dol_print_error(null, "Failed to include file ".$file);
385 return '';
386 }
387
388 $obj = new $classname();
389 '@phan-var-force ModelNumRefExpedition $obj';
390 $numref = $obj->getNextValue($soc, $this);
391
392 if ($numref != "") {
393 return $numref;
394 } else {
395 dol_print_error($this->db, get_class($this)."::getNextNumRef ".$obj->error);
396 return "";
397 }
398 } else {
399 print $langs->trans("Error")." ".$langs->trans("Error_EXPEDITION_ADDON_NUMBER_NotDefined");
400 return "";
401 }
402 }
403
411 public function create($user, $notrigger = 0)
412 {
413 $now = dol_now();
414
415 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
416 $error = 0;
417
418 // Clean parameters
419 $this->tracking_number = dol_sanitizeFileName($this->tracking_number);
420 if (empty($this->fk_project)) {
421 $this->fk_project = 0;
422 }
423 if (empty($this->date_creation)) {
424 $this->date_creation = $now;
425 }
426 if (empty($this->date_shipping) && !empty($this->date_expedition)) {
427 $this->date_shipping = $this->date_expedition;
428 }
429 $this->entity = setEntity($this);
430
431 $this->user = $user;
432
433 $this->db->begin();
434
435 $sql = "INSERT INTO ".MAIN_DB_PREFIX."expedition (";
436 $sql .= "ref";
437 $sql .= ", entity";
438 $sql .= ", ref_customer";
439 $sql .= ", ref_ext";
440 $sql .= ", date_creation";
441 $sql .= ", fk_user_author";
442 $sql .= ", date_expedition";
443 $sql .= ", date_delivery";
444 $sql .= ", fk_soc";
445 $sql .= ", fk_projet";
446 $sql .= ", fk_address";
447 $sql .= ", fk_shipping_method";
448 $sql .= ", tracking_number";
449 $sql .= ", weight";
450 $sql .= ", size";
451 $sql .= ", width";
452 $sql .= ", height";
453 $sql .= ", weight_units";
454 $sql .= ", size_units";
455 $sql .= ", note_private";
456 $sql .= ", note_public";
457 $sql .= ", model_pdf";
458 $sql .= ", fk_incoterms, location_incoterms";
459 $sql .= ", signed_status";
460 $sql .= ") VALUES (";
461 $sql .= "'(PROV)'";
462 $sql .= ", ".((int) $this->entity);
463 $sql .= ", ".($this->ref_customer ? "'".$this->db->escape($this->ref_customer)."'" : "null");
464 $sql .= ", ".($this->ref_ext ? "'".$this->db->escape($this->ref_ext)."'" : "null");
465 $sql .= ", '".$this->db->idate($this->date_creation)."'";
466 $sql .= ", ".((int) $user->id);
467 $sql .= ", ".($this->date_shipping > 0 ? "'".$this->db->idate($this->date_shipping)."'" : "null");
468 $sql .= ", ".($this->date_delivery > 0 ? "'".$this->db->idate($this->date_delivery)."'" : "null");
469 $sql .= ", ".($this->socid > 0 ? ((int) $this->socid) : "null");
470 $sql .= ", ".($this->fk_project > 0 ? ((int) $this->fk_project) : "null");
471 $sql .= ", ".($this->fk_delivery_address > 0 ? $this->fk_delivery_address : "null");
472 $sql .= ", ".($this->shipping_method_id > 0 ? ((int) $this->shipping_method_id) : "null");
473 $sql .= ", '".$this->db->escape($this->tracking_number)."'";
474 $sql .= ", ".(is_numeric($this->weight) ? $this->weight : 'NULL');
475 $sql .= ", ".(is_numeric($this->sizeS) ? $this->sizeS : 'NULL'); // TODO Should use this->trueDepth
476 $sql .= ", ".(is_numeric($this->sizeW) ? $this->sizeW : 'NULL'); // TODO Should use this->trueWidth
477 $sql .= ", ".(is_numeric($this->sizeH) ? $this->sizeH : 'NULL'); // TODO Should use this->trueHeight
478 $sql .= ", ".($this->weight_units != '' ? (int) $this->weight_units : 'NULL');
479 $sql .= ", ".($this->size_units != '' ? (int) $this->size_units : 'NULL');
480 $sql .= ", ".(!empty($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : "null");
481 $sql .= ", ".(!empty($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : "null");
482 $sql .= ", ".(!empty($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null");
483 $sql .= ", ".(int) $this->fk_incoterms;
484 $sql .= ", '".$this->db->escape($this->location_incoterms)."'";
485 $sql .= ", ".($this->signed_status);
486 $sql .= ")";
487
488 dol_syslog(get_class($this)."::create", LOG_DEBUG);
489 $resql = $this->db->query($sql);
490 if ($resql) {
491 $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX."expedition");
492
493 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition";
494 $sql .= " SET ref = '(PROV".$this->id.")'";
495 $sql .= " WHERE rowid = ".((int) $this->id);
496
497 dol_syslog(get_class($this)."::create", LOG_DEBUG);
498 if ($this->db->query($sql)) {
499 // Insert of lines
500 $num = count($this->lines);
501 $kits_list = array();
502 if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) {
503 for ($i = 0; $i < $num; $i++) {
504 $objectsrc = new OrderLine($this->db);
505 $objectsrc->fetch($this->lines[$i]->origin_line_id);
506 if ($this->lines[$i]->product_type == "9" && $objectsrc->special_code == SUBTOTALS_SPECIAL_CODE) {
507 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) {
508 $error++;
509 }
510 continue;
511 }
512 if (empty($this->lines[$i]->product_type) || getDolGlobalString('STOCK_SUPPORTS_SERVICES') || getDolGlobalString('SHIPMENT_SUPPORTS_SERVICES')) {
513 // virtual products
514 $line = $this->lines[$i];
515 if ($line->fk_product > 0) {
516 if (!isset($kits_list[$line->fk_product])) {
517 if (!is_object($line->product)) {
518 $line_product = new Product($this->db);
519 $result = $line_product->fetch($line->fk_product, '', '', '', 1, 1, 1);
520 if ($result <= 0) {
521 $error++;
522 }
523 } else {
524 $line_product = $line->product;
525 }
526
527 // get all children of virtual product
528 $line_product->get_sousproduits_arbo();
529 $prods_arbo = $line_product->get_arbo_each_prod($line->qty);
530 if (count($prods_arbo) > 0) {
531 $kits_list[$line->fk_product] = array(
532 'arbo' => $prods_arbo,
533 'total_qty' => $line->qty,
534 );
535 }
536 } else {
537 $kits_list[$line->fk_product]['total_qty'] += $line->qty;
538 }
539 }
540 }
541 }
542 }
543 $kits_id_cached = array();
544 $sub_kits_id_cached = array();
545 for ($i = 0; $i < $num; $i++) {
546 $line = $this->lines[$i];
547 if (empty($line->product_type) || getDolGlobalString('STOCK_SUPPORTS_SERVICES') || getDolGlobalString('SHIPMENT_SUPPORTS_SERVICES')) {
548 $line_id = 0;
549 if (!isset($kits_id_cached[$line->fk_product])) {
550 if (!isset($line->detail_batch) || (isset($kits_list[$line->fk_product]) && !getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE'))) { // no batch management or is kit
551 $qty = isset($kits_list[$line->fk_product]) ? $kits_list[$line->fk_product]['total_qty'] : $line->qty;
552 $warehouse_id = (isset($kits_list[$line->fk_product]) && !getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE')) ? 0 : $line->entrepot_id;
553 $line_id = $this->create_line($warehouse_id, $line->origin_line_id, $qty, $line->rang, $line->array_options, 0, $line->fk_product);
554 if ($line_id <= 0) {
555 $error++;
556 }
557 if (isset($kits_list[$line->fk_product])) $kits_id_cached[$line->fk_product] = $line_id;
558 } else { // with batch management
559 if ($this->create_line_batch($line, $line->array_options) <= 0) {
560 $error++;
561 }
562 }
563 } else {
564 $line_id = $kits_id_cached[$line->fk_product];
565 }
566
567 // virtual products
568 if (isset($kits_list[$line->fk_product]) && !getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE')) {
569 $prods_arbo = $kits_list[$line->fk_product]['arbo'];
570 $total_qty = $kits_list[$line->fk_product]['total_qty'];
571
572 // get all children of virtual product
573 $parent_line_id = $line_id; // parent line created
574 $level_last = 1;
575 $product_child_id = 0;
576 foreach ($prods_arbo as $index => $product_child_arr) {
577 // 'id' => Id product
578 // 'id_parent' => Id parent product
579 // 'ref' => Ref product
580 // 'nb' => Nb of units that compose parent product
581 // 'nb_total' => // Nb of units for all nb of product
582 // 'stock' => Stock
583 // 'stock_alert' => Stock alert
584 // 'label' => Label
585 // 'fullpath' => // Full path label
586 // 'type' =>
587 // 'desiredstock' => Desired stock
588 // 'level' => Level
589 // 'incdec' => Need to be incremented or decremented
590 // 'entity' => Entity
591 $product_child_level = (int) $product_child_arr['level'];
592 $product_child_incdec = !empty($product_child_arr['incdec']);
593
594 // detect new level
595 if ($product_child_level != $level_last) {
596 $parent_line_id = $line_id; // last line id
597 $parent_product_id = $product_child_id; // last line id
598 if (isset($kits_id_cached[$parent_product_id])) {
599 $parent_line_id = $kits_id_cached[$parent_product_id];
600 } else {
601 $kits_id_cached[$parent_product_id] = $parent_line_id;
602 }
603 }
604
605 // determine if it's a kit : check next level
606 $is_kit = false;
607 $next_level = $product_child_level;
608 $next_index = $index + 1;
609 if (isset($prods_arbo[$next_index])) {
610 $next_level = (int) $prods_arbo[$next_index]['level'];
611 }
612 if ($next_level > $product_child_level) {
613 $is_kit = true;
614 }
615
616 // determine quantity of sub-product
617 $product_child_id = (int) $product_child_arr['id'];
618 $product_child_qty = (float) $product_child_arr['nb_total']; // by default
619 $warehouse_id = $line->entrepot_id; // by default
620 if ($is_kit || !$product_child_incdec) {
621 if (!$product_child_incdec) {
622 $product_child_qty = 0;
623 }
624 $warehouse_id = 0; // no warehouse used for a kit or if stock is not managed (empty incdec)
625 }
626
627 // create line for a child of virtual product
628 if (!isset($sub_kits_id_cached[$product_child_id]) || $warehouse_id > 0) {
629 $line_id = $this->create_line($warehouse_id, ($parent_line_id ? 0 : $line->origin_line_id), $product_child_qty, $line->rang, $line->array_options, $parent_line_id, $product_child_id);
630 if ($line_id <= 0) {
631 $error++;
632 dol_syslog(__METHOD__ . ' : ' . $this->errorsToString(), LOG_ERR);
633 break;
634 }
635
636 // if kit or not manage stock (empty incdec)
637 if (empty($warehouse_id)) {
638 $sub_kits_id_cached[$product_child_id] = $line_id;
639 }
640 }
641
642 $level_last = $product_child_level;
643 }
644 }
645 }
646 }
647
648 if (!$error && $this->id && $this->origin_id) {
649 $ret = $this->add_object_linked();
650 if (!$ret) {
651 $error++;
652 }
653 }
654
655 if (!$error && $this->id && getDolGlobalInt('SHIPPING_USE_ITS_OWN_CONTACTS') && getDolGlobalString('MAIN_PROPAGATE_CONTACTS_FROM_ORIGIN') && !empty($this->origin_type) && !empty($this->origin_id)) { // Get contact from origin object
656 $originforcontact = $this->origin_type;
657 $originidforcontact = $this->origin_id;
658
659 $sqlcontact = "SELECT ctc.code, ctc.source, ec.fk_socpeople FROM ".MAIN_DB_PREFIX."element_contact as ec, ".MAIN_DB_PREFIX."c_type_contact as ctc";
660 $sqlcontact .= " WHERE element_id = ".((int) $originidforcontact)." AND ec.fk_c_type_contact = ctc.rowid AND ctc.element = '".$this->db->escape($originforcontact)."'";
661
662 $resqlcontact = $this->db->query($sqlcontact);
663 if ($resqlcontact) {
664 while ($objcontact = $this->db->fetch_object($resqlcontact)) {
665 $this->add_contact($objcontact->fk_socpeople, $objcontact->code, $objcontact->source); // May failed because of duplicate key or because code of contact type does not exists for new object
666 }
667 } else {
668 dol_print_error($this->db);
669 }
670 }
671
672 // Actions on extra fields
673 if (!$error) {
674 $result = $this->insertExtraFields();
675 if ($result < 0) {
676 $error++;
677 }
678 }
679
680 if (!$error && !$notrigger) {
681 // Call trigger
682 $result = $this->call_trigger('SHIPPING_CREATE', $user);
683 if ($result < 0) {
684 $error++;
685 }
686 // End call triggers
687
688 if (!$error) {
689 $this->db->commit();
690 return $this->id;
691 } else {
692 foreach ($this->errors as $errmsg) {
693 dol_syslog(get_class($this)."::create ".$errmsg, LOG_ERR);
694 $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
695 }
696 $this->db->rollback();
697 return -1 * $error;
698 }
699 } else {
700 $error++;
701 $this->db->rollback();
702 return -3;
703 }
704 } else {
705 $error++;
706 $this->error = $this->db->lasterror()." - sql=$sql";
707 $this->db->rollback();
708 return -2;
709 }
710 } else {
711 $error++;
712 $this->error = $this->db->error()." - sql=$sql";
713 $this->db->rollback();
714 return -1;
715 }
716 }
717
718 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
731 public function create_line($entrepot_id, $origin_line_id, $qty, $rang = 0, $array_options = [], $parent_line_id = 0, $product_id = 0)
732 {
733 //phpcs:enable
734 global $user;
735
736 $expeditionline = new ExpeditionLigne($this->db);
737 $expeditionline->fk_expedition = $this->id;
738 $expeditionline->entrepot_id = $entrepot_id;
739 $expeditionline->fk_elementdet = $origin_line_id;
740 $expeditionline->element_type = $this->origin;
741 $expeditionline->fk_parent = $parent_line_id;
742 $expeditionline->fk_product = $product_id;
743 $expeditionline->qty = $qty;
744 $expeditionline->rang = $rang;
745 $expeditionline->array_options = $array_options;
746
747 if (!($expeditionline->fk_product > 0)) {
748 $order_line = new OrderLine($this->db);
749 $order_line->fetch($expeditionline->fk_elementdet);
750 $expeditionline->fk_product = $order_line->fk_product;
751 }
752
753 if (($lineId = $expeditionline->insert($user)) < 0) {
754 $this->errors[] = $expeditionline->error;
755 }
756 return $lineId;
757 }
758
759
760 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
768 public function create_line_batch($line_ext, $array_options = [])
769 {
770 // phpcs:enable
771 $error = 0;
772 $stockLocationQty = array(); // associated array with batch qty in stock location
773
774 $tab = $line_ext->detail_batch;
775 // create stockLocation Qty array
776 foreach ($tab as $detbatch) {
777 if (!empty($detbatch->fk_warehouse)) {
778 if (empty($stockLocationQty[$detbatch->fk_warehouse])) {
779 $stockLocationQty[$detbatch->fk_warehouse] = 0;
780 }
781 $stockLocationQty[$detbatch->fk_warehouse] += $detbatch->qty;
782 }
783 }
784 // create shipment lines
785 foreach ($stockLocationQty as $stockLocation => $qty) {
786 $line_id = $this->create_line($stockLocation, $line_ext->origin_line_id, $qty, $line_ext->rang, $array_options);
787 if ($line_id < 0) {
788 $error++;
789 } else {
790 // create shipment batch lines for stockLocation
791 foreach ($tab as $detbatch) {
792 if ($detbatch->fk_warehouse == $stockLocation) {
793 if (!($detbatch->create($line_id) > 0)) { // Create an ExpeditionLineBatch
794 $this->errors = $detbatch->errors;
795 $error++;
796 }
797 }
798 }
799 }
800 }
801
802 if (!$error) {
803 return 1;
804 } else {
805 return -1;
806 }
807 }
808
818 public function fetch($id, $ref = '', $ref_ext = '', $notused = '')
819 {
820 global $conf;
821
822 // Check parameters
823 if (empty($id) && empty($ref) && empty($ref_ext)) {
824 return -1;
825 }
826
827 $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.signed_status, e.fk_projet as fk_project, e.billed";
828 $sql .= ", e.date_valid";
829 $sql .= ", e.weight, e.weight_units, e.size, e.size_units, e.width, e.height";
830 $sql .= ", e.date_expedition as date_expedition, e.model_pdf, e.fk_address, e.date_delivery";
831 $sql .= ", e.fk_shipping_method, e.tracking_number";
832 $sql .= ", e.note_private, e.note_public";
833 $sql .= ', e.fk_incoterms, e.location_incoterms';
834 $sql .= ', e.signed_status';
835 $sql .= ', i.libelle as label_incoterms';
836 $sql .= ', s.libelle as shipping_method';
837 $sql .= ", el.fk_source as origin_id, el.sourcetype as origin_type";
838 $sql .= " FROM ".MAIN_DB_PREFIX."expedition as e";
839 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."element_element as el ON el.fk_target = e.rowid AND el.targettype = '".$this->db->escape($this->element)."'";
840 $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_incoterms as i ON e.fk_incoterms = i.rowid';
841 $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_shipment_mode as s ON e.fk_shipping_method = s.rowid';
842 $sql .= " WHERE e.entity IN (".getEntity('expedition').")";
843 if ($id) {
844 $sql .= " AND e.rowid = ".((int) $id);
845 }
846 if ($ref) {
847 $sql .= " AND e.ref='".$this->db->escape($ref)."'";
848 }
849 if ($ref_ext) {
850 $sql .= " AND e.ref_ext='".$this->db->escape($ref_ext)."'";
851 }
852
853 dol_syslog(get_class($this)."::fetch", LOG_DEBUG);
854 $result = $this->db->query($sql);
855 if ($result) {
856 if ($this->db->num_rows($result)) {
857 $obj = $this->db->fetch_object($result);
858
859 $this->id = $obj->rowid;
860 $this->entity = $obj->entity;
861 $this->ref = $obj->ref;
862 $this->socid = $obj->socid;
863 $this->ref_customer = $obj->ref_customer;
864 $this->ref_ext = $obj->ref_ext;
865 $this->status = $obj->fk_statut;
866 $this->statut = $this->status; // Deprecated
867 $this->signed_status = $obj->signed_status;
868 $this->user_author_id = $obj->fk_user_author;
869 $this->fk_user_author = $obj->fk_user_author;
870 $this->date_creation = $this->db->jdate($obj->date_creation);
871 $this->date_valid = $this->db->jdate($obj->date_valid);
872 $this->date = $this->db->jdate($obj->date_expedition); // TODO deprecated
873 $this->date_expedition = $this->db->jdate($obj->date_expedition); // TODO deprecated
874 $this->date_shipping = $this->db->jdate($obj->date_expedition); // Date real
875 $this->date_delivery = $this->db->jdate($obj->date_delivery); // Date planned
876 $this->fk_delivery_address = $obj->fk_address;
877 $this->model_pdf = $obj->model_pdf;
878 $this->shipping_method_id = $obj->fk_shipping_method;
879 $this->shipping_method = $obj->shipping_method;
880 $this->tracking_number = $obj->tracking_number;
881 $this->origin = ($obj->origin_type ? $obj->origin_type : 'commande'); // For compatibility
882 $this->origin_type = ($obj->origin_type ? $obj->origin_type : 'commande');
883 $this->origin_id = $obj->origin_id;
884 $this->billed = $obj->billed;
885 $this->fk_project = $obj->fk_project;
886 $this->signed_status = $obj->signed_status;
887 $this->trueWeight = $obj->weight;
888 $this->weight_units = $obj->weight_units;
889
890 $this->trueWidth = $obj->width;
891 $this->width_units = $obj->size_units;
892 $this->trueHeight = $obj->height;
893 $this->height_units = $obj->size_units;
894 $this->trueDepth = $obj->size;
895 $this->depth_units = $obj->size_units;
896
897 $this->note_public = $obj->note_public;
898 $this->note_private = $obj->note_private;
899
900 // A denormalized value
901 $this->trueSize = $obj->size."x".$obj->width."x".$obj->height;
902 $this->size_units = $obj->size_units;
903
904 //Incoterms
905 $this->fk_incoterms = $obj->fk_incoterms;
906 $this->location_incoterms = $obj->location_incoterms;
907 $this->label_incoterms = $obj->label_incoterms;
908
909 $this->db->free($result);
910
911 // Tracking url
912 $this->getUrlTrackingStatus($obj->tracking_number);
913
914 // Thirdparty
915 $result = $this->fetch_thirdparty(); // TODO Remove this
916
917 // Retrieve extrafields
918 $this->fetch_optionals();
919
920 // Fix Get multicurrency param for transmitted
921 if (isModEnabled('multicurrency')) {
922 if (!empty($this->multicurrency_code)) {
923 $this->multicurrency_code = $this->thirdparty->multicurrency_code;
924 }
925 if (getDolGlobalString('MULTICURRENCY_USE_ORIGIN_TX') && !empty($this->thirdparty->multicurrency_tx)) {
926 $this->multicurrency_tx = $this->thirdparty->multicurrency_tx;
927 }
928 }
929
930 /*
931 * Lines
932 */
933 $result = $this->fetch_lines();
934 if ($result < 0) {
935 return -3;
936 }
937
938 return 1;
939 } else {
940 dol_syslog(get_class($this).'::Fetch no expedition found', LOG_ERR);
941 $this->error = 'Shipment with id '.$id.' not found';
942 return 0;
943 }
944 } else {
945 $this->error = $this->db->error();
946 return -1;
947 }
948 }
949
957 public function valid($user, $notrigger = 0)
958 {
959 global $conf;
960
961 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
962
963 dol_syslog(get_class($this)."::valid");
964
965 // Protection
966 if ($this->status) {
967 dol_syslog(get_class($this)."::valid not in draft status", LOG_WARNING);
968 return 0;
969 }
970
971 if (!((!getDolGlobalString('MAIN_USE_ADVANCED_PERMS') && $user->hasRight('expedition', 'creer'))
972 || (getDolGlobalString('MAIN_USE_ADVANCED_PERMS') && $user->hasRight('expedition', 'shipping_advance', 'validate')))) {
973 $this->error = 'Permission denied';
974 dol_syslog(get_class($this)."::valid ".$this->error, LOG_ERR);
975 return -1;
976 }
977
978 $this->db->begin();
979
980 $error = 0;
981
982 // Define new ref
983 $soc = new Societe($this->db);
984 $soc->fetch($this->socid);
985
986 // Class of company linked to order
987 $result = $soc->setAsCustomer();
988
989 // Define new ref
990 if (!$error && (preg_match('/^[\‍(]?PROV/i', $this->ref) || empty($this->ref))) { // empty should not happened, but when it occurs, the test save life
991 $numref = $this->getNextNumRef($soc);
992 } elseif (!empty($this->ref)) {
993 $numref = $this->ref;
994 } else {
995 $numref = "EXP".$this->id;
996 }
997 $this->newref = dol_sanitizeFileName($numref);
998
999 $now = dol_now();
1000
1001 // Validate
1002 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET";
1003 $sql .= " ref='".$this->db->escape($numref)."'";
1004 $sql .= ", fk_statut = 1";
1005 $sql .= ", date_valid = '".$this->db->idate($now)."'";
1006 $sql .= ", fk_user_valid = ".((int) $user->id);
1007 $sql .= " WHERE rowid = ".((int) $this->id);
1008
1009 dol_syslog(get_class($this)."::valid update expedition", LOG_DEBUG);
1010 $resql = $this->db->query($sql);
1011 if (!$resql) {
1012 $this->error = $this->db->lasterror();
1013 $error++;
1014 }
1015
1016 // If stock increment is done on sending (recommended choice)
1017 if (!$error && isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
1018 $result = $this->manageStockMvtOnEvt($user, "ShipmentValidatedInDolibarr");
1019 if ($result < 0) {
1020 return -2;
1021 }
1022 }
1023
1024 // Change status of order to "shipment in process"
1025 $triggerKey = 'SHIPPING_'; // Because when the trigger is fired the object is a shipping and not the real target object, so I add a prefix like SHIPPING_ to avoid confusion
1026 if ($this->origin == 'commande') {
1027 $triggerKey.= 'ORDER_SHIPMENTONPROCESS';
1028 } else {
1029 $triggerKey.= strtoupper($this->origin).'_SHIPMENTONPROCESS';
1030 }
1031
1032 // TODO : load the origin object to trigger the right setStatus according to origin object
1033 $ret = $this->setStatut(Commande::STATUS_SHIPMENTONPROCESS, $this->origin_id, $this->origin, $triggerKey);
1034 if (!$ret) {
1035 $error++;
1036 }
1037
1038 if (!$error && !$notrigger) {
1039 // Call trigger
1040 $result = $this->call_trigger('SHIPPING_VALIDATE', $user);
1041 if ($result < 0) {
1042 $error++;
1043 }
1044 // End call triggers
1045 }
1046
1047 if (!$error) {
1048 $this->oldref = $this->ref;
1049
1050 // Rename directory if dir was a temporary ref
1051 if (preg_match('/^[\‍(]?PROV/i', $this->ref)) {
1052 // Now we rename also files into index
1053 $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)."'";
1054 $sql .= " WHERE filename LIKE '".$this->db->escape($this->ref)."%' AND filepath = 'expedition/sending/".$this->db->escape($this->ref)."' and entity = ".((int) $conf->entity);
1055 $resql = $this->db->query($sql);
1056 if (!$resql) {
1057 $error++;
1058 $this->error = $this->db->lasterror();
1059 }
1060 $sql = 'UPDATE '.MAIN_DB_PREFIX."ecm_files set filepath = 'expedition/sending/".$this->db->escape($this->newref)."'";
1061 $sql .= " WHERE filepath = 'expedition/sending/".$this->db->escape($this->ref)."' and entity = ".((int) $conf->entity);
1062 $resql = $this->db->query($sql);
1063 if (!$resql) {
1064 $error++;
1065 $this->error = $this->db->lasterror();
1066 }
1067
1068 // We rename directory ($this->ref = old ref, $num = new ref) in order not to lose the attachments
1069 $oldref = dol_sanitizeFileName($this->ref);
1070 $newref = dol_sanitizeFileName($numref);
1071 $dirsource = $conf->expedition->dir_output.'/sending/'.$oldref;
1072 $dirdest = $conf->expedition->dir_output.'/sending/'.$newref;
1073 if (!$error && file_exists($dirsource)) {
1074 dol_syslog(get_class($this)."::valid rename dir ".$dirsource." into ".$dirdest);
1075
1076 if (@rename($dirsource, $dirdest)) {
1077 dol_syslog("Rename ok");
1078 // Rename docs starting with $oldref with $newref
1079 $listoffiles = dol_dir_list($conf->expedition->dir_output.'/sending/'.$newref, 'files', 1, '^'.preg_quote($oldref, '/'));
1080 foreach ($listoffiles as $fileentry) {
1081 $dirsource = $fileentry['name'];
1082 $dirdest = preg_replace('/^'.preg_quote($oldref, '/').'/', $newref, $dirsource);
1083 $dirsource = $fileentry['path'].'/'.$dirsource;
1084 $dirdest = $fileentry['path'].'/'.$dirdest;
1085 @rename($dirsource, $dirdest);
1086 }
1087 }
1088 }
1089 }
1090 }
1091
1092 // Set new ref and current status
1093 if (!$error) {
1094 $this->ref = $numref;
1095 $this->statut = self::STATUS_VALIDATED;
1097 }
1098
1099 if (!$error) {
1100 $this->db->commit();
1101 return 1;
1102 } else {
1103 $this->db->rollback();
1104 return -1 * $error;
1105 }
1106 }
1107
1108
1109 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1116 public function create_delivery($user)
1117 {
1118 // phpcs:enable
1119 global $conf;
1120
1121 if (getDolGlobalInt('MAIN_SUBMODULE_DELIVERY')) {
1122 if ($this->status == self::STATUS_VALIDATED || $this->status == self::STATUS_CLOSED) {
1123 // Expedition validee
1124 include_once DOL_DOCUMENT_ROOT.'/delivery/class/delivery.class.php';
1125 $delivery = new Delivery($this->db);
1126 $result = $delivery->create_from_sending($user, $this->id);
1127 if ($result > 0) {
1128 return $result;
1129 } else {
1130 $this->error = $delivery->error;
1131 return $result;
1132 }
1133 } else {
1134 return 0;
1135 }
1136 } else {
1137 return 0;
1138 }
1139 }
1140
1155 public function addline($entrepot_id, $id, $qty, $array_options = [], $fk_product = 0, $fk_parent = 0)
1156 {
1157 global $conf, $langs;
1158
1159 $num = count($this->lines);
1160 $line = new ExpeditionLigne($this->db);
1161
1162 $line->entrepot_id = $entrepot_id;
1163 $line->origin_line_id = $id;
1164 $line->fk_elementdet = $id;
1165 $line->element_type = 'order';
1166 $line->fk_parent = $fk_parent;
1167 $line->fk_product = $fk_product;
1168 $line->qty = $qty;
1169
1170 $orderline = new OrderLine($this->db);
1171 $orderline->fetch($id);
1172
1173 // Copy the rang of the order line to the expedition line
1174 $line->rang = $orderline->rang;
1175 $line->product_type = $orderline->product_type;
1176 if (!($line->fk_product > 0)) {
1177 $line->fk_product = $orderline->fk_product;
1178 }
1179
1180 if (isModEnabled('stock') && !empty($orderline->fk_product)) {
1181 $product = new Product($this->db);
1182 $product->fetch($orderline->fk_product);
1183
1184 if (!($entrepot_id > 0) && !getDolGlobalString('STOCK_WAREHOUSE_NOT_REQUIRED_FOR_SHIPMENTS') && !(getDolGlobalString('SHIPMENT_SUPPORTS_SERVICES') && $line->product_type == Product::TYPE_SERVICE) && $product->stockable_product == Product::ENABLED_STOCK) {
1185 $langs->load("errors");
1186 $this->error = $langs->trans("ErrorWarehouseRequiredIntoShipmentLine");
1187 return -1;
1188 }
1189
1190 if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT')) {
1191 $productChildrenNb = 0;
1192 if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) {
1193 $productChildrenNb = $product->hasFatherOrChild(1);
1194 }
1195 if ($productChildrenNb > 0) {
1196 $product_stock = null;
1197 $product->loadStockForVirtualProduct('warehouseopen', $line->qty);
1198 if ($entrepot_id > 0) {
1199 if (isset($product->stock_warehouse[$entrepot_id])) {
1200 $product_stock = $product->stock_warehouse[$entrepot_id]->real;
1201 }
1202 } else {
1203 foreach ($product->stock_warehouse as $componentStockWarehouse) {
1204 if ($product_stock === null) {
1205 $product_stock = $componentStockWarehouse->real;
1206 } else {
1207 $product_stock = min($product_stock, $componentStockWarehouse->real);
1208 }
1209 }
1210 }
1211 if ($product_stock === null) {
1212 $product_stock = 0;
1213 }
1214 } else {
1215 // Check must be done for stock of product into warehouse if $entrepot_id defined
1216 if ($entrepot_id > 0) {
1217 $product->load_stock('warehouseopen');
1218 $product_stock = $product->stock_warehouse[$entrepot_id]->real;
1219 } else {
1220 $product_stock = $product->stock_reel;
1221 }
1222 }
1223
1224 $product_type = $product->type;
1225 if ($product_type == 0 || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
1226 $isavirtualproduct = ($productChildrenNb > 0);
1227 // The product is qualified for a check of quantity (must be enough in stock to be added into shipment).
1228 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.
1229 if ($product->stockable_product == Product::ENABLED_STOCK && $product_stock < $qty) {
1230 $langs->load("errors");
1231 $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $product->ref);
1232 $this->errorhidden = 'ErrorStockIsNotEnoughToAddProductOnShipment';
1233
1234 $this->db->rollback();
1235 return -3;
1236 }
1237 }
1238 }
1239 }
1240 }
1241
1242 // If product need a batch number, we should not have called this function but addline_batch instead.
1243 // If this happen, we may have a bug in card.php page
1244 if (isModEnabled('productbatch') && !empty($line->fk_product) && !empty($orderline->product_tobatch)) {
1245 $this->error = 'ADDLINE_WAS_CALLED_INSTEAD_OF_ADDLINEBATCH '.$orderline->id.' '.$line->fk_product; //
1246 return -4;
1247 }
1248
1249 // extrafields
1250 if (!getDolGlobalString('MAIN_EXTRAFIELDS_DISABLED') && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used
1251 $line->array_options = $array_options;
1252 }
1253
1254 $this->lines[$num] = $line;
1255
1256 return 1;
1257 }
1258
1259 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1268 public function addline_batch($dbatch, $array_options = [], $origin_line = null)
1269 {
1270 // phpcs:enable
1271 global $conf, $langs;
1272
1273 $num = count($this->lines);
1274 $linebatch = null;
1275 if ($dbatch['qty'] > 0 || ($dbatch['qty'] == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1276 $line = new ExpeditionLigne($this->db);
1277 $tab = array();
1278 foreach ($dbatch['detail'] as $key => $value) {
1279 if ($value['q'] > 0 || ($value['q'] == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1280 // $value['q']=qty to move
1281 // $value['id_batch']=id into llx_product_batch of record to move
1282 //var_dump($value);
1283
1284 $linebatch = new ExpeditionLineBatch($this->db);
1285 $ret = $linebatch->fetchFromStock($value['id_batch']); // load serial, sellby, eatby
1286 if ($ret < 0) {
1287 $this->setErrorsFromObject($linebatch);
1288 return -1;
1289 }
1290 $linebatch->qty = $value['q'];
1291 if ($linebatch->qty == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS')) {
1292 $linebatch->batch = null;
1293 }
1294 $tab[] = $linebatch;
1295
1296 if (getDolGlobalString("STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT", '0')) {
1297 require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
1298 $prod_batch = new Productbatch($this->db);
1299 $prod_batch->fetch($value['id_batch']);
1300
1301 if ($prod_batch->qty < $linebatch->qty) {
1302 $langs->load("errors");
1303 $this->errors[] = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $prod_batch->fk_product);
1304 dol_syslog(get_class($this)."::addline_batch error=Product ".$prod_batch->batch.": ".$this->errorsToString(), LOG_ERR);
1305 $this->db->rollback();
1306 return -1;
1307 }
1308 }
1309
1310 //var_dump($linebatch);
1311 }
1312 }
1313 if (is_object($linebatch)) {
1314 $line->entrepot_id = $linebatch->fk_warehouse;
1315 }
1316 $line->origin_line_id = $dbatch['ix_l']; // deprecated
1317 $line->fk_elementdet = $dbatch['ix_l'];
1318 $line->qty = $dbatch['qty'];
1319 $line->detail_batch = $tab;
1320 if (!($line->rang > 0)) {
1321 $line->rang = $origin_line->rang;
1322 }
1323 if (!($line->fk_product > 0)) {
1324 $line->fk_product = $origin_line->fk_product;
1325 }
1326
1327 // extrafields
1328 if (!getDolGlobalString('MAIN_EXTRAFIELDS_DISABLED') && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used
1329 $line->array_options = $array_options;
1330 }
1331
1332 //var_dump($line);
1333 $this->lines[$num] = $line;
1334 return 1;
1335 }
1336 return 0;
1337 }
1338
1346 public function update($user = null, $notrigger = 0)
1347 {
1348 global $conf;
1349 $error = 0;
1350
1351 // Clean parameters
1352
1353 if (isset($this->ref)) {
1354 $this->ref = trim($this->ref);
1355 }
1356 if (isset($this->entity)) {
1357 $this->entity = (int) $this->entity;
1358 }
1359 if (isset($this->ref_customer)) {
1360 $this->ref_customer = trim($this->ref_customer);
1361 }
1362 if (isset($this->socid)) {
1363 $this->socid = (int) $this->socid;
1364 }
1365 if (isset($this->fk_user_author)) {
1366 $this->fk_user_author = (int) $this->fk_user_author;
1367 }
1368 if (isset($this->fk_user_valid)) { // @phan-ignore-current-line PhanUndeclaredProperty
1369 // If set, then accept @phan-ignore-next-line PhanUndeclaredProperty
1370 $this->fk_user_valid = (int) $this->fk_user_valid;
1371 }
1372 if (isset($this->fk_delivery_address)) {
1373 $this->fk_delivery_address = (int) $this->fk_delivery_address;
1374 }
1375 if (isset($this->shipping_method_id)) {
1376 $this->shipping_method_id = (int) $this->shipping_method_id;
1377 }
1378 if (isset($this->tracking_number)) {
1379 $this->tracking_number = trim($this->tracking_number);
1380 }
1381 if (isset($this->statut)) {
1382 $this->statut = (int) $this->statut;
1383 }
1384 if (isset($this->status)) {
1385 $this->status = (int) $this->status;
1386 }
1387 if (isset($this->trueDepth)) {
1388 $this->trueDepth = trim($this->trueDepth);
1389 }
1390 if (isset($this->trueWidth)) {
1391 $this->trueWidth = trim($this->trueWidth);
1392 }
1393 if (isset($this->trueHeight)) {
1394 $this->trueHeight = trim($this->trueHeight);
1395 }
1396 if (isset($this->size_units)) {
1397 $this->size_units = trim($this->size_units);
1398 }
1399 if (isset($this->weight_units)) {
1400 $this->weight_units = (int) $this->weight_units;
1401 }
1402 if (isset($this->trueWeight)) {
1403 $this->weight = trim((string) $this->trueWeight);
1404 }
1405 if (isset($this->note_private)) {
1406 $this->note_private = trim($this->note_private);
1407 }
1408 if (isset($this->note_public)) {
1409 $this->note_public = trim($this->note_public);
1410 }
1411 if (isset($this->model_pdf)) {
1412 $this->model_pdf = trim($this->model_pdf);
1413 }
1414 if (!empty($this->date_expedition)) {
1415 $this->date_shipping = $this->date_expedition;
1416 }
1417
1418 // Check parameters
1419 // Put here code to add control on parameters values
1420
1421 // Update request
1422 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET";
1423 $sql .= " ref = ".(isset($this->ref) ? "'".$this->db->escape($this->ref)."'" : "null").",";
1424 $sql .= " ref_ext = ".(isset($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null").",";
1425 $sql .= " ref_customer = ".(isset($this->ref_customer) ? "'".$this->db->escape($this->ref_customer)."'" : "null").",";
1426 $sql .= " fk_soc = ".(isset($this->socid) ? $this->socid : "null").",";
1427 $sql .= " date_creation = ".(dol_strlen($this->date_creation) != 0 ? "'".$this->db->idate($this->date_creation)."'" : 'null').",";
1428 $sql .= " fk_user_author = ".(isset($this->fk_user_author) ? $this->fk_user_author : "null").",";
1429 $sql .= " date_valid = ".(dol_strlen($this->date_valid) != 0 ? "'".$this->db->idate($this->date_valid)."'" : 'null').",";
1430 $sql .= " fk_user_valid = ".(isset($this->fk_user_valid) ? $this->fk_user_valid : "null").",";
1431 $sql .= " date_expedition = ".(dol_strlen($this->date_shipping) != 0 ? "'".$this->db->idate($this->date_shipping)."'" : 'null').",";
1432 $sql .= " date_delivery = ".(dol_strlen($this->date_delivery) != 0 ? "'".$this->db->idate($this->date_delivery)."'" : 'null').",";
1433 $sql .= " fk_address = ".(isset($this->fk_delivery_address) ? $this->fk_delivery_address : "null").",";
1434 $sql .= " fk_shipping_method = ".((isset($this->shipping_method_id) && $this->shipping_method_id > 0) ? $this->shipping_method_id : "null").",";
1435 $sql .= " tracking_number = ".(isset($this->tracking_number) ? "'".$this->db->escape($this->tracking_number)."'" : "null").",";
1436 $sql .= " fk_statut = ".(isset($this->status) ? $this->status : "null").",";
1437 $sql .= " fk_projet = ".(isset($this->fk_project) ? $this->fk_project : "null").",";
1438 $sql .= " height = ".(($this->trueHeight != '') ? $this->trueHeight : "null").",";
1439 $sql .= " width = ".(($this->trueWidth != '') ? $this->trueWidth : "null").",";
1440 $sql .= " size_units = ".(isset($this->size_units) ? $this->size_units : "null").",";
1441 $sql .= " size = ".(($this->trueDepth != '') ? $this->trueDepth : "null").",";
1442 $sql .= " weight_units = ".(isset($this->weight_units) ? $this->weight_units : "null").",";
1443 $sql .= " weight = ".(($this->trueWeight != '') ? $this->trueWeight : "null").",";
1444 $sql .= " note_private = ".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : "null").",";
1445 $sql .= " note_public = ".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : "null").",";
1446 $sql .= " model_pdf = ".(isset($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null").",";
1447 $sql .= " entity = ".((int) $conf->entity);
1448 $sql .= " WHERE rowid = ".((int) $this->id);
1449
1450 $this->db->begin();
1451
1452 dol_syslog(get_class($this)."::update", LOG_DEBUG);
1453 $resql = $this->db->query($sql);
1454 if (!$resql) {
1455 $error++;
1456 $this->errors[] = "Error ".$this->db->lasterror();
1457 }
1458
1459 // Actions on extra fields
1460 if (!$error) {
1461 $result = $this->insertExtraFields();
1462 if ($result < 0) {
1463 $error++;
1464 }
1465 }
1466
1467 if (!$error && !$notrigger) {
1468 // Call trigger
1469 $result = $this->call_trigger('SHIPPING_MODIFY', $user);
1470 if ($result < 0) {
1471 $error++;
1472 }
1473 // End call triggers
1474 }
1475
1476 // Commit or rollback
1477 if ($error) {
1478 foreach ($this->errors as $errmsg) {
1479 dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR);
1480 $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
1481 }
1482 $this->db->rollback();
1483 return -1 * $error;
1484 } else {
1485 $this->db->commit();
1486 return 1;
1487 }
1488 }
1489
1490
1498 public function cancel($notrigger = 0, $also_update_stock = false)
1499 {
1500 global $conf, $langs, $user;
1501
1502 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1503
1504 $error = 0;
1505 $this->error = '';
1506
1507 $this->db->begin();
1508
1509 // Add a protection to refuse deleting if shipment has at least one delivery
1510 $this->fetchObjectLinked($this->id, 'shipping', 0, 'delivery'); // Get deliveries linked to this shipment
1511 if (isset($this->linkedObjectsIds['delivery']) && count($this->linkedObjectsIds['delivery']) > 0) {
1512 $this->error = 'ErrorThereIsSomeDeliveries';
1513 $error++;
1514 }
1515
1516 if (!$error && !$notrigger) {
1517 // Call trigger
1518 $result = $this->call_trigger('SHIPPING_CANCEL', $user);
1519 if ($result < 0) {
1520 $error++;
1521 }
1522 // End call triggers
1523 }
1524
1525 // Stock control
1526 $can_update_stock = isModEnabled('stock') &&
1527 ((getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') && $this->status > self::STATUS_DRAFT) ||
1528 (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE') && $this->status == self::STATUS_CLOSED && $also_update_stock));
1529 if (!$error) {
1530 require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php";
1531
1532 $langs->load("agenda");
1533
1534 // Loop on each product line to add a stock movement (contain sub-products)
1535 $sql = "SELECT ";
1536 $sql .= " ed.fk_product";
1537 $sql .= ", ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id";
1538 $sql .= ", SUM(".$this->db->ifsql("pa.rowid IS NOT NULL", "1", "0").") as iskit";
1539 $sql .= ", ".$this->db->ifsql("pai.incdec IS NULL", "1", "pai.incdec")." as incdec";
1540 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
1541 $sql .= " LEFT JOIN ".$this->db->prefix()."product_association as pa ON pa.fk_product_pere = ed.fk_product";
1542 $sql .= " LEFT JOIN ".$this->db->prefix()."expeditiondet as edp ON edp.rowid = ed.fk_parent";
1543 $sql .= " LEFT JOIN ".$this->db->prefix()."product_association as pai ON pai.fk_product_pere = edp.fk_product AND pai.fk_product_fils = ed.fk_product";
1544 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
1545 $sql .= " GROUP BY ed.fk_product, ed.qty, ed.fk_entrepot, ed.rowid, pai.incdec";
1546 $sql .= $this->db->order("ed.rowid", "DESC");
1547
1548 dol_syslog(get_class($this)."::delete select details", LOG_DEBUG);
1549 $resql = $this->db->query($sql);
1550 if ($resql) {
1551 $cpt = $this->db->num_rows($resql);
1552
1553 $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1554
1555 for ($i = 0; $i < $cpt; $i++) {
1556 dol_syslog(get_class($this)."::delete movement index ".$i);
1557 $obj = $this->db->fetch_object($resql);
1558 $line_id = (int) $obj->expeditiondet_id;
1559
1560 if ($can_update_stock && (empty($obj->iskit) || getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE')) && !empty($obj->incdec)) {
1561 $mouvS = new MouvementStock($this->db);
1562 // we do not log origin because it will be deleted
1563 $mouvS->origin = '';
1564 // get lot/serial
1565 $lotArray = null;
1566 if (isModEnabled('productbatch')) {
1567 $lotArray = $shipmentlinebatch->fetchAll($obj->expeditiondet_id);
1568 if (!is_array($lotArray)) {
1569 $error++;
1570 $this->errors[] = "Error ".$this->db->lasterror();
1571 }
1572 }
1573
1574 if (empty($lotArray)) {
1575 // no lot/serial
1576 // We increment stock of product (and sub-products)
1577 // We use warehouse selected for each line
1578 $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $obj->qty, 0, $langs->trans("ShipmentCanceledInDolibarr", $this->ref), '', '', '', '', 0, '', 0, (empty($obj->iskit) ? 1 : 0)); // Price is set to 0, because we don't want to see WAP changed
1579 if ($result < 0) {
1580 $error++;
1581 $this->errors = array_merge($this->errors, $mouvS->errors);
1582 break;
1583 }
1584 } else {
1585 // We increment stock of batches
1586 // We use warehouse selected for each line
1587 foreach ($lotArray as $lot) {
1588 $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $lot->qty, 0, $langs->trans("ShipmentCanceledInDolibarr", $this->ref), $lot->eatby, $lot->sellby, (string) $lot->batch, '', 0, '', 0, (empty($obj->iskit) ? 1 : 0)); // Price is set to 0, because we don't want to see WAP changed
1589 if ($result < 0) {
1590 $error++;
1591 $this->errors = array_merge($this->errors, $mouvS->errors);
1592 break;
1593 }
1594 }
1595 if ($error) {
1596 break; // break for loop in case of error
1597 }
1598 }
1599 }
1600
1601 if (!$error) {
1602 // delete all children and batches of this shipment line
1603 $shipment_line = new ExpeditionLigne($this->db);
1604 $res = $shipment_line->fetch($line_id);
1605 if ($res > 0) {
1606 $result = $shipment_line->delete($user);
1607 if ($result < 0) {
1608 $error++;
1609 $this->errors[] = "Error ".$shipment_line->errorsToString();
1610 }
1611 } else {
1612 $error++;
1613 $this->errors[] = "Error ".$shipment_line->errorsToString();
1614 }
1615 }
1616
1617 if ($error) {
1618 break;
1619 }
1620 }
1621 } else {
1622 $error++;
1623 $this->errors[] = "Error ".$this->db->lasterror();
1624 }
1625 }
1626
1627 if (!$error) {
1628 // Delete linked object
1629 $res = $this->deleteObjectLinked();
1630 if ($res < 0) {
1631 $error++;
1632 }
1633
1634 // No delete expedition
1635 if (!$error) {
1636 $sql = "SELECT rowid FROM ".$this->db->prefix()."expedition";
1637 $sql .= " WHERE rowid = ".((int) $this->id);
1638
1639 if ($this->db->query($sql)) {
1640 if (!empty($this->origin) && $this->origin_id > 0) {
1641 $this->fetch_origin();
1643 '@phan-var-force Facture|Commande $origin_object';
1644 if ($origin_object->status == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress"
1645 // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress"
1646 $origin_object->loadExpeditions();
1647 //var_dump($this->$origin->expeditions);exit;
1648 if (count($origin_object->expeditions) <= 0) {
1650 }
1651 }
1652 }
1653
1654 if (!$error) {
1655 $this->db->commit();
1656
1657 // We delete PDFs
1658 $ref = dol_sanitizeFileName($this->ref);
1659 if (!empty($conf->expedition->dir_output)) {
1660 $dir = $conf->expedition->dir_output.'/sending/'.$ref;
1661 $file = $dir.'/'.$ref.'.pdf';
1662 if (file_exists($file)) {
1663 if (!dol_delete_file($file)) {
1664 return 0;
1665 }
1666 }
1667 if (file_exists($dir)) {
1668 if (!dol_delete_dir_recursive($dir)) {
1669 $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir);
1670 return 0;
1671 }
1672 }
1673 }
1674
1675 return 1;
1676 } else {
1677 $this->db->rollback();
1678 return -1;
1679 }
1680 } else {
1681 $this->error = $this->db->lasterror()." - sql=$sql";
1682 $this->db->rollback();
1683 return -3;
1684 }
1685 } else {
1686 $this->db->rollback();
1687 return -2;
1688 }
1689 } else {
1690 $this->db->rollback();
1691 return -1;
1692 }
1693 }
1694
1704 public function delete($user = null, $notrigger = 0, $also_update_stock = false)
1705 {
1706 global $conf, $langs;
1707
1708 if (empty($user)) {
1709 global $user;
1710 }
1711
1712 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1713
1714 $error = 0;
1715 $this->error = '';
1716
1717 $this->db->begin();
1718
1719 // Add a protection to refuse deleting if shipment has at least one delivery
1720 $this->fetchObjectLinked($this->id, 'shipping', 0, 'delivery'); // Get deliveries linked to this shipment
1721 if (isset($this->linkedObjectsIds['delivery']) && count($this->linkedObjectsIds['delivery']) > 0) {
1722 $this->error = 'ErrorThereIsSomeDeliveries';
1723 $error++;
1724 }
1725
1726 if (!$error && !$notrigger) {
1727 // Call trigger
1728 $result = $this->call_trigger('SHIPPING_DELETE', $user);
1729 if ($result < 0) {
1730 $error++;
1731 }
1732 // End call triggers
1733 }
1734
1735 // Stock control
1736 $can_update_stock = isModEnabled('stock') &&
1737 ((getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') && $this->status > self::STATUS_DRAFT) ||
1738 (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE') && $this->status == self::STATUS_CLOSED && $also_update_stock));
1739 if (!$error) {
1740 require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php";
1741
1742 $langs->load("agenda");
1743
1744 // we try deletion of batch line even if module batch not enabled in case of the module were enabled and disabled previously
1745 $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1746
1747 // Loop on each product line to add a stock movement (contain sub-products)
1748 $sql = "SELECT ";
1749 $sql .= " ed.fk_product";
1750 $sql .= ", ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id";
1751 $sql .= ", SUM(".$this->db->ifsql("pa.rowid IS NOT NULL", "1", "0").") as iskit";
1752 $sql .= ", ".$this->db->ifsql("pai.incdec IS NULL", "1", "pai.incdec")." as incdec";
1753 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
1754 $sql .= " LEFT JOIN ".$this->db->prefix()."product_association as pa ON pa.fk_product_pere = ed.fk_product";
1755 $sql .= " LEFT JOIN ".$this->db->prefix()."expeditiondet as edp ON edp.rowid = ed.fk_parent";
1756 $sql .= " LEFT JOIN ".$this->db->prefix()."product_association as pai ON pai.fk_product_pere = edp.fk_product AND pai.fk_product_fils = ed.fk_product";
1757 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
1758 $sql .= " GROUP BY ed.fk_product, ed.qty, ed.fk_entrepot, ed.rowid, pai.incdec";
1759 $sql .= $this->db->order("ed.rowid", "DESC");
1760
1761 dol_syslog(get_class($this)."::delete select details", LOG_DEBUG);
1762 $resql = $this->db->query($sql);
1763 if ($resql) {
1764 $cpt = $this->db->num_rows($resql);
1765 for ($i = 0; $i < $cpt; $i++) {
1766 dol_syslog(get_class($this)."::delete movement index ".$i);
1767 $obj = $this->db->fetch_object($resql);
1768 $line_id = (int) $obj->expeditiondet_id;
1769
1770 if ($can_update_stock && (empty($obj->iskit) || getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE')) && !empty($obj->incdec)) {
1771 $mouvS = new MouvementStock($this->db);
1772 // we do not log origin because it will be deleted
1773 $mouvS->origin = '';
1774 // get lot/serial
1775 $lotArray = $shipmentlinebatch->fetchAll($line_id);
1776 if (!is_array($lotArray)) {
1777 $error++;
1778 $this->errors[] = "Error ".$this->db->lasterror();
1779 }
1780 if (empty($lotArray)) {
1781 // no lot/serial
1782 // We increment stock of product (disable for sub-products : already in shipment lines)
1783 // We use warehouse selected for each line
1784 $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $obj->qty, 0, $langs->trans("ShipmentDeletedInDolibarr", $this->ref), '', '', '', '', 0, '', 0, (empty($obj->iskit) ? 1 : 0)); // Price is set to 0, because we don't want to see WAP changed
1785 if ($result < 0) {
1786 $error++;
1787 $this->errors = array_merge($this->errors, $mouvS->errors);
1788 break;
1789 }
1790 } else {
1791 // We increment stock of batches
1792 // We use warehouse selected for each line
1793 foreach ($lotArray as $lot) {
1794 $result = $mouvS->reception($user, $obj->fk_product, $obj->fk_entrepot, $lot->qty, 0, $langs->trans("ShipmentDeletedInDolibarr", $this->ref), $lot->eatby, $lot->sellby, (string) $lot->batch, '', 0, '', 0, (empty($obj->iskit) ? 1 : 0)); // Price is set to 0, because we don't want to see WAP changed
1795 if ($result < 0) {
1796 $error++;
1797 $this->errors = array_merge($this->errors, $mouvS->errors);
1798 break;
1799 }
1800 }
1801 if ($error) {
1802 break; // break for loop in case of error
1803 }
1804 }
1805 }
1806
1807 if (!$error) {
1808 // delete all children and batches of this shipment line
1809 $shipment_line = new ExpeditionLigne($this->db);
1810 $res = $shipment_line->fetch($line_id);
1811 if ($res > 0) {
1812 $result = $shipment_line->delete($user);
1813 if ($result < 0) {
1814 $error++;
1815 $this->errors[] = "Error ".$shipment_line->errorsToString();
1816 }
1817 } else {
1818 $error++;
1819 $this->errors[] = "Error ".$shipment_line->errorsToString();
1820 }
1821 }
1822
1823 if ($error) {
1824 break;
1825 }
1826 }
1827 } else {
1828 $error++;
1829 $this->errors[] = "Error ".$this->db->lasterror();
1830 }
1831 }
1832
1833 if (!$error) {
1834 // Delete linked object
1835 $res = $this->deleteObjectLinked();
1836 if ($res < 0) {
1837 $error++;
1838 }
1839
1840 // delete extrafields
1841 $res = $this->deleteExtraFields();
1842 if ($res < 0) {
1843 $error++;
1844 }
1845
1846 if (!$error) {
1847 // Delete linked contacts
1848 $res = $this->delete_linked_contact();
1849 if ($res < 0) {
1850 $error++;
1851 }
1852 }
1853 if (!$error) {
1854 $sql = "DELETE FROM ".$this->db->prefix()."expedition";
1855 $sql .= " WHERE rowid = ".((int) $this->id);
1856
1857 if ($this->db->query($sql)) {
1858 if (!empty($this->origin) && $this->origin_id > 0) {
1859 $this->fetch_origin();
1861 '@phan-var-force Facture|Commande $origin_object';
1862 if ($origin_object->status == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress"
1863 // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress"
1864 $origin_object->loadExpeditions();
1865 //var_dump($this->$origin->expeditions);exit;
1866 if (count($origin_object->expeditions) <= 0) {
1868 }
1869 }
1870 }
1871
1872 if (!$error) {
1873 $this->db->commit();
1874
1875 // Delete record into ECM index (Note that delete is also done when deleting files with the dol_delete_dir_recursive
1876 $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
1877 $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
1878
1879 // We delete PDFs
1880 $ref = dol_sanitizeFileName($this->ref);
1881 if (!empty($conf->expedition->dir_output)) {
1882 $dir = $conf->expedition->dir_output . '/sending/' . $ref;
1883 $file = $dir . '/' . $ref . '.pdf';
1884 if (file_exists($file)) {
1885 if (!dol_delete_file($file)) {
1886 return 0;
1887 }
1888 }
1889 if (file_exists($dir)) {
1890 if (!dol_delete_dir_recursive($dir)) {
1891 $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir);
1892 return 0;
1893 }
1894 }
1895 }
1896
1897 return 1;
1898 } else {
1899 $this->db->rollback();
1900 return -1;
1901 }
1902 } else {
1903 $this->error = $this->db->lasterror()." - sql=$sql";
1904 $this->db->rollback();
1905 return -3;
1906 }
1907 } else {
1908 $this->db->rollback();
1909 return -2;
1910 }
1911 } else {
1912 $this->db->rollback();
1913 return -1;
1914 }
1915 }
1916
1917 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1923 public function fetch_lines()
1924 {
1925 // phpcs:enable
1926 global $mysoc;
1927
1928 $this->lines = array();
1929
1930 // NOTE: This fetch_lines is special because it groups all lines with the same origin_line_id into one line.
1931 // TODO: See if we can restore a common fetch_lines (one line = one record)
1932
1933 $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";
1934 $sql .= ", cd.total_ht, cd.total_localtax1, cd.total_localtax2, cd.total_ttc, cd.total_tva";
1935 $sql .= ", cd.fk_remise_except, cd.fk_product_fournisseur_price as fk_fournprice";
1936 $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";
1937 $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, cd.special_code";
1938 $sql .= ", ed.rowid as line_id, ed.qty as qty_shipped, ed.fk_element, ed.fk_elementdet, ed.element_type, ed.fk_entrepot, ed.extraparams";
1939 $sql .= ", p.ref as product_ref, p.label as product_label, p.fk_product_type, p.barcode as product_barcode";
1940 $sql .= ", p.weight, p.weight_units, p.length, p.length_units, p.width, p.width_units, p.height, p.height_units";
1941 $sql .= ", p.surface, p.surface_units, p.volume, p.volume_units, p.tosell as product_tosell, p.tobuy as product_tobuy";
1942 $sql .= ", p.tobatch as product_tobatch, p.stockable_product";
1943 $sql .= " FROM ".MAIN_DB_PREFIX."expeditiondet as ed, ".MAIN_DB_PREFIX."commandedet as cd";
1944 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product";
1945 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
1946 $sql .= " AND ed.fk_elementdet = cd.rowid";
1947 $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.
1948
1949 dol_syslog(get_class($this)."::fetch_lines", LOG_DEBUG);
1950 $resql = $this->db->query($sql);
1951 if ($resql) {
1952 include_once DOL_DOCUMENT_ROOT.'/core/lib/price.lib.php';
1953
1954 $num = $this->db->num_rows($resql);
1955 $i = 0;
1956 $line = new ExpeditionLigne($this->db);
1957 $lineindex = 0;
1958 $originline = 0;
1959
1960 $this->total_ht = 0;
1961 $this->total_tva = 0;
1962 $this->total_ttc = 0;
1963 $this->total_localtax1 = 0;
1964 $this->total_localtax2 = 0;
1965
1966 $this->multicurrency_total_ht = 0;
1967 $this->multicurrency_total_tva = 0;
1968 $this->multicurrency_total_ttc = 0;
1969
1970 $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1971
1972 $line = new ExpeditionLigne($this->db); // always set $line for PHAN analyser. @phan-var-force muse be used after an assignation, and there is no assignation for $line.
1973 while ($i < $num) {
1974 $obj = $this->db->fetch_object($resql);
1975
1976 if ($originline > 0 && $originline == $obj->fk_elementdet) {
1977 // '@phan-var-force ExpeditionLigne $line'; // $line from previous loop
1978 $line->entrepot_id = 0; // entrepod_id in details_entrepot
1979 $line->qty_shipped += $obj->qty_shipped;
1980 } else {
1981 $line = new ExpeditionLigne($this->db); // new group to start
1982 $line->entrepot_id = $obj->fk_entrepot; // this is a property of a shipment line
1983 $line->qty_shipped = $obj->qty_shipped; // this is a property of a shipment line
1984 }
1985
1986 $detail_entrepot = new stdClass();
1987 $detail_entrepot->entrepot_id = $obj->fk_entrepot;
1988 $detail_entrepot->qty_shipped = $obj->qty_shipped;
1989 $detail_entrepot->line_id = $obj->line_id;
1990 $line->details_entrepot[] = $detail_entrepot;
1991
1992 $line->line_id = $obj->line_id; // TODO deprecated
1993 $line->rowid = $obj->line_id; // TODO deprecated
1994 $line->id = $obj->line_id;
1995
1996 $line->fk_origin = 'orderline'; // TODO deprecated, we already have element_type that can be use to guess type of line
1997
1998 $line->fk_element = $obj->fk_element;
1999 $line->origin_id = $obj->fk_element;
2000 $line->fk_elementdet = $obj->fk_elementdet;
2001 $line->origin_line_id = $obj->fk_elementdet;
2002 $line->element_type = $obj->element_type;
2003
2004 $line->fk_expedition = $this->id; // id of parent
2005
2006 $line->stockable_product = $obj->stockable_product;
2007 $line->product_type = $obj->product_type;
2008 $line->fk_product = $obj->fk_product;
2009 $line->fk_product_type = $obj->fk_product_type;
2010 $line->ref = $obj->product_ref; // TODO deprecated
2011 $line->product_ref = $obj->product_ref;
2012 $line->product_label = $obj->product_label;
2013 $line->libelle = $obj->product_label; // TODO deprecated
2014 $line->product_barcode = $obj->product_barcode; // Barcode number product
2015 $line->product_tosell = $obj->product_tosell;
2016 $line->product_tobuy = $obj->product_tobuy;
2017 $line->product_tobatch = $obj->product_tobatch;
2018 $line->fk_fournprice = $obj->fk_fournprice;
2019 $line->label = $obj->custom_label;
2020 $line->description = $obj->description;
2021 $line->qty_asked = $obj->qty_asked;
2022 $line->rang = $obj->rang;
2023 $line->weight = $obj->weight;
2024 $line->weight_units = $obj->weight_units;
2025 $line->length = $obj->length;
2026 $line->length_units = $obj->length_units;
2027 $line->width = $obj->width;
2028 $line->width_units = $obj->width_units;
2029 $line->height = $obj->height;
2030 $line->height_units = $obj->height_units;
2031 $line->surface = $obj->surface;
2032 $line->surface_units = $obj->surface_units;
2033 $line->volume = $obj->volume;
2034 $line->volume_units = $obj->volume_units;
2035 $line->stockable_product = $obj->stockable_product;
2036 $line->fk_unit = $obj->fk_unit;
2037
2038 $line->extraparams = !empty($obj->extraparams) ? (array) json_decode($obj->extraparams, true) : array();
2039
2040 $line->pa_ht = $obj->pa_ht;
2041
2042 // Local taxes
2043 $localtax_array = array(0 => $obj->localtax1_type, 1 => $obj->localtax1_tx, 2 => $obj->localtax2_type, 3 => $obj->localtax2_tx);
2044 $localtax1_tx = get_localtax($obj->tva_tx, 1, $this->thirdparty);
2045 $localtax2_tx = get_localtax($obj->tva_tx, 2, $this->thirdparty);
2046
2047 // For invoicing
2048 $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
2049 $line->desc = $obj->description; // We need ->desc because some code into CommonObject use desc (property defined for other elements)
2050 $line->qty = $line->qty_shipped;
2051 $line->total_ht = (float) $tabprice[0];
2052 $line->total_localtax1 = (float) $tabprice[9];
2053 $line->total_localtax2 = (float) $tabprice[10];
2054 $line->total_ttc = (float) $tabprice[2];
2055 $line->total_tva = (float) $tabprice[1];
2056 $line->vat_src_code = $obj->vat_src_code;
2057 $line->tva_tx = $obj->tva_tx;
2058 $line->localtax1_tx = $obj->localtax1_tx;
2059 $line->localtax2_tx = $obj->localtax2_tx;
2060 $line->info_bits = $obj->info_bits;
2061 $line->price = $obj->price;
2062 $line->subprice = $obj->subprice;
2063 $line->fk_remise_except = $obj->fk_remise_except;
2064 $line->remise_percent = $obj->remise_percent;
2065
2066 $this->total_ht += $tabprice[0];
2067 $this->total_tva += $tabprice[1];
2068 $this->total_ttc += $tabprice[2];
2069 $this->total_localtax1 += $tabprice[9];
2070 $this->total_localtax2 += $tabprice[10];
2071
2072 $line->date_start = $this->db->jdate($obj->date_start);
2073 $line->date_end = $this->db->jdate($obj->date_end);
2074
2075 $line->special_code = $obj->special_code;
2076
2077 // Multicurrency
2078 $this->fk_multicurrency = $obj->fk_multicurrency;
2079 $this->multicurrency_code = $obj->multicurrency_code;
2080 $line->multicurrency_subprice = $obj->multicurrency_subprice;
2081 $line->multicurrency_total_ht = $obj->multicurrency_total_ht;
2082 $line->multicurrency_total_tva = $obj->multicurrency_total_tva;
2083 $line->multicurrency_total_ttc = $obj->multicurrency_total_ttc;
2084
2085 $this->multicurrency_total_ht += $obj->multicurrency_total_ht;
2086 $this->multicurrency_total_tva += $obj->multicurrency_total_tva;
2087 $this->multicurrency_total_ttc += $obj->multicurrency_total_ttc;
2088
2089 if ($originline != $obj->fk_elementdet) {
2090 $line->detail_batch = array();
2091 }
2092
2093 // Detail of batch
2094 if (isModEnabled('productbatch') && $obj->line_id > 0 && $obj->product_tobatch > 0) {
2095 $newdetailbatch = $shipmentlinebatch->fetchAll($obj->line_id, $obj->fk_product);
2096
2097 if (is_array($newdetailbatch)) {
2098 if ($originline != $obj->fk_elementdet) {
2099 $line->detail_batch = $newdetailbatch;
2100 } else {
2101 $line->detail_batch = array_merge($line->detail_batch, $newdetailbatch);
2102 }
2103 }
2104 }
2105
2106 // virtual product : find all children stock (group by product id and warehouse id)
2107 if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) {
2108 $detail_children = array(); // detail by product : array of [warehouse_id => total_qty]
2109 $line_child_list = array();
2110 $res = $line->findAllChild($line->id, $line_child_list, 1);
2111 if ($res > 0) {
2112 foreach ($line_child_list as $child_line) {
2113 foreach ($child_line as $child_obj) {
2114 $child_product_id = (int) $child_obj->fk_product;
2115 $child_warehouse_id = (int) $child_obj->fk_warehouse;
2116
2117 if ($child_warehouse_id > 0) {
2118 // child quantities group by warehouses
2119 if (!isset($detail_children[$child_product_id])) {
2120 $detail_children[$child_product_id] = array();
2121 }
2122 if (!isset($detail_children[$child_product_id][$child_warehouse_id])) {
2123 $detail_children[$child_product_id][$child_warehouse_id] = 0;
2124 }
2125 $detail_children[$child_product_id][$child_warehouse_id] += $child_obj->qty;
2126 }
2127 }
2128 }
2129 }
2130 $line->detail_children = $detail_children;
2131 }
2132
2133 $line->fetch_optionals();
2134
2135 if ($originline != $obj->fk_elementdet) {
2136 $this->lines[$lineindex] = $line;
2137 $lineindex++;
2138 } else {
2139 $line->total_ht += $tabprice[0];
2140 $line->total_localtax1 += $tabprice[9];
2141 $line->total_localtax2 += $tabprice[10];
2142 $line->total_ttc += $tabprice[2];
2143 $line->total_tva += $tabprice[1];
2144 }
2145
2146 $i++;
2147 $originline = $obj->fk_elementdet;
2148 }
2149 $this->db->free($resql);
2150 return 1;
2151 } else {
2152 $this->error = $this->db->error();
2153 return -3;
2154 }
2155 }
2156
2164 public function deleteLine($user, $lineid)
2165 {
2166 global $user;
2167
2168 if ($this->status == self::STATUS_DRAFT) {
2169 $this->db->begin();
2170
2171 $line = new ExpeditionLigne($this->db);
2172
2173 // For triggers
2174 $line->fetch($lineid);
2175
2176 if ($line->delete($user) > 0) {
2177 //$this->update_price(1);
2178
2179 $this->db->commit();
2180 return 1;
2181 } else {
2182 $this->db->rollback();
2183 return -1;
2184 }
2185 } else {
2186 $this->error = 'ErrorDeleteLineNotAllowedByObjectStatus';
2187 return -2;
2188 }
2189 }
2190
2191
2198 public function getTooltipContentArray($params)
2199 {
2200 global $conf, $langs;
2201
2202 $langs->load('sendings');
2203
2204 $nofetch = !empty($params['nofetch']);
2205
2206 $datas = array();
2207 $datas['picto'] = img_picto('', $this->picto).' <u class="paddingrightonly">'.$langs->trans("Shipment").'</u>';
2208 if (isset($this->status)) {
2209 $datas['picto'] .= ' '.$this->getLibStatut(5);
2210 }
2211 $datas['ref'] = '<br><b>'.$langs->trans('Ref').':</b> '.$this->ref;
2212 $datas['refcustomer'] = '<br><b>'.$langs->trans('RefCustomer').':</b> '.($this->ref_customer ? $this->ref_customer : $this->ref_client);
2213 if (!$nofetch) {
2214 $langs->load('companies');
2215 if (empty($this->thirdparty)) {
2216 $this->fetch_thirdparty();
2217 }
2218 $datas['customer'] = '<br><b>'.$langs->trans('Customer').':</b> '.$this->thirdparty->getNomUrl(1, '', 0, 1);
2219 }
2220
2221 return $datas;
2222 }
2223
2235 public function getNomUrl($withpicto = 0, $option = '', $max = 0, $short = 0, $notooltip = 0, $save_lastsearch_value = -1)
2236 {
2237 global $langs, $hookmanager;
2238
2239 $result = '';
2240 $params = [
2241 'id' => $this->id,
2242 'objecttype' => $this->element,
2243 'option' => $option,
2244 'nofetch' => 1,
2245 ];
2246 $classfortooltip = 'classfortooltip';
2247 $dataparams = '';
2248 if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
2249 $classfortooltip = 'classforajaxtooltip';
2250 $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
2251 $label = '';
2252 } else {
2253 $label = implode($this->getTooltipContentArray($params));
2254 }
2255
2256 $url = DOL_URL_ROOT.'/expedition/card.php?id='.$this->id;
2257
2258 if ($short) {
2259 return $url;
2260 }
2261
2262 if ($option !== 'nolink') {
2263 // Add param to save lastsearch_values or not
2264 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
2265 if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
2266 $add_save_lastsearch_values = 1;
2267 }
2268 if ($add_save_lastsearch_values) {
2269 $url .= '&save_lastsearch_values=1';
2270 }
2271 }
2272
2273 $linkclose = '';
2274 if (empty($notooltip)) {
2275 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
2276 $label = $langs->trans("Shipment");
2277 $linkclose .= ' alt="'.dolPrintHTMLForAttribute($label).'"';
2278 }
2279 $linkclose .= ($label ? ' title="'.dolPrintHTMLForAttribute($label).'"' : ' title="tocomplete"');
2280 $linkclose .= $dataparams.' class="'.$classfortooltip.'"';
2281 }
2282
2283 $linkstart = '<a href="'.$url.'"';
2284 $linkstart .= $linkclose.'>';
2285 $linkend = '</a>';
2286
2287 $result .= $linkstart;
2288 if ($withpicto) {
2289 $result .= img_object(($notooltip ? '' : $label), ($this->picto ? $this->picto : 'generic'), ($notooltip ? (($withpicto != 2) ? 'class="paddingright"' : '') : 'class="'.(($withpicto != 2) ? 'paddingright ' : '').'"'), 0, 0, $notooltip ? 0 : 1);
2290 }
2291 if ($withpicto != 2) {
2292 $result .= $this->ref;
2293 }
2294 $result .= $linkend;
2295 global $action;
2296 $hookmanager->initHooks(array($this->element . 'dao'));
2297 $parameters = array('id' => $this->id, 'getnomurl' => &$result);
2298 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
2299 if ($reshook > 0) {
2300 $result = $hookmanager->resPrint;
2301 } else {
2302 $result .= $hookmanager->resPrint;
2303 }
2304 return $result;
2305 }
2306
2313 public function getLibStatut($mode = 0)
2314 {
2315 return $this->LibStatut($this->status, $mode);
2316 }
2317
2318 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2326 public function LibStatut($status, $mode)
2327 {
2328 // phpcs:enable
2329 global $langs;
2330
2331 $labelStatus = $langs->transnoentitiesnoconv($this->labelStatus[$status]);
2332 $labelStatusShort = $langs->transnoentitiesnoconv($this->labelStatusShort[$status]);
2333
2334 $statusType = 'status'.$status;
2335 if ($status == self::STATUS_VALIDATED) {
2336 $statusType = 'status4';
2337 }
2338 if ($status == self::STATUS_CLOSED) {
2339 $statusType = 'status6';
2340 }
2341 if ($status == self::STATUS_CANCELED) {
2342 $statusType = 'status9';
2343 }
2344
2345 $signed_label = ' (' . $this->getLibSignedStatus() . ')';
2346 $status_label = $this->signed_status ? $labelStatus . $signed_label : $labelStatus;
2347 $status_label_short = $this->signed_status ? $labelStatusShort . $signed_label : $labelStatusShort;
2348
2349 return dolGetStatus($status_label, $status_label_short, '', $statusType, $mode);
2350 }
2351
2359 public function getKanbanView($option = '', $arraydata = null)
2360 {
2361 global $langs, $conf;
2362
2363 $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
2364
2365 $return = '<div class="box-flex-item box-flex-grow-zero">';
2366 $return .= '<div class="info-box info-box-sm">';
2367 $return .= '<div class="info-box-icon bg-infobox-action">';
2368 $return .= img_picto('', 'order');
2369 $return .= '</div>';
2370 $return .= '<div class="info-box-content">';
2371 $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">'.(method_exists($this, 'getNomUrl') ? $this->getNomUrl() : $this->ref).'</span>';
2372 if ($selected >= 0) {
2373 $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
2374 }
2375 if (property_exists($this, 'thirdparty') && is_object($this->thirdparty)) {
2376 $return .= '<br><div class="info-box-ref tdoverflowmax150">'.$this->thirdparty->getNomUrl(1).'</div>';
2377 }
2378 if (property_exists($this, 'total_ht')) {
2379 $return .= '<div class="info-box-ref amount">'.price($this->total_ht, 0, $langs, 0, -1, -1, $conf->currency).' '.$langs->trans('HT').'</div>';
2380 }
2381 if (method_exists($this, 'getLibStatut')) {
2382 $return .= '<div class="info-box-status">'.$this->getLibStatut(3).'</div>';
2383 }
2384 $return .= '</div>';
2385 $return .= '</div>';
2386 $return .= '</div>';
2387
2388 return $return;
2389 }
2390
2398 public function initAsSpecimen()
2399 {
2400 global $langs;
2401
2402 $now = dol_now();
2403
2404 dol_syslog(get_class($this)."::initAsSpecimen");
2405
2406 $order = new Commande($this->db);
2407 $order->initAsSpecimen();
2408
2409 // Initialise parameters
2410 $this->id = 0;
2411 $this->ref = 'SPECIMEN';
2412 $this->specimen = 1;
2414 $this->livraison_id = 0;
2415 $this->date = $now;
2416 $this->date_creation = $now;
2417 $this->date_valid = $now;
2418 $this->date_delivery = $now + 24 * 3600;
2419 $this->date_expedition = $now + 24 * 3600;
2420
2421 $this->entrepot_id = 0;
2422 $this->fk_delivery_address = 0;
2423 $this->socid = 1;
2424
2425 $this->commande_id = 0;
2426 $this->commande = $order;
2427
2428 $this->origin_id = 1;
2429 $this->origin_type = 'commande';
2430
2431 $this->note_private = 'Private note';
2432 $this->note_public = 'Public note';
2433
2434 $nbp = min(1000, GETPOSTINT('nblines') ? GETPOSTINT('nblines') : 5); // We can force the nb of lines to test from command line (but not more than 1000)
2435 $xnbp = 0;
2436 while ($xnbp < $nbp) {
2437 $line = new ExpeditionLigne($this->db);
2438 $line->product_desc = $langs->trans("Description")." ".$xnbp;
2439 $line->product_label = $langs->trans("Description")." ".$xnbp;
2440 $line->qty = 10;
2441 $line->qty_asked = 5;
2442 $line->qty_shipped = 4;
2443 $line->fk_product = $this->commande->lines[$xnbp]->fk_product;
2444
2445 $line->weight = 1.123456;
2446 $line->weight_units = 0; // kg
2447
2448 $line->volume = 2.34567;
2449 $line->volume_unit = 0;
2450
2451 $this->lines[] = $line;
2452 $xnbp++;
2453 }
2454
2455 return 1;
2456 }
2457
2458 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2467 public function set_date_livraison($user, $delivery_date)
2468 {
2469 // phpcs:enable
2470 return $this->setDeliveryDate($user, $delivery_date);
2471 }
2472
2480 public function setDeliveryDate($user, $delivery_date)
2481 {
2482 if ($user->hasRight('expedition', 'creer')) {
2483 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition";
2484 $sql .= " SET date_delivery = ".($delivery_date ? "'".$this->db->idate($delivery_date)."'" : 'null');
2485 $sql .= " WHERE rowid = ".((int) $this->id);
2486
2487 dol_syslog(get_class($this)."::setDeliveryDate", LOG_DEBUG);
2488 $resql = $this->db->query($sql);
2489 if ($resql) {
2490 $this->date_delivery = $delivery_date;
2491 return 1;
2492 } else {
2493 $this->error = $this->db->error();
2494 return -1;
2495 }
2496 } else {
2497 return -2;
2498 }
2499 }
2500
2508 public function setShippingDate($user, $shipping_date)
2509 {
2510 if ($user->hasRight('expedition', 'creer')) {
2511 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition";
2512 $sql .= " SET date_expedition = ".($shipping_date ? "'".$this->db->idate($shipping_date)."'" : 'null');
2513 $sql .= " WHERE rowid = ".((int) $this->id);
2514
2515 dol_syslog(get_class($this)."::setShippingDate", LOG_DEBUG);
2516 $resql = $this->db->query($sql);
2517 if ($resql) {
2518 $this->date_shipping = $shipping_date;
2519 return 1;
2520 } else {
2521 $this->error = $this->db->error();
2522 return -1;
2523 }
2524 } else {
2525 return -2;
2526 }
2527 }
2528
2529 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2535 public function fetch_delivery_methods()
2536 {
2537 // phpcs:enable
2538 global $langs;
2539 $this->meths = array();
2540
2541 $sql = "SELECT em.rowid, em.code, em.libelle as label";
2542 $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2543 $sql .= " WHERE em.active = 1";
2544 $sql .= " ORDER BY em.libelle ASC";
2545
2546 $resql = $this->db->query($sql);
2547 if ($resql) {
2548 while ($obj = $this->db->fetch_object($resql)) {
2549 $label = $langs->trans('SendingMethod'.$obj->code);
2550 $this->meths[$obj->rowid] = ($label != 'SendingMethod'.$obj->code ? $label : $obj->label);
2551 }
2552 }
2553 }
2554
2555 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2562 public function list_delivery_methods($id = 0)
2563 {
2564 // phpcs:enable
2565 global $langs;
2566
2567 $this->listmeths = array();
2568 $i = 0;
2569
2570 $sql = "SELECT em.rowid, em.code, em.libelle as label, em.description, em.tracking, em.active";
2571 $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2572 if (!empty($id)) {
2573 $sql .= " WHERE em.rowid=".((int) $id);
2574 }
2575
2576 $resql = $this->db->query($sql);
2577 if ($resql) {
2578 while ($obj = $this->db->fetch_object($resql)) {
2579 $this->listmeths[$i]['rowid'] = $obj->rowid;
2580 $this->listmeths[$i]['code'] = $obj->code;
2581 $label = $langs->trans('SendingMethod'.$obj->code);
2582 $this->listmeths[$i]['libelle'] = ($label != 'SendingMethod'.$obj->code ? $label : $obj->label);
2583 $this->listmeths[$i]['description'] = $obj->description;
2584 $this->listmeths[$i]['tracking'] = $obj->tracking;
2585 $this->listmeths[$i]['active'] = $obj->active;
2586 $i++;
2587 }
2588 }
2589 }
2590
2597 public function getUrlTrackingStatus($value = '')
2598 {
2599 if (!empty($this->shipping_method_id)) {
2600 $sql = "SELECT em.code, em.tracking";
2601 $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2602 $sql .= " WHERE em.rowid = ".((int) $this->shipping_method_id);
2603
2604 $resql = $this->db->query($sql);
2605 if ($resql) {
2606 if ($obj = $this->db->fetch_object($resql)) {
2607 $tracking = $obj->tracking;
2608 }
2609 }
2610 }
2611
2612 if (!empty($tracking) && !empty($value)) {
2613 $url = str_replace('{TRACKID}', $value, $tracking);
2614 $this->tracking_url = sprintf('<a target="_blank" rel="noopener noreferrer" href="%s">%s</a>', $url, ($value ? $value : 'url'));
2615 } else {
2616 $this->tracking_url = $value;
2617 }
2618 }
2619
2625 public function setClosed()
2626 {
2627 global $user;
2628
2629 $error = 0;
2630
2631 // Protection. This avoid to move stock later when we should not
2632 if ($this->status == self::STATUS_CLOSED) {
2633 return 0;
2634 }
2635
2636 $this->db->begin();
2637
2638 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET fk_statut = ".self::STATUS_CLOSED;
2639 if (empty($this->date_shipping)) { // Date of real shipment was not yet set, we force it on closing
2640 $sql .= ", date_expedition = '".$this->db->escape($this->db->idate(dol_now()))."'";
2641 }
2642 $sql .= " WHERE rowid = ".((int) $this->id)." AND fk_statut > 0";
2643
2644 $resql = $this->db->query($sql);
2645 if ($resql) {
2646 // Set order billed if 100% of order is shipped (qty in shipment lines match qty in order lines)
2647 if ($this->origin == 'commande' && $this->origin_id > 0) {
2648 $order = new Commande($this->db);
2649 $order->fetch($this->origin_id);
2650
2651 $order->loadExpeditions(self::STATUS_CLOSED); // Fill $order->expeditions = array(orderlineid => qty)
2652
2653 $shipments_match_order = 1;
2654 foreach ($order->lines as $line) {
2655 $lineid = $line->id;
2656 $qty = $line->qty;
2657 if (($line->product_type == 0 || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) && $order->expeditions[$lineid] != $qty) {
2658 $shipments_match_order = 0;
2659 $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';
2660 dol_syslog($text);
2661 break;
2662 }
2663 }
2664 if ($shipments_match_order) {
2665 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');
2666 // We close the order
2667 $order->cloture($user); // Note this may also create an invoice if module workflow ask it
2668 }
2669 }
2670
2671 $this->statut = self::STATUS_CLOSED; // Will be revert to STATUS_VALIDATED at end if there is a rollback
2672 $this->status = self::STATUS_CLOSED; // Will be revert to STATUS_VALIDATED at end if there is a rollback
2673
2674 // If stock increment is done on closing
2675 if (!$error && isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
2676 $result = $this->manageStockMvtOnEvt($user);
2677 if ($result < 0) {
2678 $error++;
2679 }
2680 }
2681
2682 // Call trigger
2683 if (!$error) {
2684 $result = $this->call_trigger('SHIPPING_CLOSED', $user);
2685 if ($result < 0) {
2686 $error++;
2687 }
2688 }
2689 } else {
2690 dol_print_error($this->db);
2691 $error++;
2692 }
2693
2694 if (!$error) {
2695 $this->db->commit();
2696 return 1;
2697 } else {
2698 $this->statut = self::STATUS_VALIDATED;
2700
2701 $this->db->rollback();
2702 return -1;
2703 }
2704 }
2705
2714 private function manageStockMvtOnEvt($user, $labelmovement = 'ShipmentClassifyClosedInDolibarr')
2715 {
2716 global $langs;
2717
2718 $error = 0;
2719
2720 require_once DOL_DOCUMENT_ROOT . '/product/stock/class/mouvementstock.class.php';
2721
2722 $langs->load("agenda");
2723
2724 // Loop on each product line to add a stock movement
2725 $sql = "SELECT";
2726 $sql .= " ed.rowid as edid, ed.fk_product, ed.qty, ed.fk_entrepot";
2727 $sql .= ", cd.rowid as cdid";
2728 $sql .= ", cd.subprice";
2729 $sql .= ", edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock";
2730 $sql .= ", e.ref";
2731 $sql .= " FROM " . $this->db->prefix() . "expeditiondet as ed";
2732 $sql .= " LEFT JOIN " . $this->db->prefix() . "commandedet as cd ON cd.rowid = ed.fk_elementdet";
2733 $sql .= " LEFT JOIN " . $this->db->prefix() . "expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid";
2734 $sql .= " INNER JOIN " . $this->db->prefix() . "expedition as e ON ed.fk_expedition = e.rowid";
2735 $sql .= " WHERE ed.fk_expedition = " . ((int) $this->id);
2736 //$sql .= " AND cd.rowid = ed.fk_elementdet";
2737
2738 dol_syslog(get_class($this) . "::valid select details", LOG_DEBUG);
2739 $resql = $this->db->query($sql);
2740 if ($resql) {
2741 $cpt = $this->db->num_rows($resql);
2742 for ($i = 0; $i < $cpt; $i++) {
2743 $obj = $this->db->fetch_object($resql);
2744 if (empty($obj->edbrowid)) {
2745 $qty = $obj->qty;
2746 } else {
2747 $qty = $obj->edbqty;
2748 }
2749 if ($qty <= 0 || ($qty < 0 && !getDolGlobalInt('SHIPMENT_ALLOW_NEGATIVE_QTY'))) {
2750 continue;
2751 }
2752 dol_syslog(get_class($this) . "::valid movement index " . $i . " ed.rowid=" . $obj->edid . " edb.rowid=" . $obj->edbrowid);
2753
2754 $mouvS = new MouvementStock($this->db);
2755 $mouvS->origin = &$this;
2756 $mouvS->setOrigin($this->element, $this->id, $obj->cdid, $obj->edid);
2757
2758 if (empty($obj->edbrowid)) {
2759 // line without batch detail
2760
2761 // 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
2762 $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, $qty, $obj->subprice, $langs->trans($labelmovement, $obj->ref));
2763 if ($result < 0) {
2764 $this->error = $mouvS->error;
2765 $this->errors = $mouvS->errors;
2766 $error++;
2767 break;
2768 }
2769 } else {
2770 // line with batch detail
2771
2772 // 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
2773 $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);
2774 if ($result < 0) {
2775 $this->error = $mouvS->error;
2776 $this->errors = $mouvS->errors;
2777 $error++;
2778 break;
2779 }
2780 }
2781
2782 // 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
2783 // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot.
2784 $sqldelete = "DELETE FROM ".$this->db->prefix()."product_stock WHERE reel = 0 AND rowid NOT IN (SELECT fk_product_stock FROM ".$this->db->prefix()."product_batch as pb)";
2785 $resqldelete = $this->db->query($sqldelete);
2786 // We do not test error, it can fails if there is child in batch details
2787 }
2788 } else {
2789 $this->error = $this->db->lasterror();
2790 $this->errors[] = $this->db->lasterror();
2791 $error++;
2792 }
2793
2794 if (!$error) {
2795 return 1;
2796 } else {
2797 return -1;
2798 }
2799 }
2800
2806 public function setBilled()
2807 {
2808 global $user;
2809 $error = 0;
2810
2811 $this->db->begin();
2812
2813 $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET billed = 1';
2814 $sql .= " WHERE rowid = ".((int) $this->id).' AND fk_statut > 0';
2815
2816 $resql = $this->db->query($sql);
2817 if ($resql) {
2818 $this->billed = 1;
2819
2820 // Call trigger
2821 $result = $this->call_trigger('SHIPPING_BILLED', $user);
2822 if ($result < 0) {
2823 $this->billed = 0;
2824 $error++;
2825 }
2826 } else {
2827 $error++;
2828 $this->errors[] = $this->db->lasterror;
2829 }
2830
2831 if (empty($error)) {
2832 $this->db->commit();
2833 return 1;
2834 } else {
2835 $this->db->rollback();
2836 return -1;
2837 }
2838 }
2839
2847 public function setDraft($user, $notrigger = 0)
2848 {
2849 // Protection
2850 if ($this->status <= self::STATUS_DRAFT) {
2851 return 0;
2852 }
2853
2854 return $this->setStatusCommon($user, self::STATUS_DRAFT, $notrigger, 'SHIPMENT_UNVALIDATE');
2855 }
2856
2862 public function reOpen()
2863 {
2864 global $langs, $user;
2865
2866 $error = 0;
2867
2868 // Protection. This avoid to move stock later when we should not
2869 if ($this->status == self::STATUS_VALIDATED) {
2870 return 0;
2871 }
2872
2873 $this->db->begin();
2874
2875 $oldbilled = $this->billed;
2876
2877 $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET fk_statut = 1';
2878 $sql .= " WHERE rowid = ".((int) $this->id).' AND fk_statut > 0';
2879
2880 $resql = $this->db->query($sql);
2881 if ($resql) {
2882 $this->statut = self::STATUS_VALIDATED;
2884 $this->billed = 0;
2885
2886 // If stock increment is done on closing
2887 if (!$error && isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
2888 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
2889
2890 $langs->load("agenda");
2891
2892 // Loop on each product line to add a stock movement
2893 // TODO possibilite d'expedier a partir d'une propale ou autre origine
2894 $sql = "SELECT cd.fk_product, cd.subprice,";
2895 $sql .= " ed.rowid, ed.qty, ed.fk_entrepot,";
2896 $sql .= " edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock";
2897 $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,";
2898 $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed";
2899 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid";
2900 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
2901 $sql .= " AND cd.rowid = ed.fk_elementdet";
2902
2903 dol_syslog(get_class($this)."::valid select details", LOG_DEBUG);
2904 $resql = $this->db->query($sql);
2905 if ($resql) {
2906 $cpt = $this->db->num_rows($resql);
2907 for ($i = 0; $i < $cpt; $i++) {
2908 $obj = $this->db->fetch_object($resql);
2909 if (empty($obj->edbrowid)) {
2910 $qty = $obj->qty;
2911 } else {
2912 $qty = $obj->edbqty;
2913 }
2914 if ($qty <= 0) {
2915 continue;
2916 }
2917 dol_syslog(get_class($this)."::reopen expedition movement index ".$i." ed.rowid=".$obj->rowid." edb.rowid=".$obj->edbrowid);
2918
2919 //var_dump($this->lines[$i]);
2920 $mouvS = new MouvementStock($this->db);
2921 $mouvS->origin = &$this;
2922 $mouvS->setOrigin($this->element, $this->id);
2923
2924 if (empty($obj->edbrowid)) {
2925 // line without batch detail
2926
2927 // 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
2928 $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, -$qty, $obj->subprice, $langs->trans("ShipmentUnClassifyCloseddInDolibarr", $this->ref));
2929 if ($result < 0) {
2930 $this->error = $mouvS->error;
2931 $this->errors = $mouvS->errors;
2932 $error++;
2933 break;
2934 }
2935 } else {
2936 // line with batch detail
2937
2938 // 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
2939 $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);
2940 if ($result < 0) {
2941 $this->error = $mouvS->error;
2942 $this->errors = $mouvS->errors;
2943 $error++;
2944 break;
2945 }
2946 }
2947 }
2948 } else {
2949 $this->error = $this->db->lasterror();
2950 $error++;
2951 }
2952 }
2953
2954 if (!$error) {
2955 // Call trigger
2956 $result = $this->call_trigger('SHIPPING_REOPEN', $user);
2957 if ($result < 0) {
2958 $error++;
2959 }
2960 }
2961 } else {
2962 $error++;
2963 $this->errors[] = $this->db->lasterror();
2964 }
2965
2966 if (!$error) {
2967 $this->db->commit();
2968 return 1;
2969 } else {
2970 $this->statut = self::STATUS_CLOSED;
2971 $this->status = self::STATUS_CLOSED;
2972 $this->billed = $oldbilled;
2973 $this->db->rollback();
2974 return -1;
2975 }
2976 }
2977
2989 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null)
2990 {
2991 $outputlangs->load("products");
2992
2993 if (!dol_strlen($modele)) {
2994 $modele = 'rouget';
2995
2996 if (!empty($this->model_pdf)) {
2997 $modele = $this->model_pdf;
2998 } elseif (getDolGlobalString('EXPEDITION_ADDON_PDF')) {
2999 $modele = getDolGlobalString('EXPEDITION_ADDON_PDF');
3000 }
3001 }
3002
3003 $modelpath = "core/modules/expedition/doc/";
3004
3005 $this->fetch_origin();
3006
3007 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
3008 }
3009
3018 public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
3019 {
3020 $tables = array(
3021 'expedition'
3022 );
3023
3024 return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
3025 }
3026}
$object ref
Definition info.php:90
Class to manage customers orders.
const STATUS_SHIPMENTONPROCESS
Shipment on process.
const STATUS_VALIDATED
Validated status.
Parent class of all other business classes (invoices, contracts, proposals, orders,...
fetch_optionals($rowid=null, $optionsArray=null)
Function to get extra fields of an object into $this->array_options This method is in most cases call...
deleteEcmFiles($mode=0)
Delete related files of object in database.
add_object_linked($origin=null, $origin_id=null, $f_user=null, $notrigger=0)
Add an object link into llx_element_element.
commonGenerateDocument($modelspath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams=null)
Common function for all objects extending CommonObject for generating documents.
fetch_thirdparty($force_thirdparty_id=0)
Load the third party of object, from id $this->socid or $this->fk_soc, into this->thirdparty.
deleteObjectLinked($sourceid=null, $sourcetype='', $targetid=null, $targettype='', $rowid=0, $f_user=null, $notrigger=0)
Delete all links between an object $this.
setErrorsFromObject($object)
setErrorsFromObject
setStatut($status, $elementId=null, $elementType='', $trigkey='', $fieldstatus='fk_statut')
Set status of an object.
deleteExtraFields()
Delete all extra fields values for the current object.
setStatusCommon($user, $status, $notrigger=0, $triggercode='')
Set to a status.
fetchObjectLinked($sourceid=null, $sourcetype='', $targetid=null, $targettype='', $clause='OR', $alsosametype=1, $orderby='sourcetype', $loadalsoobjects=1)
Fetch array of objects linked to current object (object of enabled modules only).
errorsToString()
Method to output saved errors.
static commonReplaceThirdparty(DoliDB $dbs, $origin_id, $dest_id, array $tables, $ignoreerrors=0)
Function used to replace a thirdparty id with another one.
fetch_origin()
Read linked origin object.
insertExtraFields($trigger='', $userused=null)
Add/Update all extra fields values for the current object.
delete_linked_contact($source='', $code='')
Delete all links between an object $this and all its contacts in llx_element_contact.
call_trigger($triggerName, $user)
Call trigger based on this instance.
add_contact($fk_socpeople, $type_contact, $source='external', $notrigger=0)
Add a link between element $this->element and a contact.
Class to manage receptions.
Class to manage Dolibarr database access.
getNomUrl($withpicto=0, $option='', $max=0, $short=0, $notooltip=0, $save_lastsearch_value=-1)
Return clickable link of object (with eventually picto)
addline_batch($dbatch, $array_options=[], $origin_line=null)
Add a shipment line with batch record.
create_delivery($user)
Create a delivery receipt from a shipment.
setDraft($user, $notrigger=0)
Set draft status.
const STATUS_SHIPMENT_IN_PROGRESS
Expedition in progress -> package exit the warehouse and is now in the truck or into the hand of the ...
getUrlTrackingStatus($value='')
Forge an set tracking url.
setClosed()
Classify the shipping as closed (this records also the stock movement)
__construct($db)
Constructor.
create_line($entrepot_id, $origin_line_id, $qty, $rang=0, $array_options=[], $parent_line_id=0, $product_id=0)
Create a expedition line.
fetch_lines()
Load lines.
addline($entrepot_id, $id, $qty, $array_options=[], $fk_product=0, $fk_parent=0)
Add an expedition line.
create($user, $notrigger=0)
Create expedition en base.
LibStatut($status, $mode)
Return label of a status.
setBilled()
Classify the shipping as invoiced (used for example by trigger when WORKFLOW_SHIPPING_CLASSIFY_BILLED...
generateDocument($modele, $outputlangs, $hidedetails=0, $hidedesc=0, $hideref=0, $moreparams=null)
Create a document onto disk according to template module.
getTooltipContentArray($params)
getTooltipContentArray
setDeliveryDate($user, $delivery_date)
Set the planned delivery date.
getKanbanView($option='', $arraydata=null)
Return clickable link of object (with eventually picto)
setShippingDate($user, $shipping_date)
Set the shipping date.
const STATUS_DRAFT
Draft status.
const STATUS_CANCELED
Canceled status.
getLibStatut($mode=0)
Return status label.
set_date_livraison($user, $delivery_date)
Set delivery date.
initAsSpecimen()
Initialise an instance with random values.
getNextNumRef($soc)
Return next expedition ref.
const STATUS_CLOSED
Closed status -> parcel was received by customer / end of process prev status : validated or shipment...
const STATUS_VALIDATED
Validated status -> parcel is ready to be sent prev status : draft next status : closed or shipment_i...
create_line_batch($line_ext, $array_options=[])
Create the detail of the expedition line.
manageStockMvtOnEvt($user, $labelmovement='ShipmentClassifyClosedInDolibarr')
Manage Stock MVt onb Close or valid Shipment.
valid($user, $notrigger=0)
Validate object and update stock if option enabled.
update($user=null, $notrigger=0)
Update database.
cancel($notrigger=0, $also_update_stock=false)
Cancel shipment.
fetch_delivery_methods()
Fetch deliveries method and return an array.
fetch($id, $ref='', $ref_ext='', $notused='')
Get object and lines from database.
reOpen()
Classify the shipping as validated/opened.
deleteLine($user, $lineid)
Delete detail line.
static replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
Function used to replace a thirdparty id with another one.
list_delivery_methods($id=0)
Fetch all deliveries method and return an array.
Class to manage lines of shipment.
CRUD class for batch number management within shipment.
Class to manage stock movements.
Class to manage order lines.
Class to manage products or services.
const TYPE_SERVICE
Service.
Manage record for batch number management.
Class to manage third parties objects (customers, suppliers, prospects...)
print $langs trans("Ref").' m titre as m m statut as status
Or an array listing all the potential status of the object: array: int of the status => translated la...
Definition index.php:171
getLibSignedStatus(int $mode=0)
Returns the label for signed status.
dol_delete_file($file, $disableglob=0, $nophperrors=0, $nohook=0, $object=null, $allowdotdot=false, $indexdatabase=1, $nolog=0)
Remove a file or several files with a mask.
dol_delete_dir_recursive($dir, $count=0, $nophperrors=0, $onlysub=0, &$countdeleted=0, $indexdatabase=1, $nolog=0, $level=0)
Remove a directory $dir and its subdirectories (or only files and subdirectories)
dol_dir_list($utf8_path, $types="all", $recursive=0, $filter="", $excludefilter=null, $sortcriteria="name", $sortorder=SORT_ASC, $mode=0, $nohook=0, $relativename="", $donotfollowsymlinks=0, $nbsecondsold=0)
Scan a directory and return a list of files/directories.
Definition files.lib.php:63
setEntity($currentobject)
Set entity id to use when to create an object.
img_picto($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $alt='', $morecss='', $marginleftonlyshort=2, $allowothertags=array())
Show picto whatever it's its name (generic function)
GETPOSTINT($paramname, $method=0)
Return the value of a $_GET or $_POST supervariable, converted into integer.
img_object($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $allowothertags=array())
Show a picto called object_picto (generic function)
dol_strlen($string, $stringencoding='UTF-8')
Make a strlen call.
dol_now($mode='auto')
Return date for now.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dolGetStatus($statusLabel='', $statusLabelShort='', $html='', $statusType='status0', $displayMode=0, $url='', $params=array())
Output the badge of a status.
dol_buildpath($path, $type=0, $returnemptyifnotfound=0)
Return path of url or filesystem.
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
dol_sanitizeFileName($str, $newstr='_', $unaccent=1, $includequotes=0)
Clean a string to use it as a file name.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
get_localtax($vatrate, $local, $thirdparty_buyer=null, $thirdparty_seller=null, $vatnpr=0)
Return localtax rate for a particular vat, when selling a product with vat $vatrate,...
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
global $conf
The following vars must be defined: $type2label $form $conf, $lang, The following vars may also be de...
Definition member.php:79
calcul_price_total($qty, $pu, $remise_percent_ligne, $txtva, $uselocaltax1_rate, $uselocaltax2_rate, $remise_percent_global, $price_base_type, $info_bits, $type, $seller=null, $localtaxes_array=[], $progress=100, $multicurrency_tx=1, $pu_devise=0, $multicurrency_code='')
Calculate totals (net, vat, ...) of a line.
Definition price.lib.php:90
$conf db user
Active Directory does not allow anonymous connections.
Definition repair.php:162