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