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 $error++;
1026 }
1027 }
1028
1029 // Change status of order to "shipment in process"
1030 if (!$error) {
1031 $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
1032 if ($this->origin == 'commande') {
1033 $triggerKey.= 'ORDER_SHIPMENTONPROCESS';
1034 } else {
1035 $triggerKey.= strtoupper($this->origin).'_SHIPMENTONPROCESS';
1036 }
1037
1038 // TODO : load the origin object to trigger the right setStatus according to origin object
1039 $ret = $this->setStatut(Commande::STATUS_SHIPMENTONPROCESS, $this->origin_id, $this->origin, $triggerKey);
1040 if (!$ret) {
1041 $error++;
1042 }
1043 }
1044
1045 if (!$error && !$notrigger) {
1046 // Call trigger
1047 $result = $this->call_trigger('SHIPPING_VALIDATE', $user);
1048 if ($result < 0) {
1049 $error++;
1050 }
1051 // End call triggers
1052 }
1053
1054 if (!$error) {
1055 $this->oldref = $this->ref;
1056
1057 // Rename directory if dir was a temporary ref
1058 if (preg_match('/^[\‍(]?PROV/i', $this->ref)) {
1059 // Now we rename also files into index
1060 $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)."'";
1061 $sql .= " WHERE filename LIKE '".$this->db->escape($this->ref)."%' AND filepath = 'expedition/sending/".$this->db->escape($this->ref)."' and entity = ".((int) $conf->entity);
1062 $resql = $this->db->query($sql);
1063 if (!$resql) {
1064 $error++;
1065 $this->error = $this->db->lasterror();
1066 }
1067 $sql = 'UPDATE '.MAIN_DB_PREFIX."ecm_files set filepath = 'expedition/sending/".$this->db->escape($this->newref)."'";
1068 $sql .= " WHERE filepath = 'expedition/sending/".$this->db->escape($this->ref)."' and entity = ".((int) $conf->entity);
1069 $resql = $this->db->query($sql);
1070 if (!$resql) {
1071 $error++;
1072 $this->error = $this->db->lasterror();
1073 }
1074
1075 // We rename directory ($this->ref = old ref, $num = new ref) in order not to lose the attachments
1076 $oldref = dol_sanitizeFileName($this->ref);
1077 $newref = dol_sanitizeFileName($numref);
1078 $dirsource = $conf->expedition->dir_output.'/sending/'.$oldref;
1079 $dirdest = $conf->expedition->dir_output.'/sending/'.$newref;
1080 if (!$error && file_exists($dirsource)) {
1081 dol_syslog(get_class($this)."::valid rename dir ".$dirsource." into ".$dirdest);
1082
1083 if (@rename($dirsource, $dirdest)) {
1084 dol_syslog("Rename ok");
1085 // Rename docs starting with $oldref with $newref
1086 $listoffiles = dol_dir_list($conf->expedition->dir_output.'/sending/'.$newref, 'files', 1, '^'.preg_quote($oldref, '/'));
1087 foreach ($listoffiles as $fileentry) {
1088 $dirsource = $fileentry['name'];
1089 $dirdest = preg_replace('/^'.preg_quote($oldref, '/').'/', $newref, $dirsource);
1090 $dirsource = $fileentry['path'].'/'.$dirsource;
1091 $dirdest = $fileentry['path'].'/'.$dirdest;
1092 @rename($dirsource, $dirdest);
1093 }
1094 }
1095 }
1096 }
1097 }
1098
1099 // Set new ref and current status
1100 if (!$error) {
1101 $this->ref = $numref;
1102 $this->statut = self::STATUS_VALIDATED;
1104 }
1105
1106 if (!$error) {
1107 $this->db->commit();
1108 return 1;
1109 } else {
1110 $this->db->rollback();
1111 return -1 * $error;
1112 }
1113 }
1114
1115
1116 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1123 public function create_delivery($user)
1124 {
1125 // phpcs:enable
1126 global $conf;
1127
1128 if (getDolGlobalInt('MAIN_SUBMODULE_DELIVERY')) {
1129 if ($this->status == self::STATUS_VALIDATED || $this->status == self::STATUS_CLOSED) {
1130 // Expedition validee
1131 include_once DOL_DOCUMENT_ROOT.'/delivery/class/delivery.class.php';
1132 $delivery = new Delivery($this->db);
1133 $result = $delivery->create_from_sending($user, $this->id);
1134 if ($result > 0) {
1135 return $result;
1136 } else {
1137 $this->error = $delivery->error;
1138 return $result;
1139 }
1140 } else {
1141 return 0;
1142 }
1143 } else {
1144 return 0;
1145 }
1146 }
1147
1162 public function addline($entrepot_id, $id, $qty, $array_options = [], $fk_product = 0, $fk_parent = 0)
1163 {
1164 global $conf, $langs;
1165
1166 $num = count($this->lines);
1167 $line = new ExpeditionLigne($this->db);
1168
1169 $line->entrepot_id = $entrepot_id;
1170 $line->origin_line_id = $id;
1171 $line->fk_elementdet = $id;
1172 $line->element_type = 'order';
1173 $line->fk_parent = $fk_parent;
1174 $line->fk_product = $fk_product;
1175 $line->qty = $qty;
1176
1177 $orderline = new OrderLine($this->db);
1178 $orderline->fetch($id);
1179
1180 // Copy the rang of the order line to the expedition line
1181 $line->rang = $orderline->rang;
1182 $line->product_type = $orderline->product_type;
1183 if (!($line->fk_product > 0)) {
1184 $line->fk_product = $orderline->fk_product;
1185 }
1186
1187 if (isModEnabled('stock') && !empty($orderline->fk_product)) {
1188 $product = new Product($this->db);
1189 $product->fetch($orderline->fk_product);
1190
1191 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)) {
1192 $langs->load("errors");
1193 $this->error = $langs->trans("ErrorWarehouseRequiredIntoShipmentLine");
1194 return -1;
1195 }
1196
1197 if (getDolGlobalString('STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT') && ($qty > 0 || !getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1198 $productChildrenNb = 0;
1199 if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) {
1200 $productChildrenNb = $product->hasFatherOrChild(1);
1201 }
1202 if ($productChildrenNb > 0) {
1203 $product_stock = null;
1204 $product->loadStockForVirtualProduct('warehouseopen', $line->qty);
1205 if ($entrepot_id > 0) {
1206 if (isset($product->stock_warehouse[$entrepot_id])) {
1207 $product_stock = $product->stock_warehouse[$entrepot_id]->real;
1208 }
1209 } else {
1210 foreach ($product->stock_warehouse as $componentStockWarehouse) {
1211 if ($product_stock === null) {
1212 $product_stock = $componentStockWarehouse->real;
1213 } else {
1214 $product_stock = min($product_stock, $componentStockWarehouse->real);
1215 }
1216 }
1217 }
1218 if ($product_stock === null) {
1219 $product_stock = 0;
1220 }
1221 } else {
1222 // Check must be done for stock of product into warehouse if $entrepot_id defined
1223 if ($entrepot_id > 0) {
1224 $product->load_stock('warehouseopen');
1225 $product_stock = $product->stock_warehouse[$entrepot_id]->real;
1226 } else {
1227 $product_stock = $product->stock_reel;
1228 }
1229 }
1230
1231 $product_type = $product->type;
1232 if ($product_type == 0 || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
1233 $isavirtualproduct = ($productChildrenNb > 0);
1234 // The product is qualified for a check of quantity (must be enough in stock to be added into shipment).
1235 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.
1236 if ($product->stockable_product == Product::ENABLED_STOCK && $product_stock < $qty) {
1237 $langs->load("errors");
1238 $this->error = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $product->ref);
1239 $this->errorhidden = 'ErrorStockIsNotEnoughToAddProductOnShipment';
1240
1241 $this->db->rollback();
1242 return -3;
1243 }
1244 }
1245 }
1246 }
1247 }
1248
1249 // If product need a batch number, we should not have called this function but addline_batch instead.
1250 // If this happen, we may have a bug in card.php page
1251 if (isModEnabled('productbatch') && !empty($line->fk_product) && !empty($orderline->product_tobatch)) {
1252 $this->error = 'ADDLINE_WAS_CALLED_INSTEAD_OF_ADDLINEBATCH '.$orderline->id.' '.$line->fk_product; //
1253 return -4;
1254 }
1255
1256 // extrafields
1257 if (!getDolGlobalString('MAIN_EXTRAFIELDS_DISABLED') && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used
1258 $line->array_options = $array_options;
1259 }
1260
1261 $this->lines[$num] = $line;
1262
1263 return 1;
1264 }
1265
1266 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1275 public function addline_batch($dbatch, $array_options = [], $origin_line = null)
1276 {
1277 // phpcs:enable
1278 global $conf, $langs;
1279
1280 $num = count($this->lines);
1281 $linebatch = null;
1282 if ($dbatch['qty'] > 0 || ($dbatch['qty'] == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1283 $line = new ExpeditionLigne($this->db);
1284 $tab = array();
1285 foreach ($dbatch['detail'] as $key => $value) {
1286 if ($value['q'] > 0 || ($value['q'] == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1287 // $value['q']=qty to move
1288 // $value['id_batch']=id into llx_product_batch of record to move
1289 //var_dump($value);
1290
1291 $linebatch = new ExpeditionLineBatch($this->db);
1292 $ret = $linebatch->fetchFromStock($value['id_batch']); // load serial, sellby, eatby
1293 if ($ret < 0) {
1294 $this->setErrorsFromObject($linebatch);
1295 return -1;
1296 }
1297 $linebatch->qty = $value['q'];
1298 if ($linebatch->qty == 0 && getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS')) {
1299 $linebatch->batch = null;
1300 }
1301 $tab[] = $linebatch;
1302
1303 if (getDolGlobalString("STOCK_MUST_BE_ENOUGH_FOR_SHIPMENT", '0') && ($linebatch->qty > 0 || !getDolGlobalString('SHIPMENT_GETS_ALL_ORDER_PRODUCTS'))) {
1304 require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
1305 $prod_batch = new Productbatch($this->db);
1306 $prod_batch->fetch($value['id_batch']);
1307
1308 if ($prod_batch->qty < $linebatch->qty) {
1309 $langs->load("errors");
1310 $this->errors[] = $langs->trans('ErrorStockIsNotEnoughToAddProductOnShipment', $prod_batch->fk_product);
1311 dol_syslog(get_class($this)."::addline_batch error=Product ".$prod_batch->batch.": ".$this->errorsToString(), LOG_ERR);
1312 $this->db->rollback();
1313 return -1;
1314 }
1315 }
1316
1317 //var_dump($linebatch);
1318 }
1319 }
1320 if (is_object($linebatch)) {
1321 $line->entrepot_id = $linebatch->fk_warehouse;
1322 }
1323 $line->origin_line_id = $dbatch['ix_l']; // deprecated
1324 $line->fk_elementdet = $dbatch['ix_l'];
1325 $line->qty = $dbatch['qty'];
1326 $line->detail_batch = $tab;
1327 if (!($line->rang > 0)) {
1328 $line->rang = $origin_line->rang;
1329 }
1330 if (!($line->fk_product > 0)) {
1331 $line->fk_product = $origin_line->fk_product;
1332 }
1333
1334 // extrafields
1335 if (!getDolGlobalString('MAIN_EXTRAFIELDS_DISABLED') && is_array($array_options) && count($array_options) > 0) { // For avoid conflicts if trigger used
1336 $line->array_options = $array_options;
1337 }
1338
1339 //var_dump($line);
1340 $this->lines[$num] = $line;
1341 return 1;
1342 }
1343 return 0;
1344 }
1345
1353 public function update($user = null, $notrigger = 0)
1354 {
1355 global $conf;
1356 $error = 0;
1357
1358 // Clean parameters
1359
1360 if (isset($this->ref)) {
1361 $this->ref = trim($this->ref);
1362 }
1363 if (isset($this->entity)) {
1364 $this->entity = (int) $this->entity;
1365 }
1366 if (isset($this->ref_customer)) {
1367 $this->ref_customer = trim($this->ref_customer);
1368 }
1369 if (isset($this->socid)) {
1370 $this->socid = (int) $this->socid;
1371 }
1372 if (isset($this->fk_user_author)) {
1373 $this->fk_user_author = (int) $this->fk_user_author;
1374 }
1375 if (isset($this->fk_user_valid)) { // @phan-ignore-current-line PhanUndeclaredProperty
1376 // If set, then accept @phan-ignore-next-line PhanUndeclaredProperty
1377 $this->fk_user_valid = (int) $this->fk_user_valid;
1378 }
1379 if (isset($this->fk_delivery_address)) {
1380 $this->fk_delivery_address = (int) $this->fk_delivery_address;
1381 }
1382 if (isset($this->shipping_method_id)) {
1383 $this->shipping_method_id = (int) $this->shipping_method_id;
1384 }
1385 if (isset($this->tracking_number)) {
1386 $this->tracking_number = trim($this->tracking_number);
1387 }
1388 if (isset($this->statut)) {
1389 $this->statut = (int) $this->statut;
1390 }
1391 if (isset($this->status)) {
1392 $this->status = (int) $this->status;
1393 }
1394 if (isset($this->trueDepth)) {
1395 $this->trueDepth = trim($this->trueDepth);
1396 }
1397 if (isset($this->trueWidth)) {
1398 $this->trueWidth = trim($this->trueWidth);
1399 }
1400 if (isset($this->trueHeight)) {
1401 $this->trueHeight = trim($this->trueHeight);
1402 }
1403 if (isset($this->size_units)) {
1404 $this->size_units = trim($this->size_units);
1405 }
1406 if (isset($this->weight_units)) {
1407 $this->weight_units = (int) $this->weight_units;
1408 }
1409 if (isset($this->trueWeight)) {
1410 $this->weight = trim((string) $this->trueWeight);
1411 }
1412 if (isset($this->note_private)) {
1413 $this->note_private = trim($this->note_private);
1414 }
1415 if (isset($this->note_public)) {
1416 $this->note_public = trim($this->note_public);
1417 }
1418 if (isset($this->model_pdf)) {
1419 $this->model_pdf = trim($this->model_pdf);
1420 }
1421 if (!empty($this->date_expedition)) {
1422 $this->date_shipping = $this->date_expedition;
1423 }
1424
1425 // Check parameters
1426 // Put here code to add control on parameters values
1427
1428 // Update request
1429 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET";
1430 $sql .= " ref = ".(isset($this->ref) ? "'".$this->db->escape($this->ref)."'" : "null").",";
1431 $sql .= " ref_ext = ".(isset($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null").",";
1432 $sql .= " ref_customer = ".(isset($this->ref_customer) ? "'".$this->db->escape($this->ref_customer)."'" : "null").",";
1433 $sql .= " fk_soc = ".(isset($this->socid) ? $this->socid : "null").",";
1434 $sql .= " date_creation = ".(dol_strlen($this->date_creation) != 0 ? "'".$this->db->idate($this->date_creation)."'" : 'null').",";
1435 $sql .= " fk_user_author = ".(isset($this->fk_user_author) ? $this->fk_user_author : "null").",";
1436 $sql .= " date_valid = ".(dol_strlen($this->date_valid) != 0 ? "'".$this->db->idate($this->date_valid)."'" : 'null').",";
1437 $sql .= " fk_user_valid = ".(isset($this->fk_user_valid) ? $this->fk_user_valid : "null").",";
1438 $sql .= " date_expedition = ".(dol_strlen($this->date_shipping) != 0 ? "'".$this->db->idate($this->date_shipping)."'" : 'null').",";
1439 $sql .= " date_delivery = ".(dol_strlen($this->date_delivery) != 0 ? "'".$this->db->idate($this->date_delivery)."'" : 'null').",";
1440 $sql .= " fk_address = ".(isset($this->fk_delivery_address) ? $this->fk_delivery_address : "null").",";
1441 $sql .= " fk_shipping_method = ".((isset($this->shipping_method_id) && $this->shipping_method_id > 0) ? $this->shipping_method_id : "null").",";
1442 $sql .= " tracking_number = ".(isset($this->tracking_number) ? "'".$this->db->escape($this->tracking_number)."'" : "null").",";
1443 $sql .= " fk_statut = ".(isset($this->status) ? $this->status : "null").",";
1444 $sql .= " fk_projet = ".(isset($this->fk_project) ? $this->fk_project : "null").",";
1445 $sql .= " height = ".(($this->trueHeight != '') ? $this->trueHeight : "null").",";
1446 $sql .= " width = ".(($this->trueWidth != '') ? $this->trueWidth : "null").",";
1447 $sql .= " size_units = ".(isset($this->size_units) ? $this->size_units : "null").",";
1448 $sql .= " size = ".(($this->trueDepth != '') ? $this->trueDepth : "null").",";
1449 $sql .= " weight_units = ".(isset($this->weight_units) ? $this->weight_units : "null").",";
1450 $sql .= " weight = ".(($this->trueWeight != '') ? $this->trueWeight : "null").",";
1451 $sql .= " note_private = ".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : "null").",";
1452 $sql .= " note_public = ".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : "null").",";
1453 $sql .= " model_pdf = ".(isset($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null").",";
1454 $sql .= " entity = ".((int) $conf->entity);
1455 $sql .= " WHERE rowid = ".((int) $this->id);
1456
1457 $this->db->begin();
1458
1459 dol_syslog(get_class($this)."::update", LOG_DEBUG);
1460 $resql = $this->db->query($sql);
1461 if (!$resql) {
1462 $error++;
1463 $this->errors[] = "Error ".$this->db->lasterror();
1464 }
1465
1466 // Actions on extra fields
1467 if (!$error) {
1468 $result = $this->insertExtraFields();
1469 if ($result < 0) {
1470 $error++;
1471 }
1472 }
1473
1474 if (!$error && !$notrigger) {
1475 // Call trigger
1476 $result = $this->call_trigger('SHIPPING_MODIFY', $user);
1477 if ($result < 0) {
1478 $error++;
1479 }
1480 // End call triggers
1481 }
1482
1483 // Commit or rollback
1484 if ($error) {
1485 foreach ($this->errors as $errmsg) {
1486 dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR);
1487 $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
1488 }
1489 $this->db->rollback();
1490 return -1 * $error;
1491 } else {
1492 $this->db->commit();
1493 return 1;
1494 }
1495 }
1496
1497
1505 public function cancel($notrigger = 0, $also_update_stock = false)
1506 {
1507 global $conf, $langs, $user;
1508
1509 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1510
1511 $error = 0;
1512 $this->error = '';
1513
1514 $this->db->begin();
1515
1516 // Add a protection to refuse deleting if shipment has at least one delivery
1517 $this->fetchObjectLinked($this->id, 'shipping', 0, 'delivery'); // Get deliveries linked to this shipment
1518 if (isset($this->linkedObjectsIds['delivery']) && count($this->linkedObjectsIds['delivery']) > 0) {
1519 $this->error = 'ErrorThereIsSomeDeliveries';
1520 $error++;
1521 }
1522
1523 if (!$error && !$notrigger) {
1524 // Call trigger
1525 $result = $this->call_trigger('SHIPPING_CANCEL', $user);
1526 if ($result < 0) {
1527 $error++;
1528 }
1529 // End call triggers
1530 }
1531
1532 // Stock control
1533 $can_update_stock = isModEnabled('stock') &&
1534 ((getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') && $this->status > self::STATUS_DRAFT) ||
1535 (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE') && $this->status == self::STATUS_CLOSED && $also_update_stock));
1536 if (!$error) {
1537 require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php";
1538
1539 $langs->load("agenda");
1540
1541 // Loop on each product line to add a stock movement (contain sub-products)
1542 $sql = "SELECT ";
1543 $sql .= " ed.fk_product";
1544 $sql .= ", ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id";
1545 $sql .= ", SUM(".$this->db->ifsql("pa.rowid IS NOT NULL", "1", "0").") as iskit";
1546 $sql .= ", ".$this->db->ifsql("pai.incdec IS NULL", "1", "pai.incdec")." as incdec";
1547 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
1548 $sql .= " LEFT JOIN ".$this->db->prefix()."product_association as pa ON pa.fk_product_pere = ed.fk_product";
1549 $sql .= " LEFT JOIN ".$this->db->prefix()."expeditiondet as edp ON edp.rowid = ed.fk_parent";
1550 $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";
1551 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
1552 $sql .= " GROUP BY ed.fk_product, ed.qty, ed.fk_entrepot, ed.rowid, pai.incdec";
1553 $sql .= $this->db->order("ed.rowid", "DESC");
1554
1555 dol_syslog(get_class($this)."::delete select details", LOG_DEBUG);
1556 $resql = $this->db->query($sql);
1557 if ($resql) {
1558 $cpt = $this->db->num_rows($resql);
1559
1560 $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1561
1562 for ($i = 0; $i < $cpt; $i++) {
1563 dol_syslog(get_class($this)."::delete movement index ".$i);
1564 $obj = $this->db->fetch_object($resql);
1565 $line_id = (int) $obj->expeditiondet_id;
1566
1567 if ($can_update_stock && (empty($obj->iskit) || getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE')) && !empty($obj->incdec)) {
1568 $mouvS = new MouvementStock($this->db);
1569 // we do not log origin because it will be deleted
1570 $mouvS->origin = '';
1571 // get lot/serial
1572 $lotArray = null;
1573 if (isModEnabled('productbatch')) {
1574 $lotArray = $shipmentlinebatch->fetchAll($obj->expeditiondet_id);
1575 if (!is_array($lotArray)) {
1576 $error++;
1577 $this->errors[] = "Error ".$this->db->lasterror();
1578 }
1579 }
1580
1581 if (empty($lotArray)) {
1582 // no lot/serial
1583 // We increment stock of product (and sub-products)
1584 // We use warehouse selected for each line
1585 $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
1586 if ($result < 0) {
1587 $error++;
1588 $this->errors = array_merge($this->errors, $mouvS->errors);
1589 break;
1590 }
1591 } else {
1592 // We increment stock of batches
1593 // We use warehouse selected for each line
1594 foreach ($lotArray as $lot) {
1595 $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
1596 if ($result < 0) {
1597 $error++;
1598 $this->errors = array_merge($this->errors, $mouvS->errors);
1599 break;
1600 }
1601 }
1602 if ($error) {
1603 break; // break for loop in case of error
1604 }
1605 }
1606 }
1607
1608 if (!$error) {
1609 // delete all children and batches of this shipment line
1610 $shipment_line = new ExpeditionLigne($this->db);
1611 $res = $shipment_line->fetch($line_id);
1612 if ($res > 0) {
1613 $result = $shipment_line->delete($user);
1614 if ($result < 0) {
1615 $error++;
1616 $this->errors[] = "Error ".$shipment_line->errorsToString();
1617 }
1618 } else {
1619 $error++;
1620 $this->errors[] = "Error ".$shipment_line->errorsToString();
1621 }
1622 }
1623
1624 if ($error) {
1625 break;
1626 }
1627 }
1628 } else {
1629 $error++;
1630 $this->errors[] = "Error ".$this->db->lasterror();
1631 }
1632 }
1633
1634 if (!$error) {
1635 // Delete linked object
1636 $res = $this->deleteObjectLinked();
1637 if ($res < 0) {
1638 $error++;
1639 }
1640
1641 // No delete expedition
1642 if (!$error) {
1643 $sql = "SELECT rowid FROM ".$this->db->prefix()."expedition";
1644 $sql .= " WHERE rowid = ".((int) $this->id);
1645
1646 if ($this->db->query($sql)) {
1647 if (!empty($this->origin) && $this->origin_id > 0) {
1648 $this->fetch_origin();
1650 '@phan-var-force Facture|Commande $origin_object';
1651 if ($origin_object->status == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress"
1652 // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress"
1653 $origin_object->loadExpeditions();
1654 //var_dump($this->$origin->expeditions);exit;
1655 if (count($origin_object->expeditions) <= 0) {
1657 }
1658 }
1659 }
1660
1661 if (!$error) {
1662 $this->db->commit();
1663
1664 // We delete PDFs
1665 $ref = dol_sanitizeFileName($this->ref);
1666 if (!empty($conf->expedition->dir_output)) {
1667 $dir = $conf->expedition->dir_output.'/sending/'.$ref;
1668 $file = $dir.'/'.$ref.'.pdf';
1669 if (file_exists($file)) {
1670 if (!dol_delete_file($file)) {
1671 return 0;
1672 }
1673 }
1674 if (file_exists($dir)) {
1675 if (!dol_delete_dir_recursive($dir)) {
1676 $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir);
1677 return 0;
1678 }
1679 }
1680 }
1681
1682 return 1;
1683 } else {
1684 $this->db->rollback();
1685 return -1;
1686 }
1687 } else {
1688 $this->error = $this->db->lasterror()." - sql=$sql";
1689 $this->db->rollback();
1690 return -3;
1691 }
1692 } else {
1693 $this->db->rollback();
1694 return -2;
1695 }
1696 } else {
1697 $this->db->rollback();
1698 return -1;
1699 }
1700 }
1701
1711 public function delete($user = null, $notrigger = 0, $also_update_stock = false)
1712 {
1713 global $conf, $langs;
1714
1715 if (empty($user)) {
1716 global $user;
1717 }
1718
1719 require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1720
1721 $error = 0;
1722 $this->error = '';
1723
1724 $this->db->begin();
1725
1726 // Add a protection to refuse deleting if shipment has at least one delivery
1727 $this->fetchObjectLinked($this->id, 'shipping', 0, 'delivery'); // Get deliveries linked to this shipment
1728 if (isset($this->linkedObjectsIds['delivery']) && count($this->linkedObjectsIds['delivery']) > 0) {
1729 $this->error = 'ErrorThereIsSomeDeliveries';
1730 $error++;
1731 }
1732
1733 if (!$error && !$notrigger) {
1734 // Call trigger
1735 $result = $this->call_trigger('SHIPPING_DELETE', $user);
1736 if ($result < 0) {
1737 $error++;
1738 }
1739 // End call triggers
1740 }
1741
1742 // Stock control
1743 $can_update_stock = isModEnabled('stock') &&
1744 ((getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') && $this->status > self::STATUS_DRAFT) ||
1745 (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE') && $this->status == self::STATUS_CLOSED && $also_update_stock));
1746 if (!$error) {
1747 require_once DOL_DOCUMENT_ROOT."/product/stock/class/mouvementstock.class.php";
1748
1749 $langs->load("agenda");
1750
1751 // we try deletion of batch line even if module batch not enabled in case of the module were enabled and disabled previously
1752 $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1753
1754 // Loop on each product line to add a stock movement (contain sub-products)
1755 $sql = "SELECT ";
1756 $sql .= " ed.fk_product";
1757 $sql .= ", ed.qty, ed.fk_entrepot, ed.rowid as expeditiondet_id";
1758 $sql .= ", SUM(".$this->db->ifsql("pa.rowid IS NOT NULL", "1", "0").") as iskit";
1759 $sql .= ", ".$this->db->ifsql("pai.incdec IS NULL", "1", "pai.incdec")." as incdec";
1760 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
1761 $sql .= " LEFT JOIN ".$this->db->prefix()."product_association as pa ON pa.fk_product_pere = ed.fk_product";
1762 $sql .= " LEFT JOIN ".$this->db->prefix()."expeditiondet as edp ON edp.rowid = ed.fk_parent";
1763 $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";
1764 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
1765 $sql .= " GROUP BY ed.fk_product, ed.qty, ed.fk_entrepot, ed.rowid, pai.incdec";
1766 $sql .= $this->db->order("ed.rowid", "DESC");
1767
1768 dol_syslog(get_class($this)."::delete select details", LOG_DEBUG);
1769 $resql = $this->db->query($sql);
1770 if ($resql) {
1771 $cpt = $this->db->num_rows($resql);
1772 for ($i = 0; $i < $cpt; $i++) {
1773 dol_syslog(get_class($this)."::delete movement index ".$i);
1774 $obj = $this->db->fetch_object($resql);
1775 $line_id = (int) $obj->expeditiondet_id;
1776
1777 if ($can_update_stock && (empty($obj->iskit) || getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE')) && !empty($obj->incdec)) {
1778 $mouvS = new MouvementStock($this->db);
1779 // we do not log origin because it will be deleted
1780 $mouvS->origin = '';
1781 // get lot/serial
1782 $lotArray = $shipmentlinebatch->fetchAll($line_id);
1783 if (!is_array($lotArray)) {
1784 $error++;
1785 $this->errors[] = "Error ".$this->db->lasterror();
1786 }
1787 if (empty($lotArray)) {
1788 // no lot/serial
1789 // We increment stock of product (disable for sub-products : already in shipment lines)
1790 // We use warehouse selected for each line
1791 $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
1792 if ($result < 0) {
1793 $error++;
1794 $this->errors = array_merge($this->errors, $mouvS->errors);
1795 break;
1796 }
1797 } else {
1798 // We increment stock of batches
1799 // We use warehouse selected for each line
1800 foreach ($lotArray as $lot) {
1801 if (empty($lot->qty)) {
1802 continue;
1803 }
1804 $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
1805 if ($result < 0) {
1806 $error++;
1807 $this->errors = array_merge($this->errors, $mouvS->errors);
1808 break;
1809 }
1810 }
1811 if ($error) {
1812 break; // break for loop in case of error
1813 }
1814 }
1815 }
1816
1817 if (!$error) {
1818 // delete all children and batches of this shipment line
1819 $shipment_line = new ExpeditionLigne($this->db);
1820 $res = $shipment_line->fetch($line_id);
1821 if ($res > 0) {
1822 $result = $shipment_line->delete($user);
1823 if ($result < 0) {
1824 $error++;
1825 $this->errors[] = "Error ".$shipment_line->errorsToString();
1826 }
1827 } else {
1828 $error++;
1829 $this->errors[] = "Error ".$shipment_line->errorsToString();
1830 }
1831 }
1832
1833 if ($error) {
1834 break;
1835 }
1836 }
1837 } else {
1838 $error++;
1839 $this->errors[] = "Error ".$this->db->lasterror();
1840 }
1841 }
1842
1843 if (!$error) {
1844 // Delete linked object
1845 $res = $this->deleteObjectLinked();
1846 if ($res < 0) {
1847 $error++;
1848 }
1849
1850 // delete extrafields
1851 $res = $this->deleteExtraFields();
1852 if ($res < 0) {
1853 $error++;
1854 }
1855
1856 if (!$error) {
1857 // Delete linked contacts
1858 $res = $this->delete_linked_contact();
1859 if ($res < 0) {
1860 $error++;
1861 }
1862 }
1863 if (!$error) {
1864 $sql = "DELETE FROM ".$this->db->prefix()."expedition";
1865 $sql .= " WHERE rowid = ".((int) $this->id);
1866
1867 if ($this->db->query($sql)) {
1868 if (!empty($this->origin) && $this->origin_id > 0) {
1869 $this->fetch_origin();
1871 '@phan-var-force Facture|Commande $origin_object';
1872 if ($origin_object->status == Commande::STATUS_SHIPMENTONPROCESS) { // If order source of shipment is "shipment in progress"
1873 // Check if there is no more shipment. If not, we can move back status of order to "validated" instead of "shipment in progress"
1874 $origin_object->loadExpeditions();
1875 //var_dump($this->$origin->expeditions);exit;
1876 if (count($origin_object->expeditions) <= 0) {
1878 }
1879 }
1880 }
1881
1882 if (!$error) {
1883 $this->db->commit();
1884
1885 // Delete record into ECM index (Note that delete is also done when deleting files with the dol_delete_dir_recursive
1886 $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
1887 $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
1888
1889 // We delete PDFs
1890 $ref = dol_sanitizeFileName($this->ref);
1891 if (!empty($conf->expedition->dir_output) && !empty($ref)) {
1892 $dir = $conf->expedition->dir_output.'/sending/'.$ref;
1893 $file = $dir.'/'.$ref.'.pdf';
1894 if (file_exists($file)) {
1895 if (!dol_delete_file($file)) {
1896 return 0;
1897 }
1898 }
1899 if (file_exists($dir)) {
1900 if (!dol_delete_dir_recursive($dir)) {
1901 $this->error = $langs->trans("ErrorCanNotDeleteDir", $dir);
1902 return 0;
1903 }
1904 }
1905 }
1906
1907 return 1;
1908 } else {
1909 $this->db->rollback();
1910 return -1;
1911 }
1912 } else {
1913 $this->error = $this->db->lasterror()." - sql=$sql";
1914 $this->db->rollback();
1915 return -3;
1916 }
1917 } else {
1918 $this->db->rollback();
1919 return -2;
1920 }
1921 } else {
1922 $this->db->rollback();
1923 return -1;
1924 }
1925 }
1926
1927 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1933 public function fetch_lines()
1934 {
1935 // phpcs:enable
1936 global $mysoc;
1937
1938 $this->lines = array();
1939
1940 // NOTE: This fetch_lines is special because it groups all lines with the same origin_line_id into one line.
1941 // TODO: See if we can restore a common fetch_lines (one line = one record)
1942
1943 $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";
1944 $sql .= ", cd.total_ht, cd.total_localtax1, cd.total_localtax2, cd.total_ttc, cd.total_tva";
1945 $sql .= ", cd.fk_remise_except, cd.fk_product_fournisseur_price as fk_fournprice";
1946 $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";
1947 $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";
1948 $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";
1949 $sql .= ", p.ref as product_ref, p.label as product_label, p.fk_product_type, p.barcode as product_barcode";
1950 $sql .= ", p.weight, p.weight_units, p.length, p.length_units, p.width, p.width_units, p.height, p.height_units";
1951 $sql .= ", p.surface, p.surface_units, p.volume, p.volume_units, p.tosell as product_tosell, p.tobuy as product_tobuy";
1952 $sql .= ", p.tobatch as product_tobatch, p.stockable_product";
1953 $sql .= " FROM ".MAIN_DB_PREFIX."expeditiondet as ed, ".MAIN_DB_PREFIX."commandedet as cd";
1954 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product";
1955 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
1956 $sql .= " AND ed.fk_elementdet = cd.rowid";
1957 $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.
1958
1959 dol_syslog(get_class($this)."::fetch_lines", LOG_DEBUG);
1960 $resql = $this->db->query($sql);
1961 if ($resql) {
1962 include_once DOL_DOCUMENT_ROOT.'/core/lib/price.lib.php';
1963
1964 $num = $this->db->num_rows($resql);
1965 $i = 0;
1966 $line = new ExpeditionLigne($this->db);
1967 $lineindex = 0;
1968 $originline = 0;
1969
1970 $this->total_ht = 0;
1971 $this->total_tva = 0;
1972 $this->total_ttc = 0;
1973 $this->total_localtax1 = 0;
1974 $this->total_localtax2 = 0;
1975
1976 $this->multicurrency_total_ht = 0;
1977 $this->multicurrency_total_tva = 0;
1978 $this->multicurrency_total_ttc = 0;
1979
1980 $shipmentlinebatch = new ExpeditionLineBatch($this->db);
1981
1982 $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.
1983 while ($i < $num) {
1984 $obj = $this->db->fetch_object($resql);
1985
1986 if ($originline > 0 && $originline == $obj->fk_elementdet) {
1987 // '@phan-var-force ExpeditionLigne $line'; // $line from previous loop
1988 $line->entrepot_id = 0; // entrepod_id in details_entrepot
1989 $line->qty_shipped += $obj->qty_shipped;
1990 } else {
1991 $line = new ExpeditionLigne($this->db); // new group to start
1992 $line->entrepot_id = $obj->fk_entrepot; // this is a property of a shipment line
1993 $line->qty_shipped = $obj->qty_shipped; // this is a property of a shipment line
1994 }
1995
1996 $detail_entrepot = new stdClass();
1997 $detail_entrepot->entrepot_id = $obj->fk_entrepot;
1998 $detail_entrepot->qty_shipped = $obj->qty_shipped;
1999 $detail_entrepot->line_id = $obj->line_id;
2000 $line->details_entrepot[] = $detail_entrepot;
2001
2002 $line->line_id = $obj->line_id; // TODO deprecated
2003 $line->rowid = $obj->line_id; // TODO deprecated
2004 $line->id = $obj->line_id;
2005
2006 $line->fk_origin = 'orderline'; // TODO deprecated, we already have element_type that can be use to guess type of line
2007
2008 $line->fk_element = $obj->fk_element;
2009 $line->origin_id = $obj->fk_element;
2010 $line->fk_elementdet = $obj->fk_elementdet;
2011 $line->origin_line_id = $obj->fk_elementdet;
2012 $line->element_type = $obj->element_type;
2013
2014 $line->fk_expedition = $this->id; // id of parent
2015
2016 $line->stockable_product = $obj->stockable_product;
2017 $line->product_type = $obj->product_type;
2018 $line->fk_product = $obj->fk_product;
2019 $line->fk_product_type = $obj->fk_product_type;
2020 $line->ref = $obj->product_ref; // TODO deprecated
2021 $line->product_ref = $obj->product_ref;
2022 $line->product_label = $obj->product_label;
2023 $line->libelle = $obj->product_label; // TODO deprecated
2024 $line->product_barcode = $obj->product_barcode; // Barcode number product
2025 $line->product_tosell = $obj->product_tosell;
2026 $line->product_tobuy = $obj->product_tobuy;
2027 $line->product_tobatch = $obj->product_tobatch;
2028 $line->fk_fournprice = $obj->fk_fournprice;
2029 $line->label = $obj->custom_label;
2030 $line->description = $obj->description;
2031 $line->qty_asked = $obj->qty_asked;
2032 $line->rang = $obj->rang;
2033 $line->weight = $obj->weight;
2034 $line->weight_units = $obj->weight_units;
2035 $line->length = $obj->length;
2036 $line->length_units = $obj->length_units;
2037 $line->width = $obj->width;
2038 $line->width_units = $obj->width_units;
2039 $line->height = $obj->height;
2040 $line->height_units = $obj->height_units;
2041 $line->surface = $obj->surface;
2042 $line->surface_units = $obj->surface_units;
2043 $line->volume = $obj->volume;
2044 $line->volume_units = $obj->volume_units;
2045 $line->stockable_product = $obj->stockable_product;
2046 $line->fk_unit = $obj->fk_unit;
2047
2048 $line->extraparams = !empty($obj->extraparams) ? (array) json_decode($obj->extraparams, true) : array();
2049
2050 $line->pa_ht = $obj->pa_ht;
2051
2052 // Local taxes
2053 $localtax_array = array(0 => $obj->localtax1_type, 1 => $obj->localtax1_tx, 2 => $obj->localtax2_type, 3 => $obj->localtax2_tx);
2054 $localtax1_tx = get_localtax($obj->tva_tx, 1, $this->thirdparty);
2055 $localtax2_tx = get_localtax($obj->tva_tx, 2, $this->thirdparty);
2056
2057 // For invoicing
2058 $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
2059 $line->desc = $obj->description; // We need ->desc because some code into CommonObject use desc (property defined for other elements)
2060 $line->qty = $line->qty_shipped;
2061 $line->total_ht = (float) $tabprice[0];
2062 $line->total_localtax1 = (float) $tabprice[9];
2063 $line->total_localtax2 = (float) $tabprice[10];
2064 $line->total_ttc = (float) $tabprice[2];
2065 $line->total_tva = (float) $tabprice[1];
2066 $line->vat_src_code = $obj->vat_src_code;
2067 $line->tva_tx = $obj->tva_tx;
2068 $line->localtax1_tx = $obj->localtax1_tx;
2069 $line->localtax2_tx = $obj->localtax2_tx;
2070 $line->info_bits = $obj->info_bits;
2071 $line->price = $obj->price;
2072 $line->subprice = $obj->subprice;
2073 $line->fk_remise_except = $obj->fk_remise_except;
2074 $line->remise_percent = $obj->remise_percent;
2075
2076 $this->total_ht += $tabprice[0];
2077 $this->total_tva += $tabprice[1];
2078 $this->total_ttc += $tabprice[2];
2079 $this->total_localtax1 += $tabprice[9];
2080 $this->total_localtax2 += $tabprice[10];
2081
2082 $line->date_start = $this->db->jdate($obj->date_start);
2083 $line->date_end = $this->db->jdate($obj->date_end);
2084
2085 $line->special_code = $obj->special_code;
2086
2087 // Multicurrency
2088 $this->fk_multicurrency = $obj->fk_multicurrency;
2089 $this->multicurrency_code = $obj->multicurrency_code;
2090 $line->multicurrency_subprice = $obj->multicurrency_subprice;
2091 $line->multicurrency_total_ht = $obj->multicurrency_total_ht;
2092 $line->multicurrency_total_tva = $obj->multicurrency_total_tva;
2093 $line->multicurrency_total_ttc = $obj->multicurrency_total_ttc;
2094
2095 $this->multicurrency_total_ht += $obj->multicurrency_total_ht;
2096 $this->multicurrency_total_tva += $obj->multicurrency_total_tva;
2097 $this->multicurrency_total_ttc += $obj->multicurrency_total_ttc;
2098
2099 if ($originline != $obj->fk_elementdet) {
2100 $line->detail_batch = array();
2101 }
2102
2103 // Detail of batch
2104 if (isModEnabled('productbatch') && $obj->line_id > 0 && $obj->product_tobatch > 0) {
2105 $newdetailbatch = $shipmentlinebatch->fetchAll($obj->line_id, $obj->fk_product);
2106
2107 if (is_array($newdetailbatch)) {
2108 if ($originline != $obj->fk_elementdet) {
2109 $line->detail_batch = $newdetailbatch;
2110 } else {
2111 $line->detail_batch = array_merge($line->detail_batch, $newdetailbatch);
2112 }
2113 }
2114 }
2115
2116 // virtual product : find all children stock (group by product id and warehouse id)
2117 if (getDolGlobalInt('PRODUIT_SOUSPRODUITS')) {
2118 $detail_children = array(); // detail by product : array of [warehouse_id => total_qty]
2119 $line_child_list = array();
2120 $res = $line->findAllChild($line->id, $line_child_list, 1);
2121 if ($res > 0) {
2122 foreach ($line_child_list as $child_line) {
2123 foreach ($child_line as $child_obj) {
2124 $child_product_id = (int) $child_obj->fk_product;
2125 $child_warehouse_id = (int) $child_obj->fk_warehouse;
2126
2127 if ($child_warehouse_id > 0) {
2128 // child quantities group by warehouses
2129 if (!isset($detail_children[$child_product_id])) {
2130 $detail_children[$child_product_id] = array();
2131 }
2132 if (!isset($detail_children[$child_product_id][$child_warehouse_id])) {
2133 $detail_children[$child_product_id][$child_warehouse_id] = 0;
2134 }
2135 $detail_children[$child_product_id][$child_warehouse_id] += $child_obj->qty;
2136 }
2137 }
2138 }
2139 }
2140 $line->detail_children = $detail_children;
2141 }
2142
2143 $line->fetch_optionals();
2144
2145 if ($originline != $obj->fk_elementdet) {
2146 $this->lines[$lineindex] = $line;
2147 $lineindex++;
2148 } else {
2149 $line->total_ht += $tabprice[0];
2150 $line->total_localtax1 += $tabprice[9];
2151 $line->total_localtax2 += $tabprice[10];
2152 $line->total_ttc += $tabprice[2];
2153 $line->total_tva += $tabprice[1];
2154 }
2155
2156 $i++;
2157 $originline = $obj->fk_elementdet;
2158 }
2159 $this->db->free($resql);
2160 return 1;
2161 } else {
2162 $this->error = $this->db->error();
2163 return -3;
2164 }
2165 }
2166
2174 public function deleteLine($user, $lineid)
2175 {
2176 global $user;
2177
2178 if ($this->status == self::STATUS_DRAFT) {
2179 $this->db->begin();
2180
2181 $line = new ExpeditionLigne($this->db);
2182
2183 // For triggers
2184 $line->fetch($lineid);
2185
2186 if ($this->id > 0 && (int) $line->fk_expedition !== (int) $this->id) {
2187 $this->db->rollback();
2188 $this->error = 'ErrorLineIDDoesNotMatchWithObjectID';
2189 return -1;
2190 }
2191
2192 if ($line->delete($user) > 0) {
2193 //$this->update_price(1);
2194
2195 $this->db->commit();
2196 return 1;
2197 } else {
2198 $this->db->rollback();
2199 return -1;
2200 }
2201 } else {
2202 $this->error = 'ErrorDeleteLineNotAllowedByObjectStatus';
2203 return -2;
2204 }
2205 }
2206
2207
2214 public function getTooltipContentArray($params)
2215 {
2216 global $conf, $langs;
2217
2218 $langs->load('sendings');
2219
2220 $nofetch = !empty($params['nofetch']);
2221
2222 $datas = array();
2223 $datas['picto'] = img_picto('', $this->picto).' <u class="paddingrightonly">'.$langs->trans("Shipment").'</u>';
2224 if (isset($this->status)) {
2225 $datas['picto'] .= ' '.$this->getLibStatut(5);
2226 }
2227 $datas['ref'] = '<br><b>'.$langs->trans('Ref').':</b> '.$this->ref;
2228 $datas['refcustomer'] = '<br><b>'.$langs->trans('RefCustomer').':</b> '.($this->ref_customer ? $this->ref_customer : $this->ref_client);
2229 if (!$nofetch) {
2230 $langs->load('companies');
2231 if (empty($this->thirdparty)) {
2232 $this->fetch_thirdparty();
2233 }
2234 $datas['customer'] = '<br><b>'.$langs->trans('Customer').':</b> '.$this->thirdparty->getNomUrl(1, '', 0, 1);
2235 }
2236
2237 return $datas;
2238 }
2239
2251 public function getNomUrl($withpicto = 0, $option = '', $max = 0, $short = 0, $notooltip = 0, $save_lastsearch_value = -1)
2252 {
2253 global $langs, $hookmanager;
2254
2255 $result = '';
2256 $params = [
2257 'id' => $this->id,
2258 'objecttype' => $this->element,
2259 'option' => $option,
2260 'nofetch' => 1,
2261 ];
2262 $classfortooltip = 'classfortooltip';
2263 $dataparams = '';
2264 if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
2265 $classfortooltip = 'classforajaxtooltip';
2266 $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
2267 $label = '';
2268 } else {
2269 $label = implode($this->getTooltipContentArray($params));
2270 }
2271
2272 $url = DOL_URL_ROOT.'/expedition/card.php?id='.$this->id;
2273
2274 if ($short) {
2275 return $url;
2276 }
2277
2278 if ($option !== 'nolink') {
2279 // Add param to save lastsearch_values or not
2280 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
2281 if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
2282 $add_save_lastsearch_values = 1;
2283 }
2284 if ($add_save_lastsearch_values) {
2285 $url .= '&save_lastsearch_values=1';
2286 }
2287 }
2288
2289 $linkclose = '';
2290 if (empty($notooltip)) {
2291 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
2292 $label = $langs->trans("Shipment");
2293 $linkclose .= ' alt="'.dolPrintHTMLForAttribute($label).'"';
2294 }
2295 $linkclose .= ($label ? ' title="'.dolPrintHTMLForAttribute($label).'"' : ' title="tocomplete"');
2296 $linkclose .= $dataparams.' class="'.$classfortooltip.'"';
2297 }
2298
2299 $linkstart = '<a href="'.$url.'"';
2300 $linkstart .= $linkclose.'>';
2301 $linkend = '</a>';
2302
2303 $result .= $linkstart;
2304 if ($withpicto) {
2305 $result .= img_object(($notooltip ? '' : $label), ($this->picto ? $this->picto : 'generic'), ($notooltip ? (($withpicto != 2) ? 'class="paddingright"' : '') : 'class="'.(($withpicto != 2) ? 'paddingright ' : '').'"'), 0, 0, $notooltip ? 0 : 1);
2306 }
2307 if ($withpicto != 2) {
2308 $result .= $this->ref;
2309 }
2310 $result .= $linkend;
2311 global $action;
2312 $hookmanager->initHooks(array($this->element . 'dao'));
2313 $parameters = array('id' => $this->id, 'getnomurl' => &$result);
2314 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
2315 if ($reshook > 0) {
2316 $result = $hookmanager->resPrint;
2317 } else {
2318 $result .= $hookmanager->resPrint;
2319 }
2320 return $result;
2321 }
2322
2329 public function getLibStatut($mode = 0)
2330 {
2331 return $this->LibStatut($this->status, $mode);
2332 }
2333
2334 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2342 public function LibStatut($status, $mode)
2343 {
2344 // phpcs:enable
2345 global $langs;
2346
2347 $labelStatus = $langs->transnoentitiesnoconv($this->labelStatus[$status]);
2348 $labelStatusShort = $langs->transnoentitiesnoconv($this->labelStatusShort[$status]);
2349
2350 $statusType = 'status'.$status;
2351 if ($status == self::STATUS_VALIDATED) {
2352 $statusType = 'status4';
2353 }
2354 if ($status == self::STATUS_CLOSED) {
2355 $statusType = 'status6';
2356 }
2357 if ($status == self::STATUS_CANCELED) {
2358 $statusType = 'status9';
2359 }
2360
2361 $signed_label = ' (' . $this->getLibSignedStatus() . ')';
2362 $status_label = $this->signed_status ? $labelStatus . $signed_label : $labelStatus;
2363 $status_label_short = $this->signed_status ? $labelStatusShort . $signed_label : $labelStatusShort;
2364
2365 return dolGetStatus($status_label, $status_label_short, '', $statusType, $mode);
2366 }
2367
2375 public function getKanbanView($option = '', $arraydata = null)
2376 {
2377 global $langs, $conf;
2378
2379 $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
2380
2381 $return = '<div class="box-flex-item box-flex-grow-zero">';
2382 $return .= '<div class="info-box info-box-sm">';
2383 $return .= '<div class="info-box-icon bg-infobox-action">';
2384 $return .= img_picto('', 'order');
2385 $return .= '</div>';
2386 $return .= '<div class="info-box-content">';
2387 $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">'.(method_exists($this, 'getNomUrl') ? $this->getNomUrl() : $this->ref).'</span>';
2388 if ($selected >= 0) {
2389 $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
2390 }
2391 if (property_exists($this, 'thirdparty') && is_object($this->thirdparty)) {
2392 $return .= '<br><div class="info-box-ref tdoverflowmax150">'.$this->thirdparty->getNomUrl(1).'</div>';
2393 }
2394 if (property_exists($this, 'total_ht')) {
2395 $return .= '<div class="info-box-ref amount">'.price($this->total_ht, 0, $langs, 0, -1, -1, $conf->currency).' '.$langs->trans('HT').'</div>';
2396 }
2397 if (method_exists($this, 'getLibStatut')) {
2398 $return .= '<div class="info-box-status">'.$this->getLibStatut(3).'</div>';
2399 }
2400 $return .= '</div>';
2401 $return .= '</div>';
2402 $return .= '</div>';
2403
2404 return $return;
2405 }
2406
2414 public function initAsSpecimen()
2415 {
2416 global $langs;
2417
2418 $now = dol_now();
2419
2420 dol_syslog(get_class($this)."::initAsSpecimen");
2421
2422 $order = new Commande($this->db);
2423 $order->initAsSpecimen();
2424
2425 // Initialise parameters
2426 $this->id = 0;
2427 $this->ref = 'SPECIMEN';
2428 $this->specimen = 1;
2430 $this->livraison_id = 0;
2431 $this->date = $now;
2432 $this->date_creation = $now;
2433 $this->date_valid = $now;
2434 $this->date_delivery = $now + 24 * 3600;
2435 $this->date_expedition = $now + 24 * 3600;
2436
2437 $this->entrepot_id = 0;
2438 $this->fk_delivery_address = 0;
2439 $this->socid = 1;
2440
2441 $this->commande_id = 0;
2442 $this->commande = $order;
2443
2444 $this->origin_id = 1;
2445 $this->origin_type = 'commande';
2446
2447 $this->note_private = 'Private note';
2448 $this->note_public = 'Public note';
2449
2450 $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)
2451 $xnbp = 0;
2452 while ($xnbp < $nbp) {
2453 $line = new ExpeditionLigne($this->db);
2454 $line->product_desc = $langs->trans("Description")." ".$xnbp;
2455 $line->product_label = $langs->trans("Description")." ".$xnbp;
2456 $line->qty = 10;
2457 $line->qty_asked = 5;
2458 $line->qty_shipped = 4;
2459 $line->fk_product = $this->commande->lines[$xnbp]->fk_product;
2460
2461 $line->weight = 1.123456;
2462 $line->weight_units = 0; // kg
2463
2464 $line->volume = 2.34567;
2465 $line->volume_unit = 0;
2466
2467 $this->lines[] = $line;
2468 $xnbp++;
2469 }
2470
2471 return 1;
2472 }
2473
2474 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2483 public function set_date_livraison($user, $delivery_date)
2484 {
2485 // phpcs:enable
2486 return $this->setDeliveryDate($user, $delivery_date);
2487 }
2488
2496 public function setDeliveryDate($user, $delivery_date)
2497 {
2498 if ($user->hasRight('expedition', 'creer')) {
2499 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition";
2500 $sql .= " SET date_delivery = ".($delivery_date ? "'".$this->db->idate($delivery_date)."'" : 'null');
2501 $sql .= " WHERE rowid = ".((int) $this->id);
2502
2503 dol_syslog(get_class($this)."::setDeliveryDate", LOG_DEBUG);
2504 $resql = $this->db->query($sql);
2505 if ($resql) {
2506 $this->date_delivery = $delivery_date;
2507 return 1;
2508 } else {
2509 $this->error = $this->db->error();
2510 return -1;
2511 }
2512 } else {
2513 return -2;
2514 }
2515 }
2516
2524 public function setShippingDate($user, $shipping_date)
2525 {
2526 if ($user->hasRight('expedition', 'creer')) {
2527 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition";
2528 $sql .= " SET date_expedition = ".($shipping_date ? "'".$this->db->idate($shipping_date)."'" : 'null');
2529 $sql .= " WHERE rowid = ".((int) $this->id);
2530
2531 dol_syslog(get_class($this)."::setShippingDate", LOG_DEBUG);
2532 $resql = $this->db->query($sql);
2533 if ($resql) {
2534 $this->date_shipping = $shipping_date;
2535 return 1;
2536 } else {
2537 $this->error = $this->db->error();
2538 return -1;
2539 }
2540 } else {
2541 return -2;
2542 }
2543 }
2544
2545 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2551 public function fetch_delivery_methods()
2552 {
2553 // phpcs:enable
2554 global $langs;
2555 $this->meths = array();
2556
2557 $sql = "SELECT em.rowid, em.code, em.libelle as label";
2558 $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2559 $sql .= " WHERE em.active = 1";
2560 $sql .= " ORDER BY em.libelle ASC";
2561
2562 $resql = $this->db->query($sql);
2563 if ($resql) {
2564 while ($obj = $this->db->fetch_object($resql)) {
2565 $label = $langs->trans('SendingMethod'.$obj->code);
2566 $this->meths[$obj->rowid] = ($label != 'SendingMethod'.$obj->code ? $label : $obj->label);
2567 }
2568 }
2569 }
2570
2571 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2578 public function list_delivery_methods($id = 0)
2579 {
2580 // phpcs:enable
2581 global $langs;
2582
2583 $this->listmeths = array();
2584 $i = 0;
2585
2586 $sql = "SELECT em.rowid, em.code, em.libelle as label, em.description, em.tracking, em.active";
2587 $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2588 if (!empty($id)) {
2589 $sql .= " WHERE em.rowid=".((int) $id);
2590 }
2591
2592 $resql = $this->db->query($sql);
2593 if ($resql) {
2594 while ($obj = $this->db->fetch_object($resql)) {
2595 $this->listmeths[$i]['rowid'] = $obj->rowid;
2596 $this->listmeths[$i]['code'] = $obj->code;
2597 $label = $langs->trans('SendingMethod'.$obj->code);
2598 $this->listmeths[$i]['libelle'] = ($label != 'SendingMethod'.$obj->code ? $label : $obj->label);
2599 $this->listmeths[$i]['description'] = $obj->description;
2600 $this->listmeths[$i]['tracking'] = $obj->tracking;
2601 $this->listmeths[$i]['active'] = $obj->active;
2602 $i++;
2603 }
2604 }
2605 }
2606
2613 public function getUrlTrackingStatus($value = '')
2614 {
2615 if (!empty($this->shipping_method_id)) {
2616 $sql = "SELECT em.code, em.tracking";
2617 $sql .= " FROM ".MAIN_DB_PREFIX."c_shipment_mode as em";
2618 $sql .= " WHERE em.rowid = ".((int) $this->shipping_method_id);
2619
2620 $resql = $this->db->query($sql);
2621 if ($resql) {
2622 if ($obj = $this->db->fetch_object($resql)) {
2623 $tracking = $obj->tracking;
2624 }
2625 }
2626 }
2627
2628 if (!empty($tracking) && !empty($value)) {
2629 $url = str_replace('{TRACKID}', $value, $tracking);
2630 $this->tracking_url = sprintf('<a target="_blank" rel="noopener noreferrer" href="%s">%s</a>', $url, ($value ? $value : 'url'));
2631 } else {
2632 $this->tracking_url = $value;
2633 }
2634 }
2635
2641 public function setClosed()
2642 {
2643 global $user;
2644
2645 $error = 0;
2646
2647 // Protection. This avoid to move stock later when we should not
2648 if ($this->status == self::STATUS_CLOSED) {
2649 return 0;
2650 }
2651
2652 $this->db->begin();
2653
2654 $sql = "UPDATE ".MAIN_DB_PREFIX."expedition SET fk_statut = ".self::STATUS_CLOSED;
2655 if (empty($this->date_shipping)) { // Date of real shipment was not yet set, we force it on closing
2656 $sql .= ", date_expedition = '".$this->db->escape($this->db->idate(dol_now()))."'";
2657 }
2658 $sql .= " WHERE rowid = ".((int) $this->id)." AND fk_statut > 0";
2659
2660 $resql = $this->db->query($sql);
2661 if ($resql) {
2662 // Set order billed if 100% of order is shipped (qty in shipment lines match qty in order lines)
2663 if ($this->origin == 'commande' && $this->origin_id > 0) {
2664 $order = new Commande($this->db);
2665 $order->fetch($this->origin_id);
2666
2667 $order->loadExpeditions(self::STATUS_CLOSED); // Fill $order->expeditions = array(orderlineid => qty)
2668
2669 $shipments_match_order = 1;
2670 foreach ($order->lines as $line) {
2671 $lineid = $line->id;
2672 $qty = $line->qty;
2673 if (($line->product_type == 0 || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) && $order->expeditions[$lineid] != $qty) {
2674 $shipments_match_order = 0;
2675 $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';
2676 dol_syslog($text);
2677 break;
2678 }
2679 }
2680 if ($shipments_match_order) {
2681 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');
2682 // We close the order
2683 $order->cloture($user, 0, 0); // 0 = do not check the close permission: this is an automatic action of the shipment closing. Note this may also create an invoice if module workflow ask it
2684 }
2685 }
2686
2687 $this->statut = self::STATUS_CLOSED; // Will be revert to STATUS_VALIDATED at end if there is a rollback
2688 $this->status = self::STATUS_CLOSED; // Will be revert to STATUS_VALIDATED at end if there is a rollback
2689
2690 // If stock increment is done on closing
2691 if (!$error && isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
2692 $result = $this->manageStockMvtOnEvt($user);
2693 if ($result < 0) {
2694 $error++;
2695 }
2696 }
2697
2698 // Call trigger
2699 if (!$error) {
2700 $result = $this->call_trigger('SHIPPING_CLOSED', $user);
2701 if ($result < 0) {
2702 $error++;
2703 }
2704 }
2705 } else {
2706 dol_print_error($this->db);
2707 $error++;
2708 }
2709
2710 if (!$error) {
2711 $this->db->commit();
2712 return 1;
2713 } else {
2714 $this->statut = self::STATUS_VALIDATED;
2716
2717 $this->db->rollback();
2718 return -1;
2719 }
2720 }
2721
2730 private function manageStockMvtOnEvt($user, $labelmovement = 'ShipmentClassifyClosedInDolibarr')
2731 {
2732 global $langs;
2733
2734 $error = 0;
2735
2736 require_once DOL_DOCUMENT_ROOT . '/product/stock/class/mouvementstock.class.php';
2737
2738 $langs->load("agenda");
2739
2740 // Loop on each product line to add a stock movement
2741 $sql = "SELECT";
2742 $sql .= " ed.rowid as edid, ed.fk_product, ed.qty, ed.fk_entrepot";
2743 $sql .= ", cd.rowid as cdid";
2744 $sql .= ", cd.subprice";
2745 $sql .= ", edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock";
2746 $sql .= ", e.ref";
2747 $sql .= " FROM " . $this->db->prefix() . "expeditiondet as ed";
2748 $sql .= " LEFT JOIN " . $this->db->prefix() . "commandedet as cd ON cd.rowid = ed.fk_elementdet";
2749 $sql .= " LEFT JOIN " . $this->db->prefix() . "expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid";
2750 $sql .= " INNER JOIN " . $this->db->prefix() . "expedition as e ON ed.fk_expedition = e.rowid";
2751 $sql .= " WHERE ed.fk_expedition = " . ((int) $this->id);
2752 //$sql .= " AND cd.rowid = ed.fk_elementdet";
2753
2754 dol_syslog(get_class($this) . "::valid select details", LOG_DEBUG);
2755 $resql = $this->db->query($sql);
2756 if ($resql) {
2757 $cpt = $this->db->num_rows($resql);
2758 for ($i = 0; $i < $cpt; $i++) {
2759 $obj = $this->db->fetch_object($resql);
2760 if (empty($obj->edbrowid)) {
2761 $qty = $obj->qty;
2762 } else {
2763 $qty = $obj->edbqty;
2764 }
2765 if ($qty <= 0 || ($qty < 0 && !getDolGlobalInt('SHIPMENT_ALLOW_NEGATIVE_QTY'))) {
2766 continue;
2767 }
2768 dol_syslog(get_class($this) . "::valid movement index " . $i . " ed.rowid=" . $obj->edid . " edb.rowid=" . $obj->edbrowid);
2769
2770 $mouvS = new MouvementStock($this->db);
2771 $mouvS->origin = &$this;
2772 $mouvS->setOrigin($this->element, $this->id, $obj->cdid, $obj->edid);
2773
2774 if (empty($obj->edbrowid)) {
2775 // line without batch detail
2776
2777 // 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
2778 $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, $qty, $obj->subprice, $langs->trans($labelmovement, $obj->ref));
2779 if ($result < 0) {
2780 $this->error = $mouvS->error;
2781 $this->errors = $mouvS->errors;
2782 $error++;
2783 break;
2784 }
2785 } else {
2786 // line with batch detail
2787
2788 // 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
2789 $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);
2790 if ($result < 0) {
2791 $this->error = $mouvS->error;
2792 $this->errors = $mouvS->errors;
2793 $error++;
2794 break;
2795 }
2796 }
2797
2798 // 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
2799 // having a lot1/qty=X and lot2/qty=-X, so 0 but we must not loose repartition of different lot.
2800 $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)";
2801 $resqldelete = $this->db->query($sqldelete);
2802 // We do not test error, it can fails if there is child in batch details
2803 }
2804 } else {
2805 $this->error = $this->db->lasterror();
2806 $this->errors[] = $this->db->lasterror();
2807 $error++;
2808 }
2809
2810 if (!$error) {
2811 return 1;
2812 } else {
2813 return -1;
2814 }
2815 }
2816
2822 public function setBilled()
2823 {
2824 global $user;
2825 $error = 0;
2826
2827 $this->db->begin();
2828
2829 $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET billed = 1';
2830 $sql .= " WHERE rowid = ".((int) $this->id).' AND fk_statut > 0';
2831
2832 $resql = $this->db->query($sql);
2833 if ($resql) {
2834 $this->billed = 1;
2835
2836 // Call trigger
2837 $result = $this->call_trigger('SHIPPING_BILLED', $user);
2838 if ($result < 0) {
2839 $this->billed = 0;
2840 $error++;
2841 }
2842 } else {
2843 $error++;
2844 $this->errors[] = $this->db->lasterror;
2845 }
2846
2847 if (empty($error)) {
2848 $this->db->commit();
2849 return 1;
2850 } else {
2851 $this->db->rollback();
2852 return -1;
2853 }
2854 }
2855
2863 public function setDraft($user, $notrigger = 0)
2864 {
2865 // Protection
2866 if ($this->status <= self::STATUS_DRAFT) {
2867 return 0;
2868 }
2869
2870 return $this->setStatusCommon($user, self::STATUS_DRAFT, $notrigger, 'SHIPMENT_UNVALIDATE');
2871 }
2872
2878 public function reOpen()
2879 {
2880 global $langs, $user;
2881
2882 $error = 0;
2883
2884 // Protection. This avoid to move stock later when we should not
2885 if ($this->status == self::STATUS_VALIDATED) {
2886 return 0;
2887 }
2888
2889 $this->db->begin();
2890
2891 $oldbilled = $this->billed;
2892
2893 $sql = 'UPDATE '.MAIN_DB_PREFIX.'expedition SET fk_statut = 1';
2894 $sql .= " WHERE rowid = ".((int) $this->id).' AND fk_statut > 0';
2895
2896 $resql = $this->db->query($sql);
2897 if ($resql) {
2898 $this->statut = self::STATUS_VALIDATED;
2900 $this->billed = 0;
2901
2902 // If stock increment is done on closing
2903 if (isModEnabled('stock') && getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
2904 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
2905
2906 $langs->load("agenda");
2907
2908 // Loop on each product line to add a stock movement
2909 // TODO possibilite d'expedier a partir d'une propale ou autre origine
2910 $sql = "SELECT cd.fk_product, cd.subprice,";
2911 $sql .= " ed.rowid, ed.qty, ed.fk_entrepot,";
2912 $sql .= " edb.rowid as edbrowid, edb.eatby, edb.sellby, edb.batch, edb.qty as edbqty, edb.fk_origin_stock";
2913 $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd,";
2914 $sql .= " ".MAIN_DB_PREFIX."expeditiondet as ed";
2915 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."expeditiondet_batch as edb on edb.fk_expeditiondet = ed.rowid";
2916 $sql .= " WHERE ed.fk_expedition = ".((int) $this->id);
2917 $sql .= " AND cd.rowid = ed.fk_elementdet";
2918
2919 dol_syslog(get_class($this)."::valid select details", LOG_DEBUG);
2920 $resql = $this->db->query($sql);
2921 if ($resql) {
2922 $cpt = $this->db->num_rows($resql);
2923 for ($i = 0; $i < $cpt; $i++) {
2924 $obj = $this->db->fetch_object($resql);
2925 if (empty($obj->edbrowid)) {
2926 $qty = $obj->qty;
2927 } else {
2928 $qty = $obj->edbqty;
2929 }
2930 if ($qty <= 0) {
2931 continue;
2932 }
2933 dol_syslog(get_class($this)."::reopen expedition movement index ".$i." ed.rowid=".$obj->rowid." edb.rowid=".$obj->edbrowid);
2934
2935 //var_dump($this->lines[$i]);
2936 $mouvS = new MouvementStock($this->db);
2937 $mouvS->origin = &$this;
2938 $mouvS->setOrigin($this->element, $this->id);
2939
2940 if (empty($obj->edbrowid)) {
2941 // line without batch detail
2942
2943 // 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
2944 $result = $mouvS->livraison($user, $obj->fk_product, $obj->fk_entrepot, -$qty, $obj->subprice, $langs->trans("ShipmentUnClassifyCloseddInDolibarr", $this->ref));
2945 if ($result < 0) {
2946 $this->error = $mouvS->error;
2947 $this->errors = $mouvS->errors;
2948 $error++;
2949 break;
2950 }
2951 } else {
2952 // line with batch detail
2953
2954 // 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
2955 $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);
2956 if ($result < 0) {
2957 $this->error = $mouvS->error;
2958 $this->errors = $mouvS->errors;
2959 $error++;
2960 break;
2961 }
2962 }
2963 }
2964 } else {
2965 $this->error = $this->db->lasterror();
2966 $error++;
2967 }
2968 }
2969
2970 if (!$error) {
2971 // Call trigger
2972 $result = $this->call_trigger('SHIPPING_REOPEN', $user);
2973 if ($result < 0) {
2974 $error++;
2975 }
2976 }
2977 } else {
2978 $error++;
2979 $this->errors[] = $this->db->lasterror();
2980 }
2981
2982 if (!$error) {
2983 $this->db->commit();
2984 return 1;
2985 } else {
2986 $this->statut = self::STATUS_CLOSED;
2987 $this->status = self::STATUS_CLOSED;
2988 $this->billed = $oldbilled;
2989 $this->db->rollback();
2990 return -1;
2991 }
2992 }
2993
3005 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null)
3006 {
3007 $outputlangs->load("products");
3008
3009 if (!dol_strlen($modele)) {
3010 $modele = 'rouget';
3011
3012 if (!empty($this->model_pdf)) {
3013 $modele = $this->model_pdf;
3014 } elseif (getDolGlobalString('EXPEDITION_ADDON_PDF')) {
3015 $modele = getDolGlobalString('EXPEDITION_ADDON_PDF');
3016 }
3017 }
3018
3019 $modelpath = "core/modules/expedition/doc/";
3020
3021 $this->fetch_origin();
3022
3023 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams);
3024 }
3025
3034 public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
3035 {
3036 $tables = array(
3037 'expedition'
3038 );
3039
3040 return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
3041 }
3042}
$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