dolibarr 23.0.4
product.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2001-2007 Rodolphe Quiedeville <rodolphe@quiedeville.org>
3 * Copyright (C) 2004-2014 Laurent Destailleur <eldy@users.sourceforge.net>
4 * Copyright (C) 2005-2015 Regis Houssin <regis.houssin@inodbox.com>
5 * Copyright (C) 2006 Andre Cianfarani <acianfa@free.fr>
6 * Copyright (C) 2007-2011 Jean Heimburger <jean@tiaris.info>
7 * Copyright (C) 2010-2018 Juanjo Menent <jmenent@2byte.es>
8 * Copyright (C) 2012 Cedric Salvador <csalvador@gpcsolutions.fr>
9 * Copyright (C) 2013-2014 Cedric GROSS <c.gross@kreiz-it.fr>
10 * Copyright (C) 2013-2016 Marcos García <marcosgdf@gmail.com>
11 * Copyright (C) 2011-2021 Open-DSI <support@open-dsi.fr>
12 * Copyright (C) 2014 Henry Florian <florian.henry@open-concept.pro>
13 * Copyright (C) 2014-2016 Philippe Grand <philippe.grand@atoo-net.com>
14 * Copyright (C) 2014 Ion agorria <ion@agorria.com>
15 * Copyright (C) 2016-2024 Ferran Marcet <fmarcet@2byte.es>
16 * Copyright (C) 2017 Gustavo Novaro
17 * Copyright (C) 2019-2025 Frédéric France <frederic.france@free.fr>
18 * Copyright (C) 2023 Benjamin Falière <benjamin.faliere@altairis.fr>
19 * Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
20 * Copyright (C) 2025 Lenin Rivas <lenin.rivas777@gmail.com>
21 * Copyright (C) 2026 Anthony Berton <anthony.berton@bb2a.fr>
22 *
23 * This program is free software; you can redistribute it and/or modify
24 * it under the terms of the GNU General Public License as published by
25 * the Free Software Foundation; either version 3 of the License, or
26 * (at your option) any later version.
27 *
28 * This program is distributed in the hope that it will be useful,
29 * but WITHOUT ANY WARRANTY; without even the implied warranty of
30 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 * GNU General Public License for more details.
32 *
33 * You should have received a copy of the GNU General Public License
34 * along with this program. If not, see <https://www.gnu.org/licenses/>.
35 */
36
42require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
43require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
44require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
45require_once DOL_DOCUMENT_ROOT.'/product/stock/class/productlot.class.php';
46require_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php';
47
51class Product extends CommonObject
52{
57 const SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY = 1;
58 const SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY = 2;
59 const SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT = 3;
60
65 public $TRIGGER_PREFIX = 'PRODUCT';
66
70 public $element = 'product';
71
75 public $table_element = 'product';
76
80 public $fk_element = 'fk_product';
81
85 protected $childtables = array(
86 'supplier_proposaldet' => array('name' => 'SupplierProposal', 'parent' => 'supplier_proposal', 'parentkey' => 'fk_supplier_proposal'),
87 'propaldet' => array('name' => 'Proposal', 'parent' => 'propal', 'parentkey' => 'fk_propal'),
88 'commandedet' => array('name' => 'Order', 'parent' => 'commande', 'parentkey' => 'fk_commande'),
89 'facturedet' => array('name' => 'Invoice', 'parent' => 'facture', 'parentkey' => 'fk_facture'),
90 'contratdet' => array('name' => 'Contract', 'parent' => 'contrat', 'parentkey' => 'fk_contrat'),
91 'facture_fourn_det' => array('name' => 'SupplierInvoice', 'parent' => 'facture_fourn', 'parentkey' => 'fk_facture_fourn'),
92 'commande_fournisseurdet' => array('name' => 'SupplierOrder', 'parent' => 'commande_fournisseur', 'parentkey' => 'fk_commande'),
93 'mrp_production' => array('name' => 'Mo', 'parent' => 'mrp_mo', 'parentkey' => 'fk_mo', 'enabled' => 'isModEnabled("mrp")'),
94 'bom_bom' => array('name' => 'BOM', 'enabled' => 'isModEnabled("bom")'),
95 'bom_bomline' => array('name' => 'BOMLine', 'parent' => 'bom_bom', 'parentkey' => 'fk_bom', 'enabled' => 'isModEnabled("bom")'),
96 );
97
103 public $picto = 'product';
104
108 protected $table_ref_field = 'ref';
109
114 public $regeximgext = '\.gif|\.jpg|\.jpeg|\.png|\.bmp|\.webp|\.xpm|\.xbm';
115
121 public $libelle;
122
128 public $label;
129
135 public $description;
136
142 public $other;
143
149 public $type = self::TYPE_PRODUCT;
150
156 public $price;
157
161 public $price_formated; // used by takepos/ajax/ajax.php
162
168 public $price_ttc;
169
173 public $price_ttc_formated; // used by takepos/ajax/ajax.php
174
180 public $price_min;
181
187 public $price_min_ttc;
188
193 public $price_base_type;
197 public $price_label;
198
200
203 public $multiprices = array();
207 public $multiprices_ttc = array();
211 public $multiprices_base_type = array();
215 public $multiprices_default_vat_code = array();
219 public $multiprices_min = array();
223 public $multiprices_min_ttc = array();
227 public $multiprices_tva_tx = array();
231 public $multiprices_recuperableonly = array();
232
234
237 public $price_by_qty;
241 public $prices_by_qty = array();
245 public $prices_by_qty_id = array();
249 public $prices_by_qty_list = array();
250
254 public $level;
255
259 public $multilangs = array();
260
264 public $default_vat_code;
265
269 public $tva_tx;
270
274 public $tva_npr = 0;
275
279 public $remise_percent;
280
284 public $localtax1_tx;
288 public $localtax2_tx;
292 public $localtax1_type;
296 public $localtax2_type;
297
298 // Properties set by get_buyprice() for return
299
303 public $desc_supplier;
307 public $vatrate_supplier;
311 public $default_vat_code_supplier;
312
316 public $fourn_multicurrency_price;
317
321 public $fourn_multicurrency_unitprice;
325 public $fourn_multicurrency_tx;
329 public $fourn_multicurrency_id;
333 public $fourn_multicurrency_code;
334
338 public $packaging;
339
340
347 public $lifetime;
348
355 public $qc_frequency;
356
362 public $stock_reel = 0;
363
369 public $stock_theorique;
370
376 public $cost_price;
377
383 public $pmp;
384
390 public $seuil_stock_alerte = 0;
391
395 public $desiredstock = 0;
396
400 public $duration_value;
404 public $duration_unit;
408 public $duration;
409
413 public $fk_default_workstation;
414
420 public $status = 0;
421
428 public $tosell;
429
435 public $status_buy = 0;
436
443 public $tobuy;
444
450 public $finished;
451
457 public $fk_default_bom;
458
464 public $product_fourn_price_id;
465
471 public $buyprice;
472
478 public $tobatch;
479
480
486 public $status_batch = 0;
487
493 public $sell_or_eat_by_mandatory = 0;
494
500 public $batch_mask = '';
501
507 public $customcode;
508
514 public $url;
515
517
520 public $weight;
521
525 public $weight_units; // scale -3, 0, 3, 6
529 public $length;
533 public $length_units; // scale -3, 0, 3, 6
537 public $width;
541 public $width_units; // scale -3, 0, 3, 6
545 public $height;
549 public $height_units; // scale -3, 0, 3, 6
553 public $surface;
557 public $surface_units; // scale -3, 0, 3, 6
561 public $volume;
565 public $volume_units; // scale -3, 0, 3, 6
566
570 public $net_measure;
574 public $net_measure_units; // scale -3, 0, 3, 6
575
579 public $accountancy_code_sell;
583 public $accountancy_code_sell_intra;
587 public $accountancy_code_sell_export;
591 public $accountancy_code_buy;
595 public $accountancy_code_buy_intra;
599 public $accountancy_code_buy_export;
600
604 public $barcode;
605
609 public $barcode_type;
610
614 public $barcode_type_code;
615
619 public $stats_propale = array();
620
624 public $stats_commande = array();
625
629 public $stats_contrat = array();
630
634 public $stats_facture = array();
635
639 public $stats_proposal_supplier = array();
640
644 public $stats_commande_fournisseur = array();
645
649 public $stats_expedition = array();
650
654 public $stats_reception = array();
655
659 public $stats_mo = array();
660
664 public $stats_bom = array();
665
669 public $stats_mrptoconsume = array();
670
674 public $stats_mrptoproduce = array();
675
679 public $stats_facturerec = array();
680
684 public $stats_facture_fournisseur = array();
685
689 public $stats_facturefournrec = array();
690
694 public $imgWidth;
698 public $imgHeight;
699
704 public $product_fourn_id;
705
710 public $product_id_already_linked;
711
716 public $nbphoto = 0;
717
721 public $stock_warehouse = array();
722
726 public $fk_default_warehouse;
727
731 public $fk_price_expression;
732
737 public $fourn_qty;
738
743 public $fourn_pu;
744
749 public $fourn_price_base_type;
750
754 public $fourn_socid;
755
761 public $ref_fourn;
762
766 public $ref_supplier;
767
773 public $fk_unit;
774
780 public $price_autogen = 0;
781
787 public $sousprods = array();
788
792 public $res;
793
794
800 public $is_object_used;
801
811 public $is_sousproduit_qty;
812
823 public $is_sousproduit_incdec;
824
828 public $mandatory_period;
829
835 public $stockable_product = 1;
836
865 public $fields = array(
866 'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'index' => 1, 'position' => 1, 'comment' => 'Id'),
867 'ref' => array('type' => 'varchar(128)', 'label' => 'Ref', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'showoncombobox' => 1, 'index' => 1, 'position' => 10, 'searchall' => 1, 'comment' => 'Reference of object'),
868 'entity' => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'visible' => 0, 'default' => '1', 'notnull' => 1, 'index' => 1, 'position' => 5),
869 'label' => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'showoncombobox' => 2, 'position' => 15, 'csslist' => 'tdoverflowmax250'),
870 'barcode' => array('type' => 'varchar(255)', 'label' => 'Barcode', 'enabled' => 'isModEnabled("barcode")', 'position' => 20, 'visible' => -1, 'showoncombobox' => 3, 'cssview' => 'tdwordbreak', 'csslist' => 'tdoverflowmax125'),
871 'fk_barcode_type' => array('type' => 'integer', 'label' => 'BarcodeType', 'enabled' => 1, 'position' => 21, 'notnull' => 0, 'visible' => -1,),
872 'note_public' => array('type' => 'html', 'label' => 'NotePublic', 'enabled' => 1, 'visible' => 0, 'position' => 61),
873 'note' => array('type' => 'html', 'label' => 'NotePrivate', 'enabled' => 1, 'visible' => 0, 'position' => 62),
874 'datec' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 500),
875 'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 501),
876 //'date_valid' =>array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-2, 'position'=>502),
877 'fk_user_author' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'UserAuthor', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 510, 'foreignkey' => 'llx_user.rowid'),
878 'fk_user_modif' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'UserModif', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'position' => 511),
879 //'fk_user_valid' =>array('type'=>'integer', 'label'=>'UserValidation', 'enabled'=>1, 'visible'=>-1, 'position'=>512),
880 'localtax1_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax1tx', 'enabled' => 1, 'position' => 150, 'notnull' => 0, 'visible' => -1,),
881 'localtax1_type' => array('type' => 'varchar(10)', 'label' => 'Localtax1type', 'enabled' => 1, 'position' => 155, 'notnull' => 1, 'visible' => -1,),
882 'localtax2_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax2tx', 'enabled' => 1, 'position' => 160, 'notnull' => 0, 'visible' => -1,),
883 'localtax2_type' => array('type' => 'varchar(10)', 'label' => 'Localtax2type', 'enabled' => 1, 'position' => 165, 'notnull' => 1, 'visible' => -1,),
884 'last_main_doc' => array('type' => 'varchar(255)', 'label' => 'LastMainDoc', 'enabled' => 1, 'visible' => -1, 'position' => 170),
885 'import_key' => array('type' => 'varchar(14)', 'label' => 'ImportId', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'index' => 0, 'position' => 1000),
886 //'tosell' =>array('type'=>'integer', 'label'=>'Status', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'default'=>'0', 'index'=>1, 'position'=>1000, 'arrayofkeyval'=>array(0=>'Draft', 1=>'Active', -1=>'Cancel')),
887 //'tobuy' =>array('type'=>'integer', 'label'=>'Status', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'default'=>'0', 'index'=>1, 'position'=>1000, 'arrayofkeyval'=>array(0=>'Draft', 1=>'Active', -1=>'Cancel')),
888 'mandatory_period' => array('type' => 'integer', 'label' => 'mandatoryperiod', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'default' => '0', 'index' => 1, 'position' => 1000),
889 'stockable_product' => array('type' => 'integer', 'label' => 'stockable_product', 'enabled' => 1, 'visible' => 1, 'default' => '1', 'notnull' => 1, 'index' => 1, 'position' => 502),
890 );
891
895 const TYPE_PRODUCT = 0;
899 const TYPE_SERVICE = 1;
900
904 const DISABLED_STOCK = 0;
905 const ENABLED_STOCK = 1;
906
912 public function __construct($db)
913 {
914 $this->db = $db;
915
916 $this->ismultientitymanaged = 1;
917 $this->isextrafieldmanaged = 1;
918
919 $this->canvas = '';
920 }
921
927 public function check()
928 {
929 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
930 $this->ref = trim($this->ref);
931 } else {
932 $this->ref = dol_sanitizeFileName(stripslashes($this->ref));
933 }
934
935 $err = 0;
936 if (dol_strlen(trim($this->ref)) == 0) {
937 $err++;
938 }
939
940 if (dol_strlen(trim($this->label)) == 0) {
941 $err++;
942 }
943
944 if ($err > 0) {
945 return 0;
946 } else {
947 return 1;
948 }
949 }
950
958 public function create($user, $notrigger = 0)
959 {
960 global $conf, $langs;
961
962 $error = 0;
963
964 // Clean parameters
965 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
966 $this->ref = trim($this->ref);
967 } else {
968 $this->ref = dol_sanitizeFileName(dol_string_nospecial(trim($this->ref)));
969 }
970 $this->label = trim($this->label);
971 $this->price_ttc = (float) price2num($this->price_ttc);
972 $this->price = (float) price2num($this->price);
973 $this->price_min_ttc = (float) price2num($this->price_min_ttc);
974 $this->price_min = (float) price2num($this->price_min);
975 $this->price_label = trim($this->price_label);
976 if (empty($this->tva_tx)) {
977 $this->tva_tx = 0;
978 }
979 if (empty($this->tva_npr)) {
980 $this->tva_npr = 0;
981 }
982 // Local taxes
983 if (empty($this->localtax1_tx)) {
984 $this->localtax1_tx = 0;
985 }
986 if (empty($this->localtax2_tx)) {
987 $this->localtax2_tx = 0;
988 }
989 if (empty($this->localtax1_type)) {
990 $this->localtax1_type = '0';
991 }
992 if (empty($this->localtax2_type)) {
993 $this->localtax2_type = '0';
994 }
995 // Price
996 if (empty($this->price_base_type) && getDolGlobalString('PRODUCT_PRICE_BASE_TYPE')) {
997 $this->price_base_type = getDolGlobalString('PRODUCT_PRICE_BASE_TYPE');
998 }
999 if (empty($this->price)) {
1000 $this->price = 0;
1001 }
1002 if (empty($this->price_min)) {
1003 $this->price_min = 0;
1004 }
1005 // Price by quantity
1006 if (empty($this->price_by_qty)) {
1007 $this->price_by_qty = 0;
1008 }
1009
1010 if (empty($this->status)) {
1011 $this->status = 0;
1012 }
1013 if (empty($this->status_buy)) {
1014 $this->status_buy = 0;
1015 }
1016 if (empty($this->stockable_product)) {
1017 $this->stockable_product = 0;
1018 }
1019
1020 $price_ht = 0;
1021 $price_ttc = 0;
1022 $price_min_ht = 0;
1023 $price_min_ttc = 0;
1024
1025 //
1026 if ($this->price_base_type == 'TTC' && $this->price_ttc > 0) {
1027 $price_ttc = price2num($this->price_ttc, 'MU');
1028 $price_ht = price2num($this->price_ttc / (1 + ($this->tva_tx / 100)), 'MU');
1029 }
1030
1031 //
1032 if ($this->price_base_type != 'TTC' && $this->price > 0) {
1033 $price_ht = price2num($this->price, 'MU');
1034 $price_ttc = price2num($this->price * (1 + ($this->tva_tx / 100)), 'MU');
1035 }
1036
1037 //
1038 if (($this->price_min_ttc > 0) && ($this->price_base_type == 'TTC')) {
1039 $price_min_ttc = price2num($this->price_min_ttc, 'MU');
1040 $price_min_ht = price2num($this->price_min_ttc / (1 + ($this->tva_tx / 100)), 'MU');
1041 }
1042
1043 //
1044 if (($this->price_min > 0) && ($this->price_base_type != 'TTC')) {
1045 $price_min_ht = price2num($this->price_min, 'MU');
1046 $price_min_ttc = price2num($this->price_min * (1 + ($this->tva_tx / 100)), 'MU');
1047 }
1048
1049 $this->accountancy_code_buy = trim((string) $this->accountancy_code_buy);
1050 $this->accountancy_code_buy_intra = trim((string) $this->accountancy_code_buy_intra);
1051 $this->accountancy_code_buy_export = trim((string) $this->accountancy_code_buy_export);
1052 $this->accountancy_code_sell = trim((string) $this->accountancy_code_sell);
1053 $this->accountancy_code_sell_intra = trim((string) $this->accountancy_code_sell_intra);
1054 $this->accountancy_code_sell_export = trim((string) $this->accountancy_code_sell_export);
1055
1056 // Normalize the accountancy codes the way the admin dropdown does it, so an API client that
1057 // sends '606111000' ends up with the same '606111' value the GUI stores (see issue #32343).
1058 // When ACCOUNTING_MANAGE_ZERO is on, trailing zeros are part of the code and must be kept.
1059 if (!getDolGlobalString('ACCOUNTING_MANAGE_ZERO')) {
1060 require_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
1061 $this->accountancy_code_buy = clean_account($this->accountancy_code_buy);
1062 $this->accountancy_code_buy_intra = clean_account($this->accountancy_code_buy_intra);
1063 $this->accountancy_code_buy_export = clean_account($this->accountancy_code_buy_export);
1064 $this->accountancy_code_sell = clean_account($this->accountancy_code_sell);
1065 $this->accountancy_code_sell_intra = clean_account($this->accountancy_code_sell_intra);
1066 $this->accountancy_code_sell_export = clean_account($this->accountancy_code_sell_export);
1067 }
1068
1069 // Barcode value
1070 $this->barcode = trim($this->barcode);
1071 $this->mandatory_period = empty($this->mandatory_period) ? 0 : $this->mandatory_period;
1072 // Check parameters
1073 if (empty($this->label)) {
1074 $langs->load('errors');
1075 $this->errors[] = $langs->trans('ErrorMandatoryParametersNotProvided');
1076 return -1;
1077 }
1078
1079 if (empty($this->ref) || $this->ref == 'auto') {
1080 // Load object modCodeProduct
1081 $module = getDolGlobalString('PRODUCT_CODEPRODUCT_ADDON', 'mod_codeproduct_leopard');
1082 if ($module != 'mod_codeproduct_leopard') { // Do not load module file for leopard
1083 if (substr($module, 0, 16) == 'mod_codeproduct_' && substr($module, -3) == 'php') {
1084 $module = substr($module, 0, dol_strlen($module) - 4);
1085 }
1086 dol_include_once('/core/modules/product/'.$module.'.php');
1087 $modCodeProduct = new $module();
1088 '@phan-var-force ModeleProductCode $modCodeProduct';
1089 if (!empty($modCodeProduct->code_auto)) {
1090 $this->ref = $modCodeProduct->getNextValue($this, $this->type);
1091 }
1092 unset($modCodeProduct);
1093 }
1094
1095 if (empty($this->ref)) {
1096 $this->error = 'ProductModuleNotSetupForAutoRef';
1097 return -2;
1098 }
1099 }
1100
1101 dol_syslog(get_class($this)."::create ref=".$this->ref." price=".$this->price." price_ttc=".$this->price_ttc." tva_tx=".$this->tva_tx." price_base_type=".$this->price_base_type, LOG_DEBUG);
1102
1103 $now = dol_now();
1104
1105 if (empty($this->date_creation)) {
1106 $this->date_creation = $now;
1107 }
1108
1109 $this->db->begin();
1110
1111 // For automatic creation during create action (not used by Dolibarr GUI, can be used by scripts)
1112 if ($this->barcode == '-1' || $this->barcode == 'auto') {
1113 $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1114 }
1115
1116 // Check more parameters
1117 // If error, this->errors[] is filled
1118 $result = $this->verify();
1119
1120 if ($result >= 0) {
1121 $sql = "SELECT count(*) as nb";
1122 $sql .= " FROM ".$this->db->prefix()."product";
1123 $sql .= " WHERE entity IN (".getEntity('product').")";
1124 $sql .= " AND ref = '".$this->db->escape($this->ref)."'";
1125
1126 $result = $this->db->query($sql);
1127 if ($result) {
1128 $obj = $this->db->fetch_object($result);
1129 if ($obj->nb == 0) {
1130 // Insert new product, no previous one found
1131 $sql = "INSERT INTO ".$this->db->prefix()."product (";
1132 $sql .= "datec";
1133 $sql .= ", entity";
1134 $sql .= ", ref";
1135 $sql .= ", ref_ext";
1136 $sql .= ", price_min";
1137 $sql .= ", price_min_ttc";
1138 $sql .= ", label";
1139 $sql .= ", fk_user_author";
1140 $sql .= ", fk_product_type";
1141 $sql .= ", price";
1142 $sql .= ", price_ttc";
1143 $sql .= ", price_base_type";
1144 $sql .= ", price_label";
1145 $sql .= ", tobuy";
1146 $sql .= ", tosell";
1147 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1148 $sql .= ", accountancy_code_buy";
1149 $sql .= ", accountancy_code_buy_intra";
1150 $sql .= ", accountancy_code_buy_export";
1151 $sql .= ", accountancy_code_sell";
1152 $sql .= ", accountancy_code_sell_intra";
1153 $sql .= ", accountancy_code_sell_export";
1154 }
1155 $sql .= ", canvas";
1156 $sql .= ", finished";
1157 $sql .= ", tobatch";
1158 $sql .= ", sell_or_eat_by_mandatory";
1159 $sql .= ", batch_mask";
1160 $sql .= ", fk_unit";
1161 $sql .= ", mandatory_period";
1162 $sql .= ", stockable_product";
1163 if (!empty($this->default_vat_code)) {
1164 $sql .= ", default_vat_code";
1165 }
1166 $sql .= ") VALUES (";
1167 $sql .= "'".$this->db->idate($this->date_creation)."'";
1168 $sql .= ", ".(!empty($this->entity) ? (int) $this->entity : (int) $conf->entity);
1169 $sql .= ", '".$this->db->escape($this->ref)."'";
1170 $sql .= ", ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
1171 $sql .= ", ".price2num($price_min_ht);
1172 $sql .= ", ".price2num($price_min_ttc);
1173 $sql .= ", ".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null");
1174 $sql .= ", ".((int) $user->id);
1175 $sql .= ", ".((int) $this->type);
1176 $sql .= ", ".price2num($price_ht, 'MT');
1177 $sql .= ", ".price2num($price_ttc, 'MT');
1178 $sql .= ", '".$this->db->escape($this->price_base_type)."'";
1179 $sql .= ", ".(!empty($this->price_label) ? "'".$this->db->escape($this->price_label)."'" : "null");
1180 $sql .= ", ".((int) $this->status);
1181 $sql .= ", ".((int) $this->status_buy);
1182 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1183 $sql .= ", '".$this->db->escape($this->accountancy_code_buy)."'";
1184 $sql .= ", '".$this->db->escape($this->accountancy_code_buy_intra)."'";
1185 $sql .= ", '".$this->db->escape($this->accountancy_code_buy_export)."'";
1186 $sql .= ", '".$this->db->escape($this->accountancy_code_sell)."'";
1187 $sql .= ", '".$this->db->escape($this->accountancy_code_sell_intra)."'";
1188 $sql .= ", '".$this->db->escape($this->accountancy_code_sell_export)."'";
1189 }
1190 $sql .= ", '".$this->db->escape($this->canvas)."'";
1191 $sql .= ", ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? 'NULL' : (int) $this->finished);
1192 $sql .= ", ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : ((int) $this->status_batch));
1193 $sql .= ", ".((empty($this->sell_or_eat_by_mandatory) || $this->sell_or_eat_by_mandatory < 0) ? 0 : ((int) $this->sell_or_eat_by_mandatory));
1194 $sql .= ", '".$this->db->escape($this->batch_mask)."'";
1195 $sql .= ", ".($this->fk_unit > 0 ? ((int) $this->fk_unit) : 'NULL');
1196 $sql .= ", '".$this->db->escape((string) $this->mandatory_period)."'";
1197 $sql .= ", ".((int) $this->stockable_product);
1198 if (!empty($this->default_vat_code)) {
1199 $sql .= ", '".$this->db->escape($this->default_vat_code)."'";
1200 }
1201 $sql .= ")";
1202 dol_syslog(get_class($this)."::Create", LOG_DEBUG);
1203
1204 $result = $this->db->query($sql);
1205 if ($result) {
1206 $id = $this->db->last_insert_id($this->db->prefix()."product");
1207
1208 if ($id > 0) {
1209 $this->id = $id;
1210 $this->price = $price_ht;
1211 $this->price_ttc = $price_ttc;
1212 $this->price_min = $price_min_ht;
1213 $this->price_min_ttc = $price_min_ttc;
1214
1215 $result = $this->_log_price($user);
1216 if ($result > 0) {
1217 if ($this->update($id, $user, 1, 'add') <= 0) {
1218 $error++;
1219 }
1220 } else {
1221 $error++;
1222 $this->error = $this->db->lasterror();
1223 }
1224
1225 // update accountancy for this entity
1226 if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1227 $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " .((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1228
1229 $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1230 $sql .= " fk_product";
1231 $sql .= ", entity";
1232 $sql .= ", accountancy_code_buy";
1233 $sql .= ", accountancy_code_buy_intra";
1234 $sql .= ", accountancy_code_buy_export";
1235 $sql .= ", accountancy_code_sell";
1236 $sql .= ", accountancy_code_sell_intra";
1237 $sql .= ", accountancy_code_sell_export";
1238 $sql .= ") VALUES (";
1239 $sql .= $this->id;
1240 $sql .= ", " . ((int) $conf->entity);
1241 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1242 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1243 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1244 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1245 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1246 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1247 $sql .= ")";
1248 $result = $this->db->query($sql);
1249 if (!$result) {
1250 $error++;
1251 $this->error = 'ErrorFailedToInsertAccountancyForEntity';
1252 }
1253 }
1254 } else {
1255 $error++;
1256 $this->error = 'ErrorFailedToGetInsertedId';
1257 }
1258 } else {
1259 $error++;
1260 $this->error = $this->db->lasterror();
1261 }
1262 } else {
1263 // Product already exists with this ref
1264 $langs->load("products");
1265 $error++;
1266 $this->error = "ErrorProductAlreadyExists";
1267 dol_syslog(get_class($this)."::Create fails, ref ".$this->ref." already exists");
1268 }
1269 } else {
1270 $error++;
1271 $this->error = $this->db->lasterror();
1272 }
1273
1274 if (!$error && !$notrigger) {
1275 // Call trigger
1276 $result = $this->call_trigger('PRODUCT_CREATE', $user);
1277 if ($result < 0) {
1278 $error++;
1279 }
1280 // End call triggers
1281 }
1282
1283 if (!$error) {
1284 $this->db->commit();
1285 return $this->id;
1286 } else {
1287 $this->db->rollback();
1288 return -$error;
1289 }
1290 } else {
1291 $this->db->rollback();
1292 dol_syslog(get_class($this)."::Create fails verify ".implode(',', $this->errors), LOG_WARNING);
1293 return -3;
1294 }
1295 }
1296
1297
1304 public function verify()
1305 {
1306 global $langs;
1307
1308 $this->errors = array();
1309
1310 $result = 0;
1311 $this->ref = trim($this->ref);
1312
1313 if (!$this->ref) {
1314 $this->errors[] = 'ErrorBadRef';
1315 $result = -2;
1316 }
1317
1318 $arrayofnonnegativevalue = array('weight' => 'Weight', 'width' => 'Width', 'height' => 'Height', 'length' => 'Length', 'surface' => 'Surface', 'volume' => 'Volume');
1319 foreach ($arrayofnonnegativevalue as $key => $value) {
1320 if (property_exists($this, $key) && !empty($this->$key) && ($this->$key < 0)) {
1321 $langs->loadLangs(array("main", "other"));
1322 $this->error = $langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv($value));
1323 $this->errors[] = $this->error;
1324 $result = -4;
1325 }
1326 }
1327
1328 $rescode = $this->check_barcode($this->barcode, $this->barcode_type_code);
1329 if ($rescode) {
1330 if ($rescode == -1) {
1331 $this->errors[] = 'ErrorBadBarCodeSyntax';
1332 } elseif ($rescode == -2) {
1333 $this->errors[] = 'ErrorBarCodeRequired';
1334 } elseif ($rescode == -3) {
1335 // Note: Common usage is to have barcode unique. For variants, we should have a different barcode.
1336 $this->errors[] = 'ErrorBarCodeAlreadyUsed';
1337 }
1338
1339 $result = -3;
1340 }
1341
1342 return $result;
1343 }
1344
1345 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1356 public function check_barcode($valuetotest, $typefortest)
1357 {
1358 // phpcs:enable
1359 global $conf;
1360
1361 if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
1362 $module = strtolower(getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM'));
1363
1364 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
1365 foreach ($dirsociete as $dirroot) {
1366 $res = dol_include_once($dirroot.$module.'.php');
1367 if ($res) {
1368 break;
1369 }
1370 }
1371
1372 $mod = new $module();
1373 '@phan-var-force ModeleNumRefBarCode $mod';
1374
1375 dol_syslog(get_class($this)."::check_barcode value=".$valuetotest." type=".$typefortest." module=".$module);
1376 $result = $mod->verif($this->db, $valuetotest, $this, 0, $typefortest);
1377 return $result;
1378 } else {
1379 return 0;
1380 }
1381 }
1382
1394 public function update($id, $user, $notrigger = 0, $action = 'update', $updatetype = false)
1395 {
1396 global $langs, $conf, $hookmanager;
1397
1398 $error = 0;
1399
1400 // Check parameters
1401 if (!$this->label) {
1402 $this->label = 'MISSING LABEL';
1403 }
1404
1405 // Clean parameters
1406 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
1407 $this->ref = trim($this->ref);
1408 } else {
1409 $this->ref = dol_string_nospecial(trim($this->ref));
1410 }
1411 $this->label = trim($this->label);
1412 $this->description = trim($this->description);
1413 $this->note_private = (isset($this->note_private) ? trim($this->note_private) : null);
1414 $this->note_public = (isset($this->note_public) ? trim($this->note_public) : null);
1415 $this->net_measure = price2num($this->net_measure);
1416 $this->net_measure_units = (!is_numeric($this->net_measure_units) ? null : (int) $this->net_measure_units);
1417 $this->weight = price2num($this->weight);
1418 $this->weight_units = (!is_numeric($this->weight_units) ? null : (int) $this->weight_units);
1419 $this->length = price2num($this->length);
1420 $this->length_units = (!is_numeric($this->length_units) ? null : (int) $this->length_units);
1421 $this->width = price2num($this->width);
1422 $this->width_units = (!is_numeric($this->width_units) ? null : (int) $this->width_units);
1423 $this->height = price2num($this->height);
1424 $this->height_units = (!is_numeric($this->height_units) ? null : (int) $this->height_units);
1425 $this->surface = price2num($this->surface);
1426 $this->surface_units = (!is_numeric($this->surface_units) ? null : (int) $this->surface_units);
1427 $this->volume = price2num($this->volume);
1428 $this->volume_units = (!is_numeric($this->volume_units) ? null : (int) $this->volume_units);
1429
1430 // set unit not defined
1431 if (is_numeric($this->length_units)) {
1432 $this->width_units = $this->length_units; // Not used yet
1433 }
1434 if (is_numeric($this->length_units)) {
1435 $this->height_units = $this->length_units; // Not used yet
1436 }
1437
1438 // Automated compute surface and volume if not filled
1439 if (empty($this->surface) && !empty($this->length) && !empty($this->width) && $this->length_units == $this->width_units) {
1440 $this->surface = (float) $this->length * (float) $this->width;
1441 $this->surface_units = measuring_units_squared((int) $this->length_units);
1442 }
1443 if (empty($this->volume) && !empty($this->surface) && !empty($this->height) && $this->length_units == $this->height_units) {
1444 $this->volume = $this->surface * (float) $this->height;
1445 $this->volume_units = measuring_units_cubed((int) $this->height_units);
1446 }
1447
1448 if (empty($this->tva_tx)) {
1449 $this->tva_tx = 0;
1450 }
1451 if (empty($this->tva_npr)) {
1452 $this->tva_npr = 0;
1453 }
1454 if (empty($this->localtax1_tx)) {
1455 $this->localtax1_tx = 0;
1456 }
1457 if (empty($this->localtax2_tx)) {
1458 $this->localtax2_tx = 0;
1459 }
1460 if (empty($this->localtax1_type)) {
1461 $this->localtax1_type = '0';
1462 }
1463 if (empty($this->localtax2_type)) {
1464 $this->localtax2_type = '0';
1465 }
1466 if (empty($this->status)) {
1467 $this->status = 0;
1468 }
1469 if (empty($this->status_buy)) {
1470 $this->status_buy = 0;
1471 }
1472
1473 if (empty($this->country_id)) {
1474 $this->country_id = 0;
1475 }
1476 if (empty($this->country_id) && !empty($this->country_code)) {
1477 $country_id = getCountry($this->country_code, '3');
1478 $this->country_id = is_int($country_id) ? $country_id : 0;
1479 }
1480
1481 if (empty($this->state_id)) {
1482 $this->state_id = 0;
1483 }
1484
1485 if (empty($this->stockable_product)) {
1486 $this->stockable_product = 0;
1487 }
1488
1489 // For automatic creation during update action (not used by Dolibarr GUI, can be used by scripts)
1490 if ($this->barcode == '-1' || $this->barcode == 'auto') {
1491 $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1492 }
1493
1494 // Barcode value
1495 $this->barcode = (empty($this->barcode) ? '' : trim($this->barcode));
1496
1497 $this->accountancy_code_buy = trim($this->accountancy_code_buy);
1498 $this->accountancy_code_buy_intra = (!empty($this->accountancy_code_buy_intra) ? trim($this->accountancy_code_buy_intra) : '');
1499 $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
1500 $this->accountancy_code_sell = trim($this->accountancy_code_sell);
1501 $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
1502 $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
1503
1504 // Normalize the accountancy codes the way the admin dropdown does it, so an API client that
1505 // sends '606111000' ends up with the same '606111' value the GUI stores (see issue #32343).
1506 // When ACCOUNTING_MANAGE_ZERO is on, trailing zeros are part of the code and must be kept.
1507 if (!getDolGlobalString('ACCOUNTING_MANAGE_ZERO')) {
1508 require_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
1509 $this->accountancy_code_buy = clean_account($this->accountancy_code_buy);
1510 $this->accountancy_code_buy_intra = clean_account($this->accountancy_code_buy_intra);
1511 $this->accountancy_code_buy_export = clean_account($this->accountancy_code_buy_export);
1512 $this->accountancy_code_sell = clean_account($this->accountancy_code_sell);
1513 $this->accountancy_code_sell_intra = clean_account($this->accountancy_code_sell_intra);
1514 $this->accountancy_code_sell_export = clean_account($this->accountancy_code_sell_export);
1515 }
1516
1517 $this->db->begin();
1518
1519 $result = 0;
1520 // Check name is required and codes are ok or unique. If error, this->errors[] is filled
1521 if ($action != 'add') {
1522 $result = $this->verify(); // We don't check when update called during a create because verify was already done
1523 } else {
1524 // we can continue
1525 $result = 0;
1526 }
1527
1528 if ($result >= 0) {
1529 // $this->oldcopy should have been set by the caller of update (here properties were already modified)
1530 // Note that this->oldcopy must be a Product instance (not stdClass), otherwise the method
1531 // hasbatch() called below will fatal. Callers may set oldcopy via dol_clone($obj, 2) (which
1532 // returns a stdClass with scalar properties only) and then we cannot call methods on it,
1533 // so re-clone with native=1 in that case (see issues #38663, #38638).
1534 if (is_null($this->oldcopy) || (is_object($this->oldcopy) && empty($this->oldcopy->id)) || !($this->oldcopy instanceof Product)) {
1535 $this->oldcopy = dol_clone($this, 1); // 1 to clone with methods to avoid fatal error with $this->oldcopy->hasbatch()
1536 }
1537 // Test if batch management is activated on existing product
1538 // If yes, we create missing entries into product_batch
1539 if ($this->hasbatch() && !$this->oldcopy->hasbatch()) {
1540 //$valueforundefinedlot = 'Undefined'; // In previous version, 39 and lower
1541 $valueforundefinedlot = '000000';
1542 if (getDolGlobalString('STOCK_DEFAULT_BATCH')) {
1543 $valueforundefinedlot = getDolGlobalString('STOCK_DEFAULT_BATCH');
1544 }
1545
1546 dol_syslog("Flag batch of product id=".$this->id." is set to ON, so we will create missing records into product_batch");
1547
1548 $this->load_stock();
1549 foreach ($this->stock_warehouse as $idW => $ObjW) { // For each warehouse where we have stocks defined for this product (for each lines in product_stock)
1550 $qty_batch = 0;
1551 foreach ($ObjW->detail_batch as $detail) { // Each lines of detail in product_batch of the current $ObjW = product_stock
1552 if ($detail->batch == $valueforundefinedlot || $detail->batch == 'Undefined') {
1553 // We discard this line, we will create it later
1554 $sqlclean = "DELETE FROM ".$this->db->prefix()."product_batch WHERE batch in('Undefined', '".$this->db->escape($valueforundefinedlot)."') AND fk_product_stock = ".((int) $ObjW->id);
1555 $result = $this->db->query($sqlclean);
1556 if (!$result) {
1557 dol_print_error($this->db);
1558 exit;
1559 }
1560 continue;
1561 }
1562
1563 $qty_batch += $detail->qty;
1564 }
1565 // Quantities in batch details are not same as stock quantity,
1566 // so we add a default batch record to complete and get same qty in parent and child table
1567 if ($ObjW->real != $qty_batch) {
1568 $ObjBatch = new Productbatch($this->db);
1569 $ObjBatch->batch = $valueforundefinedlot;
1570 $ObjBatch->qty = ($ObjW->real - $qty_batch);
1571 $ObjBatch->fk_product_stock = (int) $ObjW->id;
1572
1573 if ($ObjBatch->create($user, 1) < 0) {
1574 $error++;
1575 $this->errors = $ObjBatch->errors;
1576 } else {
1577 // we also add lot record if not exist
1578 $ObjLot = new Productlot($this->db);
1579 // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1580 if ($ObjLot->fetch(0, $this->id, $valueforundefinedlot) == 0) {
1581 $ObjLot->fk_product = $this->id;
1582 $ObjLot->entity = (int) $this->entity;
1583 $ObjLot->fk_user_creat = $user->id;
1584 $ObjLot->batch = $valueforundefinedlot;
1585 if ($ObjLot->create($user, 1) < 0) {
1586 $error++;
1587 $this->errors = $ObjLot->errors;
1588 }
1589 }
1590 }
1591 }
1592 }
1593 }
1594
1595 $sql = "UPDATE ".$this->db->prefix()."product";
1596 $sql .= " SET label = '".$this->db->escape($this->label)."'";
1597
1598 if ($updatetype && ($this->isProduct() || $this->isService())) {
1599 $sql .= ", fk_product_type = ".((int) $this->type);
1600 }
1601
1602 $sql .= ", ref = '".$this->db->escape($this->ref)."'";
1603 $sql .= ", ref_ext = ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
1604 $sql .= ", default_vat_code = ".($this->default_vat_code ? "'".$this->db->escape($this->default_vat_code)."'" : "null");
1605 $sql .= ", tva_tx = ".((float) $this->tva_tx);
1606 $sql .= ", recuperableonly = ".((int) $this->tva_npr);
1607 $sql .= ", localtax1_tx = ".((float) $this->localtax1_tx);
1608 $sql .= ", localtax2_tx = ".((float) $this->localtax2_tx);
1609 $sql .= ", localtax1_type = ".($this->localtax1_type != '' ? "'".$this->db->escape($this->localtax1_type)."'" : "'0'");
1610 $sql .= ", localtax2_type = ".($this->localtax2_type != '' ? "'".$this->db->escape($this->localtax2_type)."'" : "'0'");
1611
1612 $sql .= ", barcode = ".(empty($this->barcode) ? "null" : "'".$this->db->escape($this->barcode)."'");
1613 $sql .= ", fk_barcode_type = ".(empty($this->barcode_type) ? "null" : $this->db->escape((string) $this->barcode_type));
1614
1615 $sql .= ", tosell = ".(int) $this->status;
1616 $sql .= ", tobuy = ".(int) $this->status_buy;
1617 $sql .= ", tobatch = ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : (int) $this->status_batch);
1618 $sql .= ", sell_or_eat_by_mandatory = ".((empty($this->sell_or_eat_by_mandatory) || $this->sell_or_eat_by_mandatory < 0) ? 0 : (int) $this->sell_or_eat_by_mandatory);
1619 $sql .= ", batch_mask = '".$this->db->escape($this->batch_mask)."'";
1620
1621 $sql .= ", finished = ".((!isset($this->finished) || $this->finished < 0 || $this->finished === '') ? "null" : (int) $this->finished);
1622 $sql .= ", fk_default_bom = ".((!isset($this->fk_default_bom) || $this->fk_default_bom < 0 || $this->fk_default_bom == '') ? "null" : (int) $this->fk_default_bom);
1623 $sql .= ", net_measure = ".($this->net_measure != '' ? "'".$this->db->escape($this->net_measure)."'" : 'null');
1624 $sql .= ", net_measure_units = ".((string) $this->net_measure_units != '' ? ((int) $this->net_measure_units) : 'null');
1625 $sql .= ", weight = ".($this->weight != '' ? "'".$this->db->escape($this->weight)."'" : 'null');
1626 $sql .= ", weight_units = ".((string) $this->weight_units != '' ? ((int) $this->weight_units) : 'null');
1627 $sql .= ", length = ".($this->length != '' ? "'".$this->db->escape($this->length)."'" : 'null');
1628 $sql .= ", length_units = ".((string) $this->length_units != '' ? ((int) $this->length_units) : 'null');
1629 $sql .= ", width= ".($this->width != '' ? "'".$this->db->escape($this->width)."'" : 'null');
1630 $sql .= ", width_units = ".((string) $this->width_units != '' ? ((int) $this->width_units) : 'null');
1631 $sql .= ", height = ".($this->height != '' ? "'".$this->db->escape($this->height)."'" : 'null');
1632 $sql .= ", height_units = ".((string) $this->height_units != '' ? ((int) $this->height_units) : 'null');
1633 $sql .= ", surface = ".($this->surface != '' ? "'".$this->db->escape($this->surface)."'" : 'null');
1634 $sql .= ", surface_units = ".((string) $this->surface_units != '' ? ((int) $this->surface_units) : 'null');
1635 $sql .= ", volume = ".($this->volume != '' ? "'".$this->db->escape($this->volume)."'" : 'null');
1636 $sql .= ", volume_units = ".((string) $this->volume_units != '' ? ((int) $this->volume_units) : 'null');
1637 $sql .= ", fk_default_warehouse = ".($this->fk_default_warehouse > 0 ? ((int) $this->fk_default_warehouse) : 'null');
1638 $sql .= ", fk_default_workstation = ".($this->fk_default_workstation > 0 ? ((int) $this->fk_default_workstation) : 'null');
1639 $sql .= ", seuil_stock_alerte = ".((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (float) $this->seuil_stock_alerte : 'null');
1640 $sql .= ", description = '".$this->db->escape($this->description)."'";
1641 $sql .= ", url = ".($this->url ? "'".$this->db->escape($this->url)."'" : 'null');
1642 $sql .= ", customcode = '".$this->db->escape($this->customcode)."'";
1643 $sql .= ", fk_country = ".($this->country_id > 0 ? (int) $this->country_id : 'null');
1644 $sql .= ", fk_state = ".($this->state_id > 0 ? (int) $this->state_id : 'null');
1645 $sql .= ", lifetime = ".($this->lifetime > 0 ? (int) $this->lifetime : 'null');
1646 $sql .= ", qc_frequency = ".($this->qc_frequency > 0 ? (int) $this->qc_frequency : 'null');
1647 $sql .= ", note = ".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : 'null');
1648 $sql .= ", note_public = ".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : 'null');
1649 $sql .= ", duration = '".$this->db->escape($this->duration_value.$this->duration_unit)."'";
1650 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1651 $sql .= ", accountancy_code_buy = '" . $this->db->escape($this->accountancy_code_buy) . "'";
1652 $sql .= ", accountancy_code_buy_intra = '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1653 $sql .= ", accountancy_code_buy_export = '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1654 $sql .= ", accountancy_code_sell= '" . $this->db->escape($this->accountancy_code_sell) . "'";
1655 $sql .= ", accountancy_code_sell_intra= '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1656 $sql .= ", accountancy_code_sell_export= '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1657 }
1658 $sql .= ", desiredstock = ".((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (float) $this->desiredstock : "null");
1659 $sql .= ", cost_price = ".($this->cost_price != '' ? ((float) $this->cost_price) : 'null');
1660 $sql .= ", fk_unit= ".(!$this->fk_unit ? 'NULL' : (int) $this->fk_unit);
1661 $sql .= ", price_autogen = ".(!$this->price_autogen ? 0 : 1);
1662 $sql .= ", fk_price_expression = ".($this->fk_price_expression != 0 ? (int) $this->fk_price_expression : 'NULL');
1663 $sql .= ", fk_user_modif = ".($user->id > 0 ? (int) $user->id : 'NULL');
1664 $sql .= ", mandatory_period = ".((int) $this->mandatory_period);
1665 $sql .= ", stockable_product = ".(int) $this->stockable_product;
1666 if (getDolGlobalString('PRODUCT_USE_CUSTOMER_PACKAGING')) {
1667 $sql .= ", packaging = ".(float) $this->packaging;
1668 }
1669
1670 // stock field is not here because it is a denormalized value from product_stock.
1671 $sql .= " WHERE rowid = ".((int) $id);
1672
1673 dol_syslog(get_class($this)."::update", LOG_DEBUG);
1674
1675 $resql = $this->db->query($sql);
1676 if ($resql) {
1677 $this->id = $id;
1678
1679 // Multilangs
1680 if (getDolGlobalInt('MAIN_MULTILANGS')) {
1681 if ($this->setMultiLangs($user) < 0) {
1682 $this->db->rollback();
1683 return -2;
1684 }
1685 }
1686
1687 $action = 'update';
1688
1689 // update accountancy for this entity
1690 if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1691 $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1692
1693 $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1694 $sql .= " fk_product";
1695 $sql .= ", entity";
1696 $sql .= ", accountancy_code_buy";
1697 $sql .= ", accountancy_code_buy_intra";
1698 $sql .= ", accountancy_code_buy_export";
1699 $sql .= ", accountancy_code_sell";
1700 $sql .= ", accountancy_code_sell_intra";
1701 $sql .= ", accountancy_code_sell_export";
1702 $sql .= ") VALUES (";
1703 $sql .= ((int) $this->id);
1704 $sql .= ", " . ((int) $conf->entity);
1705 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1706 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1707 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1708 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1709 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1710 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1711 $sql .= ")";
1712 $result = $this->db->query($sql);
1713 if (!$result) {
1714 $error++;
1715 $this->error = 'ErrorFailedToUpdateAccountancyForEntity';
1716 }
1717 }
1718
1719 if (!$this->hasbatch() && $this->oldcopy->hasbatch()) {
1720 // Selection of all product stock movements that contains batchs
1721 $sql = 'SELECT pb.qty, ps.fk_entrepot, pb.batch FROM '.MAIN_DB_PREFIX.'product_batch as pb';
1722 $sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'product_stock as ps ON (ps.rowid = pb.fk_product_stock)';
1723 $sql .= ' WHERE ps.fk_product = '.(int) $this->id;
1724
1725 $resql = $this->db->query($sql);
1726 if ($resql) {
1727 $inventorycode = dol_print_date(dol_now(), '%Y%m%d%H%M%S');
1728
1729 while ($obj = $this->db->fetch_object($resql)) {
1730 $value = $obj->qty;
1731 $fk_entrepot = $obj->fk_entrepot;
1732 $price = 0;
1733 $dlc = '';
1734 $dluo = '';
1735 $batch = $obj->batch;
1736
1737 // To know how to revert stockMouvement (add or remove)
1738 $addOremove = $value > 0 ? 1 : 0; // 1 if remove, 0 if add
1739 $label = $langs->trans('BatchStockMouvementAddInGlobal');
1740 $res = $this->correct_stock_batch($user, $fk_entrepot, abs($value), $addOremove, $label, $price, $dlc, $dluo, $batch, $inventorycode, '', null, 0, null, true);
1741
1742 if ($res > 0) {
1743 $label = $langs->trans('BatchStockMouvementAddInGlobal');
1744 $res = $this->correct_stock($user, $fk_entrepot, abs($value), (int) empty($addOremove), $label, $price, $inventorycode, '', null, 0);
1745 if ($res < 0) {
1746 $error++;
1747 }
1748 } else {
1749 $error++;
1750 }
1751 }
1752 }
1753 }
1754
1755 // Actions on extra fields
1756 if (!$error) {
1757 $result = $this->insertExtraFields();
1758 if ($result < 0) {
1759 $error++;
1760 }
1761 }
1762
1763 if (!$error && !$notrigger) {
1764 // Call trigger
1765 $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1766 if ($result < 0) {
1767 $error++;
1768 }
1769 // End call triggers
1770 }
1771
1772 if (!$error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref)) {
1773 // We remove directory
1774 if ($conf->product->dir_output) {
1775 $olddir = $conf->product->dir_output."/".dol_sanitizeFileName($this->oldcopy->ref);
1776 $newdir = $conf->product->dir_output."/".dol_sanitizeFileName($this->ref);
1777 if (file_exists($olddir)) {
1778 // include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1779 // $res = dol_move($olddir, $newdir);
1780 // do not use dol_move with directory
1781 $res = @rename($olddir, $newdir);
1782 if (!$res) {
1783 $langs->load("errors");
1784 $this->error = $langs->trans('ErrorFailToRenameDir', $olddir, $newdir);
1785 $error++;
1786 } else {
1787 // to keep old entries with the new dir
1788 require_once DOL_DOCUMENT_ROOT . '/ecm/class/ecmfiles.class.php';
1789 $ecmfiles = new EcmFiles($this->db);
1790 $ecmfiles->updateAfterRename("produit/".dol_sanitizeFileName($this->oldcopy->ref), "produit/".dol_sanitizeFileName($this->ref));
1791 }
1792 }
1793 }
1794 }
1795
1796 if (!$error) {
1797 if (isModEnabled('variants')) {
1798 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1799
1800 $comb = new ProductCombination($this->db);
1801
1802 foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) {
1803 $currcomb->updateProperties($this, $user);
1804 }
1805 }
1806
1807 $this->db->commit();
1808 return 1;
1809 } else {
1810 $this->db->rollback();
1811 return -$error;
1812 }
1813 } else {
1814 if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1815 $langs->load("errors");
1816 if (empty($conf->barcode->enabled) || empty($this->barcode)) {
1817 $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductAlreadyExists", $this->ref);
1818 } else {
1819 $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductBarCodeAlreadyExists", $this->barcode);
1820 }
1821 $this->errors[] = $this->error;
1822 $this->db->rollback();
1823 return -1;
1824 } else {
1825 $this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1826 $this->errors[] = $this->error;
1827 $this->db->rollback();
1828 return -2;
1829 }
1830 }
1831 } else {
1832 $this->db->rollback();
1833 dol_syslog(get_class($this)."::Update fails verify ".implode(',', $this->errors), LOG_WARNING);
1834 return -3;
1835 }
1836 }
1837
1845 public function delete(User $user, $notrigger = 0)
1846 {
1847 global $conf;
1848 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1849
1850 $error = 0;
1851
1852 // Check parameters
1853 if (empty($this->id)) {
1854 $this->error = "Object must be fetched before calling delete";
1855 return -1;
1856 }
1857 if (($this->isProduct() && !$user->hasRight('produit', 'supprimer')) || ($this->isService() && !$user->hasRight('service', 'supprimer'))) {
1858 $this->error = "ErrorForbidden";
1859 return 0;
1860 }
1861
1862 $objectisused = $this->isObjectUsed($this->id);
1863 if (empty($objectisused)) {
1864 $this->db->begin();
1865
1866 if (empty($notrigger)) {
1867 // Call trigger
1868 $result = $this->call_trigger('PRODUCT_DELETE', $user);
1869 if ($result < 0) {
1870 $error++;
1871 }
1872 // End call triggers
1873 }
1874
1875 // Delete from product_batch on product delete
1876 if (!$error) {
1877 $sql = "DELETE FROM ".$this->db->prefix().'product_batch';
1878 $sql .= " WHERE fk_product_stock IN (";
1879 $sql .= "SELECT rowid FROM ".$this->db->prefix().'product_stock';
1880 $sql .= " WHERE fk_product = ".((int) $this->id).")";
1881
1882 $result = $this->db->query($sql);
1883 if (!$result) {
1884 $error++;
1885 $this->errors[] = $this->db->lasterror();
1886 }
1887 }
1888
1889 // Delete all child tables
1890 if (!$error) {
1891 $elements = array('product_fournisseur_price', 'product_price', 'product_lang', 'categorie_product', 'product_stock', 'product_customer_price', 'product_lot'); // product_batch is done before
1892 foreach ($elements as $table) {
1893 if (!$error) {
1894 $sql = "DELETE FROM ".$this->db->prefix().$table;
1895 $sql .= " WHERE fk_product = ".(int) $this->id;
1896
1897 $result = $this->db->query($sql);
1898 if (!$result) {
1899 $error++;
1900 $this->errors[] = $this->db->lasterror();
1901 }
1902 }
1903 }
1904 }
1905
1906 if (!$error && isModEnabled('variants')) {
1907 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1908 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
1909
1910 //If it is a parent product, then we remove the association with child products
1911 $prodcomb = new ProductCombination($this->db);
1912
1913 if ($prodcomb->deleteByFkProductParent($user, $this->id) < 0) {
1914 $error++;
1915 $this->errors[] = 'Error deleting combinations';
1916 }
1917
1918 //We also check if it is a child product
1919 if (!$error && ($prodcomb->fetchByFkProductChild($this->id) > 0) && ($prodcomb->delete($user) < 0)) {
1920 $error++;
1921 $this->errors[] = 'Error deleting child combination';
1922 }
1923 }
1924
1925 // Delete from product_association
1926 if (!$error) {
1927 $sql = "DELETE FROM ".$this->db->prefix()."product_association";
1928 $sql .= " WHERE fk_product_pere = ".(int) $this->id." OR fk_product_fils = ".(int) $this->id;
1929
1930 $result = $this->db->query($sql);
1931 if (!$result) {
1932 $error++;
1933 $this->errors[] = $this->db->lasterror();
1934 }
1935 }
1936
1937 // Remove extrafields
1938 if (!$error) {
1939 $result = $this->deleteExtraFields();
1940 if ($result < 0) {
1941 $error++;
1942 dol_syslog(get_class($this)."::delete error -4 ".$this->error, LOG_ERR);
1943 }
1944 }
1945
1946 // Delete product
1947 if (!$error) {
1948 $sqlz = "DELETE FROM ".$this->db->prefix()."product";
1949 $sqlz .= " WHERE rowid = ".(int) $this->id;
1950
1951 $resultz = $this->db->query($sqlz);
1952 if (!$resultz) {
1953 $error++;
1954 $this->errors[] = $this->db->lasterror();
1955 }
1956 }
1957
1958 // Delete record into ECM index and physically
1959 if (!$error) {
1960 $res = $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
1961 $res = $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
1962 if (!$res) {
1963 $error++;
1964 }
1965 }
1966
1967 if (!$error) {
1968 // We remove directory
1969 $ref = dol_sanitizeFileName($this->ref);
1970 if ($conf->product->dir_output) {
1971 $dir = $conf->product->dir_output."/".$ref;
1972 if (file_exists($dir)) {
1973 $res = @dol_delete_dir_recursive($dir);
1974 if (!$res) {
1975 $this->errors[] = 'ErrorFailToDeleteDir';
1976 $error++;
1977 }
1978 }
1979 }
1980 }
1981
1982 if (!$error) {
1983 $this->db->commit();
1984 return 1;
1985 } else {
1986 foreach ($this->errors as $errmsg) {
1987 dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
1988 $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
1989 }
1990 $this->db->rollback();
1991 return -$error;
1992 }
1993 } else {
1994 $this->error = "ErrorRecordIsUsedCantDelete";
1995 return 0;
1996 }
1997 }
1998
2004 public static function getSellOrEatByMandatoryList()
2005 {
2006 global $langs;
2007
2008 $sellByLabel = $langs->trans('SellByDate');
2009 $eatByLabel = $langs->trans('EatByDate');
2010 return array(
2011 self::SELL_OR_EAT_BY_MANDATORY_ID_NONE => $langs->trans('BatchSellOrEatByMandatoryNone'),
2012 self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY => $sellByLabel,
2013 self::SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY => $eatByLabel,
2014 self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT => $langs->trans('BatchSellOrEatByMandatoryAll', $sellByLabel, $eatByLabel),
2015 );
2016 }
2017
2024 {
2025 $sellOrEatByMandatoryLabel = '';
2026
2027 $sellOrEatByMandatoryList = self::getSellOrEatByMandatoryList();
2028 if (isset($sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory])) {
2029 $sellOrEatByMandatoryLabel = $sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory];
2030 }
2031
2032 return $sellOrEatByMandatoryLabel;
2033 }
2034
2041 public function setMultiLangs($user)
2042 {
2043 global $langs;
2044
2045 $langs_available = $langs->get_available_languages(DOL_DOCUMENT_ROOT, 0, 2);
2046 $current_lang = $langs->getDefaultLang();
2047
2048 foreach ($langs_available as $key => $value) {
2049 if ($key == $current_lang) {
2050 $sql = "SELECT rowid";
2051 $sql .= " FROM ".$this->db->prefix()."product_lang";
2052 $sql .= " WHERE fk_product = ".((int) $this->id);
2053 $sql .= " AND lang = '".$this->db->escape($key)."'";
2054
2055 $result = $this->db->query($sql);
2056
2057 if ($this->db->num_rows($result)) { // if there is already a description line for this language
2058 $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
2059 $sql2 .= " SET ";
2060 $sql2 .= " label='".$this->db->escape($this->label)."',";
2061 $sql2 .= " description='".$this->db->escape($this->description)."'";
2062 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2063 $sql2 .= ", note='".$this->db->escape($this->other)."'";
2064 }
2065 $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
2066 } else {
2067 $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
2068 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2069 $sql2 .= ", note";
2070 }
2071 $sql2 .= ")";
2072 $sql2 .= " VALUES(".((int) $this->id).",'".$this->db->escape($key)."','".$this->db->escape($this->label)."',";
2073 $sql2 .= " '".$this->db->escape($this->description)."'";
2074 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2075 $sql2 .= ", '".$this->db->escape($this->other)."'";
2076 }
2077 $sql2 .= ")";
2078 }
2079 dol_syslog(get_class($this).'::setMultiLangs key = current_lang = '.$key);
2080 if (!$this->db->query($sql2)) {
2081 $this->error = $this->db->lasterror();
2082 return -1;
2083 }
2084 } elseif (isset($this->multilangs[$key])) {
2085 if (empty($this->multilangs[$key]["label"])) {
2086 $this->errors[] = $key . ' : ' . $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Label"));
2087 return -1;
2088 }
2089
2090 $sql = "SELECT rowid";
2091 $sql .= " FROM ".$this->db->prefix()."product_lang";
2092 $sql .= " WHERE fk_product = ".((int) $this->id);
2093 $sql .= " AND lang = '".$this->db->escape($key)."'";
2094
2095 $result = $this->db->query($sql);
2096
2097 if ($this->db->num_rows($result)) { // if there is already a description line for this language
2098 $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
2099 $sql2 .= " SET ";
2100 $sql2 .= " label = '".$this->db->escape($this->multilangs["$key"]["label"])."',";
2101 $sql2 .= " description = '".$this->db->escape($this->multilangs["$key"]["description"])."'";
2102 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2103 // @phan-suppress-next-line PhanTypeInvalidDimOffset
2104 $sql2 .= ", note = '".$this->db->escape($this->multilangs["$key"]["other"])."'";
2105 }
2106 $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
2107 } else {
2108 $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
2109 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2110 $sql2 .= ", note";
2111 }
2112 $sql2 .= ")";
2113 $sql2 .= " VALUES(".((int) $this->id).",'".$this->db->escape($key)."','".$this->db->escape($this->multilangs["$key"]["label"])."',";
2114 $sql2 .= " '".$this->db->escape($this->multilangs["$key"]["description"])."'";
2115 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2116 // @phan-suppress-next-line PhanTypeInvalidDimOffset
2117 $sql2 .= ", '".$this->db->escape($this->multilangs["$key"]["other"])."'";
2118 }
2119 $sql2 .= ")";
2120 }
2121
2122 // We do not save if main fields are empty
2123 if ($this->multilangs["$key"]["label"] || $this->multilangs["$key"]["description"]) {
2124 if (!$this->db->query($sql2)) {
2125 $this->error = $this->db->lasterror();
2126 return -1;
2127 }
2128 }
2129 } else {
2130 // language is not current language and we didn't provide a multilang description for this language
2131 }
2132 }
2133
2134 // Call trigger
2135 $result = $this->call_trigger('PRODUCT_SET_MULTILANGS', $user);
2136 if ($result < 0) {
2137 $this->error = $this->db->lasterror();
2138 return -1;
2139 }
2140 // End call triggers
2141
2142 return 1;
2143 }
2144
2153 public function delMultiLangs($langtodelete, $user)
2154 {
2155 $sql = "DELETE FROM ".$this->db->prefix()."product_lang";
2156 $sql .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($langtodelete)."'";
2157
2158 dol_syslog(get_class($this).'::delMultiLangs', LOG_DEBUG);
2159 $result = $this->db->query($sql);
2160 if ($result) {
2161 // Call trigger
2162 $result = $this->call_trigger('PRODUCT_DEL_MULTILANGS', $user);
2163 if ($result < 0) {
2164 $this->error = $this->db->lasterror();
2165 dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
2166 return -1;
2167 }
2168 // End call triggers
2169 unset($this->multilangs[$langtodelete]);
2170 return 1;
2171 } else {
2172 $this->error = $this->db->lasterror();
2173 dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
2174 return -1;
2175 }
2176 }
2177
2186 public function setAccountancyCode($type, $value)
2187 {
2188 global $user;
2189
2190 $error = 0;
2191
2192 $this->db->begin();
2193
2194 if ($type == 'buy') {
2195 $field = 'accountancy_code_buy';
2196 } elseif ($type == 'buy_intra') {
2197 $field = 'accountancy_code_buy_intra';
2198 } elseif ($type == 'buy_export') {
2199 $field = 'accountancy_code_buy_export';
2200 } elseif ($type == 'sell') {
2201 $field = 'accountancy_code_sell';
2202 } elseif ($type == 'sell_intra') {
2203 $field = 'accountancy_code_sell_intra';
2204 } elseif ($type == 'sell_export') {
2205 $field = 'accountancy_code_sell_export';
2206 } else {
2207 return -1;
2208 }
2209
2210 $sql = "UPDATE ".$this->db->prefix().$this->table_element." SET ";
2211 $sql .= "$field = '".$this->db->escape($value)."'";
2212 $sql .= " WHERE rowid = ".((int) $this->id);
2213
2214 dol_syslog(__METHOD__, LOG_DEBUG);
2215 $resql = $this->db->query($sql);
2216
2217 if ($resql) {
2218 // Call trigger
2219 $result = $this->call_trigger('PRODUCT_MODIFY', $user);
2220 if ($result < 0) {
2221 $error++;
2222 }
2223 // End call triggers
2224
2225 if ($error) {
2226 $this->db->rollback();
2227 return -1;
2228 }
2229
2230 $this->$field = $value;
2231
2232 $this->db->commit();
2233 return 1;
2234 } else {
2235 $this->error = $this->db->lasterror();
2236 $this->db->rollback();
2237 return -1;
2238 }
2239 }
2240
2246 public function getMultiLangs()
2247 {
2248 global $langs;
2249
2250 $current_lang = $langs->getDefaultLang();
2251
2252 $sql = "SELECT lang, label, description, note as other";
2253 $sql .= " FROM ".$this->db->prefix()."product_lang";
2254 $sql .= " WHERE fk_product = ".((int) $this->id);
2255
2256 $result = $this->db->query($sql);
2257 if ($result) {
2258 while ($obj = $this->db->fetch_object($result)) {
2259 //print 'lang='.$obj->lang.' current='.$current_lang.'<br>';
2260 if ($obj->lang == $current_lang) { // si on a les traduct. dans la langue courante on les charge en infos principales.
2261 $this->label = $obj->label;
2262 $this->description = $obj->description;
2263 $this->other = $obj->other;
2264 }
2265 $this->multilangs[(string) $obj->lang]["label"] = $obj->label;
2266 $this->multilangs[(string) $obj->lang]["description"] = $obj->description;
2267 $this->multilangs[(string) $obj->lang]["other"] = $obj->other;
2268 }
2269 return 1;
2270 } else {
2271 $this->error = "Error: ".$this->db->lasterror()." - ".$sql;
2272 return -1;
2273 }
2274 }
2275
2282 private function getArrayForPriceCompare($level = 0)
2283 {
2284 $testExit = array('multiprices','multiprices_ttc','multiprices_base_type','multiprices_min','multiprices_min_ttc','multiprices_tva_tx','multiprices_recuperableonly');
2285
2286 foreach ($testExit as $field) {
2287 if (!isset($this->$field)) {
2288 return array();
2289 }
2290 $tmparray = $this->$field;
2291 if (!isset($tmparray[$level])) {
2292 return array();
2293 }
2294 }
2295
2296 $lastPrice = array(
2297 'level' => $level ? $level : 1,
2298 'multiprices' => (float) $this->multiprices[$level],
2299 'multiprices_ttc' => (float) $this->multiprices_ttc[$level],
2300 'multiprices_base_type' => $this->multiprices_base_type[$level],
2301 'multiprices_min' => (float) $this->multiprices_min[$level],
2302 'multiprices_min_ttc' => (float) $this->multiprices_min_ttc[$level],
2303 'multiprices_tva_tx' => (float) $this->multiprices_tva_tx[$level],
2304 'multiprices_recuperableonly' => (float) $this->multiprices_recuperableonly[$level],
2305 );
2306
2307 return $lastPrice;
2308 }
2309
2310
2311 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2319 private function _log_price($user, $level = 0)
2320 {
2321 // phpcs:enable
2322 global $conf;
2323
2324 $now = dol_now();
2325
2326 // Clean parameters
2327 if (empty($this->price_by_qty)) {
2328 $this->price_by_qty = 0;
2329 }
2330
2331 // Add new price
2332 $sql = "INSERT INTO ".$this->db->prefix()."product_price(price_level,date_price, fk_product, fk_user_author, price_label, price, price_ttc, price_base_type,tosell, tva_tx, default_vat_code, recuperableonly,";
2333 $sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, price_min,price_min_ttc,price_by_qty,entity,fk_price_expression) ";
2334 $sql .= " VALUES(".($level ? ((int) $level) : 1).", '".$this->db->idate($now)."', ".((int) $this->id).", ".((int) $user->id).", ".(empty($this->price_label) ? "null" : "'".$this->db->escape($this->price_label)."'").", ".((float) price2num($this->price)).", ".((float) price2num($this->price_ttc)).",'".$this->db->escape($this->price_base_type)."',".((int) $this->status).", ".((float) price2num($this->tva_tx)).", ".($this->default_vat_code ? ("'".$this->db->escape($this->default_vat_code)."'") : "null").", ".((int) $this->tva_npr).",";
2335 $sql .= " ".price2num($this->localtax1_tx).", ".price2num($this->localtax2_tx).", '".$this->db->escape($this->localtax1_type)."', '".$this->db->escape($this->localtax2_type)."', ".price2num($this->price_min).", ".price2num($this->price_min_ttc).", ".price2num($this->price_by_qty).", ".((int) $conf->entity).",".($this->fk_price_expression > 0 ? ((int) $this->fk_price_expression) : 'null');
2336 $sql .= ")";
2337
2338 dol_syslog(get_class($this)."::_log_price", LOG_DEBUG);
2339 $resql = $this->db->query($sql);
2340 if (!$resql) {
2341 $this->error = $this->db->lasterror();
2342 dol_print_error($this->db);
2343 return -1;
2344 } else {
2345 return 1;
2346 }
2347 }
2348
2349
2350 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2358 public function log_price_delete($user, $rowid)
2359 {
2360 // phpcs:enable
2361 $sql = "DELETE FROM ".$this->db->prefix()."product_price_by_qty";
2362 $sql .= " WHERE fk_product_price = ".((int) $rowid);
2363 $sql .= " AND fk_product_price IN (SELECT pp.rowid FROM ".$this->db->prefix()."product_price as pp WHERE pp.fk_product = ".((int) $this->id).")";
2364 $resql = $this->db->query($sql);
2365
2366 $sql = "DELETE FROM ".$this->db->prefix()."product_price";
2367 $sql .= " WHERE rowid = ".((int) $rowid);
2368 $sql .= " AND fk_product = ".((int) $this->id);
2369 $resql = $this->db->query($sql);
2370 if ($resql) {
2371 return 1;
2372 } else {
2373 $this->error = $this->db->lasterror();
2374 return -1;
2375 }
2376 }
2377
2378
2388 public function getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp = 0)
2389 {
2390 global $hookmanager, $action;
2391
2392 // Call hook if any
2393 if (is_object($hookmanager)) {
2394 $parameters = array('thirdparty_seller' => $thirdparty_seller, 'thirdparty_buyer' => $thirdparty_buyer, 'pqp' => $pqp);
2395 // Note that $action and $object may have been modified by some hooks
2396 $reshook = $hookmanager->executeHooks('getSellPrice', $parameters, $this, $action);
2397 if ($reshook > 0) {
2398 return $hookmanager->resArray;
2399 }
2400 }
2401
2402 // Update if prices fields are defined
2403 $tva_tx = get_default_tva($thirdparty_seller, $thirdparty_buyer, $this->id);
2404 $tva_npr = get_default_npr($thirdparty_seller, $thirdparty_buyer, $this->id);
2405 if (empty($tva_tx)) {
2406 $tva_npr = 0;
2407 }
2408
2409 $pu_ht = $this->price;
2410 $pu_ttc = $this->price_ttc;
2411 $price_min = $this->price_min;
2412 $price_min_ttc = $this->price_min_ttc;
2413 $price_base_type = $this->price_base_type;
2414
2415 // if price by customer / level
2416 if (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) {
2417 require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
2418
2419 $prodcustprice = new ProductCustomerPrice($this->db);
2420
2421 $filter = array('t.fk_product' => (string) $this->id, 't.fk_soc' => (string) $thirdparty_buyer->id);
2422
2423 // If a price per customer exist
2424 $pricebycustomerexist = false;
2425 $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
2426 if ($result) {
2427 if (count($prodcustprice->lines) > 0) {
2428 $date_now = (int) floor(dol_now() / 86400) * 86400; // date without hours
2429 foreach ($prodcustprice->lines as $k => $custprice_line) {
2430 if ($custprice_line->date_begin <= $date_now && (empty($custprice_line->date_end) || $date_now <= $custprice_line->date_end)) {
2431 $pricebycustomerexist = true;
2432 $pu_ht = price($custprice_line->price);
2433 $price_min = price($custprice_line->price_min);
2434 $price_min_ttc = price($custprice_line->price_min_ttc);
2435 $pu_ttc = price($custprice_line->price_ttc);
2436 $price_base_type = $custprice_line->price_base_type;
2437 $tva_tx = $custprice_line->tva_tx;
2438 if ($custprice_line->default_vat_code && !preg_match('/\‍(.*\‍)/', $tva_tx)) {
2439 $tva_tx .= ' (' . $custprice_line->default_vat_code . ')';
2440 }
2441 $tva_npr = $custprice_line->recuperableonly;
2442 if (empty($tva_tx)) {
2443 $tva_npr = 0;
2444 }
2445 break;
2446 }
2447 }
2448 }
2449 }
2450
2451 if (!$pricebycustomerexist && !empty($thirdparty_buyer->price_level)) {
2452 $pu_ht = $this->multiprices[$thirdparty_buyer->price_level];
2453 $pu_ttc = $this->multiprices_ttc[$thirdparty_buyer->price_level];
2454 $price_min = $this->multiprices_min[$thirdparty_buyer->price_level];
2455 $price_min_ttc = $this->multiprices_min_ttc[$thirdparty_buyer->price_level];
2456 $price_base_type = $this->multiprices_base_type[$thirdparty_buyer->price_level];
2457 if (getDolGlobalString('PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL')) {
2458 // using this option is a bug. kept for backward compatibility
2459 if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
2460 $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
2461 }
2462 if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
2463 $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
2464 }
2465 if (empty($tva_tx)) {
2466 $tva_npr = 0;
2467 }
2468 }
2469 }
2470 } elseif (getDolGlobalString('PRODUIT_MULTIPRICES') && !empty($thirdparty_buyer->price_level)) { // // If price per segment
2471 $pu_ht = $this->multiprices[$thirdparty_buyer->price_level];
2472 $pu_ttc = $this->multiprices_ttc[$thirdparty_buyer->price_level];
2473 $price_min = $this->multiprices_min[$thirdparty_buyer->price_level];
2474 $price_min_ttc = $this->multiprices_min_ttc[$thirdparty_buyer->price_level];
2475 $price_base_type = $this->multiprices_base_type[$thirdparty_buyer->price_level];
2476 if (getDolGlobalString('PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL')) {
2477 // using this option is a bug. kept for backward compatibility
2478 if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
2479 $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
2480 }
2481 if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
2482 $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
2483 }
2484 if (empty($tva_tx)) {
2485 $tva_npr = 0;
2486 }
2487 }
2488 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES')) {
2489 // If price per customer
2490 require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
2491
2492 $prodcustprice = new ProductCustomerPrice($this->db);
2493
2494 $filter = array('t.fk_product' => (string) $this->id, 't.fk_soc' => (string) $thirdparty_buyer->id);
2495
2496 $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
2497 if ($result) {
2498 if (count($prodcustprice->lines) > 0) {
2499 $date_now = (int) floor(dol_now() / 86400) * 86400; // date without hours
2500 foreach ($prodcustprice->lines as $k => $custprice_line) {
2501 if ($custprice_line->date_begin <= $date_now && (empty($custprice_line->date_end) || $date_now <= $custprice_line->date_end)) {
2502 $pu_ht = price($custprice_line->price);
2503 $price_min = price($custprice_line->price_min);
2504 $price_min_ttc = price($custprice_line->price_min_ttc);
2505 $pu_ttc = price($custprice_line->price_ttc);
2506 $price_base_type = $custprice_line->price_base_type;
2507 $tva_tx = $custprice_line->tva_tx;
2508 if ($custprice_line->default_vat_code && !preg_match('/\‍(.*\‍)/', $tva_tx)) {
2509 $tva_tx .= ' (' . $custprice_line->default_vat_code . ')';
2510 }
2511 $tva_npr = $custprice_line->recuperableonly;
2512 if (empty($tva_tx)) {
2513 $tva_npr = 0;
2514 }
2515 break;
2516 }
2517 }
2518 }
2519 }
2520 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY')) {
2521 // If price per quantity
2522 if ($this->prices_by_qty[0]) {
2523 // yes, this product has some prices per quantity
2524 // Search price into product_price_by_qty from $this->id
2525 foreach ($this->prices_by_qty_list[0] as $priceforthequantityarray) {
2526 if ($priceforthequantityarray['rowid'] != $pqp) {
2527 continue;
2528 }
2529 // We found the price
2530 if ($priceforthequantityarray['price_base_type'] == 'HT') {
2531 $pu_ht = $priceforthequantityarray['unitprice'];
2532 } else {
2533 $pu_ttc = $priceforthequantityarray['unitprice'];
2534 }
2535 break;
2536 }
2537 }
2538 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) {
2539 // If price per quantity and customer
2540 if ($this->prices_by_qty[$thirdparty_buyer->price_level]) {
2541 // yes, this product has some prices per quantity
2542 // Search price into product_price_by_qty from $this->id
2543 foreach ($this->prices_by_qty_list[$thirdparty_buyer->price_level] as $priceforthequantityarray) {
2544 if ($priceforthequantityarray['rowid'] != $pqp) {
2545 continue;
2546 }
2547 // We found the price
2548 if ($priceforthequantityarray['price_base_type'] == 'HT') {
2549 $pu_ht = $priceforthequantityarray['unitprice'];
2550 } else {
2551 $pu_ttc = $priceforthequantityarray['unitprice'];
2552 }
2553 break;
2554 }
2555 }
2556 }
2557
2558 $localtax1 = get_localtax((string) $tva_tx, 1, $thirdparty_buyer, $thirdparty_seller, $tva_npr);
2559 $localtax2 = get_localtax((string) $tva_tx, 2, $thirdparty_buyer, $thirdparty_seller, $tva_npr);
2560
2561 return array('pu_ht' => $pu_ht, 'pu_ttc' => $pu_ttc, 'price_min' => $price_min, 'price_min_ttc' => $price_min_ttc, 'price_base_type' => $price_base_type, 'tva_tx' => $tva_tx, 'tva_npr' => $tva_npr, 'localtax1' => $localtax1, 'localtax2' => $localtax2);
2562 }
2563
2564 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2578 public function get_buyprice($prodfournprice, $qty, $product_id = 0, $fourn_ref = '', $fk_soc = 0)
2579 {
2580 // phpcs:enable
2581 global $action, $hookmanager;
2582
2583 // Call hook if any
2584 if (is_object($hookmanager)) {
2585 $parameters = array(
2586 'prodfournprice' => $prodfournprice,
2587 'qty' => $qty,
2588 'product_id' => $product_id,
2589 'fourn_ref' => $fourn_ref,
2590 'fk_soc' => $fk_soc,
2591 );
2592 // Note that $action and $object may have been modified by some hooks
2593 $reshook = $hookmanager->executeHooks('getBuyPrice', $parameters, $this, $action);
2594 if ($reshook > 0) {
2595 return $hookmanager->resArray;
2596 }
2597 }
2598
2599 $result = 0;
2600
2601 // We do a first search with a select by searching with couple prodfournprice and qty only (later we will search on triplet qty/product_id/fourn_ref)
2602 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2603 $sql .= " pfp.fk_product, pfp.ref_fourn as ref_supplier, pfp.desc_fourn as desc_supplier, pfp.tva_tx, pfp.default_vat_code, pfp.fk_supplier_price_expression,";
2604 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2605 $sql .= " pfp.packaging";
2606 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2607 $sql .= " WHERE pfp.rowid = ".((int) $prodfournprice);
2608 if ($qty > 0) {
2609 $sql .= " AND pfp.quantity <= ".((float) $qty);
2610 }
2611 $sql .= " ORDER BY pfp.quantity DESC";
2612
2613 dol_syslog(get_class($this)."::get_buyprice first search by prodfournprice/qty", LOG_DEBUG);
2614 $resql = $this->db->query($sql);
2615 if ($resql) {
2616 $obj = $this->db->fetch_object($resql);
2617 if ($obj && $obj->quantity > 0) { // If we found a supplier prices from the id of supplier price
2618 if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2619 $prod_supplier = new ProductFournisseur($this->db);
2620 $prod_supplier->product_fourn_price_id = $obj->rowid;
2621 $prod_supplier->id = $obj->fk_product;
2622 $prod_supplier->fourn_qty = $obj->quantity;
2623 $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2624 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2625
2626 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2627 $priceparser = new PriceParser($this->db);
2628 $price_result = $priceparser->parseProductSupplier($prod_supplier);
2629 if ($price_result >= 0) {
2630 $obj->price = $price_result;
2631 }
2632 }
2633 $this->product_fourn_price_id = $obj->rowid;
2634 $this->buyprice = $obj->price; // deprecated
2635 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product of supplier
2636 $this->fourn_price_base_type = 'HT'; // Price base type
2637 $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2638 $this->ref_fourn = $obj->ref_supplier; // deprecated
2639 $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2640 $this->desc_supplier = $obj->desc_supplier; // desc supplier
2641 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2642 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2643 $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2644 $this->fourn_multicurrency_price = $obj->multicurrency_price;
2645 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2646 $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2647 $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2648 $this->fourn_multicurrency_code = $obj->multicurrency_code;
2649 if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2650 $this->packaging = (float) $obj->packaging;
2651 }
2652 // Expose the supplier minimum purchase quantity so callers can enforce qty_min
2653 // on top of packaging-multiple rounding (#38783). pfp.quantity is the lowest
2654 // qty for which this price line is valid; for "below-min" callers this is the
2655 // floor they must round up to.
2656 $this->fourn_qty = $obj->quantity;
2657 $result = $obj->fk_product;
2658 return $result;
2659 } else { // If not found
2660 // We do a second search by doing a select again but searching with less reliable criteria: couple qty/id product, and if set fourn_ref or fk_soc.
2661 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2662 $sql .= " pfp.fk_product, pfp.ref_fourn as ref_supplier, pfp.desc_fourn as desc_supplier, pfp.tva_tx, pfp.default_vat_code, pfp.fk_supplier_price_expression,";
2663 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2664 $sql .= " pfp.packaging";
2665 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2666 $sql .= " WHERE 1 = 1";
2667 if ($product_id > 0) {
2668 $sql .= " AND pfp.fk_product = ".((int) $product_id);
2669 }
2670 if ($fourn_ref != 'none') {
2671 $sql .= " AND pfp.ref_fourn = '".$this->db->escape($fourn_ref)."'";
2672 }
2673 if ($fk_soc > 0) {
2674 $sql .= " AND pfp.fk_soc = ".((int) $fk_soc);
2675 }
2676 if ($qty > 0) {
2677 $sql .= " AND pfp.quantity <= ".((float) $qty);
2678 }
2679 $sql .= " ORDER BY pfp.quantity DESC";
2680 $sql .= " LIMIT 1";
2681
2682 dol_syslog(get_class($this)."::get_buyprice second search from qty/ref/product_id", LOG_DEBUG);
2683 $resql = $this->db->query($sql);
2684 if ($resql) {
2685 $obj = $this->db->fetch_object($resql);
2686 if ($obj && $obj->quantity > 0) { // If found
2687 if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2688 $prod_supplier = new ProductFournisseur($this->db);
2689 $prod_supplier->product_fourn_price_id = $obj->rowid;
2690 $prod_supplier->id = $obj->fk_product;
2691 $prod_supplier->fourn_qty = $obj->quantity;
2692 $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2693 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2694
2695 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2696 $priceparser = new PriceParser($this->db);
2697 $price_result = $priceparser->parseProductSupplier($prod_supplier);
2698 if ($price_result >= 0) {
2699 $obj->price = $price_result;
2700 }
2701 }
2702 $this->product_fourn_price_id = $obj->rowid;
2703 $this->buyprice = $obj->price; // deprecated
2704 $this->fourn_qty = $obj->quantity; // min quantity for price for a virtual supplier
2705 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product for a virtual supplier
2706 $this->fourn_price_base_type = 'HT'; // Price base type for a virtual supplier
2707 $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2708 $this->ref_fourn = $obj->ref_supplier; // deprecated
2709 $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2710 $this->desc_supplier = $obj->desc_supplier; // desc supplier
2711 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2712 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2713 $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2714 $this->fourn_multicurrency_price = $obj->multicurrency_price;
2715 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2716 $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2717 $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2718 $this->fourn_multicurrency_code = $obj->multicurrency_code;
2719 if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2720 $this->packaging = (float) $obj->packaging;
2721 }
2722 // Expose pfp.quantity so callers can enforce qty_min on top of packaging
2723 // rounding (#38783).
2724 $this->fourn_qty = $obj->quantity;
2725 $result = $obj->fk_product;
2726 return $result;
2727 } else {
2728 return -1; // Ce produit n'existe pas avec cet id tarif fournisseur ou existe mais qte insuffisante, ni pour le couple produit/ref fournisseur dans la quantité.
2729 }
2730 } else {
2731 $this->error = $this->db->lasterror();
2732 return -3;
2733 }
2734 }
2735 } else {
2736 $this->error = $this->db->lasterror();
2737 return -2;
2738 }
2739 }
2740
2741
2760 public function updatePrice($newprice, $newpricebase, $user, $newvat = null, $newminprice = 0, $level = 0, $newnpr = 0, $newpbq = 0, $ignore_autogen = 0, $localtaxes_array = array(), $newdefaultvatcode = '', $price_label = '', $notrigger = 0)
2761 {
2762 $lastPriceData = $this->getArrayForPriceCompare($level); // temporary store current price before update
2763
2764 $id = $this->id;
2765
2766 dol_syslog(get_class($this)."::update_price id=".$id." newprice=".$newprice." newpricebase=".$newpricebase." newminprice=".$newminprice." level=".$level." npr=".$newnpr." newdefaultvatcode=".$newdefaultvatcode);
2767
2768 // Clean parameters
2769 if (empty($this->tva_tx)) {
2770 $this->tva_tx = 0.0;
2771 }
2772 if (empty($newnpr)) {
2773 $newnpr = 0.0;
2774 }
2775 if (empty($newminprice)) {
2776 $newminprice = 0.0;
2777 }
2778
2779 // Check parameters
2780 if ($newvat === null || $newvat == '') { // Maintain '' for backwards compatibility
2781 $newvat = (float) $this->tva_tx;
2782 }
2783
2784 $localtaxtype1 = '';
2785 $localtaxtype2 = '';
2786
2787 // If multiprices are enabled, then we check if the current product is subject to price autogeneration
2788 // Price will be modified ONLY when the first one is the one that is being modified
2789 if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && !$ignore_autogen && $this->price_autogen && ($level == 1)) {
2790 return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpbq);
2791 }
2792
2793 if (!empty($newminprice) && ($newminprice > $newprice)) {
2794 $this->error = 'ErrorPriceCantBeLowerThanMinPrice';
2795 return -1;
2796 }
2797
2798 if ($newprice === 0 || $newprice !== '') {
2799 if ($newpricebase == 'TTC') {
2800 $price_ttc = (float) price2num($newprice, 'MU');
2801 $price = (float) price2num($newprice) / (1 + ((float) $newvat / 100));
2802 $price = (float) price2num($price, 'MU');
2803
2804 if ((string) $newminprice != '0') {
2805 $price_min_ttc = (float) price2num($newminprice, 'MU');
2806 $price_min = (float) price2num($newminprice) / (1 + ($newvat / 100));
2807 $price_min = (float) price2num($price_min, 'MU');
2808 } else {
2809 $price_min = 0.0;
2810 $price_min_ttc = 0.0;
2811 }
2812 } else {
2813 $price = (float) price2num($newprice, 'MU');
2814 $price_ttc = ($newnpr != 1) ? (float) price2num($newprice) * (1 + ($newvat / 100)) : $price;
2815 $price_ttc = (float) price2num($price_ttc, 'MU');
2816
2817 if ((string) $newminprice != '0') {
2818 $price_min = (float) price2num($newminprice, 'MU');
2819 $price_min_ttc = (float) price2num($newminprice) * (1 + ($newvat / 100));
2820 $price_min_ttc = (float) price2num($price_min_ttc, 'MU');
2821 //print 'X'.$newminprice.'-'.$price_min;
2822 } else {
2823 $price_min = 0.0;
2824 $price_min_ttc = 0.0;
2825 }
2826 }
2827 //print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc;
2828 if (count($localtaxes_array) > 0) {
2829 $localtaxtype1 = $localtaxes_array['0'];
2830 $localtax1 = $localtaxes_array['1'];
2831 $localtaxtype2 = $localtaxes_array['2'];
2832 $localtax2 = $localtaxes_array['3'];
2833 } else {
2834 // if array empty, we try to use the vat code
2835 if (!empty($newdefaultvatcode)) {
2836 global $mysoc;
2837 // Get record from code
2838 $sql = "SELECT t.rowid, t.code, t.recuperableonly as tva_npr, t.localtax1, t.localtax2, t.localtax1_type, t.localtax2_type";
2839 $sql .= " FROM ".MAIN_DB_PREFIX."c_tva as t, ".MAIN_DB_PREFIX."c_country as c";
2840 $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '".$this->db->escape($mysoc->country_code)."'";
2841 $sql .= " AND t.taux = ".((float) $newdefaultvatcode)." AND t.active = 1";
2842 $sql .= " AND t.code = '".$this->db->escape($newdefaultvatcode)."'";
2843 $resql = $this->db->query($sql);
2844 if ($resql) {
2845 $obj = $this->db->fetch_object($resql);
2846 if ($obj) {
2847 $npr = $obj->tva_npr;
2848 $localtax1 = $obj->localtax1;
2849 $localtax2 = $obj->localtax2;
2850 $localtaxtype1 = $obj->localtax1_type;
2851 $localtaxtype2 = $obj->localtax2_type;
2852 }
2853 }
2854 } else {
2855 // old method. deprecated because we can't retrieve type
2856 $localtaxtype1 = '0';
2857 $localtax1 = get_localtax($newvat, 1);
2858 $localtaxtype2 = '0';
2859 $localtax2 = get_localtax($newvat, 2);
2860 }
2861 }
2862 if (empty($localtax1)) {
2863 $localtax1 = 0; // If = '' then = 0
2864 }
2865 if (empty($localtax2)) {
2866 $localtax2 = 0; // If = '' then = 0
2867 }
2868
2869 $this->db->begin();
2870
2871 // Ne pas mettre de quote sur les numeriques decimaux.
2872 // Ceci provoque des stockages avec arrondis en base au lieu des valeurs exactes.
2873 $sql = "UPDATE ".$this->db->prefix()."product SET";
2874 $sql .= " price_base_type = '".$this->db->escape($newpricebase)."',";
2875 $sql .= " price = ".(float) $price.",";
2876 $sql .= " price_ttc = ".(float) $price_ttc.",";
2877 $sql .= " price_min = ".(float) $price_min.",";
2878 $sql .= " price_min_ttc = ".(float) $price_min_ttc.",";
2879 $sql .= " localtax1_tx = ".($localtax1 >= 0 ? (float) $localtax1 : 'NULL').",";
2880 $sql .= " localtax2_tx = ".($localtax2 >= 0 ? (float) $localtax2 : 'NULL').",";
2881 $sql .= " localtax1_type = ".($localtaxtype1 != '' ? "'".$this->db->escape($localtaxtype1)."'" : "'0'").",";
2882 $sql .= " localtax2_type = ".($localtaxtype2 != '' ? "'".$this->db->escape($localtaxtype2)."'" : "'0'").",";
2883 $sql .= " default_vat_code = ".($newdefaultvatcode ? "'".$this->db->escape($newdefaultvatcode)."'" : "null").",";
2884 $sql .= " price_label = ".(!empty($price_label) ? "'".$this->db->escape($price_label)."'" : "null").",";
2885 $sql .= " tva_tx = ".(float) price2num($newvat).",";
2886 $sql .= " recuperableonly = '".$this->db->escape((string) $newnpr)."'";
2887 $sql .= " WHERE rowid = ".((int) $id);
2888
2889 dol_syslog(get_class($this)."::update_price", LOG_DEBUG);
2890 $resql = $this->db->query($sql);
2891 if ($resql) {
2892 $this->multiprices[$level] = $price;
2893 $this->multiprices_ttc[$level] = $price_ttc;
2894 $this->multiprices_min[$level] = $price_min;
2895 $this->multiprices_min_ttc[$level] = $price_min_ttc;
2896 $this->multiprices_base_type[$level] = $newpricebase;
2897 $this->multiprices_default_vat_code[$level] = $newdefaultvatcode;
2898 $this->multiprices_tva_tx[$level] = $newvat;
2899 $this->multiprices_recuperableonly[$level] = $newnpr;
2900
2901 $this->price = $price;
2902 $this->price_label = $price_label;
2903 $this->price_ttc = $price_ttc;
2904 $this->price_min = $price_min;
2905 $this->price_min_ttc = $price_min_ttc;
2906 $this->price_base_type = $newpricebase;
2907 $this->default_vat_code = $newdefaultvatcode;
2908 $this->tva_tx = $newvat;
2909 $this->tva_npr = $newnpr;
2910
2911 //Local taxes
2912 $this->localtax1_tx = $localtax1;
2913 $this->localtax2_tx = $localtax2;
2914 $this->localtax1_type = $localtaxtype1;
2915 $this->localtax2_type = $localtaxtype2;
2916
2917 // Price by quantity
2918 $this->price_by_qty = $newpbq;
2919
2920 // check if price have really change before log
2921 $newPriceData = $this->getArrayForPriceCompare($level);
2922 if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || (!getDolGlobalString('PRODUIT_MULTIPRICES') && !getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES'))) {
2923 $this->_log_price($user, $level); // Save price for level into table product_price
2924 }
2925
2926 $this->level = $level; // Store level of price edited for trigger
2927
2928 // Call trigger
2929 if (!$notrigger) {
2930 $result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user);
2931 if ($result < 0) {
2932 $this->db->rollback();
2933 return -1;
2934 }
2935 }
2936 // End call triggers
2937
2938 $this->db->commit();
2939 } else {
2940 $this->db->rollback();
2941 $this->error = $this->db->lasterror();
2942 return -1;
2943 }
2944 }
2945
2946 return 1;
2947 }
2948
2956 public function setPriceExpression($expression_id)
2957 {
2958 global $user;
2959
2960 $this->fk_price_expression = $expression_id;
2961
2962 return $this->update($this->id, $user);
2963 }
2964
2977 public function fetch($id = 0, $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0)
2978 {
2979 include_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
2980
2981 global $conf;
2982
2983 dol_syslog(get_class($this)."::fetch id=".$id." ref=".$ref." ref_ext=".$ref_ext);
2984
2985 // Check parameters
2986 if (!$id && !$ref && !$ref_ext && !$barcode) {
2987 $this->error = 'ErrorWrongParameters';
2988 dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR);
2989 return -1;
2990 }
2991
2992 $sql = "SELECT p.rowid, p.ref, p.ref_ext, p.label, p.description, p.url, p.note_public, p.note as note_private, p.customcode, p.fk_country, p.fk_state, p.lifetime, p.qc_frequency, p.price, p.price_ttc,";
2993 $sql .= " p.price_min, p.price_min_ttc, p.price_base_type, p.cost_price, p.default_vat_code, p.tva_tx, p.recuperableonly as tva_npr, p.localtax1_tx, p.localtax2_tx, p.localtax1_type, p.localtax2_type, p.tosell,";
2994 $sql .= " p.tobuy, p.fk_product_type, p.duration, p.fk_default_warehouse, p.fk_default_workstation, p.seuil_stock_alerte, p.canvas, p.net_measure, p.net_measure_units, p.weight, p.weight_units,";
2995 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.last_main_doc,";
2996 $sql .= " p.surface, p.surface_units, p.volume, p.volume_units, p.barcode, p.fk_barcode_type, p.finished, p.fk_default_bom, p.mandatory_period, p.packaging,";
2997 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
2998 $sql .= " p.accountancy_code_buy, p.accountancy_code_buy_intra, p.accountancy_code_buy_export, p.accountancy_code_sell, p.accountancy_code_sell_intra, p.accountancy_code_sell_export,";
2999 } else {
3000 $sql .= " ppe.accountancy_code_buy, ppe.accountancy_code_buy_intra, ppe.accountancy_code_buy_export, ppe.accountancy_code_sell, ppe.accountancy_code_sell_intra, ppe.accountancy_code_sell_export,";
3001 }
3002
3003 // For MultiCompany
3004 // PMP per entity & Stocks Sharings stock_reel includes only stocks shared with this entity
3005 $separatedEntityPMP = false; // Set to true to get the AWP from table llx_product_perentity instead of field 'pmp' into llx_product.
3006 $separatedStock = false; // Set to true will count stock from subtable llx_product_stock. It is slower than using denormalized field 'stock', but it is required when using multientity and shared warehouses.
3007 $visibleWarehousesEntities = $conf->entity;
3008 if (getDolGlobalString('MULTICOMPANY_PRODUCT_SHARING_ENABLED')) {
3009 if (getDolGlobalString('MULTICOMPANY_PMP_PER_ENTITY_ENABLED')) {
3010 $checkPMPPerEntity = $this->db->query("SELECT pmp FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = ".((int) $id)." AND entity = ".(int) $conf->entity);
3011 if ($this->db->num_rows($checkPMPPerEntity) > 0) {
3012 $separatedEntityPMP = true;
3013 }
3014 }
3015 global $mc;
3016 $separatedStock = true;
3017 if (isset($mc->sharings['stock']) && !empty($mc->sharings['stock'])) {
3018 $visibleWarehousesEntities .= "," . implode(",", $mc->sharings['stock']);
3019 }
3020 }
3021 if ($separatedEntityPMP) {
3022 $sql .= " ppe.pmp,";
3023 } else {
3024 $sql .= " p.pmp,";
3025 }
3026 $sql .= " p.datec, GREATEST(p.tms, COALESCE(pef.tms, p.tms)) AS tms, p.import_key, p.entity, p.desiredstock, p.tobatch, p.sell_or_eat_by_mandatory, p.batch_mask, p.fk_unit,";
3027 $sql .= " p.fk_price_expression, p.price_autogen, p.stockable_product, p.model_pdf,";
3028 $sql .= " p.price_label,";
3029 if ($separatedStock) {
3030 $sql .= " SUM(sp.reel) as stock";
3031 } else {
3032 $sql .= " p.stock";
3033 }
3034 $sql .= " FROM ".$this->db->prefix()."product as p";
3035 $sql .= " LEFT JOIN ".$this->db->prefix()."product_extrafields as pef ON pef.fk_object=p.rowid";
3036 if (getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED') || $separatedEntityPMP) {
3037 $sql .= " LEFT JOIN " . $this->db->prefix() . "product_perentity as ppe ON ppe.fk_product = p.rowid AND ppe.entity = " . ((int) $conf->entity);
3038 }
3039 if ($separatedStock) {
3040 $sql .= " LEFT JOIN " . $this->db->prefix() . "product_stock as sp ON sp.fk_product = p.rowid AND sp.fk_entrepot IN (SELECT rowid FROM ".$this->db->prefix()."entrepot WHERE entity IN (".$this->db->sanitize($visibleWarehousesEntities)."))";
3041 }
3042
3043 if ($id) {
3044 $sql .= " WHERE p.rowid = ".((int) $id);
3045 } else {
3046 $sql .= " WHERE p.entity IN (".getEntity($this->element).")";
3047 if ($ref) {
3048 $sql .= " AND p.ref = '".$this->db->escape($ref)."'";
3049 } elseif ($ref_ext) {
3050 $sql .= " AND p.ref_ext = '".$this->db->escape($ref_ext)."'";
3051 } elseif ($barcode) {
3052 $sql .= " AND p.barcode = '".$this->db->escape($barcode)."'";
3053 }
3054 }
3055 if ($separatedStock) {
3056 $sql .= " GROUP BY p.rowid, p.ref, p.ref_ext, p.label, p.description, p.url, p.note_public, p.note, p.customcode, p.fk_country, p.fk_state, p.lifetime, p.qc_frequency, p.price, p.price_ttc,";
3057 $sql .= " p.price_min, p.price_min_ttc, p.price_base_type, p.cost_price, p.default_vat_code, p.tva_tx, p.recuperableonly, p.localtax1_tx, p.localtax2_tx, p.localtax1_type, p.localtax2_type, p.tosell,";
3058 $sql .= " p.tobuy, p.fk_product_type, p.duration, p.fk_default_warehouse, p.fk_default_workstation, p.seuil_stock_alerte, p.canvas, p.net_measure, p.net_measure_units, p.weight, p.weight_units,";
3059 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
3060 $sql .= " p.surface, p.surface_units, p.volume, p.volume_units, p.barcode, p.fk_barcode_type, p.finished, p.fk_default_bom, p.mandatory_period, p.packaging,";
3061 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
3062 $sql .= " p.accountancy_code_buy, p.accountancy_code_buy_intra, p.accountancy_code_buy_export, p.accountancy_code_sell, p.accountancy_code_sell_intra, p.accountancy_code_sell_export,";
3063 } else {
3064 $sql .= " ppe.accountancy_code_buy, ppe.accountancy_code_buy_intra, ppe.accountancy_code_buy_export, ppe.accountancy_code_sell, ppe.accountancy_code_sell_intra, ppe.accountancy_code_sell_export,";
3065 }
3066 if ($separatedEntityPMP) {
3067 $sql .= " ppe.pmp,";
3068 } else {
3069 $sql .= " p.pmp,";
3070 }
3071 $sql .= " p.datec, p.tms, p.import_key, p.entity, p.desiredstock, p.tobatch, p.sell_or_eat_by_mandatory, p.batch_mask, p.fk_unit,";
3072 $sql .= " p.fk_price_expression, p.price_autogen, p.stockable_product, p.model_pdf,";
3073 $sql .= " p.price_label";
3074 // can't be false TODO fix
3075 // if (!$separatedStock) {
3076 // $sql .= ", p.stock";
3077 // }
3078 }
3079
3080 $resql = $this->db->query($sql);
3081 if ($resql) {
3082 unset($this->oldcopy);
3083
3084 if ($this->db->num_rows($resql) > 0) {
3085 $obj = $this->db->fetch_object($resql);
3086
3087 $this->id = $obj->rowid;
3088 $this->ref = $obj->ref;
3089 $this->ref_ext = $obj->ref_ext;
3090 $this->label = $obj->label;
3091 $this->description = $obj->description;
3092 $this->url = $obj->url;
3093 $this->note_public = $obj->note_public;
3094 $this->note_private = $obj->note_private;
3095 $this->note = $obj->note_private; // deprecated
3096
3097 $this->type = $obj->fk_product_type;
3098 $this->price_label = $obj->price_label;
3099 $this->status = $obj->tosell;
3100 $this->status_buy = $obj->tobuy;
3101 $this->status_batch = $obj->tobatch;
3102 $this->sell_or_eat_by_mandatory = $obj->sell_or_eat_by_mandatory;
3103 $this->batch_mask = $obj->batch_mask;
3104
3105 $this->customcode = $obj->customcode;
3106 $this->country_id = $obj->fk_country;
3107 $this->country_code = getCountry($this->country_id, '2', $this->db);
3108 $this->state_id = $obj->fk_state;
3109 $this->lifetime = $obj->lifetime;
3110 $this->qc_frequency = $obj->qc_frequency;
3111 $this->price = $obj->price;
3112 $this->price_ttc = $obj->price_ttc;
3113 $this->price_min = $obj->price_min;
3114 $this->price_min_ttc = $obj->price_min_ttc;
3115 $this->price_base_type = $obj->price_base_type;
3116 $this->cost_price = isset($obj->cost_price) ? (float) $obj->cost_price : null;
3117 $this->default_vat_code = $obj->default_vat_code;
3118 $this->tva_tx = $obj->tva_tx;
3120 $this->tva_npr = $obj->tva_npr;
3122 $this->localtax1_tx = $obj->localtax1_tx;
3123 $this->localtax2_tx = $obj->localtax2_tx;
3124 $this->localtax1_type = $obj->localtax1_type;
3125 $this->localtax2_type = $obj->localtax2_type;
3126
3127 $this->finished = $obj->finished;
3128 $this->fk_default_bom = $obj->fk_default_bom;
3129
3130 $this->duration = $obj->duration;
3131 $matches = [];
3132 preg_match('/([\d.]+)(\w+)/', $obj->duration, $matches);
3133 $this->duration_value = !empty($matches[1]) ? (float) $matches[1] : 0;
3134 $this->duration_unit = !empty($matches[2]) ? (string) $matches[2] : null;
3135 $this->canvas = $obj->canvas;
3136 $this->net_measure = $obj->net_measure;
3137 $this->net_measure_units = $obj->net_measure_units;
3138 $this->weight = $obj->weight;
3139 $this->weight_units = (is_null($obj->weight_units) ? 0 : $obj->weight_units);
3140 $this->length = $obj->length;
3141 $this->length_units = (is_null($obj->length_units) ? 0 : $obj->length_units);
3142 $this->width = $obj->width;
3143 $this->width_units = (is_null($obj->width_units) ? 0 : $obj->width_units);
3144 $this->height = $obj->height;
3145 $this->height_units = (is_null($obj->height_units) ? 0 : $obj->height_units);
3146
3147 $this->surface = $obj->surface;
3148 $this->surface_units = (is_null($obj->surface_units) ? 0 : $obj->surface_units);
3149 $this->volume = $obj->volume;
3150 $this->volume_units = (is_null($obj->volume_units) ? 0 : $obj->volume_units);
3151 $this->barcode = $obj->barcode;
3152 $this->barcode_type = $obj->fk_barcode_type;
3153
3154 $this->accountancy_code_buy = $obj->accountancy_code_buy;
3155 $this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra;
3156 $this->accountancy_code_buy_export = $obj->accountancy_code_buy_export;
3157 $this->accountancy_code_sell = $obj->accountancy_code_sell;
3158 $this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra;
3159 $this->accountancy_code_sell_export = $obj->accountancy_code_sell_export;
3160
3161 $this->fk_default_warehouse = $obj->fk_default_warehouse;
3162 $this->fk_default_workstation = $obj->fk_default_workstation;
3163 $this->seuil_stock_alerte = $obj->seuil_stock_alerte;
3164 $this->desiredstock = $obj->desiredstock;
3165 $this->stock_reel = $obj->stock;
3166 $this->stockable_product = $obj->stockable_product;
3167 $this->pmp = $obj->pmp;
3168
3169 $this->date_creation = $this->db->jdate($obj->datec);
3170 $this->date_modification = $this->db->jdate($obj->tms);
3171
3172 $this->import_key = $obj->import_key;
3173 $this->entity = $obj->entity;
3174
3175 $this->ref_ext = $obj->ref_ext;
3176 $this->fk_price_expression = $obj->fk_price_expression;
3177 $this->fk_unit = $obj->fk_unit;
3178 $this->price_autogen = $obj->price_autogen;
3179 $this->model_pdf = $obj->model_pdf;
3180 $this->last_main_doc = $obj->last_main_doc;
3181
3182 $this->mandatory_period = $obj->mandatory_period;
3183
3184 if (getDolGlobalString('PRODUCT_USE_CUSTOMER_PACKAGING')) {
3185 $this->packaging = (float) $obj->packaging;
3186 }
3187
3188 $this->db->free($resql);
3189
3190 // fetch optionals attributes and labels
3191 $this->fetch_optionals();
3192
3193 // Multilangs
3194 if (getDolGlobalInt('MAIN_MULTILANGS') && empty($ignore_lang_load)) {
3195 $this->getMultiLangs();
3196 }
3197
3198 // Load multiprices array
3199 if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && empty($ignore_price_load)) { // prices per segment
3200 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
3201 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
3202 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3203 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
3204 $sql .= " ,price_label";
3205 $sql .= " FROM ".$this->db->prefix()."product_price";
3206 $sql .= " WHERE entity IN (".getEntity('productprice').")";
3207 $sql .= " AND price_level=".((int) $i);
3208 $sql .= " AND fk_product = ".((int) $this->id);
3209 $sql .= " ORDER BY date_price DESC, rowid DESC"; // Get the most recent line
3210 $sql .= " LIMIT 1"; // Only the first one
3211 $resql = $this->db->query($sql);
3212 if ($resql) {
3213 $result = $this->db->fetch_array($resql);
3214
3215 $this->multiprices[$i] = $result ? $result["price"] : null;
3216 $this->multiprices_ttc[$i] = $result ? $result["price_ttc"] : null;
3217 $this->multiprices_min[$i] = $result ? $result["price_min"] : null;
3218 $this->multiprices_min_ttc[$i] = $result ? $result["price_min_ttc"] : null;
3219 $this->multiprices_base_type[$i] = $result ? $result["price_base_type"] : null;
3220 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
3221 $this->multiprices_tva_tx[$i] = $result ? $result["tva_tx"].(!empty($result['default_vat_code']) ? ' ('.$result['default_vat_code'].')' : '') : null;
3222 $this->multiprices_recuperableonly[$i] = $result ? $result["recuperableonly"] : null;
3223
3224 // Price by quantity
3225 /*
3226 $this->prices_by_qty[$i]=$result["price_by_qty"];
3227 $this->prices_by_qty_id[$i]=$result["rowid"];
3228 // Récuperation de la liste des prix selon qty si flag positionné
3229 if ($this->prices_by_qty[$i] == 1)
3230 {
3231 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
3232 $sql.= " FROM ".$this->db->prefix()."product_price_by_qty";
3233 $sql.= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
3234 $sql.= " ORDER BY quantity ASC";
3235
3236 $resql = $this->db->query($sql);
3237 if ($resql)
3238 {
3239 $resultat=array();
3240 $ii=0;
3241 while ($result= $this->db->fetch_array($resql)) {
3242 $resultat[$ii]=array();
3243 $resultat[$ii]["rowid"]=$result["rowid"];
3244 $resultat[$ii]["price"]= $result["price"];
3245 $resultat[$ii]["unitprice"]= $result["unitprice"];
3246 $resultat[$ii]["quantity"]= $result["quantity"];
3247 $resultat[$ii]["remise_percent"]= $result["remise_percent"];
3248 $resultat[$ii]["remise"]= $result["remise"]; // deprecated
3249 $resultat[$ii]["price_base_type"]= $result["price_base_type"];
3250 $ii++;
3251 }
3252 $this->prices_by_qty_list[$i]=$resultat;
3253 }
3254 else
3255 {
3256 dol_print_error($this->db);
3257 return -1;
3258 }
3259 }*/
3260 } else {
3261 $this->error = $this->db->lasterror;
3262 return -1;
3263 }
3264 }
3265 } elseif ((getDolGlobalString('PRODUIT_CUSTOMER_PRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && empty($ignore_price_load)) { // prices per customers
3266 // Nothing loaded by default. List may be very long.
3267 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY') && empty($ignore_price_load)) { // prices per quantity
3268 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3269 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid";
3270 $sql .= " FROM ".$this->db->prefix()."product_price";
3271 $sql .= " WHERE fk_product = ".((int) $this->id);
3272 $sql .= " ORDER BY date_price DESC, rowid DESC";
3273 $sql .= " LIMIT 1";
3274
3275 $resql = $this->db->query($sql);
3276 if ($resql) {
3277 $result = $this->db->fetch_array($resql);
3278
3279 if ($result) {
3280 // Price by quantity
3281 $this->prices_by_qty[0] = $result["price_by_qty"];
3282 $this->prices_by_qty_id[0] = $result["rowid"];
3283 // Récuperation de la liste des prix selon qty si flag positionné
3284 if ($this->prices_by_qty[0] == 1) {
3285 $sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise, remise, price_base_type";
3286 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
3287 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[0]);
3288 $sql .= " ORDER BY quantity ASC";
3289
3290 $resql = $this->db->query($sql);
3291 if ($resql) {
3292 $resultat = array();
3293 $ii = 0;
3294 while ($result = $this->db->fetch_array($resql)) {
3295 $resultat[$ii] = array();
3296 $resultat[$ii]["rowid"] = $result["rowid"];
3297 $resultat[$ii]["price"] = $result["price"];
3298 $resultat[$ii]["unitprice"] = $result["unitprice"];
3299 $resultat[$ii]["quantity"] = $result["quantity"];
3300 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
3301 //$resultat[$ii]["remise"]= $result["remise"]; // deprecated
3302 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
3303 $ii++;
3304 }
3305 $this->prices_by_qty_list[0] = $resultat;
3306 } else {
3307 $this->error = $this->db->lasterror;
3308 return -1;
3309 }
3310 }
3311 }
3312 } else {
3313 $this->error = $this->db->lasterror;
3314 return -1;
3315 }
3316 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') && empty($ignore_price_load)) { // prices per customer and quantity
3317 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
3318 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
3319 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3320 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
3321 $sql .= " FROM ".$this->db->prefix()."product_price";
3322 $sql .= " WHERE entity IN (".getEntity('productprice').")";
3323 $sql .= " AND price_level=".((int) $i);
3324 $sql .= " AND fk_product = ".((int) $this->id);
3325 $sql .= " ORDER BY date_price DESC, rowid DESC";
3326 $sql .= " LIMIT 1";
3327 $resql = $this->db->query($sql);
3328 if (!$resql) {
3329 $this->error = $this->db->lasterror;
3330 return -1;
3331 } else {
3332 $result = $this->db->fetch_array($resql);
3333 $this->multiprices[$i] = (!empty($result["price"]) ? $result["price"] : 0);
3334 $this->multiprices_ttc[$i] = (!empty($result["price_ttc"]) ? $result["price_ttc"] : 0);
3335 $this->multiprices_min[$i] = (!empty($result["price_min"]) ? $result["price_min"] : 0);
3336 $this->multiprices_min_ttc[$i] = (!empty($result["price_min_ttc"]) ? $result["price_min_ttc"] : 0);
3337 $this->multiprices_base_type[$i] = (!empty($result["price_base_type"]) ? $result["price_base_type"] : '');
3338 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
3339 $this->multiprices_tva_tx[$i] = (!empty($result["tva_tx"]) ? $result["tva_tx"] : 0); // TODO Add ' ('.$result['default_vat_code'].')'
3340 $this->multiprices_recuperableonly[$i] = (!empty($result["recuperableonly"]) ? $result["recuperableonly"] : 0);
3341
3342 // Price by quantity
3343 $this->prices_by_qty[$i] = (!empty($result["price_by_qty"]) ? $result["price_by_qty"] : 0);
3344 $this->prices_by_qty_id[$i] = (!empty($result["rowid"]) ? $result["rowid"] : 0);
3345 // Récuperation de la liste des prix selon qty si flag positionné
3346 if ($this->prices_by_qty[$i] == 1) {
3347 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
3348 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
3349 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
3350 $sql .= " ORDER BY quantity ASC";
3351
3352 $resql = $this->db->query($sql);
3353 if ($resql) {
3354 $resultat = array();
3355 $ii = 0;
3356 while ($result = $this->db->fetch_array($resql)) {
3357 $resultat[$ii] = array();
3358 $resultat[$ii]["rowid"] = $result["rowid"];
3359 $resultat[$ii]["price"] = $result["price"];
3360 $resultat[$ii]["unitprice"] = $result["unitprice"];
3361 $resultat[$ii]["quantity"] = $result["quantity"];
3362 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
3363 $resultat[$ii]["remise"] = $result["remise"]; // deprecated
3364 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
3365 $ii++;
3366 }
3367 $this->prices_by_qty_list[$i] = $resultat;
3368 } else {
3369 $this->error = $this->db->lasterror;
3370 return -1;
3371 }
3372 }
3373 }
3374 }
3375 }
3376
3377 if (isModEnabled('dynamicprices') && !empty($this->fk_price_expression) && empty($ignore_expression)) {
3378 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
3379 $priceparser = new PriceParser($this->db);
3380 $price_result = $priceparser->parseProduct($this);
3381 if ($price_result >= 0) {
3382 $this->price = $price_result;
3383 // Calculate the VAT
3384 $this->price_ttc = (float) price2num($this->price) * (1 + ($this->tva_tx / 100));
3385 $this->price_ttc = (float) price2num($this->price_ttc, 'MU');
3386 }
3387 }
3388
3389 // We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product.
3390 // Instead we just init the stock_warehouse array
3391 $this->stock_warehouse = array();
3392
3393 return 1;
3394 } else {
3395 return 0;
3396 }
3397 } else {
3398 $this->error = $this->db->lasterror();
3399 return -1;
3400 }
3401 }
3402
3403 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3410 public function load_stats_mo($socid = 0)
3411 {
3412 // phpcs:enable
3413 global $user, $hookmanager, $action;
3414
3415 $error = 0;
3416
3417 foreach (array('toconsume', 'consumed', 'toproduce', 'produced') as $role) {
3418 $this->stats_mo['customers_'.$role] = 0;
3419 $this->stats_mo['nb_'.$role] = 0;
3420 $this->stats_mo['qty_'.$role] = 0;
3421
3422 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3423 $sql .= " SUM(mp.qty) as qty";
3424 $sql .= " FROM ".$this->db->prefix()."mrp_mo as c";
3425 $sql .= " INNER JOIN ".$this->db->prefix()."mrp_production as mp ON mp.fk_mo=c.rowid";
3426 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3427 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3428 }
3429 $sql .= " WHERE ";
3430 $sql .= " c.entity IN (".getEntity('mo').")";
3431
3432 $sql .= " AND mp.fk_product = ".((int) $this->id);
3433 $sql .= " AND mp.role ='".$this->db->escape($role)."'";
3434 if ($socid > 0) {
3435 $sql .= " AND c.fk_soc = ".((int) $socid);
3436 }
3437
3438 $result = $this->db->query($sql);
3439 if ($result) {
3440 $obj = $this->db->fetch_object($result);
3441 $this->stats_mo['customers_'.$role] = $obj->nb_customers ? $obj->nb_customers : 0;
3442 $this->stats_mo['nb_'.$role] = $obj->nb ? $obj->nb : 0;
3443 $this->stats_mo['qty_'.$role] = $obj->qty ? price2num($obj->qty, 'MS') : 0; // qty may be a float due to the SUM()
3444 } else {
3445 $this->error = $this->db->error();
3446 $error++;
3447 }
3448 }
3449
3450 if (!empty($error)) {
3451 return -1;
3452 }
3453
3454 $parameters = array('socid' => $socid);
3455 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3456 if ($reshook > 0) {
3457 $this->stats_mo = $hookmanager->resArray['stats_mo'];
3458 }
3459
3460 return 1;
3461 }
3462
3463 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3470 public function load_stats_bom($socid = 0)
3471 {
3472 // phpcs:enable
3473 global $hookmanager, $action;
3474
3475 $error = 0;
3476
3477 $this->stats_bom['nb_toproduce'] = 0;
3478 $this->stats_bom['nb_toconsume'] = 0;
3479 $this->stats_bom['qty_toproduce'] = 0;
3480 $this->stats_bom['qty_toconsume'] = 0;
3481
3482 $sql = "SELECT COUNT(DISTINCT b.rowid) as nb_toproduce,";
3483 $sql .= " SUM(b.qty) as qty_toproduce";
3484 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3485 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom = b.rowid";
3486 $sql .= " WHERE ";
3487 $sql .= " b.entity IN (".getEntity('bom').")";
3488 $sql .= " AND b.fk_product =".((int) $this->id);
3489 $sql .= " GROUP BY b.rowid";
3490
3491 $result = $this->db->query($sql);
3492 if ($result) {
3493 $obj = $this->db->fetch_object($result);
3494 $this->stats_bom['nb_toproduce'] = !empty($obj->nb_toproduce) ? $obj->nb_toproduce : 0;
3495 $this->stats_bom['qty_toproduce'] = !empty($obj->qty_toproduce) ? price2num($obj->qty_toproduce) : 0;
3496 } else {
3497 $this->error = $this->db->error();
3498 $error++;
3499 }
3500
3501 $sql = "SELECT COUNT(DISTINCT bl.rowid) as nb_toconsume,";
3502 $sql .= " SUM(bl.qty) as qty_toconsume";
3503 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3504 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
3505 $sql .= " WHERE ";
3506 $sql .= " b.entity IN (".getEntity('bom').")";
3507 $sql .= " AND bl.fk_product =".((int) $this->id);
3508
3509 $result = $this->db->query($sql);
3510 if ($result) {
3511 $obj = $this->db->fetch_object($result);
3512 $this->stats_bom['nb_toconsume'] = !empty($obj->nb_toconsume) ? $obj->nb_toconsume : 0;
3513 $this->stats_bom['qty_toconsume'] = !empty($obj->qty_toconsume) ? price2num($obj->qty_toconsume) : 0;
3514 } else {
3515 $this->error = $this->db->error();
3516 $error++;
3517 }
3518
3519 if (!empty($error)) {
3520 return -1;
3521 }
3522
3523 $parameters = array('socid' => $socid);
3524 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3525 if ($reshook > 0) {
3526 $this->stats_bom = $hookmanager->resArray['stats_bom'];
3527 }
3528
3529 return 1;
3530 }
3531
3532 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3539 public function load_stats_propale($socid = 0)
3540 {
3541 // phpcs:enable
3542 global $user, $hookmanager, $action;
3543
3544 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,";
3545 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3546 $sql .= " FROM ".$this->db->prefix()."propaldet as pd";
3547 $sql .= ", ".$this->db->prefix()."propal as p";
3548 $sql .= ", ".$this->db->prefix()."societe as s";
3549 $sql .= " WHERE p.rowid = pd.fk_propal";
3550 $sql .= " AND p.fk_soc = s.rowid";
3551 $sql .= " AND p.entity IN (".getEntity('propal').")";
3552 $sql .= " AND pd.fk_product = ".((int) $this->id);
3553 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3554 $sql .= " AND EXISTS (SELECT 1 FROM ".$this->db->prefix()."societe_commerciaux as sc WHERE sc.fk_soc = p.fk_soc AND sc.fk_user = ".((int) $user->id).")";
3555 }
3556 //$sql.= " AND pr.fk_statut != 0";
3557 if ($socid > 0) {
3558 $sql .= " AND p.fk_soc = ".((int) $socid);
3559 }
3560 // Add where from hooks
3561 $parameters = array('socid' => $socid, 'type_element' => 'propal');
3562 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3563 $sql .= $hookmanager->resPrint;
3564 $result = $this->db->query($sql);
3565 if ($result) {
3566 $obj = $this->db->fetch_object($result);
3567 $this->stats_propale['customers'] = $obj->nb_customers;
3568 $this->stats_propale['nb'] = $obj->nb;
3569 $this->stats_propale['rows'] = $obj->nb_rows;
3570 $this->stats_propale['qty'] = $obj->qty ? $obj->qty : 0;
3571
3572 // if it's a virtual product, maybe it is in proposal by extension
3573 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3574 $TFather = $this->getFather();
3575 if (is_array($TFather) && !empty($TFather)) {
3576 foreach ($TFather as &$fatherData) {
3577 $pFather = new Product($this->db);
3578 $pFather->id = $fatherData['id'];
3579 $qtyCoef = $fatherData['qty'];
3580
3581 if ($fatherData['incdec']) {
3582 $pFather->load_stats_propale($socid);
3583
3584 $this->stats_propale['customers'] += $pFather->stats_propale['customers'];
3585 $this->stats_propale['nb'] += $pFather->stats_propale['nb'];
3586 $this->stats_propale['rows'] += $pFather->stats_propale['rows'];
3587 $this->stats_propale['qty'] += $pFather->stats_propale['qty'] * $qtyCoef;
3588 }
3589 }
3590 }
3591 }
3592
3593 $parameters = array('socid' => $socid);
3594 $reshook = $hookmanager->executeHooks('loadStatsCustomerProposal', $parameters, $this, $action);
3595 if ($reshook > 0) {
3596 $this->stats_propale = $hookmanager->resArray['stats_propale'];
3597 }
3598
3599 return 1;
3600 } else {
3601 $this->error = $this->db->error();
3602 return -1;
3603 }
3604 }
3605
3606
3607 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3614 public function load_stats_proposal_supplier($socid = 0)
3615 {
3616 // phpcs:enable
3617 global $user, $hookmanager, $action;
3618
3619 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,";
3620 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3621 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as pd";
3622 $sql .= ", ".$this->db->prefix()."supplier_proposal as p";
3623 $sql .= ", ".$this->db->prefix()."societe as s";
3624 $sql .= " WHERE p.rowid = pd.fk_supplier_proposal";
3625 $sql .= " AND p.fk_soc = s.rowid";
3626 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
3627 $sql .= " AND pd.fk_product = ".((int) $this->id);
3628 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3629 $sql .= " AND EXISTS (SELECT 1 FROM ".$this->db->prefix()."societe_commerciaux as sc WHERE sc.fk_soc = p.fk_soc AND sc.fk_user = ".((int) $user->id).")";
3630 }
3631 //$sql.= " AND pr.fk_statut != 0";
3632 if ($socid > 0) {
3633 $sql .= " AND p.fk_soc = ".((int) $socid);
3634 }
3635
3636 $result = $this->db->query($sql);
3637 if ($result) {
3638 $obj = $this->db->fetch_object($result);
3639 $this->stats_proposal_supplier['suppliers'] = $obj->nb_suppliers;
3640 $this->stats_proposal_supplier['nb'] = $obj->nb;
3641 $this->stats_proposal_supplier['rows'] = $obj->nb_rows;
3642 $this->stats_proposal_supplier['qty'] = $obj->qty ? $obj->qty : 0;
3643
3644 $parameters = array('socid' => $socid);
3645 $reshook = $hookmanager->executeHooks('loadStatsSupplierProposal', $parameters, $this, $action);
3646 if ($reshook > 0) {
3647 $this->stats_proposal_supplier = $hookmanager->resArray['stats_proposal_supplier'];
3648 }
3649
3650 return 1;
3651 } else {
3652 $this->error = $this->db->error();
3653 return -1;
3654 }
3655 }
3656
3657
3658 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3667 public function load_stats_commande($socid = 0, $filtrestatut = '', $forVirtualStock = 0)
3668 {
3669 // phpcs:enable
3670 global $user, $hookmanager, $action;
3671
3672
3673 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3674 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3675 $sql .= " FROM ".$this->db->prefix()."commandedet as cd";
3676 $sql .= ", ".$this->db->prefix()."commande as c";
3677 $sql .= ", ".$this->db->prefix()."societe as s";
3678 $sql .= " WHERE c.rowid = cd.fk_commande";
3679 $sql .= " AND c.fk_soc = s.rowid";
3680 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'commande').")";
3681 $sql .= " AND cd.fk_product = ".((int) $this->id);
3682 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3683 $sql .= " AND EXISTS (SELECT 1 FROM ".$this->db->prefix()."societe_commerciaux as sc WHERE sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id).")";
3684 }
3685 if ($socid > 0) {
3686 $sql .= " AND c.fk_soc = ".((int) $socid);
3687 }
3688 if ($filtrestatut != '') {
3689 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3690 }
3691 // Add where from hooks
3692 $parameters = array('socid' => $socid, 'type_element' => 'order');
3693 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3694 $sql .= $hookmanager->resPrint;
3695 $result = $this->db->query($sql);
3696 if ($result) {
3697 $obj = $this->db->fetch_object($result);
3698 $this->stats_commande['customers'] = $obj->nb_customers;
3699 $this->stats_commande['nb'] = $obj->nb;
3700 $this->stats_commande['rows'] = $obj->nb_rows;
3701 $this->stats_commande['qty'] = $obj->qty ? $obj->qty : 0;
3702
3703 // if it's a virtual product, maybe it is in order by extension
3704 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3705 $TFather = $this->getFather();
3706 if (is_array($TFather) && !empty($TFather)) {
3707 foreach ($TFather as &$fatherData) {
3708 $pFather = new Product($this->db);
3709 $pFather->id = $fatherData['id'];
3710 $qtyCoef = $fatherData['qty'];
3711
3712 if ($fatherData['incdec']) {
3713 $pFather->load_stats_commande($socid, $filtrestatut);
3714
3715 $this->stats_commande['customers'] += $pFather->stats_commande['customers'];
3716 $this->stats_commande['nb'] += $pFather->stats_commande['nb'];
3717 $this->stats_commande['rows'] += $pFather->stats_commande['rows'];
3718 $this->stats_commande['qty'] += $pFather->stats_commande['qty'] * $qtyCoef;
3719 }
3720 }
3721 }
3722 }
3723
3724 // If stock decrease is on invoice validation, the theoretical stock continue to
3725 // count the orders lines containing product in theoretical stock when some are already removed by invoice validation.
3726 if ($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
3727 // Do the request using an UNION instead of a OR in the JOIN that is very slow
3728 if (getDolGlobalString('DECREASE_ONLY_UNINVOICEDPRODUCTS')) {
3729 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is on, we make a compensation but only if order not yet invoice.
3730 $adeduire = 0;
3731 $sql = "SELECT SUM(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3732 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3733 //$sql .= " JOIN ".$this->db->prefix()."element_element as el ON ((el.fk_target = f.rowid AND el.targettype = 'facture' AND sourcetype = 'commande') OR (el.fk_source = f.rowid AND el.targettype = 'commande' AND sourcetype = 'facture'))";
3734 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_target = f.rowid AND el.targettype = 'facture' AND sourcetype = 'commande' ";
3735 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid";
3736 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3737 $sql .= " AND EXISTS (SELECT cd.fk_product FROM ".$this->db->prefix()."commandedet as cd WHERE cd.fk_product = fd.fk_product AND cd.fk_commande = c.rowid)"; // We check that the product is in order lines
3738
3739 $sql .= " UNION ";
3740
3741 $sql .= "SELECT SUM(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3742 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3743 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_source = f.rowid AND el.targettype = 'commande' AND sourcetype = 'facture' ";
3744 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_target = c.rowid";
3745 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3746 $sql .= " AND EXISTS (SELECT cd.fk_product FROM ".$this->db->prefix()."commandedet as cd WHERE cd.fk_product = fd.fk_product AND cd.fk_commande = c.rowid)"; // We check that the product is in order lines
3747 // Add where from hooks
3748 $parameters = array('socid' => $socid, 'type_element' => 'order');
3749 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3750 $sql .= $hookmanager->resPrint;
3751 $resql = $this->db->query($sql);
3752 if ($resql) {
3753 while ($obj = $this->db->fetch_object($resql)) {
3754 $adeduire += $obj->count;
3755 }
3756 }
3757
3758 $this->stats_commande['qty'] -= $adeduire;
3759 } else {
3760 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is off, we make a compensation with lines of invoices linked to the order
3761 include_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
3762
3763 // For every order having invoice already validated we need to decrease stock cause it's in physical stock
3764 $adeduire = 0;
3765 $sql = "SELECT sum(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3766 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3767 //$sql .= " JOIN ".$this->db->prefix()."element_element as el ON ((el.fk_target = f.rowid AND el.targettype = 'facture' AND sourcetype = 'commande') OR (el.fk_source = f.rowid AND el.targettype = 'commande' AND sourcetype = 'facture'))";
3768 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_target = f.rowid AND el.targettype = 'facture' AND sourcetype = 'commande' ";
3769 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid";
3770 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product = ".((int) $this->id);
3771 $sql .= " AND EXISTS (SELECT cd.fk_product FROM ".$this->db->prefix()."commandedet as cd WHERE cd.fk_product = fd.fk_product AND cd.fk_commande = c.rowid)"; // We check that the product is in order lines
3772
3773 $sql .= " UNION ALL ";
3774
3775 $sql .= "SELECT sum(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3776 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3777 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_source = f.rowid AND el.targettype = 'commande' AND sourcetype = 'facture' ";
3778 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_target = c.rowid";
3779 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product = ".((int) $this->id);
3780 $sql .= " AND EXISTS (SELECT cd.fk_product FROM ".$this->db->prefix()."commandedet as cd WHERE cd.fk_product = fd.fk_product AND cd.fk_commande = c.rowid)"; // We check that the product is in order lines
3781
3782 dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3783 // Add where from hooks
3784 $parameters = array('socid' => $socid, 'type_element' => 'order');
3785 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3786 $sql .= $hookmanager->resPrint;
3787 $resql = $this->db->query($sql);
3788 if ($resql) {
3789 while ($obj = $this->db->fetch_object($resql)) {
3790 $adeduire += $obj->count;
3791 }
3792 } else {
3793 $this->error = $this->db->error();
3794 return -1;
3795 }
3796
3797 $this->stats_commande['qty'] -= $adeduire;
3798 }
3799 }
3800
3801 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3802 $reshook = $hookmanager->executeHooks('loadStatsCustomerOrder', $parameters, $this, $action);
3803 if ($reshook > 0) {
3804 $this->stats_commande = $hookmanager->resArray['stats_commande'];
3805 }
3806 return 1;
3807 } else {
3808 $this->error = $this->db->error();
3809 return -1;
3810 }
3811 }
3812
3813 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3823 public function load_stats_commande_fournisseur($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3824 {
3825 // phpcs:enable
3826 global $user, $hookmanager, $action;
3827
3828 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,";
3829 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3830 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as cd";
3831 $sql .= ", ".$this->db->prefix()."commande_fournisseur as c";
3832 $sql .= ", ".$this->db->prefix()."societe as s";
3833 $sql .= " WHERE c.rowid = cd.fk_commande";
3834 $sql .= " AND c.fk_soc = s.rowid";
3835 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
3836 $sql .= " AND cd.fk_product = ".((int) $this->id);
3837 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3838 $sql .= " AND EXISTS (SELECT 1 FROM ".$this->db->prefix()."societe_commerciaux as sc WHERE sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id).")";
3839 }
3840 if ($socid > 0) {
3841 $sql .= " AND c.fk_soc = ".((int) $socid);
3842 }
3843 if ($filtrestatut != '') {
3844 $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")"; // Peut valoir 0
3845 }
3846 if (!empty($dateofvirtualstock)) {
3847 $sql .= " AND c.date_livraison <= '".$this->db->idate($dateofvirtualstock)."'";
3848 }
3849
3850 $result = $this->db->query($sql);
3851 if ($result) {
3852 $obj = $this->db->fetch_object($result);
3853 $this->stats_commande_fournisseur['suppliers'] = $obj->nb_suppliers;
3854 $this->stats_commande_fournisseur['nb'] = $obj->nb;
3855 $this->stats_commande_fournisseur['rows'] = $obj->nb_rows;
3856 $this->stats_commande_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3857
3858 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3859 $reshook = $hookmanager->executeHooks('loadStatsSupplierOrder', $parameters, $this, $action);
3860 if ($reshook > 0) {
3861 $this->stats_commande_fournisseur = $hookmanager->resArray['stats_commande_fournisseur'];
3862 }
3863
3864 return 1;
3865 } else {
3866 $this->error = $this->db->error().' sql='.$sql;
3867 return -1;
3868 }
3869 }
3870
3871 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3881 public function load_stats_sending($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $filterShipmentStatus = '')
3882 {
3883 // phpcs:enable
3884 global $user, $hookmanager, $action;
3885
3886 $sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,";
3887 $sql .= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty";
3888 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
3889 $sql .= ", ".$this->db->prefix()."commandedet as cd";
3890 $sql .= ", ".$this->db->prefix()."commande as c";
3891 $sql .= ", ".$this->db->prefix()."expedition as e";
3892 $sql .= ", ".$this->db->prefix()."societe as s";
3893 $sql .= " WHERE e.rowid = ed.fk_expedition";
3894 $sql .= " AND c.rowid = cd.fk_commande";
3895 $sql .= " AND e.fk_soc = s.rowid";
3896 $sql .= " AND e.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'expedition').")";
3897 $sql .= " AND ed.fk_elementdet = cd.rowid";
3898 $sql .= " AND cd.fk_product = ".((int) $this->id);
3899 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3900 $sql .= " AND EXISTS (SELECT 1 FROM ".$this->db->prefix()."societe_commerciaux as sc WHERE sc.fk_soc = e.fk_soc AND sc.fk_user = ".((int) $user->id).")";
3901 }
3902 if ($socid > 0) {
3903 $sql .= " AND e.fk_soc = ".((int) $socid);
3904 }
3905 if ($filtrestatut != '') {
3906 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3907 }
3908 if (!empty($filterShipmentStatus)) {
3909 $sql .= " AND e.fk_statut IN (".$this->db->sanitize($filterShipmentStatus).")";
3910 }
3911
3912 $result = $this->db->query($sql);
3913 if ($result) {
3914 $obj = $this->db->fetch_object($result);
3915 $this->stats_expedition['customers'] = $obj->nb_customers;
3916 $this->stats_expedition['nb'] = $obj->nb;
3917 $this->stats_expedition['rows'] = $obj->nb_rows;
3918 $this->stats_expedition['qty'] = $obj->qty ? $obj->qty : 0;
3919
3920 // if it's a virtual product, maybe it is in sending by extension
3921 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3922 $TFather = $this->getFather();
3923 if (is_array($TFather) && !empty($TFather)) {
3924 foreach ($TFather as &$fatherData) {
3925 $pFather = new Product($this->db);
3926 $pFather->id = $fatherData['id'];
3927 $qtyCoef = $fatherData['qty'];
3928
3929 if ($fatherData['incdec']) {
3930 $pFather->load_stats_sending($socid, $filtrestatut, $forVirtualStock);
3931
3932 $this->stats_expedition['customers'] += $pFather->stats_expedition['customers'];
3933 $this->stats_expedition['nb'] += $pFather->stats_expedition['nb'];
3934 $this->stats_expedition['rows'] += $pFather->stats_expedition['rows'];
3935 $this->stats_expedition['qty'] += $pFather->stats_expedition['qty'] * $qtyCoef;
3936 }
3937 }
3938 }
3939 }
3940
3941 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock, 'filterShipmentStatus' => $filterShipmentStatus);
3942 $reshook = $hookmanager->executeHooks('loadStatsSending', $parameters, $this, $action);
3943 if ($reshook > 0) {
3944 $this->stats_expedition = $hookmanager->resArray['stats_expedition'];
3945 }
3946
3947 return 1;
3948 } else {
3949 $this->error = $this->db->error();
3950 return -1;
3951 }
3952 }
3953
3954 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3964 public function load_stats_reception($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3965 {
3966 // phpcs:enable
3967 global $user, $hookmanager, $action;
3968
3969 $sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_suppliers, COUNT(DISTINCT cf.rowid) as nb,";
3970 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3971 $sql .= " FROM ".$this->db->prefix()."receptiondet_batch as fd";
3972 $sql .= ", ".$this->db->prefix()."commande_fournisseur as cf";
3973 $sql .= ", ".$this->db->prefix()."societe as s";
3974 $sql .= " WHERE cf.rowid = fd.fk_element";
3975 $sql .= " AND cf.fk_soc = s.rowid";
3976 $sql .= " AND cf.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
3977 $sql .= " AND fd.fk_product = ".((int) $this->id);
3978 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3979 $sql .= " AND EXISTS (SELECT 1 FROM ".$this->db->prefix()."societe_commerciaux as sc WHERE sc.fk_soc = cf.fk_soc AND sc.fk_user = ".((int) $user->id).")";
3980 }
3981 if ($socid > 0) {
3982 $sql .= " AND cf.fk_soc = ".((int) $socid);
3983 }
3984 if ($filtrestatut != '') {
3985 $sql .= " AND cf.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3986 }
3987 if (!empty($dateofvirtualstock)) {
3988 $sql .= " AND fd.datec <= '".$this->db->idate($dateofvirtualstock)."'";
3989 }
3990
3991 $result = $this->db->query($sql);
3992 if ($result) {
3993 $obj = $this->db->fetch_object($result);
3994 $this->stats_reception['suppliers'] = $obj->nb_suppliers;
3995 $this->stats_reception['nb'] = $obj->nb;
3996 $this->stats_reception['rows'] = $obj->nb_rows;
3997 $this->stats_reception['qty'] = $obj->qty ? $obj->qty : 0;
3998
3999 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
4000 $reshook = $hookmanager->executeHooks('loadStatsReception', $parameters, $this, $action);
4001 if ($reshook > 0) {
4002 $this->stats_reception = $hookmanager->resArray['stats_reception'];
4003 }
4004
4005 return 1;
4006 } else {
4007 $this->error = $this->db->error();
4008 return -1;
4009 }
4010 }
4011
4012 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4023 public function load_stats_inproduction($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null, $warehouseid = 0)
4024 {
4025 // phpcs:enable
4026 global $user, $hookmanager, $action;
4027
4028 $serviceStockIsEnabled = isModEnabled("service") && getDolGlobalString('STOCK_SUPPORTS_SERVICES');
4029
4030 $sql = "SELECT COUNT(DISTINCT m.fk_soc) as nb_customers, COUNT(DISTINCT m.rowid) as nb,";
4031 $sql .= " COUNT(mp.rowid) as nb_rows, SUM(mp.qty) as qty, role";
4032 $sql .= " FROM ".$this->db->prefix()."mrp_production as mp";
4033 $sql .= ", ".$this->db->prefix()."mrp_mo as m";
4034 $sql .= " LEFT JOIN ".$this->db->prefix()."societe as s ON s.rowid = m.fk_soc";
4035 $sql .= " WHERE m.rowid = mp.fk_mo";
4036 $sql .= " AND m.entity IN (".getEntity(($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE')) ? 'stock' : 'mrp').")";
4037 $sql .= " AND mp.fk_product = ".((int) $this->id);
4038 $sql .= " AND (mp.disable_stock_change IN (0) OR mp.disable_stock_change IS NULL)";
4039 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
4040 $sql .= " AND EXISTS (SELECT 1 FROM ".$this->db->prefix()."societe_commerciaux as sc WHERE sc.fk_soc = m.fk_soc AND sc.fk_user = ".((int) $user->id).")";
4041 }
4042 if ($socid > 0) {
4043 $sql .= " AND m.fk_soc = ".((int) $socid);
4044 }
4045 if ($filtrestatut != '') {
4046 $sql .= " AND m.status IN (".$this->db->sanitize($filtrestatut).")";
4047 }
4048 if (!empty($dateofvirtualstock)) {
4049 $sql .= " AND m.date_valid <= '".$this->db->idate($dateofvirtualstock)."'"; // better date to code ? end of production ?
4050 }
4051 if (!$serviceStockIsEnabled) {
4052 $sql .= "AND EXISTS (SELECT p.rowid FROM ".$this->db->prefix()."product AS p WHERE p.rowid = ".((int) $this->id)." AND p.fk_product_type IN (0))";
4053 }
4054 if (!empty($warehouseid)) {
4055 $sql .= " AND m.fk_warehouse = ".((int) $warehouseid);
4056 }
4057 $sql .= " GROUP BY role";
4058
4059 if ($warehouseid) {
4060 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
4061 } else {
4062 $this->stats_mrptoconsume['customers'] = 0;
4063 $this->stats_mrptoconsume['nb'] = 0;
4064 $this->stats_mrptoconsume['rows'] = 0;
4065 $this->stats_mrptoconsume['qty'] = 0.0;
4066 $this->stats_mrptoproduce['customers'] = 0;
4067 $this->stats_mrptoproduce['nb'] = 0;
4068 $this->stats_mrptoproduce['rows'] = 0;
4069 $this->stats_mrptoproduce['qty'] = 0.0;
4070 }
4071
4072 $result = $this->db->query($sql);
4073 if ($result) {
4074 while ($obj = $this->db->fetch_object($result)) {
4075 if ($obj->role == 'toconsume' && empty($warehouseid)) {
4076 $this->stats_mrptoconsume['customers'] += (int) $obj->nb_customers;
4077 $this->stats_mrptoconsume['nb'] += (int) $obj->nb;
4078 $this->stats_mrptoconsume['rows'] += (int) $obj->nb_rows;
4079 $this->stats_mrptoconsume['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4080 }
4081 if ($obj->role == 'consumed' && empty($warehouseid)) {
4082 //$this->stats_mrptoconsume['customers'] += $obj->nb_customers;
4083 //$this->stats_mrptoconsume['nb'] += $obj->nb;
4084 //$this->stats_mrptoconsume['rows'] += $obj->nb_rows;
4085 $this->stats_mrptoconsume['qty'] -= ($obj->qty ? (float) $obj->qty : 0.0);
4086 }
4087 if ($obj->role == 'toproduce') {
4088 if ($warehouseid) {
4089 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4090 } else {
4091 $this->stats_mrptoproduce['customers'] += (int) $obj->nb_customers;
4092 $this->stats_mrptoproduce['nb'] += (int) $obj->nb;
4093 $this->stats_mrptoproduce['rows'] += (int) $obj->nb_rows;
4094 $this->stats_mrptoproduce['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4095 }
4096 }
4097 if ($obj->role == 'produced') {
4098 //$this->stats_mrptoproduce['customers'] += $obj->nb_customers;
4099 //$this->stats_mrptoproduce['nb'] += $obj->nb;
4100 //$this->stats_mrptoproduce['rows'] += $obj->nb_rows;
4101 if ($warehouseid) {
4102 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
4103 } else {
4104 $this->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
4105 }
4106 }
4107 }
4108
4109 // Clean data
4110 if ($warehouseid) {
4111 if ($this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] < 0) {
4112 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
4113 }
4114 } else {
4115 if ($this->stats_mrptoconsume['qty'] < 0) {
4116 $this->stats_mrptoconsume['qty'] = 0;
4117 }
4118 if ($this->stats_mrptoproduce['qty'] < 0) {
4119 $this->stats_mrptoproduce['qty'] = 0;
4120 }
4121 }
4122
4123 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
4124 $reshook = $hookmanager->executeHooks('loadStatsInProduction', $parameters, $this, $action);
4125 if ($reshook > 0) {
4126 $this->stats_mrptoproduce = $hookmanager->resArray['stats_mrptoproduce'];
4127 }
4128
4129 return 1;
4130 } else {
4131 $this->error = $this->db->error();
4132 return -1;
4133 }
4134 }
4135
4136 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4143 public function load_stats_contrat($socid = 0)
4144 {
4145 // phpcs:enable
4146 global $user, $hookmanager, $action;
4147
4148 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
4149 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
4150 $sql .= " FROM ".$this->db->prefix()."contratdet as cd";
4151 $sql .= ", ".$this->db->prefix()."contrat as c";
4152 $sql .= ", ".$this->db->prefix()."societe as s";
4153 $sql .= " WHERE c.rowid = cd.fk_contrat";
4154 $sql .= " AND c.fk_soc = s.rowid";
4155 $sql .= " AND c.entity IN (".getEntity('contract').")";
4156 $sql .= " AND cd.fk_product = ".((int) $this->id);
4157 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4158 $sql .= " AND EXISTS (SELECT 1 FROM ".$this->db->prefix()."societe_commerciaux as sc WHERE sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id).")";
4159 }
4160 //$sql.= " AND c.statut != 0";
4161 if ($socid > 0) {
4162 $sql .= " AND c.fk_soc = ".((int) $socid);
4163 }
4164
4165 $result = $this->db->query($sql);
4166 if ($result) {
4167 $obj = $this->db->fetch_object($result);
4168 $this->stats_contrat['customers'] = $obj->nb_customers;
4169 $this->stats_contrat['nb'] = $obj->nb;
4170 $this->stats_contrat['rows'] = $obj->nb_rows;
4171 $this->stats_contrat['qty'] = $obj->qty ? $obj->qty : 0;
4172
4173 // if it's a virtual product, maybe it is in contract by extension
4174 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4175 $TFather = $this->getFather();
4176 if (is_array($TFather) && !empty($TFather)) {
4177 foreach ($TFather as &$fatherData) {
4178 $pFather = new Product($this->db);
4179 $pFather->id = $fatherData['id'];
4180 $qtyCoef = $fatherData['qty'];
4181
4182 if ($fatherData['incdec']) {
4183 $pFather->load_stats_contrat($socid);
4184
4185 $this->stats_contrat['customers'] += $pFather->stats_contrat['customers'];
4186 $this->stats_contrat['nb'] += $pFather->stats_contrat['nb'];
4187 $this->stats_contrat['rows'] += $pFather->stats_contrat['rows'];
4188 $this->stats_contrat['qty'] += $pFather->stats_contrat['qty'] * $qtyCoef;
4189 }
4190 }
4191 }
4192 }
4193
4194 $parameters = array('socid' => $socid);
4195 $reshook = $hookmanager->executeHooks('loadStatsContract', $parameters, $this, $action);
4196 if ($reshook > 0) {
4197 $this->stats_contrat = $hookmanager->resArray['stats_contrat'];
4198 }
4199
4200 return 1;
4201 } else {
4202 $this->error = $this->db->error().' sql='.$sql;
4203 return -1;
4204 }
4205 }
4206
4207 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4214 public function load_stats_facture($socid = 0)
4215 {
4216 // phpcs:enable
4217 global $user, $hookmanager, $action;
4218
4219 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4220 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(".$this->db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1').") as qty";
4221 $sql .= " FROM ".$this->db->prefix()."facturedet as fd";
4222 $sql .= ", ".$this->db->prefix()."facture as f";
4223 $sql .= ", ".$this->db->prefix()."societe as s";
4224 $sql .= " WHERE f.rowid = fd.fk_facture";
4225 $sql .= " AND f.fk_soc = s.rowid";
4226 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4227 $sql .= " AND fd.fk_product = ".((int) $this->id);
4228 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4229 $sql .= " AND EXISTS (SELECT 1 FROM ".$this->db->prefix()."societe_commerciaux as sc WHERE sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id).")";
4230 }
4231 //$sql.= " AND f.fk_statut != 0";
4232 if ($socid > 0) {
4233 $sql .= " AND f.fk_soc = ".((int) $socid);
4234 }
4235
4236 $result = $this->db->query($sql);
4237 if ($result) {
4238 $obj = $this->db->fetch_object($result);
4239 $this->stats_facture['customers'] = $obj->nb_customers;
4240 $this->stats_facture['nb'] = $obj->nb;
4241 $this->stats_facture['rows'] = $obj->nb_rows;
4242 $this->stats_facture['qty'] = $obj->qty ? $obj->qty : 0;
4243
4244 // if it's a virtual product, maybe it is in invoice by extension
4245 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4246 $TFather = $this->getFather();
4247 if (is_array($TFather) && !empty($TFather)) {
4248 foreach ($TFather as &$fatherData) {
4249 $pFather = new Product($this->db);
4250 $pFather->id = $fatherData['id'];
4251 $qtyCoef = $fatherData['qty'];
4252
4253 if ($fatherData['incdec']) {
4254 $pFather->load_stats_facture($socid);
4255
4256 $this->stats_facture['customers'] += $pFather->stats_facture['customers'];
4257 $this->stats_facture['nb'] += $pFather->stats_facture['nb'];
4258 $this->stats_facture['rows'] += $pFather->stats_facture['rows'];
4259 $this->stats_facture['qty'] += $pFather->stats_facture['qty'] * $qtyCoef;
4260 }
4261 }
4262 }
4263 }
4264
4265 $parameters = array('socid' => $socid);
4266 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoice', $parameters, $this, $action);
4267 if ($reshook > 0) {
4268 $this->stats_facture = $hookmanager->resArray['stats_facture'];
4269 }
4270
4271 return 1;
4272 } else {
4273 $this->error = $this->db->error();
4274 return -1;
4275 }
4276 }
4277
4278
4279 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4286 public function load_stats_facturerec($socid = 0)
4287 {
4288 // phpcs:enable
4289 global $user, $hookmanager, $action;
4290
4291 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4292 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4293 $sql .= " FROM ".MAIN_DB_PREFIX."facturedet_rec as fd";
4294 $sql .= ", ".MAIN_DB_PREFIX."facture_rec as f";
4295 $sql .= ", ".MAIN_DB_PREFIX."societe as s";
4296 $sql .= " WHERE f.rowid = fd.fk_facture";
4297 $sql .= " AND f.fk_soc = s.rowid";
4298 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4299 $sql .= " AND fd.fk_product = ".((int) $this->id);
4300 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4301 $sql .= " AND EXISTS (SELECT 1 FROM ".$this->db->prefix()."societe_commerciaux as sc WHERE sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id).")";
4302 }
4303 //$sql.= " AND f.fk_statut != 0";
4304 if ($socid > 0) {
4305 $sql .= " AND f.fk_soc = ".((int) $socid);
4306 }
4307
4308 $result = $this->db->query($sql);
4309 if ($result) {
4310 $obj = $this->db->fetch_object($result);
4311 $this->stats_facturerec['customers'] = (int) $obj->nb_customers;
4312 $this->stats_facturerec['nb'] = (int) $obj->nb;
4313 $this->stats_facturerec['rows'] = (int) $obj->nb_rows;
4314 $this->stats_facturerec['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4315
4316 // if it's a virtual product, maybe it is in invoice by extension
4317 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4318 $TFather = $this->getFather();
4319 if (is_array($TFather) && !empty($TFather)) {
4320 foreach ($TFather as &$fatherData) {
4321 $pFather = new Product($this->db);
4322 $pFather->id = $fatherData['id'];
4323 $qtyCoef = $fatherData['qty'];
4324
4325 if ($fatherData['incdec']) {
4326 $pFather->load_stats_facture($socid);
4327
4328 $this->stats_facturerec['customers'] += $pFather->stats_facturerec['customers'];
4329 $this->stats_facturerec['nb'] += $pFather->stats_facturerec['nb'];
4330 $this->stats_facturerec['rows'] += $pFather->stats_facturerec['rows'];
4331 $this->stats_facturerec['qty'] += $pFather->stats_facturerec['qty'] * $qtyCoef;
4332 }
4333 }
4334 }
4335 }
4336
4337 $parameters = array('socid' => $socid);
4338 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoiceRec', $parameters, $this, $action);
4339 if ($reshook > 0) {
4340 $this->stats_facturerec = $hookmanager->resArray['stats_facturerec'];
4341 }
4342
4343 return 1;
4344 } else {
4345 $this->error = $this->db->error();
4346 return -1;
4347 }
4348 }
4349
4350 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4357 public function load_stats_facture_fournisseur($socid = 0)
4358 {
4359 // phpcs:enable
4360 global $user, $hookmanager, $action;
4361
4362 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
4363 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4364 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as fd";
4365 $sql .= ", ".$this->db->prefix()."facture_fourn as f";
4366 $sql .= ", ".$this->db->prefix()."societe as s";
4367 $sql .= " WHERE f.rowid = fd.fk_facture_fourn";
4368 $sql .= " AND f.fk_soc = s.rowid";
4369 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4370 $sql .= " AND fd.fk_product = ".((int) $this->id);
4371 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4372 $sql .= " AND EXISTS (SELECT 1 FROM ".$this->db->prefix()."societe_commerciaux as sc WHERE sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id).")";
4373 }
4374 //$sql.= " AND f.fk_statut != 0";
4375 if ($socid > 0) {
4376 $sql .= " AND f.fk_soc = ".((int) $socid);
4377 }
4378
4379 $result = $this->db->query($sql);
4380 if ($result) {
4381 $obj = $this->db->fetch_object($result);
4382 $this->stats_facture_fournisseur['suppliers'] = (int) $obj->nb_suppliers;
4383 $this->stats_facture_fournisseur['nb'] = (int) $obj->nb;
4384 $this->stats_facture_fournisseur['rows'] = (int) $obj->nb_rows;
4385 $this->stats_facture_fournisseur['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4386
4387 $parameters = array('socid' => $socid);
4388 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoice', $parameters, $this, $action);
4389 if ($reshook > 0) {
4390 $this->stats_facture_fournisseur = $hookmanager->resArray['stats_facture_fournisseur'];
4391 }
4392
4393 return 1;
4394 } else {
4395 $this->error = $this->db->error();
4396 return -1;
4397 }
4398 }
4399
4400 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4407 public function load_stats_facturefournrec($socid = 0)
4408 {
4409 // phpcs:enable
4410 global $user, $hookmanager, $action;
4411
4412 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
4413 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4414 $sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn_det_rec as fd";
4415 $sql .= ", ".MAIN_DB_PREFIX."facture_fourn_rec as f";
4416 $sql .= ", ".MAIN_DB_PREFIX."societe as s";
4417 if (!$user->hasRight('societe', 'client', 'voir')) {
4418 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
4419 }
4420 $sql .= " WHERE f.rowid = fd.fk_facture";
4421 $sql .= " AND f.fk_soc = s.rowid";
4422 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4423 $sql .= " AND fd.fk_product = ".((int) $this->id);
4424 if (!$user->hasRight('societe', 'client', 'voir')) {
4425 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4426 }
4427 //$sql.= " AND f.fk_statut != 0";
4428 if ($socid > 0) {
4429 $sql .= " AND f.fk_soc = ".((int) $socid);
4430 }
4431
4432 $result = $this->db->query($sql);
4433 if ($result) {
4434 $obj = $this->db->fetch_object($result);
4435 $this->stats_facturefournrec['suppliers'] = (int) $obj->nb_suppliers;
4436 $this->stats_facturefournrec['nb'] = (int) $obj->nb;
4437 $this->stats_facturefournrec['rows'] = (int) $obj->nb_rows;
4438 $this->stats_facturefournrec['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4439
4440 $parameters = array('socid' => $socid);
4441 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoiceRec', $parameters, $this, $action);
4442 if ($reshook > 0) {
4443 $this->stats_facturefournrec = $hookmanager->resArray['stats_facturefournrec'];
4444 }
4445
4446 return 1;
4447 } else {
4448 $this->error = $this->db->error();
4449 return -1;
4450 }
4451 }
4452
4453 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4462 private function _get_stats($sql, $mode, $year = 0)
4463 {
4464 // phpcs:enable
4465 $tab = array();
4466
4467 $resql = $this->db->query($sql);
4468 if ($resql) {
4469 $num = $this->db->num_rows($resql);
4470 $i = 0;
4471 while ($i < $num) {
4472 $arr = $this->db->fetch_array($resql);
4473 if (is_array($arr)) {
4474 $keyfortab = (string) $arr[1];
4475 if ($year == -1) {
4476 $keyfortab = substr($keyfortab, -2);
4477 }
4478
4479 if ($mode == 'byunit') {
4480 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[0]; // 1st field
4481 } elseif ($mode == 'bynumber') {
4482 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4483 } elseif ($mode == 'byamount') {
4484 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4485 } else {
4486 // Bad value for $mode
4487 return -1;
4488 }
4489 }
4490 $i++;
4491 }
4492 } else {
4493 $this->error = $this->db->error().' sql='.$sql;
4494 return -1;
4495 }
4496
4497 if (empty($year)) {
4498 $year = dol_print_date(time(), '%Y');
4499 $month = dol_print_date(time(), '%m');
4500 } elseif ($year == -1) {
4501 $year = '';
4502 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4503 } else {
4504 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4505 }
4506
4507 $result = array();
4508
4509 for ($j = 0; $j < 12; $j++) {
4510 // $idx is 'D', 'N', 'O', 'S', ... (First letter of month in user language)
4511 $idx = ucfirst(dol_trunc(dol_print_date(dol_mktime(12, 0, 0, $month, 1, 1970), "%b"), 1, 'right', 'UTF-8', 1));
4512
4513 // print $idx.'-'.$year.'-'.$month.'<br>';
4514 $result[$j] = array($idx, isset($tab[$year.$month]) ? $tab[$year.$month] : 0);
4515 // $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0);
4516
4517 $month = "0".($month - 1);
4518 if (dol_strlen($month) == 3) {
4519 $month = substr($month, 1);
4520 }
4521 if ($month == 0) {
4522 $month = 12;
4523 if ($year !== '') { // $year is '' when we want stats for all years
4524 $year -= 1;
4525 }
4526 }
4527 }
4528
4529 return array_reverse($result);
4530 }
4531
4532
4533 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4544 public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4545 {
4546 // phpcs:enable
4547 global $user;
4548
4549 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4550 if ($mode == 'bynumber') {
4551 $sql .= ", count(DISTINCT f.rowid)";
4552 }
4553 $sql .= ", sum(d.total_ht) as total_ht";
4554 $sql .= " FROM ".$this->db->prefix()."facturedet as d, ".$this->db->prefix()."facture as f, ".$this->db->prefix()."societe as s";
4555 if ($filteronproducttype >= 0) {
4556 $sql .= ", ".$this->db->prefix()."product as p";
4557 }
4558 if (!$user->hasRight('societe', 'client', 'voir')) {
4559 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4560 }
4561 $sql .= " WHERE f.rowid = d.fk_facture";
4562 if ($this->id > 0) {
4563 $sql .= " AND d.fk_product = ".((int) $this->id);
4564 } else {
4565 $sql .= " AND d.fk_product > 0";
4566 }
4567 if ($filteronproducttype >= 0) {
4568 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4569 }
4570 $sql .= " AND f.fk_soc = s.rowid";
4571 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4572 if (!$user->hasRight('societe', 'client', 'voir')) {
4573 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4574 }
4575 if ($socid > 0) {
4576 $sql .= " AND f.fk_soc = $socid";
4577 }
4578 $sql .= $morefilter;
4579 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4580 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4581
4582 return $this->_get_stats($sql, $mode, $year);
4583 }
4584
4585
4586 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4597 public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4598 {
4599 // phpcs:enable
4600 global $user;
4601
4602 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4603 if ($mode == 'bynumber') {
4604 $sql .= ", count(DISTINCT f.rowid)";
4605 }
4606 $sql .= ", sum(d.total_ht) as total_ht";
4607 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as d, ".$this->db->prefix()."facture_fourn as f, ".$this->db->prefix()."societe as s";
4608 if ($filteronproducttype >= 0) {
4609 $sql .= ", ".$this->db->prefix()."product as p";
4610 }
4611 if (!$user->hasRight('societe', 'client', 'voir')) {
4612 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4613 }
4614 $sql .= " WHERE f.rowid = d.fk_facture_fourn";
4615 if ($this->id > 0) {
4616 $sql .= " AND d.fk_product = ".((int) $this->id);
4617 } else {
4618 $sql .= " AND d.fk_product > 0";
4619 }
4620 if ($filteronproducttype >= 0) {
4621 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4622 }
4623 $sql .= " AND f.fk_soc = s.rowid";
4624 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4625 if (!$user->hasRight('societe', 'client', 'voir')) {
4626 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4627 }
4628 if ($socid > 0) {
4629 $sql .= " AND f.fk_soc = $socid";
4630 }
4631 $sql .= $morefilter;
4632 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4633 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4634
4635 return $this->_get_stats($sql, $mode, $year);
4636 }
4637
4638 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4649 public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4650 {
4651 // phpcs:enable
4652 global $user, $hookmanager;
4653
4654 $sql = "SELECT sum(d.qty) as qty, date_format(p.datep, '%Y%m')";
4655 if ($mode == 'bynumber') {
4656 $sql .= ", count(DISTINCT p.rowid)";
4657 }
4658 $sql .= ", sum(d.total_ht) as total_ht";
4659 $sql .= " FROM ".$this->db->prefix()."propaldet as d, ".$this->db->prefix()."propal as p, ".$this->db->prefix()."societe as s";
4660 if ($filteronproducttype >= 0) {
4661 $sql .= ", ".$this->db->prefix()."product as prod";
4662 }
4663 if (!$user->hasRight('societe', 'client', 'voir')) {
4664 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4665 }
4666 $sql .= " WHERE p.rowid = d.fk_propal";
4667 if ($this->id > 0) {
4668 $sql .= " AND d.fk_product = ".((int) $this->id);
4669 } else {
4670 $sql .= " AND d.fk_product > 0";
4671 }
4672 if ($filteronproducttype >= 0) {
4673 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4674 }
4675 $sql .= " AND p.fk_soc = s.rowid";
4676 $sql .= " AND p.entity IN (".getEntity('propal').")";
4677 if (!$user->hasRight('societe', 'client', 'voir')) {
4678 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4679 }
4680 if ($socid > 0) {
4681 $sql .= " AND p.fk_soc = ".((int) $socid);
4682 }
4683 $sql .= $morefilter;
4684 $parameters = array('socid' => $user->socid);
4685 $hookmanager->executeHooks('productGetNbPropal', $parameters, $this); // Note that $action and $object may have been modified by hook
4686 $sql .= $hookmanager->resPrint;
4687 $sql .= " GROUP BY date_format(p.datep,'%Y%m')";
4688 $sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC";
4689 return $this->_get_stats($sql, $mode, $year);
4690 }
4691
4692 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4703 public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4704 {
4705 // phpcs:enable
4706 global $user;
4707
4708 $sql = "SELECT sum(d.qty) as qty, date_format(p.date_valid, '%Y%m')";
4709 if ($mode == 'bynumber') {
4710 $sql .= ", count(DISTINCT p.rowid)";
4711 }
4712 $sql .= ", sum(d.total_ht) as total_ht";
4713 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as d, ".$this->db->prefix()."supplier_proposal as p, ".$this->db->prefix()."societe as s";
4714 if ($filteronproducttype >= 0) {
4715 $sql .= ", ".$this->db->prefix()."product as prod";
4716 }
4717 if (!$user->hasRight('societe', 'client', 'voir')) {
4718 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4719 }
4720 $sql .= " WHERE p.rowid = d.fk_supplier_proposal";
4721 if ($this->id > 0) {
4722 $sql .= " AND d.fk_product = ".((int) $this->id);
4723 } else {
4724 $sql .= " AND d.fk_product > 0";
4725 }
4726 if ($filteronproducttype >= 0) {
4727 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4728 }
4729 $sql .= " AND p.fk_soc = s.rowid";
4730 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
4731 if (!$user->hasRight('societe', 'client', 'voir')) {
4732 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4733 }
4734 if ($socid > 0) {
4735 $sql .= " AND p.fk_soc = ".((int) $socid);
4736 }
4737 $sql .= $morefilter;
4738 $sql .= " GROUP BY date_format(p.date_valid,'%Y%m')";
4739 $sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
4740
4741 return $this->_get_stats($sql, $mode, $year);
4742 }
4743
4744 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4755 public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4756 {
4757 // phpcs:enable
4758 global $user, $hookmanager;
4759
4760 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4761 if ($mode == 'bynumber') {
4762 $sql .= ", count(DISTINCT c.rowid)";
4763 }
4764 $sql .= ", sum(d.total_ht) as total_ht";
4765 $sql .= " FROM ".$this->db->prefix()."commandedet as d, ".$this->db->prefix()."commande as c, ".$this->db->prefix()."societe as s";
4766 if ($filteronproducttype >= 0) {
4767 $sql .= ", ".$this->db->prefix()."product as p";
4768 }
4769 if (!$user->hasRight('societe', 'client', 'voir')) {
4770 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4771 }
4772 $sql .= " WHERE c.rowid = d.fk_commande";
4773 if ($this->id > 0) {
4774 $sql .= " AND d.fk_product = ".((int) $this->id);
4775 } else {
4776 $sql .= " AND d.fk_product > 0";
4777 }
4778 if ($filteronproducttype >= 0) {
4779 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4780 }
4781 $sql .= " AND c.fk_soc = s.rowid";
4782 $sql .= " AND c.entity IN (".getEntity('commande').")";
4783 if (!$user->hasRight('societe', 'client', 'voir')) {
4784 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4785 }
4786 if ($socid > 0) {
4787 $sql .= " AND c.fk_soc = ".((int) $socid);
4788 }
4789 $sql .= $morefilter;
4790 $parameters = array('socid' => $user->socid);
4791 $hookmanager->executeHooks('productGetNbOrder', $parameters, $this); // Note that $action and $object may have been modified by hook
4792 $sql .= $hookmanager->resPrint;
4793 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4794 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4795
4796 return $this->_get_stats($sql, $mode, $year);
4797 }
4798
4799 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4810 public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4811 {
4812 // phpcs:enable
4813 global $user;
4814
4815 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4816 if ($mode == 'bynumber') {
4817 $sql .= ", count(DISTINCT c.rowid)";
4818 }
4819 $sql .= ", sum(d.total_ht) as total_ht";
4820 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as d, ".$this->db->prefix()."commande_fournisseur as c, ".$this->db->prefix()."societe as s";
4821 if ($filteronproducttype >= 0) {
4822 $sql .= ", ".$this->db->prefix()."product as p";
4823 }
4824 if (!$user->hasRight('societe', 'client', 'voir')) {
4825 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4826 }
4827 $sql .= " WHERE c.rowid = d.fk_commande";
4828 if ($this->id > 0) {
4829 $sql .= " AND d.fk_product = ".((int) $this->id);
4830 } else {
4831 $sql .= " AND d.fk_product > 0";
4832 }
4833 if ($filteronproducttype >= 0) {
4834 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4835 }
4836 $sql .= " AND c.fk_soc = s.rowid";
4837 $sql .= " AND c.entity IN (".getEntity('supplier_order').")";
4838 if (!$user->hasRight('societe', 'client', 'voir')) {
4839 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4840 }
4841 if ($socid > 0) {
4842 $sql .= " AND c.fk_soc = ".((int) $socid);
4843 }
4844 $sql .= $morefilter;
4845 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4846 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4847
4848 return $this->_get_stats($sql, $mode, $year);
4849 }
4850
4851 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4862 public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4863 {
4864 // phpcs:enable
4865 global $user;
4866
4867 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_contrat, '%Y%m')";
4868 if ($mode == 'bynumber') {
4869 $sql .= ", count(DISTINCT c.rowid)";
4870 }
4871 $sql .= ", sum(d.total_ht) as total_ht";
4872 $sql .= " FROM ".$this->db->prefix()."contratdet as d, ".$this->db->prefix()."contrat as c, ".$this->db->prefix()."societe as s";
4873 if ($filteronproducttype >= 0) {
4874 $sql .= ", ".$this->db->prefix()."product as p";
4875 }
4876 if (!$user->hasRight('societe', 'client', 'voir')) {
4877 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4878 }
4879 $sql .= " WHERE c.entity IN (".getEntity('contract').")";
4880 $sql .= " AND c.rowid = d.fk_contrat";
4881
4882 if ($this->id > 0) {
4883 $sql .= " AND d.fk_product = ".((int) $this->id);
4884 } else {
4885 $sql .= " AND d.fk_product > 0";
4886 }
4887 if ($filteronproducttype >= 0) {
4888 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4889 }
4890 $sql .= " AND c.fk_soc = s.rowid";
4891
4892 if (!$user->hasRight('societe', 'client', 'voir')) {
4893 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4894 }
4895 if ($socid > 0) {
4896 $sql .= " AND c.fk_soc = ".((int) $socid);
4897 }
4898 $sql .= $morefilter;
4899 $sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')";
4900 $sql .= " ORDER BY date_format(c.date_contrat,'%Y%m') DESC";
4901
4902 return $this->_get_stats($sql, $mode, $year);
4903 }
4904
4905 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4916 public function get_nb_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4917 {
4918 // phpcs:enable
4919 global $user;
4920
4921 $sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')";
4922 if ($mode == 'bynumber') {
4923 $sql .= ", count(DISTINCT d.rowid)";
4924 }
4925 $sql .= " FROM ".$this->db->prefix()."mrp_mo as d LEFT JOIN ".$this->db->prefix()."societe as s ON d.fk_soc = s.rowid";
4926 if ($filteronproducttype >= 0) {
4927 $sql .= ", ".$this->db->prefix()."product as p";
4928 }
4929 if (!$user->hasRight('societe', 'client', 'voir')) {
4930 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4931 }
4932
4933 $sql .= " WHERE d.entity IN (".getEntity('mo').")";
4934 $sql .= " AND d.status > 0";
4935
4936 if ($this->id > 0) {
4937 $sql .= " AND d.fk_product = ".((int) $this->id);
4938 } else {
4939 $sql .= " AND d.fk_product > 0";
4940 }
4941 if ($filteronproducttype >= 0) {
4942 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4943 }
4944
4945 if (!$user->hasRight('societe', 'client', 'voir')) {
4946 $sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4947 }
4948 if ($socid > 0) {
4949 $sql .= " AND d.fk_soc = ".((int) $socid);
4950 }
4951 $sql .= $morefilter;
4952 $sql .= " GROUP BY date_format(d.date_valid,'%Y%m')";
4953 $sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC";
4954
4955 return $this->_get_stats($sql, $mode, $year);
4956 }
4957
4958 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4969 public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
4970 {
4971 // phpcs:enable
4972 global $user;
4973
4974 // Clean parameters
4975 if (!is_numeric($id_pere)) {
4976 $id_pere = 0;
4977 }
4978 if (!is_numeric($id_fils)) {
4979 $id_fils = 0;
4980 }
4981 if (!is_numeric($incdec)) {
4982 $incdec = 0;
4983 }
4984
4985 $result = $this->del_sousproduit($id_pere, $id_fils);
4986 if ($result < 0) {
4987 return $result;
4988 }
4989
4990 // Check not already father of id_pere (to avoid father -> child -> father links)
4991 $sql = "SELECT fk_product_pere from ".$this->db->prefix()."product_association";
4992 $sql .= " WHERE fk_product_pere = ".((int) $id_fils)." AND fk_product_fils = ".((int) $id_pere);
4993 if (!$this->db->query($sql)) {
4994 dol_print_error($this->db);
4995 return -1;
4996 } else {
4997 //Selection of the highest row
4998 $sql = "SELECT MAX(rang) as max_rank FROM ".$this->db->prefix()."product_association";
4999 $sql .= " WHERE fk_product_pere = ".((int) $id_pere);
5000 $resql = $this->db->query($sql);
5001 if ($resql) {
5002 $obj = $this->db->fetch_object($resql);
5003 $rank = $obj->max_rank + 1;
5004 //Addition of a product with the highest rank +1
5005 $sql = "INSERT INTO ".$this->db->prefix()."product_association(fk_product_pere,fk_product_fils,qty,incdec,rang)";
5006 $sql .= " VALUES (".((int) $id_pere).", ".((int) $id_fils).", ".price2num($qty, 'MS').", ".((int) $incdec).", ".((int) $rank).")";
5007 if (! $this->db->query($sql)) {
5008 dol_print_error($this->db);
5009 return -1;
5010 } else {
5011 if (!$notrigger) {
5012 // Call trigger
5013 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_ADD', $user);
5014 if ($result < 0) {
5015 $this->error = $this->db->lasterror();
5016 dol_syslog(get_class($this).'::addSubproduct error='.$this->error, LOG_ERR);
5017 return -1;
5018 }
5019 }
5020 // End call triggers
5021
5022 return 1;
5023 }
5024 } else {
5025 dol_print_error($this->db);
5026 return -1;
5027 }
5028 }
5029 }
5030
5031 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5042 public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
5043 {
5044 // phpcs:enable
5045 global $user;
5046
5047 // Clean parameters
5048 if (!is_numeric($id_pere)) {
5049 $id_pere = 0;
5050 }
5051 if (!is_numeric($id_fils)) {
5052 $id_fils = 0;
5053 }
5054 if (!is_numeric($incdec)) {
5055 $incdec = 1;
5056 }
5057 if (!is_numeric($qty)) {
5058 $qty = 1;
5059 }
5060
5061 $sql = 'UPDATE '.$this->db->prefix().'product_association SET ';
5062 $sql .= 'qty = '.price2num($qty, 'MS');
5063 $sql .= ',incdec = '.((int) $incdec);
5064 $sql .= ' WHERE fk_product_pere = '.((int) $id_pere).' AND fk_product_fils = '.((int) $id_fils);
5065
5066 if (!$this->db->query($sql)) {
5067 dol_print_error($this->db);
5068 return -1;
5069 } else {
5070 if (!$notrigger) {
5071 // Call trigger
5072 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_UPDATE', $user);
5073 if ($result < 0) {
5074 $this->error = $this->db->lasterror();
5075 dol_syslog(get_class($this).'::updateSubproduct error='.$this->error, LOG_ERR);
5076 return -1;
5077 }
5078 // End call triggers
5079 }
5080
5081 return 1;
5082 }
5083 }
5084
5085 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5094 public function del_sousproduit($fk_parent, $fk_child, $notrigger = 0)
5095 {
5096 // phpcs:enable
5097 global $user;
5098
5099 if (!is_numeric($fk_parent)) {
5100 $fk_parent = 0;
5101 }
5102 if (!is_numeric($fk_child)) {
5103 $fk_child = 0;
5104 }
5105
5106 $sql = "DELETE FROM ".$this->db->prefix()."product_association";
5107 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5108 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5109
5110 dol_syslog(get_class($this).'::del_sousproduit', LOG_DEBUG);
5111 if (!$this->db->query($sql)) {
5112 dol_print_error($this->db);
5113 return -1;
5114 }
5115
5116 // Updated ranks so that none are missing
5117 $sqlrank = "SELECT rowid, rang FROM ".$this->db->prefix()."product_association";
5118 $sqlrank .= " WHERE fk_product_pere = ".((int) $fk_parent);
5119 $sqlrank .= " ORDER BY rang";
5120 $resqlrank = $this->db->query($sqlrank);
5121 if ($resqlrank) {
5122 $cpt = 0;
5123 while ($objrank = $this->db->fetch_object($resqlrank)) {
5124 $cpt++;
5125 $sql = "UPDATE ".$this->db->prefix()."product_association";
5126 $sql .= " SET rang = ".((int) $cpt);
5127 $sql .= " WHERE rowid = ".((int) $objrank->rowid);
5128 if (! $this->db->query($sql)) {
5129 dol_print_error($this->db);
5130 return -1;
5131 }
5132 }
5133 }
5134
5135 if (!$notrigger) {
5136 // Call trigger
5137 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_DELETE', $user);
5138 if ($result < 0) {
5139 $this->error = $this->db->lasterror();
5140 dol_syslog(get_class($this).'::delSubproduct error='.$this->error, LOG_ERR);
5141 return -1;
5142 }
5143 // End call triggers
5144 }
5145
5146 return 1;
5147 }
5148
5149 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5157 public function is_sousproduit($fk_parent, $fk_child)
5158 {
5159 // phpcs:enable
5160 $sql = "SELECT fk_product_pere, qty, incdec";
5161 $sql .= " FROM ".$this->db->prefix()."product_association";
5162 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5163 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5164
5165 $result = $this->db->query($sql);
5166 if ($result) {
5167 $num = $this->db->num_rows($result);
5168
5169 if ($num > 0) {
5170 $obj = $this->db->fetch_object($result);
5171
5172 $this->is_sousproduit_qty = $obj->qty;
5173 $this->is_sousproduit_incdec = $obj->incdec;
5174
5175 return 1;
5176 } else {
5177 return 0;
5178 }
5179 } else {
5180 dol_print_error($this->db);
5181 return -1;
5182 }
5183 }
5184
5185
5186 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5197 public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
5198 {
5199 // phpcs:enable
5200 global $conf;
5201
5202 $now = dol_now();
5203
5204 dol_syslog(get_class($this)."::add_fournisseur id_fourn = ".$id_fourn." ref_fourn=".$ref_fourn." quantity=".$quantity, LOG_DEBUG);
5205
5206 // Clean parameters
5207 $quantity = price2num($quantity, 'MS');
5208
5209 if ($ref_fourn) {
5210 // Check if ref is not already used
5211 $sql = "SELECT rowid, fk_product";
5212 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5213 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5214 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5215 $sql .= " AND fk_product <> ".((int) $this->id);
5216 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5217
5218 $resql = $this->db->query($sql);
5219 if ($resql) {
5220 $obj = $this->db->fetch_object($resql);
5221 if ($obj) {
5222 // If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
5223 $this->product_id_already_linked = $obj->fk_product;
5224 return -3;
5225 }
5226 $this->db->free($resql);
5227 }
5228 }
5229
5230 $sql = "SELECT rowid";
5231 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5232 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5233 if ($ref_fourn) {
5234 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5235 } else {
5236 $sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
5237 }
5238 $sql .= " AND quantity = ".((float) $quantity);
5239 $sql .= " AND fk_product = ".((int) $this->id);
5240 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5241
5242 $resql = $this->db->query($sql);
5243 if ($resql) {
5244 $obj = $this->db->fetch_object($resql);
5245
5246 // The reference supplier does not exist, we create it for this product.
5247 if (empty($obj)) {
5248 $sql = "INSERT INTO ".$this->db->prefix()."product_fournisseur_price(";
5249 $sql .= "datec";
5250 $sql .= ", entity";
5251 $sql .= ", fk_product";
5252 $sql .= ", fk_soc";
5253 $sql .= ", ref_fourn";
5254 $sql .= ", quantity";
5255 $sql .= ", fk_user";
5256 $sql .= ", tva_tx";
5257 $sql .= ") VALUES (";
5258 $sql .= "'".$this->db->idate($now)."'";
5259 $sql .= ", ".((int) $conf->entity);
5260 $sql .= ", ".((int) $this->id);
5261 $sql .= ", ".((int) $id_fourn);
5262 $sql .= ", '".$this->db->escape($ref_fourn)."'";
5263 $sql .= ", ".((float) $quantity);
5264 $sql .= ", ".((int) $user->id);
5265 $sql .= ", 0";
5266 $sql .= ")";
5267
5268 if ($this->db->query($sql)) {
5269 $this->product_fourn_price_id = $this->db->last_insert_id($this->db->prefix()."product_fournisseur_price");
5270 return 1;
5271 } else {
5272 $this->error = $this->db->lasterror();
5273 return -1;
5274 }
5275 } else {
5276 // If the supplier price already exists for this product and quantity
5277 $this->product_fourn_price_id = $obj->rowid;
5278 return 0;
5279 }
5280 } else {
5281 $this->error = $this->db->lasterror();
5282 return -2;
5283 }
5284 }
5285
5286
5287 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5293 public function list_suppliers()
5294 {
5295 // phpcs:enable
5296 global $conf;
5297
5298 $list = array();
5299
5300 $sql = "SELECT DISTINCT p.fk_soc";
5301 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as p";
5302 $sql .= " WHERE p.fk_product = ".((int) $this->id);
5303 $sql .= " AND p.entity = ".((int) $conf->entity);
5304
5305 $result = $this->db->query($sql);
5306 if ($result) {
5307 $num = $this->db->num_rows($result);
5308 $i = 0;
5309 while ($i < $num) {
5310 $obj = $this->db->fetch_object($result);
5311 $list[$i] = $obj->fk_soc;
5312 $i++;
5313 }
5314 }
5315
5316 return $list;
5317 }
5318
5319 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5327 public function clone_price($fromId, $toId)
5328 {
5329 // phpcs:enable
5330 global $user;
5331
5332 $now = dol_now();
5333
5334 $this->db->begin();
5335
5336 // prices
5337 $sql = "INSERT INTO ".$this->db->prefix()."product_price (";
5338 $sql .= " entity";
5339 $sql .= ", fk_product";
5340 $sql .= ", date_price";
5341 $sql .= ", price_level";
5342 $sql .= ", price";
5343 $sql .= ", price_ttc";
5344 $sql .= ", price_min";
5345 $sql .= ", price_min_ttc";
5346 $sql .= ", price_base_type";
5347 $sql .= ", price_label";
5348 $sql .= ", default_vat_code";
5349 $sql .= ", tva_tx";
5350 $sql .= ", recuperableonly";
5351 $sql .= ", localtax1_tx";
5352 $sql .= ", localtax1_type";
5353 $sql .= ", localtax2_tx";
5354 $sql .= ", localtax2_type";
5355 $sql .= ", fk_user_author";
5356 $sql .= ", tosell";
5357 $sql .= ", price_by_qty";
5358 $sql .= ", fk_price_expression";
5359 $sql .= ", fk_multicurrency";
5360 $sql .= ", multicurrency_code";
5361 $sql .= ", multicurrency_tx";
5362 $sql .= ", multicurrency_price";
5363 $sql .= ", multicurrency_price_ttc";
5364 $sql .= ")";
5365 $sql .= " SELECT";
5366 $sql .= " entity";
5367 $sql .= ", ".((int) $toId);
5368 $sql .= ", '".$this->db->idate($now)."'";
5369 $sql .= ", price_level";
5370 $sql .= ", price";
5371 $sql .= ", price_ttc";
5372 $sql .= ", price_min";
5373 $sql .= ", price_min_ttc";
5374 $sql .= ", price_base_type";
5375 $sql .= ", price_label";
5376 $sql .= ", default_vat_code";
5377 $sql .= ", tva_tx";
5378 $sql .= ", recuperableonly";
5379 $sql .= ", localtax1_tx";
5380 $sql .= ", localtax1_type";
5381 $sql .= ", localtax2_tx";
5382 $sql .= ", localtax2_type";
5383 $sql .= ", ".((int) $user->id);
5384 $sql .= ", tosell";
5385 $sql .= ", price_by_qty";
5386 $sql .= ", fk_price_expression";
5387 $sql .= ", fk_multicurrency";
5388 $sql .= ", multicurrency_code";
5389 $sql .= ", multicurrency_tx";
5390 $sql .= ", multicurrency_price";
5391 $sql .= ", multicurrency_price_ttc";
5392 $sql .= " FROM ".$this->db->prefix()."product_price ps";
5393 $sql .= " WHERE fk_product = ".((int) $fromId);
5394 $sql .= " AND date_price IN (SELECT MAX(pd.date_price) FROM ".$this->db->prefix()."product_price pd WHERE pd.fk_product = ".((int) $fromId)." AND pd.price_level = ps.price_level)";
5395 $sql .= " ORDER BY date_price DESC";
5396
5397 dol_syslog(__METHOD__, LOG_DEBUG);
5398 $resql = $this->db->query($sql);
5399 if (!$resql) {
5400 $this->db->rollback();
5401 return -1;
5402 }
5403
5404 $this->db->commit();
5405 return 1;
5406 }
5407
5408 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5416 public function clone_associations($fromId, $toId)
5417 {
5418 // phpcs:enable
5419 $this->db->begin();
5420
5421 $sql = 'INSERT INTO '.$this->db->prefix().'product_association (fk_product_pere, fk_product_fils, qty, incdec)';
5422 $sql .= " SELECT ".((int) $toId).", fk_product_fils, qty, incdec FROM ".$this->db->prefix()."product_association";
5423 $sql .= " WHERE fk_product_pere = ".((int) $fromId);
5424
5425 dol_syslog(get_class($this).'::clone_association', LOG_DEBUG);
5426 if (!$this->db->query($sql)) {
5427 $this->db->rollback();
5428 return -1;
5429 }
5430
5431 $this->db->commit();
5432 return 1;
5433 }
5434
5435 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5443 public function clone_fournisseurs($fromId, $toId)
5444 {
5445 // phpcs:enable
5446 $this->db->begin();
5447
5448 $now = dol_now();
5449
5450 // les fournisseurs
5451 /*$sql = "INSERT ".$this->db->prefix()."product_fournisseur ("
5452 . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
5453 . " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, ref_fourn, fk_user_author"
5454 . " FROM ".$this->db->prefix()."product_fournisseur"
5455 . " WHERE fk_product = ".((int) $fromId);
5456
5457 if ( ! $this->db->query($sql ) )
5458 {
5459 $this->db->rollback();
5460 return -1;
5461 }*/
5462
5463 // les prix de fournisseurs.
5464 $sql = "INSERT ".$this->db->prefix()."product_fournisseur_price (";
5465 $sql .= " datec, fk_product, fk_soc, price, quantity, fk_user, tva_tx)";
5466 $sql .= " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, price, quantity, fk_user, tva_tx";
5467 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5468 $sql .= " WHERE fk_product = ".((int) $fromId);
5469
5470 dol_syslog(get_class($this).'::clone_fournisseurs', LOG_DEBUG);
5471 $resql = $this->db->query($sql);
5472 if (!$resql) {
5473 $this->db->rollback();
5474 return -1;
5475 } else {
5476 $this->db->commit();
5477 return 1;
5478 }
5479 }
5480
5481 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5494 public function fetch_prod_arbo($prod, $compl_path = '', $multiply = 1, $level = 1, $id_parent = 0, $ignore_stock_load = 0)
5495 {
5496 // phpcs:enable
5497 $tmpproduct = null;
5498
5499 if ($multiply < 1) {
5500 dol_syslog(get_class($this).'::'.__FUNCTION__.' Product quantity rounded up to 1 from '.$multiply.'. May result in unexpected end quantity for product children of '.$id_parent, LOG_WARNING);
5501 $multiply = 1;
5502 }
5503
5504 //var_dump($prod);
5505 foreach ($prod as $id_product => $desc_pere) { // $id_product is 0 (first call starting with root top) or an id of a sub_product
5506 if (is_array($desc_pere)) { // If desc_pere is an array, this means it's a child
5507 $id = (!empty($desc_pere[0]) ? $desc_pere[0] : '');
5508 $nb = (!empty($desc_pere[1]) ? $desc_pere[1] : '');
5509 $type = (!empty($desc_pere[2]) ? $desc_pere[2] : '');
5510 $label = (!empty($desc_pere[3]) ? $desc_pere[3] : '');
5511 $incdec = (!empty($desc_pere[4]) ? $desc_pere[4] : 0);
5512
5513 //print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n";
5514 if (is_null($tmpproduct)) {
5515 $tmpproduct = new Product($this->db); // So we initialize tmpproduct only once for all loop.
5516 }
5517 $tmpproduct->fetch($id); // Load product to get ->ref
5518
5519 if (empty($ignore_stock_load) && ($tmpproduct->isProduct() || getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) {
5520 $tmpproduct->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
5521 }
5522
5523 $this->res[] = array(
5524 'id' => $id, // Id product
5525 'id_parent' => $id_parent,
5526 'ref' => $tmpproduct->ref, // Ref product
5527 'nb' => $nb, // Nb of units that compose parent product
5528 'nb_total' => $nb * $multiply, // Nb of units for all nb of product
5529 'stock' => $tmpproduct->stock_reel, // Stock
5530 'stock_alert' => $tmpproduct->seuil_stock_alerte, // Stock alert
5531 'label' => $label,
5532 'fullpath' => $compl_path.$label, // Label
5533 'type' => $type, // Nb of units that compose parent product
5534 'desiredstock' => $tmpproduct->desiredstock,
5535 'level' => $level,
5536 'incdec' => $incdec,
5537 'entity' => $tmpproduct->entity
5538 );
5539
5540 // Recursive call if there child has children of its own
5541 if (isset($desc_pere['childs']) && is_array($desc_pere['childs'])) {
5542 if (!is_int($desc_pere[1] * $multiply)) {
5543 dol_syslog(get_class($this).'::'.__FUNCTION__.' Source product quantity and multiplier may result in unexpected end quantity for '.$label.'/'.$multiply.' Child product:'.json_encode($desc_pere), LOG_WARNING);
5544 }
5545
5546 //print 'YYY We go down for '.$desc_pere[3]." -> \n";
5547 $this->fetch_prod_arbo($desc_pere['childs'], $compl_path.$desc_pere[3]." -> ", (int) ceil($desc_pere[1] * $multiply), $level + 1, $id, $ignore_stock_load);
5548 }
5549 }
5550 }
5551 }
5552
5553 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5562 public function get_arbo_each_prod($multiply = 1, $ignore_stock_load = 0)
5563 {
5564 // phpcs:enable
5565 $this->res = array();
5566 if (isset($this->sousprods) && is_array($this->sousprods)) {
5567 foreach ($this->sousprods as $prod_name => $desc_product) {
5568 if (is_array($desc_product)) {
5569 $this->fetch_prod_arbo($desc_product, "", $multiply, 1, $this->id, $ignore_stock_load); // This set $this->res
5570 }
5571 }
5572 }
5573 //var_dump($res);
5574 return $this->res;
5575 }
5576
5584 public function hasFatherOrChild($mode = 0)
5585 {
5586 $nb = 0;
5587
5588 $sql = "SELECT COUNT(pa.rowid) as nb";
5589 $sql .= " FROM ".$this->db->prefix()."product_association as pa";
5590 if ($mode == 0) {
5591 $sql .= " WHERE pa.fk_product_fils = ".((int) $this->id)." OR pa.fk_product_pere = ".((int) $this->id);
5592 } elseif ($mode == -1) {
5593 $sql .= " WHERE pa.fk_product_fils = ".((int) $this->id); // We are a child, so we found lines that link to parents (can have several parents)
5594 } elseif ($mode == 1) {
5595 $sql .= " WHERE pa.fk_product_pere = ".((int) $this->id); // We are a parent, so we found lines that link to children (can have several children)
5596 }
5597
5598 $resql = $this->db->query($sql);
5599 if ($resql) {
5600 $obj = $this->db->fetch_object($resql);
5601 if ($obj) {
5602 $nb = (int) $obj->nb;
5603 }
5604 } else {
5605 return -1;
5606 }
5607
5608 return $nb;
5609 }
5610
5616 public function hasVariants()
5617 {
5618 $nb = 0;
5619 if (!isModEnabled('variants')) {
5620 return $nb;
5621 }
5622 $sql = "SELECT count(rowid) as nb FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_parent = ".((int) $this->id);
5623 $sql .= " AND entity IN (".getEntity('product').")";
5624
5625 $resql = $this->db->query($sql);
5626 if ($resql) {
5627 $obj = $this->db->fetch_object($resql);
5628 if ($obj) {
5629 $nb = (int) $obj->nb;
5630 }
5631 }
5632
5633 return $nb;
5634 }
5635
5636
5642 public function isVariant()
5643 {
5644 if (isModEnabled('variants')) {
5645 $sql = "SELECT rowid FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_child = ".((int) $this->id)." AND entity IN (".getEntity('product').")";
5646
5647 $query = $this->db->query($sql);
5648
5649 if ($query) {
5650 if (!$this->db->num_rows($query)) {
5651 return false;
5652 }
5653 return true;
5654 } else {
5655 dol_print_error($this->db);
5656 return -1;
5657 }
5658 } else {
5659 return false;
5660 }
5661 }
5662
5669 public function getFather()
5670 {
5671 $sql = "SELECT p.rowid, p.label as label, p.ref as ref, pa.fk_product_pere as id, p.fk_product_type, pa.qty, pa.incdec, p.entity";
5672 $sql .= ", p.tosell as status, p.tobuy as status_buy";
5673 $sql .= " FROM ".$this->db->prefix()."product_association as pa,";
5674 $sql .= " ".$this->db->prefix()."product as p";
5675 $sql .= " WHERE p.rowid = pa.fk_product_pere";
5676 $sql .= " AND pa.fk_product_fils = ".((int) $this->id);
5677
5678 $res = $this->db->query($sql);
5679 if ($res) {
5680 $prods = array();
5681 while ($record = $this->db->fetch_array($res)) {
5682 // $record['id'] = $record['rowid'] = id of father
5683 $prods[$record['id']] = array();
5684 $prods[$record['id']]['id'] = $record['rowid'];
5685 $prods[$record['id']]['ref'] = $record['ref'];
5686 $prods[$record['id']]['label'] = $record['label'];
5687 $prods[$record['id']]['qty'] = $record['qty'];
5688 $prods[$record['id']]['incdec'] = $record['incdec'];
5689 $prods[$record['id']]['fk_product_type'] = $record['fk_product_type'];
5690 $prods[$record['id']]['entity'] = $record['entity'];
5691 $prods[$record['id']]['status'] = $record['status'];
5692 $prods[$record['id']]['status_buy'] = $record['status_buy'];
5693 }
5694 return $prods;
5695 } else {
5696 dol_print_error($this->db);
5697 return -1;
5698 }
5699 }
5700
5701
5711 public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array())
5712 {
5713 if (empty($id)) {
5714 return array();
5715 }
5716
5717 $sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,";
5718 $sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec,";
5719 $sql .= " pa.rowid as fk_association, pa.rang";
5720 $sql .= " FROM ".$this->db->prefix()."product as p,";
5721 $sql .= " ".$this->db->prefix()."product_association as pa";
5722 $sql .= " WHERE p.rowid = pa.fk_product_fils";
5723 $sql .= " AND pa.fk_product_pere = ".((int) $id);
5724 $sql .= " AND pa.fk_product_fils <> ".((int) $id); // This should not happens, it is to avoid infinite loop if it happens
5725 $sql .= " ORDER BY pa.rang";
5726
5727 dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level. ' parents='.(is_array($parents) ? implode(',', $parents) : $parents), LOG_DEBUG);
5728
5729 // Protection against infinite loop
5730 if ($level > 30) {
5731 return array();
5732 }
5733
5734 $res = $this->db->query($sql);
5735 if ($res) {
5736 $prods = array();
5737 if ($this->db->num_rows($res) > 0) {
5738 $parents[] = $id;
5739 }
5740
5741 while ($rec = $this->db->fetch_array($res)) {
5742 if (in_array($rec['id'], $parents)) {
5743 dol_syslog(get_class($this).'::getChildsArbo the product id='.$rec['rowid'].' was already found at a higher level in tree. We discard to avoid infinite loop', LOG_WARNING);
5744 continue; // We discard this child if it is already found at a higher level in tree in the same branch.
5745 }
5746
5747 $prods[$rec['rowid']] = array(
5748 0 => $rec['rowid'],
5749 1 => $rec['qty'],
5750 2 => $rec['fk_product_type'],
5751 3 => $this->db->escape($rec['label']),
5752 4 => $rec['incdec'],
5753 5 => $rec['ref'],
5754 6 => $rec['fk_association'],
5755 7 => $rec['rang']
5756 );
5757 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
5758 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
5759 if (empty($firstlevelonly)) {
5760 $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, $parents);
5761 foreach ($listofchilds as $keyChild => $valueChild) {
5762 $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
5763 }
5764 }
5765 }
5766
5767 return $prods;
5768 } else {
5769 dol_print_error($this->db);
5770 return -1;
5771 }
5772 }
5773
5774 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5781 public function get_sousproduits_arbo()
5782 {
5783 // phpcs:enable
5784 $parent = array();
5785
5786 foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) { // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
5787 $parent[$this->label][$keyChild] = $valueChild;
5788 }
5789 foreach ($parent as $key => $value) { // key=label, value is array of children
5790 $this->sousprods[$key] = $value; // @phan-suppress-current-line PhanTypeMismatchProperty
5791 }
5792 }
5793
5801 public function getTooltipContentArray($params)
5802 {
5803 global $conf, $langs, $user;
5804
5805 $langs->loadLangs(array('products', 'other'));
5806
5807 $datas = array();
5808 $nofetch = !empty($params['nofetch']);
5809
5810 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5811 return ['optimize' => $langs->trans("ShowProduct")];
5812 }
5813
5814 // Does user has permission to read product/service
5815 $permissiontoreadproduct = 0;
5816 if ($this->type == self::TYPE_PRODUCT && $user->hasRight('product', 'read')) {
5817 $permissiontoreadproduct = 1;
5818 }
5819 if ($this->type == self::TYPE_SERVICE && $user->hasRight('service', 'read')) {
5820 $permissiontoreadproduct = 1;
5821 }
5822
5823 if (!empty($this->entity) && $permissiontoreadproduct) {
5824 $tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80, 0, 0, 0, 0, '1');
5825 if ($this->nbphoto > 0) {
5826 $datas['photo'] = '<div class="photointooltip floatright">'."\n" . $tmpphoto . '</div>';
5827 }
5828 }
5829
5830 if ($this->isProduct()) {
5831 $datas['picto'] = img_picto('', 'product').' <u class="paddingrightonly">'.$langs->trans("Product").'</u>';
5832 } elseif ($this->isService()) {
5833 $datas['picto'] = img_picto('', 'service').' <u class="paddingrightonly">'.$langs->trans("Service").'</u>';
5834 }
5835 if (isset($this->status) && isset($this->status_buy)) {
5836 $datas['status'] = ' '.$this->getLibStatut(5, 0) . ' '.$this->getLibStatut(5, 1);
5837 }
5838
5839 if (!empty($this->ref)) {
5840 $datas['ref'] = '<br><b>'.$langs->trans('ProductRef').':</b> '.$this->ref;
5841 }
5842 if (!empty($this->label)) {
5843 $datas['label'] = '<br><b>'.$langs->trans('ProductLabel').':</b> '.$this->label.'<br>';
5844 }
5845
5846 if ($permissiontoreadproduct) {
5847 if (!empty($this->description) && getDolGlobalString('PRODUCT_SHOW_DESCRIPTION_IN_TOOLTIP')) {
5848 $datas['description'] = '<br><b>'.$langs->trans('ProductDescription').':</b> '.dolGetFirstLineOfText($this->description, 5);
5849 }
5850
5851 $datas['stockmanaged'] = "<br><b>".$langs->trans("StockableProduct").'</b>: '.yn($this->isStockManaged());
5852
5853 if ($this->isStockManaged()) {
5854 if (isModEnabled('productbatch')) {
5855 $langs->load("productbatch");
5856 $datas['batchstatus'] = "<br><b>".$langs->trans("ManageLotSerial").'</b>: '.$this->getLibStatut(0, 2);
5857 if ($this->status_batch) {
5858 $datas['batchdlc'] = "<br><b>".$langs->trans("BatchSellOrEatByMandatoryList", $langs->transnoentitiesnoconv("SellByDate"), $langs->transnoentitiesnoconv("EatByDate")).'</b>: '.$this->getSellOrEatByMandatoryLabel();
5859 }
5860 }
5861 }
5862 if (isModEnabled('barcode')) {
5863 $datas['barcode'] = '<br><b>'.$langs->trans('BarCode').':</b> '.$this->barcode;
5864 }
5865
5866 if ($this->isProduct()) {
5867 if ($this->weight) {
5868 $datas['weight'] = "<br><b>".$langs->trans("Weight").'</b>: '.$this->weight.' '.measuringUnitString(0, "weight", $this->weight_units);
5869 }
5870 $labelsize = "";
5871 if ($this->length) {
5872 $labelsize .= "<b>".$langs->trans("Length").'</b>: '.$this->length.' '.measuringUnitString(0, 'size', $this->length_units);
5873 }
5874 if ($this->width) {
5875 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Width").'</b>: '.$this->width.' '.measuringUnitString(0, 'size', $this->width_units);
5876 }
5877 if ($this->height) {
5878 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Height").'</b>: '.$this->height.' '.measuringUnitString(0, 'size', $this->height_units);
5879 }
5880 if ($labelsize) {
5881 $datas['size'] = "<br>".$labelsize;
5882 }
5883
5884 $labelsurfacevolume = "";
5885 if ($this->surface) {
5886 $labelsurfacevolume .= "<b>".$langs->trans("Surface").'</b>: '.$this->surface.' '.measuringUnitString(0, 'surface', $this->surface_units);
5887 }
5888 if ($this->volume) {
5889 $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Volume").'</b>: '.$this->volume.' '.measuringUnitString(0, 'volume', $this->volume_units);
5890 }
5891 if ($labelsurfacevolume) {
5892 $datas['surface'] = "<br>" . $labelsurfacevolume;
5893 }
5894 }
5895 if ($this->isService() && !empty($this->duration_value)) {
5896 // Duration
5897 $datas['duration'] = '<br><b>'.$langs->trans("Duration").':</b> '.$this->duration_value;
5898 if ($this->duration_value > 1) {
5899 $dur = array("i" => $langs->trans("Minutes"), "h" => $langs->trans("Hours"), "d" => $langs->trans("Days"), "w" => $langs->trans("Weeks"), "m" => $langs->trans("Months"), "y" => $langs->trans("Years"));
5900 } elseif ($this->duration_value > 0) {
5901 $dur = array("i" => $langs->trans("Minute"), "h" => $langs->trans("Hour"), "d" => $langs->trans("Day"), "w" => $langs->trans("Week"), "m" => $langs->trans("Month"), "y" => $langs->trans("Year"));
5902 }
5903 $datas['duration'] .= (!empty($this->duration_unit) && isset($dur[$this->duration_unit]) ? "&nbsp;".$langs->trans($dur[$this->duration_unit]) : '');
5904 }
5905 if (empty($user->socid)) {
5906 if ($this->isStockManaged() && isset($this->pmp) && $this->pmp) {
5907 $datas['pmp'] = "<br><b>".$langs->trans("PMPValue").'</b>: '.price($this->pmp, 0, '', 1, -1, -1, $conf->currency);
5908 }
5909
5910 if (isModEnabled('accounting')) {
5911 if ($this->status && isset($this->accountancy_code_sell)) {
5912 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5913 $selllabel = '<br>';
5914 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellCode').':</b> '.length_accountg($this->accountancy_code_sell);
5915 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellIntraCode').':</b> '.length_accountg($this->accountancy_code_sell_intra);
5916 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellExportCode').':</b> '.length_accountg($this->accountancy_code_sell_export);
5917 $datas['accountancysell'] = $selllabel;
5918 }
5919 if ($this->status_buy && isset($this->accountancy_code_buy)) {
5920 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5921 $buylabel = '';
5922 if (empty($this->status)) {
5923 $buylabel .= '<br>';
5924 }
5925 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyCode').':</b> '.length_accountg($this->accountancy_code_buy);
5926 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyIntraCode').':</b> '.length_accountg($this->accountancy_code_buy_intra);
5927 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyExportCode').':</b> '.length_accountg($this->accountancy_code_buy_export);
5928 $datas['accountancybuy'] = $buylabel;
5929 }
5930 }
5931 }
5932 // show categories for this record only in ajax to not overload lists
5933 if (isModEnabled('category') && !$nofetch) {
5934 require_once DOL_DOCUMENT_ROOT . '/categories/class/categorie.class.php';
5935 $form = new Form($this->db);
5936 $datas['categories'] = '<br>' . $form->showCategories($this->id, Categorie::TYPE_PRODUCT, 1);
5937 }
5938 }
5939
5940 return $datas;
5941 }
5942
5956 public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0, $morecss = '', $add_label = 0, $sep = ' - ')
5957 {
5958 global $langs, $hookmanager;
5959
5960 include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
5961
5962 $result = '';
5963
5964 $newref = (string) $this->ref;
5965 if ($maxlength) {
5966 $newref = dol_trunc($newref, $maxlength, 'middle');
5967 }
5968 $params = [
5969 'id' => $this->id,
5970 'objecttype' => ($this->type == 1 ? 'service' : 'product'),
5971 'option' => $option,
5972 'nofetch' => 1,
5973 ];
5974 $classfortooltip = 'classfortooltip';
5975 $dataparams = '';
5976 if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
5977 $classfortooltip = 'classforajaxtooltip';
5978 $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
5979 $label = '';
5980 } else {
5981 $label = implode($this->getTooltipContentArray($params));
5982 }
5983
5984 $linkclose = '';
5985 if (empty($notooltip)) {
5986 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5987 $label = $langs->trans("ShowProduct");
5988 $linkclose .= ' alt="'.dol_escape_htmltag($label, 1, 1).'"';
5989 }
5990 $linkclose .= ($label ? ' title="'.dol_escape_htmltag($label, 1, 1).'"' : ' title="tocomplete"');
5991 $linkclose .= $dataparams.' class="nowraponall '.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
5992 } else {
5993 $linkclose = ' class="nowraponall'.($morecss ? ' '.$morecss : '').'"';
5994 }
5995
5996 if ($option == 'supplier' || $option == 'category') {
5997 $url = DOL_URL_ROOT.'/product/price_suppliers.php?id='.$this->id;
5998 } elseif ($option == 'stock') {
5999 $url = DOL_URL_ROOT.'/product/stock/product.php?id='.$this->id;
6000 } elseif ($option == 'composition') {
6001 $url = DOL_URL_ROOT.'/product/composition/card.php?id='.$this->id;
6002 } else {
6003 $url = DOL_URL_ROOT.'/product/card.php?id='.$this->id;
6004 }
6005
6006 if ($option !== 'nolink') {
6007 // Add param to save lastsearch_values or not
6008 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
6009 if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
6010 $add_save_lastsearch_values = 1;
6011 }
6012 if ($add_save_lastsearch_values) {
6013 $url .= '&save_lastsearch_values=1';
6014 }
6015 }
6016
6017 $linkstart = '<a href="'.$url.'"';
6018 $linkstart .= $linkclose.'>';
6019 $linkend = '</a>';
6020
6021 $result .= $linkstart;
6022 if ($withpicto) {
6023 if ($this->isProduct()) {
6024 $result .= (img_object(($notooltip ? '' : $label), 'product', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
6025 }
6026 if ($this->isService()) {
6027 $result .= (img_object(($notooltip ? '' : $label), 'service', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
6028 }
6029 }
6030 $result .= '<span class="aaa">'.dol_escape_htmltag($newref).'</span>';
6031 $result .= $linkend;
6032 if ($withpicto != 2) {
6033 $result .= (($add_label && $this->label) ? $sep.dol_trunc($this->label, ($add_label > 1 ? $add_label : 0)) : '');
6034 }
6035
6036 global $action;
6037 $hookmanager->initHooks(array('productdao'));
6038 $parameters = array('id' => $this->id, 'getnomurl' => &$result, 'label' => &$label);
6039 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
6040 if ($reshook > 0) {
6041 $result = $hookmanager->resPrint;
6042 } else {
6043 $result .= $hookmanager->resPrint;
6044 }
6045
6046 return $result;
6047 }
6048
6049
6060 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
6061 {
6062 global $langs;
6063
6064 $langs->load("products");
6065 $outputlangs->load("products");
6066
6067 // Positionne le modele sur le nom du modele a utiliser
6068 if (!dol_strlen($modele)) {
6069 $modele = getDolGlobalString('PRODUCT_ADDON_PDF', 'strato');
6070 }
6071
6072 $modelpath = "core/modules/product/doc/";
6073
6074 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
6075 }
6076
6084 public function getLibStatut($mode = 0, $type = 0)
6085 {
6086 switch ($type) {
6087 case 0:
6088 return $this->LibStatut($this->status, $mode, $type);
6089 case 1:
6090 return $this->LibStatut($this->status_buy, $mode, $type);
6091 case 2:
6092 return $this->LibStatut($this->status_batch, $mode, $type);
6093 default:
6094 //Simulate previous behavior but should return an error string
6095 return $this->LibStatut($this->status_buy, $mode, $type);
6096 }
6097 }
6098
6099 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6108 public function LibStatut($status, $mode = 0, $type = 0)
6109 {
6110 // phpcs:enable
6111 global $langs;
6112
6113 $labelStatus = $labelStatusShort = '';
6114
6115 $langs->load('products');
6116 if (isModEnabled('productbatch')) {
6117 $langs->load("productbatch");
6118 }
6119
6120 if ($type == 2) {
6121 switch ($mode) {
6122 case 0:
6123 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatch') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial')));
6124 return dolGetStatus($label);
6125 case 1:
6126 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort')));
6127 return dolGetStatus($label);
6128 case 2:
6129 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 1, 2);
6130 case 3:
6131 return dolGetStatus($langs->transnoentitiesnoconv('ProductStatusNotOnBatch'), '', '', empty($status) ? 'status5' : 'status4', 3, 'dot');
6132 case 4:
6133 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 0, 2);
6134 case 5:
6135 return $this->LibStatut($status, 1, 2).' '.$this->LibStatut($status, 3, 2);
6136 default:
6137 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'));
6138 }
6139 }
6140
6141 $statuttrans = empty($status) ? 'status5' : 'status4';
6142
6143 if ($status == 0) {
6144 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6145 if ($type == 0) {
6146 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnSellShort');
6147 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnSell');
6148 } elseif ($type == 1) {
6149 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBuyShort');
6150 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBuy');
6151 } elseif ($type == 2) {
6152 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBatch');
6153 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort');
6154 }
6155 } elseif ($status == 1) {
6156 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6157 if ($type == 0) {
6158 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSellShort');
6159 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSell');
6160 } elseif ($type == 1) {
6161 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBuyShort');
6162 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBuy');
6163 } elseif ($type == 2) {
6164 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBatch');
6165 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBatchShort');
6166 }
6167 } elseif ($type == 2 && $status == 2) {
6168 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSerial');
6169 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSerialShort');
6170 }
6171
6172 if ($mode > 6) {
6173 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'), '', '', 'status0', 0);
6174 } else {
6175 return dolGetStatus($labelStatus, $labelStatusShort, '', $statuttrans, $mode);
6176 }
6177 }
6178
6179
6185 public function getLibFinished()
6186 {
6187 global $langs;
6188
6189 $langs->load('products');
6190 $label = '';
6191
6192 if (isset($this->finished) && $this->finished >= 0) {
6193 $sql = "SELECT label, code FROM ".$this->db->prefix()."c_product_nature where code = ".((int) $this->finished)." AND active=1";
6194 $resql = $this->db->query($sql);
6195 if (!$resql) {
6196 $this->error = $this->db->error().' sql='.$sql;
6197 dol_syslog(__METHOD__.' Error '.$this->error, LOG_ERR);
6198 return -1;
6199 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
6200 $label = $langs->trans($res['label']);
6201 }
6202 $this->db->free($resql);
6203 }
6204
6205 return $label;
6206 }
6207
6208
6209 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6226 public function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null)
6227 {
6228 // phpcs:enable
6229 if ($id_entrepot) {
6230 $this->db->begin();
6231
6232 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
6233
6234 // Ensure $nbpiece is a number and positive
6235 $nbpiece = (float) $nbpiece;
6236 if ($nbpiece < 0) {
6237 if (!$movement) {
6238 $movement = 1;
6239 }
6240 $nbpiece = abs($nbpiece);
6241 }
6242 $op = array();
6243 $op[0] = $nbpiece;
6244 $op[1] = -$nbpiece;
6245
6246 $movementstock = new MouvementStock($this->db);
6247 $movementstock->setOrigin($origin_element, (int) $origin_id); // Set ->origin_type and ->origin_id
6248 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', '', '', '', false, 0, $disablestockchangeforsubproduct);
6249
6250 if ($result >= 0) {
6251 if ($extrafields) {
6252 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
6253 $movementstock->array_options = $array_options;
6254 $movementstock->insertExtraFields();
6255 }
6256 $this->db->commit();
6257 return 1;
6258 } else {
6259 $this->setErrorsFromObject($movementstock);
6260
6261 $this->db->rollback();
6262 return -1;
6263 }
6264 }
6265
6266 return -1;
6267 }
6268
6269 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6290 public function correct_stock_batch($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $dlc = '', $dluo = '', $lot = '', $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null, $force_update_batch = false)
6291 {
6292 // phpcs:enable
6293 if ($id_entrepot) {
6294 $this->db->begin();
6295
6296 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
6297
6298 // Ensure $nbpiece is a number and positive
6299 $nbpiece = (float) $nbpiece;
6300 if ($nbpiece < 0) {
6301 if (!$movement) {
6302 $movement = 1;
6303 }
6304 $nbpiece = abs($nbpiece);
6305 }
6306
6307 $op = array();
6308 $op[0] = $nbpiece;
6309 $op[1] = -$nbpiece;
6310
6311 $movementstock = new MouvementStock($this->db);
6312 $movementstock->setOrigin($origin_element, (int) $origin_id); // Set ->origin_type and ->fk_origin
6313 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', $dlc, $dluo, $lot, false, 0, $disablestockchangeforsubproduct, 0, $force_update_batch);
6314
6315 if ($result >= 0) {
6316 if ($extrafields) {
6317 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
6318 $movementstock->array_options = $array_options;
6319 $movementstock->insertExtraFields();
6320 }
6321 $this->db->commit();
6322 return 1;
6323 } else {
6324 $this->error = $movementstock->error;
6325 $this->errors = $movementstock->errors;
6326
6327 $this->db->rollback();
6328 return -1;
6329 }
6330 }
6331 return -1;
6332 }
6333
6334 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6347 public function load_stock($option = '', $includedraftpoforvirtual = null, $dateofvirtualstock = null)
6348 {
6349 // phpcs:enable
6350 $this->stock_reel = 0;
6351 $this->stock_warehouse = array();
6352 $this->stock_theorique = 0;
6353
6354 // Set filter on warehouse status
6355 $warehouseStatus = array();
6356 if (preg_match('/warehouseclosed/', $option)) {
6358 }
6359 if (preg_match('/warehouseopen/', $option)) {
6361 }
6362 if (preg_match('/warehouseinternal/', $option)) {
6363 if (getDolGlobalString('ENTREPOT_EXTRA_STATUS')) {
6365 } else {
6367 }
6368 }
6369
6370 $sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot";
6371 $sql .= " FROM ".$this->db->prefix()."product_stock as ps";
6372 $sql .= ", ".$this->db->prefix()."entrepot as w";
6373 $sql .= " WHERE w.entity IN (".getEntity('stock').")";
6374 $sql .= " AND w.rowid = ps.fk_entrepot";
6375 $sql .= " AND ps.fk_product = ".((int) $this->id);
6376 if (count($warehouseStatus)) {
6377 $sql .= " AND w.statut IN (".$this->db->sanitize(implode(',', $warehouseStatus)).")";
6378 }
6379
6380 $sql .= " ORDER BY ps.reel ".(getDolGlobalString('DO_NOT_TRY_TO_DEFRAGMENT_STOCKS_WAREHOUSE') ? 'DESC' : 'ASC'); // Note : qty ASC is important for expedition card, to avoid stock fragmentation;
6381
6382 dol_syslog(get_class($this)."::load_stock", LOG_DEBUG);
6383 $result = $this->db->query($sql);
6384 if ($result) {
6385 $num = $this->db->num_rows($result);
6386 $i = 0;
6387 if ($num > 0) {
6388 while ($i < $num) {
6389 $row = $this->db->fetch_object($result);
6390 $this->stock_warehouse[$row->fk_entrepot] = new stdClass();
6391 $this->stock_warehouse[$row->fk_entrepot]->real = $row->reel;
6392 $this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid;
6393 if ((!preg_match('/nobatch/', $option)) && $this->hasbatch()) {
6394 $this->stock_warehouse[$row->fk_entrepot]->detail_batch = Productbatch::findAll($this->db, $row->rowid, 1, $this->id);
6395 }
6396 $this->stock_reel += $row->reel;
6397 $i++;
6398 }
6399 $this->stock_reel = (float) price2num($this->stock_reel, 'MS');
6400 }
6401 $this->db->free($result);
6402
6403 if (!preg_match('/novirtual/', $option)) {
6404 $this->load_virtual_stock($includedraftpoforvirtual, $dateofvirtualstock); // This load stock_theorique and also load all arrays stats_xxx...
6405 }
6406
6407 return 1;
6408 } else {
6409 $this->error = $this->db->lasterror();
6410 return -1;
6411 }
6412 }
6413
6414
6415 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6425 public function load_virtual_stock($includedraftpoforvirtual = null, $dateofvirtualstock = null)
6426 {
6427 // phpcs:enable
6428 global $hookmanager, $action;
6429
6430 $stock_commande_client = 0;
6431 $stock_commande_fournisseur = 0;
6432 $stock_sending_client = 0;
6433 $stock_reception_fournisseur = 0;
6434 $stock_inproduction = 0;
6435
6436 //dol_syslog("load_virtual_stock");
6437
6438 if (isModEnabled('order')) {
6439 $result = $this->load_stats_commande(0, '1,2', 1);
6440 if ($result < 0) {
6441 dol_print_error($this->db, $this->error);
6442 }
6443 $stock_commande_client = $this->stats_commande['qty'];
6444 }
6445 if (isModEnabled("shipping")) {
6446 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
6447 $filterShipmentStatus = '';
6448 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
6449 $filterShipmentStatus = Expedition::STATUS_VALIDATED.','.Expedition::STATUS_CLOSED;
6450 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6451 $filterShipmentStatus = Expedition::STATUS_CLOSED;
6452 }
6453 $result = $this->load_stats_sending(0, '1,2', 1, $filterShipmentStatus);
6454 if ($result < 0) {
6455 dol_print_error($this->db, $this->error);
6456 }
6457 $stock_sending_client = $this->stats_expedition['qty'];
6458 }
6459 // Include supplier order lines
6460 if (isModEnabled("supplier_order")) {
6461 $filterStatus = getDolGlobalString('SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK', '3,4');
6462 if (isset($includedraftpoforvirtual)) {
6463 $filterStatus = '0,1,2,'.$filterStatus; // 1,2 may have already been inside $filterStatus but it is better to have twice than missing $filterStatus does not include them
6464 }
6465 $result = $this->load_stats_commande_fournisseur(0, $filterStatus, 1, $dateofvirtualstock);
6466 if ($result < 0) {
6467 dol_print_error($this->db, $this->error);
6468 }
6469 $stock_commande_fournisseur = $this->stats_commande_fournisseur['qty'];
6470 }
6471 // Include reception lines
6472 if (isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) {
6473 $filterStatus = '4';
6474 if (isset($includedraftpoforvirtual)) {
6475 $filterStatus = '0,'.$filterStatus;
6476 }
6477 $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock);
6478 if ($result < 0) {
6479 dol_print_error($this->db, $this->error);
6480 }
6481 $stock_reception_fournisseur = $this->stats_reception['qty'];
6482 }
6483 // Include manufacturing
6484 if (isModEnabled('mrp')) {
6485 $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock);
6486 if ($result < 0) {
6487 dol_print_error($this->db, $this->error);
6488 }
6489 $stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty'];
6490 }
6491
6492 $this->stock_theorique = $this->stock_reel + $stock_inproduction;
6493
6494 // $weBillOrderOrShipmentReception is set to 'order' or 'shipmentreception'. it will be used to know how to make virtual stock
6495 // calculation when we have a stock increase or decrease on billing. Do we have to count orders to bill or shipment/reception to bill ?
6496 $weBillOrderOrShipmentReception = getDolGlobalString('STOCK_DO_WE_BILL_ORDER_OR_SHIPMENTECEPTION_FOR_VIRTUALSTOCK', 'order');
6497
6498 // Stock decrease mode
6499 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') || getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6500 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6501 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_VALIDATE_ORDER')) {
6502 if (getDolGlobalString('STOCK_CALCULATE_ON_VALIDATE_ORDER_INCLUDE_DRAFT')) { // By default, draft means "does not exist", so we do not include them by default, except if option is on
6503 $tmpnewprod = dol_clone($this, 1);
6504 $result = $tmpnewprod->load_stats_commande(0, '0', 1); // Get qty in draft orders
6505 $this->stock_theorique += $tmpnewprod->stats_commande['qty'];
6506 }
6507 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'order') {
6508 $this->stock_theorique -= $stock_commande_client;
6509 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6510 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6511 }
6512
6513 // Stock Increase mode
6514 if (getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION') || getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION_CLOSE')) {
6515 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6516 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_DISPATCH_ORDER')) { // This option is similar to STOCK_CALCULATE_ON_RECEPTION_CLOSE but when module Reception is not enabled
6517 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6518 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER')) { // Warning: stock change "on approval", not on validation !
6519 if (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER_INCLUDE_DRAFT')) { // By default, draft means "does not exist", so we do not include them by default, except if option is on
6520 $tmpnewprod = dol_clone($this, 1);
6521 $result = $tmpnewprod->load_stats_commande_fournisseur(0, '0', 1); // Get qty in draft orders
6522 $this->stock_theorique += $this->stats_commande_fournisseur['qty'];
6523 }
6524 $this->stock_theorique -= $stock_reception_fournisseur;
6525 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'order') {
6526 $this->stock_theorique += $stock_commande_fournisseur;
6527 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6528 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6529 }
6530
6531 $parameters = array('id' => $this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual);
6532 // Note that $action and $object may have been modified by some hooks
6533 $reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action);
6534 if ($reshook > 0) {
6535 $this->stock_theorique = $hookmanager->resArray['stock_theorique'];
6536 } elseif ($reshook == 0 && isset($hookmanager->resArray['stock_stats_hook'])) {
6537 $this->stock_theorique += $hookmanager->resArray['stock_stats_hook'];
6538 }
6539
6540 //Virtual Stock by Warehouse
6541 if (!empty($this->stock_warehouse) && getDolGlobalString('STOCK_ALLOW_VIRTUAL_STOCK_PER_WAREHOUSE')) {
6542 foreach ($this->stock_warehouse as $warehouseid => $stockwarehouse) {
6543 if (isModEnabled('mrp')) {
6544 $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock, $warehouseid);
6545 if ($result < 0) {
6546 dol_print_error($this->db, $this->error);
6547 }
6548 }
6549
6550 if ($this->fk_default_warehouse == $warehouseid) {
6551 $this->stock_warehouse[$warehouseid]->virtual = $this->stock_warehouse[$warehouseid]->real + $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] + $this->stats_commande_fournisseur['qty'] - ($this->stats_commande['qty'] + $this->stats_mrptoconsume['qty']);
6552 } else {
6553 $this->stock_warehouse[$warehouseid]->virtual = $this->stock_warehouse[$warehouseid]->real + $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'];
6554 }
6555 }
6556 }
6557
6558 return 1;
6559 }
6560
6568 public function loadStockForVirtualProduct($option = '', $qtyWish = 1)
6569 {
6570 $this->stock_warehouse = array();
6571 $error = 0;
6572
6573 $this->get_sousproduits_arbo();
6574 $prods_arbo = $this->get_arbo_each_prod($qtyWish, 1);
6575 if (count($prods_arbo) > 0) {
6576 $productCachedList = array();
6577 $stockByComponentList = array();
6578
6579 foreach ($prods_arbo as $componentArr) {
6580 $componentId = $componentArr['id'];
6581 // only component whose manage stock
6582 if ($componentArr['incdec'] == 1) {
6583 if (!isset($productCachedList[$componentId])) {
6584 $componentStatic = new self($this->db);
6585 $componentStatic->fetch($componentId);
6586 // check if it's a sub-kit
6587 $childrenNb = $componentStatic->hasFatherOrChild(1);
6588 if ($childrenNb == 0) {
6589 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6590 if (!isset($stockByComponentList[$componentId])) {
6591 $stockByComponentList[$componentId] = array(
6592 'qty_need' => 0
6593 );
6594 }
6595 $stockByComponentList[$componentId]['qty_need'] += $componentArr['nb_total'];
6596 }
6597 $productCachedList[$componentId] = $componentStatic;
6598 }
6599 }
6600 }
6601
6602 if (!empty($stockByComponentList)) {
6603 foreach ($stockByComponentList as $componentId => $stockByComponentArr) {
6604 if (!isset($productCachedList[$componentId])) {
6605 $componentStatic = new self($this->db);
6606 $componentStatic->fetch($componentId);
6607 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6608 $productCachedList[$componentId] = $componentStatic;
6609 }
6610 $component = $productCachedList[$componentId];
6611
6612
6613 if ($component->stock_reel < $stockByComponentArr['qty_need']) {
6614 // not enough stock for this component to assemble this virtual product
6615 $error++;
6616 $this->error = 'Not enough component [id='.$componentId.'] in stock, real='.$component->stock_reel.' and need='.$stockByComponentArr['qty_need'];
6617 $this->errors[] = $this->error;
6618 dol_syslog(__METHOD__.' : '.$this->error, LOG_ERR);
6619 } else {
6620 if (!empty($component->stock_warehouse)) {
6621 foreach ($component->stock_warehouse as $warehouseId => $warehouseObj) {
6622 $kitWarehouseAvailable = new stdClass();
6623 $kitWarehouseAvailable->id = $warehouseObj->id;
6624 $kitWarehouseAvailable->real = $qtyWish;
6625 $this->stock_warehouse[$warehouseId] = $kitWarehouseAvailable;
6626 }
6627 }
6628 }
6629
6630 if ($error) {
6631 break;
6632 }
6633 }
6634 } elseif (getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE') && empty($productCachedList)) {
6635 // if all sub product are not stock managed when use parent stock
6636 $this->load_stock('warehouseopen');
6637 }
6638 }
6639
6640 if ($error) {
6641 return -1;
6642 } else {
6643 return 1;
6644 }
6645 }
6646
6654 public function loadBatchInfo($batch)
6655 {
6656 $result = array();
6657
6658 $sql = "SELECT pb.batch, pb.eatby, pb.sellby, SUM(pb.qty) AS qty FROM ".$this->db->prefix()."product_batch as pb, ".$this->db->prefix()."product_stock as ps";
6659 $sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = ".((int) $this->id)." AND pb.batch = '".$this->db->escape($batch)."'";
6660 $sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby";
6661 dol_syslog(get_class($this)."::loadBatchInfo load first entry found for lot/serial = ".$batch, LOG_DEBUG);
6662 $resql = $this->db->query($sql);
6663 if ($resql) {
6664 $num = $this->db->num_rows($resql);
6665 $i = 0;
6666 while ($i < $num) {
6667 $obj = $this->db->fetch_object($resql);
6668 $result[] = array('batch' => $batch, 'eatby' => $this->db->jdate($obj->eatby), 'sellby' => $this->db->jdate($obj->sellby), 'qty' => $obj->qty);
6669 $i++;
6670 }
6671 return $result;
6672 } else {
6673 dol_print_error($this->db);
6674 $this->db->rollback();
6675 return array();
6676 }
6677 }
6678
6679 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6688 public function add_photo($sdir, $file)
6689 {
6690 // phpcs:enable
6691 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6692
6693 $result = 0;
6694
6695 $dir = $sdir;
6696 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6697 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos";
6698 } else {
6699 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product').dol_sanitizeFileName($this->ref);
6700 }
6701
6702 dol_mkdir($dir);
6703
6704 $dir_osencoded = $dir;
6705
6706 if (is_dir($dir_osencoded)) {
6707 $originImage = $dir.'/'.$file['name'];
6708
6709 // Cree fichier en taille origine
6710 $result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
6711
6712 if (file_exists(dol_osencode($originImage))) {
6713 // Create thumbs
6714 $this->addThumbs($originImage);
6715 }
6716 }
6717
6718 if (is_numeric($result) && $result > 0) {
6719 return 1;
6720 } else {
6721 return -1;
6722 }
6723 }
6724
6725 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6732 public function is_photo_available($sdir)
6733 {
6734 // phpcs:enable
6735 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6736 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6737
6738 $dir = $sdir;
6739 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6740 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos/";
6741 } else {
6742 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product');
6743 }
6744
6745 $dir_osencoded = dol_osencode($dir);
6746 if (file_exists($dir_osencoded)) {
6747 $handle = opendir($dir_osencoded);
6748 if (is_resource($handle)) {
6749 while (($file = readdir($handle)) !== false) {
6750 if (!utf8_check($file)) {
6751 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // To be sure data is stored in UTF8 in memory
6752 }
6753 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6754 return true;
6755 }
6756 }
6757 }
6758 }
6759
6760 return false;
6761 }
6762
6763 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6771 public function liste_photos($dir, $nbmax = 0)
6772 {
6773 // phpcs:enable
6774 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6775 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6776
6777 $nbphoto = 0;
6778 $tabobj = array();
6779
6780 $dir_osencoded = dol_osencode($dir);
6781 $handle = @opendir($dir_osencoded);
6782 if (is_resource($handle)) {
6783 while (($file = readdir($handle)) !== false) {
6784 if (!utf8_check($file)) {
6785 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // readdir returns ISO
6786 }
6787 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6788 $nbphoto++;
6789
6790 // We forge name of thumb.
6791 $photo = $file;
6792 $photo_vignette = '';
6793 $regs = array();
6794 if (preg_match('/('.$this->regeximgext.')$/i', $photo, $regs)) {
6795 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $photo).'_small'.$regs[0];
6796 }
6797
6798 $dirthumb = $dir.'thumbs/';
6799
6800 // Object
6801 $obj = array();
6802 $obj['photo'] = $photo;
6803 if ($photo_vignette && dol_is_file($dirthumb.$photo_vignette)) {
6804 $obj['photo_vignette'] = 'thumbs/'.$photo_vignette;
6805 } else {
6806 $obj['photo_vignette'] = "";
6807 }
6808
6809 $tabobj[$nbphoto - 1] = $obj;
6810
6811 // Do we have to continue with next photo ?
6812 if ($nbmax && $nbphoto >= $nbmax) {
6813 break;
6814 }
6815 }
6816 }
6817
6818 closedir($handle);
6819 }
6820
6821 return $tabobj;
6822 }
6823
6824 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6831 public function delete_photo($file)
6832 {
6833 // phpcs:enable
6834 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6835 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6836
6837 $dir = dirname($file).'/'; // Chemin du dossier contenant l'image d'origine
6838 $dirthumb = $dir.'/thumbs/'; // Chemin du dossier contenant la vignette
6839 $filename = preg_replace('/'.preg_quote($dir, '/').'/i', '', $file); // Nom du fichier
6840
6841 // On efface l'image d'origine
6842 dol_delete_file($file, 0, 0, 0, $this); // For triggers
6843
6844 // Si elle existe, on efface la vignette
6845 if (preg_match('/('.$this->regeximgext.')$/i', $filename, $regs)) {
6846 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_small'.$regs[0];
6847 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6848 dol_delete_file($dirthumb.$photo_vignette);
6849 }
6850
6851 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_mini'.$regs[0];
6852 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6853 dol_delete_file($dirthumb.$photo_vignette);
6854 }
6855 }
6856 }
6857
6858 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6865 public function get_image_size($file)
6866 {
6867 // phpcs:enable
6868 $file_osencoded = dol_osencode($file);
6869 $infoImg = getimagesize($file_osencoded); // Get information on image
6870 $this->imgWidth = $infoImg[0]; // Largeur de l'image
6871 $this->imgHeight = $infoImg[1]; // Hauteur de l'image
6872 }
6873
6879 public function loadStateBoard()
6880 {
6881 global $hookmanager;
6882
6883 $this->nb = array();
6884
6885 $sql = "SELECT count(p.rowid) as nb, fk_product_type";
6886 $sql .= " FROM ".$this->db->prefix()."product as p";
6887 $sql .= ' WHERE p.entity IN ('.getEntity($this->element, 1).')';
6888 // Add where from hooks
6889 if (is_object($hookmanager)) {
6890 $parameters = array();
6891 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
6892 $sql .= $hookmanager->resPrint;
6893 }
6894 $sql .= ' GROUP BY fk_product_type';
6895
6896 $resql = $this->db->query($sql);
6897 if ($resql) {
6898 while ($obj = $this->db->fetch_object($resql)) {
6899 if ($obj->fk_product_type == 1) {
6900 $this->nb["services"] = $obj->nb;
6901 } else {
6902 $this->nb["products"] = $obj->nb;
6903 }
6904 }
6905 $this->db->free($resql);
6906 return 1;
6907 } else {
6908 dol_print_error($this->db);
6909 $this->error = $this->db->error();
6910 return -1;
6911 }
6912 }
6913
6919 public function isProduct()
6920 {
6921 return $this->type == Product::TYPE_PRODUCT;
6922 }
6923
6929 public function isService()
6930 {
6931 return $this->type == Product::TYPE_SERVICE;
6932 }
6933
6939 public function isStockManaged()
6940 {
6941 return (($this->isProduct() || ($this->isService() && getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) && ($this->stockable_product > 0));
6942 }
6943
6949 public function isMandatoryPeriod()
6950 {
6951 return $this->mandatory_period == 1;
6952 }
6953
6959 public function hasbatch()
6960 {
6961 return $this->status_batch > 0;
6962 }
6963
6964
6965 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6974 public function get_barcode($object, $type = '')
6975 {
6976 // phpcs:enable
6977 global $conf;
6978
6979 $result = '';
6980 if (getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
6981 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
6982 foreach ($dirsociete as $dirroot) {
6983 $res = dol_include_once($dirroot . getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM').'.php');
6984 if ($res) {
6985 break;
6986 }
6987 }
6988 $var = getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM');
6989 $mod = new $var();
6990 '@phan-var-force ModeleNumRefBarCode $mod';
6991
6992 $result = $mod->getNextValue($object, $type);
6993
6994 dol_syslog(get_class($this)."::get_barcode barcode=".$result." module=".$var);
6995 }
6996 return $result;
6997 }
6998
7006 public function initAsSpecimen()
7007 {
7008 $now = dol_now();
7009
7010 // Initialize parameters
7011 $this->specimen = 1;
7012 $this->id = 0;
7013 $this->ref = 'PRODUCT_SPEC';
7014 $this->label = 'PRODUCT SPECIMEN';
7015 $this->description = 'This is description of this product specimen that was created the '.dol_print_date($now, 'dayhourlog').'.';
7016 $this->specimen = 1;
7017 $this->country_id = 1;
7018 $this->status = 1;
7019 $this->status_buy = 1;
7020 $this->tobatch = 0;
7021 $this->sell_or_eat_by_mandatory = 0;
7022 $this->note_private = 'This is a comment (private)';
7023 $this->note_public = 'This is a comment (public)';
7024 $this->date_creation = $now;
7025 $this->date_modification = $now;
7026
7027 $this->weight = 4;
7028 $this->weight_units = 3;
7029
7030 $this->length = 5;
7031 $this->length_units = 1;
7032 $this->width = 6;
7033 $this->width_units = 0;
7034 $this->height = null;
7035 $this->height_units = null;
7036
7037 $this->surface = 30;
7038 $this->surface_units = 0;
7039 $this->volume = 300;
7040 $this->volume_units = 0;
7041
7042 $this->barcode = -1; // Create barcode automatically
7043
7044 return 1;
7045 }
7046
7056 public function getLabelOfUnit($type = 'long', $outputlangs = null, $noentities = 0)
7057 {
7058 global $langs;
7059
7060 if (empty($this->fk_unit)) {
7061 return '';
7062 }
7063 if (empty($outputlangs)) {
7064 $outputlangs = $langs;
7065 }
7066
7067 $outputlangs->load('products');
7068 $label = '';
7069
7070 $sql = "SELECT code, label, short_label FROM ".$this->db->prefix()."c_units where rowid = ".((int) $this->fk_unit);
7071
7072 $resql = $this->db->query($sql);
7073 if (!$resql) {
7074 $this->error = $this->db->error();
7075 dol_syslog(get_class($this)."::getLabelOfUnit Error ".$this->error, LOG_ERR);
7076 return -1;
7077 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
7078 if ($type == 'short') {
7079 if ($noentities) {
7080 $label = $outputlangs->transnoentitiesnoconv($res['short_label']);
7081 } else {
7082 $label = $outputlangs->trans($res['short_label']);
7083 }
7084 } elseif ($type == 'code') {
7085 $label = $res['code'];
7086 } else {
7087 if ($outputlangs->trans('unit'.$res['code']) == 'unit'.$res['code']) {
7088 // No translation available
7089 $label = $res['label'];
7090 } else {
7091 // Return the translated value
7092 if ($noentities) {
7093 $label = $outputlangs->transnoentitiesnoconv('unit'.$res['code']);
7094 } else {
7095 $label = $outputlangs->trans('unit'.$res['code']);
7096 }
7097 }
7098 }
7099 }
7100 $this->db->free($resql);
7101
7102 return $label;
7103 }
7104
7105 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7111 public function min_recommended_price()
7112 {
7113 // phpcs:enable
7114 $maxpricesupplier = 0;
7115
7116 if (getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE')) {
7117 include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
7118 $product_fourn = new ProductFournisseur($this->db);
7119 $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', '');
7120
7121 if (is_array($product_fourn_list) && count($product_fourn_list) > 0) {
7122 foreach ($product_fourn_list as $productfourn) {
7123 if ($productfourn->fourn_unitprice > $maxpricesupplier) {
7124 $maxpricesupplier = $productfourn->fourn_unitprice;
7125 }
7126 }
7127
7128 $maxpricesupplier *= getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE');
7129 }
7130 }
7131
7132 return $maxpricesupplier;
7133 }
7134
7135
7146 public function setCategories($categories)
7147 {
7148 require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
7149 return parent::setCategoriesCommon($categories, Categorie::TYPE_PRODUCT);
7150 }
7151
7160 public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
7161 {
7162 /* we firstly delete origin product prices existing in destination before update */
7163 $sql = " DELETE FROM ".$dbs->prefix()."product_customer_price";
7164 $sql .= " WHERE fk_soc = ".(int) $origin_id;
7165 $sql .= " AND fk_product IN (SELECT fk_product FROM (SELECT fk_product FROM ".$dbs->prefix()."product_customer_price WHERE fk_soc = ".(int) $dest_id.") AS tmp)";
7166 //$sql .= ' AND EXISTS (SELECT 1 FROM '.$dbs->prefix().'product_customer_price p_new WHERE p_new.fk_product = p_old.fk_product AND p_new.fk_soc = '.(int) $dest_id.')';
7167
7168 if (!$dbs->query($sql)) {
7169 return false;
7170 }
7171
7172 $tables = array(
7173 'product_customer_price',
7174 'product_customer_price_log'
7175 );
7176
7177 return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
7178 }
7179
7191 public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
7192 {
7193 $sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM ".$this->db->prefix()."product_pricerules";
7194 $query = $this->db->query($sql);
7195
7196 $rules = array();
7197
7198 while ($result = $this->db->fetch_object($query)) {
7199 $rules[$result->level] = $result;
7200 }
7201
7202 //Because prices can be based on other level's prices, we temporarily store them
7203 $prices = array(
7204 1 => $baseprice
7205 );
7206
7207 $nbofproducts = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
7208 for ($i = 1; $i <= $nbofproducts; $i++) {
7209 $price = $baseprice;
7210 $price_min = $baseprice;
7211
7212 //We have to make sure it does exist and it is > 0
7213 //First price level only allows changing min_price
7214 if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) {
7215 $price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent / 100));
7216 }
7217
7218 $prices[$i] = $price;
7219
7220 //We have to make sure it does exist and it is > 0
7221 if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) {
7222 $price_min = $price * (1 - ($rules[$i]->var_min_percent / 100));
7223 }
7224
7225 //Little check to make sure the price is modified before triggering generation
7226 $check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i]));
7227 $check_type = ($baseprice == $this->multiprices_base_type[$i]);
7228
7229 if ($check_amount && $check_type) {
7230 continue;
7231 }
7232
7233 if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, 1) < 0) {
7234 return -1;
7235 }
7236 }
7237
7238 return 1;
7239 }
7240
7246 public function getRights()
7247 {
7248 global $user;
7249
7250 if ($this->isProduct()) {
7251 return $user->rights->produit;
7252 } else {
7253 return $user->rights->service;
7254 }
7255 }
7256
7263 public function info($id)
7264 {
7265 $sql = "SELECT p.rowid, p.ref, p.datec as date_creation, GREATEST(p.tms, COALESCE(pef.tms, p.tms)) as date_modification,";
7266 $sql .= " p.fk_user_author, p.fk_user_modif";
7267 $sql .= " FROM ".$this->db->prefix().$this->table_element." as p";
7268 $sql .= " LEFT JOIN ".$this->db->prefix().$this->table_element."_extrafields as pef ON pef.fk_object=p.rowid";
7269 $sql .= " WHERE p.rowid = ".((int) $id);
7270
7271 $result = $this->db->query($sql);
7272 if ($result) {
7273 if ($this->db->num_rows($result)) {
7274 $obj = $this->db->fetch_object($result);
7275
7276 $this->id = $obj->rowid;
7277 $this->ref = $obj->ref;
7278
7279 $this->user_creation_id = $obj->fk_user_author;
7280 $this->user_modification_id = $obj->fk_user_modif;
7281
7282 $this->date_creation = $this->db->jdate($obj->date_creation);
7283 $this->date_modification = $this->db->jdate($obj->date_modification);
7284 }
7285
7286 $this->db->free($result);
7287 } else {
7288 dol_print_error($this->db);
7289 }
7290 }
7291
7292
7298 public function getProductDurationHours()
7299 {
7300 if (empty($this->duration_value)) {
7301 $this->errors[] = 'ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
7302 return -1;
7303 }
7304 if ($this->duration_unit == 's') {
7305 $prodDurationHours = 1. / 3600;
7306 } elseif ($this->duration_unit == 'i' || $this->duration_unit == 'mn' || $this->duration_unit == 'min') {
7307 $prodDurationHours = 1. / 60;
7308 } elseif ($this->duration_unit == 'h') {
7309 $prodDurationHours = 1.;
7310 } elseif ($this->duration_unit == 'd') {
7311 $prodDurationHours = 24.;
7312 } elseif ($this->duration_unit == 'w') {
7313 $prodDurationHours = 24. * 7;
7314 } elseif ($this->duration_unit == 'm') {
7315 $prodDurationHours = 24. * 30;
7316 } elseif ($this->duration_unit == 'y') {
7317 $prodDurationHours = 24. * 365;
7318 } else {
7319 // Unknown unit: the duration cannot be converted. Returning 0 silently made callers
7320 // compute a zero duration, and time.php divides by this value (#40805).
7321 $this->errors[] = 'ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
7322 return -1;
7323 }
7324 $prodDurationHours *= $this->duration_value;
7325
7326 return $prodDurationHours;
7327 }
7328
7329
7337 public function getKanbanView($option = '', $arraydata = null)
7338 {
7339 global $langs, $conf;
7340
7341 $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
7342
7343 $return = '<div class="box-flex-item box-flex-grow-zero">';
7344 $return .= '<div class="info-box info-box-sm">';
7345 $return .= '<div class="info-box-img">';
7346 $label = '';
7347 if ($this->entity !== null && $this->is_photo_available($conf->product->multidir_output[$this->entity])) {
7348 $label .= $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 120, 160, 0, 0, 0, '', 'photoref photokanban');
7349 $return .= $label;
7350 } else {
7351 if ($this->isProduct()) {
7352 $label .= img_picto('', 'product');
7353 } elseif ($this->isService()) {
7354 $label .= img_picto('', 'service');
7355 }
7356 $return .= $label;
7357 }
7358 $return .= '</div>';
7359 $return .= '<div class="info-box-content">';
7360 $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">' . $this->getNomUrl() . '</span>';
7361 if ($selected >= 0) {
7362 $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
7363 }
7364 $return .= '<br><span class="info-box-label opacitymedium inline-block tdoverflowmax150 valignmiddle" title="'.dol_escape_htmltag($this->label).'">'.dol_escape_htmltag($this->label).'</span>';
7365 if ($this->price_base_type == 'TTC') {
7366 $return .= '<br><span class="info-box-status amount">'.price($this->price_ttc).' '.$langs->trans("TTC").'</span>';
7367 } else {
7368 if ($this->status) {
7369 $return .= '<br><span class="info-box-status amount">'.price($this->price).' '.$langs->trans("HT").'</span>';
7370 }
7371 }
7372 $br = 1;
7373 if ($this->isProduct()) {
7374 $return .= '<br><div class="info-box-status opacitymedium inline-block valignmiddle">'.img_picto($langs->trans('PhysicalStock'), 'stock').'</div><div class="inline-block valignmiddle paddingleft" title="'.$langs->trans('PhysicalStock').'">'.$this->stock_reel.'</div>';
7375 $br = 0;
7376 }
7377 if ($br) {
7378 $return .= '<br>';
7379 $return .= '<div class="info-box-status inline-block valignmiddle">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7380 } else {
7381 $return .= '<div class="info-box-status inline-block valignmiddle marginleftonly paddingleft">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7382 }
7383 $return .= '</div>';
7384 $return .= '</div>';
7385 $return .= '</div>';
7386 return $return;
7387 }
7388}
7389
7395{
7396 public $picto = 'service';
7397}
if(! $sortfield) if(! $sortorder) $object
Definition account.php:100
length_accountg($account)
Return General accounting account with defined length (used for product and miscellaneous)
clean_account($account)
Return accounting account without zero on the right.
$object ref
Definition info.php:90
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.
commonGenerateDocument($modelspath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams=null)
Common function for all objects extending CommonObject for generating documents.
setErrorsFromObject($object)
setErrorsFromObject
isObjectUsed($id=0, $entity=0)
Function to check if an object is used by others (by children).
deleteExtraFields()
Delete all extra fields values for the current object.
static commonReplaceThirdparty(DoliDB $dbs, $origin_id, $dest_id, array $tables, $ignoreerrors=0)
Function used to replace a thirdparty id with another one.
show_photos($modulepart, $sdir, $size=0, $nbmax=0, $nbbyrow=5, $showfilename=0, $showaction=0, $maxHeight=120, $maxWidth=160, $nolink=0, $overwritetitle=0, $usesharelink=0, $cache='', $addphotorefcss='photoref')
Show photos of an object (nbmax maximum), into several columns.
insertExtraFields($trigger='', $userused=null)
Add/Update all extra fields values for the current object.
addThumbs($file, $quality=50)
Build thumb.
Class to manage Dolibarr database access.
Class to manage ECM files.
const STATUS_OPEN_INTERNAL
Warehouse open and only operations for stock transfers/corrections allowed (not for customer shipping...
const STATUS_OPEN_ALL
Warehouse open and any operations are allowed (customer shipping, supplier dispatch,...
const STATUS_CLOSED
Warehouse closed, inactive.
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...
const STATUS_DRAFT
Draft status.
Class to manage generation of HTML components Only common components must be here.
Class to manage stock movements.
Class to parse product price expressions.
Class ProductCombination Used to represent the relation between a product and one of its variants.
File of class to manage predefined price products or services by customer.
Class to manage predefined suppliers products.
Class to manage products or services.
static replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
Function used to replace a thirdparty id with another one.
get_nb_achat($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units or supplier invoices in which product is included.
getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp=0)
Return price of sell of a product for a seller/buyer/product.
__construct($db)
Constructor.
is_sousproduit($fk_parent, $fk_child)
Check if it is a sub-product into a kit.
const DISABLED_STOCK
Stockable product.
const SELL_OR_EAT_BY_MANDATORY_ID_NONE
Const sell or eat by mandatory id.
isStockManaged()
Return if the object is managed in stock.
setPriceExpression($expression_id)
Sets the supplier price expression.
getArrayForPriceCompare($level=0)
used to check if price have really change to avoid log pollution
get_arbo_each_prod($multiply=1, $ignore_stock_load=0)
Build the tree of subproducts and return it.
check_barcode($valuetotest, $typefortest)
Check barcode.
list_suppliers()
Return list of suppliers providing the product or service.
load_stats_mo($socid=0)
Charge tableau des stats OF pour le produit/service.
isVariant()
Return if loaded product is a variant.
updatePrice($newprice, $newpricebase, $user, $newvat=null, $newminprice=0, $level=0, $newnpr=0, $newpbq=0, $ignore_autogen=0, $localtaxes_array=array(), $newdefaultvatcode='', $price_label='', $notrigger=0)
Modify customer price of a product/Service for a given level.
hasVariants()
Return if a product has variants or not.
delMultiLangs($langtodelete, $user)
Delete a language for this product.
load_stats_proposal_supplier($socid=0)
Charge tableau des stats propale pour le produit/service.
getLibFinished()
Retour label of nature of product.
add_sousproduit($id_pere, $id_fils, $qty, $incdec=1, $notrigger=0)
Link a product/service to a parent product/service.
add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
Add a supplier price for the product.
hasFatherOrChild($mode=0)
Count all parent and children products for current product (first level only)
load_stats_facturerec($socid=0)
Load array of statistics for recurring invoice for product/service.
get_nb_propalsupplier($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units in proposals in which product is included.
load_stats_facturefournrec($socid=0)
Load array of statistics for recurring supplier invoice for product/service.
get_nb_contract($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units in orders in which product is included.
load_stats_facture_fournisseur($socid=0)
Load array of statistics for vendor invoice for product/service.
get_nb_ordersupplier($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units in orders in which product is included.
getMultiLangs()
Load array this->multilangs.
get_nb_mos($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units in orders in which product is included.
clone_associations($fromId, $toId)
Clone links between products.
create($user, $notrigger=0)
Insert product into database.
load_stats_contrat($socid=0)
Charge tableau des stats contrat pour le produit/service.
isService()
Return if the object is a service.
getRights()
Returns the rights used for this class.
loadBatchInfo($batch)
Load existing information about a serial.
load_stock($option='', $includedraftpoforvirtual=null, $dateofvirtualstock=null)
Load information about stock of a product into ->stock_reel, ->stock_warehouse[] (including stock_war...
getProductDurationHours()
Return the duration of a service in hours (for a service based on duration fields)
get_buyprice($prodfournprice, $qty, $product_id=0, $fourn_ref='', $fk_soc=0)
Read price used by a provider.
clone_fournisseurs($fromId, $toId)
Recopie les fournisseurs et prix fournisseurs d'un produit/service sur un autre.
const TYPE_PRODUCT
Regular product.
add_photo($sdir, $file)
Move an uploaded file described into $file array into target directory $sdir.
log_price_delete($user, $rowid)
Delete a price line of this product.
info($id)
Load information for tab info.
correct_stock($user, $id_entrepot, $nbpiece, $movement, $label='', $price=0, $inventorycode='', $origin_element='', $origin_id=null, $disablestockchangeforsubproduct=0, $extrafields=null)
Adjust stock in a warehouse for product.
generateDocument($modele, $outputlangs, $hidedetails=0, $hidedesc=0, $hideref=0)
Create a document onto disk according to template module.
static getSellOrEatByMandatoryList()
Get the array of labels of Sell by or Eat by all mandatory flags for each status.
getChildsArbo($id, $firstlevelonly=0, $level=1, $parents=array())
Return children of product $id.
load_virtual_stock($includedraftpoforvirtual=null, $dateofvirtualstock=null)
Load value ->stock_theorique of a product.
load_stats_propale($socid=0)
Charge tableau des stats propale pour le produit/service.
get_barcode($object, $type='')
Get a barcode from the module to generate barcode values.
setAccountancyCode($type, $value)
Sets an accountancy code for a product.
load_stats_facture($socid=0)
Charge tableau des stats facture pour le produit/service.
setCategories($categories)
Sets object to supplied categories.
load_stats_reception($socid=0, $filtrestatut='', $forVirtualStock=0, $dateofvirtualstock=null)
Charge tableau des stats réception fournisseur pour le produit/service.
get_nb_propal($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units in proposals in which product is included.
update($id, $user, $notrigger=0, $action='update', $updatetype=false)
Update a record into database.
setMultiLangs($user)
Update or add a translation for a product.
correct_stock_batch($user, $id_entrepot, $nbpiece, $movement, $label='', $price=0, $dlc='', $dluo='', $lot='', $inventorycode='', $origin_element='', $origin_id=null, $disablestockchangeforsubproduct=0, $extrafields=null, $force_update_batch=false)
Adjust stock in a warehouse for product with batch number.
load_stats_bom($socid=0)
Charge tableau des stats OF pour le produit/service.
hasbatch()
Return if the object has a batch management.
del_sousproduit($fk_parent, $fk_child, $notrigger=0)
Remove a link between a subproduct and a parent product/service.
fetch($id=0, $ref='', $ref_ext='', $barcode='', $ignore_expression=0, $ignore_price_load=0, $ignore_lang_load=0)
Load a product in memory from database.
update_sousproduit($id_pere, $id_fils, $qty, $incdec=1, $notrigger=0)
Modify composed product.
load_stats_commande($socid=0, $filtrestatut='', $forVirtualStock=0)
Charge tableau des stats commande client pour le produit/service.
delete_photo($file)
Delete a photo and its thumbs.
fetch_prod_arbo($prod, $compl_path='', $multiply=1, $level=1, $id_parent=0, $ignore_stock_load=0)
Function recursive, used only by get_arbo_each_prod(), to build tree of subproducts into ->res Define...
getKanbanView($option='', $arraydata=null)
Return clickable link of object (with eventually picto)
getLibStatut($mode=0, $type=0)
Return label of status of object.
load_stats_sending($socid=0, $filtrestatut='', $forVirtualStock=0, $filterShipmentStatus='')
Charge tableau des stats expedition client pour le produit/service.
clone_price($fromId, $toId)
Recopie les prix d'un produit/service sur un autre.
load_stats_inproduction($socid=0, $filtrestatut='', $forVirtualStock=0, $dateofvirtualstock=null, $warehouseid=0)
Charge tableau des stats production pour le produit/service.
check()
Check that ref and label are ok.
initAsSpecimen()
Initialise an instance with random values.
liste_photos($dir, $nbmax=0)
Return an array with all photos of product found on disk.
loadStateBoard()
Load indicators this->nb for the dashboard.
getFather()
Return all parent products for current product (first level only)
getNomUrl($withpicto=0, $option='', $maxlength=0, $save_lastsearch_value=-1, $notooltip=0, $morecss='', $add_label=0, $sep=' - ')
Return clickable link of object (with eventually picto)
getSellOrEatByMandatoryLabel()
Get the label for sell by or eat by mandatory flag of the current product.
verify()
Check properties of product are ok (like name, barcode, ...).
get_sousproduits_arbo()
get_nb_order($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units in orders in which product is included.
loadStockForVirtualProduct($option='', $qtyWish=1)
Load stock for components of virtual product (first level only)
min_recommended_price()
Return minimum product recommended price.
_log_price($user, $level=0)
Insert a track that we changed a customer price.
getLabelOfUnit($type='long', $outputlangs=null, $noentities=0)
Reads the units dictionary to return the translation code of a unit (if type='code'),...
_get_stats($sql, $mode, $year=0)
Return an array formatted for showing graphs.
load_stats_commande_fournisseur($socid=0, $filtrestatut='', $forVirtualStock=0, $dateofvirtualstock=null)
Charge tableau des stats commande fournisseur pour le produit/service.
isMandatoryPeriod()
Return if the object has a constraint on mandatory_period.
isProduct()
Return if the object is a product.
generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
Generates prices for a product based on product multiprice generation rules.
LibStatut($status, $mode=0, $type=0)
Return label of a given status.
const TYPE_SERVICE
Service.
is_photo_available($sdir)
Return if at least one photo is available.
get_image_size($file)
Load size of image file.
get_nb_vente($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units or customers invoices in which product is included.
getTooltipContentArray($params)
getTooltipContentArray
Class to manage products or services.
Manage record for batch number management.
static findAll($dbs, $fk_product_stock, $with_qty=0, $fk_product=0)
Return all batch detail records for a given product and warehouse.
Class with list of lots and properties.
Class to manage Dolibarr users.
hasRight($module, $permlevel1, $permlevel2='')
Return if a user has a permission.
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
getCountry($searchkey, $withcode='', $dbtouse=null, $outputlangs=null, $entconv=1, $searchlabel='')
Return country label, code or id from an id, code or label.
global $mysoc
print $script_file $mode $langs defaultlang(is_numeric($duration_value) ? " delay=". $duration_value :"").(is_numeric($duration_value2) ? " after cd cd cd description as description
Only used if Module[ID]Desc translation string is not found.
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_is_file($pathoffile)
Return if path is a file.
dol_move_uploaded_file($src_file, $dest_file, $allowoverwrite, $disablevirusscan=0, $uploaderrorcode=0, $nohook=0, $keyforsourcefile='addedfile', $upload_dir='', $mode=0)
Check validity of a file upload from an GUI page, and move it to its final destination.
dol_now($mode='gmt')
Return date for now.
dol_mktime($hour, $minute, $second, $month, $day, $year, $gm='auto', $check=1)
Return a timestamp date built from detailed information (by default a local PHP server timestamp) Rep...
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)
dolGetFirstLineOfText($text, $nboflines=1, $charset='UTF-8')
Return first line of text.
dol_osencode($str)
Return a string encoded into OS filesystem encoding.
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
dol_string_nospecial($str, $newstr='_', $badcharstoreplace='', $badcharstoremove='', $keepspaces=0)
Clean a string from all punctuation characters to use it as a ref or login.
img_object($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $allowothertags=array())
Show a picto called object_picto (generic function)
dol_sanitizeFileName($str, $newstr='_', $unaccent=1, $includequotes=0, $allowdash=0)
Clean a string to use it as a file name.
dol_strlen($string, $stringencoding='UTF-8')
Make a strlen call.
price($amount, $form=0, $outlangs='', $trunc=1, $rounding=-1, $forcerounding=-1, $currency_code='')
Function to format a value into an amount for visual output Function used into PDF and HTML pages.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
if(!function_exists( 'dol_getprefix')) dol_include_once($relpath, $classname='')
Make an include_once using default root and alternate root if it fails.
get_default_npr(Societe $thirdparty_seller, Societe $thirdparty_buyer, $idprod=0, $idprodfournprice=0)
Function that returns whether VAT must be recoverable collected VAT (e.g.: VAT NPR in France)
yn($yesno, $format=1, $color=0)
Return yes or no in current language.
dolGetStatus($statusLabel='', $statusLabelShort='', $html='', $statusType='status0', $displayMode=0, $url='', $params=array())
Output the badge of a status.
dol_clone($srcobject, $native=2)
Create a clone of instance of object (new instance with same value for each properties) With native =...
dol_print_date($time, $format='', $tzoutput='auto', $outputlangs=null, $encodetooutput=false, $decorate=0)
Output date in a string format according to outputlangs (or langs if not defined).
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
dol_trunc($string, $size=40, $trunc='right', $stringencoding='UTF-8', $nodot=0, $display=0)
Truncate a string to a particular length adding '…' if string larger than length.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
isModEnabled($module)
Is Dolibarr module enabled.
utf8_check($str)
Check if a string is in UTF8.
get_default_tva(Societe $thirdparty_seller, Societe $thirdparty_buyer, $idprod=0, $idprodfournprice=0)
Function that return vat rate of a product line (according to seller, buyer and product vat rate) VAT...
get_localtax($vatrate, $local, $thirdparty_buyer=null, $thirdparty_seller=null, $vatnpr=0)
Return localtax rate for a particular VAT rate, when selling a product with vat $vatrate,...
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
getEntity($element, $shared=1, $currentobject=null)
Get list of entity id to use.
dol_mkdir($dir, $dataroot='', $newmask='')
Creation of a directory (this can create recursive subdir)
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
Returns text escaped for inclusion in HTML alt or title or value tags, or into values of HTML input f...
image_format_supported($file, $acceptsvg=0)
Return if a filename is file name of a supported image format.
query($query, $usesavepoint=0, $type='auto', $result_mode=0)
Execute a SQL request and return the resultset.
measuring_units_squared($unitscale)
Transform a given unit scale into the square of that unit, if known.
measuringUnitString($unitid, $measuring_style='', $unitscale=null, $use_short_label=0, $outputlangs=null)
Return translation label of a unit key.
measuring_units_cubed($unit)
Transform a given unit scale into the cube of that unit, if known.
if(preg_match('/(crypted|dolcrypt):/i', $dolibarr_main_db_pass)||!empty($dolibarr_main_db_encrypted_pass)) $conf db type
'integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter[:Sortfield]]]',...
Definition repair.php:132