dolibarr 24.0.0-beta
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-2026 Frédéric France <frederic.france@free.fr>
18 * Copyright (C) 2023 Benjamin Falière <benjamin.faliere@altairis.fr>
19 * Copyright (C) 2024-2026 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|\.avif';
115
121 public $ref;
122
128 public $libelle;
129
135 public $label;
136
142 public $description;
143
149 public $other;
150
156 public $type = self::TYPE_PRODUCT;
157
163 public $price;
164
168 public $price_formated; // used by takepos/ajax/ajax.php
169
175 public $price_ttc;
176
180 public $price_ttc_formated; // used by takepos/ajax/ajax.php
181
187 public $price_min;
188
194 public $price_min_ttc;
195
200 public $price_base_type;
204 public $price_label;
205
207
210 public $multiprices = array();
214 public $multiprices_ttc = array();
218 public $multiprices_base_type = array();
222 public $multiprices_default_vat_code = array();
226 public $multiprices_min = array();
230 public $multiprices_min_ttc = array();
234 public $multiprices_tva_tx = array();
238 public $multiprices_recuperableonly = array();
239
241
244 public $price_by_qty;
248 public $prices_by_qty = array();
252 public $prices_by_qty_id = array();
256 public $prices_by_qty_list = array();
257
261 public $level;
262
266 public $multilangs = array();
267
271 public $default_vat_code;
272
276 public $tva_tx;
277
281 public $tva_npr = 0;
282
286 public $remise_percent;
287
291 public $localtax1_tx;
295 public $localtax2_tx;
299 public $localtax1_type;
303 public $localtax2_type;
304
305 // Properties set by get_buyprice() for return
306
310 public $desc_supplier;
314 public $vatrate_supplier;
318 public $default_vat_code_supplier;
319
323 public $fourn_multicurrency_price;
324
328 public $fourn_multicurrency_unitprice;
332 public $fourn_multicurrency_tx;
336 public $fourn_multicurrency_id;
340 public $fourn_multicurrency_code;
341
345 public $packaging;
346
347
354 public $lifetime;
355
362 public $qc_frequency;
363
369 public $stock_reel = 0;
370
376 public $stock_theorique;
377
383 public $cost_price;
384
390 public $pmp;
391
397 public $seuil_stock_alerte = 0;
398
402 public $desiredstock = 0;
403
407 public $duration_value;
411 public $duration_unit;
415 public $duration;
416
420 public $fk_default_workstation;
421
427 public $status = 0;
428
435 public $tosell;
436
442 public $status_buy = 0;
443
450 public $tobuy;
451
457 public $finished;
458
464 public $fk_default_bom;
465
471 public $product_fourn_price_id;
472
478 public $buyprice;
479
485 public $tobatch;
486
487
493 public $status_batch = 0;
494
500 public $sell_or_eat_by_mandatory = 0;
501
507 public $batch_mask = '';
508
514 public $customcode;
515
521 public $url;
522
524
527 public $weight;
528
532 public $weight_units; // scale -3, 0, 3, 6
536 public $length;
540 public $length_units; // scale -3, 0, 3, 6
544 public $width;
548 public $width_units; // scale -3, 0, 3, 6
552 public $height;
556 public $height_units; // scale -3, 0, 3, 6
560 public $surface;
564 public $surface_units; // scale -3, 0, 3, 6
568 public $volume;
572 public $volume_units; // scale -3, 0, 3, 6
573
577 public $net_measure;
581 public $net_measure_units; // scale -3, 0, 3, 6
582
586 public $accountancy_code_sell;
590 public $accountancy_code_sell_intra;
594 public $accountancy_code_sell_export;
598 public $accountancy_code_buy;
602 public $accountancy_code_buy_intra;
606 public $accountancy_code_buy_export;
607
611 public $barcode;
612
616 public $barcode_type;
617
621 public $barcode_type_code;
622
626 public $stats_propale = array();
627
631 public $stats_commande = array();
632
636 public $stats_contrat = array();
637
641 public $stats_facture = array();
642
646 public $stats_proposal_supplier = array();
647
651 public $stats_commande_fournisseur = array();
652
656 public $stats_expedition = array();
657
661 public $stats_reception = array();
662
666 public $stats_mo = array();
667
671 public $stats_bom = array();
672
676 public $stats_mrptoconsume = array();
677
681 public $stats_mrptoproduce = array();
682
686 public $stats_facturerec = array();
687
691 public $stats_facture_fournisseur = array();
692
696 public $stats_facturefournrec = array();
697
701 public $imgWidth;
705 public $imgHeight;
706
711 public $product_fourn_id;
712
717 public $product_id_already_linked;
718
723 public $nbphoto = 0;
724
728 public $stock_warehouse = array();
729
733 public $fk_default_warehouse;
734
738 public $fk_price_expression;
739
744 public $fourn_qty;
745
750 public $fourn_pu;
751
756 public $fourn_price_base_type;
757
761 public $fourn_socid;
762
768 public $ref_fourn;
769
773 public $ref_supplier;
774
780 public $fk_unit;
781
787 public $price_autogen = 0;
788
794 public $sousprods = array();
795
799 public $res;
800
801
807 public $is_object_used;
808
818 public $is_sousproduit_qty;
819
830 public $is_sousproduit_incdec;
831
835 public $mandatory_period;
836
842 public $stockable_product = 1;
843
872 public $fields = array(
873 'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'index' => 1, 'position' => 1, 'comment' => 'Id'),
874 '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'),
875 'entity' => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'visible' => 0, 'default' => '1', 'notnull' => 1, 'index' => 1, 'position' => 5),
876 'label' => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'showoncombobox' => 2, 'position' => 15, 'csslist' => 'tdoverflowmax250'),
877 'barcode' => array('type' => 'varchar(255)', 'label' => 'Barcode', 'enabled' => 'isModEnabled("barcode")', 'position' => 20, 'visible' => -1, 'showoncombobox' => 3, 'cssview' => 'tdwordbreak', 'csslist' => 'tdoverflowmax125'),
878 'fk_barcode_type' => array('type' => 'integer', 'label' => 'BarcodeType', 'enabled' => 1, 'position' => 21, 'notnull' => 0, 'visible' => -1,),
879 'note_public' => array('type' => 'html', 'label' => 'NotePublic', 'enabled' => 1, 'visible' => 0, 'position' => 61),
880 'note' => array('type' => 'html', 'label' => 'NotePrivate', 'enabled' => 1, 'visible' => 0, 'position' => 62),
881 'datec' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 500),
882 'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 501),
883 //'date_valid' =>array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-2, 'position'=>502),
884 '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'),
885 'fk_user_modif' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'UserModif', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'position' => 511),
886 //'fk_user_valid' =>array('type'=>'integer', 'label'=>'UserValidation', 'enabled'=>1, 'visible'=>-1, 'position'=>512),
887 'localtax1_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax1tx', 'enabled' => 1, 'position' => 150, 'notnull' => 0, 'visible' => -1,),
888 'localtax1_type' => array('type' => 'varchar(10)', 'label' => 'Localtax1type', 'enabled' => 1, 'position' => 155, 'notnull' => 1, 'visible' => -1,),
889 'localtax2_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax2tx', 'enabled' => 1, 'position' => 160, 'notnull' => 0, 'visible' => -1,),
890 'localtax2_type' => array('type' => 'varchar(10)', 'label' => 'Localtax2type', 'enabled' => 1, 'position' => 165, 'notnull' => 1, 'visible' => -1,),
891 'last_main_doc' => array('type' => 'varchar(255)', 'label' => 'LastMainDoc', 'enabled' => 1, 'visible' => -1, 'position' => 170),
892 'import_key' => array('type' => 'varchar(14)', 'label' => 'ImportId', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'index' => 0, 'position' => 1000),
893 //'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')),
894 //'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')),
895 'mandatory_period' => array('type' => 'integer', 'label' => 'mandatoryperiod', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'default' => '0', 'index' => 1, 'position' => 1000),
896 'stockable_product' => array('type' => 'integer', 'label' => 'stockable_product', 'enabled' => 1, 'visible' => 1, 'default' => '1', 'notnull' => 1, 'index' => 1, 'position' => 502),
897 );
898
902 const TYPE_PRODUCT = 0;
906 const TYPE_SERVICE = 1;
907
911 const DISABLED_STOCK = 0;
912 const ENABLED_STOCK = 1;
913
919 public function __construct($db)
920 {
921 $this->db = $db;
922
923 $this->ismultientitymanaged = 1;
924 $this->isextrafieldmanaged = 1;
925
926 $this->canvas = '';
927 }
928
934 public function check()
935 {
936 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
937 $this->ref = trim($this->ref);
938 } else {
939 $this->ref = dol_sanitizeFileName(stripslashes($this->ref));
940 }
941
942 $err = 0;
943 if (dol_strlen(trim($this->ref)) == 0) {
944 $err++;
945 }
946
947 if (dol_strlen(trim($this->label)) == 0) {
948 $err++;
949 }
950
951 if ($err > 0) {
952 return 0;
953 } else {
954 return 1;
955 }
956 }
957
965 public function create($user, $notrigger = 0)
966 {
967 global $conf, $langs;
968
969 $error = 0;
970
971 // Clean parameters
972 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
973 $this->ref = trim($this->ref);
974 } else {
975 $this->ref = dol_sanitizeFileName(dol_string_nospecial(trim($this->ref)));
976 }
977 $this->label = trim($this->label);
978 $this->price_ttc = (float) price2num($this->price_ttc);
979 $this->price = (float) price2num($this->price);
980 $this->price_min_ttc = (float) price2num($this->price_min_ttc);
981 $this->price_min = (float) price2num($this->price_min);
982 $this->price_label = trim($this->price_label);
983 if (empty($this->tva_tx)) {
984 $this->tva_tx = 0;
985 }
986 if (empty($this->tva_npr)) {
987 $this->tva_npr = 0;
988 }
989 // Local taxes
990 if (empty($this->localtax1_tx)) {
991 $this->localtax1_tx = 0;
992 }
993 if (empty($this->localtax2_tx)) {
994 $this->localtax2_tx = 0;
995 }
996 if (empty($this->localtax1_type)) {
997 $this->localtax1_type = '0';
998 }
999 if (empty($this->localtax2_type)) {
1000 $this->localtax2_type = '0';
1001 }
1002 // Price
1003 if (empty($this->price_base_type) && getDolGlobalString('PRODUCT_PRICE_BASE_TYPE')) {
1004 $this->price_base_type = getDolGlobalString('PRODUCT_PRICE_BASE_TYPE');
1005 }
1006 if (empty($this->price)) {
1007 $this->price = 0;
1008 }
1009 if (empty($this->price_min)) {
1010 $this->price_min = 0;
1011 }
1012 // Price by quantity
1013 if (empty($this->price_by_qty)) {
1014 $this->price_by_qty = 0;
1015 }
1016
1017 if (empty($this->status)) {
1018 $this->status = 0;
1019 }
1020 if (empty($this->status_buy)) {
1021 $this->status_buy = 0;
1022 }
1023 if (empty($this->stockable_product)) {
1024 $this->stockable_product = 0;
1025 }
1026
1027 $price_ht = 0;
1028 $price_ttc = 0;
1029 $price_min_ht = 0;
1030 $price_min_ttc = 0;
1031
1032 //
1033 if ($this->price_base_type == 'TTC' && $this->price_ttc > 0) {
1034 $price_ttc = price2num($this->price_ttc, 'MU');
1035 $price_ht = price2num($this->price_ttc / (1 + ($this->tva_tx / 100)), 'MU');
1036 }
1037
1038 //
1039 if ($this->price_base_type != 'TTC' && $this->price > 0) {
1040 $price_ht = price2num($this->price, 'MU');
1041 $price_ttc = price2num($this->price * (1 + ($this->tva_tx / 100)), 'MU');
1042 }
1043
1044 //
1045 if (($this->price_min_ttc > 0) && ($this->price_base_type == 'TTC')) {
1046 $price_min_ttc = price2num($this->price_min_ttc, 'MU');
1047 $price_min_ht = price2num($this->price_min_ttc / (1 + ($this->tva_tx / 100)), 'MU');
1048 }
1049
1050 //
1051 if (($this->price_min > 0) && ($this->price_base_type != 'TTC')) {
1052 $price_min_ht = price2num($this->price_min, 'MU');
1053 $price_min_ttc = price2num($this->price_min * (1 + ($this->tva_tx / 100)), 'MU');
1054 }
1055
1056 $this->accountancy_code_buy = trim((string) $this->accountancy_code_buy);
1057 $this->accountancy_code_buy_intra = trim((string) $this->accountancy_code_buy_intra);
1058 $this->accountancy_code_buy_export = trim((string) $this->accountancy_code_buy_export);
1059 $this->accountancy_code_sell = trim((string) $this->accountancy_code_sell);
1060 $this->accountancy_code_sell_intra = trim((string) $this->accountancy_code_sell_intra);
1061 $this->accountancy_code_sell_export = trim((string) $this->accountancy_code_sell_export);
1062
1063 // Normalize the accountancy codes the way the admin dropdown does it, so an API client that
1064 // sends '606111000' ends up with the same '606111' value the GUI stores (see issue #32343).
1065 // When ACCOUNTING_MANAGE_ZERO is on, trailing zeros are part of the code and must be kept.
1066 if (!getDolGlobalString('ACCOUNTING_MANAGE_ZERO')) {
1067 require_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
1068 $this->accountancy_code_buy = clean_account($this->accountancy_code_buy);
1069 $this->accountancy_code_buy_intra = clean_account($this->accountancy_code_buy_intra);
1070 $this->accountancy_code_buy_export = clean_account($this->accountancy_code_buy_export);
1071 $this->accountancy_code_sell = clean_account($this->accountancy_code_sell);
1072 $this->accountancy_code_sell_intra = clean_account($this->accountancy_code_sell_intra);
1073 $this->accountancy_code_sell_export = clean_account($this->accountancy_code_sell_export);
1074 }
1075
1076 // Barcode value
1077 $this->barcode = trim($this->barcode);
1078 $this->mandatory_period = empty($this->mandatory_period) ? 0 : $this->mandatory_period;
1079 // Check parameters
1080 if (empty($this->label)) {
1081 $langs->load('errors');
1082 $this->errors[] = $langs->trans('ErrorMandatoryParametersNotProvided');
1083 return -1;
1084 }
1085
1086 if (empty($this->ref) || $this->ref == 'auto') {
1087 // Load object modCodeProduct
1088 $module = getDolGlobalString('PRODUCT_CODEPRODUCT_ADDON', 'mod_codeproduct_leopard');
1089 if ($module != 'mod_codeproduct_leopard') { // Do not load module file for leopard
1090 if (substr($module, 0, 16) == 'mod_codeproduct_' && substr($module, -3) == 'php') {
1091 $module = substr($module, 0, dol_strlen($module) - 4);
1092 }
1093 dol_include_once('/core/modules/product/'.$module.'.php');
1094 $modCodeProduct = new $module();
1095 '@phan-var-force ModeleProductCode $modCodeProduct';
1096 if (!empty($modCodeProduct->code_auto)) {
1097 $this->ref = $modCodeProduct->getNextValue($this, $this->type);
1098 }
1099 unset($modCodeProduct);
1100 }
1101
1102 if (empty($this->ref)) {
1103 $this->error = 'ProductModuleNotSetupForAutoRef';
1104 return -2;
1105 }
1106 }
1107
1108 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);
1109
1110 $now = dol_now();
1111
1112 if (empty($this->date_creation)) {
1113 $this->date_creation = $now;
1114 }
1115
1116 $this->db->begin();
1117
1118 // For automatic creation during create action (not used by Dolibarr GUI, can be used by scripts)
1119 if ($this->barcode == '-1' || $this->barcode == 'auto') {
1120 $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1121 }
1122
1123 // Check more parameters
1124 // If error, this->errors[] is filled
1125 $result = $this->verify();
1126
1127 if ($result >= 0) {
1128 $sql = "SELECT count(*) as nb";
1129 $sql .= " FROM ".$this->db->prefix()."product";
1130 $sql .= " WHERE entity IN (".getEntity('product').")";
1131 $sql .= " AND ref = '".$this->db->escape($this->ref)."'";
1132
1133 $result = $this->db->query($sql);
1134 if ($result) {
1135 $obj = $this->db->fetch_object($result);
1136 if ($obj->nb == 0) {
1137 // Insert new product, no previous one found
1138 $sql = "INSERT INTO ".$this->db->prefix()."product (";
1139 $sql .= "datec";
1140 $sql .= ", entity";
1141 $sql .= ", ref";
1142 $sql .= ", ref_ext";
1143 $sql .= ", price_min";
1144 $sql .= ", price_min_ttc";
1145 $sql .= ", label";
1146 $sql .= ", fk_user_author";
1147 $sql .= ", fk_product_type";
1148 $sql .= ", price";
1149 $sql .= ", price_ttc";
1150 $sql .= ", price_base_type";
1151 $sql .= ", price_label";
1152 $sql .= ", tobuy";
1153 $sql .= ", tosell";
1154 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1155 $sql .= ", accountancy_code_buy";
1156 $sql .= ", accountancy_code_buy_intra";
1157 $sql .= ", accountancy_code_buy_export";
1158 $sql .= ", accountancy_code_sell";
1159 $sql .= ", accountancy_code_sell_intra";
1160 $sql .= ", accountancy_code_sell_export";
1161 }
1162 $sql .= ", canvas";
1163 $sql .= ", finished";
1164 $sql .= ", tobatch";
1165 $sql .= ", sell_or_eat_by_mandatory";
1166 $sql .= ", batch_mask";
1167 $sql .= ", fk_unit";
1168 $sql .= ", mandatory_period";
1169 $sql .= ", stockable_product";
1170 if (!empty($this->default_vat_code)) {
1171 $sql .= ", default_vat_code";
1172 }
1173 $sql .= ") VALUES (";
1174 $sql .= "'".$this->db->idate($this->date_creation)."'";
1175 $sql .= ", ".(!empty($this->entity) ? (int) $this->entity : (int) $conf->entity);
1176 $sql .= ", '".$this->db->escape($this->ref)."'";
1177 $sql .= ", ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
1178 $sql .= ", ".price2num($price_min_ht);
1179 $sql .= ", ".price2num($price_min_ttc);
1180 $sql .= ", ".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null");
1181 $sql .= ", ".((int) $user->id);
1182 $sql .= ", ".((int) $this->type);
1183 $sql .= ", ".price2num($price_ht, 'MT');
1184 $sql .= ", ".price2num($price_ttc, 'MT');
1185 $sql .= ", '".$this->db->escape($this->price_base_type)."'";
1186 $sql .= ", ".(!empty($this->price_label) ? "'".$this->db->escape($this->price_label)."'" : "null");
1187 $sql .= ", ".((int) $this->status);
1188 $sql .= ", ".((int) $this->status_buy);
1189 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1190 $sql .= ", '".$this->db->escape($this->accountancy_code_buy)."'";
1191 $sql .= ", '".$this->db->escape($this->accountancy_code_buy_intra)."'";
1192 $sql .= ", '".$this->db->escape($this->accountancy_code_buy_export)."'";
1193 $sql .= ", '".$this->db->escape($this->accountancy_code_sell)."'";
1194 $sql .= ", '".$this->db->escape($this->accountancy_code_sell_intra)."'";
1195 $sql .= ", '".$this->db->escape($this->accountancy_code_sell_export)."'";
1196 }
1197 $sql .= ", '".$this->db->escape($this->canvas)."'";
1198 $sql .= ", ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? 'NULL' : (int) $this->finished);
1199 $sql .= ", ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : ((int) $this->status_batch));
1200 $sql .= ", ".((empty($this->sell_or_eat_by_mandatory) || $this->sell_or_eat_by_mandatory < 0) ? 0 : ((int) $this->sell_or_eat_by_mandatory));
1201 $sql .= ", '".$this->db->escape($this->batch_mask)."'";
1202 $sql .= ", ".($this->fk_unit > 0 ? ((int) $this->fk_unit) : 'NULL');
1203 $sql .= ", '".$this->db->escape((string) $this->mandatory_period)."'";
1204 $sql .= ", ".((int) $this->stockable_product);
1205 if (!empty($this->default_vat_code)) {
1206 $sql .= ", '".$this->db->escape($this->default_vat_code)."'";
1207 }
1208 $sql .= ")";
1209 dol_syslog(get_class($this)."::Create", LOG_DEBUG);
1210
1211 $result = $this->db->query($sql);
1212 if ($result) {
1213 $id = $this->db->last_insert_id($this->db->prefix()."product");
1214
1215 if ($id > 0) {
1216 $this->id = $id;
1217 $this->price = $price_ht;
1218 $this->price_ttc = $price_ttc;
1219 $this->price_min = $price_min_ht;
1220 $this->price_min_ttc = $price_min_ttc;
1221
1222 $result = $this->_log_price($user);
1223 if ($result > 0) {
1224 if ($this->update($id, $user, 1, 'add') <= 0) {
1225 $error++;
1226 }
1227 } else {
1228 $error++;
1229 $this->error = $this->db->lasterror();
1230 }
1231
1232 // update accountancy for this entity
1233 if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1234 $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " .((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1235
1236 $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1237 $sql .= " fk_product";
1238 $sql .= ", entity";
1239 $sql .= ", accountancy_code_buy";
1240 $sql .= ", accountancy_code_buy_intra";
1241 $sql .= ", accountancy_code_buy_export";
1242 $sql .= ", accountancy_code_sell";
1243 $sql .= ", accountancy_code_sell_intra";
1244 $sql .= ", accountancy_code_sell_export";
1245 $sql .= ") VALUES (";
1246 $sql .= $this->id;
1247 $sql .= ", " . ((int) $conf->entity);
1248 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1249 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1250 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1251 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1252 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1253 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1254 $sql .= ")";
1255 $result = $this->db->query($sql);
1256 if (!$result) {
1257 $error++;
1258 $this->error = 'ErrorFailedToInsertAccountancyForEntity';
1259 }
1260 }
1261 } else {
1262 $error++;
1263 $this->error = 'ErrorFailedToGetInsertedId';
1264 }
1265 } else {
1266 $error++;
1267 $this->error = $this->db->lasterror();
1268 }
1269 } else {
1270 // Product already exists with this ref
1271 $langs->load("products");
1272 $error++;
1273 $this->error = "ErrorProductAlreadyExists";
1274 dol_syslog(get_class($this)."::Create fails, ref ".$this->ref." already exists");
1275 }
1276 } else {
1277 $error++;
1278 $this->error = $this->db->lasterror();
1279 }
1280
1281 if (!$error && !$notrigger) {
1282 // Call trigger
1283 $result = $this->call_trigger('PRODUCT_CREATE', $user);
1284 if ($result < 0) {
1285 $error++;
1286 }
1287 // End call triggers
1288 }
1289
1290 if (!$error) {
1291 $this->db->commit();
1292 return $this->id;
1293 } else {
1294 $this->db->rollback();
1295 return -$error;
1296 }
1297 } else {
1298 $this->db->rollback();
1299 dol_syslog(get_class($this)."::Create fails verify ".implode(',', $this->errors), LOG_WARNING);
1300 return -3;
1301 }
1302 }
1303
1304
1311 public function verify()
1312 {
1313 global $langs;
1314
1315 $this->errors = array();
1316
1317 $result = 0;
1318 $this->ref = trim($this->ref);
1319
1320 if (!$this->ref) {
1321 $this->errors[] = 'ErrorBadRef';
1322 $result = -2;
1323 }
1324
1325 $arrayofnonnegativevalue = array('weight' => 'Weight', 'width' => 'Width', 'height' => 'Height', 'length' => 'Length', 'surface' => 'Surface', 'volume' => 'Volume');
1326 foreach ($arrayofnonnegativevalue as $key => $value) {
1327 if (property_exists($this, $key) && !empty($this->$key) && ($this->$key < 0)) {
1328 $langs->loadLangs(array("main", "other"));
1329 $this->error = $langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv($value));
1330 $this->errors[] = $this->error;
1331 $result = -4;
1332 }
1333 }
1334
1335 $rescode = $this->check_barcode($this->barcode, $this->barcode_type_code);
1336 if ($rescode) {
1337 if ($rescode == -1) {
1338 $this->errors[] = 'ErrorBadBarCodeSyntax';
1339 } elseif ($rescode == -2) {
1340 $this->errors[] = 'ErrorBarCodeRequired';
1341 } elseif ($rescode == -3) {
1342 // Note: Common usage is to have barcode unique. For variants, we should have a different barcode.
1343 $this->errors[] = 'ErrorBarCodeAlreadyUsed';
1344 }
1345
1346 $result = -3;
1347 }
1348
1349 return $result;
1350 }
1351
1352 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1363 public function check_barcode($valuetotest, $typefortest)
1364 {
1365 // phpcs:enable
1366 global $conf;
1367
1368 if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
1369 $module = strtolower(getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM'));
1370
1371 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
1372 foreach ($dirsociete as $dirroot) {
1373 $res = dol_include_once($dirroot.$module.'.php');
1374 if ($res) {
1375 break;
1376 }
1377 }
1378
1379 $mod = new $module();
1380 '@phan-var-force ModeleNumRefBarCode $mod';
1383 dol_syslog(get_class($this)."::check_barcode value=".$valuetotest." type=".$typefortest." module=".$module);
1384 $result = $mod->verif($this->db, $valuetotest, $this, 0, $typefortest);
1385 return $result;
1386 } else {
1387 return 0;
1388 }
1389 }
1390
1402 public function update($id, $user, $notrigger = 0, $action = 'update', $updatetype = false)
1403 {
1404 global $langs, $conf, $hookmanager;
1405
1406 $error = 0;
1407
1408 // Check parameters
1409 if (!$this->label) {
1410 $this->label = 'MISSING LABEL';
1411 }
1412
1413 // Clean parameters
1414 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
1415 $this->ref = trim($this->ref);
1416 } else {
1417 $this->ref = dol_string_nospecial(trim($this->ref));
1418 }
1419 $this->label = trim($this->label);
1420 $this->description = trim($this->description);
1421 $this->note_private = (isset($this->note_private) ? trim($this->note_private) : null);
1422 $this->note_public = (isset($this->note_public) ? trim($this->note_public) : null);
1423 $this->net_measure = price2num($this->net_measure);
1424 $this->net_measure_units = (!is_numeric($this->net_measure_units) ? null : (int) $this->net_measure_units);
1425 $this->weight = price2num($this->weight);
1426 $this->weight_units = (!is_numeric($this->weight_units) ? null : (int) $this->weight_units);
1427 $this->length = price2num($this->length);
1428 $this->length_units = (!is_numeric($this->length_units) ? null : (int) $this->length_units);
1429 $this->width = price2num($this->width);
1430 $this->width_units = (!is_numeric($this->width_units) ? null : (int) $this->width_units);
1431 $this->height = price2num($this->height);
1432 $this->height_units = (!is_numeric($this->height_units) ? null : (int) $this->height_units);
1433 $this->surface = price2num($this->surface);
1434 $this->surface_units = (!is_numeric($this->surface_units) ? null : (int) $this->surface_units);
1435 $this->volume = price2num($this->volume);
1436 $this->volume_units = (!is_numeric($this->volume_units) ? null : (int) $this->volume_units);
1437
1438 // set unit not defined
1439 if (is_numeric($this->length_units)) {
1440 $this->width_units = $this->length_units; // Not used yet
1441 }
1442 if (is_numeric($this->length_units)) {
1443 $this->height_units = $this->length_units; // Not used yet
1444 }
1445
1446 // Automated compute surface and volume if not filled
1447 if (empty($this->surface) && !empty($this->length) && !empty($this->width) && $this->length_units == $this->width_units) {
1448 $this->surface = (float) $this->length * (float) $this->width;
1449 $this->surface_units = measuring_units_squared((int) $this->length_units);
1450 }
1451 if (empty($this->volume) && !empty($this->surface) && !empty($this->height) && $this->length_units == $this->height_units) {
1452 $this->volume = $this->surface * (float) $this->height;
1453 $this->volume_units = measuring_units_cubed((int) $this->height_units);
1454 }
1455
1456 if (empty($this->tva_tx)) {
1457 $this->tva_tx = 0;
1458 }
1459 if (empty($this->tva_npr)) {
1460 $this->tva_npr = 0;
1461 }
1462 if (empty($this->localtax1_tx)) {
1463 $this->localtax1_tx = 0;
1464 }
1465 if (empty($this->localtax2_tx)) {
1466 $this->localtax2_tx = 0;
1467 }
1468 if (empty($this->localtax1_type)) {
1469 $this->localtax1_type = '0';
1470 }
1471 if (empty($this->localtax2_type)) {
1472 $this->localtax2_type = '0';
1473 }
1474 if (empty($this->status)) {
1475 $this->status = 0;
1476 }
1477 if (empty($this->status_buy)) {
1478 $this->status_buy = 0;
1479 }
1480
1481 if (empty($this->country_id)) {
1482 $this->country_id = 0;
1483 }
1484 if (empty($this->country_id) && !empty($this->country_code)) {
1485 $country_id = getCountry($this->country_code, '3');
1486 $this->country_id = is_int($country_id) ? $country_id : 0;
1487 }
1488
1489 if (empty($this->state_id)) {
1490 $this->state_id = 0;
1491 }
1492
1493 if (empty($this->stockable_product)) {
1494 $this->stockable_product = 0;
1495 }
1496
1497 // For automatic creation during update action (not used by Dolibarr GUI, can be used by scripts)
1498 if ($this->barcode == '-1' || $this->barcode == 'auto') {
1499 $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1500 }
1501
1502 // Barcode value
1503 $this->barcode = (empty($this->barcode) ? '' : trim($this->barcode));
1504
1505 $this->accountancy_code_buy = trim($this->accountancy_code_buy);
1506 $this->accountancy_code_buy_intra = (!empty($this->accountancy_code_buy_intra) ? trim($this->accountancy_code_buy_intra) : '');
1507 $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
1508 $this->accountancy_code_sell = trim($this->accountancy_code_sell);
1509 $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
1510 $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
1511
1512 // Normalize the accountancy codes the way the admin dropdown does it, so an API client that
1513 // sends '606111000' ends up with the same '606111' value the GUI stores (see issue #32343).
1514 // When ACCOUNTING_MANAGE_ZERO is on, trailing zeros are part of the code and must be kept.
1515 if (!getDolGlobalString('ACCOUNTING_MANAGE_ZERO')) {
1516 require_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
1517 $this->accountancy_code_buy = clean_account($this->accountancy_code_buy);
1518 $this->accountancy_code_buy_intra = clean_account($this->accountancy_code_buy_intra);
1519 $this->accountancy_code_buy_export = clean_account($this->accountancy_code_buy_export);
1520 $this->accountancy_code_sell = clean_account($this->accountancy_code_sell);
1521 $this->accountancy_code_sell_intra = clean_account($this->accountancy_code_sell_intra);
1522 $this->accountancy_code_sell_export = clean_account($this->accountancy_code_sell_export);
1523 }
1524
1525 $this->db->begin();
1526
1527 $result = 0;
1528 // Check name is required and codes are ok or unique. If error, this->errors[] is filled
1529 if ($action != 'add') {
1530 $result = $this->verify(); // We don't check when update called during a create because verify was already done
1531 } else {
1532 // we can continue
1533 $result = 0;
1534 }
1535
1536 if ($result >= 0) {
1537 // $this->oldcopy should have been set by the caller of update (here properties were already modified)
1538 // Note that this->oldcopy must be a Product instance (not stdClass), otherwise the method
1539 // hasbatch() called below will fatal. Callers may set oldcopy via dol_clone($obj, 2) (which
1540 // returns a stdClass with scalar properties only) and then we cannot call methods on it,
1541 // so re-clone with native=1 in that case (see issues #38663, #38638).
1542 if (is_null($this->oldcopy) || (is_object($this->oldcopy) && empty($this->oldcopy->id)) || !($this->oldcopy instanceof Product)) {
1543 $this->oldcopy = dol_clone($this, 1); // 1 to clone with methods to avoid fatal error with $this->oldcopy->hasbatch()
1544 }
1545 // Test if batch management is activated on existing product
1546 // If yes, we create missing entries into product_batch
1547 if ($this->hasbatch() && !$this->oldcopy->hasbatch()) {
1548 //$valueforundefinedlot = 'Undefined'; // In previous version, 39 and lower
1549 $valueforundefinedlot = '000000';
1550 if (getDolGlobalString('STOCK_DEFAULT_BATCH')) {
1551 $valueforundefinedlot = getDolGlobalString('STOCK_DEFAULT_BATCH');
1552 }
1553
1554 dol_syslog("Flag batch of product id=".$this->id." is set to ON, so we will create missing records into product_batch");
1555
1556 $this->load_stock();
1557 foreach ($this->stock_warehouse as $idW => $ObjW) { // For each warehouse where we have stocks defined for this product (for each lines in product_stock)
1558 $qty_batch = 0;
1559 foreach ($ObjW->detail_batch as $detail) { // Each lines of detail in product_batch of the current $ObjW = product_stock
1560 if ($detail->batch == $valueforundefinedlot || $detail->batch == 'Undefined') {
1561 // We discard this line, we will create it later
1562 $sqlclean = "DELETE FROM ".$this->db->prefix()."product_batch WHERE batch in('Undefined', '".$this->db->escape($valueforundefinedlot)."') AND fk_product_stock = ".((int) $ObjW->id);
1563 $result = $this->db->query($sqlclean);
1564 if (!$result) {
1565 dol_print_error($this->db);
1566 exit;
1567 }
1568 continue;
1569 }
1570
1571 $qty_batch += $detail->qty;
1572 }
1573 // Quantities in batch details are not same as stock quantity,
1574 // so we add a default batch record to complete and get same qty in parent and child table
1575 if ($ObjW->real != $qty_batch) {
1576 $ObjBatch = new Productbatch($this->db);
1577 $ObjBatch->batch = $valueforundefinedlot;
1578 $ObjBatch->qty = ($ObjW->real - $qty_batch);
1579 $ObjBatch->fk_product_stock = (int) $ObjW->id;
1580
1581 if ($ObjBatch->create($user, 1) < 0) {
1582 $error++;
1583 $this->errors = $ObjBatch->errors;
1584 } else {
1585 // we also add lot record if not exist
1586 $ObjLot = new Productlot($this->db);
1587 // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1588 if ($ObjLot->fetch(0, $this->id, $valueforundefinedlot) == 0) {
1589 $ObjLot->fk_product = $this->id;
1590 $ObjLot->entity = (int) $this->entity;
1591 $ObjLot->fk_user_creat = $user->id;
1592 $ObjLot->batch = $valueforundefinedlot;
1593 if ($ObjLot->create($user, 1) < 0) {
1594 $error++;
1595 $this->errors = $ObjLot->errors;
1596 }
1597 }
1598 }
1599 }
1600 }
1601 }
1602
1603 $sql = "UPDATE ".$this->db->prefix()."product";
1604 $sql .= " SET label = '".$this->db->escape($this->label)."'";
1605
1606 if ($updatetype && ($this->isProduct() || $this->isService())) {
1607 $sql .= ", fk_product_type = ".((int) $this->type);
1608 }
1609
1610 $sql .= ", ref = '".$this->db->escape($this->ref)."'";
1611 $sql .= ", ref_ext = ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
1612 $sql .= ", default_vat_code = ".($this->default_vat_code ? "'".$this->db->escape($this->default_vat_code)."'" : "null");
1613 $sql .= ", tva_tx = ".((float) $this->tva_tx);
1614 $sql .= ", recuperableonly = ".((int) $this->tva_npr);
1615 $sql .= ", localtax1_tx = ".((float) $this->localtax1_tx);
1616 $sql .= ", localtax2_tx = ".((float) $this->localtax2_tx);
1617 $sql .= ", localtax1_type = ".($this->localtax1_type != '' ? "'".$this->db->escape($this->localtax1_type)."'" : "'0'");
1618 $sql .= ", localtax2_type = ".($this->localtax2_type != '' ? "'".$this->db->escape($this->localtax2_type)."'" : "'0'");
1619
1620 $sql .= ", barcode = ".(empty($this->barcode) ? "null" : "'".$this->db->escape($this->barcode)."'");
1621 $sql .= ", fk_barcode_type = ".(empty($this->barcode_type) ? "null" : $this->db->escape((string) $this->barcode_type));
1622
1623 $sql .= ", tosell = ".(int) $this->status;
1624 $sql .= ", tobuy = ".(int) $this->status_buy;
1625 $sql .= ", tobatch = ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : (int) $this->status_batch);
1626 $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);
1627 $sql .= ", batch_mask = '".$this->db->escape($this->batch_mask)."'";
1628
1629 $sql .= ", finished = ".((!isset($this->finished) || $this->finished < 0 || $this->finished === '') ? "null" : (int) $this->finished);
1630 $sql .= ", fk_default_bom = ".((!isset($this->fk_default_bom) || $this->fk_default_bom < 0 || $this->fk_default_bom == '') ? "null" : (int) $this->fk_default_bom);
1631 $sql .= ", net_measure = ".($this->net_measure != '' ? "'".$this->db->escape($this->net_measure)."'" : 'null');
1632 $sql .= ", net_measure_units = ".((string) $this->net_measure_units != '' ? ((int) $this->net_measure_units) : 'null');
1633 $sql .= ", weight = ".($this->weight != '' ? "'".$this->db->escape($this->weight)."'" : 'null');
1634 $sql .= ", weight_units = ".((string) $this->weight_units != '' ? ((int) $this->weight_units) : 'null');
1635 $sql .= ", length = ".($this->length != '' ? "'".$this->db->escape($this->length)."'" : 'null');
1636 $sql .= ", length_units = ".((string) $this->length_units != '' ? ((int) $this->length_units) : 'null');
1637 $sql .= ", width= ".($this->width != '' ? "'".$this->db->escape($this->width)."'" : 'null');
1638 $sql .= ", width_units = ".((string) $this->width_units != '' ? ((int) $this->width_units) : 'null');
1639 $sql .= ", height = ".($this->height != '' ? "'".$this->db->escape($this->height)."'" : 'null');
1640 $sql .= ", height_units = ".((string) $this->height_units != '' ? ((int) $this->height_units) : 'null');
1641 $sql .= ", surface = ".($this->surface != '' ? "'".$this->db->escape($this->surface)."'" : 'null');
1642 $sql .= ", surface_units = ".((string) $this->surface_units != '' ? ((int) $this->surface_units) : 'null');
1643 $sql .= ", volume = ".($this->volume != '' ? "'".$this->db->escape($this->volume)."'" : 'null');
1644 $sql .= ", volume_units = ".((string) $this->volume_units != '' ? ((int) $this->volume_units) : 'null');
1645 $sql .= ", fk_default_warehouse = ".($this->fk_default_warehouse > 0 ? ((int) $this->fk_default_warehouse) : 'null');
1646 $sql .= ", fk_default_workstation = ".($this->fk_default_workstation > 0 ? ((int) $this->fk_default_workstation) : 'null');
1647 $sql .= ", seuil_stock_alerte = ".((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (float) $this->seuil_stock_alerte : 'null');
1648 $sql .= ", description = '".$this->db->escape($this->description)."'";
1649 $sql .= ", url = ".($this->url ? "'".$this->db->escape($this->url)."'" : 'null');
1650 $sql .= ", customcode = '".$this->db->escape($this->customcode)."'";
1651 $sql .= ", fk_country = ".($this->country_id > 0 ? (int) $this->country_id : 'null');
1652 $sql .= ", fk_state = ".($this->state_id > 0 ? (int) $this->state_id : 'null');
1653 $sql .= ", lifetime = ".($this->lifetime > 0 ? (int) $this->lifetime : 'null');
1654 $sql .= ", qc_frequency = ".($this->qc_frequency > 0 ? (int) $this->qc_frequency : 'null');
1655 $sql .= ", note = ".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : 'null');
1656 $sql .= ", note_public = ".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : 'null');
1657 $sql .= ", duration = '".$this->db->escape($this->duration_value.$this->duration_unit)."'";
1658 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1659 $sql .= ", accountancy_code_buy = '" . $this->db->escape($this->accountancy_code_buy) . "'";
1660 $sql .= ", accountancy_code_buy_intra = '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1661 $sql .= ", accountancy_code_buy_export = '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1662 $sql .= ", accountancy_code_sell= '" . $this->db->escape($this->accountancy_code_sell) . "'";
1663 $sql .= ", accountancy_code_sell_intra= '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1664 $sql .= ", accountancy_code_sell_export= '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1665 }
1666 $sql .= ", desiredstock = ".((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (float) $this->desiredstock : "null");
1667 $sql .= ", cost_price = ".($this->cost_price != '' ? ((float) $this->cost_price) : 'null');
1668 $sql .= ", fk_unit= ".(!$this->fk_unit ? 'NULL' : (int) $this->fk_unit);
1669 $sql .= ", price_autogen = ".(!$this->price_autogen ? 0 : 1);
1670 $sql .= ", fk_price_expression = ".($this->fk_price_expression != 0 ? (int) $this->fk_price_expression : 'NULL');
1671 $sql .= ", fk_user_modif = ".($user->id > 0 ? (int) $user->id : 'NULL');
1672 $sql .= ", mandatory_period = ".((int) $this->mandatory_period);
1673 $sql .= ", stockable_product = ".(int) $this->stockable_product;
1674 if (getDolGlobalString('PRODUCT_USE_CUSTOMER_PACKAGING')) {
1675 $sql .= ", packaging = ".(float) $this->packaging;
1676 }
1677
1678 // stock field is not here because it is a denormalized value from product_stock.
1679 $sql .= " WHERE rowid = ".((int) $id);
1680
1681 dol_syslog(get_class($this)."::update", LOG_DEBUG);
1682
1683 $resql = $this->db->query($sql);
1684 if ($resql) {
1685 $this->id = $id;
1686
1687 // Multilangs
1688 if (getDolGlobalInt('MAIN_MULTILANGS')) {
1689 if ($this->setMultiLangs($user, $notrigger) < 0) {
1690 $this->db->rollback();
1691 return -2;
1692 }
1693 }
1694
1695 $action = 'update';
1696
1697 // update accountancy for this entity
1698 if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1699 $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1700
1701 $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1702 $sql .= " fk_product";
1703 $sql .= ", entity";
1704 $sql .= ", accountancy_code_buy";
1705 $sql .= ", accountancy_code_buy_intra";
1706 $sql .= ", accountancy_code_buy_export";
1707 $sql .= ", accountancy_code_sell";
1708 $sql .= ", accountancy_code_sell_intra";
1709 $sql .= ", accountancy_code_sell_export";
1710 $sql .= ") VALUES (";
1711 $sql .= ((int) $this->id);
1712 $sql .= ", " . ((int) $conf->entity);
1713 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1714 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1715 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1716 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1717 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1718 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1719 $sql .= ")";
1720 $result = $this->db->query($sql);
1721 if (!$result) {
1722 $error++;
1723 $this->error = 'ErrorFailedToUpdateAccountancyForEntity';
1724 }
1725 }
1726
1727 if (!$this->hasbatch() && $this->oldcopy->hasbatch()) {
1728 // Selection of all product stock movements that contains batchs
1729 $sql = 'SELECT pb.qty, ps.fk_entrepot, pb.batch FROM '.MAIN_DB_PREFIX.'product_batch as pb';
1730 $sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'product_stock as ps ON (ps.rowid = pb.fk_product_stock)';
1731 $sql .= ' WHERE ps.fk_product = '.(int) $this->id;
1732
1733 $resql = $this->db->query($sql);
1734 if ($resql) {
1735 $inventorycode = dol_print_date(dol_now(), '%Y%m%d%H%M%S');
1736
1737 while ($obj = $this->db->fetch_object($resql)) {
1738 $value = $obj->qty;
1739 $fk_entrepot = $obj->fk_entrepot;
1740 $price = 0;
1741 $dlc = '';
1742 $dluo = '';
1743 $batch = $obj->batch;
1744
1745 // To know how to revert stockMouvement (add or remove)
1746 $addOremove = $value > 0 ? 1 : 0; // 1 if remove, 0 if add
1747 $label = $langs->trans('BatchStockMouvementAddInGlobal');
1748 $res = $this->correct_stock_batch($user, $fk_entrepot, abs($value), $addOremove, $label, $price, $dlc, $dluo, $batch, $inventorycode, '', null, 0, null, true);
1749
1750 if ($res > 0) {
1751 $label = $langs->trans('BatchStockMouvementAddInGlobal');
1752 $res = $this->correct_stock($user, $fk_entrepot, abs($value), (int) empty($addOremove), $label, $price, $inventorycode, '', null, 0);
1753 if ($res < 0) {
1754 $error++;
1755 }
1756 } else {
1757 $error++;
1758 }
1759 }
1760 }
1761 }
1762
1763 // Actions on extra fields
1764 if (!$error) {
1765 $result = $this->insertExtraFields();
1766 if ($result < 0) {
1767 $error++;
1768 }
1769 }
1770
1771 if (!$error && !$notrigger) {
1772 // Call trigger
1773 $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1774 if ($result < 0) {
1775 $error++;
1776 }
1777 // End call triggers
1778 }
1779
1780 if (!$error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref)) {
1781 // We remove directory
1782 if ($conf->product->dir_output) {
1783 $olddir = $conf->product->dir_output."/".dol_sanitizeFileName($this->oldcopy->ref);
1784 $newdir = $conf->product->dir_output."/".dol_sanitizeFileName($this->ref);
1785 if (file_exists($olddir)) {
1786 // include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1787 // $res = dol_move($olddir, $newdir);
1788 // do not use dol_move with directory
1789 $res = @rename($olddir, $newdir);
1790 if (!$res) {
1791 $langs->load("errors");
1792 $this->error = $langs->trans('ErrorFailToRenameDir', $olddir, $newdir);
1793 $error++;
1794 } else {
1795 // to keep old entries with the new dir
1796 require_once DOL_DOCUMENT_ROOT . '/ecm/class/ecmfiles.class.php';
1797 $ecmfiles = new EcmFiles($this->db);
1798 $ecmfiles->updateAfterRename("produit/".dol_sanitizeFileName($this->oldcopy->ref), "produit/".dol_sanitizeFileName($this->ref));
1799 }
1800 }
1801 }
1802 }
1803
1804 if (!$error) {
1805 if (isModEnabled('variants')) {
1806 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1807
1808 $comb = new ProductCombination($this->db);
1809
1810 foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) {
1811 $currcomb->updateProperties($this, $user);
1812 }
1813 }
1814
1815 $this->db->commit();
1816 return 1;
1817 } else {
1818 $this->db->rollback();
1819 return -$error;
1820 }
1821 } else {
1822 if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1823 $langs->load("errors");
1824 if (empty($conf->barcode->enabled) || empty($this->barcode)) {
1825 $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductAlreadyExists", $this->ref);
1826 } else {
1827 $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductBarCodeAlreadyExists", $this->barcode);
1828 }
1829 $this->errors[] = $this->error;
1830 $this->db->rollback();
1831 return -1;
1832 } else {
1833 $this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1834 $this->errors[] = $this->error;
1835 $this->db->rollback();
1836 return -2;
1837 }
1838 }
1839 } else {
1840 $this->db->rollback();
1841 dol_syslog(get_class($this)."::Update fails verify ".implode(',', $this->errors), LOG_WARNING);
1842 return -3;
1843 }
1844 }
1845
1853 public function delete(User $user, $notrigger = 0)
1854 {
1855 global $conf;
1856 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1857
1858 $error = 0;
1859
1860 // Check parameters
1861 if (empty($this->id)) {
1862 $this->error = "Object must be fetched before calling delete";
1863 return -1;
1864 }
1865 if (($this->isProduct() && !$user->hasRight('produit', 'supprimer')) || ($this->isService() && !$user->hasRight('service', 'supprimer'))) {
1866 $this->error = "ErrorForbidden";
1867 return 0;
1868 }
1869
1870 $objectisused = $this->isObjectUsed($this->id);
1871 if (empty($objectisused)) {
1872 $this->db->begin();
1873
1874 if (empty($notrigger)) {
1875 // Call trigger
1876 $result = $this->call_trigger('PRODUCT_DELETE', $user);
1877 if ($result < 0) {
1878 $error++;
1879 }
1880 // End call triggers
1881 }
1882
1883 // Delete from product_batch on product delete
1884 if (!$error) {
1885 $sql = "DELETE FROM ".$this->db->prefix().'product_batch';
1886 $sql .= " WHERE fk_product_stock IN (";
1887 $sql .= "SELECT rowid FROM ".$this->db->prefix().'product_stock';
1888 $sql .= " WHERE fk_product = ".((int) $this->id).")";
1889
1890 $result = $this->db->query($sql);
1891 if (!$result) {
1892 $error++;
1893 $this->errors[] = $this->db->lasterror();
1894 }
1895 }
1896
1897 // Delete pricelogs so we don't have any orphan rows, see #38383
1898 if (!$error) {
1899 $sql = "DELETE FROM " . $this->db->prefix() . "product_fournisseur_price_log";
1900 $sql .= " WHERE fk_product_fournisseur IN (";
1901 $sql .= " SELECT rowid FROM " . $this->db->prefix() . "product_fournisseur_price";
1902 $sql .= " WHERE fk_product = " . ((int) $this->id);
1903 $sql .= " )";
1904
1905 $resql = $this->db->query($sql);
1906 if (!$resql) {
1907 $error++;
1908 $this->errors[] = $this->db->lasterror();
1909 }
1910 }
1911
1912 // Delete all child tables
1913 if (!$error) {
1914 $elements = array('product_fournisseur_price', 'product_price', 'product_lang', 'categorie_product', 'product_stock', 'product_customer_price', 'product_lot'); // product_batch is done before
1915 foreach ($elements as $table) {
1916 if (!$error) {
1917 $sql = "DELETE FROM ".$this->db->prefix().$table;
1918 $sql .= " WHERE fk_product = ".(int) $this->id;
1919
1920 $result = $this->db->query($sql);
1921 if (!$result) {
1922 $error++;
1923 $this->errors[] = $this->db->lasterror();
1924 }
1925 }
1926 }
1927 }
1928
1929 if (!$error && isModEnabled('variants')) {
1930 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1931 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
1932
1933 // If it is a parent product, then we remove the association with child products
1934 $prodcomb = new ProductCombination($this->db);
1935
1936 if ($prodcomb->deleteByFkProductParent($user, $this->id) < 0) {
1937 $error++;
1938 $this->errors[] = 'Error deleting combinations';
1939 }
1940
1941 // We also check if it is a child product
1942 if (!$error && ($prodcomb->fetchByFkProductChild($this->id) > 0) && ($prodcomb->delete($user) < 0)) {
1943 $error++;
1944 $this->errors[] = 'Error deleting child combination';
1945 }
1946 }
1947
1948 // Delete from product_association
1949 if (!$error) {
1950 $sql = "DELETE FROM ".$this->db->prefix()."product_association";
1951 $sql .= " WHERE fk_product_pere = ".(int) $this->id." OR fk_product_fils = ".(int) $this->id;
1952
1953 $result = $this->db->query($sql);
1954 if (!$result) {
1955 $error++;
1956 $this->errors[] = $this->db->lasterror();
1957 }
1958 }
1959
1960 // Remove extrafields
1961 if (!$error) {
1962 $result = $this->deleteExtraFields();
1963 if ($result < 0) {
1964 $error++;
1965 dol_syslog(get_class($this)."::delete error -4 ".$this->error, LOG_ERR);
1966 }
1967 }
1968
1969 // Delete product
1970 if (!$error) {
1971 $sqlz = "DELETE FROM ".$this->db->prefix()."product";
1972 $sqlz .= " WHERE rowid = ".(int) $this->id;
1973
1974 $resultz = $this->db->query($sqlz);
1975 if (!$resultz) {
1976 $error++;
1977 $this->errors[] = $this->db->lasterror();
1978 }
1979 }
1980
1981 // Delete record into ECM index and physically
1982 if (!$error) {
1983 $res = $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
1984 $res = $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
1985 if (!$res) {
1986 $error++;
1987 }
1988 }
1989
1990 if (!$error) {
1991 // We remove directory
1992 $ref = dol_sanitizeFileName($this->ref);
1993 if ($conf->product->dir_output) {
1994 $dir = $conf->product->dir_output."/".$ref;
1995 if (file_exists($dir)) {
1996 $res = @dol_delete_dir_recursive($dir);
1997 if (!$res) {
1998 $this->errors[] = 'ErrorFailToDeleteDir';
1999 $error++;
2000 }
2001 }
2002 }
2003 }
2004
2005 if (!$error) {
2006 $this->db->commit();
2007 return 1;
2008 } else {
2009 foreach ($this->errors as $errmsg) {
2010 dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
2011 $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
2012 }
2013 $this->db->rollback();
2014 return -$error;
2015 }
2016 } else {
2017 $this->error = "ErrorRecordIsUsedCantDelete";
2018 return 0;
2019 }
2020 }
2021
2027 public static function getSellOrEatByMandatoryList()
2028 {
2029 global $langs;
2030
2031 $sellByLabel = $langs->trans('SellByDate');
2032 $eatByLabel = $langs->trans('EatByDate');
2033 return array(
2034 self::SELL_OR_EAT_BY_MANDATORY_ID_NONE => $langs->trans('BatchSellOrEatByMandatoryNone'),
2035 self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY => $sellByLabel,
2036 self::SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY => $eatByLabel,
2037 self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT => $langs->trans('BatchSellOrEatByMandatoryAll', $sellByLabel, $eatByLabel),
2038 );
2039 }
2040
2047 {
2048 $sellOrEatByMandatoryLabel = '';
2049
2050 $sellOrEatByMandatoryList = self::getSellOrEatByMandatoryList();
2051 if (isset($sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory])) {
2052 $sellOrEatByMandatoryLabel = $sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory];
2053 }
2054
2055 return $sellOrEatByMandatoryLabel;
2056 }
2057
2065 public function setMultiLangs($user, $notrigger = 0)
2066 {
2067 global $langs;
2068
2069 $langs_available = $langs->get_available_languages(DOL_DOCUMENT_ROOT, 0, 2);
2070 $current_lang = $langs->getDefaultLang();
2071
2072 foreach ($langs_available as $key => $value) {
2073 if ($key == $current_lang) {
2074 $sql = "SELECT rowid";
2075 $sql .= " FROM ".$this->db->prefix()."product_lang";
2076 $sql .= " WHERE fk_product = ".((int) $this->id);
2077 $sql .= " AND lang = '".$this->db->escape($key)."'";
2078
2079 $result = $this->db->query($sql);
2080
2081 if ($this->db->num_rows($result)) { // if there is already a description line for this language
2082 $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
2083 $sql2 .= " SET ";
2084 $sql2 .= " label='".$this->db->escape($this->label)."',";
2085 $sql2 .= " description='".$this->db->escape($this->description)."'";
2086 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2087 $sql2 .= ", note='".$this->db->escape($this->other)."'";
2088 }
2089 $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
2090 } else {
2091 $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
2092 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2093 $sql2 .= ", note";
2094 }
2095 $sql2 .= ")";
2096 $sql2 .= " VALUES(".((int) $this->id).",'".$this->db->escape($key)."','".$this->db->escape($this->label)."',";
2097 $sql2 .= " '".$this->db->escape($this->description)."'";
2098 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2099 $sql2 .= ", '".$this->db->escape($this->other)."'";
2100 }
2101 $sql2 .= ")";
2102 }
2103 dol_syslog(get_class($this).'::setMultiLangs key = current_lang = '.$key);
2104 if (!$this->db->query($sql2)) {
2105 $this->error = $this->db->lasterror();
2106 return -1;
2107 }
2108 } elseif (isset($this->multilangs[$key])) {
2109 if (empty($this->multilangs[$key]["label"])) {
2110 $this->errors[] = $key . ' : ' . $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Label"));
2111 return -1;
2112 }
2113
2114 $sql = "SELECT rowid";
2115 $sql .= " FROM ".$this->db->prefix()."product_lang";
2116 $sql .= " WHERE fk_product = ".((int) $this->id);
2117 $sql .= " AND lang = '".$this->db->escape($key)."'";
2118
2119 $result = $this->db->query($sql);
2120
2121 if ($this->db->num_rows($result)) { // if there is already a description line for this language
2122 $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
2123 $sql2 .= " SET ";
2124 $sql2 .= " label = '".$this->db->escape($this->multilangs["$key"]["label"])."',";
2125 $sql2 .= " description = '".$this->db->escape($this->multilangs["$key"]["description"])."'";
2126 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2127 // @phan-suppress-next-line PhanTypeInvalidDimOffset
2128 $sql2 .= ", note = '".$this->db->escape($this->multilangs["$key"]["other"])."'";
2129 }
2130 $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
2131 } else {
2132 $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
2133 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2134 $sql2 .= ", note";
2135 }
2136 $sql2 .= ")";
2137 $sql2 .= " VALUES(".((int) $this->id).",'".$this->db->escape($key)."','".$this->db->escape($this->multilangs["$key"]["label"])."',";
2138 $sql2 .= " '".$this->db->escape($this->multilangs["$key"]["description"])."'";
2139 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2140 // @phan-suppress-next-line PhanTypeInvalidDimOffset
2141 $sql2 .= ", '".$this->db->escape($this->multilangs["$key"]["other"])."'";
2142 }
2143 $sql2 .= ")";
2144 }
2145
2146 // We do not save if main fields are empty
2147 if ($this->multilangs["$key"]["label"] || $this->multilangs["$key"]["description"]) {
2148 if (!$this->db->query($sql2)) {
2149 $this->error = $this->db->lasterror();
2150 return -1;
2151 }
2152 }
2153 } else {
2154 // language is not current language and we didn't provide a multilang description for this language
2155 }
2156 }
2157
2158 if (empty($notrigger)) {
2159 // Call trigger
2160 $result = $this->call_trigger('PRODUCT_SET_MULTILANGS', $user);
2161 if ($result < 0) {
2162 $this->error = $this->db->lasterror();
2163 return -1;
2164 }
2165 // End call triggers
2166 }
2167
2168 return 1;
2169 }
2170
2180 public function delMultiLangs($langtodelete, $user, $notrigger = 0)
2181 {
2182 $sql = "DELETE FROM ".$this->db->prefix()."product_lang";
2183 $sql .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($langtodelete)."'";
2184
2185 dol_syslog(get_class($this).'::delMultiLangs', LOG_DEBUG);
2186 $result = $this->db->query($sql);
2187 if ($result) {
2188 if (empty($notrigger)) {
2189 // Call trigger
2190 $result = $this->call_trigger('PRODUCT_DEL_MULTILANGS', $user);
2191 if ($result < 0) {
2192 $this->error = $this->db->lasterror();
2193 dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
2194 return -1;
2195 }
2196 // End call triggers
2197 }
2198 unset($this->multilangs[$langtodelete]);
2199 return 1;
2200 } else {
2201 $this->error = $this->db->lasterror();
2202 dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
2203 return -1;
2204 }
2205 }
2206
2215 public function setAccountancyCode($type, $value)
2216 {
2217 global $user;
2218
2219 $error = 0;
2220
2221 $this->db->begin();
2222
2223 if ($type == 'buy') {
2224 $field = 'accountancy_code_buy';
2225 } elseif ($type == 'buy_intra') {
2226 $field = 'accountancy_code_buy_intra';
2227 } elseif ($type == 'buy_export') {
2228 $field = 'accountancy_code_buy_export';
2229 } elseif ($type == 'sell') {
2230 $field = 'accountancy_code_sell';
2231 } elseif ($type == 'sell_intra') {
2232 $field = 'accountancy_code_sell_intra';
2233 } elseif ($type == 'sell_export') {
2234 $field = 'accountancy_code_sell_export';
2235 } else {
2236 return -1;
2237 }
2238
2239 $sql = "UPDATE ".$this->db->prefix().$this->table_element." SET ";
2240 $sql .= "$field = '".$this->db->escape($value)."'";
2241 $sql .= " WHERE rowid = ".((int) $this->id);
2242
2243 dol_syslog(__METHOD__, LOG_DEBUG);
2244 $resql = $this->db->query($sql);
2245
2246 if ($resql) {
2247 // Call trigger
2248 $result = $this->call_trigger('PRODUCT_MODIFY', $user);
2249 if ($result < 0) {
2250 $error++;
2251 }
2252 // End call triggers
2253
2254 if ($error) {
2255 $this->db->rollback();
2256 return -1;
2257 }
2258
2259 $this->$field = $value;
2260
2261 $this->db->commit();
2262 return 1;
2263 } else {
2264 $this->error = $this->db->lasterror();
2265 $this->db->rollback();
2266 return -1;
2267 }
2268 }
2269
2275 public function getMultiLangs()
2276 {
2277 global $langs;
2278
2279 $current_lang = $langs->getDefaultLang();
2280
2281 $sql = "SELECT lang, label, description, note as other";
2282 $sql .= " FROM ".$this->db->prefix()."product_lang";
2283 $sql .= " WHERE fk_product = ".((int) $this->id);
2284
2285 $result = $this->db->query($sql);
2286 if ($result) {
2287 while ($obj = $this->db->fetch_object($result)) {
2288 //print 'lang='.$obj->lang.' current='.$current_lang.'<br>';
2289 if ($obj->lang == $current_lang) { // si on a les traduct. dans la langue courante on les charge en infos principales.
2290 $this->label = $obj->label;
2291 $this->description = $obj->description;
2292 $this->other = $obj->other;
2293 }
2294 $this->multilangs[(string) $obj->lang]["label"] = $obj->label;
2295 $this->multilangs[(string) $obj->lang]["description"] = $obj->description;
2296 $this->multilangs[(string) $obj->lang]["other"] = $obj->other;
2297 }
2298 return 1;
2299 } else {
2300 $this->error = "Error: ".$this->db->lasterror()." - ".$sql;
2301 return -1;
2302 }
2303 }
2304
2311 private function getArrayForPriceCompare($level = 0)
2312 {
2313 $testExit = array('multiprices','multiprices_ttc','multiprices_base_type','multiprices_min','multiprices_min_ttc','multiprices_tva_tx','multiprices_recuperableonly');
2314
2315 foreach ($testExit as $field) {
2316 if (!isset($this->$field)) {
2317 return array();
2318 }
2319 $tmparray = $this->$field;
2320 if (!isset($tmparray[$level])) {
2321 return array();
2322 }
2323 }
2324
2325 $lastPrice = array(
2326 'level' => $level ? $level : 1,
2327 'multiprices' => (float) $this->multiprices[$level],
2328 'multiprices_ttc' => (float) $this->multiprices_ttc[$level],
2329 'multiprices_base_type' => $this->multiprices_base_type[$level],
2330 'multiprices_min' => (float) $this->multiprices_min[$level],
2331 'multiprices_min_ttc' => (float) $this->multiprices_min_ttc[$level],
2332 'multiprices_tva_tx' => (float) $this->multiprices_tva_tx[$level],
2333 'multiprices_recuperableonly' => (float) $this->multiprices_recuperableonly[$level],
2334 );
2335
2336 return $lastPrice;
2337 }
2338
2339
2340 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2348 private function _log_price($user, $level = 0)
2349 {
2350 // phpcs:enable
2351 global $conf;
2352
2353 $now = dol_now();
2354
2355 // Clean parameters
2356 if (empty($this->price_by_qty)) {
2357 $this->price_by_qty = 0;
2358 }
2359
2360 // Add new price
2361 $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,";
2362 $sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, price_min,price_min_ttc,price_by_qty,entity,fk_price_expression) ";
2363 $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).",";
2364 $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');
2365 $sql .= ")";
2366
2367 dol_syslog(get_class($this)."::_log_price", LOG_DEBUG);
2368 $resql = $this->db->query($sql);
2369 if (!$resql) {
2370 $this->error = $this->db->lasterror();
2371 dol_print_error($this->db);
2372 return -1;
2373 } else {
2374 return 1;
2375 }
2376 }
2377
2378
2379 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2387 public function log_price_delete($user, $rowid)
2388 {
2389 // phpcs:enable
2390 $sql = "DELETE FROM ".$this->db->prefix()."product_price_by_qty";
2391 $sql .= " WHERE fk_product_price = ".((int) $rowid);
2392 $resql = $this->db->query($sql);
2393
2394 $sql = "DELETE FROM ".$this->db->prefix()."product_price_extrafields";
2395 $sql .= " WHERE fk_object = ".((int) $rowid);
2396 $resql = $this->db->query($sql);
2397
2398 $sql = "DELETE FROM ".$this->db->prefix()."product_price";
2399 $sql .= " WHERE rowid = ".((int) $rowid);
2400 $resql = $this->db->query($sql);
2401 if ($resql) {
2402 return 1;
2403 } else {
2404 $this->error = $this->db->lasterror();
2405 return -1;
2406 }
2407 }
2408
2417 public function fetchAllPriceLogs($id)
2418 {
2419 dol_syslog(__METHOD__, LOG_DEBUG);
2420
2421 $logs = array();
2422
2423 $sql = "SELECT";
2424 $sql .= " pl.rowid as log_rowid,";
2425 $sql .= " pl.datec,";
2426 $sql .= " pl.price,";
2427 $sql .= " pl.quantity,";
2428 $sql .= " pl.fk_user,";
2429 $sql .= " pl.multicurrency_code,";
2430 $sql .= " pl.multicurrency_price,";
2431 // Supplier info
2432 $sql .= " pp.fk_soc as supplier_id,";
2433 $sql .= " pp.ref_fourn as supplier_ref,";
2434 $sql .= " pp.entity as entity";
2435
2436 $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price_log as pl";
2437 $sql .= " INNER JOIN " . $this->db->prefix() . "product_fournisseur_price as pp ON pl.fk_product_fournisseur = pp.rowid";
2438
2439 $sql .= " WHERE pp.fk_product = " . ((int) $id);
2440 $sql .= " AND pp.entity IN (" . getEntity('product') . ")";
2441 $sql .= " ORDER BY pl.datec DESC";
2442
2443 $resql = $this->db->query($sql);
2444 if ($resql) {
2445 while ($obj = $this->db->fetch_object($resql)) {
2446 if (is_string($obj->datec)) {
2447 $obj->datec = strtotime($obj->datec);
2448 }
2449 $logs[] = $obj;
2450 }
2451 $this->db->free($resql);
2452 } else {
2453 $this->error = $this->db->lasterror();
2454 }
2455
2456 return $logs;
2457 }
2458
2468 public function getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp = 0)
2469 {
2470 global $hookmanager, $action;
2471
2472 // Call hook if any
2473 if (is_object($hookmanager)) {
2474 $parameters = array('thirdparty_seller' => $thirdparty_seller, 'thirdparty_buyer' => $thirdparty_buyer, 'pqp' => $pqp);
2475 // Note that $action and $object may have been modified by some hooks
2476 $reshook = $hookmanager->executeHooks('getSellPrice', $parameters, $this, $action);
2477 if ($reshook > 0) {
2478 return $hookmanager->resArray;
2479 }
2480 }
2481
2482 // Update if prices fields are defined
2483 $tva_tx = get_default_tva($thirdparty_seller, $thirdparty_buyer, $this->id);
2484 $tva_npr = get_default_npr($thirdparty_seller, $thirdparty_buyer, $this->id);
2485 if (empty($tva_tx)) {
2486 $tva_npr = 0;
2487 }
2488
2489 $pu_ht = $this->price;
2490 $pu_ttc = $this->price_ttc;
2491 $price_min = $this->price_min;
2492 $price_min_ttc = $this->price_min_ttc;
2493 $price_base_type = (empty($this->price_base_type) ? 'HT' : $this->price_base_type);
2494
2495 // if price by customer / level
2496 if (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) {
2497 require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
2498
2499 $prodcustprice = new ProductCustomerPrice($this->db);
2500
2501 $filter = array('t.fk_product' => (string) $this->id, 't.fk_soc' => (string) $thirdparty_buyer->id);
2502
2503 // If a price per customer exist
2504 $pricebycustomerexist = false;
2505 $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
2506 if ($result) {
2507 if (count($prodcustprice->lines) > 0) {
2508 $date_now = (int) floor(dol_now() / 86400) * 86400; // date without hours
2509 foreach ($prodcustprice->lines as $k => $custprice_line) {
2510 if ($custprice_line->date_begin <= $date_now && (empty($custprice_line->date_end) || $date_now <= $custprice_line->date_end)) {
2511 $pricebycustomerexist = true;
2512 $pu_ht = price($custprice_line->price);
2513 $price_min = price($custprice_line->price_min);
2514 $price_min_ttc = price($custprice_line->price_min_ttc);
2515 $pu_ttc = price($custprice_line->price_ttc);
2516 $price_base_type = $custprice_line->price_base_type;
2517 $tva_tx = $custprice_line->tva_tx;
2518 if ($custprice_line->default_vat_code && !preg_match('/\‍(.*\‍)/', $tva_tx)) {
2519 $tva_tx .= ' (' . $custprice_line->default_vat_code . ')';
2520 }
2521 $tva_npr = $custprice_line->recuperableonly;
2522 if (empty($tva_tx)) {
2523 $tva_npr = 0;
2524 }
2525 break;
2526 }
2527 }
2528 }
2529 }
2530
2531 if (!$pricebycustomerexist && !empty($thirdparty_buyer->price_level)) {
2532 $pu_ht = isset($this->multiprices[$thirdparty_buyer->price_level]) ? $this->multiprices[$thirdparty_buyer->price_level] : 0;
2533 $pu_ttc = isset($this->multiprices_ttc[$thirdparty_buyer->price_level]) ? $this->multiprices_ttc[$thirdparty_buyer->price_level] : 0;
2534 $price_min = isset($this->multiprices_min[$thirdparty_buyer->price_level]) ? $this->multiprices_min[$thirdparty_buyer->price_level] : 0;
2535 $price_min_ttc = isset($this->multiprices_min_ttc[$thirdparty_buyer->price_level]) ? $this->multiprices_min_ttc[$thirdparty_buyer->price_level] : 0;
2536 $price_base_type = isset($this->multiprices_base_type[$thirdparty_buyer->price_level]) ? $this->multiprices_base_type[$thirdparty_buyer->price_level] : 'HT';
2537 if (getDolGlobalString('PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL')) {
2538 // using this option is a bug. kept for backward compatibility
2539 if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
2540 $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
2541 }
2542 if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
2543 $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
2544 }
2545 if (empty($tva_tx)) {
2546 $tva_npr = 0;
2547 }
2548 }
2549 }
2550 } elseif (getDolGlobalString('PRODUIT_MULTIPRICES') && !empty($thirdparty_buyer->price_level)) { // // If price per segment
2551 $pu_ht = isset($this->multiprices[$thirdparty_buyer->price_level]) ? $this->multiprices[$thirdparty_buyer->price_level] : 0;
2552 $pu_ttc = isset($this->multiprices_ttc[$thirdparty_buyer->price_level]) ? $this->multiprices_ttc[$thirdparty_buyer->price_level] : 0;
2553 $price_min = isset($this->multiprices_min[$thirdparty_buyer->price_level]) ? $this->multiprices_min[$thirdparty_buyer->price_level] : 0;
2554 $price_min_ttc = isset($this->multiprices_min_ttc[$thirdparty_buyer->price_level]) ? $this->multiprices_min_ttc[$thirdparty_buyer->price_level] : 0;
2555 $price_base_type = isset($this->multiprices_base_type[$thirdparty_buyer->price_level]) ? $this->multiprices_base_type[$thirdparty_buyer->price_level] : 'HT';
2556 if (getDolGlobalString('PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL')) {
2557 // using this option is a bug. kept for backward compatibility
2558 if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
2559 $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
2560 }
2561 if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
2562 $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
2563 }
2564 if (empty($tva_tx)) {
2565 $tva_npr = 0;
2566 }
2567 }
2568 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES')) {
2569 // If price per customer
2570 require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
2571
2572 $prodcustprice = new ProductCustomerPrice($this->db);
2573
2574 $filter = array('t.fk_product' => (string) $this->id, 't.fk_soc' => (string) $thirdparty_buyer->id);
2575
2576 $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
2577 if ($result) {
2578 if (count($prodcustprice->lines) > 0) {
2579 $date_now = (int) floor(dol_now() / 86400) * 86400; // date without hours
2580 foreach ($prodcustprice->lines as $k => $custprice_line) {
2581 if ($custprice_line->date_begin <= $date_now && (empty($custprice_line->date_end) || $date_now <= $custprice_line->date_end)) {
2582 $pu_ht = price($custprice_line->price);
2583 $price_min = price($custprice_line->price_min);
2584 $price_min_ttc = price($custprice_line->price_min_ttc);
2585 $pu_ttc = price($custprice_line->price_ttc);
2586 $price_base_type = $custprice_line->price_base_type;
2587 $tva_tx = $custprice_line->tva_tx;
2588 if ($custprice_line->default_vat_code && !preg_match('/\‍(.*\‍)/', $tva_tx)) {
2589 $tva_tx .= ' (' . $custprice_line->default_vat_code . ')';
2590 }
2591 $tva_npr = $custprice_line->recuperableonly;
2592 if (empty($tva_tx)) {
2593 $tva_npr = 0;
2594 }
2595 break;
2596 }
2597 }
2598 }
2599 }
2600 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY')) {
2601 // If price per quantity
2602 if (!empty($this->prices_by_qty[0])) {
2603 // yes, this product has some prices per quantity
2604 // Search price into product_price_by_qty from $this->id
2605 foreach ($this->prices_by_qty_list[0] as $priceforthequantityarray) {
2606 if ($priceforthequantityarray['rowid'] != $pqp) {
2607 continue;
2608 }
2609 // We found the price
2610 if ($priceforthequantityarray['price_base_type'] == 'HT') {
2611 $pu_ht = $priceforthequantityarray['unitprice'];
2612 } else {
2613 $pu_ttc = $priceforthequantityarray['unitprice'];
2614 }
2615 break;
2616 }
2617 }
2618 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) {
2619 // If price per quantity and customer
2620 if (!empty($this->prices_by_qty[$thirdparty_buyer->price_level])) {
2621 // yes, this product has some prices per quantity
2622 // Search price into product_price_by_qty from $this->id
2623 foreach ($this->prices_by_qty_list[$thirdparty_buyer->price_level] as $priceforthequantityarray) {
2624 if ($priceforthequantityarray['rowid'] != $pqp) {
2625 continue;
2626 }
2627 // We found the price
2628 if ($priceforthequantityarray['price_base_type'] == 'HT') {
2629 $pu_ht = $priceforthequantityarray['unitprice'];
2630 } else {
2631 $pu_ttc = $priceforthequantityarray['unitprice'];
2632 }
2633 break;
2634 }
2635 }
2636 }
2637
2638 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);
2639 }
2640
2641 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2655 public function get_buyprice($prodfournprice, $qty, $product_id = 0, $fourn_ref = '', $fk_soc = 0)
2656 {
2657 // phpcs:enable
2658 global $action, $hookmanager;
2659
2660 // Call hook if any
2661 if (is_object($hookmanager)) {
2662 $parameters = array(
2663 'prodfournprice' => $prodfournprice,
2664 'qty' => $qty,
2665 'product_id' => $product_id,
2666 'fourn_ref' => $fourn_ref,
2667 'fk_soc' => $fk_soc,
2668 );
2669 // Note that $action and $object may have been modified by some hooks
2670 $reshook = $hookmanager->executeHooks('getBuyPrice', $parameters, $this, $action);
2671 if ($reshook > 0) {
2672 return $hookmanager->resArray;
2673 }
2674 }
2675
2676 $result = 0;
2677
2678 // 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)
2679 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2680 $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,";
2681 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2682 $sql .= " pfp.packaging";
2683 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2684 $sql .= " WHERE pfp.rowid = ".((int) $prodfournprice);
2685 if ($qty > 0) {
2686 $sql .= " AND pfp.quantity <= ".((float) $qty);
2687 }
2688 $sql .= " ORDER BY pfp.quantity DESC";
2689
2690 dol_syslog(get_class($this)."::get_buyprice first search by prodfournprice/qty", LOG_DEBUG);
2691 $resql = $this->db->query($sql);
2692 if ($resql) {
2693 $obj = $this->db->fetch_object($resql);
2694 if ($obj && $obj->quantity > 0) { // If we found a supplier prices from the id of supplier price
2695 if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2696 $prod_supplier = new ProductFournisseur($this->db);
2697 $prod_supplier->product_fourn_price_id = $obj->rowid;
2698 $prod_supplier->id = $obj->fk_product;
2699 $prod_supplier->fourn_qty = $obj->quantity;
2700 $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2701 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2702
2703 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2704 $priceparser = new PriceParser($this->db);
2705 $price_result = $priceparser->parseProductSupplier($prod_supplier);
2706 if ($price_result >= 0) {
2707 $obj->price = $price_result;
2708 }
2709 }
2710 $this->product_fourn_price_id = $obj->rowid;
2711 $this->buyprice = $obj->price; // deprecated
2712 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product of supplier
2713 $this->fourn_price_base_type = 'HT'; // Price base type
2714 $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2715 $this->ref_fourn = $obj->ref_supplier; // deprecated
2716 $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2717 $this->desc_supplier = $obj->desc_supplier; // desc supplier
2718 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2719 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2720 $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2721 $this->fourn_multicurrency_price = $obj->multicurrency_price;
2722 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2723 $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2724 $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2725 $this->fourn_multicurrency_code = $obj->multicurrency_code;
2726 if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2727 $this->packaging = (float) $obj->packaging;
2728 }
2729 // Expose the supplier minimum purchase quantity so callers can enforce qty_min
2730 // on top of packaging-multiple rounding (#38783). pfp.quantity is the lowest
2731 // qty for which this price line is valid; for "below-min" callers this is the
2732 // floor they must round up to.
2733 $this->fourn_qty = $obj->quantity;
2734 $result = $obj->fk_product;
2735 return $result;
2736 } else { // If not found
2737 // 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.
2738 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2739 $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,";
2740 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2741 $sql .= " pfp.packaging";
2742 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2743 $sql .= " WHERE 1 = 1";
2744 if ($product_id > 0) {
2745 $sql .= " AND pfp.fk_product = ".((int) $product_id);
2746 }
2747 if ($fourn_ref != 'none') {
2748 $sql .= " AND pfp.ref_fourn = '".$this->db->escape($fourn_ref)."'";
2749 }
2750 if ($fk_soc > 0) {
2751 $sql .= " AND pfp.fk_soc = ".((int) $fk_soc);
2752 }
2753 if ($qty > 0) {
2754 $sql .= " AND pfp.quantity <= ".((float) $qty);
2755 }
2756 $sql .= " ORDER BY pfp.quantity DESC";
2757 $sql .= " LIMIT 1";
2758
2759 dol_syslog(get_class($this)."::get_buyprice second search from qty/ref/product_id", LOG_DEBUG);
2760 $resql = $this->db->query($sql);
2761 if ($resql) {
2762 $obj = $this->db->fetch_object($resql);
2763 if ($obj && $obj->quantity > 0) { // If found
2764 if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2765 $prod_supplier = new ProductFournisseur($this->db);
2766 $prod_supplier->product_fourn_price_id = $obj->rowid;
2767 $prod_supplier->id = $obj->fk_product;
2768 $prod_supplier->fourn_qty = $obj->quantity;
2769 $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2770 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2771
2772 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2773 $priceparser = new PriceParser($this->db);
2774 $price_result = $priceparser->parseProductSupplier($prod_supplier);
2775 if ($price_result >= 0) {
2776 $obj->price = $price_result;
2777 }
2778 }
2779 $this->product_fourn_price_id = $obj->rowid;
2780 $this->buyprice = $obj->price; // deprecated
2781 $this->fourn_qty = $obj->quantity; // min quantity for price for a virtual supplier
2782 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product for a virtual supplier
2783 $this->fourn_price_base_type = 'HT'; // Price base type for a virtual supplier
2784 $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2785 $this->ref_fourn = $obj->ref_supplier; // deprecated
2786 $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2787 $this->desc_supplier = $obj->desc_supplier; // desc supplier
2788 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2789 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2790 $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2791 $this->fourn_multicurrency_price = $obj->multicurrency_price;
2792 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2793 $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2794 $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2795 $this->fourn_multicurrency_code = $obj->multicurrency_code;
2796 if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2797 $this->packaging = (float) $obj->packaging;
2798 }
2799 // Expose pfp.quantity so callers can enforce qty_min on top of packaging
2800 // rounding (#38783).
2801 $this->fourn_qty = $obj->quantity;
2802 $result = $obj->fk_product;
2803 return $result;
2804 } else {
2805 return -1; // This product does not exist with this supplier price ID, or it exists but with insufficient quantity; nor does it exist for the product/supplier ref pair in the required quantity.
2806 }
2807 } else {
2808 $this->error = $this->db->lasterror();
2809 return -3;
2810 }
2811 }
2812 } else {
2813 $this->error = $this->db->lasterror();
2814 return -2;
2815 }
2816 }
2817
2818
2837 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)
2838 {
2839 $lastPriceData = $this->getArrayForPriceCompare($level); // temporary store current price before update
2840
2841 $id = $this->id;
2842
2843 dol_syslog(get_class($this)."::update_price id=".$id." newprice=".$newprice." newpricebase=".$newpricebase." newminprice=".$newminprice." level=".$level." npr=".$newnpr." newdefaultvatcode=".$newdefaultvatcode);
2844
2845 // Clean parameters
2846 if (empty($this->tva_tx)) {
2847 $this->tva_tx = 0.0;
2848 }
2849 if (empty($newnpr)) {
2850 $newnpr = 0.0;
2851 }
2852 if (empty($newminprice)) {
2853 $newminprice = 0.0;
2854 }
2855
2856 // Check parameters
2857 if ($newvat === null || $newvat == '') { // Maintain '' for backwards compatibility
2858 $newvat = (float) $this->tva_tx;
2859 }
2860
2861 $localtaxtype1 = '';
2862 $localtaxtype2 = '';
2863
2864 // If multiprices are enabled, then we check if the current product is subject to price autogeneration
2865 // Price will be modified ONLY when the first one is the one that is being modified
2866 if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && !$ignore_autogen && $this->price_autogen && ($level == 1)) {
2867 return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpbq);
2868 }
2869
2870 if (!empty($newminprice) && ($newminprice > $newprice)) {
2871 $this->error = 'ErrorPriceCantBeLowerThanMinPrice';
2872 return -1;
2873 }
2874
2875 if ($newprice === 0 || $newprice !== '') {
2876 if ($newpricebase == 'TTC') {
2877 $price_ttc = (float) price2num($newprice, 'MU');
2878 $price = (float) price2num($newprice) / (1 + ((float) $newvat / 100));
2879 $price = (float) price2num($price, 'MU');
2880
2881 if ((string) $newminprice != '0') {
2882 $price_min_ttc = (float) price2num($newminprice, 'MU');
2883 $price_min = (float) price2num($newminprice) / (1 + ($newvat / 100));
2884 $price_min = (float) price2num($price_min, 'MU');
2885 } else {
2886 $price_min = 0.0;
2887 $price_min_ttc = 0.0;
2888 }
2889 } else {
2890 $price = (float) price2num($newprice, 'MU');
2891 $price_ttc = ($newnpr != 1) ? (float) price2num($newprice) * (1 + ($newvat / 100)) : $price;
2892 $price_ttc = (float) price2num($price_ttc, 'MU');
2893
2894 if ((string) $newminprice != '0') {
2895 $price_min = (float) price2num($newminprice, 'MU');
2896 $price_min_ttc = (float) price2num($newminprice) * (1 + ($newvat / 100));
2897 $price_min_ttc = (float) price2num($price_min_ttc, 'MU');
2898 //print 'X'.$newminprice.'-'.$price_min;
2899 } else {
2900 $price_min = 0.0;
2901 $price_min_ttc = 0.0;
2902 }
2903 }
2904 //print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc;
2905 if (count($localtaxes_array) > 0) {
2906 $localtaxtype1 = $localtaxes_array['0'];
2907 $localtax1 = $localtaxes_array['1'];
2908 $localtaxtype2 = $localtaxes_array['2'];
2909 $localtax2 = $localtaxes_array['3'];
2910 } else {
2911 // if array empty, we try to use the vat code
2912 if (!empty($newdefaultvatcode)) {
2913 global $mysoc;
2914 // Get record from code
2915 $sql = "SELECT t.rowid, t.code, t.recuperableonly as tva_npr, t.localtax1, t.localtax2, t.localtax1_type, t.localtax2_type";
2916 $sql .= " FROM ".MAIN_DB_PREFIX."c_tva as t, ".MAIN_DB_PREFIX."c_country as c";
2917 $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '".$this->db->escape($mysoc->country_code)."'";
2918 $sql .= " AND t.taux = ".((float) $newdefaultvatcode)." AND t.active = 1";
2919 $sql .= " AND t.code = '".$this->db->escape($newdefaultvatcode)."'";
2920 $resql = $this->db->query($sql);
2921 if ($resql) {
2922 $obj = $this->db->fetch_object($resql);
2923 if ($obj) {
2924 $npr = $obj->tva_npr;
2925 $localtax1 = $obj->localtax1;
2926 $localtax2 = $obj->localtax2;
2927 $localtaxtype1 = $obj->localtax1_type;
2928 $localtaxtype2 = $obj->localtax2_type;
2929 }
2930 }
2931 } else {
2932 // old method. deprecated because we can't retrieve type
2933 $localtaxtype1 = '0';
2934 $localtax1 = get_localtax($newvat, 1);
2935 $localtaxtype2 = '0';
2936 $localtax2 = get_localtax($newvat, 2);
2937 }
2938 }
2939 if (empty($localtax1)) {
2940 $localtax1 = 0; // If = '' then = 0
2941 }
2942 if (empty($localtax2)) {
2943 $localtax2 = 0; // If = '' then = 0
2944 }
2945
2946 $this->db->begin();
2947
2948 // Don't put quotes here on decimal numbers.
2949 // This causes storage with base rounding instead of exact values.
2950 $sql = "UPDATE ".$this->db->prefix()."product SET";
2951 $sql .= " price_base_type = '".$this->db->escape($newpricebase)."',";
2952 $sql .= " price = ".(float) $price.",";
2953 $sql .= " price_ttc = ".(float) $price_ttc.",";
2954 $sql .= " price_min = ".(float) $price_min.",";
2955 $sql .= " price_min_ttc = ".(float) $price_min_ttc.",";
2956 $sql .= " localtax1_tx = ".($localtax1 >= 0 ? (float) $localtax1 : 'NULL').",";
2957 $sql .= " localtax2_tx = ".($localtax2 >= 0 ? (float) $localtax2 : 'NULL').",";
2958 $sql .= " localtax1_type = ".($localtaxtype1 != '' ? "'".$this->db->escape($localtaxtype1)."'" : "'0'").",";
2959 $sql .= " localtax2_type = ".($localtaxtype2 != '' ? "'".$this->db->escape($localtaxtype2)."'" : "'0'").",";
2960 $sql .= " default_vat_code = ".($newdefaultvatcode ? "'".$this->db->escape($newdefaultvatcode)."'" : "null").",";
2961 $sql .= " price_label = ".(!empty($price_label) ? "'".$this->db->escape($price_label)."'" : "null").",";
2962 $sql .= " tva_tx = ".(float) price2num($newvat).",";
2963 $sql .= " recuperableonly = '".$this->db->escape((string) $newnpr)."'";
2964 $sql .= " WHERE rowid = ".((int) $id);
2965
2966 dol_syslog(get_class($this)."::update_price", LOG_DEBUG);
2967 $resql = $this->db->query($sql);
2968 if ($resql) {
2969 $this->multiprices[$level] = $price;
2970 $this->multiprices_ttc[$level] = $price_ttc;
2971 $this->multiprices_min[$level] = $price_min;
2972 $this->multiprices_min_ttc[$level] = $price_min_ttc;
2973 $this->multiprices_base_type[$level] = $newpricebase;
2974 $this->multiprices_default_vat_code[$level] = $newdefaultvatcode;
2975 $this->multiprices_tva_tx[$level] = $newvat;
2976 $this->multiprices_recuperableonly[$level] = $newnpr;
2977
2978 $this->price = $price;
2979 $this->price_label = $price_label;
2980 $this->price_ttc = $price_ttc;
2981 $this->price_min = $price_min;
2982 $this->price_min_ttc = $price_min_ttc;
2983 $this->price_base_type = $newpricebase;
2984 $this->default_vat_code = $newdefaultvatcode;
2985 $this->tva_tx = $newvat;
2986 $this->tva_npr = $newnpr;
2987
2988 //Local taxes
2989 $this->localtax1_tx = $localtax1;
2990 $this->localtax2_tx = $localtax2;
2991 $this->localtax1_type = $localtaxtype1;
2992 $this->localtax2_type = $localtaxtype2;
2993
2994 // Price by quantity
2995 $this->price_by_qty = $newpbq;
2996
2997 // check if price have really change before log
2998 $newPriceData = $this->getArrayForPriceCompare($level);
2999 if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || (!getDolGlobalString('PRODUIT_MULTIPRICES') && !getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES'))) {
3000 $this->_log_price($user, $level); // Save price for level into table product_price
3001 }
3002
3003 $this->level = $level; // Store level of price edited for trigger
3004
3005 // Call trigger
3006 if (!$notrigger) {
3007 $result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user);
3008 if ($result < 0) {
3009 $this->db->rollback();
3010 return -1;
3011 }
3012 }
3013 // End call triggers
3014
3015 $this->db->commit();
3016 } else {
3017 $this->db->rollback();
3018 $this->error = $this->db->lasterror();
3019 return -1;
3020 }
3021 }
3022
3023 return 1;
3024 }
3025
3033 public function setPriceExpression($expression_id)
3034 {
3035 global $user;
3036
3037 $this->fk_price_expression = $expression_id;
3038
3039 return $this->update($this->id, $user);
3040 }
3041
3054 public function fetch($id = 0, $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0)
3055 {
3056 include_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
3057
3058 global $conf;
3059
3060 dol_syslog(get_class($this)."::fetch id=".$id." ref=".$ref." ref_ext=".$ref_ext);
3061
3062 // Check parameters
3063 if (!$id && !$ref && !$ref_ext && !$barcode) {
3064 $this->error = 'ErrorWrongParameters';
3065 dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR);
3066 return -1;
3067 }
3068
3069 $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,";
3070 $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,";
3071 $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,";
3072 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.last_main_doc,";
3073 $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,";
3074 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
3075 $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,";
3076 } else {
3077 $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,";
3078 }
3079
3080 // For MultiCompany
3081 // PMP per entity & Stocks Sharings stock_reel includes only stocks shared with this entity
3082 $separatedEntityPMP = false; // Set to true to get the AWP from table llx_product_perentity instead of field 'pmp' into llx_product.
3083 $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.
3084 $visibleWarehousesEntities = $conf->entity;
3085 if (getDolGlobalString('MULTICOMPANY_PRODUCT_SHARING_ENABLED')) {
3086 if (getDolGlobalString('MULTICOMPANY_PMP_PER_ENTITY_ENABLED')) {
3087 $checkPMPPerEntity = $this->db->query("SELECT pmp FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = ".((int) $id)." AND entity = ".(int) $conf->entity);
3088 if ($this->db->num_rows($checkPMPPerEntity) > 0) {
3089 $separatedEntityPMP = true;
3090 }
3091 }
3092 global $mc;
3093 $separatedStock = true;
3094 if (isset($mc->sharings['stock']) && !empty($mc->sharings['stock'])) {
3095 $visibleWarehousesEntities .= "," . implode(",", $mc->sharings['stock']);
3096 }
3097 }
3098 if ($separatedEntityPMP) {
3099 $sql .= " ppe.pmp,";
3100 } else {
3101 $sql .= " p.pmp,";
3102 }
3103 $sql .= " p.datec, GREATEST(p.tms, pef.tms) AS tms, p.import_key, p.entity, p.desiredstock, p.tobatch, p.sell_or_eat_by_mandatory, p.batch_mask, p.fk_unit,";
3104 $sql .= " p.fk_price_expression, p.price_autogen, p.stockable_product, p.model_pdf,";
3105 $sql .= " p.price_label,";
3106 if ($separatedStock) {
3107 $sql .= " SUM(sp.reel) as stock";
3108 } else {
3109 $sql .= " p.stock";
3110 }
3111 $sql .= " FROM ".$this->db->prefix()."product as p";
3112 $sql .= " LEFT JOIN ".$this->db->prefix()."product_extrafields as pef ON pef.fk_object=p.rowid";
3113 if (getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED') || $separatedEntityPMP) {
3114 $sql .= " LEFT JOIN " . $this->db->prefix() . "product_perentity as ppe ON ppe.fk_product = p.rowid AND ppe.entity = " . ((int) $conf->entity);
3115 }
3116 if ($separatedStock) {
3117 $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)."))";
3118 }
3119
3120 if ($id) {
3121 $sql .= " WHERE p.rowid = ".((int) $id);
3122 } else {
3123 $sql .= " WHERE p.entity IN (".getEntity($this->element).")";
3124 if ($ref) {
3125 $sql .= " AND p.ref = '".$this->db->escape($ref)."'";
3126 } elseif ($ref_ext) {
3127 $sql .= " AND p.ref_ext = '".$this->db->escape($ref_ext)."'";
3128 } elseif ($barcode) {
3129 $sql .= " AND p.barcode = '".$this->db->escape($barcode)."'";
3130 }
3131 }
3132 if ($separatedStock) {
3133 $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,";
3134 $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,";
3135 $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,";
3136 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
3137 $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,";
3138 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
3139 $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,";
3140 } else {
3141 $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,";
3142 }
3143 if ($separatedEntityPMP) {
3144 $sql .= " ppe.pmp,";
3145 } else {
3146 $sql .= " p.pmp,";
3147 }
3148 $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,";
3149 $sql .= " p.fk_price_expression, p.price_autogen, p.stockable_product, p.model_pdf,";
3150 $sql .= " p.price_label";
3151 // can't be false TODO fix
3152 // if (!$separatedStock) {
3153 // $sql .= ", p.stock";
3154 // }
3155 }
3156
3157 $resql = $this->db->query($sql);
3158 if ($resql) {
3159 unset($this->oldcopy);
3160
3161 if ($this->db->num_rows($resql) > 0) {
3162 $obj = $this->db->fetch_object($resql);
3163
3164 $this->id = $obj->rowid;
3165 $this->ref = $obj->ref;
3166 $this->ref_ext = $obj->ref_ext;
3167 $this->label = $obj->label;
3168 $this->description = $obj->description;
3169 $this->url = $obj->url;
3170 $this->note_public = $obj->note_public;
3171 $this->note_private = $obj->note_private;
3172 $this->note = $obj->note_private; // deprecated
3173
3174 $this->type = $obj->fk_product_type;
3175 $this->price_label = $obj->price_label;
3176 $this->status = $obj->tosell;
3177 $this->status_buy = $obj->tobuy;
3178 $this->status_batch = $obj->tobatch;
3179 $this->sell_or_eat_by_mandatory = $obj->sell_or_eat_by_mandatory;
3180 $this->batch_mask = $obj->batch_mask;
3181
3182 $this->customcode = $obj->customcode;
3183 $this->country_id = $obj->fk_country;
3184 $this->country_code = getCountry($this->country_id, '2', $this->db);
3185 $this->state_id = $obj->fk_state;
3186 $this->lifetime = $obj->lifetime;
3187 $this->qc_frequency = $obj->qc_frequency;
3188 $this->price = $obj->price;
3189 $this->price_ttc = $obj->price_ttc;
3190 $this->price_min = $obj->price_min;
3191 $this->price_min_ttc = $obj->price_min_ttc;
3192 $this->price_base_type = (empty($obj->price_base_type) ? 'HT' : $obj->price_base_type);
3193 $this->cost_price = isset($obj->cost_price) ? (float) $obj->cost_price : null;
3194 $this->default_vat_code = $obj->default_vat_code;
3195 $this->tva_tx = $obj->tva_tx;
3197 $this->tva_npr = $obj->tva_npr;
3199 $this->localtax1_tx = $obj->localtax1_tx;
3200 $this->localtax2_tx = $obj->localtax2_tx;
3201 $this->localtax1_type = $obj->localtax1_type;
3202 $this->localtax2_type = $obj->localtax2_type;
3203
3204 $this->finished = $obj->finished;
3205 $this->fk_default_bom = $obj->fk_default_bom;
3206
3207 $this->duration = $obj->duration;
3208 $matches = [];
3209 preg_match('/([\d.]+)(\w+)/', $obj->duration, $matches);
3210 $this->duration_value = !empty($matches[1]) ? (float) $matches[1] : 0;
3211 $this->duration_unit = !empty($matches[2]) ? (string) $matches[2] : null;
3212 $this->canvas = $obj->canvas;
3213 $this->net_measure = $obj->net_measure;
3214 $this->net_measure_units = $obj->net_measure_units;
3215 $this->weight = $obj->weight;
3216 $this->weight_units = (is_null($obj->weight_units) ? 0 : $obj->weight_units);
3217 $this->length = $obj->length;
3218 $this->length_units = (is_null($obj->length_units) ? 0 : $obj->length_units);
3219 $this->width = $obj->width;
3220 $this->width_units = (is_null($obj->width_units) ? 0 : $obj->width_units);
3221 $this->height = $obj->height;
3222 $this->height_units = (is_null($obj->height_units) ? 0 : $obj->height_units);
3223
3224 $this->surface = $obj->surface;
3225 $this->surface_units = (is_null($obj->surface_units) ? 0 : $obj->surface_units);
3226 $this->volume = $obj->volume;
3227 $this->volume_units = (is_null($obj->volume_units) ? 0 : $obj->volume_units);
3228 $this->barcode = $obj->barcode;
3229 $this->barcode_type = $obj->fk_barcode_type;
3230
3231 $this->accountancy_code_buy = $obj->accountancy_code_buy;
3232 $this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra;
3233 $this->accountancy_code_buy_export = $obj->accountancy_code_buy_export;
3234 $this->accountancy_code_sell = $obj->accountancy_code_sell;
3235 $this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra;
3236 $this->accountancy_code_sell_export = $obj->accountancy_code_sell_export;
3237
3238 $this->fk_default_warehouse = $obj->fk_default_warehouse;
3239 $this->fk_default_workstation = $obj->fk_default_workstation;
3240 $this->seuil_stock_alerte = $obj->seuil_stock_alerte;
3241 $this->desiredstock = $obj->desiredstock;
3242 $this->stock_reel = $obj->stock;
3243 $this->stockable_product = $obj->stockable_product;
3244 $this->pmp = $obj->pmp;
3245
3246 $this->date_creation = $this->db->jdate($obj->datec);
3247 $this->date_modification = $this->db->jdate($obj->tms);
3248
3249 $this->import_key = $obj->import_key;
3250 $this->entity = $obj->entity;
3251
3252 $this->ref_ext = $obj->ref_ext;
3253 $this->fk_price_expression = $obj->fk_price_expression;
3254 $this->fk_unit = $obj->fk_unit;
3255 $this->price_autogen = $obj->price_autogen;
3256 $this->model_pdf = $obj->model_pdf;
3257 $this->last_main_doc = $obj->last_main_doc;
3258
3259 $this->mandatory_period = $obj->mandatory_period;
3260
3261 if (getDolGlobalString('PRODUCT_USE_CUSTOMER_PACKAGING')) {
3262 $this->packaging = (float) $obj->packaging;
3263 }
3264
3265 $this->db->free($resql);
3266
3267 // fetch optionals attributes and labels
3268 $this->fetch_optionals();
3269
3270 // Multilangs
3271 if (getDolGlobalInt('MAIN_MULTILANGS') && empty($ignore_lang_load)) {
3272 $this->getMultiLangs();
3273 }
3274
3275 // Load multiprices array
3276 if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && empty($ignore_price_load)) { // prices per segment
3277 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
3278 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
3279 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3280 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
3281 $sql .= " ,price_label";
3282 $sql .= " FROM ".$this->db->prefix()."product_price";
3283 $sql .= " WHERE entity IN (".getEntity('productprice').")";
3284 $sql .= " AND price_level=".((int) $i);
3285 $sql .= " AND fk_product = ".((int) $this->id);
3286 $sql .= " ORDER BY date_price DESC, rowid DESC"; // Get the most recent line
3287 $sql .= " LIMIT 1"; // Only the first one
3288 $resql = $this->db->query($sql);
3289 if ($resql) {
3290 $result = $this->db->fetch_array($resql);
3291
3292 $this->multiprices[$i] = $result ? $result["price"] : null;
3293 $this->multiprices_ttc[$i] = $result ? $result["price_ttc"] : null;
3294 $this->multiprices_min[$i] = $result ? $result["price_min"] : null;
3295 $this->multiprices_min_ttc[$i] = $result ? $result["price_min_ttc"] : null;
3296 $this->multiprices_base_type[$i] = $result ? $result["price_base_type"] : null;
3297 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
3298 $this->multiprices_tva_tx[$i] = $result ? $result["tva_tx"].(!empty($result['default_vat_code']) ? ' ('.$result['default_vat_code'].')' : '') : null;
3299 $this->multiprices_recuperableonly[$i] = $result ? $result["recuperableonly"] : null;
3300
3301 // Price by quantity
3302 /*
3303 $this->prices_by_qty[$i]=$result["price_by_qty"];
3304 $this->prices_by_qty_id[$i]=$result["rowid"];
3305 // Récuperation de la liste des prix selon qty si flag positionné
3306 if ($this->prices_by_qty[$i] == 1)
3307 {
3308 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
3309 $sql.= " FROM ".$this->db->prefix()."product_price_by_qty";
3310 $sql.= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
3311 $sql.= " ORDER BY quantity ASC";
3312
3313 $resql = $this->db->query($sql);
3314 if ($resql)
3315 {
3316 $resultat=array();
3317 $ii=0;
3318 while ($result= $this->db->fetch_array($resql)) {
3319 $resultat[$ii]=array();
3320 $resultat[$ii]["rowid"]=$result["rowid"];
3321 $resultat[$ii]["price"]= $result["price"];
3322 $resultat[$ii]["unitprice"]= $result["unitprice"];
3323 $resultat[$ii]["quantity"]= $result["quantity"];
3324 $resultat[$ii]["remise_percent"]= $result["remise_percent"];
3325 $resultat[$ii]["remise"]= $result["remise"]; // deprecated
3326 $resultat[$ii]["price_base_type"]= $result["price_base_type"];
3327 $ii++;
3328 }
3329 $this->prices_by_qty_list[$i]=$resultat;
3330 }
3331 else
3332 {
3333 dol_print_error($this->db);
3334 return -1;
3335 }
3336 }*/
3337 } else {
3338 $this->error = $this->db->lasterror;
3339 return -1;
3340 }
3341 }
3342 } elseif ((getDolGlobalString('PRODUIT_CUSTOMER_PRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && empty($ignore_price_load)) { // prices per customers
3343 // Nothing loaded by default. List may be very long.
3344 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY') && empty($ignore_price_load)) { // prices per quantity
3345 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3346 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid";
3347 $sql .= " FROM ".$this->db->prefix()."product_price";
3348 $sql .= " WHERE fk_product = ".((int) $this->id);
3349 $sql .= " ORDER BY date_price DESC, rowid DESC";
3350 $sql .= " LIMIT 1";
3351
3352 $resql = $this->db->query($sql);
3353 if ($resql) {
3354 $result = $this->db->fetch_array($resql);
3355
3356 if ($result) {
3357 // Price by quantity
3358 $this->prices_by_qty[0] = $result["price_by_qty"];
3359 $this->prices_by_qty_id[0] = $result["rowid"];
3360 // Récuperation de la liste des prix selon qty si flag positionné
3361 if ($this->prices_by_qty[0] == 1) {
3362 $sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise, remise, price_base_type";
3363 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
3364 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[0]);
3365 $sql .= " ORDER BY quantity ASC";
3366
3367 $resql = $this->db->query($sql);
3368 if ($resql) {
3369 $resultat = array();
3370 $ii = 0;
3371 while ($result = $this->db->fetch_array($resql)) {
3372 $resultat[$ii] = array();
3373 $resultat[$ii]["rowid"] = $result["rowid"];
3374 $resultat[$ii]["price"] = $result["price"];
3375 $resultat[$ii]["unitprice"] = $result["unitprice"];
3376 $resultat[$ii]["quantity"] = $result["quantity"];
3377 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
3378 //$resultat[$ii]["remise"]= $result["remise"]; // deprecated
3379 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
3380 $ii++;
3381 }
3382 $this->prices_by_qty_list[0] = $resultat;
3383 } else {
3384 $this->error = $this->db->lasterror;
3385 return -1;
3386 }
3387 }
3388 }
3389 } else {
3390 $this->error = $this->db->lasterror;
3391 return -1;
3392 }
3393 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') && empty($ignore_price_load)) { // prices per customer and quantity
3394 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
3395 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
3396 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3397 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
3398 $sql .= " FROM ".$this->db->prefix()."product_price";
3399 $sql .= " WHERE entity IN (".getEntity('productprice').")";
3400 $sql .= " AND price_level=".((int) $i);
3401 $sql .= " AND fk_product = ".((int) $this->id);
3402 $sql .= " ORDER BY date_price DESC, rowid DESC";
3403 $sql .= " LIMIT 1";
3404 $resql = $this->db->query($sql);
3405 if (!$resql) {
3406 $this->error = $this->db->lasterror;
3407 return -1;
3408 } else {
3409 $result = $this->db->fetch_array($resql);
3410 $this->multiprices[$i] = (!empty($result["price"]) ? $result["price"] : 0);
3411 $this->multiprices_ttc[$i] = (!empty($result["price_ttc"]) ? $result["price_ttc"] : 0);
3412 $this->multiprices_min[$i] = (!empty($result["price_min"]) ? $result["price_min"] : 0);
3413 $this->multiprices_min_ttc[$i] = (!empty($result["price_min_ttc"]) ? $result["price_min_ttc"] : 0);
3414 $this->multiprices_base_type[$i] = (!empty($result["price_base_type"]) ? $result["price_base_type"] : '');
3415 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
3416 $this->multiprices_tva_tx[$i] = (!empty($result["tva_tx"]) ? $result["tva_tx"] : 0); // TODO Add ' ('.$result['default_vat_code'].')'
3417 $this->multiprices_recuperableonly[$i] = (!empty($result["recuperableonly"]) ? $result["recuperableonly"] : 0);
3418
3419 // Price by quantity
3420 $this->prices_by_qty[$i] = (!empty($result["price_by_qty"]) ? $result["price_by_qty"] : 0);
3421 $this->prices_by_qty_id[$i] = (!empty($result["rowid"]) ? $result["rowid"] : 0);
3422 // Récuperation de la liste des prix selon qty si flag positionné
3423 if ($this->prices_by_qty[$i] == 1) {
3424 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
3425 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
3426 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
3427 $sql .= " ORDER BY quantity ASC";
3428
3429 $resql = $this->db->query($sql);
3430 if ($resql) {
3431 $resultat = array();
3432 $ii = 0;
3433 while ($result = $this->db->fetch_array($resql)) {
3434 $resultat[$ii] = array();
3435 $resultat[$ii]["rowid"] = $result["rowid"];
3436 $resultat[$ii]["price"] = $result["price"];
3437 $resultat[$ii]["unitprice"] = $result["unitprice"];
3438 $resultat[$ii]["quantity"] = $result["quantity"];
3439 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
3440 $resultat[$ii]["remise"] = $result["remise"]; // deprecated
3441 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
3442 $ii++;
3443 }
3444 $this->prices_by_qty_list[$i] = $resultat;
3445 } else {
3446 $this->error = $this->db->lasterror;
3447 return -1;
3448 }
3449 }
3450 }
3451 }
3452 }
3453
3454 if (isModEnabled('dynamicprices') && !empty($this->fk_price_expression) && empty($ignore_expression)) {
3455 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
3456 $priceparser = new PriceParser($this->db);
3457 $price_result = $priceparser->parseProduct($this);
3458 if ($price_result >= 0) {
3459 $this->price = $price_result;
3460 // Calculate the VAT
3461 $this->price_ttc = (float) price2num($this->price) * (1 + ($this->tva_tx / 100));
3462 $this->price_ttc = (float) price2num($this->price_ttc, 'MU');
3463 }
3464 }
3465
3466 // We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product.
3467 // Instead we just init the stock_warehouse array
3468 $this->stock_warehouse = array();
3469
3470 return 1;
3471 } else {
3472 return 0;
3473 }
3474 } else {
3475 $this->error = $this->db->lasterror();
3476 return -1;
3477 }
3478 }
3479
3480 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3487 public function load_stats_mo($socid = 0)
3488 {
3489 // phpcs:enable
3490 global $user, $hookmanager, $action;
3491
3492 $error = 0;
3493
3494 foreach (array('toconsume', 'consumed', 'toproduce', 'produced') as $role) {
3495 $this->stats_mo['customers_'.$role] = 0;
3496 $this->stats_mo['nb_'.$role] = 0;
3497 $this->stats_mo['qty_'.$role] = 0;
3498
3499 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3500 $sql .= " SUM(mp.qty) as qty";
3501 $sql .= " FROM ".$this->db->prefix()."mrp_mo as c";
3502 $sql .= " INNER JOIN ".$this->db->prefix()."mrp_production as mp ON mp.fk_mo=c.rowid";
3503 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3504 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3505 }
3506 $sql .= " WHERE ";
3507 $sql .= " c.entity IN (".getEntity('mo').")";
3508
3509 $sql .= " AND mp.fk_product = ".((int) $this->id);
3510 $sql .= " AND mp.role ='".$this->db->escape($role)."'";
3511 if ($socid > 0) {
3512 $sql .= " AND c.fk_soc = ".((int) $socid);
3513 }
3514
3515 $result = $this->db->query($sql);
3516 if ($result) {
3517 $obj = $this->db->fetch_object($result);
3518 $this->stats_mo['customers_'.$role] = $obj->nb_customers ? $obj->nb_customers : 0;
3519 $this->stats_mo['nb_'.$role] = $obj->nb ? $obj->nb : 0;
3520 $this->stats_mo['qty_'.$role] = $obj->qty ? price2num($obj->qty, 'MS') : 0; // qty may be a float due to the SUM()
3521 } else {
3522 $this->error = $this->db->error();
3523 $error++;
3524 }
3525 }
3526
3527 if (!empty($error)) {
3528 return -1;
3529 }
3530
3531 $parameters = array('socid' => $socid);
3532 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3533 if ($reshook > 0) {
3534 $this->stats_mo = $hookmanager->resArray['stats_mo'];
3535 }
3536
3537 return 1;
3538 }
3539
3540 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3547 public function load_stats_bom($socid = 0)
3548 {
3549 // phpcs:enable
3550 global $hookmanager, $action;
3551
3552 $error = 0;
3553
3554 $this->stats_bom['nb_toproduce'] = 0;
3555 $this->stats_bom['nb_toconsume'] = 0;
3556 $this->stats_bom['qty_toproduce'] = 0;
3557 $this->stats_bom['qty_toconsume'] = 0;
3558
3559 $sql = "SELECT COUNT(DISTINCT b.rowid) as nb_toproduce,";
3560 $sql .= " SUM(b.qty) as qty_toproduce";
3561 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3562 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom = b.rowid";
3563 $sql .= " WHERE ";
3564 $sql .= " b.entity IN (".getEntity('bom').")";
3565 $sql .= " AND b.fk_product =".((int) $this->id);
3566 $sql .= " GROUP BY b.rowid";
3567
3568 $result = $this->db->query($sql);
3569 if ($result) {
3570 $obj = $this->db->fetch_object($result);
3571 $this->stats_bom['nb_toproduce'] = !empty($obj->nb_toproduce) ? $obj->nb_toproduce : 0;
3572 $this->stats_bom['qty_toproduce'] = !empty($obj->qty_toproduce) ? price2num($obj->qty_toproduce) : 0;
3573 } else {
3574 $this->error = $this->db->error();
3575 $error++;
3576 }
3577
3578 $sql = "SELECT COUNT(DISTINCT bl.rowid) as nb_toconsume,";
3579 $sql .= " SUM(bl.qty) as qty_toconsume";
3580 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3581 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
3582 $sql .= " WHERE ";
3583 $sql .= " b.entity IN (".getEntity('bom').")";
3584 $sql .= " AND bl.fk_product =".((int) $this->id);
3585
3586 $result = $this->db->query($sql);
3587 if ($result) {
3588 $obj = $this->db->fetch_object($result);
3589 $this->stats_bom['nb_toconsume'] = !empty($obj->nb_toconsume) ? $obj->nb_toconsume : 0;
3590 $this->stats_bom['qty_toconsume'] = !empty($obj->qty_toconsume) ? price2num($obj->qty_toconsume) : 0;
3591 } else {
3592 $this->error = $this->db->error();
3593 $error++;
3594 }
3595
3596 if (!empty($error)) {
3597 return -1;
3598 }
3599
3600 $parameters = array('socid' => $socid);
3601 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3602 if ($reshook > 0) {
3603 $this->stats_bom = $hookmanager->resArray['stats_bom'];
3604 }
3605
3606 return 1;
3607 }
3608
3609 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3616 public function load_stats_propale($socid = 0)
3617 {
3618 // phpcs:enable
3619 global $user, $hookmanager, $action;
3620
3621 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,";
3622 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3623 $sql .= " FROM ".$this->db->prefix()."propaldet as pd";
3624 $sql .= ", ".$this->db->prefix()."propal as p";
3625 $sql .= ", ".$this->db->prefix()."societe as s";
3626 $sql .= " WHERE p.rowid = pd.fk_propal";
3627 $sql .= " AND p.fk_soc = s.rowid";
3628 $sql .= " AND p.entity IN (".getEntity('propal').")";
3629 $sql .= " AND pd.fk_product = ".((int) $this->id);
3630 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3631 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = p.fk_soc AND sc.fk_user = ".((int) $user->id);
3632 }
3633 //$sql.= " AND pr.fk_statut != 0";
3634 if ($socid > 0) {
3635 $sql .= " AND p.fk_soc = ".((int) $socid);
3636 }
3637 // Add where from hooks
3638 $parameters = array('socid' => $socid, 'type_element' => 'propal');
3639 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3640 $sql .= $hookmanager->resPrint;
3641 $result = $this->db->query($sql);
3642 if ($result) {
3643 $obj = $this->db->fetch_object($result);
3644 $this->stats_propale['customers'] = (int) $obj->nb_customers;
3645 $this->stats_propale['nb'] = (int) $obj->nb;
3646 $this->stats_propale['rows'] = (int) $obj->nb_rows;
3647 $this->stats_propale['qty'] = $obj->qty ? (float) $obj->qty : 0;
3648
3649 // if it's a virtual product, maybe it is in proposal by extension
3650 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3651 $TFather = $this->getFather();
3652 if (is_array($TFather) && !empty($TFather)) {
3653 foreach ($TFather as &$fatherData) {
3654 $pFather = new Product($this->db);
3655 $pFather->id = (int) $fatherData['id'];
3656 $qtyCoef = $fatherData['qty'];
3657
3658 if ($fatherData['incdec']) {
3659 $pFather->load_stats_propale($socid);
3660
3661 if (!empty($pFather->stats_propale)) {
3662 $this->stats_propale['customers'] += $pFather->stats_propale['customers'];
3663 $this->stats_propale['nb'] += $pFather->stats_propale['nb'];
3664 $this->stats_propale['rows'] += $pFather->stats_propale['rows'];
3665 $this->stats_propale['qty'] += $pFather->stats_propale['qty'] * $qtyCoef;
3666 }
3667 }
3668 }
3669 }
3670 }
3671
3672 $parameters = array('socid' => $socid);
3673 $reshook = $hookmanager->executeHooks('loadStatsCustomerProposal', $parameters, $this, $action);
3674 if ($reshook > 0) {
3675 $this->stats_propale = $hookmanager->resArray['stats_propale'];
3676 }
3677
3678 return 1;
3679 } else {
3680 $this->error = $this->db->error();
3681 return -1;
3682 }
3683 }
3684
3685
3686 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3693 public function load_stats_proposal_supplier($socid = 0)
3694 {
3695 // phpcs:enable
3696 global $user, $hookmanager, $action;
3697
3698 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,";
3699 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3700 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as pd";
3701 $sql .= ", ".$this->db->prefix()."supplier_proposal as p";
3702 $sql .= ", ".$this->db->prefix()."societe as s";
3703 $sql .= " WHERE p.rowid = pd.fk_supplier_proposal";
3704 $sql .= " AND p.fk_soc = s.rowid";
3705 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
3706 $sql .= " AND pd.fk_product = ".((int) $this->id);
3707 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3708 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = p.fk_soc AND sc.fk_user = ".((int) $user->id);
3709 }
3710 //$sql.= " AND pr.fk_statut != 0";
3711 if ($socid > 0) {
3712 $sql .= " AND p.fk_soc = ".((int) $socid);
3713 }
3714
3715 $result = $this->db->query($sql);
3716 if ($result) {
3717 $obj = $this->db->fetch_object($result);
3718 $this->stats_proposal_supplier['suppliers'] = $obj->nb_suppliers;
3719 $this->stats_proposal_supplier['nb'] = $obj->nb;
3720 $this->stats_proposal_supplier['rows'] = $obj->nb_rows;
3721 $this->stats_proposal_supplier['qty'] = $obj->qty ? $obj->qty : 0;
3722
3723 $parameters = array('socid' => $socid);
3724 $reshook = $hookmanager->executeHooks('loadStatsSupplierProposal', $parameters, $this, $action);
3725 if ($reshook > 0) {
3726 $this->stats_proposal_supplier = $hookmanager->resArray['stats_proposal_supplier'];
3727 }
3728
3729 return 1;
3730 } else {
3731 $this->error = $this->db->error();
3732 return -1;
3733 }
3734 }
3735
3736
3737 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3746 public function load_stats_commande($socid = 0, $filtrestatut = '', $forVirtualStock = 0)
3747 {
3748 // phpcs:enable
3749 global $user, $hookmanager, $action;
3750
3751
3752 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3753 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3754 $sql .= " FROM ".$this->db->prefix()."commandedet as cd";
3755 $sql .= ", ".$this->db->prefix()."commande as c";
3756 $sql .= ", ".$this->db->prefix()."societe as s";
3757 $sql .= " WHERE c.rowid = cd.fk_commande";
3758 $sql .= " AND c.fk_soc = s.rowid";
3759 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'commande').")";
3760 $sql .= " AND cd.fk_product = ".((int) $this->id);
3761 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3762 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3763 }
3764 if ($socid > 0) {
3765 $sql .= " AND c.fk_soc = ".((int) $socid);
3766 }
3767 if ($filtrestatut != '') {
3768 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3769 }
3770 // Add where from hooks
3771 $parameters = array('socid' => $socid, 'type_element' => 'order');
3772 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3773 $sql .= $hookmanager->resPrint;
3774 $result = $this->db->query($sql);
3775 if ($result) {
3776 $obj = $this->db->fetch_object($result);
3777 $this->stats_commande['customers'] = (int) $obj->nb_customers;
3778 $this->stats_commande['nb'] = (int) $obj->nb;
3779 $this->stats_commande['rows'] = (int) $obj->nb_rows;
3780 $this->stats_commande['qty'] = $obj->qty ? (float) $obj->qty : 0;
3781
3782 // if it's a virtual product, maybe it is in order by extension
3783 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3784 $TFather = $this->getFather();
3785 if (is_array($TFather) && !empty($TFather)) {
3786 foreach ($TFather as &$fatherData) {
3787 $pFather = new Product($this->db);
3788 $pFather->id = $fatherData['id'];
3789 $qtyCoef = $fatherData['qty'];
3790
3791 if ($fatherData['incdec']) {
3792 $pFather->load_stats_commande($socid, $filtrestatut);
3793
3794 $this->stats_commande['customers'] += $pFather->stats_commande['customers'];
3795 $this->stats_commande['nb'] += $pFather->stats_commande['nb'];
3796 $this->stats_commande['rows'] += $pFather->stats_commande['rows'];
3797 $this->stats_commande['qty'] += $pFather->stats_commande['qty'] * $qtyCoef;
3798 }
3799 }
3800 }
3801 }
3802
3803 // If stock decrease is on invoice validation, the theoretical stock continue to
3804 // count the orders lines containing product in theoretical stock when some are already removed by invoice validation.
3805 if ($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
3806 // Do the request using an UNION instead of a OR in the JOIN that is very slow
3807 if (getDolGlobalString('DECREASE_ONLY_UNINVOICEDPRODUCTS')) {
3808 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is on, we make a compensation but only if order not yet invoice.
3809 $adeduire = 0;
3810 $sql = "SELECT SUM(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3811 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3812 //$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'))";
3813 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_target = f.rowid AND el.targettype = 'facture' AND sourcetype = 'commande' ";
3814 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid";
3815 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3816 $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
3817
3818 $sql .= " UNION ";
3819
3820 $sql .= "SELECT SUM(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3821 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3822 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_source = f.rowid AND el.targettype = 'commande' AND sourcetype = 'facture' ";
3823 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_target = c.rowid";
3824 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3825 $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
3826 // Add where from hooks
3827 $parameters = array('socid' => $socid, 'type_element' => 'order');
3828 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3829 $sql .= $hookmanager->resPrint;
3830 $resql = $this->db->query($sql);
3831 if ($resql) {
3832 while ($obj = $this->db->fetch_object($resql)) {
3833 $adeduire += (float) $obj->count;
3834 }
3835 }
3836
3837 $this->stats_commande['qty'] -= $adeduire;
3838 } else {
3839 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is off, we make a compensation with lines of invoices linked to the order
3840 include_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
3841
3842 // For every order having invoice already validated we need to decrease stock cause it's in physical stock
3843 $adeduire = 0;
3844 $sql = "SELECT sum(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3845 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3846 //$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'))";
3847 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_target = f.rowid AND el.targettype = 'facture' AND sourcetype = 'commande' ";
3848 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid";
3849 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product = ".((int) $this->id);
3850 $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
3851
3852 $sql .= " UNION ALL ";
3853
3854 $sql .= "SELECT sum(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3855 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3856 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_source = f.rowid AND el.targettype = 'commande' AND sourcetype = 'facture' ";
3857 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_target = c.rowid";
3858 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product = ".((int) $this->id);
3859 $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
3860
3861 dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3862 // Add where from hooks
3863 $parameters = array('socid' => $socid, 'type_element' => 'order');
3864 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3865 $sql .= $hookmanager->resPrint;
3866 $resql = $this->db->query($sql);
3867 if ($resql) {
3868 while ($obj = $this->db->fetch_object($resql)) {
3869 $adeduire += (float) $obj->count;
3870 }
3871 } else {
3872 $this->error = $this->db->error();
3873 return -1;
3874 }
3875
3876 $this->stats_commande['qty'] -= $adeduire;
3877 }
3878 }
3879
3880 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3881 $reshook = $hookmanager->executeHooks('loadStatsCustomerOrder', $parameters, $this, $action);
3882 if ($reshook > 0) {
3883 $this->stats_commande = $hookmanager->resArray['stats_commande'];
3884 }
3885 return 1;
3886 } else {
3887 $this->error = $this->db->error();
3888 return -1;
3889 }
3890 }
3891
3892 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3902 public function load_stats_commande_fournisseur($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3903 {
3904 // phpcs:enable
3905 global $user, $hookmanager, $action;
3906
3907 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,";
3908 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3909 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as cd";
3910 $sql .= ", ".$this->db->prefix()."commande_fournisseur as c";
3911 $sql .= ", ".$this->db->prefix()."societe as s";
3912 $sql .= " WHERE c.rowid = cd.fk_commande";
3913 $sql .= " AND c.fk_soc = s.rowid";
3914 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
3915 $sql .= " AND cd.fk_product = ".((int) $this->id);
3916 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3917 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3918 }
3919 if ($socid > 0) {
3920 $sql .= " AND c.fk_soc = ".((int) $socid);
3921 }
3922 if ($filtrestatut != '') {
3923 $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")"; // Peut valoir 0
3924 }
3925 if (!empty($dateofvirtualstock)) {
3926 $sql .= " AND c.date_livraison <= '".$this->db->idate($dateofvirtualstock)."'";
3927 }
3928
3929 $result = $this->db->query($sql);
3930 if ($result) {
3931 $obj = $this->db->fetch_object($result);
3932 $this->stats_commande_fournisseur['suppliers'] = $obj->nb_suppliers;
3933 $this->stats_commande_fournisseur['nb'] = $obj->nb;
3934 $this->stats_commande_fournisseur['rows'] = $obj->nb_rows;
3935 $this->stats_commande_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3936
3937 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3938 $reshook = $hookmanager->executeHooks('loadStatsSupplierOrder', $parameters, $this, $action);
3939 if ($reshook > 0) {
3940 $this->stats_commande_fournisseur = $hookmanager->resArray['stats_commande_fournisseur'];
3941 }
3942
3943 return 1;
3944 } else {
3945 $this->error = $this->db->error().' sql='.$sql;
3946 return -1;
3947 }
3948 }
3949
3950 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3960 public function load_stats_sending($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $filterShipmentStatus = '')
3961 {
3962 // phpcs:enable
3963 global $user, $hookmanager, $action;
3964
3965 $sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,";
3966 $sql .= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty";
3967 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
3968 $sql .= ", ".$this->db->prefix()."commandedet as cd";
3969 $sql .= ", ".$this->db->prefix()."commande as c";
3970 $sql .= ", ".$this->db->prefix()."expedition as e";
3971 $sql .= ", ".$this->db->prefix()."societe as s";
3972 $sql .= " WHERE e.rowid = ed.fk_expedition";
3973 $sql .= " AND c.rowid = cd.fk_commande";
3974 $sql .= " AND e.fk_soc = s.rowid";
3975 $sql .= " AND e.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'expedition').")";
3976 $sql .= " AND ed.fk_elementdet = cd.rowid";
3977 $sql .= " AND cd.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 .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = e.fk_soc AND sc.fk_user = ".((int) $user->id);
3980 }
3981 if ($socid > 0) {
3982 $sql .= " AND e.fk_soc = ".((int) $socid);
3983 }
3984 if ($filtrestatut != '') {
3985 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3986 }
3987 if (!empty($filterShipmentStatus)) {
3988 $sql .= " AND e.fk_statut IN (".$this->db->sanitize($filterShipmentStatus).")";
3989 }
3990
3991 $result = $this->db->query($sql);
3992 if ($result) {
3993 $obj = $this->db->fetch_object($result);
3994 $this->stats_expedition['customers'] = (int) $obj->nb_customers;
3995 $this->stats_expedition['nb'] = (int) $obj->nb;
3996 $this->stats_expedition['rows'] = (int) $obj->nb_rows;
3997 $this->stats_expedition['qty'] = $obj->qty ? (float) $obj->qty : 0;
3998
3999 // if it's a virtual product, maybe it is in sending by extension
4000 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4001 $TFather = $this->getFather();
4002 if (is_array($TFather) && !empty($TFather)) {
4003 foreach ($TFather as &$fatherData) {
4004 $pFather = new Product($this->db);
4005 $pFather->id = $fatherData['id'];
4006 $qtyCoef = $fatherData['qty'];
4007
4008 if ($fatherData['incdec']) {
4009 $pFather->load_stats_sending($socid, $filtrestatut, $forVirtualStock);
4010
4011 if (!empty($pFather->stats_expedition)) {
4012 $this->stats_expedition['customers'] += $pFather->stats_expedition['customers'];
4013 $this->stats_expedition['nb'] += $pFather->stats_expedition['nb'];
4014 $this->stats_expedition['rows'] += $pFather->stats_expedition['rows'];
4015 $this->stats_expedition['qty'] += $pFather->stats_expedition['qty'] * $qtyCoef;
4016 }
4017 }
4018 }
4019 }
4020 }
4021
4022 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock, 'filterShipmentStatus' => $filterShipmentStatus);
4023 $reshook = $hookmanager->executeHooks('loadStatsSending', $parameters, $this, $action);
4024 if ($reshook > 0) {
4025 $this->stats_expedition = $hookmanager->resArray['stats_expedition'];
4026 }
4027
4028 return 1;
4029 } else {
4030 $this->error = $this->db->error();
4031 return -1;
4032 }
4033 }
4034
4035 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4045 public function load_stats_reception($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
4046 {
4047 // phpcs:enable
4048 global $user, $hookmanager, $action;
4049
4050 $sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_suppliers, COUNT(DISTINCT cf.rowid) as nb,";
4051 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4052 $sql .= " FROM ".$this->db->prefix()."receptiondet_batch as fd";
4053 $sql .= ", ".$this->db->prefix()."commande_fournisseur as cf";
4054 $sql .= ", ".$this->db->prefix()."societe as s";
4055 $sql .= " WHERE cf.rowid = fd.fk_element";
4056 $sql .= " AND cf.fk_soc = s.rowid";
4057 $sql .= " AND cf.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
4058 $sql .= " AND fd.fk_product = ".((int) $this->id);
4059 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
4060 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = cf.fk_soc AND sc.fk_user = ".((int) $user->id);
4061 }
4062 if ($socid > 0) {
4063 $sql .= " AND cf.fk_soc = ".((int) $socid);
4064 }
4065 if ($filtrestatut != '') {
4066 $sql .= " AND cf.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
4067 }
4068 if (!empty($dateofvirtualstock)) {
4069 $sql .= " AND fd.datec <= '".$this->db->idate($dateofvirtualstock)."'";
4070 }
4071
4072 $result = $this->db->query($sql);
4073 if ($result) {
4074 $obj = $this->db->fetch_object($result);
4075 $this->stats_reception['suppliers'] = $obj->nb_suppliers;
4076 $this->stats_reception['nb'] = $obj->nb;
4077 $this->stats_reception['rows'] = $obj->nb_rows;
4078 $this->stats_reception['qty'] = $obj->qty ? $obj->qty : 0;
4079
4080 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
4081 $reshook = $hookmanager->executeHooks('loadStatsReception', $parameters, $this, $action);
4082 if ($reshook > 0) {
4083 $this->stats_reception = $hookmanager->resArray['stats_reception'];
4084 }
4085
4086 return 1;
4087 } else {
4088 $this->error = $this->db->error();
4089 return -1;
4090 }
4091 }
4092
4093 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4104 public function load_stats_inproduction($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null, $warehouseid = 0)
4105 {
4106 // phpcs:enable
4107 global $user, $hookmanager, $action;
4108
4109 $serviceStockIsEnabled = isModEnabled("service") && getDolGlobalString('STOCK_SUPPORTS_SERVICES');
4110
4111 $sql = "SELECT COUNT(DISTINCT m.fk_soc) as nb_customers, COUNT(DISTINCT m.rowid) as nb,";
4112 $sql .= " COUNT(mp.rowid) as nb_rows, SUM(mp.qty) as qty, role";
4113 $sql .= " FROM ".$this->db->prefix()."mrp_production as mp";
4114 $sql .= ", ".$this->db->prefix()."mrp_mo as m";
4115 $sql .= " LEFT JOIN ".$this->db->prefix()."societe as s ON s.rowid = m.fk_soc";
4116 $sql .= " WHERE m.rowid = mp.fk_mo";
4117 $sql .= " AND m.entity IN (".getEntity(($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE')) ? 'stock' : 'mrp').")";
4118 $sql .= " AND mp.fk_product = ".((int) $this->id);
4119 $sql .= " AND (mp.disable_stock_change IN (0) OR mp.disable_stock_change IS NULL)";
4120 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
4121 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = m.fk_soc AND sc.fk_user = ".((int) $user->id);
4122 }
4123 if ($socid > 0) {
4124 $sql .= " AND m.fk_soc = ".((int) $socid);
4125 }
4126 if ($filtrestatut != '') {
4127 $sql .= " AND m.status IN (".$this->db->sanitize($filtrestatut).")";
4128 }
4129 if (!empty($dateofvirtualstock)) {
4130 $sql .= " AND m.date_valid <= '".$this->db->idate($dateofvirtualstock)."'"; // better date to code ? end of production ?
4131 }
4132 if (!$serviceStockIsEnabled) {
4133 $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))";
4134 }
4135 if (!empty($warehouseid)) {
4136 $sql .= " AND m.fk_warehouse = ".((int) $warehouseid);
4137 }
4138 $sql .= " GROUP BY role";
4139
4140 if ($warehouseid) {
4141 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
4142 } else {
4143 $this->stats_mrptoconsume['customers'] = 0;
4144 $this->stats_mrptoconsume['nb'] = 0;
4145 $this->stats_mrptoconsume['rows'] = 0;
4146 $this->stats_mrptoconsume['qty'] = 0.0;
4147 $this->stats_mrptoproduce['customers'] = 0;
4148 $this->stats_mrptoproduce['nb'] = 0;
4149 $this->stats_mrptoproduce['rows'] = 0;
4150 $this->stats_mrptoproduce['qty'] = 0.0;
4151 }
4152
4153 $result = $this->db->query($sql);
4154 if ($result) {
4155 while ($obj = $this->db->fetch_object($result)) {
4156 if ($obj->role == 'toconsume' && empty($warehouseid)) {
4157 $this->stats_mrptoconsume['customers'] += (int) $obj->nb_customers;
4158 $this->stats_mrptoconsume['nb'] += (int) $obj->nb;
4159 $this->stats_mrptoconsume['rows'] += (int) $obj->nb_rows;
4160 $this->stats_mrptoconsume['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4161 }
4162 if ($obj->role == 'consumed' && empty($warehouseid)) {
4163 //$this->stats_mrptoconsume['customers'] += $obj->nb_customers;
4164 //$this->stats_mrptoconsume['nb'] += $obj->nb;
4165 //$this->stats_mrptoconsume['rows'] += $obj->nb_rows;
4166 $this->stats_mrptoconsume['qty'] -= ($obj->qty ? (float) $obj->qty : 0.0);
4167 }
4168 if ($obj->role == 'toproduce') {
4169 if ($warehouseid) {
4170 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4171 } else {
4172 $this->stats_mrptoproduce['customers'] += (int) $obj->nb_customers;
4173 $this->stats_mrptoproduce['nb'] += (int) $obj->nb;
4174 $this->stats_mrptoproduce['rows'] += (int) $obj->nb_rows;
4175 $this->stats_mrptoproduce['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4176 }
4177 }
4178 if ($obj->role == 'produced') {
4179 //$this->stats_mrptoproduce['customers'] += $obj->nb_customers;
4180 //$this->stats_mrptoproduce['nb'] += $obj->nb;
4181 //$this->stats_mrptoproduce['rows'] += $obj->nb_rows;
4182 if ($warehouseid) {
4183 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
4184 } else {
4185 $this->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
4186 }
4187 }
4188 }
4189
4190 // Clean data
4191 if ($warehouseid) {
4192 if ($this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] < 0) {
4193 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
4194 }
4195 } else {
4196 if ($this->stats_mrptoconsume['qty'] < 0) {
4197 $this->stats_mrptoconsume['qty'] = 0;
4198 }
4199 if ($this->stats_mrptoproduce['qty'] < 0) {
4200 $this->stats_mrptoproduce['qty'] = 0;
4201 }
4202 }
4203
4204 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
4205 $reshook = $hookmanager->executeHooks('loadStatsInProduction', $parameters, $this, $action);
4206 if ($reshook > 0) {
4207 $this->stats_mrptoproduce = $hookmanager->resArray['stats_mrptoproduce'];
4208 }
4209
4210 return 1;
4211 } else {
4212 $this->error = $this->db->error();
4213 return -1;
4214 }
4215 }
4216
4217 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4224 public function load_stats_contrat($socid = 0)
4225 {
4226 // phpcs:enable
4227 global $user, $hookmanager, $action;
4228
4229 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
4230 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
4231 $sql .= " FROM ".$this->db->prefix()."contratdet as cd";
4232 $sql .= ", ".$this->db->prefix()."contrat as c";
4233 $sql .= ", ".$this->db->prefix()."societe as s";
4234 $sql .= " WHERE c.rowid = cd.fk_contrat";
4235 $sql .= " AND c.fk_soc = s.rowid";
4236 $sql .= " AND c.entity IN (".getEntity('contract').")";
4237 $sql .= " AND cd.fk_product = ".((int) $this->id);
4238 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4239 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
4240 }
4241 //$sql.= " AND c.statut != 0";
4242 if ($socid > 0) {
4243 $sql .= " AND c.fk_soc = ".((int) $socid);
4244 }
4245
4246 $result = $this->db->query($sql);
4247 if ($result) {
4248 $obj = $this->db->fetch_object($result);
4249 $this->stats_contrat['customers'] = (int) $obj->nb_customers;
4250 $this->stats_contrat['nb'] = (int) $obj->nb;
4251 $this->stats_contrat['rows'] = (int) $obj->nb_rows;
4252 $this->stats_contrat['qty'] = $obj->qty ? (float) $obj->qty : 0;
4253
4254 // if it's a virtual product, maybe it is in contract by extension
4255 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4256 $TFather = $this->getFather();
4257 if (is_array($TFather) && !empty($TFather)) {
4258 foreach ($TFather as &$fatherData) {
4259 $pFather = new Product($this->db);
4260 $pFather->id = $fatherData['id'];
4261 $qtyCoef = $fatherData['qty'];
4262
4263 if ($fatherData['incdec']) {
4264 $pFather->load_stats_contrat($socid);
4265
4266 if (!empty($pFather->stats_contrat)) {
4267 $this->stats_contrat['customers'] += $pFather->stats_contrat['customers'];
4268 $this->stats_contrat['nb'] += $pFather->stats_contrat['nb'];
4269 $this->stats_contrat['rows'] += $pFather->stats_contrat['rows'];
4270 $this->stats_contrat['qty'] += $pFather->stats_contrat['qty'] * $qtyCoef;
4271 }
4272 }
4273 }
4274 }
4275 }
4276
4277 $parameters = array('socid' => $socid);
4278 $reshook = $hookmanager->executeHooks('loadStatsContract', $parameters, $this, $action);
4279 if ($reshook > 0) {
4280 $this->stats_contrat = $hookmanager->resArray['stats_contrat'];
4281 }
4282
4283 return 1;
4284 } else {
4285 $this->error = $this->db->error().' sql='.$sql;
4286 return -1;
4287 }
4288 }
4289
4290 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4297 public function load_stats_facture($socid = 0)
4298 {
4299 // phpcs:enable
4300 global $user, $hookmanager, $action;
4301
4302 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4303 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(".$this->db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1').") as qty";
4304 $sql .= " FROM ".$this->db->prefix()."facturedet as fd";
4305 $sql .= ", ".$this->db->prefix()."facture as f";
4306 $sql .= ", ".$this->db->prefix()."societe as s";
4307 $sql .= " WHERE f.rowid = fd.fk_facture";
4308 $sql .= " AND f.fk_soc = s.rowid";
4309 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4310 $sql .= " AND fd.fk_product = ".((int) $this->id);
4311 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4312 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4313 }
4314 //$sql.= " AND f.fk_statut != 0";
4315 if ($socid > 0) {
4316 $sql .= " AND f.fk_soc = ".((int) $socid);
4317 }
4318
4319 $result = $this->db->query($sql);
4320 if ($result) {
4321 $obj = $this->db->fetch_object($result);
4322 $this->stats_facture['customers'] = (int) $obj->nb_customers;
4323 $this->stats_facture['nb'] = (int) $obj->nb;
4324 $this->stats_facture['rows'] = (int) $obj->nb_rows;
4325 $this->stats_facture['qty'] = $obj->qty ? (float) $obj->qty : 0;
4326
4327 // if it's a virtual product, maybe it is in invoice by extension
4328 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4329 $TFather = $this->getFather();
4330 if (is_array($TFather) && !empty($TFather)) {
4331 foreach ($TFather as &$fatherData) {
4332 $pFather = new Product($this->db);
4333 $pFather->id = $fatherData['id'];
4334 $qtyCoef = $fatherData['qty'];
4335
4336 if ($fatherData['incdec']) {
4337 $pFather->load_stats_facture($socid);
4338
4339 if (!empty($pFather->stats_facture)) {
4340 $this->stats_facture['customers'] += $pFather->stats_facture['customers'];
4341 $this->stats_facture['nb'] += $pFather->stats_facture['nb'];
4342 $this->stats_facture['rows'] += $pFather->stats_facture['rows'];
4343 $this->stats_facture['qty'] += $pFather->stats_facture['qty'] * $qtyCoef;
4344 }
4345 }
4346 }
4347 }
4348 }
4349
4350 $parameters = array('socid' => $socid);
4351 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoice', $parameters, $this, $action);
4352 if ($reshook > 0) {
4353 $this->stats_facture = $hookmanager->resArray['stats_facture'];
4354 }
4355
4356 return 1;
4357 } else {
4358 $this->error = $this->db->error();
4359 return -1;
4360 }
4361 }
4362
4363
4364 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4371 public function load_stats_facturerec($socid = 0)
4372 {
4373 // phpcs:enable
4374 global $user, $hookmanager, $action;
4375
4376 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4377 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4378 $sql .= " FROM ".MAIN_DB_PREFIX."facturedet_rec as fd";
4379 $sql .= ", ".MAIN_DB_PREFIX."facture_rec as f";
4380 $sql .= ", ".MAIN_DB_PREFIX."societe as s";
4381 $sql .= " WHERE f.rowid = fd.fk_facture";
4382 $sql .= " AND f.fk_soc = s.rowid";
4383 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4384 $sql .= " AND fd.fk_product = ".((int) $this->id);
4385 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4386 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4387 }
4388 //$sql.= " AND f.fk_statut != 0";
4389 if ($socid > 0) {
4390 $sql .= " AND f.fk_soc = ".((int) $socid);
4391 }
4392
4393 $result = $this->db->query($sql);
4394 if ($result) {
4395 $obj = $this->db->fetch_object($result);
4396 $this->stats_facturerec['customers'] = (int) $obj->nb_customers;
4397 $this->stats_facturerec['nb'] = (int) $obj->nb;
4398 $this->stats_facturerec['rows'] = (int) $obj->nb_rows;
4399 $this->stats_facturerec['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4400
4401 // if it's a virtual product, maybe it is in invoice by extension
4402 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4403 $TFather = $this->getFather();
4404 if (is_array($TFather) && !empty($TFather)) {
4405 foreach ($TFather as &$fatherData) {
4406 $pFather = new Product($this->db);
4407 $pFather->id = $fatherData['id'];
4408 $qtyCoef = $fatherData['qty'];
4409
4410 if ($fatherData['incdec']) {
4411 $pFather->load_stats_facture($socid);
4412
4413 $this->stats_facturerec['customers'] += $pFather->stats_facturerec['customers'];
4414 $this->stats_facturerec['nb'] += $pFather->stats_facturerec['nb'];
4415 $this->stats_facturerec['rows'] += $pFather->stats_facturerec['rows'];
4416 $this->stats_facturerec['qty'] += $pFather->stats_facturerec['qty'] * $qtyCoef;
4417 }
4418 }
4419 }
4420 }
4421
4422 $parameters = array('socid' => $socid);
4423 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoiceRec', $parameters, $this, $action);
4424 if ($reshook > 0) {
4425 $this->stats_facturerec = $hookmanager->resArray['stats_facturerec'];
4426 }
4427
4428 return 1;
4429 } else {
4430 $this->error = $this->db->error();
4431 return -1;
4432 }
4433 }
4434
4435 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4442 public function load_stats_facture_fournisseur($socid = 0)
4443 {
4444 // phpcs:enable
4445 global $user, $hookmanager, $action;
4446
4447 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
4448 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4449 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as fd";
4450 $sql .= ", ".$this->db->prefix()."facture_fourn as f";
4451 $sql .= ", ".$this->db->prefix()."societe as s";
4452 $sql .= " WHERE f.rowid = fd.fk_facture_fourn";
4453 $sql .= " AND f.fk_soc = s.rowid";
4454 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4455 $sql .= " AND fd.fk_product = ".((int) $this->id);
4456 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4457 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4458 }
4459 //$sql.= " AND f.fk_statut != 0";
4460 if ($socid > 0) {
4461 $sql .= " AND f.fk_soc = ".((int) $socid);
4462 }
4463
4464 $result = $this->db->query($sql);
4465 if ($result) {
4466 $obj = $this->db->fetch_object($result);
4467 $this->stats_facture_fournisseur['suppliers'] = (int) $obj->nb_suppliers;
4468 $this->stats_facture_fournisseur['nb'] = (int) $obj->nb;
4469 $this->stats_facture_fournisseur['rows'] = (int) $obj->nb_rows;
4470 $this->stats_facture_fournisseur['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4471
4472 $parameters = array('socid' => $socid);
4473 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoice', $parameters, $this, $action);
4474 if ($reshook > 0) {
4475 $this->stats_facture_fournisseur = $hookmanager->resArray['stats_facture_fournisseur'];
4476 }
4477
4478 return 1;
4479 } else {
4480 $this->error = $this->db->error();
4481 return -1;
4482 }
4483 }
4484
4485 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4492 public function load_stats_facturefournrec($socid = 0)
4493 {
4494 // phpcs:enable
4495 global $user, $hookmanager, $action;
4496
4497 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
4498 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4499 $sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn_det_rec as fd";
4500 $sql .= ", ".MAIN_DB_PREFIX."facture_fourn_rec as f";
4501 $sql .= ", ".MAIN_DB_PREFIX."societe as s";
4502 if (!$user->hasRight('societe', 'client', 'voir')) {
4503 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
4504 }
4505 $sql .= " WHERE f.rowid = fd.fk_facture";
4506 $sql .= " AND f.fk_soc = s.rowid";
4507 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4508 $sql .= " AND fd.fk_product = ".((int) $this->id);
4509 if (!$user->hasRight('societe', 'client', 'voir')) {
4510 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4511 }
4512 //$sql.= " AND f.fk_statut != 0";
4513 if ($socid > 0) {
4514 $sql .= " AND f.fk_soc = ".((int) $socid);
4515 }
4516
4517 $result = $this->db->query($sql);
4518 if ($result) {
4519 $obj = $this->db->fetch_object($result);
4520 $this->stats_facturefournrec['suppliers'] = (int) $obj->nb_suppliers;
4521 $this->stats_facturefournrec['nb'] = (int) $obj->nb;
4522 $this->stats_facturefournrec['rows'] = (int) $obj->nb_rows;
4523 $this->stats_facturefournrec['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4524
4525 $parameters = array('socid' => $socid);
4526 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoiceRec', $parameters, $this, $action);
4527 if ($reshook > 0) {
4528 $this->stats_facturefournrec = $hookmanager->resArray['stats_facturefournrec'];
4529 }
4530
4531 return 1;
4532 } else {
4533 $this->error = $this->db->error();
4534 return -1;
4535 }
4536 }
4537
4538 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4547 private function _get_stats($sql, $mode, $year = 0)
4548 {
4549 // phpcs:enable
4550 $tab = array();
4551
4552 $resql = $this->db->query($sql);
4553 if ($resql) {
4554 $num = $this->db->num_rows($resql);
4555 $i = 0;
4556 while ($i < $num) {
4557 $arr = $this->db->fetch_array($resql);
4558 if (is_array($arr)) {
4559 $keyfortab = (string) $arr[1];
4560 if ($year == -1) {
4561 $keyfortab = substr($keyfortab, -2);
4562 }
4563
4564 if ($mode == 'byunit') {
4565 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[0]; // 1st field
4566 } elseif ($mode == 'bynumber') {
4567 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4568 } elseif ($mode == 'byamount') {
4569 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4570 } else {
4571 // Bad value for $mode
4572 return -1;
4573 }
4574 }
4575 $i++;
4576 }
4577 } else {
4578 $this->error = $this->db->error().' sql='.$sql;
4579 return -1;
4580 }
4581
4582 if (empty($year)) {
4583 $year = dol_print_date(time(), '%Y');
4584 $month = dol_print_date(time(), '%m');
4585 } elseif ($year == -1) {
4586 $year = '';
4587 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4588 } else {
4589 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4590 }
4591
4592 $result = array();
4593
4594 for ($j = 0; $j < 12; $j++) {
4595 // $idx is 'D', 'N', 'O', 'S', ... (First letter of month in user language)
4596 $idx = ucfirst(dol_trunc(dol_print_date(dol_mktime(12, 0, 0, $month, 1, 1970), "%b"), 1, 'right', 'UTF-8', 1));
4597
4598 // print $idx.'-'.$year.'-'.$month.'<br>';
4599 $result[$j] = array($idx, isset($tab[$year.$month]) ? $tab[$year.$month] : 0);
4600 // $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0);
4601
4602 $month = "0".($month - 1);
4603 if (dol_strlen($month) == 3) {
4604 $month = substr($month, 1);
4605 }
4606 if ($month == 0) {
4607 $month = 12;
4608 $year -= 1;
4609 }
4610 }
4611
4612 return array_reverse($result);
4613 }
4614
4615
4616 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4627 public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4628 {
4629 // phpcs:enable
4630 global $user;
4631
4632 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4633 if ($mode == 'bynumber') {
4634 $sql .= ", count(DISTINCT f.rowid)";
4635 }
4636 $sql .= ", sum(d.total_ht) as total_ht";
4637 $sql .= " FROM ".$this->db->prefix()."facturedet as d, ".$this->db->prefix()."facture as f, ".$this->db->prefix()."societe as s";
4638 if ($filteronproducttype >= 0) {
4639 $sql .= ", ".$this->db->prefix()."product as p";
4640 }
4641 if (!$user->hasRight('societe', 'client', 'voir')) {
4642 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4643 }
4644 $sql .= " WHERE f.rowid = d.fk_facture";
4645 if ($this->id > 0) {
4646 $sql .= " AND d.fk_product = ".((int) $this->id);
4647 } else {
4648 $sql .= " AND d.fk_product > 0";
4649 }
4650 if ($filteronproducttype >= 0) {
4651 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4652 }
4653 $sql .= " AND f.fk_soc = s.rowid";
4654 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4655 if (!$user->hasRight('societe', 'client', 'voir')) {
4656 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4657 }
4658 if ($socid > 0) {
4659 $sql .= " AND f.fk_soc = $socid";
4660 }
4661 $sql .= $morefilter;
4662 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4663 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4664
4665 return $this->_get_stats($sql, $mode, $year);
4666 }
4667
4668
4669 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4680 public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4681 {
4682 // phpcs:enable
4683 global $user;
4684
4685 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4686 if ($mode == 'bynumber') {
4687 $sql .= ", count(DISTINCT f.rowid)";
4688 }
4689 $sql .= ", sum(d.total_ht) as total_ht";
4690 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as d, ".$this->db->prefix()."facture_fourn as f, ".$this->db->prefix()."societe as s";
4691 if ($filteronproducttype >= 0) {
4692 $sql .= ", ".$this->db->prefix()."product as p";
4693 }
4694 if (!$user->hasRight('societe', 'client', 'voir')) {
4695 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4696 }
4697 $sql .= " WHERE f.rowid = d.fk_facture_fourn";
4698 if ($this->id > 0) {
4699 $sql .= " AND d.fk_product = ".((int) $this->id);
4700 } else {
4701 $sql .= " AND d.fk_product > 0";
4702 }
4703 if ($filteronproducttype >= 0) {
4704 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4705 }
4706 $sql .= " AND f.fk_soc = s.rowid";
4707 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4708 if (!$user->hasRight('societe', 'client', 'voir')) {
4709 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4710 }
4711 if ($socid > 0) {
4712 $sql .= " AND f.fk_soc = $socid";
4713 }
4714 $sql .= $morefilter;
4715 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4716 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4717
4718 return $this->_get_stats($sql, $mode, $year);
4719 }
4720
4721 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4732 public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4733 {
4734 // phpcs:enable
4735 global $user, $hookmanager;
4736
4737 $sql = "SELECT sum(d.qty) as qty, date_format(p.datep, '%Y%m')";
4738 if ($mode == 'bynumber') {
4739 $sql .= ", count(DISTINCT p.rowid)";
4740 }
4741 $sql .= ", sum(d.total_ht) as total_ht";
4742 $sql .= " FROM ".$this->db->prefix()."propaldet as d, ".$this->db->prefix()."propal as p, ".$this->db->prefix()."societe as s";
4743 if ($filteronproducttype >= 0) {
4744 $sql .= ", ".$this->db->prefix()."product as prod";
4745 }
4746 if (!$user->hasRight('societe', 'client', 'voir')) {
4747 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4748 }
4749 $sql .= " WHERE p.rowid = d.fk_propal";
4750 if ($this->id > 0) {
4751 $sql .= " AND d.fk_product = ".((int) $this->id);
4752 } else {
4753 $sql .= " AND d.fk_product > 0";
4754 }
4755 if ($filteronproducttype >= 0) {
4756 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4757 }
4758 $sql .= " AND p.fk_soc = s.rowid";
4759 $sql .= " AND p.entity IN (".getEntity('propal').")";
4760 if (!$user->hasRight('societe', 'client', 'voir')) {
4761 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4762 }
4763 if ($socid > 0) {
4764 $sql .= " AND p.fk_soc = ".((int) $socid);
4765 }
4766 $sql .= $morefilter;
4767 $parameters = array('socid' => $user->socid);
4768 $hookmanager->executeHooks('productGetNbPropal', $parameters, $this); // Note that $action and $object may have been modified by hook
4769 $sql .= $hookmanager->resPrint;
4770 $sql .= " GROUP BY date_format(p.datep,'%Y%m')";
4771 $sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC";
4772 return $this->_get_stats($sql, $mode, $year);
4773 }
4774
4775 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4786 public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4787 {
4788 // phpcs:enable
4789 global $user;
4790
4791 $sql = "SELECT sum(d.qty) as qty, date_format(p.date_valid, '%Y%m')";
4792 if ($mode == 'bynumber') {
4793 $sql .= ", count(DISTINCT p.rowid)";
4794 }
4795 $sql .= ", sum(d.total_ht) as total_ht";
4796 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as d, ".$this->db->prefix()."supplier_proposal as p, ".$this->db->prefix()."societe as s";
4797 if ($filteronproducttype >= 0) {
4798 $sql .= ", ".$this->db->prefix()."product as prod";
4799 }
4800 if (!$user->hasRight('societe', 'client', 'voir')) {
4801 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4802 }
4803 $sql .= " WHERE p.rowid = d.fk_supplier_proposal";
4804 if ($this->id > 0) {
4805 $sql .= " AND d.fk_product = ".((int) $this->id);
4806 } else {
4807 $sql .= " AND d.fk_product > 0";
4808 }
4809 if ($filteronproducttype >= 0) {
4810 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4811 }
4812 $sql .= " AND p.fk_soc = s.rowid";
4813 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
4814 if (!$user->hasRight('societe', 'client', 'voir')) {
4815 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4816 }
4817 if ($socid > 0) {
4818 $sql .= " AND p.fk_soc = ".((int) $socid);
4819 }
4820 $sql .= $morefilter;
4821 $sql .= " GROUP BY date_format(p.date_valid,'%Y%m')";
4822 $sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
4823
4824 return $this->_get_stats($sql, $mode, $year);
4825 }
4826
4827 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4838 public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4839 {
4840 // phpcs:enable
4841 global $user, $hookmanager;
4842
4843 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4844 if ($mode == 'bynumber') {
4845 $sql .= ", count(DISTINCT c.rowid)";
4846 }
4847 $sql .= ", sum(d.total_ht) as total_ht";
4848 $sql .= " FROM ".$this->db->prefix()."commandedet as d, ".$this->db->prefix()."commande as c, ".$this->db->prefix()."societe as s";
4849 if ($filteronproducttype >= 0) {
4850 $sql .= ", ".$this->db->prefix()."product as p";
4851 }
4852 if (!$user->hasRight('societe', 'client', 'voir')) {
4853 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4854 }
4855 $sql .= " WHERE c.rowid = d.fk_commande";
4856 if ($this->id > 0) {
4857 $sql .= " AND d.fk_product = ".((int) $this->id);
4858 } else {
4859 $sql .= " AND d.fk_product > 0";
4860 }
4861 if ($filteronproducttype >= 0) {
4862 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4863 }
4864 $sql .= " AND c.fk_soc = s.rowid";
4865 $sql .= " AND c.entity IN (".getEntity('commande').")";
4866 if (!$user->hasRight('societe', 'client', 'voir')) {
4867 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4868 }
4869 if ($socid > 0) {
4870 $sql .= " AND c.fk_soc = ".((int) $socid);
4871 }
4872 $sql .= $morefilter;
4873 $parameters = array('socid' => $user->socid);
4874 $hookmanager->executeHooks('productGetNbOrder', $parameters, $this); // Note that $action and $object may have been modified by hook
4875 $sql .= $hookmanager->resPrint;
4876 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4877 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4878
4879 return $this->_get_stats($sql, $mode, $year);
4880 }
4881
4882 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4893 public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4894 {
4895 // phpcs:enable
4896 global $user;
4897
4898 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4899 if ($mode == 'bynumber') {
4900 $sql .= ", count(DISTINCT c.rowid)";
4901 }
4902 $sql .= ", sum(d.total_ht) as total_ht";
4903 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as d, ".$this->db->prefix()."commande_fournisseur as c, ".$this->db->prefix()."societe as s";
4904 if ($filteronproducttype >= 0) {
4905 $sql .= ", ".$this->db->prefix()."product as p";
4906 }
4907 if (!$user->hasRight('societe', 'client', 'voir')) {
4908 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4909 }
4910 $sql .= " WHERE c.rowid = d.fk_commande";
4911 if ($this->id > 0) {
4912 $sql .= " AND d.fk_product = ".((int) $this->id);
4913 } else {
4914 $sql .= " AND d.fk_product > 0";
4915 }
4916 if ($filteronproducttype >= 0) {
4917 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4918 }
4919 $sql .= " AND c.fk_soc = s.rowid";
4920 $sql .= " AND c.entity IN (".getEntity('supplier_order').")";
4921 if (!$user->hasRight('societe', 'client', 'voir')) {
4922 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4923 }
4924 if ($socid > 0) {
4925 $sql .= " AND c.fk_soc = ".((int) $socid);
4926 }
4927 $sql .= $morefilter;
4928 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4929 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4930
4931 return $this->_get_stats($sql, $mode, $year);
4932 }
4933
4934 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4945 public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4946 {
4947 // phpcs:enable
4948 global $user;
4949
4950 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_contrat, '%Y%m')";
4951 if ($mode == 'bynumber') {
4952 $sql .= ", count(DISTINCT c.rowid)";
4953 }
4954 $sql .= ", sum(d.total_ht) as total_ht";
4955 $sql .= " FROM ".$this->db->prefix()."contratdet as d, ".$this->db->prefix()."contrat as c, ".$this->db->prefix()."societe as s";
4956 if ($filteronproducttype >= 0) {
4957 $sql .= ", ".$this->db->prefix()."product as p";
4958 }
4959 if (!$user->hasRight('societe', 'client', 'voir')) {
4960 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4961 }
4962 $sql .= " WHERE c.entity IN (".getEntity('contract').")";
4963 $sql .= " AND c.rowid = d.fk_contrat";
4964
4965 if ($this->id > 0) {
4966 $sql .= " AND d.fk_product = ".((int) $this->id);
4967 } else {
4968 $sql .= " AND d.fk_product > 0";
4969 }
4970 if ($filteronproducttype >= 0) {
4971 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4972 }
4973 $sql .= " AND c.fk_soc = s.rowid";
4974
4975 if (!$user->hasRight('societe', 'client', 'voir')) {
4976 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4977 }
4978 if ($socid > 0) {
4979 $sql .= " AND c.fk_soc = ".((int) $socid);
4980 }
4981 $sql .= $morefilter;
4982 $sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')";
4983 $sql .= " ORDER BY date_format(c.date_contrat,'%Y%m') DESC";
4984
4985 return $this->_get_stats($sql, $mode, $year);
4986 }
4987
4988 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4999 public function get_nb_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
5000 {
5001 // phpcs:enable
5002 global $user;
5003
5004 $sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')";
5005 if ($mode == 'bynumber') {
5006 $sql .= ", count(DISTINCT d.rowid)";
5007 }
5008 $sql .= " FROM ".$this->db->prefix()."mrp_mo as d LEFT JOIN ".$this->db->prefix()."societe as s ON d.fk_soc = s.rowid";
5009 if ($filteronproducttype >= 0) {
5010 $sql .= ", ".$this->db->prefix()."product as p";
5011 }
5012 if (!$user->hasRight('societe', 'client', 'voir')) {
5013 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
5014 }
5015
5016 $sql .= " WHERE d.entity IN (".getEntity('mo').")";
5017 $sql .= " AND d.status > 0";
5018
5019 if ($this->id > 0) {
5020 $sql .= " AND d.fk_product = ".((int) $this->id);
5021 } else {
5022 $sql .= " AND d.fk_product > 0";
5023 }
5024 if ($filteronproducttype >= 0) {
5025 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
5026 }
5027
5028 if (!$user->hasRight('societe', 'client', 'voir')) {
5029 $sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
5030 }
5031 if ($socid > 0) {
5032 $sql .= " AND d.fk_soc = ".((int) $socid);
5033 }
5034 $sql .= $morefilter;
5035 $sql .= " GROUP BY date_format(d.date_valid,'%Y%m')";
5036 $sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC";
5037
5038 return $this->_get_stats($sql, $mode, $year);
5039 }
5040
5041 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5052 public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
5053 {
5054 // phpcs:enable
5055 global $user;
5056
5057 // Clean parameters
5058 if (!is_numeric($id_pere)) {
5059 $id_pere = 0;
5060 }
5061 if (!is_numeric($id_fils)) {
5062 $id_fils = 0;
5063 }
5064 if (!is_numeric($incdec)) {
5065 $incdec = 0;
5066 }
5067
5068 $result = $this->del_sousproduit($id_pere, $id_fils);
5069 if ($result < 0) {
5070 return $result;
5071 }
5072
5073 // Check not already father of id_pere (to avoid father -> child -> father links)
5074 $sql = "SELECT fk_product_pere from ".$this->db->prefix()."product_association";
5075 $sql .= " WHERE fk_product_pere = ".((int) $id_fils)." AND fk_product_fils = ".((int) $id_pere);
5076 if (!$this->db->query($sql)) {
5077 dol_print_error($this->db);
5078 return -1;
5079 } else {
5080 //Selection of the highest row
5081 $sql = "SELECT MAX(rang) as max_rank FROM ".$this->db->prefix()."product_association";
5082 $sql .= " WHERE fk_product_pere = ".((int) $id_pere);
5083 $resql = $this->db->query($sql);
5084 if ($resql) {
5085 $obj = $this->db->fetch_object($resql);
5086 $rank = $obj->max_rank + 1;
5087 //Addition of a product with the highest rank +1
5088 $sql = "INSERT INTO ".$this->db->prefix()."product_association(fk_product_pere,fk_product_fils,qty,incdec,rang)";
5089 $sql .= " VALUES (".((int) $id_pere).", ".((int) $id_fils).", ".price2num($qty, 'MS').", ".((int) $incdec).", ".((int) $rank).")";
5090 if (! $this->db->query($sql)) {
5091 dol_print_error($this->db);
5092 return -1;
5093 } else {
5094 if (!$notrigger) {
5095 // Call trigger
5096 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_ADD', $user);
5097 if ($result < 0) {
5098 $this->error = $this->db->lasterror();
5099 dol_syslog(get_class($this).'::addSubproduct error='.$this->error, LOG_ERR);
5100 return -1;
5101 }
5102 }
5103 // End call triggers
5104
5105 return 1;
5106 }
5107 } else {
5108 dol_print_error($this->db);
5109 return -1;
5110 }
5111 }
5112 }
5113
5114 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5125 public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
5126 {
5127 // phpcs:enable
5128 global $user;
5129
5130 // Clean parameters
5131 if (!is_numeric($id_pere)) {
5132 $id_pere = 0;
5133 }
5134 if (!is_numeric($id_fils)) {
5135 $id_fils = 0;
5136 }
5137 if (!is_numeric($incdec)) {
5138 $incdec = 1;
5139 }
5140 if (!is_numeric($qty)) {
5141 $qty = 1;
5142 }
5143
5144 $sql = 'UPDATE '.$this->db->prefix().'product_association SET ';
5145 $sql .= 'qty = '.price2num($qty, 'MS');
5146 $sql .= ',incdec = '.((int) $incdec);
5147 $sql .= ' WHERE fk_product_pere = '.((int) $id_pere).' AND fk_product_fils = '.((int) $id_fils);
5148
5149 if (!$this->db->query($sql)) {
5150 dol_print_error($this->db);
5151 return -1;
5152 } else {
5153 if (!$notrigger) {
5154 // Call trigger
5155 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_UPDATE', $user);
5156 if ($result < 0) {
5157 $this->error = $this->db->lasterror();
5158 dol_syslog(get_class($this).'::updateSubproduct error='.$this->error, LOG_ERR);
5159 return -1;
5160 }
5161 // End call triggers
5162 }
5163
5164 return 1;
5165 }
5166 }
5167
5168 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5177 public function del_sousproduit($fk_parent, $fk_child, $notrigger = 0)
5178 {
5179 // phpcs:enable
5180 global $user;
5181
5182 if (!is_numeric($fk_parent)) {
5183 $fk_parent = 0;
5184 }
5185 if (!is_numeric($fk_child)) {
5186 $fk_child = 0;
5187 }
5188
5189 $sql = "DELETE FROM ".$this->db->prefix()."product_association";
5190 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5191 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5192
5193 dol_syslog(get_class($this).'::del_sousproduit', LOG_DEBUG);
5194 if (!$this->db->query($sql)) {
5195 dol_print_error($this->db);
5196 return -1;
5197 }
5198
5199 // Updated ranks so that none are missing
5200 $sqlrank = "SELECT rowid, rang FROM ".$this->db->prefix()."product_association";
5201 $sqlrank .= " WHERE fk_product_pere = ".((int) $fk_parent);
5202 $sqlrank .= " ORDER BY rang";
5203 $resqlrank = $this->db->query($sqlrank);
5204 if ($resqlrank) {
5205 $cpt = 0;
5206 while ($objrank = $this->db->fetch_object($resqlrank)) {
5207 $cpt++;
5208 $sql = "UPDATE ".$this->db->prefix()."product_association";
5209 $sql .= " SET rang = ".((int) $cpt);
5210 $sql .= " WHERE rowid = ".((int) $objrank->rowid);
5211 if (! $this->db->query($sql)) {
5212 dol_print_error($this->db);
5213 return -1;
5214 }
5215 }
5216 }
5217
5218 if (!$notrigger) {
5219 // Call trigger
5220 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_DELETE', $user);
5221 if ($result < 0) {
5222 $this->error = $this->db->lasterror();
5223 dol_syslog(get_class($this).'::delSubproduct error='.$this->error, LOG_ERR);
5224 return -1;
5225 }
5226 // End call triggers
5227 }
5228
5229 return 1;
5230 }
5231
5232 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5240 public function is_sousproduit($fk_parent, $fk_child)
5241 {
5242 // phpcs:enable
5243 $sql = "SELECT fk_product_pere, qty, incdec";
5244 $sql .= " FROM ".$this->db->prefix()."product_association";
5245 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5246 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5247
5248 $result = $this->db->query($sql);
5249 if ($result) {
5250 $num = $this->db->num_rows($result);
5251
5252 if ($num > 0) {
5253 $obj = $this->db->fetch_object($result);
5254
5255 $this->is_sousproduit_qty = $obj->qty;
5256 $this->is_sousproduit_incdec = $obj->incdec;
5257
5258 return 1;
5259 } else {
5260 return 0;
5261 }
5262 } else {
5263 dol_print_error($this->db);
5264 return -1;
5265 }
5266 }
5267
5268
5269 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5280 public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
5281 {
5282 // phpcs:enable
5283 global $conf;
5284
5285 $now = dol_now();
5286
5287 dol_syslog(get_class($this)."::add_fournisseur id_fourn = ".$id_fourn." ref_fourn=".$ref_fourn." quantity=".$quantity, LOG_DEBUG);
5288
5289 // Clean parameters
5290 $quantity = price2num($quantity, 'MS');
5291
5292 if ($ref_fourn) {
5293 // Check if ref is not already used
5294 $sql = "SELECT rowid, fk_product";
5295 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5296 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5297 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5298 $sql .= " AND fk_product <> ".((int) $this->id);
5299 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5300
5301 $resql = $this->db->query($sql);
5302 if ($resql) {
5303 $obj = $this->db->fetch_object($resql);
5304 if ($obj) {
5305 // If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
5306 $this->product_id_already_linked = $obj->fk_product;
5307 return -3;
5308 }
5309 $this->db->free($resql);
5310 }
5311 }
5312
5313 $sql = "SELECT rowid";
5314 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5315 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5316 if ($ref_fourn) {
5317 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5318 } else {
5319 $sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
5320 }
5321 $sql .= " AND quantity = ".((float) $quantity);
5322 $sql .= " AND fk_product = ".((int) $this->id);
5323 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5324
5325 $resql = $this->db->query($sql);
5326 if ($resql) {
5327 $obj = $this->db->fetch_object($resql);
5328
5329 // The reference supplier does not exist, we create it for this product.
5330 if (empty($obj)) {
5331 $sql = "INSERT INTO ".$this->db->prefix()."product_fournisseur_price(";
5332 $sql .= "datec";
5333 $sql .= ", entity";
5334 $sql .= ", fk_product";
5335 $sql .= ", fk_soc";
5336 $sql .= ", ref_fourn";
5337 $sql .= ", quantity";
5338 $sql .= ", fk_user";
5339 $sql .= ", tva_tx";
5340 $sql .= ") VALUES (";
5341 $sql .= "'".$this->db->idate($now)."'";
5342 $sql .= ", ".((int) $conf->entity);
5343 $sql .= ", ".((int) $this->id);
5344 $sql .= ", ".((int) $id_fourn);
5345 $sql .= ", '".$this->db->escape($ref_fourn)."'";
5346 $sql .= ", ".((float) $quantity);
5347 $sql .= ", ".((int) $user->id);
5348 $sql .= ", 0";
5349 $sql .= ")";
5350
5351 if ($this->db->query($sql)) {
5352 $this->product_fourn_price_id = $this->db->last_insert_id($this->db->prefix()."product_fournisseur_price");
5353 return 1;
5354 } else {
5355 $this->error = $this->db->lasterror();
5356 return -1;
5357 }
5358 } else {
5359 // If the supplier price already exists for this product and quantity
5360 $this->product_fourn_price_id = $obj->rowid;
5361 return 0;
5362 }
5363 } else {
5364 $this->error = $this->db->lasterror();
5365 return -2;
5366 }
5367 }
5368
5369
5370 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5376 public function list_suppliers()
5377 {
5378 // phpcs:enable
5379 global $conf;
5380
5381 $list = array();
5382
5383 $sql = "SELECT DISTINCT p.fk_soc";
5384 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as p";
5385 $sql .= " WHERE p.fk_product = ".((int) $this->id);
5386 $sql .= " AND p.entity = ".((int) $conf->entity);
5387
5388 $result = $this->db->query($sql);
5389 if ($result) {
5390 $num = $this->db->num_rows($result);
5391 $i = 0;
5392 while ($i < $num) {
5393 $obj = $this->db->fetch_object($result);
5394 $list[$i] = $obj->fk_soc;
5395 $i++;
5396 }
5397 }
5398
5399 return $list;
5400 }
5401
5402 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5410 public function clone_price($fromId, $toId)
5411 {
5412 // phpcs:enable
5413 global $user;
5414
5415 $now = dol_now();
5416
5417 $this->db->begin();
5418
5419 // prices
5420 $sql = "INSERT INTO ".$this->db->prefix()."product_price (";
5421 $sql .= " entity";
5422 $sql .= ", fk_product";
5423 $sql .= ", date_price";
5424 $sql .= ", price_level";
5425 $sql .= ", price";
5426 $sql .= ", price_ttc";
5427 $sql .= ", price_min";
5428 $sql .= ", price_min_ttc";
5429 $sql .= ", price_base_type";
5430 $sql .= ", price_label";
5431 $sql .= ", default_vat_code";
5432 $sql .= ", tva_tx";
5433 $sql .= ", recuperableonly";
5434 $sql .= ", localtax1_tx";
5435 $sql .= ", localtax1_type";
5436 $sql .= ", localtax2_tx";
5437 $sql .= ", localtax2_type";
5438 $sql .= ", fk_user_author";
5439 $sql .= ", tosell";
5440 $sql .= ", price_by_qty";
5441 $sql .= ", fk_price_expression";
5442 $sql .= ", fk_multicurrency";
5443 $sql .= ", multicurrency_code";
5444 $sql .= ", multicurrency_tx";
5445 $sql .= ", multicurrency_price";
5446 $sql .= ", multicurrency_price_ttc";
5447 $sql .= ")";
5448 $sql .= " SELECT";
5449 $sql .= " entity";
5450 $sql .= ", ".((int) $toId);
5451 $sql .= ", '".$this->db->idate($now)."'";
5452 $sql .= ", price_level";
5453 $sql .= ", price";
5454 $sql .= ", price_ttc";
5455 $sql .= ", price_min";
5456 $sql .= ", price_min_ttc";
5457 $sql .= ", price_base_type";
5458 $sql .= ", price_label";
5459 $sql .= ", default_vat_code";
5460 $sql .= ", tva_tx";
5461 $sql .= ", recuperableonly";
5462 $sql .= ", localtax1_tx";
5463 $sql .= ", localtax1_type";
5464 $sql .= ", localtax2_tx";
5465 $sql .= ", localtax2_type";
5466 $sql .= ", ".((int) $user->id);
5467 $sql .= ", tosell";
5468 $sql .= ", price_by_qty";
5469 $sql .= ", fk_price_expression";
5470 $sql .= ", fk_multicurrency";
5471 $sql .= ", multicurrency_code";
5472 $sql .= ", multicurrency_tx";
5473 $sql .= ", multicurrency_price";
5474 $sql .= ", multicurrency_price_ttc";
5475 $sql .= " FROM ".$this->db->prefix()."product_price ps";
5476 $sql .= " WHERE fk_product = ".((int) $fromId);
5477 $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)";
5478 $sql .= " ORDER BY date_price DESC";
5479
5480 dol_syslog(__METHOD__, LOG_DEBUG);
5481 $resql = $this->db->query($sql);
5482 if (!$resql) {
5483 $this->db->rollback();
5484 return -1;
5485 }
5486
5487 $this->db->commit();
5488 return 1;
5489 }
5490
5491 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5499 public function clone_associations($fromId, $toId)
5500 {
5501 // phpcs:enable
5502 $this->db->begin();
5503
5504 $sql = 'INSERT INTO '.$this->db->prefix().'product_association (fk_product_pere, fk_product_fils, qty, incdec)';
5505 $sql .= " SELECT ".((int) $toId).", fk_product_fils, qty, incdec FROM ".$this->db->prefix()."product_association";
5506 $sql .= " WHERE fk_product_pere = ".((int) $fromId);
5507
5508 dol_syslog(get_class($this).'::clone_association', LOG_DEBUG);
5509 if (!$this->db->query($sql)) {
5510 $this->db->rollback();
5511 return -1;
5512 }
5513
5514 $this->db->commit();
5515 return 1;
5516 }
5517
5518 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5526 public function clone_fournisseurs($fromId, $toId)
5527 {
5528 // phpcs:enable
5529 $this->db->begin();
5530
5531 $now = dol_now();
5532
5533 // les fournisseurs
5534 /*$sql = "INSERT ".$this->db->prefix()."product_fournisseur ("
5535 . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
5536 . " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, ref_fourn, fk_user_author"
5537 . " FROM ".$this->db->prefix()."product_fournisseur"
5538 . " WHERE fk_product = ".((int) $fromId);
5539
5540 if ( ! $this->db->query($sql ) )
5541 {
5542 $this->db->rollback();
5543 return -1;
5544 }*/
5545
5546 // les prix de fournisseurs.
5547 $sql = "INSERT ".$this->db->prefix()."product_fournisseur_price (";
5548 $sql .= " datec, fk_product, fk_soc, price, quantity, fk_user, tva_tx)";
5549 $sql .= " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, price, quantity, fk_user, tva_tx";
5550 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5551 $sql .= " WHERE fk_product = ".((int) $fromId);
5552
5553 dol_syslog(get_class($this).'::clone_fournisseurs', LOG_DEBUG);
5554 $resql = $this->db->query($sql);
5555 if (!$resql) {
5556 $this->db->rollback();
5557 return -1;
5558 } else {
5559 $this->db->commit();
5560 return 1;
5561 }
5562 }
5563
5564 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5577 public function fetch_prod_arbo($prod, $compl_path = '', $multiply = 1, $level = 1, $id_parent = 0, $ignore_stock_load = 0)
5578 {
5579 // phpcs:enable
5580 $tmpproduct = null;
5581
5582 if ($multiply < 1) {
5583 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);
5584 $multiply = 1;
5585 }
5586
5587 //var_dump($prod);
5588 foreach ($prod as $id_product => $desc_pere) { // $id_product is 0 (first call starting with root top) or an id of a sub_product
5589 if (is_array($desc_pere)) { // If desc_pere is an array, this means it's a child
5590 $id = (!empty($desc_pere[0]) ? $desc_pere[0] : '');
5591 $nb = (!empty($desc_pere[1]) ? $desc_pere[1] : '');
5592 $type = (!empty($desc_pere[2]) ? $desc_pere[2] : '');
5593 $label = (!empty($desc_pere[3]) ? $desc_pere[3] : '');
5594 $incdec = (!empty($desc_pere[4]) ? $desc_pere[4] : 0);
5595
5596 //print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n";
5597 if (is_null($tmpproduct)) {
5598 $tmpproduct = new Product($this->db); // So we initialize tmpproduct only once for all loop.
5599 }
5600 $tmpproduct->fetch($id); // Load product to get ->ref
5601
5602 if (empty($ignore_stock_load) && ($tmpproduct->isProduct() || getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) {
5603 $tmpproduct->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
5604 }
5605
5606 $this->res[] = array(
5607 'id' => $id, // Id product
5608 'id_parent' => $id_parent,
5609 'ref' => $tmpproduct->ref, // Ref product
5610 'nb' => $nb, // Nb of units that compose parent product
5611 'nb_total' => $nb * $multiply, // Nb of units for all nb of product
5612 'stock' => $tmpproduct->stock_reel, // Stock
5613 'stock_alert' => $tmpproduct->seuil_stock_alerte, // Stock alert
5614 'label' => $label,
5615 'fullpath' => $compl_path.$label, // Label
5616 'type' => $type, // Nb of units that compose parent product
5617 'desiredstock' => $tmpproduct->desiredstock,
5618 'level' => $level,
5619 'incdec' => $incdec,
5620 'entity' => $tmpproduct->entity
5621 );
5622
5623 // Recursive call if there child has children of its own
5624 if (isset($desc_pere['childs']) && is_array($desc_pere['childs'])) {
5625 if (!is_int($desc_pere[1] * $multiply)) {
5626 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);
5627 }
5628
5629 //print 'YYY We go down for '.$desc_pere[3]." -> \n";
5630 $this->fetch_prod_arbo($desc_pere['childs'], $compl_path.$desc_pere[3]." -> ", (int) ceil($desc_pere[1] * $multiply), $level + 1, $id, $ignore_stock_load);
5631 }
5632 }
5633 }
5634 }
5635
5636 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5645 public function get_arbo_each_prod($multiply = 1, $ignore_stock_load = 0)
5646 {
5647 // phpcs:enable
5648 $this->res = array();
5649 if (isset($this->sousprods) && is_array($this->sousprods)) {
5650 foreach ($this->sousprods as $prod_name => $desc_product) {
5651 if (is_array($desc_product)) {
5652 $this->fetch_prod_arbo($desc_product, "", $multiply, 1, $this->id, $ignore_stock_load); // This set $this->res
5653 }
5654 }
5655 }
5656 //var_dump($res);
5657 return $this->res;
5658 }
5659
5667 public function hasFatherOrChild($mode = 0)
5668 {
5669 $nb = 0;
5670
5671 $sql = "SELECT COUNT(pa.rowid) as nb";
5672 $sql .= " FROM ".$this->db->prefix()."product_association as pa";
5673 if ($mode == 0) {
5674 $sql .= " WHERE pa.fk_product_fils = ".((int) $this->id)." OR pa.fk_product_pere = ".((int) $this->id);
5675 } elseif ($mode == -1) {
5676 $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)
5677 } elseif ($mode == 1) {
5678 $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)
5679 }
5680
5681 $resql = $this->db->query($sql);
5682 if ($resql) {
5683 $obj = $this->db->fetch_object($resql);
5684 if ($obj) {
5685 $nb = (int) $obj->nb;
5686 }
5687 } else {
5688 return -1;
5689 }
5690
5691 return $nb;
5692 }
5693
5699 public function hasVariants()
5700 {
5701 $nb = 0;
5702 if (!isModEnabled('variants')) {
5703 return $nb;
5704 }
5705 $sql = "SELECT count(rowid) as nb FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_parent = ".((int) $this->id);
5706 $sql .= " AND entity IN (".getEntity('product').")";
5707
5708 $resql = $this->db->query($sql);
5709 if ($resql) {
5710 $obj = $this->db->fetch_object($resql);
5711 if ($obj) {
5712 $nb = (int) $obj->nb;
5713 }
5714 }
5715
5716 return $nb;
5717 }
5718
5719
5725 public function isVariant()
5726 {
5727 if (isModEnabled('variants')) {
5728 $sql = "SELECT rowid FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_child = ".((int) $this->id)." AND entity IN (".getEntity('product').")";
5729
5730 $query = $this->db->query($sql);
5731
5732 if ($query) {
5733 if (!$this->db->num_rows($query)) {
5734 return false;
5735 }
5736 return true;
5737 } else {
5738 dol_print_error($this->db);
5739 return -1;
5740 }
5741 } else {
5742 return false;
5743 }
5744 }
5745
5752 public function getFather()
5753 {
5754 $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";
5755 $sql .= ", p.tosell as status, p.tobuy as status_buy";
5756 $sql .= " FROM ".$this->db->prefix()."product_association as pa,";
5757 $sql .= " ".$this->db->prefix()."product as p";
5758 $sql .= " WHERE p.rowid = pa.fk_product_pere";
5759 $sql .= " AND pa.fk_product_fils = ".((int) $this->id);
5760
5761 $res = $this->db->query($sql);
5762 if ($res) {
5763 $prods = array();
5764 while ($record = $this->db->fetch_array($res)) {
5765 // $record['id'] = $record['rowid'] = id of father
5766 $prods[$record['id']] = array();
5767 $prods[$record['id']]['id'] = $record['rowid'];
5768 $prods[$record['id']]['ref'] = $record['ref'];
5769 $prods[$record['id']]['label'] = $record['label'];
5770 $prods[$record['id']]['qty'] = $record['qty'];
5771 $prods[$record['id']]['incdec'] = $record['incdec'];
5772 $prods[$record['id']]['fk_product_type'] = $record['fk_product_type'];
5773 $prods[$record['id']]['entity'] = $record['entity'];
5774 $prods[$record['id']]['status'] = $record['status'];
5775 $prods[$record['id']]['status_buy'] = $record['status_buy'];
5776 }
5777 return $prods;
5778 } else {
5779 dol_print_error($this->db);
5780 return -1;
5781 }
5782 }
5783
5784
5794 public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array())
5795 {
5796 if (empty($id)) {
5797 return array();
5798 }
5799
5800 $sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,";
5801 $sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec,";
5802 $sql .= " pa.rowid as fk_association, pa.rang";
5803 $sql .= " FROM ".$this->db->prefix()."product as p,";
5804 $sql .= " ".$this->db->prefix()."product_association as pa";
5805 $sql .= " WHERE p.rowid = pa.fk_product_fils";
5806 $sql .= " AND pa.fk_product_pere = ".((int) $id);
5807 $sql .= " AND pa.fk_product_fils <> ".((int) $id); // This should not happens, it is to avoid infinite loop if it happens
5808 $sql .= " ORDER BY pa.rang";
5809
5810 dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level. ' parents='.(is_array($parents) ? implode(',', $parents) : $parents), LOG_DEBUG);
5811
5812 // Protection against infinite loop
5813 if ($level > 30) {
5814 return array();
5815 }
5816
5817 $res = $this->db->query($sql);
5818 if ($res) {
5819 $prods = array();
5820 if ($this->db->num_rows($res) > 0) {
5821 $parents[] = $id;
5822 }
5823
5824 while ($rec = $this->db->fetch_array($res)) {
5825 if (in_array($rec['id'], $parents)) {
5826 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);
5827 continue; // We discard this child if it is already found at a higher level in tree in the same branch.
5828 }
5829
5830 $prods[$rec['rowid']] = array(
5831 0 => $rec['rowid'],
5832 1 => $rec['qty'],
5833 2 => $rec['fk_product_type'],
5834 3 => $this->db->escape($rec['label']),
5835 4 => $rec['incdec'],
5836 5 => $rec['ref'],
5837 6 => $rec['fk_association'],
5838 7 => $rec['rang']
5839 );
5840 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
5841 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
5842 if (empty($firstlevelonly)) {
5843 $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, $parents);
5844 foreach ($listofchilds as $keyChild => $valueChild) {
5845 $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
5846 }
5847 }
5848 }
5849
5850 return $prods;
5851 } else {
5852 dol_print_error($this->db);
5853 return -1;
5854 }
5855 }
5856
5857 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5864 public function get_sousproduits_arbo()
5865 {
5866 // phpcs:enable
5867 $parent = array();
5868
5869 foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) { // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
5870 $parent[$this->label][$keyChild] = $valueChild;
5871 }
5872 foreach ($parent as $key => $value) { // key=label, value is array of children
5873 $this->sousprods[$key] = $value; // @phan-suppress-current-line PhanTypeMismatchProperty
5874 }
5875 }
5876
5884 public function getTooltipContentArray($params)
5885 {
5886 global $conf, $langs, $user;
5887
5888 $langs->loadLangs(array('products', 'other'));
5889
5890 $datas = array();
5891 $nofetch = !empty($params['nofetch']);
5892
5893 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5894 return ['optimize' => $langs->trans("ShowProduct")];
5895 }
5896
5897 // Does user has permission to read product/service
5898 $permissiontoreadproduct = 0;
5899 if ($this->type == self::TYPE_PRODUCT && $user->hasRight('product', 'read')) {
5900 $permissiontoreadproduct = 1;
5901 }
5902 if ($this->type == self::TYPE_SERVICE && $user->hasRight('service', 'read')) {
5903 $permissiontoreadproduct = 1;
5904 }
5905
5906 if (!empty($this->entity) && $permissiontoreadproduct) {
5907 $tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80, 0, 0, 0, 0, '1');
5908 if ($this->nbphoto > 0) {
5909 $datas['photo'] = '<div class="photointooltip floatright">'."\n" . $tmpphoto . '</div>';
5910 }
5911 }
5912
5913 if ($this->isProduct()) {
5914 $datas['picto'] = img_picto('', 'product').' <u class="paddingrightonly">'.$langs->trans("Product").'</u>';
5915 } elseif ($this->isService()) {
5916 $datas['picto'] = img_picto('', 'service').' <u class="paddingrightonly">'.$langs->trans("Service").'</u>';
5917 }
5918 if (isset($this->status) && isset($this->status_buy)) {
5919 $datas['status'] = ' '.$this->getLibStatut(5, 0) . ' '.$this->getLibStatut(5, 1);
5920 }
5921
5922 if (!empty($this->ref)) {
5923 $datas['ref'] = '<br><b>'.$langs->trans('ProductRef').':</b> '.$this->ref;
5924 }
5925 if (!empty($this->label)) {
5926 $datas['label'] = '<br><b>'.$langs->trans('ProductLabel').':</b> '.$this->label.'<br>';
5927 }
5928
5929 if ($permissiontoreadproduct) {
5930 if (!empty($this->description) && getDolGlobalString('PRODUCT_SHOW_DESCRIPTION_IN_TOOLTIP')) {
5931 $datas['description'] = '<br><b>'.$langs->trans('ProductDescription').':</b> '.dolGetFirstLineOfText($this->description, 5);
5932 }
5933
5934 $datas['stockmanaged'] = "<br><b>".$langs->trans("StockableProduct").'</b>: '.yn($this->isStockManaged());
5935
5936 if ($this->isStockManaged()) {
5937 if (isModEnabled('productbatch')) {
5938 $langs->load("productbatch");
5939 $datas['batchstatus'] = "<br><b>".$langs->trans("ManageLotSerial").'</b>: '.$this->getLibStatut(0, 2);
5940 if ($this->status_batch) {
5941 $datas['batchdlc'] = "<br><b>".$langs->trans("BatchSellOrEatByMandatoryList", $langs->transnoentitiesnoconv("SellByDate"), $langs->transnoentitiesnoconv("EatByDate")).'</b>: '.$this->getSellOrEatByMandatoryLabel();
5942 }
5943 }
5944 }
5945 if (isModEnabled('barcode')) {
5946 $datas['barcode'] = '<br><b>'.$langs->trans('BarCode').':</b> '.$this->barcode;
5947 }
5948
5949 if ($this->isProduct()) {
5950 if ($this->weight) {
5951 $datas['weight'] = "<br><b>".$langs->trans("Weight").'</b>: '.$this->weight.' '.measuringUnitString(0, "weight", $this->weight_units);
5952 }
5953 $labelsize = "";
5954 if ($this->length) {
5955 $labelsize .= "<b>".$langs->trans("Length").'</b>: '.$this->length.' '.measuringUnitString(0, 'size', $this->length_units);
5956 }
5957 if ($this->width) {
5958 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Width").'</b>: '.$this->width.' '.measuringUnitString(0, 'size', $this->width_units);
5959 }
5960 if ($this->height) {
5961 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Height").'</b>: '.$this->height.' '.measuringUnitString(0, 'size', $this->height_units);
5962 }
5963 if ($labelsize) {
5964 $datas['size'] = "<br>".$labelsize;
5965 }
5966
5967 $labelsurfacevolume = "";
5968 if ($this->surface) {
5969 $labelsurfacevolume .= "<b>".$langs->trans("Surface").'</b>: '.$this->surface.' '.measuringUnitString(0, 'surface', $this->surface_units);
5970 }
5971 if ($this->volume) {
5972 $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Volume").'</b>: '.$this->volume.' '.measuringUnitString(0, 'volume', $this->volume_units);
5973 }
5974 if ($labelsurfacevolume) {
5975 $datas['surface'] = "<br>" . $labelsurfacevolume;
5976 }
5977 }
5978 if ($this->isService() && !empty($this->duration_value)) {
5979 // Duration
5980 $datas['duration'] = '<br><b>'.$langs->trans("Duration").':</b> '.$this->duration_value;
5981 if ($this->duration_value > 1) {
5982 $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"));
5983 } elseif ($this->duration_value > 0) {
5984 $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"));
5985 }
5986 $datas['duration'] .= (!empty($this->duration_unit) && isset($dur[$this->duration_unit]) ? "&nbsp;".$langs->trans($dur[$this->duration_unit]) : '');
5987 }
5988 if (empty($user->socid)) {
5989 if ($this->isStockManaged() && isset($this->pmp) && $this->pmp) {
5990 $datas['pmp'] = "<br><b>".$langs->trans("PMPValue").'</b>: '.price($this->pmp, 0, '', 1, -1, -1, $conf->currency);
5991 }
5992
5993 if (isModEnabled('accounting')) {
5994 if ($this->status && isset($this->accountancy_code_sell)) {
5995 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5996 $selllabel = '<br>';
5997 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellCode').':</b> '.length_accountg($this->accountancy_code_sell);
5998 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellIntraCode').':</b> '.length_accountg($this->accountancy_code_sell_intra);
5999 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellExportCode').':</b> '.length_accountg($this->accountancy_code_sell_export);
6000 $datas['accountancysell'] = $selllabel;
6001 }
6002 if ($this->status_buy && isset($this->accountancy_code_buy)) {
6003 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
6004 $buylabel = '';
6005 if (empty($this->status)) {
6006 $buylabel .= '<br>';
6007 }
6008 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyCode').':</b> '.length_accountg($this->accountancy_code_buy);
6009 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyIntraCode').':</b> '.length_accountg($this->accountancy_code_buy_intra);
6010 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyExportCode').':</b> '.length_accountg($this->accountancy_code_buy_export);
6011 $datas['accountancybuy'] = $buylabel;
6012 }
6013 }
6014 }
6015 // show categories for this record only in ajax to not overload lists
6016 if (isModEnabled('category') && !$nofetch) {
6017 require_once DOL_DOCUMENT_ROOT . '/categories/class/categorie.class.php';
6018 $form = new Form($this->db);
6019 $datas['categories'] = '<br>' . $form->showCategories($this->id, Categorie::TYPE_PRODUCT, 1);
6020 }
6021 }
6022
6023 return $datas;
6024 }
6025
6039 public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0, $morecss = '', $add_label = 0, $sep = ' - ')
6040 {
6041 global $langs, $hookmanager;
6042
6043 include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
6044
6045 $result = '';
6046
6047 $newref = (string) $this->ref;
6048 if ($maxlength) {
6049 $newref = dol_trunc($newref, $maxlength, 'middle');
6050 }
6051 $params = [
6052 'id' => $this->id,
6053 'objecttype' => ($this->type == 1 ? 'service' : 'product'),
6054 'option' => $option,
6055 'nofetch' => 1,
6056 ];
6057 $classfortooltip = 'classfortooltip';
6058 $dataparams = '';
6059 if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
6060 $classfortooltip = 'classforajaxtooltip';
6061 $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
6062 $label = '';
6063 } else {
6064 $label = implode($this->getTooltipContentArray($params));
6065 }
6066
6067 $linkclose = '';
6068 if (empty($notooltip)) {
6069 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
6070 $label = $langs->trans("ShowProduct");
6071 $linkclose .= ' alt="'.dol_escape_htmltag($label, 1, 1).'"';
6072 }
6073 $linkclose .= ($label ? ' title="'.dol_escape_htmltag($label, 1, 1).'"' : ' title="tocomplete"');
6074 $linkclose .= $dataparams.' class="nowraponall '.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
6075 } else {
6076 $linkclose = ' class="nowraponall'.($morecss ? ' '.$morecss : '').'"';
6077 }
6078
6079 if ($option == 'supplier' || $option == 'category') {
6080 $url = DOL_URL_ROOT.'/product/price_suppliers.php?id='.$this->id;
6081 } elseif ($option == 'stock') {
6082 $url = DOL_URL_ROOT.'/product/stock/product.php?id='.$this->id;
6083 } elseif ($option == 'composition') {
6084 $url = DOL_URL_ROOT.'/product/composition/card.php?id='.$this->id;
6085 } else {
6086 $url = DOL_URL_ROOT.'/product/card.php?id='.$this->id;
6087 }
6088
6089 if ($option !== 'nolink') {
6090 // Add param to save lastsearch_values or not
6091 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
6092 if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
6093 $add_save_lastsearch_values = 1;
6094 }
6095 if ($add_save_lastsearch_values) {
6096 $url .= '&save_lastsearch_values=1';
6097 }
6098 }
6099
6100 $linkstart = '<a href="'.$url.'"';
6101 $linkstart .= $linkclose.'>';
6102 $linkend = '</a>';
6103
6104 $result .= $linkstart;
6105 if ($withpicto) {
6106 if ($this->isProduct()) {
6107 $result .= (img_object(($notooltip ? '' : $label), 'product', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
6108 }
6109 if ($this->isService()) {
6110 $result .= (img_object(($notooltip ? '' : $label), 'service', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
6111 }
6112 }
6113 $result .= '<span class="aaa">'.dol_escape_htmltag($newref).'</span>';
6114 $result .= $linkend;
6115 if ($withpicto != 2) {
6116 $result .= (($add_label && $this->label) ? $sep.dol_trunc($this->label, ($add_label > 1 ? $add_label : 0)) : '');
6117 }
6118
6119 global $action;
6120 $hookmanager->initHooks(array('productdao'));
6121 $parameters = array('id' => $this->id, 'getnomurl' => &$result, 'label' => &$label);
6122 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
6123 if ($reshook > 0) {
6124 $result = $hookmanager->resPrint;
6125 } else {
6126 $result .= $hookmanager->resPrint;
6127 }
6128
6129 return $result;
6130 }
6131
6132
6143 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
6144 {
6145 global $langs;
6146
6147 $langs->load("products");
6148 $outputlangs->load("products");
6149
6150 // Positionne le modele sur le nom du modele a utiliser
6151 if (!dol_strlen($modele)) {
6152 $modele = getDolGlobalString('PRODUCT_ADDON_PDF', 'strato');
6153 }
6154
6155 $modelpath = "core/modules/product/doc/";
6156
6157 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
6158 }
6159
6167 public function getLibStatut($mode = 0, $type = 0)
6168 {
6169 switch ($type) {
6170 case 0:
6171 return $this->LibStatut($this->status, $mode, $type);
6172 case 1:
6173 return $this->LibStatut($this->status_buy, $mode, $type);
6174 case 2:
6175 return $this->LibStatut($this->status_batch, $mode, $type);
6176 default:
6177 //Simulate previous behavior but should return an error string
6178 return $this->LibStatut($this->status_buy, $mode, $type);
6179 }
6180 }
6181
6182 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6191 public function LibStatut($status, $mode = 0, $type = 0)
6192 {
6193 // phpcs:enable
6194 global $langs;
6195
6196 $labelStatus = $labelStatusShort = '';
6197
6198 $langs->load('products');
6199 if (isModEnabled('productbatch')) {
6200 $langs->load("productbatch");
6201 }
6202
6203 if ($type == 2) {
6204 switch ($mode) {
6205 case 0:
6206 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatch') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial')));
6207 return dolGetStatus($label);
6208 case 1:
6209 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort')));
6210 return dolGetStatus($label);
6211 case 2:
6212 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 1, 2);
6213 case 3:
6214 return dolGetStatus($langs->transnoentitiesnoconv('ProductStatusNotOnBatch'), '', '', empty($status) ? 'status5' : 'status4', 3, 'dot');
6215 case 4:
6216 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 0, 2);
6217 case 5:
6218 return $this->LibStatut($status, 1, 2).' '.$this->LibStatut($status, 3, 2);
6219 default:
6220 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'));
6221 }
6222 }
6223
6224 $statuttrans = empty($status) ? 'status5' : 'status4';
6225
6226 if ($status == 0) {
6227 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6228 if ($type == 0) {
6229 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnSellShort');
6230 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnSell');
6231 } elseif ($type == 1) {
6232 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBuyShort');
6233 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBuy');
6234 } elseif ($type == 2) {
6235 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBatch');
6236 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort');
6237 }
6238 } elseif ($status == 1) {
6239 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6240 if ($type == 0) {
6241 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSellShort');
6242 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSell');
6243 } elseif ($type == 1) {
6244 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBuyShort');
6245 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBuy');
6246 } elseif ($type == 2) {
6247 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBatch');
6248 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBatchShort');
6249 }
6250 } elseif ($type == 2 && $status == 2) {
6251 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSerial');
6252 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSerialShort');
6253 }
6254
6255 if ($mode > 6) {
6256 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'), '', '', 'status0', 0);
6257 } else {
6258 return dolGetStatus($labelStatus, $labelStatusShort, '', $statuttrans, $mode);
6259 }
6260 }
6261
6262
6268 public function getLibFinished()
6269 {
6270 global $langs;
6271
6272 $langs->load('products');
6273 $label = '';
6274
6275 if (isset($this->finished) && $this->finished >= 0) {
6276 $sql = "SELECT label, code FROM ".$this->db->prefix()."c_product_nature where code = ".((int) $this->finished)." AND active=1";
6277 $resql = $this->db->query($sql);
6278 if (!$resql) {
6279 $this->error = $this->db->error().' sql='.$sql;
6280 dol_syslog(__METHOD__.' Error '.$this->error, LOG_ERR);
6281 return -1;
6282 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
6283 $label = $langs->trans($res['label']);
6284 }
6285 $this->db->free($resql);
6286 }
6287
6288 return $label;
6289 }
6290
6291
6292 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6309 public function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null)
6310 {
6311 // phpcs:enable
6312 if ($id_entrepot) {
6313 $this->db->begin();
6314
6315 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
6316
6317 // Ensure $nbpiece is a number and positive
6318 $nbpiece = (float) $nbpiece;
6319 if ($nbpiece < 0) {
6320 if (!$movement) {
6321 $movement = 1;
6322 }
6323 $nbpiece = abs($nbpiece);
6324 }
6325 $op = array();
6326 $op[0] = $nbpiece;
6327 $op[1] = -$nbpiece;
6328
6329 $movementstock = new MouvementStock($this->db);
6330 $movementstock->setOrigin($origin_element, (int) $origin_id); // Set ->origin_type and ->origin_id
6331 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', '', '', '', false, 0, $disablestockchangeforsubproduct);
6332
6333 if ($result >= 0) {
6334 if ($extrafields) {
6335 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
6336 $movementstock->array_options = $array_options;
6337 $movementstock->insertExtraFields();
6338 }
6339 $this->db->commit();
6340 return 1;
6341 } else {
6342 $this->setErrorsFromObject($movementstock);
6343
6344 $this->db->rollback();
6345 return -1;
6346 }
6347 }
6348
6349 return -1;
6350 }
6351
6352 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6373 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)
6374 {
6375 // phpcs:enable
6376 if ($id_entrepot) {
6377 $this->db->begin();
6378
6379 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
6380
6381 // Ensure $nbpiece is a number and positive
6382 $nbpiece = (float) $nbpiece;
6383 if ($nbpiece < 0) {
6384 if (!$movement) {
6385 $movement = 1;
6386 }
6387 $nbpiece = abs($nbpiece);
6388 }
6389
6390 $op = array();
6391 $op[0] = $nbpiece;
6392 $op[1] = -$nbpiece;
6393
6394 $movementstock = new MouvementStock($this->db);
6395 $movementstock->setOrigin($origin_element, (int) $origin_id); // Set ->origin_type and ->fk_origin
6396 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', $dlc, $dluo, $lot, false, 0, $disablestockchangeforsubproduct, 0, $force_update_batch);
6397
6398 if ($result >= 0) {
6399 if ($extrafields) {
6400 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
6401 $movementstock->array_options = $array_options;
6402 $movementstock->insertExtraFields();
6403 }
6404 $this->db->commit();
6405 return 1;
6406 } else {
6407 $this->error = $movementstock->error;
6408 $this->errors = $movementstock->errors;
6409
6410 $this->db->rollback();
6411 return -1;
6412 }
6413 }
6414 return -1;
6415 }
6416
6417 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6430 public function load_stock($option = '', $includedraftpoforvirtual = null, $dateofvirtualstock = null)
6431 {
6432 // phpcs:enable
6433 $this->stock_reel = 0;
6434 $this->stock_warehouse = array();
6435 $this->stock_theorique = 0;
6436
6437 // Set filter on warehouse status
6438 $warehouseStatus = array();
6439 if (preg_match('/warehouseclosed/', $option)) {
6441 }
6442 if (preg_match('/warehouseopen/', $option)) {
6444 }
6445 if (preg_match('/warehouseinternal/', $option)) {
6446 if (getDolGlobalString('ENTREPOT_EXTRA_STATUS')) {
6448 } else {
6450 }
6451 }
6452
6453 $sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot";
6454 $sql .= " FROM ".$this->db->prefix()."product_stock as ps";
6455 $sql .= ", ".$this->db->prefix()."entrepot as w";
6456 $sql .= " WHERE w.entity IN (".getEntity('stock').")";
6457 $sql .= " AND w.rowid = ps.fk_entrepot";
6458 $sql .= " AND ps.fk_product = ".((int) $this->id);
6459 if (count($warehouseStatus)) {
6460 $sql .= " AND w.statut IN (".$this->db->sanitize(implode(',', $warehouseStatus)).")";
6461 }
6462
6463 $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;
6464
6465 dol_syslog(get_class($this)."::load_stock", LOG_DEBUG);
6466 $result = $this->db->query($sql);
6467 if ($result) {
6468 $num = $this->db->num_rows($result);
6469 $i = 0;
6470 if ($num > 0) {
6471 while ($i < $num) {
6472 $row = $this->db->fetch_object($result);
6473 $this->stock_warehouse[$row->fk_entrepot] = new stdClass();
6474 $this->stock_warehouse[$row->fk_entrepot]->real = $row->reel;
6475 $this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid;
6476 if ((!preg_match('/nobatch/', $option)) && $this->hasbatch()) {
6477 $this->stock_warehouse[$row->fk_entrepot]->detail_batch = Productbatch::findAll($this->db, $row->rowid, 1, $this->id);
6478 }
6479 $this->stock_reel += $row->reel;
6480 $i++;
6481 }
6482 $this->stock_reel = (float) price2num($this->stock_reel, 'MS');
6483 }
6484 $this->db->free($result);
6485
6486 if (!preg_match('/novirtual/', $option)) {
6487 $this->load_virtual_stock($includedraftpoforvirtual, $dateofvirtualstock); // This load stock_theorique and also load all arrays stats_xxx...
6488 }
6489
6490 return 1;
6491 } else {
6492 $this->error = $this->db->lasterror();
6493 return -1;
6494 }
6495 }
6496
6497
6498 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6508 public function load_virtual_stock($includedraftpoforvirtual = null, $dateofvirtualstock = null)
6509 {
6510 // phpcs:enable
6511 global $hookmanager, $action;
6512
6513 $stock_commande_client = 0;
6514 $stock_commande_fournisseur = 0;
6515 $stock_sending_client = 0;
6516 $stock_reception_fournisseur = 0;
6517 $stock_inproduction = 0;
6518
6519 //dol_syslog("load_virtual_stock");
6520
6521 if (isModEnabled('order')) {
6522 $result = $this->load_stats_commande(0, '1,2', 1);
6523 if ($result < 0) {
6524 dol_print_error($this->db, $this->error);
6525 }
6526 $stock_commande_client = $this->stats_commande['qty'];
6527 }
6528 if (isModEnabled("shipping")) {
6529 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
6530 $filterShipmentStatus = '';
6531 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
6532 $filterShipmentStatus = Expedition::STATUS_VALIDATED.','.Expedition::STATUS_CLOSED;
6533 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6534 $filterShipmentStatus = Expedition::STATUS_CLOSED;
6535 }
6536 $result = $this->load_stats_sending(0, '1,2', 1, $filterShipmentStatus);
6537 if ($result < 0) {
6538 dol_print_error($this->db, $this->error);
6539 }
6540 $stock_sending_client = $this->stats_expedition['qty'];
6541 }
6542 // Include supplier order lines
6543 if (isModEnabled("supplier_order")) {
6544 $filterStatus = getDolGlobalString('SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK', '3,4');
6545 if (isset($includedraftpoforvirtual)) {
6546 $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
6547 }
6548 $result = $this->load_stats_commande_fournisseur(0, $filterStatus, 1, $dateofvirtualstock);
6549 if ($result < 0) {
6550 dol_print_error($this->db, $this->error);
6551 }
6552 $stock_commande_fournisseur = $this->stats_commande_fournisseur['qty'];
6553 }
6554 // Include reception lines
6555 if (isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) {
6556 $filterStatus = '4';
6557 if (isset($includedraftpoforvirtual)) {
6558 $filterStatus = '0,'.$filterStatus;
6559 }
6560 $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock);
6561 if ($result < 0) {
6562 dol_print_error($this->db, $this->error);
6563 }
6564 $stock_reception_fournisseur = $this->stats_reception['qty'];
6565 }
6566 // Include manufacturing
6567 if (isModEnabled('mrp')) {
6568 $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock);
6569 if ($result < 0) {
6570 dol_print_error($this->db, $this->error);
6571 }
6572 $stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty'];
6573 }
6574
6575 $this->stock_theorique = $this->stock_reel + $stock_inproduction;
6576
6577 // $weBillOrderOrShipmentReception is set to 'order' or 'shipmentreception'. it will be used to know how to make virtual stock
6578 // calculation when we have a stock increase or decrease on billing. Do we have to count orders to bill or shipment/reception to bill ?
6579 $weBillOrderOrShipmentReception = getDolGlobalString('STOCK_DO_WE_BILL_ORDER_OR_SHIPMENTECEPTION_FOR_VIRTUALSTOCK', 'order');
6580
6581 // Stock decrease mode
6582 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') || getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6583 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6584 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_VALIDATE_ORDER')) {
6585 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
6586 $tmpnewprod = dol_clone($this, 1);
6587 $result = $tmpnewprod->load_stats_commande(0, '0', 1); // Get qty in draft orders
6588 $this->stock_theorique += $tmpnewprod->stats_commande['qty'];
6589 }
6590 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'order') {
6591 $this->stock_theorique -= $stock_commande_client;
6592 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6593 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6594 }
6595
6596 // Stock Increase mode
6597 if (getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION') || getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION_CLOSE')) {
6598 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6599 } 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
6600 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6601 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER')) { // Warning: stock change "on approval", not on validation !
6602 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
6603 $tmpnewprod = dol_clone($this, 1);
6604 $result = $tmpnewprod->load_stats_commande_fournisseur(0, '0', 1); // Get qty in draft orders
6605 $this->stock_theorique += $this->stats_commande_fournisseur['qty'];
6606 }
6607 $this->stock_theorique -= $stock_reception_fournisseur;
6608 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'order') {
6609 $this->stock_theorique += $stock_commande_fournisseur;
6610 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6611 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6612 }
6613
6614 $parameters = array('id' => $this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual);
6615 // Note that $action and $object may have been modified by some hooks
6616 $reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action);
6617 if ($reshook > 0) {
6618 $this->stock_theorique = $hookmanager->resArray['stock_theorique'];
6619 } elseif ($reshook == 0 && isset($hookmanager->resArray['stock_stats_hook'])) {
6620 $this->stock_theorique += $hookmanager->resArray['stock_stats_hook'];
6621 }
6622
6623 //Virtual Stock by Warehouse
6624 if (!empty($this->stock_warehouse) && getDolGlobalString('STOCK_ALLOW_VIRTUAL_STOCK_PER_WAREHOUSE')) {
6625 foreach ($this->stock_warehouse as $warehouseid => $stockwarehouse) {
6626 if (isModEnabled('mrp')) {
6627 $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock, $warehouseid);
6628 if ($result < 0) {
6629 dol_print_error($this->db, $this->error);
6630 }
6631 }
6632
6633 if ($this->fk_default_warehouse == $warehouseid) {
6634 $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']);
6635 } else {
6636 $this->stock_warehouse[$warehouseid]->virtual = $this->stock_warehouse[$warehouseid]->real + $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'];
6637 }
6638 }
6639 }
6640
6641 return 1;
6642 }
6643
6651 public function loadStockForVirtualProduct($option = '', $qtyWish = 1)
6652 {
6653 $this->stock_warehouse = array();
6654 $error = 0;
6655
6656 $this->get_sousproduits_arbo();
6657 $prods_arbo = $this->get_arbo_each_prod($qtyWish, 1);
6658 if (count($prods_arbo) > 0) {
6659 $productCachedList = array();
6660 $stockByComponentList = array();
6661
6662 foreach ($prods_arbo as $componentArr) {
6663 $componentId = $componentArr['id'];
6664 // only component whose manage stock
6665 if ($componentArr['incdec'] == 1) {
6666 if (!isset($productCachedList[$componentId])) {
6667 $componentStatic = new self($this->db);
6668 $componentStatic->fetch($componentId);
6669 // check if it's a sub-kit
6670 $childrenNb = $componentStatic->hasFatherOrChild(1);
6671 if ($childrenNb == 0) {
6672 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6673 if (!isset($stockByComponentList[$componentId])) {
6674 $stockByComponentList[$componentId] = array(
6675 'qty_need' => 0
6676 );
6677 }
6678 $stockByComponentList[$componentId]['qty_need'] += $componentArr['nb_total'];
6679 }
6680 $productCachedList[$componentId] = $componentStatic;
6681 }
6682 }
6683 }
6684
6685 if (!empty($stockByComponentList)) {
6686 foreach ($stockByComponentList as $componentId => $stockByComponentArr) {
6687 if (!isset($productCachedList[$componentId])) {
6688 $componentStatic = new self($this->db);
6689 $componentStatic->fetch($componentId);
6690 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6691 $productCachedList[$componentId] = $componentStatic;
6692 }
6693 $component = $productCachedList[$componentId];
6694
6695
6696 if ($component->stock_reel < $stockByComponentArr['qty_need']) {
6697 // not enough stock for this component to assemble this virtual product
6698 $error++;
6699 $this->error = 'Not enough component [id='.$componentId.'] in stock, real='.$component->stock_reel.' and need='.$stockByComponentArr['qty_need'];
6700 $this->errors[] = $this->error;
6701 dol_syslog(__METHOD__.' : '.$this->error, LOG_ERR);
6702 } else {
6703 if (!empty($component->stock_warehouse)) {
6704 foreach ($component->stock_warehouse as $warehouseId => $warehouseObj) {
6705 $kitWarehouseAvailable = new stdClass();
6706 $kitWarehouseAvailable->id = $warehouseObj->id;
6707 $kitWarehouseAvailable->real = $qtyWish;
6708 $this->stock_warehouse[$warehouseId] = $kitWarehouseAvailable;
6709 }
6710 }
6711 }
6712
6713 if ($error) {
6714 break;
6715 }
6716 }
6717 } elseif (getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE') && empty($productCachedList)) {
6718 // if all sub product are not stock managed when use parent stock
6719 $this->load_stock('warehouseopen');
6720 }
6721 }
6722
6723 if ($error) {
6724 return -1;
6725 } else {
6726 return 1;
6727 }
6728 }
6729
6737 public function loadBatchInfo($batch)
6738 {
6739 $result = array();
6740
6741 $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";
6742 $sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = ".((int) $this->id)." AND pb.batch = '".$this->db->escape($batch)."'";
6743 $sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby";
6744 dol_syslog(get_class($this)."::loadBatchInfo load first entry found for lot/serial = ".$batch, LOG_DEBUG);
6745 $resql = $this->db->query($sql);
6746 if ($resql) {
6747 $num = $this->db->num_rows($resql);
6748 $i = 0;
6749 while ($i < $num) {
6750 $obj = $this->db->fetch_object($resql);
6751 $result[] = array('batch' => $batch, 'eatby' => $this->db->jdate($obj->eatby), 'sellby' => $this->db->jdate($obj->sellby), 'qty' => $obj->qty);
6752 $i++;
6753 }
6754 return $result;
6755 } else {
6756 dol_print_error($this->db);
6757 $this->db->rollback();
6758 return array();
6759 }
6760 }
6761
6762 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6771 public function add_photo($sdir, $file)
6772 {
6773 // phpcs:enable
6774 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6775
6776 $result = 0;
6777
6778 $dir = $sdir;
6779 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6780 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos";
6781 } else {
6782 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product').dol_sanitizeFileName($this->ref);
6783 }
6784
6785 dol_mkdir($dir);
6786
6787 $dir_osencoded = $dir;
6788
6789 if (is_dir($dir_osencoded)) {
6790 $originImage = $dir.'/'.$file['name'];
6791
6792 // Cree fichier en taille origine
6793 $result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
6794
6795 if (file_exists(dol_osencode($originImage))) {
6796 // Create thumbs
6797 $this->addThumbs($originImage);
6798 }
6799 }
6800
6801 if (is_numeric($result) && $result > 0) {
6802 return 1;
6803 } else {
6804 return -1;
6805 }
6806 }
6807
6808 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6815 public function is_photo_available($sdir)
6816 {
6817 // phpcs:enable
6818 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6819 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6820
6821 $dir = $sdir;
6822 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6823 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos/";
6824 } else {
6825 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product');
6826 }
6827
6828 $dir_osencoded = dol_osencode($dir);
6829 if (file_exists($dir_osencoded)) {
6830 $handle = opendir($dir_osencoded);
6831 if (is_resource($handle)) {
6832 while (($file = readdir($handle)) !== false) {
6833 if (!utf8_check($file)) {
6834 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // To be sure data is stored in UTF8 in memory
6835 }
6836 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6837 return true;
6838 }
6839 }
6840 }
6841 }
6842
6843 return false;
6844 }
6845
6846 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6854 public function liste_photos($dir, $nbmax = 0)
6855 {
6856 // phpcs:enable
6857 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6858 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6859
6860 $nbphoto = 0;
6861 $tabobj = array();
6862
6863 $dir_osencoded = dol_osencode($dir);
6864 $handle = @opendir($dir_osencoded);
6865 if (is_resource($handle)) {
6866 while (($file = readdir($handle)) !== false) {
6867 if (!utf8_check($file)) {
6868 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // readdir returns ISO
6869 }
6870 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6871 $nbphoto++;
6872
6873 // We forge name of thumb.
6874 $photo = $file;
6875 $photo_vignette = '';
6876 $regs = array();
6877 if (preg_match('/('.$this->regeximgext.')$/i', $photo, $regs)) {
6878 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $photo).'_small'.$regs[0];
6879 }
6880
6881 $dirthumb = $dir.'thumbs/';
6882
6883 // Object
6884 $obj = array();
6885 $obj['photo'] = $photo;
6886 if ($photo_vignette && dol_is_file($dirthumb.$photo_vignette)) {
6887 $obj['photo_vignette'] = 'thumbs/'.$photo_vignette;
6888 } else {
6889 $obj['photo_vignette'] = "";
6890 }
6891
6892 $tabobj[$nbphoto - 1] = $obj;
6893
6894 // Do we have to continue with next photo ?
6895 if ($nbmax && $nbphoto >= $nbmax) {
6896 break;
6897 }
6898 }
6899 }
6900
6901 closedir($handle);
6902 }
6903
6904 return $tabobj;
6905 }
6906
6907 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6914 public function delete_photo($file)
6915 {
6916 // phpcs:enable
6917 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6918 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6919
6920 $dir = dirname($file).'/'; // Chemin du dossier contenant l'image d'origine
6921 $dirthumb = $dir.'/thumbs/'; // Chemin du dossier contenant la vignette
6922 $filename = preg_replace('/'.preg_quote($dir, '/').'/i', '', $file); // Nom du fichier
6923
6924 // On efface l'image d'origine
6925 dol_delete_file($file, 0, 0, 0, $this); // For triggers
6926
6927 // Si elle existe, on efface la vignette
6928 if (preg_match('/('.$this->regeximgext.')$/i', $filename, $regs)) {
6929 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_small'.$regs[0];
6930 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6931 dol_delete_file($dirthumb.$photo_vignette);
6932 }
6933
6934 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_mini'.$regs[0];
6935 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6936 dol_delete_file($dirthumb.$photo_vignette);
6937 }
6938 }
6939 }
6940
6941 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6948 public function get_image_size($file)
6949 {
6950 // phpcs:enable
6951 $file_osencoded = dol_osencode($file);
6952 $infoImg = getimagesize($file_osencoded); // Get information on image
6953 $this->imgWidth = $infoImg[0]; // Largeur de l'image
6954 $this->imgHeight = $infoImg[1]; // Hauteur de l'image
6955 }
6956
6962 public function loadStateBoard()
6963 {
6964 global $hookmanager;
6965
6966 $this->nb = array();
6967
6968 $sql = "SELECT count(p.rowid) as nb, fk_product_type";
6969 $sql .= " FROM ".$this->db->prefix()."product as p";
6970 $sql .= ' WHERE p.entity IN ('.getEntity($this->element, 1).')';
6971 // Add where from hooks
6972 if (is_object($hookmanager)) {
6973 $parameters = array();
6974 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
6975 $sql .= $hookmanager->resPrint;
6976 }
6977 $sql .= ' GROUP BY fk_product_type';
6978
6979 $resql = $this->db->query($sql);
6980 if ($resql) {
6981 while ($obj = $this->db->fetch_object($resql)) {
6982 if ($obj->fk_product_type == 1) {
6983 $this->nb["services"] = $obj->nb;
6984 } else {
6985 $this->nb["products"] = $obj->nb;
6986 }
6987 }
6988 $this->db->free($resql);
6989 return 1;
6990 } else {
6991 dol_print_error($this->db);
6992 $this->error = $this->db->error();
6993 return -1;
6994 }
6995 }
6996
7002 public function isProduct()
7003 {
7004 return $this->type == Product::TYPE_PRODUCT;
7005 }
7006
7012 public function isService()
7013 {
7014 return $this->type == Product::TYPE_SERVICE;
7015 }
7016
7022 public function isStockManaged()
7023 {
7024 return (($this->isProduct() || ($this->isService() && getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) && ($this->stockable_product > 0));
7025 }
7026
7032 public function isMandatoryPeriod()
7033 {
7034 return $this->mandatory_period == 1;
7035 }
7036
7042 public function hasbatch()
7043 {
7044 return $this->status_batch > 0;
7045 }
7046
7047
7048 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7057 public function get_barcode($object, $type = '')
7058 {
7059 // phpcs:enable
7060 global $conf;
7061
7062 $result = '';
7063 if (getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
7064 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
7065 foreach ($dirsociete as $dirroot) {
7066 $res = dol_include_once($dirroot . getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM').'.php');
7067 if ($res) {
7068 break;
7069 }
7070 }
7071 $var = getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM');
7072 $mod = new $var();
7073 '@phan-var-force ModeleNumRefBarCode $mod';
7074
7075 $result = $mod->getNextValue($object, $type);
7076
7077 dol_syslog(get_class($this)."::get_barcode barcode=".$result." module=".$var);
7078 }
7079 return $result;
7080 }
7081
7089 public function initAsSpecimen()
7090 {
7091 $now = dol_now();
7092
7093 // Initialize parameters
7094 $this->specimen = 1;
7095 $this->id = 0;
7096 $this->ref = 'PRODUCT_SPEC';
7097 $this->label = 'PRODUCT SPECIMEN';
7098 $this->description = 'This is description of this product specimen that was created the '.dol_print_date($now, 'dayhourlog').'.';
7099 $this->specimen = 1;
7100 $this->country_id = 1;
7101 $this->status = 1;
7102 $this->status_buy = 1;
7103 $this->tobatch = 0;
7104 $this->sell_or_eat_by_mandatory = 0;
7105 $this->note_private = 'This is a comment (private)';
7106 $this->note_public = 'This is a comment (public)';
7107 $this->date_creation = $now;
7108 $this->date_modification = $now;
7109
7110 $this->weight = 4;
7111 $this->weight_units = 3;
7112
7113 $this->length = 5;
7114 $this->length_units = 1;
7115 $this->width = 6;
7116 $this->width_units = 0;
7117 $this->height = null;
7118 $this->height_units = null;
7119
7120 $this->surface = 30;
7121 $this->surface_units = 0;
7122 $this->volume = 300;
7123 $this->volume_units = 0;
7124
7125 $this->barcode = -1; // Create barcode automatically
7126
7127 return 1;
7128 }
7129
7139 public function getLabelOfUnit($type = 'long', $outputlangs = null, $noentities = 0)
7140 {
7141 global $langs;
7142
7143 if (empty($this->fk_unit)) {
7144 return '';
7145 }
7146 if (empty($outputlangs)) {
7147 $outputlangs = $langs;
7148 }
7149
7150 $outputlangs->load('products');
7151 $label = '';
7152
7153 $sql = "SELECT code, label, short_label FROM ".$this->db->prefix()."c_units where rowid = ".((int) $this->fk_unit);
7154
7155 $resql = $this->db->query($sql);
7156 if (!$resql) {
7157 $this->error = $this->db->error();
7158 dol_syslog(get_class($this)."::getLabelOfUnit Error ".$this->error, LOG_ERR);
7159 return -1;
7160 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
7161 if ($type == 'short') {
7162 if ($noentities) {
7163 $label = $outputlangs->transnoentitiesnoconv($res['short_label']);
7164 } else {
7165 $label = $outputlangs->trans($res['short_label']);
7166 }
7167 } elseif ($type == 'code') {
7168 $label = $res['code'];
7169 } else {
7170 if ($outputlangs->trans('unit'.$res['code']) == 'unit'.$res['code']) {
7171 // No translation available
7172 $label = $res['label'];
7173 } else {
7174 // Return the translated value
7175 if ($noentities) {
7176 $label = $outputlangs->transnoentitiesnoconv('unit'.$res['code']);
7177 } else {
7178 $label = $outputlangs->trans('unit'.$res['code']);
7179 }
7180 }
7181 }
7182 }
7183 $this->db->free($resql);
7184
7185 return $label;
7186 }
7187
7188 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7194 public function min_recommended_price()
7195 {
7196 // phpcs:enable
7197 $maxpricesupplier = 0;
7198
7199 if (getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE')) {
7200 include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
7201 $product_fourn = new ProductFournisseur($this->db);
7202 $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', '');
7203
7204 if (is_array($product_fourn_list) && count($product_fourn_list) > 0) {
7205 foreach ($product_fourn_list as $productfourn) {
7206 if ($productfourn->fourn_unitprice > $maxpricesupplier) {
7207 $maxpricesupplier = $productfourn->fourn_unitprice;
7208 }
7209 }
7210
7211 $maxpricesupplier *= getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE');
7212 }
7213 }
7214
7215 return $maxpricesupplier;
7216 }
7217
7218
7229 public function setCategories($categories)
7230 {
7231 require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
7232 return parent::setCategoriesCommon($categories, Categorie::TYPE_PRODUCT);
7233 }
7234
7243 public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
7244 {
7245 $tables = array(
7246 'product_customer_price',
7247 'product_customer_price_log'
7248 );
7249
7250 return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
7251 }
7252
7264 public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
7265 {
7266 $sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM ".$this->db->prefix()."product_pricerules";
7267 $query = $this->db->query($sql);
7268
7269 $rules = array();
7270
7271 while ($result = $this->db->fetch_object($query)) {
7272 $rules[$result->level] = $result;
7273 }
7274
7275 //Because prices can be based on other level's prices, we temporarily store them
7276 $prices = array(
7277 1 => $baseprice
7278 );
7279
7280 $nbofproducts = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
7281 for ($i = 1; $i <= $nbofproducts; $i++) {
7282 $price = $baseprice;
7283 $price_min = $baseprice;
7284
7285 //We have to make sure it does exist and it is > 0
7286 //First price level only allows changing min_price
7287 if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) {
7288 $price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent / 100));
7289 }
7290
7291 $prices[$i] = $price;
7292
7293 //We have to make sure it does exist and it is > 0
7294 if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) {
7295 $price_min = $price * (1 - ($rules[$i]->var_min_percent / 100));
7296 }
7297
7298 //Little check to make sure the price is modified before triggering generation
7299 $check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i]));
7300 $check_type = ($baseprice == $this->multiprices_base_type[$i]);
7301
7302 if ($check_amount && $check_type) {
7303 continue;
7304 }
7305
7306 if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, 1) < 0) {
7307 return -1;
7308 }
7309 }
7310
7311 return 1;
7312 }
7313
7319 public function getRights()
7320 {
7321 global $user;
7322
7323 if ($this->isProduct()) {
7324 return $user->rights->produit;
7325 } else {
7326 return $user->rights->service;
7327 }
7328 }
7329
7336 public function info($id)
7337 {
7338 $sql = "SELECT p.rowid, p.ref, p.datec as date_creation, GREATEST(p.tms, pef.tms) as date_modification,";
7339 $sql .= " p.fk_user_author, p.fk_user_modif";
7340 $sql .= " FROM ".$this->db->prefix().$this->table_element." as p";
7341 $sql .= " LEFT JOIN ".$this->db->prefix().$this->table_element."_extrafields as pef ON pef.fk_object=p.rowid";
7342 $sql .= " WHERE p.rowid = ".((int) $id);
7343
7344 $result = $this->db->query($sql);
7345 if ($result) {
7346 if ($this->db->num_rows($result)) {
7347 $obj = $this->db->fetch_object($result);
7348
7349 $this->id = $obj->rowid;
7350 $this->ref = $obj->ref;
7351
7352 $this->user_creation_id = $obj->fk_user_author;
7353 $this->user_modification_id = $obj->fk_user_modif;
7354
7355 $this->date_creation = $this->db->jdate($obj->date_creation);
7356 $this->date_modification = $this->db->jdate($obj->date_modification);
7357 }
7358
7359 $this->db->free($result);
7360 } else {
7361 dol_print_error($this->db);
7362 }
7363 }
7364
7365
7371 public function getProductDurationHours()
7372 {
7373 if (empty($this->duration_value)) {
7374 $this->errors[] = 'ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
7375 return -1;
7376 }
7377 if ($this->duration_unit == 's') {
7378 $prodDurationHours = 1. / 3600;
7379 } elseif ($this->duration_unit == 'i' || $this->duration_unit == 'mn' || $this->duration_unit == 'min') {
7380 $prodDurationHours = 1. / 60;
7381 } elseif ($this->duration_unit == 'h') {
7382 $prodDurationHours = 1.;
7383 } elseif ($this->duration_unit == 'd') {
7384 $prodDurationHours = 24.;
7385 } elseif ($this->duration_unit == 'w') {
7386 $prodDurationHours = 24. * 7;
7387 } elseif ($this->duration_unit == 'm') {
7388 $prodDurationHours = 24. * 30;
7389 } elseif ($this->duration_unit == 'y') {
7390 $prodDurationHours = 24. * 365;
7391 } else {
7392 $prodDurationHours = 0.0;
7393 }
7394 $prodDurationHours *= $this->duration_value;
7395
7396 return $prodDurationHours;
7397 }
7398
7399
7407 public function getKanbanView($option = '', $arraydata = null)
7408 {
7409 global $langs, $conf;
7410
7411 $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
7412
7413 $return = '<div class="box-flex-item box-flex-grow-zero">';
7414 $return .= '<div class="info-box info-box-sm">';
7415 $return .= '<div class="info-box-img">';
7416 $label = '';
7417 if ($this->entity !== null && $this->is_photo_available($conf->product->multidir_output[$this->entity])) {
7418 $label .= $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 120, 160, 0, 0, 0, '', 'photoref photokanban');
7419 $return .= $label;
7420 } else {
7421 if ($this->isProduct()) {
7422 $label .= img_picto('', 'product');
7423 } elseif ($this->isService()) {
7424 $label .= img_picto('', 'service');
7425 }
7426 $return .= $label;
7427 }
7428 $return .= '</div>';
7429 $return .= '<div class="info-box-content">';
7430 $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">' . $this->getNomUrl() . '</span>';
7431 if ($selected >= 0) {
7432 $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
7433 }
7434 $return .= '<br><span class="info-box-label opacitymedium inline-block tdoverflowmax150 valignmiddle" title="'.dol_escape_htmltag($this->label).'">'.dol_escape_htmltag($this->label).'</span>';
7435 if ($this->price_base_type == 'TTC') {
7436 $return .= '<br><span class="info-box-status amount">'.price($this->price_ttc).' '.$langs->trans("TTC").'</span>';
7437 } else {
7438 if ($this->status) {
7439 $return .= '<br><span class="info-box-status amount">'.price($this->price).' '.$langs->trans("HT").'</span>';
7440 }
7441 }
7442 $br = 1;
7443 if ($this->isProduct()) {
7444 $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>';
7445 $br = 0;
7446 }
7447 if ($br) {
7448 $return .= '<br>';
7449 $return .= '<div class="info-box-status inline-block valignmiddle">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7450 } else {
7451 $return .= '<div class="info-box-status inline-block valignmiddle marginleftonly paddingleft">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7452 }
7453 $return .= '</div>';
7454 $return .= '</div>';
7455 $return .= '</div>';
7456 return $return;
7457 }
7458}
7459
7465{
7466 public $picto = 'service';
7467}
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.
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.
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.
setMultiLangs($user, $notrigger=0)
Update or add a translation for a 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)
Copies the suppliers and supplier pricing of a product/service onto another one.
const TYPE_PRODUCT
Regular product.
fetchAllPriceLogs($id)
Get all price change logs for a product, enriched with supplier info.
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.
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.
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.
delMultiLangs($langtodelete, $user, $notrigger=0)
Delete a language for this product.
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:168
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.
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
dol_delete_file($file, $disableglob=0, $nophperrors=0, $nohook=0, $object=null, $allowdotdot=false, $indexdatabase=1, $nolog=0)
Remove a file or several files with a mask.
dol_delete_dir_recursive($dir, $count=0, $nophperrors=0, $onlysub=0, &$countdeleted=0, $indexdatabase=1, $nolog=0, $level=0)
Remove a directory $dir and its subdirectories (or only files and subdirectories)
dol_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.
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.
print $langs trans("Show") . '< td style="' . $timeColor . '" align="center"> s</td > badge status0 badge status4 badge status3 Error badge status8< td align="center">< span class="badge ' . $badge . '"></span ></td >< td align="center">< a href="#" class="button button-small" onclick="openLogModal(this)" data-req="' . dol_escape_htmltag($reqSafe) . '" data-res="' . dol_escape_htmltag($resSafe) . '" data-err="' . dol_escape_htmltag($errSafe) . '">< span class="fa fa-search-plus"></span ></a ></td ></tr >< tr >< td colspan="' . $colspan . '" class="opacitymedium"></td ></tr ></table ></div ></form > logModal none logModal none s a JSON string
buildzip.php
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:130