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-2026 Nick Fragoulis
19 * Copyright (C) 2026 Pierre Ardoin <developpeur@lesmetiersdubatiment.fr>
20 *
21 * This program is free software; you can redistribute it and/or modify
22 * it under the terms of the GNU General Public License as published by
23 * the Free Software Foundation; either version 3 of the License, or
24 * (at your option) any later version.
25 *
26 * This program is distributed in the hope that it will be useful,
27 * but WITHOUT ANY WARRANTY; without even the implied warranty of
28 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 * GNU General Public License for more details.
30 *
31 * You should have received a copy of the GNU General Public License
32 * along with this program. If not, see <https://www.gnu.org/licenses/>.
33 */
34
41require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
42require_once DOL_DOCUMENT_ROOT."/expedition/class/expeditionligne.class.php";
43require_once DOL_DOCUMENT_ROOT.'/core/class/commonincoterm.class.php';
44if (isModEnabled("propal")) {
45 require_once DOL_DOCUMENT_ROOT.'/comm/propal/class/propal.class.php';
46}
47if (isModEnabled('order')) {
48 require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
49}
50require_once DOL_DOCUMENT_ROOT.'/expedition/class/expeditionlinebatch.class.php';
51require_once DOL_DOCUMENT_ROOT.'/core/class/commonsignedobject.class.php';
52require_once DOL_DOCUMENT_ROOT.'/subtotals/class/commonsubtotal.class.php';
53
60{
61 use CommonIncoterm;
62 use CommonSignedObject;
63 use CommonSubtotal;
64
68 public $element = "shipping";
69
73 public $fk_element = "fk_expedition";
74
78 public $table_element = "expedition";
79
83 public $table_element_line = "expeditiondet";
84
88 public $class_element_line = 'ExpeditionLigne';
89
93 public $picto = 'dolly';
94
95
99 public $fields = array();
100
105 public $user_author_id;
106
111 public $fk_user_author;
112
117 public $fk_user_valid;
118
122 public $socid;
123
129 public $ref_client;
130
134 public $ref_customer;
135
139 public $entrepot_id;
140
144 public $tracking_number;
145
149 public $tracking_url;
153 public $billed = 0;
154
158 public $trueWeight;
162 public $weight_units;
166 public $trueWidth;
170 public $width_units;
174 public $trueHeight;
178 public $height_units;
182 public $trueDepth;
186 public $depth_units;
190 public $trueSize;
191
195 public $livraison_id;
196
200 public $multicurrency_subprice;
201
205 public $size_units;
206
210 public $sizeH;
211
215 public $sizeS;
216
220 public $sizeW;
221
225 public $weight;
226
230 public $date_delivery;
231
237 public $date;
238
244 public $date_expedition;
245
250 public $date_shipping;
251
255 public $date_valid;
256
260 public $meths;
264 public $listmeths; // List of carriers
265
269 public $commande_id;
270
274 public $commande;
275
279 public $lines = array();
280
281 // Multicurrency
285 public $fk_multicurrency;
286
290 public $multicurrency_code;
294 public $multicurrency_tx;
298 public $multicurrency_total_ht;
302 public $multicurrency_total_tva;
306 public $multicurrency_total_ttc;
307
311 const STATUS_DRAFT = 0;
312
320
326 const STATUS_CLOSED = 2;
327
331 const STATUS_CANCELED = -1;
332
341
347 public function __construct($db)
348 {
349 $this->db = $db;
350
351 $this->ismultientitymanaged = 1;
352 $this->isextrafieldmanaged = 1;
353
354 // List of long language codes for status
355 $this->labelStatus = array();
356 $this->labelStatus[-1] = 'StatusSendingCanceled';
357 $this->labelStatus[0] = 'StatusSendingDraft';
358 $this->labelStatus[1] = 'StatusSendingValidated';
359 $this->labelStatus[2] = 'StatusSendingProcessed';
360
361 // List of short language codes for status
362 $this->labelStatusShort = array();
363 $this->labelStatusShort[-1] = 'StatusSendingCanceledShort';
364 $this->labelStatusShort[0] = 'StatusSendingDraftShort';
365 $this->labelStatusShort[1] = 'StatusSendingValidatedShort';
366 $this->labelStatusShort[2] = 'StatusSendingProcessedShort';
367 }
368
375 public function getNextNumRef($soc)
376 {
377 global $langs, $conf;
378 $langs->load("sendings");
379
380 if (getDolGlobalString('EXPEDITION_ADDON_NUMBER')) {
381 $mybool = false;
382
383 $file = getDolGlobalString('EXPEDITION_ADDON_NUMBER') . ".php";
384 $classname = getDolGlobalString('EXPEDITION_ADDON_NUMBER');
385
386 // Include file with class
387 $dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']);
388
389 foreach ($dirmodels as $reldir) {
390 $dir = dol_buildpath($reldir."core/modules/expedition/");
391
392 // Load file with numbering class (if found)
393 $mybool = ((bool) @include_once $dir.$file) || $mybool;
394 }
395
396 if (!$mybool) {
397 dol_print_error(null, "Failed to include file ".$file);
398 return '';
399 }
400
401 $obj = new $classname();
402 '@phan-var-force ModelNumRefExpedition $obj';
404 $numref = $obj->getNextValue($soc, $this);
405
406 if ($numref != "") {
407 return $numref;
408 } else {
409 dol_print_error($this->db, get_class($this)."::getNextNumRef ".$obj->error);
410 return "";
411 }
412 } else {
413 print $langs->trans("Error")." ".$langs->trans("Error_EXPEDITION_ADDON_NUMBER_NotDefined");
414 return "";
415 }
416 }
417
425 public function create($user, $notrigger = 0)
426 {
427 $now = dol_now();
428
429 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
430 $error = 0;
431
432 // Clean parameters
433 $this->tracking_number = dol_sanitizeFileName((string) $this->tracking_number);
434 $this->tracking_number = preg_replace('/\s+/', '', $this->tracking_number);
435
436 if (empty($this->fk_project)) {
437 $this->fk_project = 0;
438 }
439 if (empty($this->date_creation)) {
440 $this->date_creation = $now;
441 }
442 if (empty($this->date_shipping) && !empty($this->date_expedition)) {
443 $this->date_shipping = $this->date_expedition;
444 }
445 $this->entity = setEntity($this);
446
447 $this->user = $user;
448
449 $this->db->begin();
450
451 $sql = "INSERT INTO ".MAIN_DB_PREFIX."expedition (";
452 $sql .= "ref";
453 $sql .= ", entity";
454 $sql .= ", ref_customer";
455 $sql .= ", ref_ext";
456 $sql .= ", date_creation";
457 $sql .= ", fk_user_author";
458 $sql .= ", date_expedition";
459 $sql .= ", date_delivery";
460 $sql .= ", fk_soc";
461 $sql .= ", fk_projet";
462 $sql .= ", fk_address";
463 $sql .= ", fk_shipping_method";
464 $sql .= ", tracking_number";
465 $sql .= ", weight";
466 $sql .= ", size";
467 $sql .= ", width";
468 $sql .= ", height";
469 $sql .= ", weight_units";
470 $sql .= ", size_units";
471 $sql .= ", note_private";
472 $sql .= ", note_public";
473 $sql .= ", model_pdf";
474 $sql .= ", fk_incoterms, location_incoterms";
475 $sql .= ", signed_status";
476 $sql .= ", billed";
477 $sql .= ") VALUES (";
478 $sql .= "'(PROV)'";
479 $sql .= ", ".((int) $this->entity);
480 $sql .= ", ".($this->ref_customer ? "'".$this->db->escape($this->ref_customer)."'" : "null");
481 $sql .= ", ".($this->ref_ext ? "'".$this->db->escape($this->ref_ext)."'" : "null");
482 $sql .= ", '".$this->db->idate($this->date_creation)."'";
483 $sql .= ", ".((int) $user->id);
484 $sql .= ", ".($this->date_shipping > 0 ? "'".$this->db->idate($this->date_shipping)."'" : "null");
485 $sql .= ", ".($this->date_delivery > 0 ? "'".$this->db->idate($this->date_delivery)."'" : "null");
486 $sql .= ", ".($this->socid > 0 ? ((int) $this->socid) : "null");
487 $sql .= ", ".($this->fk_project > 0 ? ((int) $this->fk_project) : "null");
488 $sql .= ", ".($this->fk_delivery_address > 0 ? ((int) $this->fk_delivery_address) : "null");
489 $sql .= ", ".($this->shipping_method_id > 0 ? ((int) $this->shipping_method_id) : "null");
490 $sql .= ", '".$this->db->escape($this->tracking_number)."'";
491 $sql .= ", ".(is_numeric($this->weight) ? (float) $this->weight : 'NULL');
492 $sql .= ", ".(is_numeric($this->sizeS) ? (float) $this->sizeS : 'NULL'); // TODO Should use this->trueDepth
493 $sql .= ", ".(is_numeric($this->sizeW) ? (float) $this->sizeW : 'NULL'); // TODO Should use this->trueWidth
494 $sql .= ", ".(is_numeric($this->sizeH) ? (float) $this->sizeH : 'NULL'); // TODO Should use this->trueHeight
495 $sql .= ", ".($this->weight_units != '' ? (int) $this->weight_units : 'NULL');
496 $sql .= ", ".($this->size_units != '' ? (int) $this->size_units : 'NULL');
497 $sql .= ", ".(!empty($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : "null");
498 $sql .= ", ".(!empty($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : "null");
499 $sql .= ", ".(!empty($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null");
500 $sql .= ", ".((int) $this->fk_incoterms);
501 $sql .= ", '".$this->db->escape($this->location_incoterms)."'";
502 $sql .= ", ".((int) $this->signed_status);
503 $sql .= ", ".((int) $this->billed);
504 $sql .= ")";
505
506 dol_syslog(get_class($this)."::create", LOG_DEBUG);
507 $resql = $this->db->query($sql);
508 if ($resql) {
509 $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX."expedition");
510
511 // update ref
512 $initialref = '(PROV'.$this->id.')';
513 if (!empty($this->ref)) {
514 $initialref = $this->ref;
515 }
516
517 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition";
518 $sql .= " SET ref = '".$this->db->escape($initialref)."'";
519 $sql .= " WHERE rowid = ".((int) $this->id);
520
521 dol_syslog(get_class($this)."::create", LOG_DEBUG);
522 if ($this->db->query($sql)) {
523 $this->ref = $initialref;
524 // Insert of lines
525 $num = count($this->lines);
526 $kits_list = array();
527 if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) {
528 for ($i = 0; $i < $num; $i++) {
529 $objectsrc = new OrderLine($this->db);
530 $objectsrc->fetch($this->lines[$i]->origin_line_id);
531 if ($this->lines[$i]->product_type == "9" && $objectsrc->special_code == SUBTOTALS_SPECIAL_CODE) {
532 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) {
533 $error++;
534 }
535 continue;
536 }
537 if (empty($this->lines[$i]->product_type) || getDolGlobalString('STOCK_SUPPORTS_SERVICES') || getDolGlobalString('SHIPMENT_SUPPORTS_SERVICES')) {
538 // virtual products
539 $line = $this->lines[$i];
540 if ($line->fk_product > 0) {
541 // We group kit lines by the fulfilled order line (fk_elementdet) and not by product id: when a same kit product
542 // is present on several distinct order lines, each order line must keep its own shipment line so the ordered
543 // qty stays consistent with the shipped qty (grouping by product would sum the shipped qty of all lines while
544 // keeping the ordered qty of a single line, resulting in a wrong/negative remaining qty to ship). Several
545 // warehouse pickings of the same order line still share the same fk_elementdet and are grouped together.
546 if (!isset($kits_list[$line->fk_elementdet])) {
547 if (!is_object($line->product)) {
548 $line_product = new Product($this->db);
549 $result = $line_product->fetch($line->fk_product, '', '', '', 1, 1, 1);
550 if ($result <= 0) {
551 $error++;
552 }
553 } else {
554 $line_product = $line->product;
555 }
556
557 // get all children of virtual product
558 $line_product->get_sousproduits_arbo();
559 $prods_arbo = $line_product->get_arbo_each_prod($line->qty);
560 if (count($prods_arbo) > 0) {
561 $kits_list[$line->fk_elementdet] = array(
562 'arbo' => $prods_arbo,
563 'total_qty' => $line->qty,
564 );
565 }
566 } else {
567 $kits_list[$line->fk_elementdet]['total_qty'] += $line->qty;
568 }
569 }
570 }
571 }
572 }
573 $kits_id_cached = array();
574 $sub_kits_id_cached = array();
575 $sub_parents_cached = array();
576 for ($i = 0; $i < $num; $i++) {
577 $line = $this->lines[$i];
578 if (empty($line->product_type) || getDolGlobalString('STOCK_SUPPORTS_SERVICES') || getDolGlobalString('SHIPMENT_SUPPORTS_SERVICES')) {
579 $line_id = 0;
580 if (!isset($kits_id_cached[$line->fk_elementdet])) {
581 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
582 $qty = isset($kits_list[$line->fk_elementdet]) ? $kits_list[$line->fk_elementdet]['total_qty'] : $line->qty;
583 $warehouse_id = (isset($kits_list[$line->fk_elementdet]) && !getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE')) ? 0 : $line->entrepot_id;
584 $line_id = $this->create_line($warehouse_id, $line->origin_line_id, $qty, $line->rang, $line->array_options, 0, $line->fk_product);
585 if ($line_id <= 0) {
586 $error++;
587 }
588 if (isset($kits_list[$line->fk_elementdet])) {
589 $kits_id_cached[$line->fk_elementdet] = $line_id;
590 }
591 } else { // with batch management
592 if ($this->create_line_batch($line, $line->array_options) <= 0) {
593 $error++;
594 }
595 }
596 } else {
597 $line_id = $kits_id_cached[$line->fk_elementdet];
598 }
599
600 // virtual products
601 if (isset($kits_list[$line->fk_elementdet]) && !getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE')) {
602 $prods_arbo = $kits_list[$line->fk_elementdet]['arbo'];
603
604 // get all children of virtual product
605 $parent_line_id = $line_id; // parent line created
606 $level_last = 1;
607 $product_child_id = 0;
608 foreach ($prods_arbo as $index => $product_child_arr) {
609 // 'id' => Id product
610 // 'id_parent' => Id parent product
611 // 'ref' => Ref product
612 // 'nb' => Nb of units that compose parent product
613 // 'nb_total' => // Nb of units for all nb of product
614 // 'stock' => Stock
615 // 'stock_alert' => Stock alert
616 // 'label' => Label
617 // 'fullpath' => // Full path label
618 // 'type' =>
619 // 'desiredstock' => Desired stock
620 // 'level' => Level
621 // 'incdec' => Need to be incremented or decremented
622 // 'entity' => Entity
623 $product_child_level = (int) $product_child_arr['level'];
624 $product_child_incdec = !empty($product_child_arr['incdec']);
625
626 // detect new level
627 if ($product_child_level != $level_last) {
628 $parent_line_id = $line_id; // last line id
629 $parent_product_id = $product_child_id; // last line id
630 if (isset($sub_parents_cached[$line->fk_elementdet][$parent_product_id])) {
631 $parent_line_id = $sub_parents_cached[$line->fk_elementdet][$parent_product_id];
632 } else {
633 $sub_parents_cached[$line->fk_elementdet][$parent_product_id] = $parent_line_id;
634 }
635 }
636
637 // determine if it's a kit : check next level
638 $is_kit = false;
639 $next_level = $product_child_level;
640 $next_index = $index + 1;
641 if (isset($prods_arbo[$next_index])) {
642 $next_level = (int) $prods_arbo[$next_index]['level'];
643 }
644 if ($next_level > $product_child_level) {
645 $is_kit = true;
646 }
647
648 // determine quantity of sub-product
649 $product_child_id = (int) $product_child_arr['id'];
650 $product_child_qty = (float) $product_child_arr['nb_total']; // by default
651 $warehouse_id = $line->entrepot_id; // by default
652 if ($is_kit || !$product_child_incdec) {
653 if (!$product_child_incdec) {
654 $product_child_qty = 0;
655 }
656 $warehouse_id = 0; // no warehouse used for a kit or if stock is not managed (empty incdec)
657 }
658
659 // create line for a child of virtual product
660 if (!isset($sub_kits_id_cached[$line->fk_elementdet][$product_child_id]) || $warehouse_id > 0) {
661 $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);
662 if ($line_id <= 0) {
663 $error++;
664 dol_syslog(__METHOD__ . ' : ' . $this->errorsToString(), LOG_ERR);
665 break;
666 }
667
668 // if kit or not manage stock (empty incdec)
669 if (empty($warehouse_id)) {
670 $sub_kits_id_cached[$line->fk_elementdet][$product_child_id] = $line_id;
671 }
672 }
673
674 $level_last = $product_child_level;
675 }
676 }
677 }
678 }
679
680 if (!$error && $this->id && $this->origin_id) {
681 $ret = $this->add_object_linked();
682 if (!$ret) {
683 $error++;
684 }
685 }
686
687 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
688 $originforcontact = $this->origin_type;
689 $originidforcontact = $this->origin_id;
690
691 $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";
692 $sqlcontact .= " WHERE element_id = ".((int) $originidforcontact)." AND ec.fk_c_type_contact = ctc.rowid AND ctc.element = '".$this->db->escape($originforcontact)."'";
693
694 $resqlcontact = $this->db->query($sqlcontact);
695 if ($resqlcontact) {
696 while ($objcontact = $this->db->fetch_object($resqlcontact)) {
697 $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
698 }
699 } else {
700 dol_print_error($this->db);
701 }
702 }
703
704 // Actions on extra fields
705 if (!$error) {
706 $result = $this->insertExtraFields();
707 if ($result < 0) {
708 $error++;
709 }
710 }
711
712 if (!$error && !$notrigger) {
713 // Call trigger
714 $result = $this->call_trigger('SHIPPING_CREATE', $user);
715 if ($result < 0) {
716 $error++;
717 }
718 // End call triggers
719
720 if (!$error) {
721 $this->db->commit();
722 return $this->id;
723 } else {
724 foreach ($this->errors as $errmsg) {
725 dol_syslog(get_class($this)."::create ".$errmsg, LOG_ERR);
726 $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
727 }
728 $this->db->rollback();
729 return -1 * $error;
730 }
731 } else {
732 $error++;
733 $this->db->rollback();
734 return -3;
735 }
736 } else {
737 $error++;
738 $this->error = $this->db->lasterror()." - sql=$sql";
739 $this->db->rollback();
740 return -2;
741 }
742 } else {
743 $error++;
744 $this->error = $this->db->error()." - sql=$sql";
745 $this->db->rollback();
746 return -1;
747 }
748 }
749
750 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
763 public function create_line($entrepot_id, $origin_line_id, $qty, $rang = 0, $array_options = [], $parent_line_id = 0, $product_id = 0)
764 {
765 //phpcs:enable
766 global $user;
767
768 $expeditionline = new ExpeditionLigne($this->db);
769 $expeditionline->fk_expedition = $this->id;
770 $expeditionline->entrepot_id = $entrepot_id;
771 $expeditionline->fk_elementdet = $origin_line_id;
772 $expeditionline->element_type = $this->origin;
773 $expeditionline->fk_parent = $parent_line_id;
774 $expeditionline->fk_product = $product_id;
775 $expeditionline->qty = $qty;
776 $expeditionline->rang = $rang;
777 $expeditionline->array_options = $array_options;
778
779 if (!($expeditionline->fk_product > 0)) {
780 $order_line = new OrderLine($this->db);
781 $order_line->fetch($expeditionline->fk_elementdet);
782 $expeditionline->fk_product = $order_line->fk_product;
783 }
784
785 if (($lineId = $expeditionline->insert($user)) < 0) {
786 $this->errors[] = $expeditionline->error;
787 }
788 return $lineId;
789 }
790
791
792 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
800 public function create_line_batch($line_ext, $array_options = [])
801 {
802 // phpcs:enable
803 $error = 0;
804 $stockLocationQty = array(); // associated array with batch qty in stock location
805
806 $tab = $line_ext->detail_batch;
807 // create stockLocation Qty array
808 foreach ($tab as $detbatch) {
809 if (!empty($detbatch->fk_warehouse)) {
810 if (empty($stockLocationQty[$detbatch->fk_warehouse])) {
811 $stockLocationQty[$detbatch->fk_warehouse] = 0;
812 }
813 $stockLocationQty[$detbatch->fk_warehouse] += $detbatch->qty;
814 }
815 }
816 // create shipment lines
817 foreach ($stockLocationQty as $stockLocation => $qty) {
818 $line_id = $this->create_line($stockLocation, $line_ext->origin_line_id, $qty, $line_ext->rang, $array_options);
819 if ($line_id < 0) {
820 $error++;
821 } else {
822 // create shipment batch lines for stockLocation
823 foreach ($tab as $detbatch) {
824 if ($detbatch->fk_warehouse == $stockLocation) {
825 if (!($detbatch->create($line_id) > 0)) { // Create an ExpeditionLineBatch
826 $this->errors = $detbatch->errors;
827 $error++;
828 }
829 }
830 }
831 }
832 }
833
834 if (!$error) {
835 return 1;
836 } else {
837 return -1;
838 }
839 }
840
850 public function fetch($id, $ref = '', $ref_ext = '', $notused = '')
851 {
852 global $conf;
853
854 // Check parameters
855 if (empty($id) && empty($ref) && empty($ref_ext)) {
856 return -1;
857 }
858
859 $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";
860 $sql .= ", e.date_valid";
861 $sql .= ", e.weight, e.weight_units, e.size, e.size_units, e.width, e.height";
862 $sql .= ", e.date_expedition as date_expedition, e.model_pdf, e.fk_address, e.date_delivery";
863 $sql .= ", e.fk_shipping_method, e.tracking_number";
864 $sql .= ", e.note_private, e.note_public";
865 $sql .= ', e.fk_incoterms, e.location_incoterms';
866 $sql .= ', e.signed_status';
867 $sql .= ', i.libelle as label_incoterms';
868 $sql .= ', s.libelle as shipping_method';
869 $sql .= ", el.fk_source as origin_id, el.sourcetype as origin_type";
870 $sql .= " FROM ".MAIN_DB_PREFIX."expedition as e";
871 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."element_element as el ON el.fk_target = e.rowid AND el.targettype = '".$this->db->escape($this->element)."'";
872 $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_incoterms as i ON e.fk_incoterms = i.rowid';
873 $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_shipment_mode as s ON e.fk_shipping_method = s.rowid';
874 $sql .= " WHERE e.entity IN (".getEntity('expedition').")";
875 if ($id) {
876 $sql .= " AND e.rowid = ".((int) $id);
877 }
878 if ($ref) {
879 $sql .= " AND e.ref='".$this->db->escape($ref)."'";
880 }
881 if ($ref_ext) {
882 $sql .= " AND e.ref_ext='".$this->db->escape($ref_ext)."'";
883 }
884
885 dol_syslog(get_class($this)."::fetch", LOG_DEBUG);
886 $result = $this->db->query($sql);
887 if ($result) {
888 if ($this->db->num_rows($result)) {
889 $obj = $this->db->fetch_object($result);
890
891 $this->id = (int) $obj->rowid;
892 $this->entity = $obj->entity;
893 $this->ref = $obj->ref;
894 $this->socid = $obj->socid;
895 $this->ref_customer = $obj->ref_customer;
896 $this->ref_ext = $obj->ref_ext;
897 $this->status = $obj->fk_statut;
898 $this->statut = $this->status; // Deprecated
899 $this->signed_status = $obj->signed_status;
900 $this->user_author_id = $obj->fk_user_author;
901 $this->fk_user_author = $obj->fk_user_author;
902 $this->user_creation_id = $obj->fk_user_author;
903 $this->date_creation = $this->db->jdate($obj->date_creation);
904 $this->date_valid = $this->db->jdate($obj->date_valid);
905 $this->date = $this->db->jdate($obj->date_expedition); // TODO deprecated
906 $this->date_expedition = $this->db->jdate($obj->date_expedition); // TODO deprecated
907 $this->date_shipping = $this->db->jdate($obj->date_expedition); // Date real
908 $this->date_delivery = $this->db->jdate($obj->date_delivery); // Date planned
909 $this->fk_delivery_address = $obj->fk_address;
910 $this->model_pdf = $obj->model_pdf;
911 $this->shipping_method_id = $obj->fk_shipping_method;
912 $this->shipping_method = $obj->shipping_method;
913 $this->tracking_number = $obj->tracking_number;
914 $this->origin = ($obj->origin_type ? $obj->origin_type : 'commande'); // For compatibility
915 $this->origin_type = ($obj->origin_type ? $obj->origin_type : 'commande');
916 $this->origin_id = $obj->origin_id;
917 $this->billed = $obj->billed;
918 $this->fk_project = $obj->fk_project;
919 $this->signed_status = $obj->signed_status;
920 $this->trueWeight = $obj->weight;
921 $this->weight_units = $obj->weight_units;
922
923 $this->trueWidth = $obj->width;
924 $this->width_units = $obj->size_units;
925 $this->trueHeight = $obj->height;
926 $this->height_units = $obj->size_units;
927 $this->trueDepth = $obj->size;
928 $this->depth_units = $obj->size_units;
929
930 $this->note_public = $obj->note_public;
931 $this->note_private = $obj->note_private;
932
933 // A denormalized value
934 $this->trueSize = $obj->size."x".$obj->width."x".$obj->height;
935 $this->size_units = $obj->size_units;
936
937 //Incoterms
938 $this->fk_incoterms = $obj->fk_incoterms;
939 $this->location_incoterms = $obj->location_incoterms;
940 $this->label_incoterms = $obj->label_incoterms;
941
942 $this->db->free($result);
943
944 // Tracking url
945 $this->getUrlTrackingStatus($obj->tracking_number);
946
947 // Thirdparty
948 $result = $this->fetch_thirdparty(); // TODO Remove this
949
950 // Retrieve extrafields
951 $this->fetch_optionals();
952
953 // Fix Get multicurrency param for transmitted
954 if (isModEnabled('multicurrency')) {
955 if (!empty($this->multicurrency_code)) {
956 $this->multicurrency_code = $this->thirdparty->multicurrency_code;
957 }
958 if (getDolGlobalString('MULTICURRENCY_USE_ORIGIN_TX') && !empty($this->thirdparty->multicurrency_tx)) {
959 $this->multicurrency_tx = $this->thirdparty->multicurrency_tx;
960 }
961 }
962
963 /*
964 * Lines
965 */
966 if (empty($obj->origin_id)) {
967 $result = $this->fetch_lines_free();
968 } else {
969 $result = $this->fetch_lines();
970 }
971 if ($result < 0) {
972 return -3;
973 }
974
975 return 1;
976 } else {
977 dol_syslog(get_class($this).'::Fetch no expedition found', LOG_ERR);
978 $this->error = 'Shipment with id '.$id.' not found';
979 return 0;
980 }
981 } else {
982 $this->error = $this->db->error();
983 return -1;
984 }
985 }
986
994 public function valid($user, $notrigger = 0)
995 {
996 global $conf;
997
998 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
999
1000 dol_syslog(get_class($this)."::valid");
1001
1002 // Protection
1003 if ($this->status) {
1004 dol_syslog(get_class($this)."::valid not in draft status", LOG_WARNING);
1005 return 0;
1006 }
1007
1008 if (!isset($this->socid)) {
1009 dol_syslog(get_class($this)."::can't valid socid not set", LOG_WARNING);
1010 return 0;
1011 }
1012
1013 if (!((!getDolGlobalString('MAIN_USE_ADVANCED_PERMS') && $user->hasRight('expedition', 'creer'))
1014 || (getDolGlobalString('MAIN_USE_ADVANCED_PERMS') && $user->hasRight('expedition', 'shipping_advance', 'validate')))) {
1015 $this->error = 'Permission denied';
1016 dol_syslog(get_class($this)."::valid ".$this->error, LOG_ERR);
1017 return -1;
1018 }
1019
1020 $this->db->begin();
1021
1022 $error = 0;
1023
1024 // Define new ref
1025 $soc = new Societe($this->db);
1026 $soc->fetch($this->socid);
1027
1028 // Class of company linked to order
1029 $result = $soc->setAsCustomer();
1030
1031 // Define new ref
1032 if (preg_match('/^[\‍(]?PROV/i', $this->ref) || empty($this->ref)) { // empty should not happened, but when it occurs, the test save life
1033 $numref = $this->getNextNumRef($soc);
1034 } elseif (!empty($this->ref)) {
1035 $numref = (string) $this->ref;
1036 } else {
1037 $numref = "EXP".$this->id;
1038 }
1039 $this->newref = dol_sanitizeFileName($numref);
1040
1041 $now = dol_now();
1042
1043 // Validate
1044 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET";
1045 $sql .= " ref = '".$this->db->escape($numref)."'";
1046 $sql .= ", fk_statut = 1";
1047 $sql .= ", date_valid = '".$this->db->idate($now)."'";
1048 $sql .= ", fk_user_valid = ".((int) $user->id);
1049 $sql .= " WHERE rowid = ".((int) $this->id);
1050
1051 dol_syslog(get_class($this)."::valid update expedition", LOG_DEBUG);
1052 $resql = $this->db->query($sql);
1053 if (!$resql) {
1054 $this->error = $this->db->lasterror();
1055 $error++;
1056 }
1057
1058 // If stock increment is done on sending (recommended choice)
1059 if (!$error && isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
1060 $result = $this->manageStockMvtOnEvt($user, "ShipmentValidatedInDolibarr");
1061 if ($result < 0) {
1062 $error++;
1063 }
1064 }
1065
1066 // Change status of order to "shipment in process"
1067 if (!$error) {
1068 $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
1069 if ($this->origin == 'commande') {
1070 $triggerKey.= 'ORDER_SHIPMENTONPROCESS';
1071 } else {
1072 $triggerKey.= strtoupper($this->origin).'_SHIPMENTONPROCESS';
1073 }
1074
1075 // TODO : load the origin object to trigger the right setStatus according to origin object
1076 $ret = $this->setStatut(Commande::STATUS_SHIPMENTONPROCESS, $this->origin_id, $this->origin, $triggerKey);
1077 if (!$ret) {
1078 $error++;
1079 }
1080 }
1081
1082 if (!$error && !$notrigger) {
1083 // Call trigger
1084 $result = $this->call_trigger('SHIPPING_VALIDATE', $user);
1085 if ($result < 0) {
1086 $error++;
1087 }
1088 // End call triggers
1089 }
1090
1091 if (!$error) {
1092 $this->oldref = $this->ref;
1093
1094 // Rename directory if dir was a temporary ref
1095 if (preg_match('/^[\‍(]?PROV/i', $this->ref)) {
1096 // Now we rename also files into index
1097 $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)."'";
1098 $sql .= " WHERE filename LIKE '".$this->db->escape($this->ref)."%' AND filepath = 'expedition/sending/".$this->db->escape($this->ref)."' and entity = ".((int) $conf->entity);
1099 $resql = $this->db->query($sql);
1100 if (!$resql) {
1101 $error++;
1102 $this->error = $this->db->lasterror();
1103 }
1104 $sql = 'UPDATE '.MAIN_DB_PREFIX."ecm_files set filepath = 'expedition/sending/".$this->db->escape($this->newref)."'";
1105 $sql .= " WHERE filepath = 'expedition/sending/".$this->db->escape($this->ref)."' and entity = ".((int) $conf->entity);
1106 $resql = $this->db->query($sql);
1107 if (!$resql) {
1108 $error++;
1109 $this->error = $this->db->lasterror();
1110 }
1111
1112 // We rename directory ($this->ref = old ref, $num = new ref) in order not to lose the attachments
1113 $oldref = dol_sanitizeFileName($this->ref);
1114 $newref = dol_sanitizeFileName($numref);
1115 $dirsource = $conf->expedition->dir_output.'/sending/'.$oldref;
1116 $dirdest = $conf->expedition->dir_output.'/sending/'.$newref;
1117 if (!$error && file_exists($dirsource)) {
1118 dol_syslog(get_class($this)."::valid rename dir ".$dirsource." into ".$dirdest);
1119
1120 if (@rename($dirsource, $dirdest)) {
1121 dol_syslog("Rename ok");
1122 // Rename docs starting with $oldref with $newref
1123 $listoffiles = dol_dir_list($conf->expedition->dir_output.'/sending/'.$newref, 'files', 1, '^'.preg_quote($oldref, '/'));
1124 foreach ($listoffiles as $fileentry) {
1125 $dirsource = $fileentry['name'];
1126 $dirdest = preg_replace('/^'.preg_quote($oldref, '/').'/', $newref, $dirsource);
1127 $dirsource = $fileentry['path'].'/'.$dirsource;
1128 $dirdest = $fileentry['path'].'/'.$dirdest;
1129 @rename($dirsource, $dirdest);
1130 }
1131 }
1132 }
1133 }
1134 }
1135
1136 // Set new ref and current status
1137 if (!$error) {
1138 $this->ref = $numref;
1139 $this->statut = self::STATUS_VALIDATED;
1141 }
1142
1143 if (!$error) {
1144 $this->db->commit();
1145 return 1;
1146 } else {
1147 $this->db->rollback();
1148 return -1 * $error;
1149 }
1150 }
1151
1152 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1159 public function line_ajaxorder($rows)
1160 {
1161 if (empty($rows) || !is_array($rows)) {
1162 return;
1163 }
1164
1165 $rowToOrigin = array();
1166 $originToRows = array();
1167
1168 $sql = "SELECT rowid, fk_elementdet";
1169 $sql .= " FROM ".$this->db->prefix()."expeditiondet";
1170 $sql .= " WHERE fk_expedition = ".((int) $this->id);
1171 $sql .= " ORDER BY rang ASC, rowid ASC";
1172
1173 $resql = $this->db->query($sql);
1174 if ($resql) {
1175 while ($obj = $this->db->fetch_object($resql)) {
1176 $rowid = (int) $obj->rowid;
1177 $originLine = (int) $obj->fk_elementdet;
1178 $rowToOrigin[$rowid] = $originLine;
1179
1180 if (!isset($originToRows[$originLine])) {
1181 $originToRows[$originLine] = array();
1182 }
1183 $originToRows[$originLine][] = $rowid;
1184 }
1185 $this->db->free($resql);
1186 } else {
1187 parent::line_ajaxorder($rows);
1188 return;
1189 }
1190
1191 $processedOrigins = array();
1192 $position = 1;
1193
1194 foreach ($rows as $rowid) {
1195 $rowid = (int) $rowid;
1196 if (empty($rowid)) {
1197 continue;
1198 }
1199
1200 $originLine = isset($rowToOrigin[$rowid]) ? $rowToOrigin[$rowid] : 0;
1201 if ($originLine > 0 && !empty($processedOrigins[$originLine])) {
1202 continue;
1203 }
1204
1205 $rowidsToUpdate = array($rowid);
1206 if ($originLine > 0 && !empty($originToRows[$originLine])) {
1207 $rowidsToUpdate = $originToRows[$originLine];
1208 $processedOrigins[$originLine] = 1;
1209 }
1210
1211 foreach ($rowidsToUpdate as $childRowId) {
1212 $this->updateRangOfLine($childRowId, $position);
1213 $position++;
1214 }
1215 }
1216 }
1217 // phpcs:enable
1218
1219 // phpcs:disable
1226 public function create_delivery($user)
1227 {
1228 // phpcs:enable
1229 if (getDolGlobalInt('MAIN_SUBMODULE_DELIVERY')) {
1230 if ($this->status == self::STATUS_VALIDATED || $this->status == self::STATUS_CLOSED) {
1231 // Expedition validated
1232 include_once DOL_DOCUMENT_ROOT.'/delivery/class/delivery.class.php';
1233 $delivery = new Delivery($this->db);
1234 $result = $delivery->create_from_sending($user, $this->id);
1235 if ($result <= 0) {
1236 $this->setErrorsFromObject($delivery);
1237 }
1238 return $result;
1239 }
1240 }
1241
1242 return 0;
1243 }
1244
1256 private function checkLineStockRequirements($fk_product, $fk_entrepot, $qty, $product_type = null, $forbid_batch_product = false, $allowEmptyWarehouse = false)
1257 {
1258 global $langs;
1259
1260 if (!($fk_product > 0)) {
1261 return 1;
1262 }
1263
1264 $warehouseId = (int) $fk_entrepot;
1265
1266 if (!isModEnabled('stock') && !($forbid_batch_product && isModEnabled('productbatch'))) {
1267 return 1;
1268 }
1269
1270 require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
1271
1272 $product = new Product($this->db);
1273 $result = $product->fetch($fk_product);
1274 if ($result <= 0) {
1275 $this->setErrorsFromObject($product);
1276 return -1;
1277 }
1278
1279 if ($product_type === null) {
1280 $product_type = $product->type;
1281 }
1282
1283 if ($forbid_batch_product && isModEnabled('productbatch') && $product->hasbatch()) {
1284 $langs->load('errors');
1285 $this->error = $langs->trans('ErrorTryToMakeMoveOnProductRequiringBatchData', $product->ref);
1286 $this->errorhidden = 'ErrorTryToMakeMoveOnProductRequiringBatchData';
1287 return -4;
1288 }
1289
1290 if (!isModEnabled('stock')) {
1291 return 1;
1292 }
1293
1294 // An order-independent line may deliberately carry NO warehouse: a sample,
1295 // goods sent out for servicing, or a delivery note issued for e-reporting
1296 // with no stock effect. Empty warehouse = no stock movement, as merged for
1297 // receptions in #39293. Order-based lines keep the requirement.
1298 if (!$allowEmptyWarehouse && !empty($qty) && !($warehouseId > 0) && !getDolGlobalString('STOCK_WAREHOUSE_NOT_REQUIRED_FOR_SHIPMENTS') && !(getDolGlobalString('SHIPMENT_SUPPORTS_SERVICES') && $product_type == Product::TYPE_SERVICE) && $product->stockable_product == Product::ENABLED_STOCK) {
1299 $langs->load("errors");
1300 $this->error = $langs->trans("ErrorWarehouseRequiredIntoShipmentLine");
1301 return -1;
1302 }
1303
1304 // No warehouse, by intent: there is nothing to check stock against.
1305 if ($allowEmptyWarehouse && !($warehouseId > 0)) {
1306 return 1;
1307 }
1308
1309 if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT') && ($qty > 0 || !getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1310 $productChildrenNb = 0;
1311 if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) {
1312 $productChildrenNb = $product->hasFatherOrChild(1);
1313 }
1314 if ($productChildrenNb > 0) {
1315 $product_stock = null;
1316 $product->loadStockForVirtualProduct('warehouseopen', $qty);
1317 if ($warehouseId > 0) {
1318 if (isset($product->stock_warehouse[$warehouseId])) {
1319 $product_stock = $product->stock_warehouse[$warehouseId]->real;
1320 }
1321 } else {
1322 foreach ($product->stock_warehouse as $componentStockWarehouse) {
1323 if ($product_stock === null) {
1324 $product_stock = $componentStockWarehouse->real;
1325 } else {
1326 $product_stock = min($product_stock, $componentStockWarehouse->real);
1327 }
1328 }
1329 }
1330 if ($product_stock === null) {
1331 $product_stock = 0;
1332 }
1333 } else {
1334 // Check must be done for stock of product into warehouse if $fk_entrepot defined
1335 if ($warehouseId > 0) {
1336 $result = $product->load_stock('warehouseopen');
1337 if ($result < 0) {
1338 $this->setErrorsFromObject($product);
1339 return -1;
1340 }
1341 $product_stock = empty($product->stock_warehouse[$warehouseId]) ? 0 : $product->stock_warehouse[$warehouseId]->real;
1342 } else {
1343 $product_stock = $product->stock_reel;
1344 }
1345 }
1346
1347 if ($product->type == Product::TYPE_PRODUCT || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
1348 $isavirtualproduct = ($productChildrenNb > 0);
1349 // The product is qualified for a check of quantity (must be enough in stock to be added into shipment).
1350 if (
1351 !$isavirtualproduct
1352 || !getDolGlobalInt('PRODUIT_SOUSPRODUITS')
1353 || ($isavirtualproduct && !getDolGlobalInt('STOCK_EXCLUDE_VIRTUAL_PRODUCTS'))
1354 ) {
1355 // If STOCK_EXCLUDE_VIRTUAL_PRODUCTS is set, we do not manage stock for kits/virtual products.
1356 if ($product->stockable_product == Product::ENABLED_STOCK && $product_stock < $qty) {
1357 $langs->load("errors");
1358 $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $product->ref);
1359 $this->errorhidden = 'ErrorStockIsNotEnoughToAddProductOnShipment';
1360
1361 return -3;
1362 }
1363 }
1364 }
1365 }
1366
1367 return 1;
1368 }
1369
1384 public function addline($entrepot_id, $id, $qty, $array_options = [], $fk_product = 0, $fk_parent = 0)
1385 {
1386 global $langs;
1387
1388 $num = count($this->lines);
1389 $line = new ExpeditionLigne($this->db);
1390
1391 $line->entrepot_id = $entrepot_id;
1392 $line->origin_line_id = $id;
1393 $line->fk_elementdet = $id;
1394 $line->element_type = 'order';
1395 $line->fk_parent = $fk_parent;
1396 $line->fk_product = $fk_product;
1397 $line->qty = $qty;
1398
1399 $orderline = new OrderLine($this->db);
1400 $orderline->fetch($id);
1401
1402 // Copy the rang of the order line to the expedition line
1403 $line->rang = $orderline->rang;
1404 $line->product_type = $orderline->product_type;
1405 if (!($line->fk_product > 0)) {
1406 $line->fk_product = $orderline->fk_product;
1407 }
1408
1409 if (!empty($orderline->fk_product)) {
1410 $result = $this->checkLineStockRequirements((int) $orderline->fk_product, $entrepot_id, (float) $line->qty, (int) $line->product_type);
1411 if ($result < 0) {
1412 return $result;
1413 }
1414 }
1415
1416 // If product need a batch number, we should not have called this function but addline_batch instead.
1417 // If this happen, we may have a bug in card.php page
1418 if (isModEnabled('productbatch') && !empty($line->fk_product) && !empty($orderline->product_tobatch)) {
1419 $this->error = 'ADDLINE_WAS_CALLED_INSTEAD_OF_ADDLINEBATCH '.$orderline->id.' '.$line->fk_product; //
1420 return -4;
1421 }
1422
1423 // extrafields
1424 if (!getDolGlobalString('MAIN_EXTRAFIELDS_DISABLED') && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used
1425 $line->array_options = $array_options;
1426 }
1427
1428 $this->lines[$num] = $line;
1429
1430 return 1;
1431 }
1432
1447 public function addlinefree($qty, $element_type, $fk_product, $fk_unit, $rang, $description, $fk_parent, $array_options = [], $fk_entrepot = 0)
1448 {
1449 global $mysoc, $langs, $user;
1450
1451 if ($this->status == self::STATUS_DRAFT) {
1452 if (empty($rang)) {
1453 $rang = 0;
1454 }
1455
1456 $qty = (float) price2num($qty);
1457
1458 $result = $this->checkLineStockRequirements((int) $fk_product, $fk_entrepot, (float) $qty, null, true, true);
1459 if ($result < 0) {
1460 return $result;
1461 }
1462
1463 $this->db->begin();
1464
1465 // Rang to use
1466 $ranktouse = $rang;
1467 if ($ranktouse == -1) {
1468 $rangmax = $this->line_max($fk_parent);
1469 $ranktouse = $rangmax + 1;
1470 }
1471
1472 // Insert line
1473 $this->line = new ExpeditionLigne($this->db);
1474 $this->line->fk_expedition = $this->id;
1475 $this->line->element_type = $element_type;
1476 $this->line->fk_product = $fk_product;
1477 $this->line->entrepot_id = (int) $fk_entrepot;
1478 $this->line->fk_entrepot = (int) $fk_entrepot;
1479 $this->line->description = $description;
1480 $this->line->desc = $description;
1481 $this->line->fk_parent = $fk_parent;
1482 $this->line->qty = (float) $qty;
1483 $this->line->fk_unit = $fk_unit;
1484 $this->line->rang = $ranktouse;
1485
1486 if (is_array($array_options) && count($array_options) > 0) {
1487 $this->line->array_options = $array_options;
1488 }
1489
1490 $result = $this->line->insert($user);
1491 if ($result > 0) {
1492 if (!isset($this->context['createfromclone'])) {
1493 if (!empty($fk_parent)) {
1494 $this->line_order(true, 'DESC');
1495 } elseif ($ranktouse > 0 && $ranktouse <= count($this->lines)) {
1496 $linecount = count($this->lines);
1497 for ($ii = $ranktouse; $ii <= $linecount; $ii++) {
1498 $this->updateRangOfLine($this->lines[$ii - 1]->id, $ii + 1);
1499 }
1500 }
1501 $this->lines[] = $this->line;
1502 }
1503
1504 $this->db->commit();
1505 return $this->line->id;
1506 } else {
1507 $this->error = $this->line->error;
1508 dol_syslog(get_class($this)."::addlinefree error=".$this->error, LOG_ERR);
1509 $this->db->rollback();
1510 return -2;
1511 }
1512 } else {
1513 dol_syslog(get_class($this)."::addlinefree status of shipment must be Draft to allow use of ->addlinefree()", LOG_ERR);
1514 return -3;
1515 }
1516 }
1517
1534 public function updatelinefree($rowid, $qty, $element_type, $fk_product, $fk_unit, $rang, $description, $fk_parent, $notrigger, $array_options = array(), $fk_entrepot = -1)
1535 {
1536 global $mysoc, $langs, $user;
1537
1538 if ($this->status == self::STATUS_DRAFT) {
1539 $this->db->begin();
1540
1541 if (empty($qty)) {
1542 $qty = 0;
1543 }
1544 if (empty($rang)) {
1545 $rang = 0;
1546 }
1547
1548 $qty = (float) $qty;
1549 $description = trim($description);
1550
1551 // Fetch current line from the database and then clone the object and set it in $oldline property
1552 $line = new ExpeditionLigne($this->db);
1553
1554 $result = $line->fetch($rowid);
1555 if ($result < 0) {
1556 $this->setErrorsFromObject($line);
1557 $this->db->rollback();
1558 return -1;
1559 }
1560 $result = $line->fetch_optionals();
1561 if ($result < 0) {
1562 $this->setErrorsFromObject($line);
1563 $this->db->rollback();
1564 return -1;
1565 }
1566
1567 // Updating a shipment line does not change its stored product.
1568 $fk_product = (int) $line->fk_product;
1569 if ($fk_entrepot < 0) {
1570 $fk_entrepot = (int) $line->entrepot_id;
1571 }
1572
1573 $result = $this->checkLineStockRequirements((int) $fk_product, $fk_entrepot, (float) $qty, null, true, true);
1574 if ($result < 0) {
1575 $this->db->rollback();
1576 return $result;
1577 }
1578
1579 $staticline = clone $line;
1580
1581 $line->oldline = $staticline;
1582 $this->line = $line;
1583 $this->line->context = $this->context;
1584 $this->line->rang = $rang;
1585 $this->line->fk_expedition = $this->id;
1586 $this->line->element_type = $element_type;
1587 $this->line->fk_product = $fk_product;
1588 $this->line->entrepot_id = (int) $fk_entrepot;
1589 $this->line->fk_entrepot = (int) $fk_entrepot;
1590 $this->line->qty = $qty;
1591 $this->line->fk_unit = $fk_unit;
1592 $this->line->fk_parent = $fk_parent;
1593 $this->line->description = $description;
1594
1595 if (is_array($array_options) && count($array_options) > 0) {
1596 // We replace values in this->line->array_options only for entries defined into $array_options
1597 foreach ($array_options as $key => $value) {
1598 $this->line->array_options[$key] = $array_options[$key];
1599 }
1600 }
1601
1602 $result = $this->line->update($user, $notrigger);
1603 if ($result > 0) {
1604 // Reorder if child line
1605 if (!empty($fk_parent)) {
1606 $this->line_order(true, 'DESC');
1607 }
1608
1609 $this->db->commit();
1610 return $result;
1611 } else {
1612 $this->error = $this->line->error;
1613
1614 $this->db->rollback();
1615 return -1;
1616 }
1617 } else {
1618 $this->error = get_class($this)."::updatelinefree Shipment status makes operation forbidden";
1619 $this->errors = array('ShipmentStatusMakeOperationForbidden');
1620 return -2;
1621 }
1622 }
1623
1624 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1633 public function addline_batch($dbatch, $array_options = [], $origin_line = null)
1634 {
1635 // phpcs:enable
1636 global $langs;
1637
1638 $num = count($this->lines);
1639 $linebatch = null;
1640 if ($dbatch['qty'] > 0 || ($dbatch['qty'] == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1641 $line = new ExpeditionLigne($this->db);
1642 $tab = array();
1643 foreach ($dbatch['detail'] as $key => $value) {
1644 if ($value['q'] > 0 || ($value['q'] == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1645 // $value['q']=qty to move
1646 // $value['id_batch']=id into llx_product_batch of record to move
1647 //var_dump($value);
1648
1649 $linebatch = new ExpeditionLineBatch($this->db);
1650 $ret = $linebatch->fetchFromStock($value['id_batch']); // load serial, sellby, eatby
1651 if ($ret < 0) {
1652 $this->setErrorsFromObject($linebatch);
1653 return -1;
1654 }
1655 $linebatch->qty = $value['q'];
1656 if ($linebatch->qty == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS')) {
1657 $linebatch->batch = null;
1658 }
1659 $tab[] = $linebatch;
1660
1661 if (getDolGlobalString("STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT", '0') && ($linebatch->qty > 0 || !getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1662 require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
1663 $prod_batch = new Productbatch($this->db);
1664 $prod_batch->fetch($value['id_batch']);
1665
1666 if ($prod_batch->qty < $linebatch->qty) {
1667 $langs->load("errors");
1668 $this->errors[] = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $prod_batch->fk_product);
1669 dol_syslog(get_class($this)."::addline_batch error=Product ".$prod_batch->batch.": ".$this->errorsToString(), LOG_ERR);
1670 return -1;
1671 }
1672 }
1673 }
1674 }
1675 if (is_object($linebatch)) {
1676 $line->entrepot_id = $linebatch->fk_warehouse;
1677 }
1678 $line->origin_line_id = $dbatch['ix_l']; // deprecated
1679 $line->fk_elementdet = $dbatch['ix_l'];
1680 $line->qty = $dbatch['qty'];
1681 $line->detail_batch = $tab;
1682 if (!($line->rang > 0)) {
1683 $line->rang = $origin_line->rang;
1684 }
1685 if (!($line->fk_product > 0)) {
1686 $line->fk_product = $origin_line->fk_product;
1687 }
1688
1689 // extrafields
1690 if (!getDolGlobalString('MAIN_EXTRAFIELDS_DISABLED') && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used
1691 $line->array_options = $array_options;
1692 }
1693
1694 //var_dump($line);
1695 $this->lines[$num] = $line;
1696 return 1;
1697 }
1698 return 0;
1699 }
1700
1708 public function update($user = null, $notrigger = 0)
1709 {
1710 global $conf;
1711 $error = 0;
1712
1713 // Clean parameters
1714
1715 if (isset($this->ref)) {
1716 $this->ref = trim($this->ref);
1717 }
1718 if (isset($this->entity)) {
1719 $this->entity = (int) $this->entity;
1720 }
1721 if (isset($this->ref_customer)) {
1722 $this->ref_customer = trim($this->ref_customer);
1723 }
1724 if (isset($this->socid)) {
1725 $this->socid = (int) $this->socid;
1726 }
1727 if (isset($this->fk_user_author)) {
1728 $this->fk_user_author = (int) $this->fk_user_author;
1729 }
1730 if (isset($this->fk_user_valid)) {
1731 $this->fk_user_valid = (int) $this->fk_user_valid;
1732 }
1733 if (isset($this->fk_delivery_address)) {
1734 $this->fk_delivery_address = (int) $this->fk_delivery_address;
1735 }
1736 if (isset($this->shipping_method_id)) {
1737 $this->shipping_method_id = (int) $this->shipping_method_id;
1738 }
1739 if (isset($this->tracking_number)) {
1740 $this->tracking_number = trim($this->tracking_number);
1741 }
1742 if (isset($this->statut)) {
1743 $this->statut = (int) $this->statut;
1744 }
1745 if (isset($this->status)) {
1746 $this->status = (int) $this->status;
1747 }
1748 if (isset($this->trueDepth)) {
1749 $this->trueDepth = trim($this->trueDepth);
1750 }
1751 if (isset($this->trueWidth)) {
1752 $this->trueWidth = trim($this->trueWidth);
1753 }
1754 if (isset($this->trueHeight)) {
1755 $this->trueHeight = trim($this->trueHeight);
1756 }
1757 if (isset($this->size_units)) {
1758 $this->size_units = trim($this->size_units);
1759 }
1760 if (isset($this->weight_units)) {
1761 $this->weight_units = (int) $this->weight_units;
1762 }
1763 if (isset($this->trueWeight)) {
1764 $this->weight = trim((string) $this->trueWeight);
1765 }
1766 if (isset($this->note_private)) {
1767 $this->note_private = trim($this->note_private);
1768 }
1769 if (isset($this->note_public)) {
1770 $this->note_public = trim($this->note_public);
1771 }
1772 if (isset($this->model_pdf)) {
1773 $this->model_pdf = trim($this->model_pdf);
1774 }
1775 if (!empty($this->date_expedition)) {
1776 $this->date_shipping = $this->date_expedition;
1777 }
1778
1779 // Check parameters
1780 // Put here code to add control on parameters values
1781
1782 // Update request
1783 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET";
1784 $sql .= " ref = ".(isset($this->ref) ? "'".$this->db->escape($this->ref)."'" : "null").",";
1785 $sql .= " ref_ext = ".(isset($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null").",";
1786 $sql .= " ref_customer = ".(isset($this->ref_customer) ? "'".$this->db->escape($this->ref_customer)."'" : "null").",";
1787 $sql .= " fk_soc = ".(isset($this->socid) ? ((int) $this->socid) : "null").",";
1788 $sql .= " date_creation = ".(dol_strlen($this->date_creation) != 0 ? "'".$this->db->idate($this->date_creation)."'" : 'null').",";
1789 $sql .= " fk_user_author = ".(isset($this->fk_user_author) ? ((int) $this->fk_user_author) : "null").",";
1790 $sql .= " date_valid = ".(dol_strlen($this->date_valid) != 0 ? "'".$this->db->idate($this->date_valid)."'" : 'null').",";
1791 $sql .= " fk_user_valid = ".(isset($this->fk_user_valid) ? ((int) $this->fk_user_valid) : "null").",";
1792 $sql .= " date_expedition = ".(dol_strlen($this->date_shipping) != 0 ? "'".$this->db->idate($this->date_shipping)."'" : 'null').",";
1793 $sql .= " date_delivery = ".(dol_strlen($this->date_delivery) != 0 ? "'".$this->db->idate($this->date_delivery)."'" : 'null').",";
1794 $sql .= " fk_address = ".(isset($this->fk_delivery_address) ? ((int) $this->fk_delivery_address) : "null").",";
1795 $sql .= " fk_shipping_method = ".((isset($this->shipping_method_id) && $this->shipping_method_id > 0) ? ((int) $this->shipping_method_id) : "null").",";
1796 $sql .= " tracking_number = ".(isset($this->tracking_number) ? "'".$this->db->escape($this->tracking_number)."'" : "null").",";
1797 $sql .= " fk_statut = ".(isset($this->status) ? ((int) $this->status) : "null").",";
1798 $sql .= " fk_projet = ".(isset($this->fk_project) ? ((int) $this->fk_project) : "null").",";
1799 $sql .= " height = ".(($this->trueHeight != '') ? ((float) $this->trueHeight) : "null").",";
1800 $sql .= " width = ".(($this->trueWidth != '') ? ((float) $this->trueWidth) : "null").",";
1801 $sql .= " size_units = ".(isset($this->size_units) ? ((int) $this->size_units) : "null").",";
1802 $sql .= " size = ".(($this->trueDepth != '') ? ((float) $this->trueDepth) : "null").",";
1803 $sql .= " weight_units = ".(isset($this->weight_units) ? ((int) $this->weight_units) : "null").",";
1804 $sql .= " weight = ".(($this->trueWeight != '') ? ((float) $this->trueWeight) : "null").",";
1805 $sql .= " note_private = ".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : "null").",";
1806 $sql .= " note_public = ".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : "null").",";
1807 $sql .= " model_pdf = ".(isset($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null").",";
1808 $sql .= " entity = ".((int) $conf->entity);
1809 $sql .= " WHERE rowid = ".((int) $this->id);
1810
1811 $this->db->begin();
1812
1813 dol_syslog(get_class($this)."::update", LOG_DEBUG);
1814 $resql = $this->db->query($sql);
1815 if (!$resql) {
1816 $error++;
1817 $this->errors[] = "Error ".$this->db->lasterror();
1818 }
1819
1820 // Actions on extra fields
1821 if (!$error) {
1822 $result = $this->insertExtraFields();
1823 if ($result < 0) {
1824 $error++;
1825 }
1826 }
1827
1828 if (!$error && !$notrigger) {
1829 // Call trigger
1830 $result = $this->call_trigger('SHIPPING_MODIFY', $user);
1831 if ($result < 0) {
1832 $error++;
1833 }
1834 // End call triggers
1835 }
1836
1837 // Commit or rollback
1838 if ($error) {
1839 foreach ($this->errors as $errmsg) {
1840 dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR);
1841 $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
1842 }
1843 $this->db->rollback();
1844 return -1 * $error;
1845 } else {
1846 $this->db->commit();
1847 return 1;
1848 }
1849 }
1850
1851
1860 public function cancel($user, $notrigger = 0, $also_update_stock = false)
1861 {
1862 global $conf, $langs, $user;
1863
1864 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1865
1866 $error = 0;
1867 $this->error = '';
1868
1869 $this->db->begin();
1870
1871 // Add a protection to refuse deleting if shipment has at least one delivery
1872 $this->fetchObjectLinked($this->id, 'shipping', 0, 'delivery'); // Get deliveries linked to this shipment
1873 if (isset($this->linkedObjectsIds['delivery']) && count($this->linkedObjectsIds['delivery']) > 0) {
1874 $this->error = 'ErrorThereIsSomeDeliveries';
1875 $error++;
1876 }
1877
1878 if (!$error && !$notrigger) {
1879 // Call trigger
1880 $result = $this->call_trigger('SHIPPING_CANCEL', $user);
1881 if ($result < 0) {
1882 $error++;
1883 }
1884 // End call triggers
1885 }
1886
1887 // Stock control
1888 $can_update_stock = isModEnabled('stock') &&
1889 ((getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') && $this->status > self::STATUS_DRAFT) ||
1890 (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE') && $this->status == self::STATUS_CLOSED && $also_update_stock));
1891 if (!$error) {
1892 require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php";
1893
1894 $langs->load("agenda");
1895
1896 // Loop on each product line to add a stock movement (contain sub-products)
1897 $sql = "SELECT ";
1898 $sql .= " ed.fk_product";
1899 $sql .= ", ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id";
1900 $sql .= ", SUM(".$this->db->ifsql("pa.rowid IS NOT NULL", "1", "0").") as iskit";
1901 $sql .= ", ".$this->db->ifsql("pai.incdec IS NULL", "1", "pai.incdec")." as incdec";
1902 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
1903 $sql .= " LEFT JOIN ".$this->db->prefix()."product_association as pa ON pa.fk_product_pere = ed.fk_product";
1904 $sql .= " LEFT JOIN ".$this->db->prefix()."expeditiondet as edp ON edp.rowid = ed.fk_parent";
1905 $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";
1906 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
1907 $sql .= " GROUP BY ed.fk_product, ed.qty, ed.fk_entrepot, ed.rowid, pai.incdec";
1908 $sql .= $this->db->order("ed.rowid", "DESC");
1909
1910 dol_syslog(get_class($this)."::delete select details", LOG_DEBUG);
1911 $resql = $this->db->query($sql);
1912 if ($resql) {
1913 $cpt = $this->db->num_rows($resql);
1914
1915 $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1916
1917 for ($i = 0; $i < $cpt; $i++) {
1918 dol_syslog(get_class($this)."::delete movement index ".$i);
1919 $obj = $this->db->fetch_object($resql);
1920 $line_id = (int) $obj->expeditiondet_id;
1921
1922 if ($can_update_stock && (empty($obj->iskit) || getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE')) && !empty($obj->incdec)) {
1923 $mouvS = new MouvementStock($this->db);
1924 // we do not log origin because it will be deleted
1925 $mouvS->origin = '';
1926 // get lot/serial
1927 $lotArray = null;
1928 if (isModEnabled('productbatch')) {
1929 $lotArray = $shipmentlinebatch->fetchAll($obj->expeditiondet_id);
1930 if (!is_array($lotArray)) {
1931 $error++;
1932 $this->errors[] = "Error ".$this->db->lasterror();
1933 }
1934 }
1935
1936 if (empty($lotArray)) {
1937 // no lot/serial
1938 // We increment stock of product (and sub-products)
1939 // We use warehouse selected for each line
1940 $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
1941 if ($result < 0) {
1942 $error++;
1943 $this->errors = array_merge($this->errors, $mouvS->errors);
1944 break;
1945 }
1946 } else {
1947 // We increment stock of batches
1948 // We use warehouse selected for each line
1949 foreach ($lotArray as $lot) {
1950 $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
1951 if ($result < 0) {
1952 $error++;
1953 $this->errors = array_merge($this->errors, $mouvS->errors);
1954 break;
1955 }
1956 }
1957 if ($error) {
1958 break; // break for loop in case of error
1959 }
1960 }
1961 }
1962
1963 if (!$error) {
1964 // delete all children and batches of this shipment line
1965 $shipment_line = new ExpeditionLigne($this->db);
1966 $res = $shipment_line->fetch($line_id);
1967 if ($res > 0) {
1968 $result = $shipment_line->delete($user);
1969 if ($result < 0) {
1970 $error++;
1971 $this->errors[] = "Error ".$shipment_line->errorsToString();
1972 }
1973 } else {
1974 $error++;
1975 $this->errors[] = "Error ".$shipment_line->errorsToString();
1976 }
1977 }
1978
1979 if ($error) {
1980 break;
1981 }
1982 }
1983 } else {
1984 $error++;
1985 $this->errors[] = "Error ".$this->db->lasterror();
1986 }
1987 }
1988
1989 // No delete expedition
1990 if (!$error) {
1991 $sql = "SELECT rowid FROM ".$this->db->prefix()."expedition";
1992 $sql .= " WHERE rowid = ".((int) $this->id);
1993
1994 if ($this->db->query($sql)) {
1995 if (!empty($this->origin) && $this->origin_id > 0) {
1996 $this->fetch_origin();
1998 '@phan-var-force Facture|Commande $origin_object';
2000 if ($origin_object->status == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress"
2001 // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress"
2002 $origin_object->loadExpeditions();
2003 if (count($origin_object->expeditions) <= 0) {
2005 }
2006 }
2007 }
2008
2009 $this->db->commit();
2010
2011 // We delete PDFs
2012 $ref = dol_sanitizeFileName($this->ref);
2013 if (!empty($conf->expedition->dir_output)) {
2014 $dir = $conf->expedition->dir_output.'/sending/'.$ref;
2015 $file = $dir.'/'.$ref.'.pdf';
2016 if (file_exists($file)) {
2017 if (!dol_delete_file($file)) {
2018 return 0;
2019 }
2020 }
2021 if (file_exists($dir)) {
2022 if (!dol_delete_dir_recursive($dir)) {
2023 $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir);
2024 return 0;
2025 }
2026 }
2027 }
2028
2029 return 1;
2030 } else {
2031 $this->error = $this->db->lasterror()." - sql=$sql";
2032 $this->db->rollback();
2033 return -3;
2034 }
2035 } else {
2036 $this->db->rollback();
2037 return -2;
2038 }
2039 }
2040
2050 public function delete($user = null, $notrigger = 0, $also_update_stock = false)
2051 {
2052 global $conf, $langs;
2053
2054 if (empty($user)) {
2055 global $user;
2056 }
2057
2058 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
2059
2060 $error = 0;
2061 $this->error = '';
2062
2063 $this->db->begin();
2064
2065 // Add a protection to refuse deleting if shipment has at least one delivery
2066 $this->fetchObjectLinked($this->id, 'shipping', 0, 'delivery'); // Get deliveries linked to this shipment
2067 if (isset($this->linkedObjectsIds['delivery']) && count($this->linkedObjectsIds['delivery']) > 0) {
2068 $this->error = 'ErrorThereIsSomeDeliveries';
2069 $error++;
2070 }
2071
2072 if (!$error && !$notrigger) {
2073 // Call trigger
2074 $result = $this->call_trigger('SHIPPING_DELETE', $user);
2075 if ($result < 0) {
2076 $error++;
2077 }
2078 // End call triggers
2079 }
2080
2081 // Stock control
2082 $can_update_stock = isModEnabled('stock') &&
2083 ((getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') && $this->status > self::STATUS_DRAFT) ||
2084 (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE') && $this->status == self::STATUS_CLOSED && $also_update_stock));
2085 if (!$error) {
2086 require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php";
2087
2088 $langs->load("agenda");
2089
2090 // we try deletion of batch line even if module batch not enabled in case of the module were enabled and disabled previously
2091 $shipmentlinebatch = new ExpeditionLineBatch($this->db);
2092
2093 // Loop on each product line to add a stock movement (contain sub-products)
2094 $sql = "SELECT ";
2095 $sql .= " ed.fk_product";
2096 $sql .= ", ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id";
2097 $sql .= ", SUM(".$this->db->ifsql("pa.rowid IS NOT NULL", "1", "0").") as iskit";
2098 $sql .= ", ".$this->db->ifsql("pai.incdec IS NULL", "1", "pai.incdec")." as incdec";
2099 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
2100 $sql .= " LEFT JOIN ".$this->db->prefix()."product_association as pa ON pa.fk_product_pere = ed.fk_product";
2101 $sql .= " LEFT JOIN ".$this->db->prefix()."expeditiondet as edp ON edp.rowid = ed.fk_parent";
2102 $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";
2103 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
2104 $sql .= " GROUP BY ed.fk_product, ed.qty, ed.fk_entrepot, ed.rowid, pai.incdec";
2105 $sql .= $this->db->order("ed.rowid", "DESC");
2106
2107 dol_syslog(get_class($this)."::delete select details", LOG_DEBUG);
2108 $resql = $this->db->query($sql);
2109 if ($resql) {
2110 $cpt = $this->db->num_rows($resql);
2111 for ($i = 0; $i < $cpt; $i++) {
2112 dol_syslog(get_class($this)."::delete movement index ".$i);
2113 $obj = $this->db->fetch_object($resql);
2114 $line_id = (int) $obj->expeditiondet_id;
2115
2116 if ($can_update_stock && (empty($obj->iskit) || getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE')) && !empty($obj->incdec)) {
2117 $mouvS = new MouvementStock($this->db);
2118 // we do not log origin because it will be deleted
2119 $mouvS->origin = '';
2120 // get lot/serial
2121 $lotArray = $shipmentlinebatch->fetchAll($line_id);
2122 if (!is_array($lotArray)) {
2123 $error++;
2124 $this->errors[] = "Error ".$this->db->lasterror();
2125 }
2126 if (empty($lotArray)) {
2127 // no lot/serial
2128 // We increment stock of product (disable for sub-products : already in shipment lines)
2129 // We use warehouse selected for each line
2130 $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
2131 if ($result < 0) {
2132 $error++;
2133 $this->errors = array_merge($this->errors, $mouvS->errors);
2134 break;
2135 }
2136 } else {
2137 // We increment stock of batches
2138 // We use warehouse selected for each line
2139 foreach ($lotArray as $lot) {
2140 if (empty($lot->qty)) {
2141 continue;
2142 }
2143 $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
2144 if ($result < 0) {
2145 $error++;
2146 $this->errors = array_merge($this->errors, $mouvS->errors);
2147 break;
2148 }
2149 }
2150 if ($error) {
2151 break; // break for loop in case of error
2152 }
2153 }
2154 }
2155
2156 if (!$error) {
2157 // delete all children and batches of this shipment line
2158 $shipment_line = new ExpeditionLigne($this->db);
2159 $res = $shipment_line->fetch($line_id);
2160 if ($res > 0) {
2161 $result = $shipment_line->delete($user);
2162 if ($result < 0) {
2163 $error++;
2164 $this->errors[] = "Error ".$shipment_line->errorsToString();
2165 }
2166 } else {
2167 $error++;
2168 $this->errors[] = "Error ".$shipment_line->errorsToString();
2169 }
2170 }
2171
2172 if ($error) {
2173 break;
2174 }
2175 }
2176 } else {
2177 $error++;
2178 $this->errors[] = "Error ".$this->db->lasterror();
2179 }
2180 }
2181
2182 if (!$error) {
2183 // Delete linked object
2184 $res = $this->deleteObjectLinked();
2185 if ($res < 0) {
2186 $error++;
2187 }
2188
2189 // delete extrafields
2190 $res = $this->deleteExtraFields();
2191 if ($res < 0) {
2192 $error++;
2193 }
2194
2195 if (!$error) {
2196 // Delete linked contacts
2197 $res = $this->delete_linked_contact();
2198 if ($res < 0) {
2199 $error++;
2200 }
2201 }
2202 if (!$error) {
2203 $sql = "DELETE FROM ".$this->db->prefix()."expedition";
2204 $sql .= " WHERE rowid = ".((int) $this->id);
2205
2206 if ($this->db->query($sql)) {
2207 if (!empty($this->origin) && $this->origin_id > 0) {
2208 $this->fetch_origin();
2210 '@phan-var-force Facture|Commande $origin_object';
2211 if ($origin_object->status == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress"
2212 // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress"
2213 $origin_object->loadExpeditions();
2214 if (count($origin_object->expeditions) <= 0) {
2216 }
2217 }
2218 }
2219
2220 $this->db->commit();
2221
2222 // Delete record into ECM index (Note that delete is also done when deleting files with the dol_delete_dir_recursive
2223 $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
2224 $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
2225
2226 // We delete PDFs
2227 $ref = dol_sanitizeFileName($this->ref);
2228 if (!empty($conf->expedition->dir_output) && !empty($ref)) {
2229 $dir = $conf->expedition->dir_output . '/sending/' . $ref;
2230 $file = $dir . '/' . $ref . '.pdf';
2231 if (file_exists($file)) {
2232 if (!dol_delete_file($file)) {
2233 return 0;
2234 }
2235 }
2236 if (file_exists($dir)) {
2237 if (!dol_delete_dir_recursive($dir)) {
2238 $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir);
2239 return 0;
2240 }
2241 }
2242 }
2243
2244 return 1;
2245 } else {
2246 $this->error = $this->db->lasterror()." - sql=$sql";
2247 $this->db->rollback();
2248 return -3;
2249 }
2250 } else {
2251 $this->db->rollback();
2252 return -2;
2253 }
2254 } else {
2255 $this->db->rollback();
2256 return -1;
2257 }
2258 }
2259
2260 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2266 public function fetch_lines()
2267 {
2268 // phpcs:enable
2269 global $mysoc;
2270
2271 $this->lines = array();
2272
2273 // NOTE: This fetch_lines is special because it groups all lines with the same origin_line_id into one line.
2274 // TODO: See if we can restore a common fetch_lines (one line = one record)
2275
2276 $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";
2277 $sql .= ", cd.total_ht, cd.total_localtax1, cd.total_localtax2, cd.total_ttc, cd.total_tva";
2278 $sql .= ", cd.fk_remise_except, cd.fk_product_fournisseur_price as fk_fournprice";
2279 $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";
2280 $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";
2281 $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";
2282 $sql .= ", p.ref as product_ref, p.label as product_label, p.fk_product_type, p.barcode as product_barcode";
2283 $sql .= ", p.weight, p.weight_units, p.length, p.length_units, p.width, p.width_units, p.height, p.height_units";
2284 $sql .= ", p.surface, p.surface_units, p.volume, p.volume_units, p.tosell as product_tosell, p.tobuy as product_tobuy";
2285 $sql .= ", p.tobatch as product_tobatch, p.stockable_product";
2286 $sql .= " FROM ".MAIN_DB_PREFIX."expeditiondet as ed, ".MAIN_DB_PREFIX."commandedet as cd";
2287 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product";
2288 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
2289 $sql .= " AND ed.fk_elementdet = cd.rowid";
2290 $sql .= " ORDER BY CASE WHEN ed.rang IS NULL OR ed.rang = 0 THEN cd.rang ELSE ed.rang END, ed.rowid";
2291
2292 dol_syslog(get_class($this)."::fetch_lines", LOG_DEBUG);
2293 $resql = $this->db->query($sql);
2294 if ($resql) {
2295 include_once DOL_DOCUMENT_ROOT.'/core/lib/price.lib.php';
2296
2297 $num = $this->db->num_rows($resql);
2298 $i = 0;
2299 $line = new ExpeditionLigne($this->db);
2300 $lineindex = 0;
2301 $originline = 0;
2302
2303 $this->total_ht = 0;
2304 $this->total_tva = 0;
2305 $this->total_ttc = 0;
2306 $this->total_localtax1 = 0;
2307 $this->total_localtax2 = 0;
2308
2309 $this->multicurrency_total_ht = 0;
2310 $this->multicurrency_total_tva = 0;
2311 $this->multicurrency_total_ttc = 0;
2312
2313 $shipmentlinebatch = new ExpeditionLineBatch($this->db);
2314
2315 $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.
2316 while ($i < $num) {
2317 $obj = $this->db->fetch_object($resql);
2318
2319 if ($originline > 0 && $originline == $obj->fk_elementdet) {
2320 // '@phan-var-force ExpeditionLigne $line'; // $line from previous loop
2321 $line->entrepot_id = 0; // entrepod_id in details_entrepot
2322 $line->qty_shipped += $obj->qty_shipped;
2323 } else {
2324 $line = new ExpeditionLigne($this->db); // new group to start
2325 $line->entrepot_id = $obj->fk_entrepot; // this is a property of a shipment line
2326 $line->qty_shipped = $obj->qty_shipped; // this is a property of a shipment line
2327 }
2328
2329 $detail_entrepot = new stdClass();
2330 $detail_entrepot->entrepot_id = $obj->fk_entrepot;
2331 $detail_entrepot->qty_shipped = $obj->qty_shipped;
2332 $detail_entrepot->line_id = $obj->line_id;
2333 $line->details_entrepot[] = $detail_entrepot;
2334
2335 $line->line_id = $obj->line_id; // TODO deprecated
2336 $line->rowid = $obj->line_id; // TODO deprecated
2337 $line->id = $obj->line_id;
2338
2339 $line->fk_origin = 'orderline'; // TODO deprecated, we already have element_type that can be use to guess type of line
2340
2341 $line->fk_element = $obj->fk_element;
2342 $line->origin_id = $obj->fk_element;
2343 $line->fk_elementdet = $obj->fk_elementdet;
2344 $line->origin_line_id = $obj->fk_elementdet;
2345 $line->element_type = $obj->element_type;
2346
2347 $line->fk_expedition = $this->id; // id of parent
2348
2349 $line->stockable_product = $obj->stockable_product;
2350 $line->product_type = $obj->product_type;
2351 $line->fk_product = $obj->fk_product;
2352 $line->fk_product_type = $obj->fk_product_type;
2353 $line->ref = $obj->product_ref; // TODO deprecated
2354 $line->product_ref = $obj->product_ref;
2355 $line->product_label = $obj->product_label;
2356 $line->libelle = $obj->product_label; // TODO deprecated
2357 $line->product_barcode = $obj->product_barcode; // Barcode number product
2358 $line->product_tosell = $obj->product_tosell;
2359 $line->product_tobuy = $obj->product_tobuy;
2360 $line->product_tobatch = $obj->product_tobatch;
2361 $line->fk_fournprice = $obj->fk_fournprice;
2362 $line->label = $obj->custom_label;
2363 $line->description = $obj->description;
2364 $line->qty_asked = $obj->qty_asked;
2365 $line->rang = $obj->rang;
2366 $line->weight = $obj->weight;
2367 $line->weight_units = $obj->weight_units;
2368 $line->length = $obj->length;
2369 $line->length_units = $obj->length_units;
2370 $line->width = $obj->width;
2371 $line->width_units = $obj->width_units;
2372 $line->height = $obj->height;
2373 $line->height_units = $obj->height_units;
2374 $line->surface = $obj->surface;
2375 $line->surface_units = $obj->surface_units;
2376 $line->volume = $obj->volume;
2377 $line->volume_units = $obj->volume_units;
2378 $line->stockable_product = $obj->stockable_product;
2379 $line->fk_unit = $obj->fk_unit;
2380
2381 $line->extraparams = !empty($obj->extraparams) ? (array) json_decode($obj->extraparams, true) : array();
2382
2383 $line->pa_ht = $obj->pa_ht;
2384
2385 // Local taxes
2386 $localtax_array = array(0 => $obj->localtax1_type, 1 => $obj->localtax1_tx, 2 => $obj->localtax2_type, 3 => $obj->localtax2_tx);
2387 $localtax1_tx = get_localtax($obj->tva_tx, 1, $this->thirdparty);
2388 $localtax2_tx = get_localtax($obj->tva_tx, 2, $this->thirdparty);
2389
2390 // For invoicing
2391 $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
2392 $line->desc = $obj->description; // We need ->desc because some code into CommonObject use desc (property defined for other elements)
2393 $line->qty = $line->qty_shipped;
2394 $line->total_ht = (float) $tabprice[0];
2395 $line->total_localtax1 = (float) $tabprice[9];
2396 $line->total_localtax2 = (float) $tabprice[10];
2397 $line->total_ttc = (float) $tabprice[2];
2398 $line->total_tva = (float) $tabprice[1];
2399 $line->vat_src_code = $obj->vat_src_code;
2400 $line->tva_tx = $obj->tva_tx;
2401 $line->localtax1_tx = $obj->localtax1_tx;
2402 $line->localtax2_tx = $obj->localtax2_tx;
2403 $line->info_bits = $obj->info_bits;
2404 $line->price = $obj->price;
2405 $line->subprice = $obj->subprice;
2406 $line->fk_remise_except = $obj->fk_remise_except;
2407 $line->remise_percent = $obj->remise_percent;
2408
2409 $this->total_ht += $tabprice[0];
2410 $this->total_tva += $tabprice[1];
2411 $this->total_ttc += $tabprice[2];
2412 $this->total_localtax1 += $tabprice[9];
2413 $this->total_localtax2 += $tabprice[10];
2414
2415 $line->date_start = $this->db->jdate($obj->date_start);
2416 $line->date_end = $this->db->jdate($obj->date_end);
2417
2418 $line->special_code = $obj->special_code;
2419
2420 // Multicurrency
2421 $this->fk_multicurrency = $obj->fk_multicurrency;
2422 $this->multicurrency_code = $obj->multicurrency_code;
2423 $line->multicurrency_subprice = $obj->multicurrency_subprice;
2424 $line->multicurrency_total_ht = $obj->multicurrency_total_ht;
2425 $line->multicurrency_total_tva = $obj->multicurrency_total_tva;
2426 $line->multicurrency_total_ttc = $obj->multicurrency_total_ttc;
2427
2428 $this->multicurrency_total_ht += $obj->multicurrency_total_ht;
2429 $this->multicurrency_total_tva += $obj->multicurrency_total_tva;
2430 $this->multicurrency_total_ttc += $obj->multicurrency_total_ttc;
2431
2432 // Detail of batch
2433 if (isModEnabled('productbatch') && $obj->line_id > 0 && $obj->product_tobatch > 0) {
2434 if ($originline != $obj->fk_elementdet) {
2435 $line->detail_batch = array();
2436 }
2437 $newdetailbatch = $shipmentlinebatch->fetchAll($obj->line_id, $obj->fk_product);
2438
2439 if (is_array($newdetailbatch)) {
2440 if ($originline != $obj->fk_elementdet) {
2441 $line->detail_batch = $newdetailbatch;
2442 } else {
2443 $line->detail_batch = array_merge($line->detail_batch, $newdetailbatch);
2444 }
2445 }
2446 }
2447
2448 // virtual product : find all children stock (group by product id and warehouse id)
2449 if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) {
2450 $detail_children = array(); // detail by product : array of [warehouse_id => total_qty]
2451 $line_child_list = array();
2452 $res = $line->findAllChild($line->id, $line_child_list, 1);
2453 if ($res > 0) {
2454 foreach ($line_child_list as $child_line) {
2455 foreach ($child_line as $child_obj) {
2456 $child_product_id = (int) $child_obj->fk_product;
2457 $child_warehouse_id = (int) $child_obj->fk_warehouse;
2458
2459 if ($child_warehouse_id > 0) {
2460 // child quantities group by warehouses
2461 if (!isset($detail_children[$child_product_id])) {
2462 $detail_children[$child_product_id] = array();
2463 }
2464 if (!isset($detail_children[$child_product_id][$child_warehouse_id])) {
2465 $detail_children[$child_product_id][$child_warehouse_id] = 0;
2466 }
2467 $detail_children[$child_product_id][$child_warehouse_id] += $child_obj->qty;
2468 }
2469 }
2470 }
2471 }
2472 $line->detail_children = $detail_children;
2473 }
2474
2475 $line->fetch_optionals();
2476
2477 if ($originline != $obj->fk_elementdet) {
2478 $this->lines[$lineindex] = $line;
2479 $lineindex++;
2480 } else {
2481 $line->total_ht += $tabprice[0];
2482 $line->total_localtax1 += $tabprice[9];
2483 $line->total_localtax2 += $tabprice[10];
2484 $line->total_ttc += $tabprice[2];
2485 $line->total_tva += $tabprice[1];
2486 }
2487
2488 $i++;
2489 $originline = $obj->fk_elementdet;
2490 }
2491 $this->db->free($resql);
2492 return 1;
2493 } else {
2494 $this->error = $this->db->error();
2495 return -3;
2496 }
2497 }
2498
2499
2500 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2506 public function fetch_lines_free()
2507 {
2508 // phpcs:enable
2509 global $mysoc;
2510
2511 $this->lines = array();
2512
2513 $sql = 'SELECT ed.rowid, ed.fk_expedition, ed.fk_entrepot, ed.fk_product, ed.fk_parent, ed.fk_unit, ed.description, ed.fk_elementdet, ed.fk_element, ed.element_type, ed.qty, ed.rang';
2514 $sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element_line.' as ed';
2515 $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'product as p ON (p.rowid = ed.fk_product)';
2516 $sql .= ' WHERE ed.fk_expedition = '.((int) $this->id);
2517 $sql .= ' ORDER BY ed.rang, ed.rowid';
2518
2519 dol_syslog(get_class($this)."::fetch_lines_free", LOG_DEBUG);
2520 $result = $this->db->query($sql);
2521 if ($result) {
2522 $num = $this->db->num_rows($result);
2523
2524 $i = 0;
2525 while ($i < $num) {
2526 $objp = $this->db->fetch_object($result);
2527
2528 $line = new ExpeditionLigne($this->db);
2529
2530 $line->rowid = $objp->rowid;
2531 $line->id = $objp->rowid;
2532 $line->fk_expedition = $this->id;
2533 $line->description = $objp->description;
2534 $line->desc = $objp->description;
2535 $line->qty = $objp->qty;
2536 $line->fk_entrepot = $objp->fk_entrepot;
2537 $line->entrepot_id = $objp->fk_entrepot;
2538 $line->fk_product = $objp->fk_product;
2539 $line->fk_parent = $objp->fk_parent;
2540 $line->rang = $objp->rang;
2541 $line->fk_element = $objp->fk_element;
2542 $line->fk_unit = $objp->fk_unit;
2543 $line->fk_elementdet = $objp->fk_elementdet;
2544 $line->element_type = $objp->element_type;
2545 $line->fetch_optionals();
2546
2547 $this->lines[$i] = $line;
2548
2549 $i++;
2550 }
2551
2552 $this->db->free($result);
2553
2554 return 1;
2555 } else {
2556 $this->error = $this->db->error();
2557 return -3;
2558 }
2559 }
2560
2566 public function getLinesArray()
2567 {
2568 return $this->fetch_lines_free();
2569 }
2570
2578 public function deleteLine($user, $lineid)
2579 {
2580 global $user;
2581
2582 if ($this->status == self::STATUS_DRAFT) {
2583 $this->db->begin();
2584
2585 $line = new ExpeditionLigne($this->db);
2586
2587 // For triggers
2588 $line->fetch($lineid);
2589
2590 if ($this->id > 0 && (int) $line->fk_expedition !== (int) $this->id) {
2591 $this->db->rollback();
2592 $this->error = 'ErrorLineIDDoesNotMatchWithObjectID';
2593 return -1;
2594 }
2595
2596 if ($line->delete($user) > 0) {
2597 //$this->update_price(1);
2598
2599 $this->db->commit();
2600 return 1;
2601 } else {
2602 $this->db->rollback();
2603 return -1;
2604 }
2605 } else {
2606 $this->error = 'ErrorDeleteLineNotAllowedByObjectStatus';
2607 return -2;
2608 }
2609 }
2610
2611
2618 public function getTooltipContentArray($params)
2619 {
2620 global $conf, $langs;
2621
2622 $langs->load('sendings');
2623
2624 $nofetch = !empty($params['nofetch']);
2625
2626 $datas = array();
2627 $datas['picto'] = img_picto('', $this->picto).' <u class="paddingrightonly">'.$langs->trans("Shipment").'</u>';
2628 if (isset($this->status)) {
2629 $datas['picto'] .= ' '.$this->getLibStatut(5);
2630 }
2631 $datas['ref'] = '<br><b>'.$langs->trans('Ref').':</b> '.$this->ref;
2632 $datas['refcustomer'] = '<br><b>'.$langs->trans('RefCustomer').':</b> '.($this->ref_customer ? $this->ref_customer : $this->ref_client);
2633 if (!$nofetch) {
2634 $langs->load('companies');
2635 if (empty($this->thirdparty)) {
2636 $this->fetch_thirdparty();
2637 }
2638 $datas['customer'] = '<br><b>'.$langs->trans('Customer').':</b> '.$this->thirdparty->getNomUrl(1, '', 0, 1);
2639 }
2640
2641 return $datas;
2642 }
2643
2656 public function getNomUrl($withpicto = 0, $option = '', $max = 0, $short = 0, $notooltip = 0, $save_lastsearch_value = -1, $addlinktonotes = 0)
2657 {
2658 global $langs, $hookmanager, $user;
2659
2660 $result = '';
2661 $params = [
2662 'id' => $this->id,
2663 'objecttype' => $this->element,
2664 'option' => $option,
2665 'nofetch' => 1,
2666 ];
2667 $classfortooltip = 'classfortooltip';
2668 $dataparams = '';
2669 if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
2670 $classfortooltip = 'classforajaxtooltip';
2671 $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
2672 $label = '';
2673 } else {
2674 $label = implode($this->getTooltipContentArray($params));
2675 }
2676
2677 $url = DOL_URL_ROOT.'/expedition/card.php?id='.$this->id;
2678
2679 if ($short) {
2680 return $url;
2681 }
2682
2683 if ($option !== 'nolink') {
2684 // Add param to save lastsearch_values or not
2685 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
2686 if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
2687 $add_save_lastsearch_values = 1;
2688 }
2689 if ($add_save_lastsearch_values) {
2690 $url .= '&save_lastsearch_values=1';
2691 }
2692 }
2693
2694 $linkclose = '';
2695 if (empty($notooltip)) {
2696 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
2697 $label = $langs->trans("Shipment");
2698 $linkclose .= ' alt="'.dolPrintHTMLForAttribute($label).'"';
2699 }
2700 $linkclose .= ($label ? ' title="'.dolPrintHTMLForAttribute($label).'"' : ' title="tocomplete"');
2701 $linkclose .= $dataparams.' class="'.$classfortooltip.'"';
2702 }
2703
2704 $linkstart = '<a href="'.$url.'"';
2705 $linkstart .= $linkclose.'>';
2706 $linkend = '</a>';
2707
2708 $result .= $linkstart;
2709 if ($withpicto) {
2710 $result .= img_object(($notooltip ? '' : $label), ($this->picto ? $this->picto : 'generic'), ($notooltip ? (($withpicto != 2) ? 'class="paddingright"' : '') : 'class="'.(($withpicto != 2) ? 'paddingright ' : '').'"'), 0, 0, $notooltip ? 0 : 1);
2711 }
2712 if ($withpicto != 2) {
2713 $result .= $this->ref;
2714 }
2715 $result .= $linkend;
2716
2717 if ($addlinktonotes) {
2718 $txttoshow = ($user->socid > 0 ? $this->note_public : $this->note_private);
2719 if ($txttoshow) {
2720 $notetoshow = $langs->trans("ViewPrivateNote").':<br>'.dol_string_nohtmltag($txttoshow, 1);
2721 $result .= ' <span class="note inline-block">';
2722 $result .= '<a href="'.DOL_URL_ROOT.'/expedition/note.php?id='.$this->id.'" class="classfortooltip" title="'.dol_escape_htmltag($notetoshow).'">';
2723 $result .= img_picto('', 'note');
2724 $result .= '</a>';
2725 $result .= '</span>';
2726 }
2727 }
2728
2729 global $action;
2730 $hookmanager->initHooks(array($this->element . 'dao'));
2731 $parameters = array('id' => $this->id, 'getnomurl' => &$result);
2732 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
2733 if ($reshook > 0) {
2734 $result = $hookmanager->resPrint;
2735 } else {
2736 $result .= $hookmanager->resPrint;
2737 }
2738 return $result;
2739 }
2740
2747 public function getLibStatut($mode = 0)
2748 {
2749 return $this->LibStatut($this->status, $mode);
2750 }
2751
2752 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2760 public function LibStatut($status, $mode)
2761 {
2762 // phpcs:enable
2763 global $langs;
2764
2765 $labelStatus = $langs->transnoentitiesnoconv($this->labelStatus[$status]);
2766 $labelStatusShort = $langs->transnoentitiesnoconv($this->labelStatusShort[$status]);
2767
2768 $statusType = 'status'.$status;
2769 if ($status == self::STATUS_VALIDATED) {
2770 $statusType = 'status4';
2771 }
2772 if ($status == self::STATUS_CLOSED) {
2773 $statusType = 'status6';
2774 }
2775 if ($status == self::STATUS_CANCELED) {
2776 $statusType = 'status9';
2777 }
2778
2779 $signed_label = ' (' . $this->getLibSignedStatus() . ')';
2780 $status_label = $this->signed_status ? $labelStatus . $signed_label : $labelStatus;
2781 $status_label_short = $this->signed_status ? $labelStatusShort . $signed_label : $labelStatusShort;
2782
2783 return dolGetStatus($status_label, $status_label_short, '', $statusType, $mode);
2784 }
2785
2793 public function getKanbanView($option = '', $arraydata = null)
2794 {
2795 global $langs, $conf;
2796
2797 $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
2798
2799 $return = '<div class="box-flex-item box-flex-grow-zero">';
2800 $return .= '<div class="info-box info-box-sm">';
2801 $return .= '<div class="info-box-icon bg-infobox-action">';
2802 $return .= img_picto('', 'order');
2803 $return .= '</div>';
2804 $return .= '<div class="info-box-content">';
2805 $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">' . $this->getNomUrl() . '</span>';
2806 if ($selected >= 0) {
2807 $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
2808 }
2809 $return .= '<br><div class="info-box-ref tdoverflowmax150">'.$this->thirdparty->getNomUrl(1).'</div>';
2810 $return .= '<div class="info-box-ref amount">'.price($this->total_ht, 0, $langs, 0, -1, -1, getDolCurrency()).' '.$langs->trans('HT').'</div>';
2811 $return .= '<div class="info-box-status">'.$this->getLibStatut(3).'</div>';
2812 $return .= '</div>';
2813 $return .= '</div>';
2814 $return .= '</div>';
2815
2816 return $return;
2817 }
2818
2826 public function initAsSpecimen()
2827 {
2828 global $langs;
2829
2830 $now = dol_now();
2831
2832 dol_syslog(get_class($this)."::initAsSpecimen");
2833
2834 $order = new Commande($this->db);
2835 $order->initAsSpecimen();
2836
2837 // Initialise parameters
2838 $this->id = 0;
2839 $this->ref = 'SPECIMEN';
2840 $this->specimen = 1;
2842 $this->livraison_id = 0;
2843 $this->date = $now;
2844 $this->date_creation = $now;
2845 $this->date_valid = $now;
2846 $this->date_delivery = $now + 24 * 3600;
2847 $this->date_expedition = $now + 24 * 3600;
2848
2849 $this->entrepot_id = 0;
2850 $this->fk_delivery_address = 0;
2851 $this->socid = 1;
2852
2853 $this->commande_id = 0;
2854 $this->commande = $order;
2855
2856 $this->origin_id = 1;
2857 $this->origin_type = 'commande';
2858
2859 $this->note_private = 'Private note';
2860 $this->note_public = 'Public note';
2861
2862 $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)
2863 $xnbp = 0;
2864 while ($xnbp < $nbp) {
2865 $line = new ExpeditionLigne($this->db);
2866 $line->product_desc = $langs->trans("Description")." ".$xnbp;
2867 $line->product_label = $langs->trans("Description")." ".$xnbp;
2868 $line->qty = 10;
2869 $line->qty_asked = 5;
2870 $line->qty_shipped = 4;
2871 $line->fk_product = $this->commande->lines[$xnbp]->fk_product;
2872
2873 $line->weight = 1.123456;
2874 $line->weight_units = 0; // kg
2875
2876 $line->volume = 2.34567;
2877 $line->volume_unit = 0;
2878
2879 $this->lines[] = $line;
2880 $xnbp++;
2881 }
2882
2883 return 1;
2884 }
2885
2886 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2895 public function set_date_livraison($user, $delivery_date)
2896 {
2897 // phpcs:enable
2898 return $this->setDeliveryDate($user, $delivery_date);
2899 }
2900
2909 public function setDeliveryDate($user, $delivery_date, $notrigger = 0)
2910 {
2911 if ($user->hasRight('expedition', 'creer')) {
2912 $this->db->begin();
2913
2914 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition";
2915 $sql .= " SET date_delivery = ".($delivery_date ? "'".$this->db->idate($delivery_date)."'" : 'null');
2916 $sql .= " WHERE rowid = ".((int) $this->id);
2917
2918 dol_syslog(get_class($this)."::setDeliveryDate", LOG_DEBUG);
2919 $resql = $this->db->query($sql);
2920 if ($resql) {
2921 $this->date_delivery = $delivery_date;
2922
2923 if (!$notrigger) {
2924 // Call trigger
2925 $result = $this->call_trigger('SHIPPING_MODIFY', $user);
2926 if ($result < 0) {
2927 $this->db->rollback();
2928 return -1;
2929 }
2930 // End call triggers
2931 }
2932
2933 $this->db->commit();
2934 return 1;
2935 } else {
2936 $this->error = $this->db->error();
2937 $this->db->rollback();
2938 return -1;
2939 }
2940 } else {
2941 return -2;
2942 }
2943 }
2944
2953 public function setShippingDate($user, $shipping_date, $notrigger = 0)
2954 {
2955 if ($user->hasRight('expedition', 'creer')) {
2956 $this->db->begin();
2957
2958 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition";
2959 $sql .= " SET date_expedition = ".($shipping_date ? "'".$this->db->idate($shipping_date)."'" : 'null');
2960 $sql .= " WHERE rowid = ".((int) $this->id);
2961
2962 dol_syslog(get_class($this)."::setShippingDate", LOG_DEBUG);
2963 $resql = $this->db->query($sql);
2964 if ($resql) {
2965 $this->date_shipping = $shipping_date;
2966
2967 if (!$notrigger) {
2968 // Call trigger
2969 $result = $this->call_trigger('SHIPPING_MODIFY', $user);
2970 if ($result < 0) {
2971 $this->db->rollback();
2972 return -1;
2973 }
2974 // End call triggers
2975 }
2976
2977 $this->db->commit();
2978 return 1;
2979 } else {
2980 $this->error = $this->db->error();
2981 $this->db->rollback();
2982 return -1;
2983 }
2984 } else {
2985 return -2;
2986 }
2987 }
2988
2989 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2995 public function fetch_delivery_methods()
2996 {
2997 // phpcs:enable
2998 global $langs;
2999 $this->meths = [];
3000
3001 $sql = "SELECT em.rowid, em.code, em.libelle as label";
3002 $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
3003 $sql .= " WHERE em.active = 1";
3004 $sql .= " ORDER BY em.libelle ASC";
3005
3006 $resql = $this->db->query($sql);
3007 if ($resql) {
3008 while ($obj = $this->db->fetch_object($resql)) {
3009 $label = $langs->trans('SendingMethod'.$obj->code);
3010 $this->meths[(int) $obj->rowid] = ($label != 'SendingMethod'.$obj->code ? $label : $obj->label);
3011 }
3012 }
3013 }
3014
3015 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3022 public function list_delivery_methods($id = 0)
3023 {
3024 // phpcs:enable
3025 global $langs;
3026
3027 $this->listmeths = [];
3028 $i = 0;
3029
3030 $sql = "SELECT em.rowid, em.code, em.libelle as label, em.description, em.tracking, em.active";
3031 $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
3032 if (!empty($id)) {
3033 $sql .= " WHERE em.rowid=".((int) $id);
3034 }
3035
3036 $resql = $this->db->query($sql);
3037 if ($resql) {
3038 while ($obj = $this->db->fetch_object($resql)) {
3039 $label = $langs->trans('SendingMethod'.$obj->code);
3040 $this->listmeths[$i] = [
3041 'rowid' => (int) $obj->rowid,
3042 'code' => $obj->code,
3043 'libelle' => ($label != 'SendingMethod'.$obj->code ? $label : $obj->label),
3044 'description' => $obj->description,
3045 'tracking' => $obj->tracking,
3046 'active' => (int) $obj->active,
3047 ];
3048 $i++;
3049 }
3050 }
3051 }
3052
3059 public function getUrlTrackingStatus($value = '')
3060 {
3061 $tracking = '';
3062 if (!empty($this->shipping_method_id)) {
3063 $sql = "SELECT em.code, em.tracking";
3064 $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
3065 $sql .= " WHERE em.rowid = ".((int) $this->shipping_method_id);
3066
3067 $resql = $this->db->query($sql);
3068 if ($resql) {
3069 if ($obj = $this->db->fetch_object($resql)) {
3070 $tracking = (string) $obj->tracking;
3071 }
3072 }
3073 }
3074
3075 if (!empty($tracking) && !empty($value)) {
3076 $url = str_replace('{TRACKID}', $value, $tracking);
3077 $this->tracking_url = sprintf('<a target="_blank" rel="noopener noreferrer" href="%s">%s</a>', $url, $value);
3078 } else {
3079 $this->tracking_url = $value;
3080 }
3081 }
3082
3088 public function setClosed()
3089 {
3090 global $user;
3091
3092 $error = 0;
3093
3094 // Protection. This avoid to move stock later when we should not
3095 if ($this->status == self::STATUS_CLOSED) {
3096 return 0;
3097 }
3098
3099 // We need to validate shipping before closing it
3100 if ($this->status == self::STATUS_DRAFT) {
3101 $this->valid($user);
3102 }
3103
3104 $this->db->begin();
3105
3106 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET fk_statut = ".self::STATUS_CLOSED;
3107 if (empty($this->date_shipping)) { // Date of real shipment was not yet set, we force it on closing
3108 $sql .= ", date_expedition = '".$this->db->escape($this->db->idate(dol_now()))."'";
3109 }
3110 $sql .= " WHERE rowid = ".((int) $this->id)." AND fk_statut > 0";
3111
3112 $resql = $this->db->query($sql);
3113 if ($resql) {
3114 // Set order billed if 100% of order is shipped (qty in shipment lines match qty in order lines)
3115 if ($this->origin_type == 'commande' && $this->origin_id > 0) {
3116 $order = new Commande($this->db);
3117 $order->fetch($this->origin_id);
3118
3119 $order->loadExpeditions(self::STATUS_CLOSED); // Fill $order->expeditions = array(orderlineid => qty)
3120
3121 $shipments_match_order = 1;
3122 foreach ($order->lines as $line) {
3123 $lineid = $line->id;
3124 $qty = $line->qty;
3125 if (($line->product_type == 0 || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) && $order->expeditions[$lineid] != $qty) {
3126 $shipments_match_order = 0;
3127 $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';
3128 dol_syslog($text);
3129 break;
3130 }
3131 }
3132 if ($shipments_match_order) {
3133 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');
3134 // We close the order
3135 $order->cloture($user, 0, 0); // 0 = do not check the close permission: this is an automatic action of the shipment closing. Note this may also create an invoice if module workflow ask it
3136 }
3137 }
3138
3139 $this->statut = self::STATUS_CLOSED; // Will be revert to STATUS_VALIDATED at end if there is a rollback
3140 $this->status = self::STATUS_CLOSED; // Will be revert to STATUS_VALIDATED at end if there is a rollback
3141
3142 // If stock increment is done on closing
3143 if (isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
3144 $result = $this->manageStockMvtOnEvt($user);
3145
3146 if ($result < 0) {
3147 $error++;
3148 }
3149 }
3150
3151 // Call trigger
3152 if (!$error) {
3153 $result = $this->call_trigger('SHIPPING_CLOSED', $user);
3154 if ($result < 0) {
3155 $error++;
3156 }
3157 }
3158 } else {
3159 dol_print_error($this->db);
3160 $error++;
3161 }
3162
3163 if (!$error) {
3164 $this->db->commit();
3165 return 1;
3166 } else {
3167 $this->statut = self::STATUS_VALIDATED;
3169
3170 $this->db->rollback();
3171 return -1;
3172 }
3173 }
3174
3184 private function manageStockMvtOnEvt($user, $labelmovement = 'ShipmentClassifyClosedInDolibarr', $reverse = false)
3185 {
3186 global $langs;
3187
3188 $error = 0;
3189
3190 require_once DOL_DOCUMENT_ROOT . '/product/stock/class/mouvementstock.class.php';
3191
3192 $langs->load("agenda");
3193
3194 // Loop on each product line to add a stock movement
3195 $sql = "SELECT";
3196 $sql .= " ed.rowid as edid, ed.fk_product, ed.qty, ed.fk_entrepot";
3197 $sql .= ", cd.rowid as cdid";
3198 $sql .= ", cd.subprice";
3199 $sql .= ", edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock";
3200 $sql .= ", e.ref";
3201 $sql .= " FROM " . $this->db->prefix() . "expeditiondet as ed";
3202 $sql .= " LEFT JOIN " . $this->db->prefix() . "commandedet as cd ON cd.rowid = ed.fk_elementdet";
3203 $sql .= " LEFT JOIN " . $this->db->prefix() . "expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid";
3204 $sql .= " INNER JOIN " . $this->db->prefix() . "expedition as e ON ed.fk_expedition = e.rowid";
3205 $sql .= " WHERE ed.fk_expedition = " . ((int) $this->id);
3206 //$sql .= " AND cd.rowid = ed.fk_elementdet";
3207
3208 dol_syslog(get_class($this) . "::valid select details", LOG_DEBUG);
3209 $resql = $this->db->query($sql);
3210 if ($resql) {
3211 $cpt = $this->db->num_rows($resql);
3212 for ($i = 0; $i < $cpt; $i++) {
3213 $obj = $this->db->fetch_object($resql);
3214 if (empty($obj->edbrowid)) {
3215 $qty = $obj->qty;
3216 } else {
3217 $qty = $obj->edbqty;
3218 }
3219 if ($qty <= 0 || ($qty < 0 && !getDolGlobalInt('SHIPMENT_ALLOW_NEGATIVE_QTY'))) {
3220 continue;
3221 }
3222 dol_syslog(get_class($this) . "::valid movement index " . $i . " ed.rowid=" . $obj->edid . " edb.rowid=" . $obj->edbrowid);
3223
3224 // livraison() decrements stock by the quantity it is given, so a negated quantity re-increments
3225 // stock instead. Used to reverse the movement created at validation, e.g. when a shipment is
3226 // set back to draft, so a later re-validation does not create a duplicate stock movement.
3227 $qtytouse = $reverse ? -$qty : $qty;
3228
3229 $mouvS = new MouvementStock($this->db);
3230 $mouvS->origin = &$this;
3231 $mouvS->setOrigin($this->element, $this->id, $obj->cdid, $obj->edid);
3232
3233 if (empty($obj->edbrowid)) {
3234 // line without batch detail
3235
3236 // 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
3237 $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, $qtytouse, $obj->subprice, $langs->trans($labelmovement, $obj->ref));
3238 if ($result < 0) {
3239 $this->setErrorsFromObject($mouvS);
3240 $error++;
3241 break;
3242 }
3243 } else {
3244 // line with batch detail
3245
3246 // 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
3247 $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, $qtytouse, $obj->subprice, $langs->trans($labelmovement, $obj->ref), '', $this->db->jdate($obj->eatby), $this->db->jdate($obj->sellby), $obj->batch, $obj->fk_origin_stock);
3248 if ($result < 0) {
3249 $this->setErrorsFromObject($mouvS);
3250 $error++;
3251 break;
3252 }
3253 }
3254
3255 // 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
3256 // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot.
3257 $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)";
3258 $resqldelete = $this->db->query($sqldelete);
3259 // The NOT IN clause already excludes the rows still referenced by product_batch (the only child FK on
3260 // product_stock), so this DELETE can not fail on a child constraint. Any failure is a real error, in
3261 // particular a deadlock (1213) that rolls back the whole transaction including the stock movements just
3262 // recorded; if we swallowed it, the caller would commit an empty transaction and report a success while
3263 // the movements were lost.
3264 if (!$resqldelete) {
3265 $this->error = $this->db->lasterror();
3266 $this->errors[] = $this->db->lasterror();
3267 $error++;
3268 break;
3269 }
3270 }
3271 } else {
3272 $this->error = $this->db->lasterror();
3273 $this->errors[] = $this->db->lasterror();
3274 $error++;
3275 }
3276
3277 if (!$error) {
3278 return 1;
3279 } else {
3280 return -1;
3281 }
3282 }
3283
3289 public function setBilled()
3290 {
3291 global $user;
3292 $error = 0;
3293
3294 $this->db->begin();
3295
3296 $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET billed = 1';
3297 $sql .= " WHERE rowid = ".((int) $this->id).' AND fk_statut > 0';
3298
3299 $resql = $this->db->query($sql);
3300 if ($resql) {
3301 $this->billed = 1;
3302
3303 // Call trigger
3304 $result = $this->call_trigger('SHIPPING_BILLED', $user);
3305 if ($result < 0) {
3306 $this->billed = 0;
3307 $error++;
3308 }
3309 } else {
3310 $error++;
3311 $this->errors[] = $this->db->lasterror;
3312 }
3313
3314 if (empty($error)) {
3315 $this->db->commit();
3316 return 1;
3317 } else {
3318 $this->db->rollback();
3319 return -1;
3320 }
3321 }
3322
3330 public function setDraft($user, $notrigger = 0)
3331 {
3332 global $langs;
3333
3334 // Protection
3335 if ($this->status <= self::STATUS_DRAFT) {
3336 return 0;
3337 }
3338
3339 $this->db->begin();
3340
3341 $error = 0;
3342
3343 // If stock was decremented on shipment validation, reverse it now, before going back to draft, so that
3344 // a later re-validation does not create a duplicate stock movement (see setStatusCommon() below, which
3345 // does not know about this class' stock logic and only updates the status).
3346 if (isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
3347 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
3348
3349 $langs->load("agenda");
3350
3351 $result = $this->manageStockMvtOnEvt($user, "ShipmentBackToDraftInDolibarr", true);
3352 if ($result < 0) {
3353 $error++;
3354 }
3355 }
3356
3357 if (!$error) {
3358 $result = $this->setStatusCommon($user, self::STATUS_DRAFT, $notrigger, 'SHIPMENT_UNVALIDATE');
3359 if ($result < 0) {
3360 $error++;
3361 }
3362 }
3363
3364 if (!$error) {
3365 $this->db->commit();
3366 return 1;
3367 } else {
3368 $this->db->rollback();
3369 return -1;
3370 }
3371 }
3372
3378 public function reOpen()
3379 {
3380 global $langs, $user;
3381
3382 $error = 0;
3383
3384 // Protection. This avoid to move stock later when we should not
3385 if ($this->status == self::STATUS_VALIDATED) {
3386 return 0;
3387 }
3388
3389 $this->db->begin();
3390
3391 $oldbilled = $this->billed;
3392
3393 $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET fk_statut = 1';
3394 $sql .= " WHERE rowid = ".((int) $this->id).' AND fk_statut > 0';
3395
3396 $resql = $this->db->query($sql);
3397 if ($resql) {
3398 $this->statut = self::STATUS_VALIDATED;
3400 $this->billed = 0;
3401
3402 // If stock increment is done on closing
3403 if (isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
3404 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
3405
3406 $langs->load("agenda");
3407
3408 // Loop on each product line to add a stock movement
3409 // TODO possibility to ship from a proposal or other origin
3410 $sql = "SELECT cd.fk_product, cd.subprice,";
3411 $sql .= " ed.rowid, ed.qty, ed.fk_entrepot,";
3412 $sql .= " edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock";
3413 $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,";
3414 $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed";
3415 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid";
3416 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
3417 $sql .= " AND cd.rowid = ed.fk_elementdet";
3418
3419 dol_syslog(get_class($this)."::valid select details", LOG_DEBUG);
3420 $resql = $this->db->query($sql);
3421 if ($resql) {
3422 $cpt = $this->db->num_rows($resql);
3423 for ($i = 0; $i < $cpt; $i++) {
3424 $obj = $this->db->fetch_object($resql);
3425 if (empty($obj->edbrowid)) {
3426 $qty = $obj->qty;
3427 } else {
3428 $qty = $obj->edbqty;
3429 }
3430 if ($qty <= 0) {
3431 continue;
3432 }
3433 dol_syslog(get_class($this)."::reopen expedition movement index ".$i." ed.rowid=".$obj->rowid." edb.rowid=".$obj->edbrowid);
3434
3435 //var_dump($this->lines[$i]);
3436 $mouvS = new MouvementStock($this->db);
3437 $mouvS->origin = &$this;
3438 $mouvS->setOrigin($this->element, $this->id);
3439
3440 if (empty($obj->edbrowid)) {
3441 // line without batch detail
3442
3443 // 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
3444 $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, -$qty, $obj->subprice, $langs->trans("ShipmentUnClassifyCloseddInDolibarr", $this->ref));
3445 if ($result < 0) {
3446 $this->setErrorsFromObject($mouvS);
3447 $error++;
3448 break;
3449 }
3450 } else {
3451 // line with batch detail
3452
3453 // 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
3454 $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);
3455 if ($result < 0) {
3456 $this->setErrorsFromObject($mouvS);
3457 $error++;
3458 break;
3459 }
3460 }
3461 }
3462 } else {
3463 $this->error = $this->db->lasterror();
3464 $error++;
3465 }
3466 }
3467
3468 if (!$error) {
3469 // Call trigger
3470 $result = $this->call_trigger('SHIPPING_REOPEN', $user);
3471 if ($result < 0) {
3472 $error++;
3473 }
3474 }
3475 } else {
3476 $error++;
3477 $this->errors[] = $this->db->lasterror();
3478 }
3479
3480 if (!$error) {
3481 $this->db->commit();
3482 return 1;
3483 } else {
3484 $this->statut = self::STATUS_CLOSED;
3485 $this->status = self::STATUS_CLOSED;
3486 $this->billed = $oldbilled;
3487 $this->db->rollback();
3488 return -1;
3489 }
3490 }
3491
3503 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null)
3504 {
3505 $outputlangs->load("products");
3506
3507 if (!dol_strlen($modele)) {
3508 $modele = 'espadon';
3509
3510 if (!empty($this->model_pdf)) {
3511 $modele = $this->model_pdf;
3512 } elseif (getDolGlobalString('EXPEDITION_ADDON_PDF')) {
3513 $modele = getDolGlobalString('EXPEDITION_ADDON_PDF');
3514 }
3515 }
3516
3517 $modelpath = "core/modules/expedition/doc/";
3518
3519 $this->fetch_origin();
3520
3521 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
3522 }
3523
3532 public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
3533 {
3534 $tables = array(
3535 'expedition'
3536 );
3537
3538 return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
3539 }
3540}
$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.
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.
manageStockMvtOnEvt($user, $labelmovement='ShipmentClassifyClosedInDolibarr', $reverse=false)
Manage Stock MVt onb Close or valid 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.
checkLineStockRequirements($fk_product, $fk_entrepot, $qty, $product_type=null, $forbid_batch_product=false, $allowEmptyWarehouse=false)
Check stock constraints for a shipment line.
setBilled()
Classify the shipping as invoiced (used for example by trigger when WORKFLOW_SHIPPING_CLASSIFY_BILLED...
updatelinefree($rowid, $qty, $element_type, $fk_product, $fk_unit, $rang, $description, $fk_parent, $notrigger, $array_options=array(), $fk_entrepot=-1)
Update a simple expedition line.
generateDocument($modele, $outputlangs, $hidedetails=0, $hidedesc=0, $hideref=0, $moreparams=null)
Create a document onto disk according to template module.
getTooltipContentArray($params)
getTooltipContentArray
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.
getNomUrl($withpicto=0, $option='', $max=0, $short=0, $notooltip=0, $save_lastsearch_value=-1, $addlinktonotes=0)
Return clickable link of object (with eventually picto)
getLibStatut($mode=0)
Return status label.
set_date_livraison($user, $delivery_date)
Set delivery date.
initAsSpecimen()
Initialise an instance with random values.
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.
addlinefree($qty, $element_type, $fk_product, $fk_unit, $rang, $description, $fk_parent, $array_options=[], $fk_entrepot=0)
Add a simple expedition line.
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_PRODUCT
Regular product.
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.
dol_string_nohtmltag($stringtoclean, $removelinefeed=1, $pagecodeto='UTF-8', $strip_tags=0, $removedoublespaces=1)
Clean a string from all HTML tags and entities.
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.
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
Definition html.lib.php:181
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:492
$conf db user
Active Directory does not allow anonymous connections.
Definition repair.php:141