dolibarr 25.0.0-alpha
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-2026 Frédéric France <frederic.france@free.fr>
15 * Copyright (C) 2020 Lenin Rivas <lenin@leninrivas.com>
16 * Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
17 * Copyright (C) 2024 William Mead <william.mead@manchenumerique.fr>
18 * Copyright (C) 2025 Nick Fragoulis
19 *
20 * This program is free software; you can redistribute it and/or modify
21 * it under the terms of the GNU General Public License as published by
22 * the Free Software Foundation; either version 3 of the License, or
23 * (at your option) any later version.
24 *
25 * This program is distributed in the hope that it will be useful,
26 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28 * GNU General Public License for more details.
29 *
30 * You should have received a copy of the GNU General Public License
31 * along with this program. If not, see <https://www.gnu.org/licenses/>.
32 */
33
40require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
41require_once DOL_DOCUMENT_ROOT."/expedition/class/expeditionligne.class.php";
42require_once DOL_DOCUMENT_ROOT.'/core/class/commonincoterm.class.php';
43if (isModEnabled("propal")) {
44 require_once DOL_DOCUMENT_ROOT.'/comm/propal/class/propal.class.php';
45}
46if (isModEnabled('order')) {
47 require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
48}
49require_once DOL_DOCUMENT_ROOT.'/expedition/class/expeditionlinebatch.class.php';
50require_once DOL_DOCUMENT_ROOT.'/core/class/commonsignedobject.class.php';
51require_once DOL_DOCUMENT_ROOT.'/subtotals/class/commonsubtotal.class.php';
52
59{
60 use CommonIncoterm;
61 use CommonSignedObject;
62 use CommonSubtotal;
63
67 public $element = "shipping";
68
72 public $fk_element = "fk_expedition";
73
77 public $table_element = "expedition";
78
82 public $table_element_line = "expeditiondet";
83
87 public $class_element_line = 'ExpeditionLigne';
88
92 public $picto = 'dolly';
93
94
98 public $fields = array();
99
104 public $user_author_id;
105
110 public $fk_user_author;
111
116 public $fk_user_valid;
117
121 public $socid;
122
128 public $ref_client;
129
133 public $ref_customer;
134
138 public $entrepot_id;
139
143 public $tracking_number;
144
148 public $tracking_url;
152 public $billed = 0;
153
157 public $trueWeight;
161 public $weight_units;
165 public $trueWidth;
169 public $width_units;
173 public $trueHeight;
177 public $height_units;
181 public $trueDepth;
185 public $depth_units;
189 public $trueSize;
190
194 public $livraison_id;
195
199 public $multicurrency_subprice;
200
204 public $size_units;
205
209 public $sizeH;
210
214 public $sizeS;
215
219 public $sizeW;
220
224 public $weight;
225
229 public $date_delivery;
230
236 public $date;
237
243 public $date_expedition;
244
249 public $date_shipping;
250
254 public $date_valid;
255
259 public $meths;
263 public $listmeths; // List of carriers
264
268 public $commande_id;
269
273 public $commande;
274
278 public $lines = array();
279
280 // Multicurrency
284 public $fk_multicurrency;
285
289 public $multicurrency_code;
293 public $multicurrency_tx;
297 public $multicurrency_total_ht;
301 public $multicurrency_total_tva;
305 public $multicurrency_total_ttc;
306
310 const STATUS_DRAFT = 0;
311
319
325 const STATUS_CLOSED = 2;
326
330 const STATUS_CANCELED = -1;
331
340
346 public function __construct($db)
347 {
348 $this->db = $db;
349
350 $this->ismultientitymanaged = 1;
351 $this->isextrafieldmanaged = 1;
352
353 // List of long language codes for status
354 $this->labelStatus = array();
355 $this->labelStatus[-1] = 'StatusSendingCanceled';
356 $this->labelStatus[0] = 'StatusSendingDraft';
357 $this->labelStatus[1] = 'StatusSendingValidated';
358 $this->labelStatus[2] = 'StatusSendingProcessed';
359
360 // List of short language codes for status
361 $this->labelStatusShort = array();
362 $this->labelStatusShort[-1] = 'StatusSendingCanceledShort';
363 $this->labelStatusShort[0] = 'StatusSendingDraftShort';
364 $this->labelStatusShort[1] = 'StatusSendingValidatedShort';
365 $this->labelStatusShort[2] = 'StatusSendingProcessedShort';
366 }
367
374 public function getNextNumRef($soc)
375 {
376 global $langs, $conf;
377 $langs->load("sendings");
378
379 if (getDolGlobalString('EXPEDITION_ADDON_NUMBER')) {
380 $mybool = false;
381
382 $file = getDolGlobalString('EXPEDITION_ADDON_NUMBER') . ".php";
383 $classname = getDolGlobalString('EXPEDITION_ADDON_NUMBER');
384
385 // Include file with class
386 $dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']);
387
388 foreach ($dirmodels as $reldir) {
389 $dir = dol_buildpath($reldir."core/modules/expedition/");
390
391 // Load file with numbering class (if found)
392 $mybool = ((bool) @include_once $dir.$file) || $mybool;
393 }
394
395 if (!$mybool) {
396 dol_print_error(null, "Failed to include file ".$file);
397 return '';
398 }
399
400 $obj = new $classname();
401 '@phan-var-force ModelNumRefExpedition $obj';
403 $numref = $obj->getNextValue($soc, $this);
404
405 if ($numref != "") {
406 return $numref;
407 } else {
408 dol_print_error($this->db, get_class($this)."::getNextNumRef ".$obj->error);
409 return "";
410 }
411 } else {
412 print $langs->trans("Error")." ".$langs->trans("Error_EXPEDITION_ADDON_NUMBER_NotDefined");
413 return "";
414 }
415 }
416
424 public function create($user, $notrigger = 0)
425 {
426 $now = dol_now();
427
428 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
429 $error = 0;
430
431 // Clean parameters
432 $this->tracking_number = dol_sanitizeFileName((string) $this->tracking_number);
433 $this->tracking_number = preg_replace('/\s+/', '', $this->tracking_number);
434
435 if (empty($this->fk_project)) {
436 $this->fk_project = 0;
437 }
438 if (empty($this->date_creation)) {
439 $this->date_creation = $now;
440 }
441 if (empty($this->date_shipping) && !empty($this->date_expedition)) {
442 $this->date_shipping = $this->date_expedition;
443 }
444 $this->entity = setEntity($this);
445
446 $this->user = $user;
447
448 $this->db->begin();
449
450 $sql = "INSERT INTO ".MAIN_DB_PREFIX."expedition (";
451 $sql .= "ref";
452 $sql .= ", entity";
453 $sql .= ", ref_customer";
454 $sql .= ", ref_ext";
455 $sql .= ", date_creation";
456 $sql .= ", fk_user_author";
457 $sql .= ", date_expedition";
458 $sql .= ", date_delivery";
459 $sql .= ", fk_soc";
460 $sql .= ", fk_projet";
461 $sql .= ", fk_address";
462 $sql .= ", fk_shipping_method";
463 $sql .= ", tracking_number";
464 $sql .= ", weight";
465 $sql .= ", size";
466 $sql .= ", width";
467 $sql .= ", height";
468 $sql .= ", weight_units";
469 $sql .= ", size_units";
470 $sql .= ", note_private";
471 $sql .= ", note_public";
472 $sql .= ", model_pdf";
473 $sql .= ", fk_incoterms, location_incoterms";
474 $sql .= ", signed_status";
475 $sql .= ", billed";
476 $sql .= ") VALUES (";
477 $sql .= "'(PROV)'";
478 $sql .= ", ".((int) $this->entity);
479 $sql .= ", ".($this->ref_customer ? "'".$this->db->escape($this->ref_customer)."'" : "null");
480 $sql .= ", ".($this->ref_ext ? "'".$this->db->escape($this->ref_ext)."'" : "null");
481 $sql .= ", '".$this->db->idate($this->date_creation)."'";
482 $sql .= ", ".((int) $user->id);
483 $sql .= ", ".($this->date_shipping > 0 ? "'".$this->db->idate($this->date_shipping)."'" : "null");
484 $sql .= ", ".($this->date_delivery > 0 ? "'".$this->db->idate($this->date_delivery)."'" : "null");
485 $sql .= ", ".($this->socid > 0 ? ((int) $this->socid) : "null");
486 $sql .= ", ".($this->fk_project > 0 ? ((int) $this->fk_project) : "null");
487 $sql .= ", ".($this->fk_delivery_address > 0 ? ((int) $this->fk_delivery_address) : "null");
488 $sql .= ", ".($this->shipping_method_id > 0 ? ((int) $this->shipping_method_id) : "null");
489 $sql .= ", '".$this->db->escape($this->tracking_number)."'";
490 $sql .= ", ".(is_numeric($this->weight) ? (float) $this->weight : 'NULL');
491 $sql .= ", ".(is_numeric($this->sizeS) ? (float) $this->sizeS : 'NULL'); // TODO Should use this->trueDepth
492 $sql .= ", ".(is_numeric($this->sizeW) ? (float) $this->sizeW : 'NULL'); // TODO Should use this->trueWidth
493 $sql .= ", ".(is_numeric($this->sizeH) ? (float) $this->sizeH : 'NULL'); // TODO Should use this->trueHeight
494 $sql .= ", ".($this->weight_units != '' ? (int) $this->weight_units : 'NULL');
495 $sql .= ", ".($this->size_units != '' ? (int) $this->size_units : 'NULL');
496 $sql .= ", ".(!empty($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : "null");
497 $sql .= ", ".(!empty($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : "null");
498 $sql .= ", ".(!empty($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null");
499 $sql .= ", ".((int) $this->fk_incoterms);
500 $sql .= ", '".$this->db->escape($this->location_incoterms)."'";
501 $sql .= ", ".((int) $this->signed_status);
502 $sql .= ", ".((int) $this->billed);
503 $sql .= ")";
504
505 dol_syslog(get_class($this)."::create", LOG_DEBUG);
506 $resql = $this->db->query($sql);
507 if ($resql) {
508 $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX."expedition");
509
510 // update ref
511 $initialref = '(PROV'.$this->id.')';
512 if (!empty($this->ref)) {
513 $initialref = $this->ref;
514 }
515
516 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition";
517 $sql .= " SET ref = '".$this->db->escape($initialref)."'";
518 $sql .= " WHERE rowid = ".((int) $this->id);
519
520 dol_syslog(get_class($this)."::create", LOG_DEBUG);
521 if ($this->db->query($sql)) {
522 $this->ref = $initialref;
523 // Insert of lines
524 $num = count($this->lines);
525 $kits_list = array();
526 if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) {
527 for ($i = 0; $i < $num; $i++) {
528 $objectsrc = new OrderLine($this->db);
529 $objectsrc->fetch($this->lines[$i]->origin_line_id);
530 if ($this->lines[$i]->product_type == "9" && $objectsrc->special_code == SUBTOTALS_SPECIAL_CODE) {
531 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) {
532 $error++;
533 }
534 continue;
535 }
536 if (empty($this->lines[$i]->product_type) || getDolGlobalString('STOCK_SUPPORTS_SERVICES') || getDolGlobalString('SHIPMENT_SUPPORTS_SERVICES')) {
537 // virtual products
538 $line = $this->lines[$i];
539 if ($line->fk_product > 0) {
540 // We group kit lines by the fulfilled order line (fk_elementdet) and not by product id: when a same kit product
541 // is present on several distinct order lines, each order line must keep its own shipment line so the ordered
542 // qty stays consistent with the shipped qty (grouping by product would sum the shipped qty of all lines while
543 // keeping the ordered qty of a single line, resulting in a wrong/negative remaining qty to ship). Several
544 // warehouse pickings of the same order line still share the same fk_elementdet and are grouped together.
545 if (!isset($kits_list[$line->fk_elementdet])) {
546 if (!is_object($line->product)) {
547 $line_product = new Product($this->db);
548 $result = $line_product->fetch($line->fk_product, '', '', '', 1, 1, 1);
549 if ($result <= 0) {
550 $error++;
551 }
552 } else {
553 $line_product = $line->product;
554 }
555
556 // get all children of virtual product
557 $line_product->get_sousproduits_arbo();
558 $prods_arbo = $line_product->get_arbo_each_prod($line->qty);
559 if (count($prods_arbo) > 0) {
560 $kits_list[$line->fk_elementdet] = array(
561 'arbo' => $prods_arbo,
562 'total_qty' => $line->qty,
563 );
564 }
565 } else {
566 $kits_list[$line->fk_elementdet]['total_qty'] += $line->qty;
567 }
568 }
569 }
570 }
571 }
572 $kits_id_cached = array();
573 $sub_kits_id_cached = array();
574 $sub_parents_cached = array();
575 for ($i = 0; $i < $num; $i++) {
576 $line = $this->lines[$i];
577 if (empty($line->product_type) || getDolGlobalString('STOCK_SUPPORTS_SERVICES') || getDolGlobalString('SHIPMENT_SUPPORTS_SERVICES')) {
578 $line_id = 0;
579 if (!isset($kits_id_cached[$line->fk_elementdet])) {
580 if (!isset($line->detail_batch) || (isset($kits_list[$line->fk_elementdet]) && !getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE'))) { // no batch management or is kit
581 $qty = isset($kits_list[$line->fk_elementdet]) ? $kits_list[$line->fk_elementdet]['total_qty'] : $line->qty;
582 $warehouse_id = (isset($kits_list[$line->fk_elementdet]) && !getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE')) ? 0 : $line->entrepot_id;
583 $line_id = $this->create_line($warehouse_id, $line->origin_line_id, $qty, $line->rang, $line->array_options, 0, $line->fk_product);
584 if ($line_id <= 0) {
585 $error++;
586 }
587 if (isset($kits_list[$line->fk_elementdet])) {
588 $kits_id_cached[$line->fk_elementdet] = $line_id;
589 }
590 } else { // with batch management
591 if ($this->create_line_batch($line, $line->array_options) <= 0) {
592 $error++;
593 }
594 }
595 } else {
596 $line_id = $kits_id_cached[$line->fk_elementdet];
597 }
598
599 // virtual products
600 if (isset($kits_list[$line->fk_elementdet]) && !getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE')) {
601 $prods_arbo = $kits_list[$line->fk_elementdet]['arbo'];
602
603 // get all children of virtual product
604 $parent_line_id = $line_id; // parent line created
605 $level_last = 1;
606 $product_child_id = 0;
607 foreach ($prods_arbo as $index => $product_child_arr) {
608 // 'id' => Id product
609 // 'id_parent' => Id parent product
610 // 'ref' => Ref product
611 // 'nb' => Nb of units that compose parent product
612 // 'nb_total' => // Nb of units for all nb of product
613 // 'stock' => Stock
614 // 'stock_alert' => Stock alert
615 // 'label' => Label
616 // 'fullpath' => // Full path label
617 // 'type' =>
618 // 'desiredstock' => Desired stock
619 // 'level' => Level
620 // 'incdec' => Need to be incremented or decremented
621 // 'entity' => Entity
622 $product_child_level = (int) $product_child_arr['level'];
623 $product_child_incdec = !empty($product_child_arr['incdec']);
624
625 // detect new level
626 if ($product_child_level != $level_last) {
627 $parent_line_id = $line_id; // last line id
628 $parent_product_id = $product_child_id; // last line id
629 if (isset($sub_parents_cached[$line->fk_elementdet][$parent_product_id])) {
630 $parent_line_id = $sub_parents_cached[$line->fk_elementdet][$parent_product_id];
631 } else {
632 $sub_parents_cached[$line->fk_elementdet][$parent_product_id] = $parent_line_id;
633 }
634 }
635
636 // determine if it's a kit : check next level
637 $is_kit = false;
638 $next_level = $product_child_level;
639 $next_index = $index + 1;
640 if (isset($prods_arbo[$next_index])) {
641 $next_level = (int) $prods_arbo[$next_index]['level'];
642 }
643 if ($next_level > $product_child_level) {
644 $is_kit = true;
645 }
646
647 // determine quantity of sub-product
648 $product_child_id = (int) $product_child_arr['id'];
649 $product_child_qty = (float) $product_child_arr['nb_total']; // by default
650 $warehouse_id = $line->entrepot_id; // by default
651 if ($is_kit || !$product_child_incdec) {
652 if (!$product_child_incdec) {
653 $product_child_qty = 0;
654 }
655 $warehouse_id = 0; // no warehouse used for a kit or if stock is not managed (empty incdec)
656 }
657
658 // create line for a child of virtual product
659 if (!isset($sub_kits_id_cached[$line->fk_elementdet][$product_child_id]) || $warehouse_id > 0) {
660 $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);
661 if ($line_id <= 0) {
662 $error++;
663 dol_syslog(__METHOD__ . ' : ' . $this->errorsToString(), LOG_ERR);
664 break;
665 }
666
667 // if kit or not manage stock (empty incdec)
668 if (empty($warehouse_id)) {
669 $sub_kits_id_cached[$line->fk_elementdet][$product_child_id] = $line_id;
670 }
671 }
672
673 $level_last = $product_child_level;
674 }
675 }
676 }
677 }
678
679 if (!$error && $this->id && $this->origin_id) {
680 $ret = $this->add_object_linked();
681 if (!$ret) {
682 $error++;
683 }
684 }
685
686 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
687 $originforcontact = $this->origin_type;
688 $originidforcontact = $this->origin_id;
689
690 $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";
691 $sqlcontact .= " WHERE element_id = ".((int) $originidforcontact)." AND ec.fk_c_type_contact = ctc.rowid AND ctc.element = '".$this->db->escape($originforcontact)."'";
692
693 $resqlcontact = $this->db->query($sqlcontact);
694 if ($resqlcontact) {
695 while ($objcontact = $this->db->fetch_object($resqlcontact)) {
696 $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
697 }
698 } else {
699 dol_print_error($this->db);
700 }
701 }
702
703 // Actions on extra fields
704 if (!$error) {
705 $result = $this->insertExtraFields();
706 if ($result < 0) {
707 $error++;
708 }
709 }
710
711 if (!$error && !$notrigger) {
712 // Call trigger
713 $result = $this->call_trigger('SHIPPING_CREATE', $user);
714 if ($result < 0) {
715 $error++;
716 }
717 // End call triggers
718
719 if (!$error) {
720 $this->db->commit();
721 return $this->id;
722 } else {
723 foreach ($this->errors as $errmsg) {
724 dol_syslog(get_class($this)."::create ".$errmsg, LOG_ERR);
725 $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
726 }
727 $this->db->rollback();
728 return -1 * $error;
729 }
730 } else {
731 $error++;
732 $this->db->rollback();
733 return -3;
734 }
735 } else {
736 $error++;
737 $this->error = $this->db->lasterror()." - sql=$sql";
738 $this->db->rollback();
739 return -2;
740 }
741 } else {
742 $error++;
743 $this->error = $this->db->error()." - sql=$sql";
744 $this->db->rollback();
745 return -1;
746 }
747 }
748
749 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
762 public function create_line($entrepot_id, $origin_line_id, $qty, $rang = 0, $array_options = [], $parent_line_id = 0, $product_id = 0)
763 {
764 //phpcs:enable
765 global $user;
766
767 $expeditionline = new ExpeditionLigne($this->db);
768 $expeditionline->fk_expedition = $this->id;
769 $expeditionline->entrepot_id = $entrepot_id;
770 $expeditionline->fk_elementdet = $origin_line_id;
771 $expeditionline->element_type = $this->origin;
772 $expeditionline->fk_parent = $parent_line_id;
773 $expeditionline->fk_product = $product_id;
774 $expeditionline->qty = $qty;
775 $expeditionline->rang = $rang;
776 $expeditionline->array_options = $array_options;
777
778 if (!($expeditionline->fk_product > 0)) {
779 $order_line = new OrderLine($this->db);
780 $order_line->fetch($expeditionline->fk_elementdet);
781 $expeditionline->fk_product = $order_line->fk_product;
782 }
783
784 if (($lineId = $expeditionline->insert($user)) < 0) {
785 $this->errors[] = $expeditionline->error;
786 }
787 return $lineId;
788 }
789
790
791 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
799 public function create_line_batch($line_ext, $array_options = [])
800 {
801 // phpcs:enable
802 $error = 0;
803 $stockLocationQty = array(); // associated array with batch qty in stock location
804
805 $tab = $line_ext->detail_batch;
806 // create stockLocation Qty array
807 foreach ($tab as $detbatch) {
808 if (!empty($detbatch->fk_warehouse)) {
809 if (empty($stockLocationQty[$detbatch->fk_warehouse])) {
810 $stockLocationQty[$detbatch->fk_warehouse] = 0;
811 }
812 $stockLocationQty[$detbatch->fk_warehouse] += $detbatch->qty;
813 }
814 }
815 // create shipment lines
816 foreach ($stockLocationQty as $stockLocation => $qty) {
817 $line_id = $this->create_line($stockLocation, $line_ext->origin_line_id, $qty, $line_ext->rang, $array_options);
818 if ($line_id < 0) {
819 $error++;
820 } else {
821 // create shipment batch lines for stockLocation
822 foreach ($tab as $detbatch) {
823 if ($detbatch->fk_warehouse == $stockLocation) {
824 if (!($detbatch->create($line_id) > 0)) { // Create an ExpeditionLineBatch
825 $this->errors = $detbatch->errors;
826 $error++;
827 }
828 }
829 }
830 }
831 }
832
833 if (!$error) {
834 return 1;
835 } else {
836 return -1;
837 }
838 }
839
849 public function fetch($id, $ref = '', $ref_ext = '', $notused = '')
850 {
851 global $conf;
852
853 // Check parameters
854 if (empty($id) && empty($ref) && empty($ref_ext)) {
855 return -1;
856 }
857
858 $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";
859 $sql .= ", e.date_valid";
860 $sql .= ", e.weight, e.weight_units, e.size, e.size_units, e.width, e.height";
861 $sql .= ", e.date_expedition as date_expedition, e.model_pdf, e.fk_address, e.date_delivery";
862 $sql .= ", e.fk_shipping_method, e.tracking_number";
863 $sql .= ", e.note_private, e.note_public";
864 $sql .= ', e.fk_incoterms, e.location_incoterms';
865 $sql .= ', e.signed_status';
866 $sql .= ', i.libelle as label_incoterms';
867 $sql .= ', s.libelle as shipping_method';
868 $sql .= ", el.fk_source as origin_id, el.sourcetype as origin_type";
869 $sql .= " FROM ".MAIN_DB_PREFIX."expedition as e";
870 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."element_element as el ON el.fk_target = e.rowid AND el.targettype = '".$this->db->escape($this->element)."'";
871 $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_incoterms as i ON e.fk_incoterms = i.rowid';
872 $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_shipment_mode as s ON e.fk_shipping_method = s.rowid';
873 $sql .= " WHERE e.entity IN (".getEntity('expedition').")";
874 if ($id) {
875 $sql .= " AND e.rowid = ".((int) $id);
876 }
877 if ($ref) {
878 $sql .= " AND e.ref='".$this->db->escape($ref)."'";
879 }
880 if ($ref_ext) {
881 $sql .= " AND e.ref_ext='".$this->db->escape($ref_ext)."'";
882 }
883
884 dol_syslog(get_class($this)."::fetch", LOG_DEBUG);
885 $result = $this->db->query($sql);
886 if ($result) {
887 if ($this->db->num_rows($result)) {
888 $obj = $this->db->fetch_object($result);
889
890 $this->id = (int) $obj->rowid;
891 $this->entity = $obj->entity;
892 $this->ref = $obj->ref;
893 $this->socid = $obj->socid;
894 $this->ref_customer = $obj->ref_customer;
895 $this->ref_ext = $obj->ref_ext;
896 $this->status = $obj->fk_statut;
897 $this->statut = $this->status; // Deprecated
898 $this->signed_status = $obj->signed_status;
899 $this->user_author_id = $obj->fk_user_author;
900 $this->fk_user_author = $obj->fk_user_author;
901 $this->user_creation_id = $obj->fk_user_author;
902 $this->date_creation = $this->db->jdate($obj->date_creation);
903 $this->date_valid = $this->db->jdate($obj->date_valid);
904 $this->date = $this->db->jdate($obj->date_expedition); // TODO deprecated
905 $this->date_expedition = $this->db->jdate($obj->date_expedition); // TODO deprecated
906 $this->date_shipping = $this->db->jdate($obj->date_expedition); // Date real
907 $this->date_delivery = $this->db->jdate($obj->date_delivery); // Date planned
908 $this->fk_delivery_address = $obj->fk_address;
909 $this->model_pdf = $obj->model_pdf;
910 $this->shipping_method_id = $obj->fk_shipping_method;
911 $this->shipping_method = $obj->shipping_method;
912 $this->tracking_number = $obj->tracking_number;
913 $this->origin = ($obj->origin_type ? $obj->origin_type : 'commande'); // For compatibility
914 $this->origin_type = ($obj->origin_type ? $obj->origin_type : 'commande');
915 $this->origin_id = $obj->origin_id;
916 $this->billed = $obj->billed;
917 $this->fk_project = $obj->fk_project;
918 $this->signed_status = $obj->signed_status;
919 $this->trueWeight = $obj->weight;
920 $this->weight_units = $obj->weight_units;
921
922 $this->trueWidth = $obj->width;
923 $this->width_units = $obj->size_units;
924 $this->trueHeight = $obj->height;
925 $this->height_units = $obj->size_units;
926 $this->trueDepth = $obj->size;
927 $this->depth_units = $obj->size_units;
928
929 $this->note_public = $obj->note_public;
930 $this->note_private = $obj->note_private;
931
932 // A denormalized value
933 $this->trueSize = $obj->size."x".$obj->width."x".$obj->height;
934 $this->size_units = $obj->size_units;
935
936 //Incoterms
937 $this->fk_incoterms = $obj->fk_incoterms;
938 $this->location_incoterms = $obj->location_incoterms;
939 $this->label_incoterms = $obj->label_incoterms;
940
941 $this->db->free($result);
942
943 // Tracking url
944 $this->getUrlTrackingStatus($obj->tracking_number);
945
946 // Thirdparty
947 $result = $this->fetch_thirdparty(); // TODO Remove this
948
949 // Retrieve extrafields
950 $this->fetch_optionals();
951
952 // Fix Get multicurrency param for transmitted
953 if (isModEnabled('multicurrency')) {
954 if (!empty($this->multicurrency_code)) {
955 $this->multicurrency_code = $this->thirdparty->multicurrency_code;
956 }
957 if (getDolGlobalString('MULTICURRENCY_USE_ORIGIN_TX') && !empty($this->thirdparty->multicurrency_tx)) {
958 $this->multicurrency_tx = $this->thirdparty->multicurrency_tx;
959 }
960 }
961
962 /*
963 * Lines
964 */
965 if (empty($obj->origin_id)) {
966 $result = $this->fetch_lines_free();
967 } else {
968 $result = $this->fetch_lines();
969 }
970 if ($result < 0) {
971 return -3;
972 }
973
974 return 1;
975 } else {
976 dol_syslog(get_class($this).'::Fetch no expedition found', LOG_ERR);
977 $this->error = 'Shipment with id '.$id.' not found';
978 return 0;
979 }
980 } else {
981 $this->error = $this->db->error();
982 return -1;
983 }
984 }
985
993 public function valid($user, $notrigger = 0)
994 {
995 global $conf;
996
997 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
998
999 dol_syslog(get_class($this)."::valid");
1000
1001 // Protection
1002 if ($this->status) {
1003 dol_syslog(get_class($this)."::valid not in draft status", LOG_WARNING);
1004 return 0;
1005 }
1006
1007 if (!isset($this->socid)) {
1008 dol_syslog(get_class($this)."::can't valid socid not set", LOG_WARNING);
1009 return 0;
1010 }
1011
1012 if (!((!getDolGlobalString('MAIN_USE_ADVANCED_PERMS') && $user->hasRight('expedition', 'creer'))
1013 || (getDolGlobalString('MAIN_USE_ADVANCED_PERMS') && $user->hasRight('expedition', 'shipping_advance', 'validate')))) {
1014 $this->error = 'Permission denied';
1015 dol_syslog(get_class($this)."::valid ".$this->error, LOG_ERR);
1016 return -1;
1017 }
1018
1019 $this->db->begin();
1020
1021 $error = 0;
1022
1023 // Define new ref
1024 $soc = new Societe($this->db);
1025 $soc->fetch($this->socid);
1026
1027 // Class of company linked to order
1028 $result = $soc->setAsCustomer();
1029
1030 // Define new ref
1031 if (preg_match('/^[\‍(]?PROV/i', $this->ref) || empty($this->ref)) { // empty should not happened, but when it occurs, the test save life
1032 $numref = $this->getNextNumRef($soc);
1033 } elseif (!empty($this->ref)) {
1034 $numref = (string) $this->ref;
1035 } else {
1036 $numref = "EXP".$this->id;
1037 }
1038 $this->newref = dol_sanitizeFileName($numref);
1039
1040 $now = dol_now();
1041
1042 // Validate
1043 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET";
1044 $sql .= " ref = '".$this->db->escape($numref)."'";
1045 $sql .= ", fk_statut = 1";
1046 $sql .= ", date_valid = '".$this->db->idate($now)."'";
1047 $sql .= ", fk_user_valid = ".((int) $user->id);
1048 $sql .= " WHERE rowid = ".((int) $this->id);
1049
1050 dol_syslog(get_class($this)."::valid update expedition", LOG_DEBUG);
1051 $resql = $this->db->query($sql);
1052 if (!$resql) {
1053 $this->error = $this->db->lasterror();
1054 $error++;
1055 }
1056
1057 // If stock increment is done on sending (recommended choice)
1058 if (!$error && isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
1059 $result = $this->manageStockMvtOnEvt($user, "ShipmentValidatedInDolibarr");
1060 if ($result < 0) {
1061 $error++;
1062 }
1063 }
1064
1065 // Change status of order to "shipment in process"
1066 $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
1067 if ($this->origin == 'commande') {
1068 $triggerKey .= 'ORDER_SHIPMENTONPROCESS';
1069 } else {
1070 $triggerKey .= strtoupper($this->origin).'_SHIPMENTONPROCESS';
1071 }
1072
1073 // TODO : load the origin object to trigger the right setStatus according to origin object
1074 if (!$error) {
1075 $ret = $this->setStatut(Commande::STATUS_SHIPMENTONPROCESS, $this->origin_id, $this->origin, $triggerKey);
1076 if (!$ret) {
1077 $error++;
1078 }
1079 }
1080
1081 if (!$error && !$notrigger) {
1082 // Call trigger
1083 $result = $this->call_trigger('SHIPPING_VALIDATE', $user);
1084 if ($result < 0) {
1085 $error++;
1086 }
1087 // End call triggers
1088 }
1089
1090 if (!$error) {
1091 $this->oldref = $this->ref;
1092
1093 // Rename directory if dir was a temporary ref
1094 if (preg_match('/^[\‍(]?PROV/i', $this->ref)) {
1095 // Now we rename also files into index
1096 $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)."'";
1097 $sql .= " WHERE filename LIKE '".$this->db->escape($this->ref)."%' AND filepath = 'expedition/sending/".$this->db->escape($this->ref)."' and entity = ".((int) $conf->entity);
1098 $resql = $this->db->query($sql);
1099 if (!$resql) {
1100 $error++;
1101 $this->error = $this->db->lasterror();
1102 }
1103 $sql = 'UPDATE '.MAIN_DB_PREFIX."ecm_files set filepath = 'expedition/sending/".$this->db->escape($this->newref)."'";
1104 $sql .= " WHERE filepath = 'expedition/sending/".$this->db->escape($this->ref)."' and entity = ".((int) $conf->entity);
1105 $resql = $this->db->query($sql);
1106 if (!$resql) {
1107 $error++;
1108 $this->error = $this->db->lasterror();
1109 }
1110
1111 // We rename directory ($this->ref = old ref, $num = new ref) in order not to lose the attachments
1112 $oldref = dol_sanitizeFileName($this->ref);
1113 $newref = dol_sanitizeFileName($numref);
1114 $dirsource = $conf->expedition->dir_output.'/sending/'.$oldref;
1115 $dirdest = $conf->expedition->dir_output.'/sending/'.$newref;
1116 if (!$error && file_exists($dirsource)) {
1117 dol_syslog(get_class($this)."::valid rename dir ".$dirsource." into ".$dirdest);
1118
1119 if (@rename($dirsource, $dirdest)) {
1120 dol_syslog("Rename ok");
1121 // Rename docs starting with $oldref with $newref
1122 $listoffiles = dol_dir_list($conf->expedition->dir_output.'/sending/'.$newref, 'files', 1, '^'.preg_quote($oldref, '/'));
1123 foreach ($listoffiles as $fileentry) {
1124 $dirsource = $fileentry['name'];
1125 $dirdest = preg_replace('/^'.preg_quote($oldref, '/').'/', $newref, $dirsource);
1126 $dirsource = $fileentry['path'].'/'.$dirsource;
1127 $dirdest = $fileentry['path'].'/'.$dirdest;
1128 @rename($dirsource, $dirdest);
1129 }
1130 }
1131 }
1132 }
1133 }
1134
1135 // Set new ref and current status
1136 if (!$error) {
1137 $this->ref = $numref;
1138 $this->statut = self::STATUS_VALIDATED;
1140 }
1141
1142 if (!$error) {
1143 $this->db->commit();
1144 return 1;
1145 } else {
1146 $this->db->rollback();
1147 return -1 * $error;
1148 }
1149 }
1150
1151 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1158 public function line_ajaxorder($rows)
1159 {
1160 if (empty($rows) || !is_array($rows)) {
1161 return;
1162 }
1163
1164 $rowToOrigin = array();
1165 $originToRows = array();
1166
1167 $sql = "SELECT rowid, fk_elementdet";
1168 $sql .= " FROM ".$this->db->prefix()."expeditiondet";
1169 $sql .= " WHERE fk_expedition = ".((int) $this->id);
1170 $sql .= " ORDER BY rang ASC, rowid ASC";
1171
1172 $resql = $this->db->query($sql);
1173 if ($resql) {
1174 while ($obj = $this->db->fetch_object($resql)) {
1175 $rowid = (int) $obj->rowid;
1176 $originLine = (int) $obj->fk_elementdet;
1177 $rowToOrigin[$rowid] = $originLine;
1178
1179 if (!isset($originToRows[$originLine])) {
1180 $originToRows[$originLine] = array();
1181 }
1182 $originToRows[$originLine][] = $rowid;
1183 }
1184 $this->db->free($resql);
1185 } else {
1186 parent::line_ajaxorder($rows);
1187 return;
1188 }
1189
1190 $processedOrigins = array();
1191 $position = 1;
1192
1193 foreach ($rows as $rowid) {
1194 $rowid = (int) $rowid;
1195 if (empty($rowid)) {
1196 continue;
1197 }
1198
1199 $originLine = isset($rowToOrigin[$rowid]) ? $rowToOrigin[$rowid] : 0;
1200 if ($originLine > 0 && !empty($processedOrigins[$originLine])) {
1201 continue;
1202 }
1203
1204 $rowidsToUpdate = array($rowid);
1205 if ($originLine > 0 && !empty($originToRows[$originLine])) {
1206 $rowidsToUpdate = $originToRows[$originLine];
1207 $processedOrigins[$originLine] = 1;
1208 }
1209
1210 foreach ($rowidsToUpdate as $childRowId) {
1211 $this->updateRangOfLine($childRowId, $position);
1212 $position++;
1213 }
1214 }
1215 }
1216 // phpcs:enable
1217
1218 // phpcs:disable
1225 public function create_delivery($user)
1226 {
1227 // phpcs:enable
1228 if (getDolGlobalInt('MAIN_SUBMODULE_DELIVERY')) {
1229 if ($this->status == self::STATUS_VALIDATED || $this->status == self::STATUS_CLOSED) {
1230 // Expedition validated
1231 include_once DOL_DOCUMENT_ROOT.'/delivery/class/delivery.class.php';
1232 $delivery = new Delivery($this->db);
1233 $result = $delivery->create_from_sending($user, $this->id);
1234 if ($result <= 0) {
1235 $this->setErrorsFromObject($delivery);
1236 }
1237 return $result;
1238 }
1239 }
1240
1241 return 0;
1242 }
1243
1258 public function addline($entrepot_id, $id, $qty, $array_options = [], $fk_product = 0, $fk_parent = 0)
1259 {
1260 global $langs;
1261
1262 $num = count($this->lines);
1263 $line = new ExpeditionLigne($this->db);
1264
1265 $line->entrepot_id = $entrepot_id;
1266 $line->origin_line_id = $id;
1267 $line->fk_elementdet = $id;
1268 $line->element_type = 'order';
1269 $line->fk_parent = $fk_parent;
1270 $line->fk_product = $fk_product;
1271 $line->qty = $qty;
1272
1273 $orderline = new OrderLine($this->db);
1274 $orderline->fetch($id);
1275
1276 // Copy the rang of the order line to the expedition line
1277 $line->rang = $orderline->rang;
1278 $line->product_type = $orderline->product_type;
1279 if (!($line->fk_product > 0)) {
1280 $line->fk_product = $orderline->fk_product;
1281 }
1282
1283 if (isModEnabled('stock') && !empty($orderline->fk_product)) {
1284 $product = new Product($this->db);
1285 $product->fetch($orderline->fk_product);
1286
1287 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 && !empty($line->qty)) {
1288 $langs->load("errors");
1289 $this->error = $langs->trans("ErrorWarehouseRequiredIntoShipmentLine");
1290 return -1;
1291 }
1292
1293 if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT') && ($qty > 0 || !getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1294 $productChildrenNb = 0;
1295 if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) {
1296 $productChildrenNb = $product->hasFatherOrChild(1);
1297 }
1298 if ($productChildrenNb > 0) {
1299 $product_stock = null;
1300 $product->loadStockForVirtualProduct('warehouseopen', $line->qty);
1301 if ($entrepot_id > 0) {
1302 if (isset($product->stock_warehouse[$entrepot_id])) {
1303 $product_stock = $product->stock_warehouse[$entrepot_id]->real;
1304 }
1305 } else {
1306 foreach ($product->stock_warehouse as $componentStockWarehouse) {
1307 if ($product_stock === null) {
1308 $product_stock = $componentStockWarehouse->real;
1309 } else {
1310 $product_stock = min($product_stock, $componentStockWarehouse->real);
1311 }
1312 }
1313 }
1314 if ($product_stock === null) {
1315 $product_stock = 0;
1316 }
1317 } else {
1318 // Check must be done for stock of product into warehouse if $entrepot_id defined
1319 if ($entrepot_id > 0) {
1320 $product->load_stock('warehouseopen');
1321 $product_stock = $product->stock_warehouse[$entrepot_id]->real;
1322 } else {
1323 $product_stock = $product->stock_reel;
1324 }
1325 }
1326
1327 $product_type = $product->type;
1328 if ($product_type == 0 || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
1329 $isavirtualproduct = ($productChildrenNb > 0);
1330 // The product is qualified for a check of quantity (must be enough in stock to be added into shipment).
1331 if (
1332 !$isavirtualproduct
1333 || !getDolGlobalInt('PRODUIT_SOUSPRODUITS')
1334 || ($isavirtualproduct && !getDolGlobalInt('STOCK_EXCLUDE_VIRTUAL_PRODUCTS'))
1335 ) {
1336 // If STOCK_EXCLUDE_VIRTUAL_PRODUCTS is set, we do not manage stock for kits/virtual products.
1337 if ($product->stockable_product == Product::ENABLED_STOCK && $product_stock < $qty) {
1338 $langs->load("errors");
1339 $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $product->ref);
1340 $this->errorhidden = 'ErrorStockIsNotEnoughToAddProductOnShipment';
1341
1342 return -3;
1343 }
1344 }
1345 }
1346 }
1347 }
1348
1349 // If product need a batch number, we should not have called this function but addline_batch instead.
1350 // If this happen, we may have a bug in card.php page
1351 if (isModEnabled('productbatch') && !empty($line->fk_product) && !empty($orderline->product_tobatch)) {
1352 $this->error = 'ADDLINE_WAS_CALLED_INSTEAD_OF_ADDLINEBATCH '.$orderline->id.' '.$line->fk_product; //
1353 return -4;
1354 }
1355
1356 // extrafields
1357 if (!getDolGlobalString('MAIN_EXTRAFIELDS_DISABLED') && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used
1358 $line->array_options = $array_options;
1359 }
1360
1361 $this->lines[$num] = $line;
1362
1363 return 1;
1364 }
1365
1379 public function addlinefree($qty, $element_type, $fk_product, $fk_unit, $rang, $description, $fk_parent, $array_options = [])
1380 {
1381 global $mysoc, $langs, $user;
1382
1383 if ($this->status == self::STATUS_DRAFT) {
1384 if (empty($rang)) {
1385 $rang = 0;
1386 }
1387
1388 $qty = (float) price2num($qty);
1389
1390 $this->db->begin();
1391
1392 // Rang to use
1393 $ranktouse = $rang;
1394 if ($ranktouse == -1) {
1395 $rangmax = $this->line_max($fk_parent);
1396 $ranktouse = $rangmax + 1;
1397 }
1398
1399 // Insert line
1400 $this->line = new ExpeditionLigne($this->db);
1401 $this->line->fk_expedition = $this->id;
1402 $this->line->element_type = $element_type;
1403 $this->line->fk_product = $fk_product;
1404 $this->line->description = $description;
1405 $this->line->desc = $description;
1406 $this->line->fk_parent = $fk_parent;
1407 $this->line->qty = (float) $qty;
1408 $this->line->fk_unit = $fk_unit;
1409 $this->line->rang = $ranktouse;
1410
1411 if (is_array($array_options) && count($array_options) > 0) {
1412 $this->line->array_options = $array_options;
1413 }
1414
1415 $result = $this->line->insert($user);
1416 if ($result > 0) {
1417 if (!isset($this->context['createfromclone'])) {
1418 if (!empty($fk_parent)) {
1419 $this->line_order(true, 'DESC');
1420 } elseif ($ranktouse > 0 && $ranktouse <= count($this->lines)) {
1421 $linecount = count($this->lines);
1422 for ($ii = $ranktouse; $ii <= $linecount; $ii++) {
1423 $this->updateRangOfLine($this->lines[$ii - 1]->id, $ii + 1);
1424 }
1425 }
1426 $this->lines[] = $this->line;
1427 }
1428
1429 $this->db->commit();
1430 return $this->line->id;
1431 } else {
1432 $this->error = $this->line->error;
1433 dol_syslog(get_class($this)."::addlinefree error=".$this->error, LOG_ERR);
1434 $this->db->rollback();
1435 return -2;
1436 }
1437 } else {
1438 dol_syslog(get_class($this)."::addlinefree status of shipment must be Draft to allow use of ->addlinefree()", LOG_ERR);
1439 return -3;
1440 }
1441 }
1442
1458 public function updatelinefree($rowid, $qty, $element_type, $fk_product, $fk_unit, $rang, $description, $fk_parent, $notrigger, $array_options = array())
1459 {
1460 global $mysoc, $langs, $user;
1461
1462 if ($this->status == self::STATUS_DRAFT) {
1463 $this->db->begin();
1464
1465 if (empty($qty)) {
1466 $qty = 0;
1467 }
1468 if (empty($rang)) {
1469 $rang = 0;
1470 }
1471
1472 $qty = (float) $qty;
1473 $description = trim($description);
1474
1475 // Fetch current line from the database and then clone the object and set it in $oldline property
1476 $line = new ExpeditionLigne($this->db);
1477
1478 $line->fetch($rowid);
1479 $line->fetch_optionals();
1480
1481 if (!empty($line->fk_product)) {
1482 $product = new Product($this->db);
1483 $result = $product->fetch($line->fk_product);
1484 $product_type = $product->type;
1485 }
1486
1487 $staticline = clone $line;
1488
1489 $line->oldline = $staticline;
1490 $this->line = $line;
1491 $this->line->context = $this->context;
1492 $this->line->rang = $rang;
1493 $this->line->fk_expedition = $this->id;
1494 $this->line->element_type = $element_type;
1495 $this->line->fk_product = $fk_product;
1496 $this->line->qty = $qty;
1497 $this->line->fk_unit = $fk_unit;
1498 $this->line->fk_parent = $fk_parent;
1499 $this->line->description = $description;
1500
1501 if (is_array($array_options) && count($array_options) > 0) {
1502 // We replace values in this->line->array_options only for entries defined into $array_options
1503 foreach ($array_options as $key => $value) {
1504 $this->line->array_options[$key] = $array_options[$key];
1505 }
1506 }
1507
1508 $result = $this->line->update($user, $notrigger);
1509 if ($result > 0) {
1510 // Reorder if child line
1511 if (!empty($fk_parent)) {
1512 $this->line_order(true, 'DESC');
1513 }
1514
1515 $this->db->commit();
1516 return $result;
1517 } else {
1518 $this->error = $this->line->error;
1519
1520 $this->db->rollback();
1521 return -1;
1522 }
1523 } else {
1524 $this->error = get_class($this)."::updatelinefree Shipment status makes operation forbidden";
1525 $this->errors = array('ShipmentStatusMakeOperationForbidden');
1526 return -2;
1527 }
1528 }
1529
1530 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1539 public function addline_batch($dbatch, $array_options = [], $origin_line = null)
1540 {
1541 // phpcs:enable
1542 global $langs;
1543
1544 $num = count($this->lines);
1545 $linebatch = null;
1546 if ($dbatch['qty'] > 0 || ($dbatch['qty'] == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1547 $line = new ExpeditionLigne($this->db);
1548 $tab = array();
1549 foreach ($dbatch['detail'] as $key => $value) {
1550 if ($value['q'] > 0 || ($value['q'] == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1551 // $value['q']=qty to move
1552 // $value['id_batch']=id into llx_product_batch of record to move
1553 //var_dump($value);
1554
1555 $linebatch = new ExpeditionLineBatch($this->db);
1556 $ret = $linebatch->fetchFromStock($value['id_batch']); // load serial, sellby, eatby
1557 if ($ret < 0) {
1558 $this->setErrorsFromObject($linebatch);
1559 return -1;
1560 }
1561 $linebatch->qty = $value['q'];
1562 if ($linebatch->qty == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS')) {
1563 $linebatch->batch = null;
1564 }
1565 $tab[] = $linebatch;
1566
1567 if (getDolGlobalString("STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT", '0') && ($linebatch->qty > 0 || !getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1568 require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
1569 $prod_batch = new Productbatch($this->db);
1570 $prod_batch->fetch($value['id_batch']);
1571
1572 if ($prod_batch->qty < $linebatch->qty) {
1573 $langs->load("errors");
1574 $this->errors[] = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $prod_batch->fk_product);
1575 dol_syslog(get_class($this)."::addline_batch error=Product ".$prod_batch->batch.": ".$this->errorsToString(), LOG_ERR);
1576 return -1;
1577 }
1578 }
1579 }
1580 }
1581 if (is_object($linebatch)) {
1582 $line->entrepot_id = $linebatch->fk_warehouse;
1583 }
1584 $line->origin_line_id = $dbatch['ix_l']; // deprecated
1585 $line->fk_elementdet = $dbatch['ix_l'];
1586 $line->qty = $dbatch['qty'];
1587 $line->detail_batch = $tab;
1588 if (!($line->rang > 0)) {
1589 $line->rang = $origin_line->rang;
1590 }
1591 if (!($line->fk_product > 0)) {
1592 $line->fk_product = $origin_line->fk_product;
1593 }
1594
1595 // extrafields
1596 if (!getDolGlobalString('MAIN_EXTRAFIELDS_DISABLED') && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used
1597 $line->array_options = $array_options;
1598 }
1599
1600 //var_dump($line);
1601 $this->lines[$num] = $line;
1602 return 1;
1603 }
1604 return 0;
1605 }
1606
1614 public function update($user = null, $notrigger = 0)
1615 {
1616 global $conf;
1617 $error = 0;
1618
1619 // Clean parameters
1620
1621 if (isset($this->ref)) {
1622 $this->ref = trim($this->ref);
1623 }
1624 if (isset($this->entity)) {
1625 $this->entity = (int) $this->entity;
1626 }
1627 if (isset($this->ref_customer)) {
1628 $this->ref_customer = trim($this->ref_customer);
1629 }
1630 if (isset($this->socid)) {
1631 $this->socid = (int) $this->socid;
1632 }
1633 if (isset($this->fk_user_author)) {
1634 $this->fk_user_author = (int) $this->fk_user_author;
1635 }
1636 if (isset($this->fk_user_valid)) {
1637 $this->fk_user_valid = (int) $this->fk_user_valid;
1638 }
1639 if (isset($this->fk_delivery_address)) {
1640 $this->fk_delivery_address = (int) $this->fk_delivery_address;
1641 }
1642 if (isset($this->shipping_method_id)) {
1643 $this->shipping_method_id = (int) $this->shipping_method_id;
1644 }
1645 if (isset($this->tracking_number)) {
1646 $this->tracking_number = trim($this->tracking_number);
1647 }
1648 if (isset($this->statut)) {
1649 $this->statut = (int) $this->statut;
1650 }
1651 if (isset($this->status)) {
1652 $this->status = (int) $this->status;
1653 }
1654 if (isset($this->trueDepth)) {
1655 $this->trueDepth = trim($this->trueDepth);
1656 }
1657 if (isset($this->trueWidth)) {
1658 $this->trueWidth = trim($this->trueWidth);
1659 }
1660 if (isset($this->trueHeight)) {
1661 $this->trueHeight = trim($this->trueHeight);
1662 }
1663 if (isset($this->size_units)) {
1664 $this->size_units = trim($this->size_units);
1665 }
1666 if (isset($this->weight_units)) {
1667 $this->weight_units = (int) $this->weight_units;
1668 }
1669 if (isset($this->trueWeight)) {
1670 $this->weight = trim((string) $this->trueWeight);
1671 }
1672 if (isset($this->note_private)) {
1673 $this->note_private = trim($this->note_private);
1674 }
1675 if (isset($this->note_public)) {
1676 $this->note_public = trim($this->note_public);
1677 }
1678 if (isset($this->model_pdf)) {
1679 $this->model_pdf = trim($this->model_pdf);
1680 }
1681 if (!empty($this->date_expedition)) {
1682 $this->date_shipping = $this->date_expedition;
1683 }
1684
1685 // Check parameters
1686 // Put here code to add control on parameters values
1687
1688 // Update request
1689 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET";
1690 $sql .= " ref = ".(isset($this->ref) ? "'".$this->db->escape($this->ref)."'" : "null").",";
1691 $sql .= " ref_ext = ".(isset($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null").",";
1692 $sql .= " ref_customer = ".(isset($this->ref_customer) ? "'".$this->db->escape($this->ref_customer)."'" : "null").",";
1693 $sql .= " fk_soc = ".(isset($this->socid) ? ((int) $this->socid) : "null").",";
1694 $sql .= " date_creation = ".(dol_strlen($this->date_creation) != 0 ? "'".$this->db->idate($this->date_creation)."'" : 'null').",";
1695 $sql .= " fk_user_author = ".(isset($this->fk_user_author) ? ((int) $this->fk_user_author) : "null").",";
1696 $sql .= " date_valid = ".(dol_strlen($this->date_valid) != 0 ? "'".$this->db->idate($this->date_valid)."'" : 'null').",";
1697 $sql .= " fk_user_valid = ".(isset($this->fk_user_valid) ? ((int) $this->fk_user_valid) : "null").",";
1698 $sql .= " date_expedition = ".(dol_strlen($this->date_shipping) != 0 ? "'".$this->db->idate($this->date_shipping)."'" : 'null').",";
1699 $sql .= " date_delivery = ".(dol_strlen($this->date_delivery) != 0 ? "'".$this->db->idate($this->date_delivery)."'" : 'null').",";
1700 $sql .= " fk_address = ".(isset($this->fk_delivery_address) ? ((int) $this->fk_delivery_address) : "null").",";
1701 $sql .= " fk_shipping_method = ".((isset($this->shipping_method_id) && $this->shipping_method_id > 0) ? ((int) $this->shipping_method_id) : "null").",";
1702 $sql .= " tracking_number = ".(isset($this->tracking_number) ? "'".$this->db->escape($this->tracking_number)."'" : "null").",";
1703 $sql .= " fk_statut = ".(isset($this->status) ? ((int) $this->status) : "null").",";
1704 $sql .= " fk_projet = ".(isset($this->fk_project) ? ((int) $this->fk_project) : "null").",";
1705 $sql .= " height = ".(($this->trueHeight != '') ? ((float) $this->trueHeight) : "null").",";
1706 $sql .= " width = ".(($this->trueWidth != '') ? ((float) $this->trueWidth) : "null").",";
1707 $sql .= " size_units = ".(isset($this->size_units) ? ((int) $this->size_units) : "null").",";
1708 $sql .= " size = ".(($this->trueDepth != '') ? ((float) $this->trueDepth) : "null").",";
1709 $sql .= " weight_units = ".(isset($this->weight_units) ? ((int) $this->weight_units) : "null").",";
1710 $sql .= " weight = ".(($this->trueWeight != '') ? ((float) $this->trueWeight) : "null").",";
1711 $sql .= " note_private = ".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : "null").",";
1712 $sql .= " note_public = ".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : "null").",";
1713 $sql .= " model_pdf = ".(isset($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null").",";
1714 $sql .= " entity = ".((int) $conf->entity);
1715 $sql .= " WHERE rowid = ".((int) $this->id);
1716
1717 $this->db->begin();
1718
1719 dol_syslog(get_class($this)."::update", LOG_DEBUG);
1720 $resql = $this->db->query($sql);
1721 if (!$resql) {
1722 $error++;
1723 $this->errors[] = "Error ".$this->db->lasterror();
1724 }
1725
1726 // Actions on extra fields
1727 if (!$error) {
1728 $result = $this->insertExtraFields();
1729 if ($result < 0) {
1730 $error++;
1731 }
1732 }
1733
1734 if (!$error && !$notrigger) {
1735 // Call trigger
1736 $result = $this->call_trigger('SHIPPING_MODIFY', $user);
1737 if ($result < 0) {
1738 $error++;
1739 }
1740 // End call triggers
1741 }
1742
1743 // Commit or rollback
1744 if ($error) {
1745 foreach ($this->errors as $errmsg) {
1746 dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR);
1747 $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
1748 }
1749 $this->db->rollback();
1750 return -1 * $error;
1751 } else {
1752 $this->db->commit();
1753 return 1;
1754 }
1755 }
1756
1757
1766 public function cancel($user, $notrigger = 0, $also_update_stock = false)
1767 {
1768 global $conf, $langs, $user;
1769
1770 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1771
1772 $error = 0;
1773 $this->error = '';
1774
1775 $this->db->begin();
1776
1777 // Add a protection to refuse deleting if shipment has at least one delivery
1778 $this->fetchObjectLinked($this->id, 'shipping', 0, 'delivery'); // Get deliveries linked to this shipment
1779 if (isset($this->linkedObjectsIds['delivery']) && count($this->linkedObjectsIds['delivery']) > 0) {
1780 $this->error = 'ErrorThereIsSomeDeliveries';
1781 $error++;
1782 }
1783
1784 if (!$error && !$notrigger) {
1785 // Call trigger
1786 $result = $this->call_trigger('SHIPPING_CANCEL', $user);
1787 if ($result < 0) {
1788 $error++;
1789 }
1790 // End call triggers
1791 }
1792
1793 // Stock control
1794 $can_update_stock = isModEnabled('stock') &&
1795 ((getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') && $this->status > self::STATUS_DRAFT) ||
1796 (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE') && $this->status == self::STATUS_CLOSED && $also_update_stock));
1797 if (!$error) {
1798 require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php";
1799
1800 $langs->load("agenda");
1801
1802 // Loop on each product line to add a stock movement (contain sub-products)
1803 $sql = "SELECT ";
1804 $sql .= " ed.fk_product";
1805 $sql .= ", ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id";
1806 $sql .= ", SUM(".$this->db->ifsql("pa.rowid IS NOT NULL", "1", "0").") as iskit";
1807 $sql .= ", ".$this->db->ifsql("pai.incdec IS NULL", "1", "pai.incdec")." as incdec";
1808 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
1809 $sql .= " LEFT JOIN ".$this->db->prefix()."product_association as pa ON pa.fk_product_pere = ed.fk_product";
1810 $sql .= " LEFT JOIN ".$this->db->prefix()."expeditiondet as edp ON edp.rowid = ed.fk_parent";
1811 $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";
1812 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
1813 $sql .= " GROUP BY ed.fk_product, ed.qty, ed.fk_entrepot, ed.rowid, pai.incdec";
1814 $sql .= $this->db->order("ed.rowid", "DESC");
1815
1816 dol_syslog(get_class($this)."::delete select details", LOG_DEBUG);
1817 $resql = $this->db->query($sql);
1818 if ($resql) {
1819 $cpt = $this->db->num_rows($resql);
1820
1821 $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1822
1823 for ($i = 0; $i < $cpt; $i++) {
1824 dol_syslog(get_class($this)."::delete movement index ".$i);
1825 $obj = $this->db->fetch_object($resql);
1826 $line_id = (int) $obj->expeditiondet_id;
1827
1828 if ($can_update_stock && (empty($obj->iskit) || getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE')) && !empty($obj->incdec)) {
1829 $mouvS = new MouvementStock($this->db);
1830 // we do not log origin because it will be deleted
1831 $mouvS->origin = '';
1832 // get lot/serial
1833 $lotArray = null;
1834 if (isModEnabled('productbatch')) {
1835 $lotArray = $shipmentlinebatch->fetchAll($obj->expeditiondet_id);
1836 if (!is_array($lotArray)) {
1837 $error++;
1838 $this->errors[] = "Error ".$this->db->lasterror();
1839 }
1840 }
1841
1842 if (empty($lotArray)) {
1843 // no lot/serial
1844 // We increment stock of product (and sub-products)
1845 // We use warehouse selected for each line
1846 $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
1847 if ($result < 0) {
1848 $error++;
1849 $this->errors = array_merge($this->errors, $mouvS->errors);
1850 break;
1851 }
1852 } else {
1853 // We increment stock of batches
1854 // We use warehouse selected for each line
1855 foreach ($lotArray as $lot) {
1856 $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
1857 if ($result < 0) {
1858 $error++;
1859 $this->errors = array_merge($this->errors, $mouvS->errors);
1860 break;
1861 }
1862 }
1863 if ($error) {
1864 break; // break for loop in case of error
1865 }
1866 }
1867 }
1868
1869 if (!$error) {
1870 // delete all children and batches of this shipment line
1871 $shipment_line = new ExpeditionLigne($this->db);
1872 $res = $shipment_line->fetch($line_id);
1873 if ($res > 0) {
1874 $result = $shipment_line->delete($user);
1875 if ($result < 0) {
1876 $error++;
1877 $this->errors[] = "Error ".$shipment_line->errorsToString();
1878 }
1879 } else {
1880 $error++;
1881 $this->errors[] = "Error ".$shipment_line->errorsToString();
1882 }
1883 }
1884
1885 if ($error) {
1886 break;
1887 }
1888 }
1889 } else {
1890 $error++;
1891 $this->errors[] = "Error ".$this->db->lasterror();
1892 }
1893 }
1894
1895 // No delete expedition
1896 if (!$error) {
1897 $sql = "SELECT rowid FROM ".$this->db->prefix()."expedition";
1898 $sql .= " WHERE rowid = ".((int) $this->id);
1899
1900 if ($this->db->query($sql)) {
1901 if (!empty($this->origin) && $this->origin_id > 0) {
1902 $this->fetch_origin();
1904 '@phan-var-force Facture|Commande $origin_object';
1906 if ($origin_object->status == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress"
1907 // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress"
1908 $origin_object->loadExpeditions();
1909 if (count($origin_object->expeditions) <= 0) {
1911 }
1912 }
1913 }
1914
1915 $this->db->commit();
1916
1917 // We delete PDFs
1918 $ref = dol_sanitizeFileName($this->ref);
1919 if (!empty($conf->expedition->dir_output)) {
1920 $dir = $conf->expedition->dir_output.'/sending/'.$ref;
1921 $file = $dir.'/'.$ref.'.pdf';
1922 if (file_exists($file)) {
1923 if (!dol_delete_file($file)) {
1924 return 0;
1925 }
1926 }
1927 if (file_exists($dir)) {
1928 if (!dol_delete_dir_recursive($dir)) {
1929 $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir);
1930 return 0;
1931 }
1932 }
1933 }
1934
1935 return 1;
1936 } else {
1937 $this->error = $this->db->lasterror()." - sql=$sql";
1938 $this->db->rollback();
1939 return -3;
1940 }
1941 } else {
1942 $this->db->rollback();
1943 return -2;
1944 }
1945 }
1946
1956 public function delete($user = null, $notrigger = 0, $also_update_stock = false)
1957 {
1958 global $conf, $langs;
1959
1960 if (empty($user)) {
1961 global $user;
1962 }
1963
1964 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1965
1966 $error = 0;
1967 $this->error = '';
1968
1969 $this->db->begin();
1970
1971 // Add a protection to refuse deleting if shipment has at least one delivery
1972 $this->fetchObjectLinked($this->id, 'shipping', 0, 'delivery'); // Get deliveries linked to this shipment
1973 if (isset($this->linkedObjectsIds['delivery']) && count($this->linkedObjectsIds['delivery']) > 0) {
1974 $this->error = 'ErrorThereIsSomeDeliveries';
1975 $error++;
1976 }
1977
1978 if (!$error && !$notrigger) {
1979 // Call trigger
1980 $result = $this->call_trigger('SHIPPING_DELETE', $user);
1981 if ($result < 0) {
1982 $error++;
1983 }
1984 // End call triggers
1985 }
1986
1987 // Stock control
1988 $can_update_stock = isModEnabled('stock') &&
1989 ((getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') && $this->status > self::STATUS_DRAFT) ||
1990 (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE') && $this->status == self::STATUS_CLOSED && $also_update_stock));
1991 if (!$error) {
1992 require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php";
1993
1994 $langs->load("agenda");
1995
1996 // we try deletion of batch line even if module batch not enabled in case of the module were enabled and disabled previously
1997 $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1998
1999 // Loop on each product line to add a stock movement (contain sub-products)
2000 $sql = "SELECT ";
2001 $sql .= " ed.fk_product";
2002 $sql .= ", ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id";
2003 $sql .= ", SUM(".$this->db->ifsql("pa.rowid IS NOT NULL", "1", "0").") as iskit";
2004 $sql .= ", ".$this->db->ifsql("pai.incdec IS NULL", "1", "pai.incdec")." as incdec";
2005 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
2006 $sql .= " LEFT JOIN ".$this->db->prefix()."product_association as pa ON pa.fk_product_pere = ed.fk_product";
2007 $sql .= " LEFT JOIN ".$this->db->prefix()."expeditiondet as edp ON edp.rowid = ed.fk_parent";
2008 $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";
2009 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
2010 $sql .= " GROUP BY ed.fk_product, ed.qty, ed.fk_entrepot, ed.rowid, pai.incdec";
2011 $sql .= $this->db->order("ed.rowid", "DESC");
2012
2013 dol_syslog(get_class($this)."::delete select details", LOG_DEBUG);
2014 $resql = $this->db->query($sql);
2015 if ($resql) {
2016 $cpt = $this->db->num_rows($resql);
2017 for ($i = 0; $i < $cpt; $i++) {
2018 dol_syslog(get_class($this)."::delete movement index ".$i);
2019 $obj = $this->db->fetch_object($resql);
2020 $line_id = (int) $obj->expeditiondet_id;
2021
2022 if ($can_update_stock && (empty($obj->iskit) || getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE')) && !empty($obj->incdec)) {
2023 $mouvS = new MouvementStock($this->db);
2024 // we do not log origin because it will be deleted
2025 $mouvS->origin = '';
2026 // get lot/serial
2027 $lotArray = $shipmentlinebatch->fetchAll($line_id);
2028 if (!is_array($lotArray)) {
2029 $error++;
2030 $this->errors[] = "Error ".$this->db->lasterror();
2031 }
2032 if (empty($lotArray)) {
2033 // no lot/serial
2034 // We increment stock of product (disable for sub-products : already in shipment lines)
2035 // We use warehouse selected for each line
2036 $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
2037 if ($result < 0) {
2038 $error++;
2039 $this->errors = array_merge($this->errors, $mouvS->errors);
2040 break;
2041 }
2042 } else {
2043 // We increment stock of batches
2044 // We use warehouse selected for each line
2045 foreach ($lotArray as $lot) {
2046 if (empty($lot->qty)) {
2047 continue;
2048 }
2049 $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
2050 if ($result < 0) {
2051 $error++;
2052 $this->errors = array_merge($this->errors, $mouvS->errors);
2053 break;
2054 }
2055 }
2056 if ($error) {
2057 break; // break for loop in case of error
2058 }
2059 }
2060 }
2061
2062 if (!$error) {
2063 // delete all children and batches of this shipment line
2064 $shipment_line = new ExpeditionLigne($this->db);
2065 $res = $shipment_line->fetch($line_id);
2066 if ($res > 0) {
2067 $result = $shipment_line->delete($user);
2068 if ($result < 0) {
2069 $error++;
2070 $this->errors[] = "Error ".$shipment_line->errorsToString();
2071 }
2072 } else {
2073 $error++;
2074 $this->errors[] = "Error ".$shipment_line->errorsToString();
2075 }
2076 }
2077
2078 if ($error) {
2079 break;
2080 }
2081 }
2082 } else {
2083 $error++;
2084 $this->errors[] = "Error ".$this->db->lasterror();
2085 }
2086 }
2087
2088 if (!$error) {
2089 // Delete linked object
2090 $res = $this->deleteObjectLinked();
2091 if ($res < 0) {
2092 $error++;
2093 }
2094
2095 // delete extrafields
2096 $res = $this->deleteExtraFields();
2097 if ($res < 0) {
2098 $error++;
2099 }
2100
2101 if (!$error) {
2102 // Delete linked contacts
2103 $res = $this->delete_linked_contact();
2104 if ($res < 0) {
2105 $error++;
2106 }
2107 }
2108 if (!$error) {
2109 $sql = "DELETE FROM ".$this->db->prefix()."expedition";
2110 $sql .= " WHERE rowid = ".((int) $this->id);
2111
2112 if ($this->db->query($sql)) {
2113 if (!empty($this->origin) && $this->origin_id > 0) {
2114 $this->fetch_origin();
2116 '@phan-var-force Facture|Commande $origin_object';
2117 if ($origin_object->status == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress"
2118 // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress"
2119 $origin_object->loadExpeditions();
2120 if (count($origin_object->expeditions) <= 0) {
2122 }
2123 }
2124 }
2125
2126 $this->db->commit();
2127
2128 // Delete record into ECM index (Note that delete is also done when deleting files with the dol_delete_dir_recursive
2129 $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
2130 $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
2131
2132 // We delete PDFs
2133 $ref = dol_sanitizeFileName($this->ref);
2134 if (!empty($conf->expedition->dir_output) && !empty($ref)) {
2135 $dir = $conf->expedition->dir_output . '/sending/' . $ref;
2136 $file = $dir . '/' . $ref . '.pdf';
2137 if (file_exists($file)) {
2138 if (!dol_delete_file($file)) {
2139 return 0;
2140 }
2141 }
2142 if (file_exists($dir)) {
2143 if (!dol_delete_dir_recursive($dir)) {
2144 $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir);
2145 return 0;
2146 }
2147 }
2148 }
2149
2150 return 1;
2151 } else {
2152 $this->error = $this->db->lasterror()." - sql=$sql";
2153 $this->db->rollback();
2154 return -3;
2155 }
2156 } else {
2157 $this->db->rollback();
2158 return -2;
2159 }
2160 } else {
2161 $this->db->rollback();
2162 return -1;
2163 }
2164 }
2165
2166 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2172 public function fetch_lines()
2173 {
2174 // phpcs:enable
2175 global $mysoc;
2176
2177 $this->lines = array();
2178
2179 // NOTE: This fetch_lines is special because it groups all lines with the same origin_line_id into one line.
2180 // TODO: See if we can restore a common fetch_lines (one line = one record)
2181
2182 $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";
2183 $sql .= ", cd.total_ht, cd.total_localtax1, cd.total_localtax2, cd.total_ttc, cd.total_tva";
2184 $sql .= ", cd.fk_remise_except, cd.fk_product_fournisseur_price as fk_fournprice";
2185 $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";
2186 $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";
2187 $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";
2188 $sql .= ", p.ref as product_ref, p.label as product_label, p.fk_product_type, p.barcode as product_barcode";
2189 $sql .= ", p.weight, p.weight_units, p.length, p.length_units, p.width, p.width_units, p.height, p.height_units";
2190 $sql .= ", p.surface, p.surface_units, p.volume, p.volume_units, p.tosell as product_tosell, p.tobuy as product_tobuy";
2191 $sql .= ", p.tobatch as product_tobatch, p.stockable_product";
2192 $sql .= " FROM ".MAIN_DB_PREFIX."expeditiondet as ed, ".MAIN_DB_PREFIX."commandedet as cd";
2193 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product";
2194 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
2195 $sql .= " AND ed.fk_elementdet = cd.rowid";
2196 $sql .= " ORDER BY CASE WHEN ed.rang IS NULL OR ed.rang = 0 THEN cd.rang ELSE ed.rang END, ed.rowid";
2197
2198 dol_syslog(get_class($this)."::fetch_lines", LOG_DEBUG);
2199 $resql = $this->db->query($sql);
2200 if ($resql) {
2201 include_once DOL_DOCUMENT_ROOT.'/core/lib/price.lib.php';
2202
2203 $num = $this->db->num_rows($resql);
2204 $i = 0;
2205 $line = new ExpeditionLigne($this->db);
2206 $lineindex = 0;
2207 $originline = 0;
2208
2209 $this->total_ht = 0;
2210 $this->total_tva = 0;
2211 $this->total_ttc = 0;
2212 $this->total_localtax1 = 0;
2213 $this->total_localtax2 = 0;
2214
2215 $this->multicurrency_total_ht = 0;
2216 $this->multicurrency_total_tva = 0;
2217 $this->multicurrency_total_ttc = 0;
2218
2219 $shipmentlinebatch = new ExpeditionLineBatch($this->db);
2220
2221 $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.
2222 while ($i < $num) {
2223 $obj = $this->db->fetch_object($resql);
2224
2225 if ($originline > 0 && $originline == $obj->fk_elementdet) {
2226 // '@phan-var-force ExpeditionLigne $line'; // $line from previous loop
2227 $line->entrepot_id = 0; // entrepod_id in details_entrepot
2228 $line->qty_shipped += $obj->qty_shipped;
2229 } else {
2230 $line = new ExpeditionLigne($this->db); // new group to start
2231 $line->entrepot_id = $obj->fk_entrepot; // this is a property of a shipment line
2232 $line->qty_shipped = $obj->qty_shipped; // this is a property of a shipment line
2233 }
2234
2235 $detail_entrepot = new stdClass();
2236 $detail_entrepot->entrepot_id = $obj->fk_entrepot;
2237 $detail_entrepot->qty_shipped = $obj->qty_shipped;
2238 $detail_entrepot->line_id = $obj->line_id;
2239 $line->details_entrepot[] = $detail_entrepot;
2240
2241 $line->line_id = $obj->line_id; // TODO deprecated
2242 $line->rowid = $obj->line_id; // TODO deprecated
2243 $line->id = $obj->line_id;
2244
2245 $line->fk_origin = 'orderline'; // TODO deprecated, we already have element_type that can be use to guess type of line
2246
2247 $line->fk_element = $obj->fk_element;
2248 $line->origin_id = $obj->fk_element;
2249 $line->fk_elementdet = $obj->fk_elementdet;
2250 $line->origin_line_id = $obj->fk_elementdet;
2251 $line->element_type = $obj->element_type;
2252
2253 $line->fk_expedition = $this->id; // id of parent
2254
2255 $line->stockable_product = $obj->stockable_product;
2256 $line->product_type = $obj->product_type;
2257 $line->fk_product = $obj->fk_product;
2258 $line->fk_product_type = $obj->fk_product_type;
2259 $line->ref = $obj->product_ref; // TODO deprecated
2260 $line->product_ref = $obj->product_ref;
2261 $line->product_label = $obj->product_label;
2262 $line->libelle = $obj->product_label; // TODO deprecated
2263 $line->product_barcode = $obj->product_barcode; // Barcode number product
2264 $line->product_tosell = $obj->product_tosell;
2265 $line->product_tobuy = $obj->product_tobuy;
2266 $line->product_tobatch = $obj->product_tobatch;
2267 $line->fk_fournprice = $obj->fk_fournprice;
2268 $line->label = $obj->custom_label;
2269 $line->description = $obj->description;
2270 $line->qty_asked = $obj->qty_asked;
2271 $line->rang = $obj->rang;
2272 $line->weight = $obj->weight;
2273 $line->weight_units = $obj->weight_units;
2274 $line->length = $obj->length;
2275 $line->length_units = $obj->length_units;
2276 $line->width = $obj->width;
2277 $line->width_units = $obj->width_units;
2278 $line->height = $obj->height;
2279 $line->height_units = $obj->height_units;
2280 $line->surface = $obj->surface;
2281 $line->surface_units = $obj->surface_units;
2282 $line->volume = $obj->volume;
2283 $line->volume_units = $obj->volume_units;
2284 $line->stockable_product = $obj->stockable_product;
2285 $line->fk_unit = $obj->fk_unit;
2286
2287 $line->extraparams = !empty($obj->extraparams) ? (array) json_decode($obj->extraparams, true) : array();
2288
2289 $line->pa_ht = $obj->pa_ht;
2290
2291 // Local taxes
2292 $localtax_array = array(0 => $obj->localtax1_type, 1 => $obj->localtax1_tx, 2 => $obj->localtax2_type, 3 => $obj->localtax2_tx);
2293 $localtax1_tx = get_localtax($obj->tva_tx, 1, $this->thirdparty);
2294 $localtax2_tx = get_localtax($obj->tva_tx, 2, $this->thirdparty);
2295
2296 // For invoicing
2297 $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
2298 $line->desc = $obj->description; // We need ->desc because some code into CommonObject use desc (property defined for other elements)
2299 $line->qty = $line->qty_shipped;
2300 $line->total_ht = (float) $tabprice[0];
2301 $line->total_localtax1 = (float) $tabprice[9];
2302 $line->total_localtax2 = (float) $tabprice[10];
2303 $line->total_ttc = (float) $tabprice[2];
2304 $line->total_tva = (float) $tabprice[1];
2305 $line->vat_src_code = $obj->vat_src_code;
2306 $line->tva_tx = $obj->tva_tx;
2307 $line->localtax1_tx = $obj->localtax1_tx;
2308 $line->localtax2_tx = $obj->localtax2_tx;
2309 $line->info_bits = $obj->info_bits;
2310 $line->price = $obj->price;
2311 $line->subprice = $obj->subprice;
2312 $line->fk_remise_except = $obj->fk_remise_except;
2313 $line->remise_percent = $obj->remise_percent;
2314
2315 $this->total_ht += $tabprice[0];
2316 $this->total_tva += $tabprice[1];
2317 $this->total_ttc += $tabprice[2];
2318 $this->total_localtax1 += $tabprice[9];
2319 $this->total_localtax2 += $tabprice[10];
2320
2321 $line->date_start = $this->db->jdate($obj->date_start);
2322 $line->date_end = $this->db->jdate($obj->date_end);
2323
2324 $line->special_code = $obj->special_code;
2325
2326 // Multicurrency
2327 $this->fk_multicurrency = $obj->fk_multicurrency;
2328 $this->multicurrency_code = $obj->multicurrency_code;
2329 $line->multicurrency_subprice = $obj->multicurrency_subprice;
2330 $line->multicurrency_total_ht = $obj->multicurrency_total_ht;
2331 $line->multicurrency_total_tva = $obj->multicurrency_total_tva;
2332 $line->multicurrency_total_ttc = $obj->multicurrency_total_ttc;
2333
2334 $this->multicurrency_total_ht += $obj->multicurrency_total_ht;
2335 $this->multicurrency_total_tva += $obj->multicurrency_total_tva;
2336 $this->multicurrency_total_ttc += $obj->multicurrency_total_ttc;
2337
2338 // Detail of batch
2339 if (isModEnabled('productbatch') && $obj->line_id > 0 && $obj->product_tobatch > 0) {
2340 if ($originline != $obj->fk_elementdet) {
2341 $line->detail_batch = array();
2342 }
2343 $newdetailbatch = $shipmentlinebatch->fetchAll($obj->line_id, $obj->fk_product);
2344
2345 if (is_array($newdetailbatch)) {
2346 if ($originline != $obj->fk_elementdet) {
2347 $line->detail_batch = $newdetailbatch;
2348 } else {
2349 $line->detail_batch = array_merge($line->detail_batch, $newdetailbatch);
2350 }
2351 }
2352 }
2353
2354 // virtual product : find all children stock (group by product id and warehouse id)
2355 if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) {
2356 $detail_children = array(); // detail by product : array of [warehouse_id => total_qty]
2357 $line_child_list = array();
2358 $res = $line->findAllChild($line->id, $line_child_list, 1);
2359 if ($res > 0) {
2360 foreach ($line_child_list as $child_line) {
2361 foreach ($child_line as $child_obj) {
2362 $child_product_id = (int) $child_obj->fk_product;
2363 $child_warehouse_id = (int) $child_obj->fk_warehouse;
2364
2365 if ($child_warehouse_id > 0) {
2366 // child quantities group by warehouses
2367 if (!isset($detail_children[$child_product_id])) {
2368 $detail_children[$child_product_id] = array();
2369 }
2370 if (!isset($detail_children[$child_product_id][$child_warehouse_id])) {
2371 $detail_children[$child_product_id][$child_warehouse_id] = 0;
2372 }
2373 $detail_children[$child_product_id][$child_warehouse_id] += $child_obj->qty;
2374 }
2375 }
2376 }
2377 }
2378 $line->detail_children = $detail_children;
2379 }
2380
2381 $line->fetch_optionals();
2382
2383 if ($originline != $obj->fk_elementdet) {
2384 $this->lines[$lineindex] = $line;
2385 $lineindex++;
2386 } else {
2387 $line->total_ht += $tabprice[0];
2388 $line->total_localtax1 += $tabprice[9];
2389 $line->total_localtax2 += $tabprice[10];
2390 $line->total_ttc += $tabprice[2];
2391 $line->total_tva += $tabprice[1];
2392 }
2393
2394 $i++;
2395 $originline = $obj->fk_elementdet;
2396 }
2397 $this->db->free($resql);
2398 return 1;
2399 } else {
2400 $this->error = $this->db->error();
2401 return -3;
2402 }
2403 }
2404
2405
2406 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2412 public function fetch_lines_free()
2413 {
2414 // phpcs:enable
2415 global $mysoc;
2416
2417 $this->lines = array();
2418
2419 $sql = 'SELECT ed.rowid, ed.fk_expedition, ed.fk_entrepot, ed.fk_product, ed.fk_unit, ed.description, ed.fk_elementdet, ed.fk_element, ed.element_type, ed.qty, ed.rang';
2420 $sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element_line.' as ed';
2421 $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'product as p ON (p.rowid = ed.fk_product)';
2422 $sql .= ' WHERE ed.fk_expedition = '.((int) $this->id);
2423 $sql .= ' ORDER BY ed.rang, ed.rowid';
2424
2425 dol_syslog(get_class($this)."::fetch_lines_free", LOG_DEBUG);
2426 $result = $this->db->query($sql);
2427 if ($result) {
2428 $num = $this->db->num_rows($result);
2429
2430 $i = 0;
2431 while ($i < $num) {
2432 $objp = $this->db->fetch_object($result);
2433
2434 $line = new ExpeditionLigne($this->db);
2435
2436 $line->rowid = $objp->rowid;
2437 $line->id = $objp->rowid;
2438 $line->fk_expedition = $this->id;
2439 $line->description = $objp->description;
2440 $line->qty = $objp->qty;
2441 $line->fk_entrepot = $objp->fk_entrepot;
2442 $line->fk_product = $objp->fk_product;
2443 $line->rang = $objp->rang;
2444 $line->fk_element = $objp->fk_element;
2445 $line->fk_unit = $objp->fk_unit;
2446 $line->fk_elementdet = $objp->fk_elementdet;
2447 $line->fk_element_type = $objp->element_type;
2448 $line->fetch_optionals();
2449
2450 $this->lines[$i] = $line;
2451
2452 $i++;
2453 }
2454
2455 $this->db->free($result);
2456
2457 return 1;
2458 } else {
2459 $this->error = $this->db->error();
2460 return -3;
2461 }
2462 }
2463
2469 public function getLinesArray()
2470 {
2471 return $this->fetch_lines_free();
2472 }
2473
2481 public function deleteLine($user, $lineid)
2482 {
2483 global $user;
2484
2485 if ($this->status == self::STATUS_DRAFT) {
2486 $this->db->begin();
2487
2488 $line = new ExpeditionLigne($this->db);
2489
2490 // For triggers
2491 $line->fetch($lineid);
2492
2493 if ($line->delete($user) > 0) {
2494 //$this->update_price(1);
2495
2496 $this->db->commit();
2497 return 1;
2498 } else {
2499 $this->db->rollback();
2500 return -1;
2501 }
2502 } else {
2503 $this->error = 'ErrorDeleteLineNotAllowedByObjectStatus';
2504 return -2;
2505 }
2506 }
2507
2508
2515 public function getTooltipContentArray($params)
2516 {
2517 global $conf, $langs;
2518
2519 $langs->load('sendings');
2520
2521 $nofetch = !empty($params['nofetch']);
2522
2523 $datas = array();
2524 $datas['picto'] = img_picto('', $this->picto).' <u class="paddingrightonly">'.$langs->trans("Shipment").'</u>';
2525 if (isset($this->status)) {
2526 $datas['picto'] .= ' '.$this->getLibStatut(5);
2527 }
2528 $datas['ref'] = '<br><b>'.$langs->trans('Ref').':</b> '.$this->ref;
2529 $datas['refcustomer'] = '<br><b>'.$langs->trans('RefCustomer').':</b> '.($this->ref_customer ? $this->ref_customer : $this->ref_client);
2530 if (!$nofetch) {
2531 $langs->load('companies');
2532 if (empty($this->thirdparty)) {
2533 $this->fetch_thirdparty();
2534 }
2535 $datas['customer'] = '<br><b>'.$langs->trans('Customer').':</b> '.$this->thirdparty->getNomUrl(1, '', 0, 1);
2536 }
2537
2538 return $datas;
2539 }
2540
2552 public function getNomUrl($withpicto = 0, $option = '', $max = 0, $short = 0, $notooltip = 0, $save_lastsearch_value = -1)
2553 {
2554 global $langs, $hookmanager;
2555
2556 $result = '';
2557 $params = [
2558 'id' => $this->id,
2559 'objecttype' => $this->element,
2560 'option' => $option,
2561 'nofetch' => 1,
2562 ];
2563 $classfortooltip = 'classfortooltip';
2564 $dataparams = '';
2565 if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
2566 $classfortooltip = 'classforajaxtooltip';
2567 $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
2568 $label = '';
2569 } else {
2570 $label = implode($this->getTooltipContentArray($params));
2571 }
2572
2573 $url = DOL_URL_ROOT.'/expedition/card.php?id='.$this->id;
2574
2575 if ($short) {
2576 return $url;
2577 }
2578
2579 if ($option !== 'nolink') {
2580 // Add param to save lastsearch_values or not
2581 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
2582 if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
2583 $add_save_lastsearch_values = 1;
2584 }
2585 if ($add_save_lastsearch_values) {
2586 $url .= '&save_lastsearch_values=1';
2587 }
2588 }
2589
2590 $linkclose = '';
2591 if (empty($notooltip)) {
2592 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
2593 $label = $langs->trans("Shipment");
2594 $linkclose .= ' alt="'.dolPrintHTMLForAttribute($label).'"';
2595 }
2596 $linkclose .= ($label ? ' title="'.dolPrintHTMLForAttribute($label).'"' : ' title="tocomplete"');
2597 $linkclose .= $dataparams.' class="'.$classfortooltip.'"';
2598 }
2599
2600 $linkstart = '<a href="'.$url.'"';
2601 $linkstart .= $linkclose.'>';
2602 $linkend = '</a>';
2603
2604 $result .= $linkstart;
2605 if ($withpicto) {
2606 $result .= img_object(($notooltip ? '' : $label), ($this->picto ? $this->picto : 'generic'), ($notooltip ? (($withpicto != 2) ? 'class="paddingright"' : '') : 'class="'.(($withpicto != 2) ? 'paddingright ' : '').'"'), 0, 0, $notooltip ? 0 : 1);
2607 }
2608 if ($withpicto != 2) {
2609 $result .= $this->ref;
2610 }
2611 $result .= $linkend;
2612 global $action;
2613 $hookmanager->initHooks(array($this->element . 'dao'));
2614 $parameters = array('id' => $this->id, 'getnomurl' => &$result);
2615 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
2616 if ($reshook > 0) {
2617 $result = $hookmanager->resPrint;
2618 } else {
2619 $result .= $hookmanager->resPrint;
2620 }
2621 return $result;
2622 }
2623
2630 public function getLibStatut($mode = 0)
2631 {
2632 return $this->LibStatut($this->status, $mode);
2633 }
2634
2635 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2643 public function LibStatut($status, $mode)
2644 {
2645 // phpcs:enable
2646 global $langs;
2647
2648 $labelStatus = $langs->transnoentitiesnoconv($this->labelStatus[$status]);
2649 $labelStatusShort = $langs->transnoentitiesnoconv($this->labelStatusShort[$status]);
2650
2651 $statusType = 'status'.$status;
2652 if ($status == self::STATUS_VALIDATED) {
2653 $statusType = 'status4';
2654 }
2655 if ($status == self::STATUS_CLOSED) {
2656 $statusType = 'status6';
2657 }
2658 if ($status == self::STATUS_CANCELED) {
2659 $statusType = 'status9';
2660 }
2661
2662 $signed_label = ' (' . $this->getLibSignedStatus() . ')';
2663 $status_label = $this->signed_status ? $labelStatus . $signed_label : $labelStatus;
2664 $status_label_short = $this->signed_status ? $labelStatusShort . $signed_label : $labelStatusShort;
2665
2666 return dolGetStatus($status_label, $status_label_short, '', $statusType, $mode);
2667 }
2668
2676 public function getKanbanView($option = '', $arraydata = null)
2677 {
2678 global $langs, $conf;
2679
2680 $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
2681
2682 $return = '<div class="box-flex-item box-flex-grow-zero">';
2683 $return .= '<div class="info-box info-box-sm">';
2684 $return .= '<div class="info-box-icon bg-infobox-action">';
2685 $return .= img_picto('', 'order');
2686 $return .= '</div>';
2687 $return .= '<div class="info-box-content">';
2688 $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">' . $this->getNomUrl() . '</span>';
2689 if ($selected >= 0) {
2690 $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
2691 }
2692 $return .= '<br><div class="info-box-ref tdoverflowmax150">'.$this->thirdparty->getNomUrl(1).'</div>';
2693 $return .= '<div class="info-box-ref amount">'.price($this->total_ht, 0, $langs, 0, -1, -1, getDolCurrency()).' '.$langs->trans('HT').'</div>';
2694 $return .= '<div class="info-box-status">'.$this->getLibStatut(3).'</div>';
2695 $return .= '</div>';
2696 $return .= '</div>';
2697 $return .= '</div>';
2698
2699 return $return;
2700 }
2701
2709 public function initAsSpecimen()
2710 {
2711 global $langs;
2712
2713 $now = dol_now();
2714
2715 dol_syslog(get_class($this)."::initAsSpecimen");
2716
2717 $order = new Commande($this->db);
2718 $order->initAsSpecimen();
2719
2720 // Initialise parameters
2721 $this->id = 0;
2722 $this->ref = 'SPECIMEN';
2723 $this->specimen = 1;
2725 $this->livraison_id = 0;
2726 $this->date = $now;
2727 $this->date_creation = $now;
2728 $this->date_valid = $now;
2729 $this->date_delivery = $now + 24 * 3600;
2730 $this->date_expedition = $now + 24 * 3600;
2731
2732 $this->entrepot_id = 0;
2733 $this->fk_delivery_address = 0;
2734 $this->socid = 1;
2735
2736 $this->commande_id = 0;
2737 $this->commande = $order;
2738
2739 $this->origin_id = 1;
2740 $this->origin_type = 'commande';
2741
2742 $this->note_private = 'Private note';
2743 $this->note_public = 'Public note';
2744
2745 $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)
2746 $xnbp = 0;
2747 while ($xnbp < $nbp) {
2748 $line = new ExpeditionLigne($this->db);
2749 $line->product_desc = $langs->trans("Description")." ".$xnbp;
2750 $line->product_label = $langs->trans("Description")." ".$xnbp;
2751 $line->qty = 10;
2752 $line->qty_asked = 5;
2753 $line->qty_shipped = 4;
2754 $line->fk_product = $this->commande->lines[$xnbp]->fk_product;
2755
2756 $line->weight = 1.123456;
2757 $line->weight_units = 0; // kg
2758
2759 $line->volume = 2.34567;
2760 $line->volume_unit = 0;
2761
2762 $this->lines[] = $line;
2763 $xnbp++;
2764 }
2765
2766 return 1;
2767 }
2768
2769 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2778 public function set_date_livraison($user, $delivery_date)
2779 {
2780 // phpcs:enable
2781 return $this->setDeliveryDate($user, $delivery_date);
2782 }
2783
2792 public function setDeliveryDate($user, $delivery_date, $notrigger = 0)
2793 {
2794 if ($user->hasRight('expedition', 'creer')) {
2795 $this->db->begin();
2796
2797 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition";
2798 $sql .= " SET date_delivery = ".($delivery_date ? "'".$this->db->idate($delivery_date)."'" : 'null');
2799 $sql .= " WHERE rowid = ".((int) $this->id);
2800
2801 dol_syslog(get_class($this)."::setDeliveryDate", LOG_DEBUG);
2802 $resql = $this->db->query($sql);
2803 if ($resql) {
2804 $this->date_delivery = $delivery_date;
2805
2806 if (!$notrigger) {
2807 // Call trigger
2808 $result = $this->call_trigger('SHIPPING_MODIFY', $user);
2809 if ($result < 0) {
2810 $this->db->rollback();
2811 return -1;
2812 }
2813 // End call triggers
2814 }
2815
2816 $this->db->commit();
2817 return 1;
2818 } else {
2819 $this->error = $this->db->error();
2820 $this->db->rollback();
2821 return -1;
2822 }
2823 } else {
2824 return -2;
2825 }
2826 }
2827
2836 public function setShippingDate($user, $shipping_date, $notrigger = 0)
2837 {
2838 if ($user->hasRight('expedition', 'creer')) {
2839 $this->db->begin();
2840
2841 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition";
2842 $sql .= " SET date_expedition = ".($shipping_date ? "'".$this->db->idate($shipping_date)."'" : 'null');
2843 $sql .= " WHERE rowid = ".((int) $this->id);
2844
2845 dol_syslog(get_class($this)."::setShippingDate", LOG_DEBUG);
2846 $resql = $this->db->query($sql);
2847 if ($resql) {
2848 $this->date_shipping = $shipping_date;
2849
2850 if (!$notrigger) {
2851 // Call trigger
2852 $result = $this->call_trigger('SHIPPING_MODIFY', $user);
2853 if ($result < 0) {
2854 $this->db->rollback();
2855 return -1;
2856 }
2857 // End call triggers
2858 }
2859
2860 $this->db->commit();
2861 return 1;
2862 } else {
2863 $this->error = $this->db->error();
2864 $this->db->rollback();
2865 return -1;
2866 }
2867 } else {
2868 return -2;
2869 }
2870 }
2871
2872 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2878 public function fetch_delivery_methods()
2879 {
2880 // phpcs:enable
2881 global $langs;
2882 $this->meths = [];
2883
2884 $sql = "SELECT em.rowid, em.code, em.libelle as label";
2885 $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2886 $sql .= " WHERE em.active = 1";
2887 $sql .= " ORDER BY em.libelle ASC";
2888
2889 $resql = $this->db->query($sql);
2890 if ($resql) {
2891 while ($obj = $this->db->fetch_object($resql)) {
2892 $label = $langs->trans('SendingMethod'.$obj->code);
2893 $this->meths[(int) $obj->rowid] = ($label != 'SendingMethod'.$obj->code ? $label : $obj->label);
2894 }
2895 }
2896 }
2897
2898 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2905 public function list_delivery_methods($id = 0)
2906 {
2907 // phpcs:enable
2908 global $langs;
2909
2910 $this->listmeths = [];
2911 $i = 0;
2912
2913 $sql = "SELECT em.rowid, em.code, em.libelle as label, em.description, em.tracking, em.active";
2914 $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2915 if (!empty($id)) {
2916 $sql .= " WHERE em.rowid=".((int) $id);
2917 }
2918
2919 $resql = $this->db->query($sql);
2920 if ($resql) {
2921 while ($obj = $this->db->fetch_object($resql)) {
2922 $label = $langs->trans('SendingMethod'.$obj->code);
2923 $this->listmeths[$i] = [
2924 'rowid' => (int) $obj->rowid,
2925 'code' => $obj->code,
2926 'libelle' => ($label != 'SendingMethod'.$obj->code ? $label : $obj->label),
2927 'description' => $obj->description,
2928 'tracking' => $obj->tracking,
2929 'active' => (int) $obj->active,
2930 ];
2931 $i++;
2932 }
2933 }
2934 }
2935
2942 public function getUrlTrackingStatus($value = '')
2943 {
2944 $tracking = '';
2945 if (!empty($this->shipping_method_id)) {
2946 $sql = "SELECT em.code, em.tracking";
2947 $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2948 $sql .= " WHERE em.rowid = ".((int) $this->shipping_method_id);
2949
2950 $resql = $this->db->query($sql);
2951 if ($resql) {
2952 if ($obj = $this->db->fetch_object($resql)) {
2953 $tracking = (string) $obj->tracking;
2954 }
2955 }
2956 }
2957
2958 if (!empty($tracking) && !empty($value)) {
2959 $url = str_replace('{TRACKID}', $value, $tracking);
2960 $this->tracking_url = sprintf('<a target="_blank" rel="noopener noreferrer" href="%s">%s</a>', $url, $value);
2961 } else {
2962 $this->tracking_url = $value;
2963 }
2964 }
2965
2971 public function setClosed()
2972 {
2973 global $user;
2974
2975 $error = 0;
2976
2977 // Protection. This avoid to move stock later when we should not
2978 if ($this->status == self::STATUS_CLOSED) {
2979 return 0;
2980 }
2981
2982 // We need to validate shipping before closing it
2983 if ($this->status == self::STATUS_DRAFT) {
2984 $this->valid($user);
2985 }
2986
2987 $this->db->begin();
2988
2989 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET fk_statut = ".self::STATUS_CLOSED;
2990 if (empty($this->date_shipping)) { // Date of real shipment was not yet set, we force it on closing
2991 $sql .= ", date_expedition = '".$this->db->escape($this->db->idate(dol_now()))."'";
2992 }
2993 $sql .= " WHERE rowid = ".((int) $this->id)." AND fk_statut > 0";
2994
2995 $resql = $this->db->query($sql);
2996 if ($resql) {
2997 // Set order billed if 100% of order is shipped (qty in shipment lines match qty in order lines)
2998 if ($this->origin_type == 'commande' && $this->origin_id > 0) {
2999 $order = new Commande($this->db);
3000 $order->fetch($this->origin_id);
3001
3002 $order->loadExpeditions(self::STATUS_CLOSED); // Fill $order->expeditions = array(orderlineid => qty)
3003
3004 $shipments_match_order = 1;
3005 foreach ($order->lines as $line) {
3006 $lineid = $line->id;
3007 $qty = $line->qty;
3008 if (($line->product_type == 0 || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) && $order->expeditions[$lineid] != $qty) {
3009 $shipments_match_order = 0;
3010 $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';
3011 dol_syslog($text);
3012 break;
3013 }
3014 }
3015 if ($shipments_match_order) {
3016 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');
3017 // We close the order
3018 $order->cloture($user); // Note this may also create an invoice if module workflow ask it
3019 }
3020 }
3021
3022 $this->statut = self::STATUS_CLOSED; // Will be revert to STATUS_VALIDATED at end if there is a rollback
3023 $this->status = self::STATUS_CLOSED; // Will be revert to STATUS_VALIDATED at end if there is a rollback
3024
3025 // If stock increment is done on closing
3026 if (isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
3027 $result = $this->manageStockMvtOnEvt($user);
3028
3029 if ($result < 0) {
3030 $error++;
3031 }
3032 }
3033
3034 // Call trigger
3035 if (!$error) {
3036 $result = $this->call_trigger('SHIPPING_CLOSED', $user);
3037 if ($result < 0) {
3038 $error++;
3039 }
3040 }
3041 } else {
3042 dol_print_error($this->db);
3043 $error++;
3044 }
3045
3046 if (!$error) {
3047 $this->db->commit();
3048 return 1;
3049 } else {
3050 $this->statut = self::STATUS_VALIDATED;
3052
3053 $this->db->rollback();
3054 return -1;
3055 }
3056 }
3057
3066 private function manageStockMvtOnEvt($user, $labelmovement = 'ShipmentClassifyClosedInDolibarr')
3067 {
3068 global $langs;
3069
3070 $error = 0;
3071
3072 require_once DOL_DOCUMENT_ROOT . '/product/stock/class/mouvementstock.class.php';
3073
3074 $langs->load("agenda");
3075
3076 // Loop on each product line to add a stock movement
3077 $sql = "SELECT";
3078 $sql .= " ed.rowid as edid, ed.fk_product, ed.qty, ed.fk_entrepot";
3079 $sql .= ", cd.rowid as cdid";
3080 $sql .= ", cd.subprice";
3081 $sql .= ", edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock";
3082 $sql .= ", e.ref";
3083 $sql .= " FROM " . $this->db->prefix() . "expeditiondet as ed";
3084 $sql .= " LEFT JOIN " . $this->db->prefix() . "commandedet as cd ON cd.rowid = ed.fk_elementdet";
3085 $sql .= " LEFT JOIN " . $this->db->prefix() . "expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid";
3086 $sql .= " INNER JOIN " . $this->db->prefix() . "expedition as e ON ed.fk_expedition = e.rowid";
3087 $sql .= " WHERE ed.fk_expedition = " . ((int) $this->id);
3088 //$sql .= " AND cd.rowid = ed.fk_elementdet";
3089
3090 dol_syslog(get_class($this) . "::valid select details", LOG_DEBUG);
3091 $resql = $this->db->query($sql);
3092 if ($resql) {
3093 $cpt = $this->db->num_rows($resql);
3094 for ($i = 0; $i < $cpt; $i++) {
3095 $obj = $this->db->fetch_object($resql);
3096 if (empty($obj->edbrowid)) {
3097 $qty = $obj->qty;
3098 } else {
3099 $qty = $obj->edbqty;
3100 }
3101 if ($qty <= 0 || ($qty < 0 && !getDolGlobalInt('SHIPMENT_ALLOW_NEGATIVE_QTY'))) {
3102 continue;
3103 }
3104 dol_syslog(get_class($this) . "::valid movement index " . $i . " ed.rowid=" . $obj->edid . " edb.rowid=" . $obj->edbrowid);
3105
3106 $mouvS = new MouvementStock($this->db);
3107 $mouvS->origin = &$this;
3108 $mouvS->setOrigin($this->element, $this->id, $obj->cdid, $obj->edid);
3109
3110 if (empty($obj->edbrowid)) {
3111 // line without batch detail
3112
3113 // 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
3114 $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, $qty, $obj->subprice, $langs->trans($labelmovement, $obj->ref));
3115 if ($result < 0) {
3116 $this->setErrorsFromObject($mouvS);
3117 $error++;
3118 break;
3119 }
3120 } else {
3121 // line with batch detail
3122
3123 // 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
3124 $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);
3125 if ($result < 0) {
3126 $this->setErrorsFromObject($mouvS);
3127 $error++;
3128 break;
3129 }
3130 }
3131
3132 // 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
3133 // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot.
3134 $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)";
3135 $resqldelete = $this->db->query($sqldelete);
3136 // We do not test error, it can fails if there is child in batch details
3137 }
3138 } else {
3139 $this->error = $this->db->lasterror();
3140 $this->errors[] = $this->db->lasterror();
3141 $error++;
3142 }
3143
3144 if (!$error) {
3145 return 1;
3146 } else {
3147 return -1;
3148 }
3149 }
3150
3156 public function setBilled()
3157 {
3158 global $user;
3159 $error = 0;
3160
3161 $this->db->begin();
3162
3163 $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET billed = 1';
3164 $sql .= " WHERE rowid = ".((int) $this->id).' AND fk_statut > 0';
3165
3166 $resql = $this->db->query($sql);
3167 if ($resql) {
3168 $this->billed = 1;
3169
3170 // Call trigger
3171 $result = $this->call_trigger('SHIPPING_BILLED', $user);
3172 if ($result < 0) {
3173 $this->billed = 0;
3174 $error++;
3175 }
3176 } else {
3177 $error++;
3178 $this->errors[] = $this->db->lasterror;
3179 }
3180
3181 if (empty($error)) {
3182 $this->db->commit();
3183 return 1;
3184 } else {
3185 $this->db->rollback();
3186 return -1;
3187 }
3188 }
3189
3197 public function setDraft($user, $notrigger = 0)
3198 {
3199 // Protection
3200 if ($this->status <= self::STATUS_DRAFT) {
3201 return 0;
3202 }
3203
3204 return $this->setStatusCommon($user, self::STATUS_DRAFT, $notrigger, 'SHIPMENT_UNVALIDATE');
3205 }
3206
3212 public function reOpen()
3213 {
3214 global $langs, $user;
3215
3216 $error = 0;
3217
3218 // Protection. This avoid to move stock later when we should not
3219 if ($this->status == self::STATUS_VALIDATED) {
3220 return 0;
3221 }
3222
3223 $this->db->begin();
3224
3225 $oldbilled = $this->billed;
3226
3227 $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET fk_statut = 1';
3228 $sql .= " WHERE rowid = ".((int) $this->id).' AND fk_statut > 0';
3229
3230 $resql = $this->db->query($sql);
3231 if ($resql) {
3232 $this->statut = self::STATUS_VALIDATED;
3234 $this->billed = 0;
3235
3236 // If stock increment is done on closing
3237 if (isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
3238 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
3239
3240 $langs->load("agenda");
3241
3242 // Loop on each product line to add a stock movement
3243 // TODO possibility to ship from a proposal or other origin
3244 $sql = "SELECT cd.fk_product, cd.subprice,";
3245 $sql .= " ed.rowid, ed.qty, ed.fk_entrepot,";
3246 $sql .= " edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock";
3247 $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,";
3248 $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed";
3249 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid";
3250 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
3251 $sql .= " AND cd.rowid = ed.fk_elementdet";
3252
3253 dol_syslog(get_class($this)."::valid select details", LOG_DEBUG);
3254 $resql = $this->db->query($sql);
3255 if ($resql) {
3256 $cpt = $this->db->num_rows($resql);
3257 for ($i = 0; $i < $cpt; $i++) {
3258 $obj = $this->db->fetch_object($resql);
3259 if (empty($obj->edbrowid)) {
3260 $qty = $obj->qty;
3261 } else {
3262 $qty = $obj->edbqty;
3263 }
3264 if ($qty <= 0) {
3265 continue;
3266 }
3267 dol_syslog(get_class($this)."::reopen expedition movement index ".$i." ed.rowid=".$obj->rowid." edb.rowid=".$obj->edbrowid);
3268
3269 //var_dump($this->lines[$i]);
3270 $mouvS = new MouvementStock($this->db);
3271 $mouvS->origin = &$this;
3272 $mouvS->setOrigin($this->element, $this->id);
3273
3274 if (empty($obj->edbrowid)) {
3275 // line without batch detail
3276
3277 // 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
3278 $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, -$qty, $obj->subprice, $langs->trans("ShipmentUnClassifyCloseddInDolibarr", $this->ref));
3279 if ($result < 0) {
3280 $this->setErrorsFromObject($mouvS);
3281 $error++;
3282 break;
3283 }
3284 } else {
3285 // line with batch detail
3286
3287 // 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
3288 $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);
3289 if ($result < 0) {
3290 $this->setErrorsFromObject($mouvS);
3291 $error++;
3292 break;
3293 }
3294 }
3295 }
3296 } else {
3297 $this->error = $this->db->lasterror();
3298 $error++;
3299 }
3300 }
3301
3302 if (!$error) {
3303 // Call trigger
3304 $result = $this->call_trigger('SHIPPING_REOPEN', $user);
3305 if ($result < 0) {
3306 $error++;
3307 }
3308 }
3309 } else {
3310 $error++;
3311 $this->errors[] = $this->db->lasterror();
3312 }
3313
3314 if (!$error) {
3315 $this->db->commit();
3316 return 1;
3317 } else {
3318 $this->statut = self::STATUS_CLOSED;
3319 $this->status = self::STATUS_CLOSED;
3320 $this->billed = $oldbilled;
3321 $this->db->rollback();
3322 return -1;
3323 }
3324 }
3325
3337 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null)
3338 {
3339 $outputlangs->load("products");
3340
3341 if (!dol_strlen($modele)) {
3342 $modele = 'rouget';
3343
3344 if (!empty($this->model_pdf)) {
3345 $modele = $this->model_pdf;
3346 } elseif (getDolGlobalString('EXPEDITION_ADDON_PDF')) {
3347 $modele = getDolGlobalString('EXPEDITION_ADDON_PDF');
3348 }
3349 }
3350
3351 $modelpath = "core/modules/expedition/doc/";
3352
3353 $this->fetch_origin();
3354
3355 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
3356 }
3357
3366 public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
3367 {
3368 $tables = array(
3369 'expedition'
3370 );
3371
3372 return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
3373 }
3374}
$object ref
Definition info.php:90
Class to manage customers orders.
const STATUS_SHIPMENTONPROCESS
Shipment on process.
const STATUS_VALIDATED
Validated status.
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...
line_order($renum=false, $rowidorder='ASC', $fk_parent_line=true)
Save a new position (field rang) for details lines.
deleteEcmFiles($mode=0)
Delete related files of object in database.
setStatut($status, $elementId=null, $elementType='', $trigkey='', $fieldstatus='')
Set status of an object.
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
updateRangOfLine($rowid, $rang)
Update position of line (rang)
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.
line_max($fk_parent_line=0)
Get max value used for position of line (rang)
fetch_origin()
Read linked origin object.
insertExtraFields($trigger='', $userused=null)
Add/Update all extra fields values for the current object.
delete_linked_contact($source='', $code='')
Delete all links between an object $this and all its contacts in llx_element_contact.
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.
setShippingDate($user, $shipping_date, $notrigger=0)
Set the shipping date.
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
addlinefree($qty, $element_type, $fk_product, $fk_unit, $rang, $description, $fk_parent, $array_options=[])
Add a simple expedition line.
getKanbanView($option='', $arraydata=null)
Return clickable link of object (with eventually picto)
const STATUS_DRAFT
Draft status.
setDeliveryDate($user, $delivery_date, $notrigger=0)
Set the planned delivery date.
line_ajaxorder($rows)
Override to keep every expeditiondet row that belongs to the displayed line in sync when drag & drop ...
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.
updatelinefree($rowid, $qty, $element_type, $fk_product, $fk_unit, $rang, $description, $fk_parent, $notrigger, $array_options=array())
Update a simple expedition line.
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.
fetch_lines_free()
Load lines of simple 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.
getLinesArray()
Create an array of shipment lines.
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:169
getLibSignedStatus(int $mode=0)
Returns the label for signed status.
global $mysoc
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
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:65
dol_now($mode='gmt')
Return date for now.
setEntity($currentobject)
Set entity id to use when to create an object.
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
dol_sanitizeFileName($str, $newstr='_', $unaccent=1, $includequotes=0, $allowdash=0)
Clean a string to use it as a file name.
dol_strlen($string, $stringencoding='UTF-8')
Make a strlen call.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
getDolCurrency()
Return the main currency ('EUR', 'USD', ...)
GETPOSTINT($paramname, $method=0, $nodefault=0)
Return the value of a $_GET or $_POST supervariable, converted into integer.
dol_buildpath($path, $type=0, $returnemptyifnotfound=0)
Return path of url or filesystem.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
isModEnabled($module)
Is Dolibarr module enabled.
get_localtax($vatrate, $local, $thirdparty_buyer=null, $thirdparty_seller=null, $vatnpr=0)
Return localtax rate for a particular VAT rate, when selling a product with vat $vatrate,...
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
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)
img_object($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $allowothertags=array())
Show a picto called object_picto (generic function)
dolGetStatus($statusLabel='', $statusLabelShort='', $html='', $statusType='status0', $displayMode=0, $url='', $params=array())
Output the badge of a status.
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
print $langs trans("Show") . '< td style="' . $timeColor . '" align="center"> s</td > badge status0 badge status4 badge status3 Error badge status8< td align="center">< span class="badge ' . $badge . '"></span ></td >< td align="center">< a href="#" class="button button-small" onclick="openLogModal(this)" data-req="' . dol_escape_htmltag($reqSafe) . '" data-res="' . dol_escape_htmltag($resSafe) . '" data-err="' . dol_escape_htmltag($errSafe) . '">< span class="fa fa-search-plus"></span ></a ></td ></tr >< tr >< td colspan="' . $colspan . '" class="opacitymedium"></td ></tr ></table ></div ></form > logModal none logModal none s a JSON string
calcul_price_total($qty, $pu, $remise_percent_ligne, $txtva, $uselocaltax1_rate, $uselocaltax2_rate, $notused, $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
print $langs trans('Date')." left Ref Label right Qty right Price right TotalHT right TotalTTC right right right right right right right right right centpercent right TotalHT right n right VAT right n right TotalVAT right n No sujeto a RE IRPF right TotalLT1 right n right TotalLT2 right n right TotalTTC right n takeposcustomercurrency takeposcustomercurrency takeposcustomercurrency takeposcustomercurrency right TotalTTC takeposcustomercurrency right takeposcustomercurrency n right Paid right PaymentTypeShortLIQ right SELECT p pos_change as p datep as date
Definition receipt.php:487
$conf db user
Active Directory does not allow anonymous connections.
Definition repair.php:134