dolibarr 24.0.1
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 .= ((int) $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 // The price columns of the table product hold the default price of the product, that is the price of the level 1
2949 // when multiprices are enabled (level is 0 when they are not). So they must not be overwritten by another level.
2950 // The vat columns are however shared by all levels, so they are always updated.
2951 $updatedefaultprice = (empty($level) || $level == 1);
2952
2953 // Don't put quotes here on decimal numbers.
2954 // This causes storage with base rounding instead of exact values.
2955 $sql = "UPDATE ".$this->db->prefix()."product SET";
2956 if ($updatedefaultprice) {
2957 $sql .= " price_base_type = '".$this->db->escape($newpricebase)."',";
2958 $sql .= " price = ".(float) $price.",";
2959 $sql .= " price_ttc = ".(float) $price_ttc.",";
2960 $sql .= " price_min = ".(float) $price_min.",";
2961 $sql .= " price_min_ttc = ".(float) $price_min_ttc.",";
2962 $sql .= " price_label = ".(!empty($price_label) ? "'".$this->db->escape($price_label)."'" : "null").",";
2963 }
2964 $sql .= " localtax1_tx = ".($localtax1 >= 0 ? (float) $localtax1 : 'NULL').",";
2965 $sql .= " localtax2_tx = ".($localtax2 >= 0 ? (float) $localtax2 : 'NULL').",";
2966 $sql .= " localtax1_type = ".($localtaxtype1 != '' ? "'".$this->db->escape($localtaxtype1)."'" : "'0'").",";
2967 $sql .= " localtax2_type = ".($localtaxtype2 != '' ? "'".$this->db->escape($localtaxtype2)."'" : "'0'").",";
2968 $sql .= " default_vat_code = ".($newdefaultvatcode ? "'".$this->db->escape($newdefaultvatcode)."'" : "null").",";
2969 $sql .= " tva_tx = ".(float) price2num($newvat).",";
2970 $sql .= " recuperableonly = '".$this->db->escape((string) $newnpr)."'";
2971 $sql .= " WHERE rowid = ".((int) $id);
2972
2973 dol_syslog(get_class($this)."::update_price", LOG_DEBUG);
2974 $resql = $this->db->query($sql);
2975 if ($resql) {
2976 $this->multiprices[$level] = $price;
2977 $this->multiprices_ttc[$level] = $price_ttc;
2978 $this->multiprices_min[$level] = $price_min;
2979 $this->multiprices_min_ttc[$level] = $price_min_ttc;
2980 $this->multiprices_base_type[$level] = $newpricebase;
2981 $this->multiprices_default_vat_code[$level] = $newdefaultvatcode;
2982 $this->multiprices_tva_tx[$level] = $newvat;
2983 $this->multiprices_recuperableonly[$level] = $newnpr;
2984
2985 // Save the default price of the product to restore it after the log if we are updating another level
2986 // (_log_price() reads the price into the properties of the object to save the price of the level).
2987 $savdefaultprice = array($this->price, $this->price_label, $this->price_ttc, $this->price_min, $this->price_min_ttc, $this->price_base_type);
2988
2989 $this->price = $price;
2990 $this->price_label = $price_label;
2991 $this->price_ttc = $price_ttc;
2992 $this->price_min = $price_min;
2993 $this->price_min_ttc = $price_min_ttc;
2994 $this->price_base_type = $newpricebase;
2995 $this->default_vat_code = $newdefaultvatcode;
2996 $this->tva_tx = $newvat;
2997 $this->tva_npr = $newnpr;
2998
2999 //Local taxes
3000 $this->localtax1_tx = $localtax1;
3001 $this->localtax2_tx = $localtax2;
3002 $this->localtax1_type = $localtaxtype1;
3003 $this->localtax2_type = $localtaxtype2;
3004
3005 // Price by quantity
3006 $this->price_by_qty = $newpbq;
3007
3008 // check if price have really change before log
3009 $newPriceData = $this->getArrayForPriceCompare($level);
3010 if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || (!getDolGlobalString('PRODUIT_MULTIPRICES') && !getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES'))) {
3011 $this->_log_price($user, $level); // Save price for level into table product_price
3012 }
3013
3014 if (!$updatedefaultprice) {
3015 // The price of the level is now saved, so we can restore the default price of the product
3016 list($this->price, $this->price_label, $this->price_ttc, $this->price_min, $this->price_min_ttc, $this->price_base_type) = $savdefaultprice;
3017 }
3018
3019 $this->level = $level; // Store level of price edited for trigger
3020
3021 // Call trigger
3022 if (!$notrigger) {
3023 $result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user);
3024 if ($result < 0) {
3025 $this->db->rollback();
3026 return -1;
3027 }
3028 }
3029 // End call triggers
3030
3031 $this->db->commit();
3032 } else {
3033 $this->db->rollback();
3034 $this->error = $this->db->lasterror();
3035 return -1;
3036 }
3037 }
3038
3039 return 1;
3040 }
3041
3049 public function setPriceExpression($expression_id)
3050 {
3051 global $user;
3052
3053 $this->fk_price_expression = $expression_id;
3054
3055 return $this->update($this->id, $user);
3056 }
3057
3070 public function fetch($id = 0, $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0)
3071 {
3072 include_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
3073
3074 global $conf;
3075
3076 dol_syslog(get_class($this)."::fetch id=".$id." ref=".$ref." ref_ext=".$ref_ext);
3077
3078 // Check parameters
3079 if (!$id && !$ref && !$ref_ext && !$barcode) {
3080 $this->error = 'ErrorWrongParameters';
3081 dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR);
3082 return -1;
3083 }
3084
3085 $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,";
3086 $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,";
3087 $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,";
3088 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.last_main_doc,";
3089 $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,";
3090 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
3091 $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,";
3092 } else {
3093 $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,";
3094 }
3095
3096 // For MultiCompany
3097 // PMP per entity & Stocks Sharings stock_reel includes only stocks shared with this entity
3098 $separatedEntityPMP = false; // Set to true to get the AWP from table llx_product_perentity instead of field 'pmp' into llx_product.
3099 $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.
3100 $visibleWarehousesEntities = $conf->entity;
3101 if (getDolGlobalString('MULTICOMPANY_PRODUCT_SHARING_ENABLED')) {
3102 if (getDolGlobalString('MULTICOMPANY_PMP_PER_ENTITY_ENABLED')) {
3103 $checkPMPPerEntity = $this->db->query("SELECT pmp FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = ".((int) $id)." AND entity = ".(int) $conf->entity);
3104 if ($this->db->num_rows($checkPMPPerEntity) > 0) {
3105 $separatedEntityPMP = true;
3106 }
3107 }
3108 global $mc;
3109 $separatedStock = true;
3110 if (isset($mc->sharings['stock']) && !empty($mc->sharings['stock'])) {
3111 $visibleWarehousesEntities .= "," . implode(",", $mc->sharings['stock']);
3112 }
3113 }
3114 if ($separatedEntityPMP) {
3115 $sql .= " ppe.pmp,";
3116 } else {
3117 $sql .= " p.pmp,";
3118 }
3119 $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,";
3120 $sql .= " p.fk_price_expression, p.price_autogen, p.stockable_product, p.model_pdf,";
3121 $sql .= " p.price_label,";
3122 if ($separatedStock) {
3123 $sql .= " SUM(sp.reel) as stock";
3124 } else {
3125 $sql .= " p.stock";
3126 }
3127 $sql .= " FROM ".$this->db->prefix()."product as p";
3128 $sql .= " LEFT JOIN ".$this->db->prefix()."product_extrafields as pef ON pef.fk_object=p.rowid";
3129 if (getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED') || $separatedEntityPMP) {
3130 $sql .= " LEFT JOIN " . $this->db->prefix() . "product_perentity as ppe ON ppe.fk_product = p.rowid AND ppe.entity = " . ((int) $conf->entity);
3131 }
3132 if ($separatedStock) {
3133 $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)."))";
3134 }
3135
3136 if ($id) {
3137 $sql .= " WHERE p.rowid = ".((int) $id);
3138 } else {
3139 $sql .= " WHERE p.entity IN (".getEntity($this->element).")";
3140 if ($ref) {
3141 $sql .= " AND p.ref = '".$this->db->escape($ref)."'";
3142 } elseif ($ref_ext) {
3143 $sql .= " AND p.ref_ext = '".$this->db->escape($ref_ext)."'";
3144 } elseif ($barcode) {
3145 $sql .= " AND p.barcode = '".$this->db->escape($barcode)."'";
3146 }
3147 }
3148 if ($separatedStock) {
3149 $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,";
3150 $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,";
3151 $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,";
3152 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
3153 $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,";
3154 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
3155 $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,";
3156 } else {
3157 $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,";
3158 }
3159 if ($separatedEntityPMP) {
3160 $sql .= " ppe.pmp,";
3161 } else {
3162 $sql .= " p.pmp,";
3163 }
3164 $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,";
3165 $sql .= " p.fk_price_expression, p.price_autogen, p.stockable_product, p.model_pdf,";
3166 $sql .= " p.price_label";
3167 // can't be false TODO fix
3168 // if (!$separatedStock) {
3169 // $sql .= ", p.stock";
3170 // }
3171 }
3172
3173 $resql = $this->db->query($sql);
3174 if ($resql) {
3175 unset($this->oldcopy);
3176
3177 if ($this->db->num_rows($resql) > 0) {
3178 $obj = $this->db->fetch_object($resql);
3179
3180 $this->id = $obj->rowid;
3181 $this->ref = $obj->ref;
3182 $this->ref_ext = $obj->ref_ext;
3183 $this->label = $obj->label;
3184 $this->description = $obj->description;
3185 $this->url = $obj->url;
3186 $this->note_public = $obj->note_public;
3187 $this->note_private = $obj->note_private;
3188 $this->note = $obj->note_private; // deprecated
3189
3190 $this->type = $obj->fk_product_type;
3191 $this->price_label = $obj->price_label;
3192 $this->status = $obj->tosell;
3193 $this->status_buy = $obj->tobuy;
3194 $this->status_batch = $obj->tobatch;
3195 $this->sell_or_eat_by_mandatory = $obj->sell_or_eat_by_mandatory;
3196 $this->batch_mask = $obj->batch_mask;
3197
3198 $this->customcode = $obj->customcode;
3199 $this->country_id = $obj->fk_country;
3200 $this->country_code = getCountry($this->country_id, '2', $this->db);
3201 $this->state_id = $obj->fk_state;
3202 $this->lifetime = $obj->lifetime;
3203 $this->qc_frequency = $obj->qc_frequency;
3204 $this->price = $obj->price;
3205 $this->price_ttc = $obj->price_ttc;
3206 $this->price_min = $obj->price_min;
3207 $this->price_min_ttc = $obj->price_min_ttc;
3208 $this->price_base_type = (empty($obj->price_base_type) ? 'HT' : $obj->price_base_type);
3209 $this->cost_price = isset($obj->cost_price) ? (float) $obj->cost_price : null;
3210 $this->default_vat_code = $obj->default_vat_code;
3211 $this->tva_tx = $obj->tva_tx;
3213 $this->tva_npr = $obj->tva_npr;
3215 $this->localtax1_tx = $obj->localtax1_tx;
3216 $this->localtax2_tx = $obj->localtax2_tx;
3217 $this->localtax1_type = $obj->localtax1_type;
3218 $this->localtax2_type = $obj->localtax2_type;
3219
3220 $this->finished = $obj->finished;
3221 $this->fk_default_bom = $obj->fk_default_bom;
3222
3223 $this->duration = $obj->duration;
3224 $matches = [];
3225 preg_match('/([\d.]+)(\w+)/', $obj->duration, $matches);
3226 $this->duration_value = !empty($matches[1]) ? (float) $matches[1] : 0;
3227 $this->duration_unit = !empty($matches[2]) ? (string) $matches[2] : null;
3228 $this->canvas = $obj->canvas;
3229 $this->net_measure = $obj->net_measure;
3230 $this->net_measure_units = $obj->net_measure_units;
3231 $this->weight = $obj->weight;
3232 $this->weight_units = (is_null($obj->weight_units) ? 0 : $obj->weight_units);
3233 $this->length = $obj->length;
3234 $this->length_units = (is_null($obj->length_units) ? 0 : $obj->length_units);
3235 $this->width = $obj->width;
3236 $this->width_units = (is_null($obj->width_units) ? 0 : $obj->width_units);
3237 $this->height = $obj->height;
3238 $this->height_units = (is_null($obj->height_units) ? 0 : $obj->height_units);
3239
3240 $this->surface = $obj->surface;
3241 $this->surface_units = (is_null($obj->surface_units) ? 0 : $obj->surface_units);
3242 $this->volume = $obj->volume;
3243 $this->volume_units = (is_null($obj->volume_units) ? 0 : $obj->volume_units);
3244 $this->barcode = $obj->barcode;
3245 $this->barcode_type = $obj->fk_barcode_type;
3246
3247 $this->accountancy_code_buy = $obj->accountancy_code_buy;
3248 $this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra;
3249 $this->accountancy_code_buy_export = $obj->accountancy_code_buy_export;
3250 $this->accountancy_code_sell = $obj->accountancy_code_sell;
3251 $this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra;
3252 $this->accountancy_code_sell_export = $obj->accountancy_code_sell_export;
3253
3254 $this->fk_default_warehouse = $obj->fk_default_warehouse;
3255 $this->fk_default_workstation = $obj->fk_default_workstation;
3256 $this->seuil_stock_alerte = $obj->seuil_stock_alerte;
3257 $this->desiredstock = $obj->desiredstock;
3258 $this->stock_reel = $obj->stock;
3259 $this->stockable_product = $obj->stockable_product;
3260 $this->pmp = $obj->pmp;
3261
3262 $this->date_creation = $this->db->jdate($obj->datec);
3263 $this->date_modification = $this->db->jdate($obj->tms);
3264
3265 $this->import_key = $obj->import_key;
3266 $this->entity = $obj->entity;
3267
3268 $this->ref_ext = $obj->ref_ext;
3269 $this->fk_price_expression = $obj->fk_price_expression;
3270 $this->fk_unit = $obj->fk_unit;
3271 $this->price_autogen = $obj->price_autogen;
3272 $this->model_pdf = $obj->model_pdf;
3273 $this->last_main_doc = $obj->last_main_doc;
3274
3275 $this->mandatory_period = $obj->mandatory_period;
3276
3277 if (getDolGlobalString('PRODUCT_USE_CUSTOMER_PACKAGING')) {
3278 $this->packaging = (float) $obj->packaging;
3279 }
3280
3281 $this->db->free($resql);
3282
3283 // fetch optionals attributes and labels
3284 $this->fetch_optionals();
3285
3286 // Multilangs
3287 if (getDolGlobalInt('MAIN_MULTILANGS') && empty($ignore_lang_load)) {
3288 $this->getMultiLangs();
3289 }
3290
3291 // Load multiprices array
3292 if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && empty($ignore_price_load)) { // prices per segment
3293 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
3294 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
3295 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3296 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
3297 $sql .= " ,price_label";
3298 $sql .= " FROM ".$this->db->prefix()."product_price";
3299 $sql .= " WHERE entity IN (".getEntity('productprice').")";
3300 $sql .= " AND price_level=".((int) $i);
3301 $sql .= " AND fk_product = ".((int) $this->id);
3302 $sql .= " ORDER BY date_price DESC, rowid DESC"; // Get the most recent line
3303 $sql .= " LIMIT 1"; // Only the first one
3304 $resql = $this->db->query($sql);
3305 if ($resql) {
3306 $result = $this->db->fetch_array($resql);
3307
3308 $this->multiprices[$i] = $result ? $result["price"] : null;
3309 $this->multiprices_ttc[$i] = $result ? $result["price_ttc"] : null;
3310 $this->multiprices_min[$i] = $result ? $result["price_min"] : null;
3311 $this->multiprices_min_ttc[$i] = $result ? $result["price_min_ttc"] : null;
3312 $this->multiprices_base_type[$i] = $result ? $result["price_base_type"] : null;
3313 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
3314 $this->multiprices_tva_tx[$i] = $result ? $result["tva_tx"].(!empty($result['default_vat_code']) ? ' ('.$result['default_vat_code'].')' : '') : null;
3315 $this->multiprices_recuperableonly[$i] = $result ? $result["recuperableonly"] : null;
3316
3317 // Price by quantity
3318 /*
3319 $this->prices_by_qty[$i]=$result["price_by_qty"];
3320 $this->prices_by_qty_id[$i]=$result["rowid"];
3321 // Get the pricelist according to the qty if flag is set
3322 if ($this->prices_by_qty[$i] == 1)
3323 {
3324 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
3325 $sql.= " FROM ".$this->db->prefix()."product_price_by_qty";
3326 $sql.= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
3327 $sql.= " ORDER BY quantity ASC";
3328
3329 $resql = $this->db->query($sql);
3330 if ($resql)
3331 {
3332 $resultat=array();
3333 $ii=0;
3334 while ($result= $this->db->fetch_array($resql)) {
3335 $resultat[$ii]=array();
3336 $resultat[$ii]["rowid"]=$result["rowid"];
3337 $resultat[$ii]["price"]= $result["price"];
3338 $resultat[$ii]["unitprice"]= $result["unitprice"];
3339 $resultat[$ii]["quantity"]= $result["quantity"];
3340 $resultat[$ii]["remise_percent"]= $result["remise_percent"];
3341 $resultat[$ii]["remise"]= $result["remise"]; // deprecated
3342 $resultat[$ii]["price_base_type"]= $result["price_base_type"];
3343 $ii++;
3344 }
3345 $this->prices_by_qty_list[$i]=$resultat;
3346 }
3347 else
3348 {
3349 dol_print_error($this->db);
3350 return -1;
3351 }
3352 }*/
3353 } else {
3354 $this->error = $this->db->lasterror;
3355 return -1;
3356 }
3357 }
3358 } elseif ((getDolGlobalString('PRODUIT_CUSTOMER_PRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && empty($ignore_price_load)) { // prices per customers
3359 // Nothing loaded by default. List may be very long.
3360 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY') && empty($ignore_price_load)) { // prices per quantity
3361 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3362 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid";
3363 $sql .= " FROM ".$this->db->prefix()."product_price";
3364 $sql .= " WHERE fk_product = ".((int) $this->id);
3365 $sql .= " ORDER BY date_price DESC, rowid DESC";
3366 $sql .= " LIMIT 1";
3367
3368 $resql = $this->db->query($sql);
3369 if ($resql) {
3370 $result = $this->db->fetch_array($resql);
3371
3372 if ($result) {
3373 // Price by quantity
3374 $this->prices_by_qty[0] = $result["price_by_qty"];
3375 $this->prices_by_qty_id[0] = $result["rowid"];
3376 // Get the pricelist according to the qty if flag is set
3377 if ($this->prices_by_qty[0] == 1) {
3378 $sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise, remise, price_base_type";
3379 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
3380 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[0]);
3381 $sql .= " ORDER BY quantity ASC";
3382
3383 $resql = $this->db->query($sql);
3384 if ($resql) {
3385 $resultat = array();
3386 $ii = 0;
3387 while ($result = $this->db->fetch_array($resql)) {
3388 $resultat[$ii] = array();
3389 $resultat[$ii]["rowid"] = $result["rowid"];
3390 $resultat[$ii]["price"] = $result["price"];
3391 $resultat[$ii]["unitprice"] = $result["unitprice"];
3392 $resultat[$ii]["quantity"] = $result["quantity"];
3393 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
3394 //$resultat[$ii]["remise"]= $result["remise"]; // deprecated
3395 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
3396 $ii++;
3397 }
3398 $this->prices_by_qty_list[0] = $resultat;
3399 } else {
3400 $this->error = $this->db->lasterror;
3401 return -1;
3402 }
3403 }
3404 }
3405 } else {
3406 $this->error = $this->db->lasterror;
3407 return -1;
3408 }
3409 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') && empty($ignore_price_load)) { // prices per customer and quantity
3410 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
3411 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
3412 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3413 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
3414 $sql .= " FROM ".$this->db->prefix()."product_price";
3415 $sql .= " WHERE entity IN (".getEntity('productprice').")";
3416 $sql .= " AND price_level=".((int) $i);
3417 $sql .= " AND fk_product = ".((int) $this->id);
3418 $sql .= " ORDER BY date_price DESC, rowid DESC";
3419 $sql .= " LIMIT 1";
3420 $resql = $this->db->query($sql);
3421 if (!$resql) {
3422 $this->error = $this->db->lasterror;
3423 return -1;
3424 } else {
3425 $result = $this->db->fetch_array($resql);
3426 $this->multiprices[$i] = (!empty($result["price"]) ? $result["price"] : 0);
3427 $this->multiprices_ttc[$i] = (!empty($result["price_ttc"]) ? $result["price_ttc"] : 0);
3428 $this->multiprices_min[$i] = (!empty($result["price_min"]) ? $result["price_min"] : 0);
3429 $this->multiprices_min_ttc[$i] = (!empty($result["price_min_ttc"]) ? $result["price_min_ttc"] : 0);
3430 $this->multiprices_base_type[$i] = (!empty($result["price_base_type"]) ? $result["price_base_type"] : '');
3431 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
3432 $this->multiprices_tva_tx[$i] = (!empty($result["tva_tx"]) ? $result["tva_tx"] : 0); // TODO Add ' ('.$result['default_vat_code'].')'
3433 $this->multiprices_recuperableonly[$i] = (!empty($result["recuperableonly"]) ? $result["recuperableonly"] : 0);
3434
3435 // Price by quantity
3436 $this->prices_by_qty[$i] = (!empty($result["price_by_qty"]) ? $result["price_by_qty"] : 0);
3437 $this->prices_by_qty_id[$i] = (!empty($result["rowid"]) ? $result["rowid"] : 0);
3438 // Get the pricelist according to the qty if flag is set
3439 if ($this->prices_by_qty[$i] == 1) {
3440 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
3441 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
3442 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
3443 $sql .= " ORDER BY quantity ASC";
3444
3445 $resql = $this->db->query($sql);
3446 if ($resql) {
3447 $resultat = array();
3448 $ii = 0;
3449 while ($result = $this->db->fetch_array($resql)) {
3450 $resultat[$ii] = array();
3451 $resultat[$ii]["rowid"] = $result["rowid"];
3452 $resultat[$ii]["price"] = $result["price"];
3453 $resultat[$ii]["unitprice"] = $result["unitprice"];
3454 $resultat[$ii]["quantity"] = $result["quantity"];
3455 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
3456 $resultat[$ii]["remise"] = $result["remise"]; // deprecated
3457 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
3458 $ii++;
3459 }
3460 $this->prices_by_qty_list[$i] = $resultat;
3461 } else {
3462 $this->error = $this->db->lasterror;
3463 return -1;
3464 }
3465 }
3466 }
3467 }
3468 }
3469
3470 if (isModEnabled('dynamicprices') && !empty($this->fk_price_expression) && empty($ignore_expression)) {
3471 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
3472 $priceparser = new PriceParser($this->db);
3473 $price_result = $priceparser->parseProduct($this);
3474 if ($price_result >= 0) {
3475 $this->price = $price_result;
3476 // Calculate the VAT
3477 $this->price_ttc = (float) price2num($this->price) * (1 + ($this->tva_tx / 100));
3478 $this->price_ttc = (float) price2num($this->price_ttc, 'MU');
3479 }
3480 }
3481
3482 // We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product.
3483 // Instead we just init the stock_warehouse array
3484 $this->stock_warehouse = array();
3485
3486 return 1;
3487 } else {
3488 return 0;
3489 }
3490 } else {
3491 $this->error = $this->db->lasterror();
3492 return -1;
3493 }
3494 }
3495
3496 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3503 public function load_stats_mo($socid = 0)
3504 {
3505 // phpcs:enable
3506 global $user, $hookmanager, $action;
3507
3508 $error = 0;
3509
3510 foreach (array('toconsume', 'consumed', 'toproduce', 'produced') as $role) {
3511 $this->stats_mo['customers_'.$role] = 0;
3512 $this->stats_mo['nb_'.$role] = 0;
3513 $this->stats_mo['qty_'.$role] = 0;
3514
3515 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3516 $sql .= " SUM(mp.qty) as qty";
3517 $sql .= " FROM ".$this->db->prefix()."mrp_mo as c";
3518 $sql .= " INNER JOIN ".$this->db->prefix()."mrp_production as mp ON mp.fk_mo=c.rowid";
3519 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3520 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3521 }
3522 $sql .= " WHERE ";
3523 $sql .= " c.entity IN (".getEntity('mo').")";
3524
3525 $sql .= " AND mp.fk_product = ".((int) $this->id);
3526 $sql .= " AND mp.role ='".$this->db->escape($role)."'";
3527 if ($socid > 0) {
3528 $sql .= " AND c.fk_soc = ".((int) $socid);
3529 }
3530
3531 $result = $this->db->query($sql);
3532 if ($result) {
3533 $obj = $this->db->fetch_object($result);
3534 $this->stats_mo['customers_'.$role] = $obj->nb_customers ? $obj->nb_customers : 0;
3535 $this->stats_mo['nb_'.$role] = $obj->nb ? $obj->nb : 0;
3536 $this->stats_mo['qty_'.$role] = $obj->qty ? price2num($obj->qty, 'MS') : 0; // qty may be a float due to the SUM()
3537 } else {
3538 $this->error = $this->db->error();
3539 $error++;
3540 }
3541 }
3542
3543 if (!empty($error)) {
3544 return -1;
3545 }
3546
3547 $parameters = array('socid' => $socid);
3548 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3549 if ($reshook > 0) {
3550 $this->stats_mo = $hookmanager->resArray['stats_mo'];
3551 }
3552
3553 return 1;
3554 }
3555
3556 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3563 public function load_stats_bom($socid = 0)
3564 {
3565 // phpcs:enable
3566 global $hookmanager, $action;
3567
3568 $error = 0;
3569
3570 $this->stats_bom['nb_toproduce'] = 0;
3571 $this->stats_bom['nb_toconsume'] = 0;
3572 $this->stats_bom['qty_toproduce'] = 0;
3573 $this->stats_bom['qty_toconsume'] = 0;
3574
3575 $sql = "SELECT COUNT(DISTINCT b.rowid) as nb_toproduce,";
3576 $sql .= " SUM(b.qty) as qty_toproduce";
3577 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3578 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom = b.rowid";
3579 $sql .= " WHERE ";
3580 $sql .= " b.entity IN (".getEntity('bom').")";
3581 $sql .= " AND b.fk_product =".((int) $this->id);
3582 $sql .= " GROUP BY b.rowid";
3583
3584 $result = $this->db->query($sql);
3585 if ($result) {
3586 $obj = $this->db->fetch_object($result);
3587 $this->stats_bom['nb_toproduce'] = !empty($obj->nb_toproduce) ? $obj->nb_toproduce : 0;
3588 $this->stats_bom['qty_toproduce'] = !empty($obj->qty_toproduce) ? price2num($obj->qty_toproduce) : 0;
3589 } else {
3590 $this->error = $this->db->error();
3591 $error++;
3592 }
3593
3594 $sql = "SELECT COUNT(DISTINCT bl.rowid) as nb_toconsume,";
3595 $sql .= " SUM(bl.qty) as qty_toconsume";
3596 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3597 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
3598 $sql .= " WHERE ";
3599 $sql .= " b.entity IN (".getEntity('bom').")";
3600 $sql .= " AND bl.fk_product =".((int) $this->id);
3601
3602 $result = $this->db->query($sql);
3603 if ($result) {
3604 $obj = $this->db->fetch_object($result);
3605 $this->stats_bom['nb_toconsume'] = !empty($obj->nb_toconsume) ? $obj->nb_toconsume : 0;
3606 $this->stats_bom['qty_toconsume'] = !empty($obj->qty_toconsume) ? price2num($obj->qty_toconsume) : 0;
3607 } else {
3608 $this->error = $this->db->error();
3609 $error++;
3610 }
3611
3612 if (!empty($error)) {
3613 return -1;
3614 }
3615
3616 $parameters = array('socid' => $socid);
3617 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3618 if ($reshook > 0) {
3619 $this->stats_bom = $hookmanager->resArray['stats_bom'];
3620 }
3621
3622 return 1;
3623 }
3624
3625 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3632 public function load_stats_propale($socid = 0)
3633 {
3634 // phpcs:enable
3635 global $user, $hookmanager, $action;
3636
3637 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,";
3638 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3639 $sql .= " FROM ".$this->db->prefix()."propaldet as pd";
3640 $sql .= ", ".$this->db->prefix()."propal as p";
3641 $sql .= ", ".$this->db->prefix()."societe as s";
3642 $sql .= " WHERE p.rowid = pd.fk_propal";
3643 $sql .= " AND p.fk_soc = s.rowid";
3644 $sql .= " AND p.entity IN (".getEntity('propal').")";
3645 $sql .= " AND pd.fk_product = ".((int) $this->id);
3646 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3647 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = p.fk_soc AND sc.fk_user = ".((int) $user->id);
3648 }
3649 //$sql.= " AND pr.fk_statut != 0";
3650 if ($socid > 0) {
3651 $sql .= " AND p.fk_soc = ".((int) $socid);
3652 }
3653 // Add where from hooks
3654 $parameters = array('socid' => $socid, 'type_element' => 'propal');
3655 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3656 $sql .= $hookmanager->resPrint;
3657 $result = $this->db->query($sql);
3658 if ($result) {
3659 $obj = $this->db->fetch_object($result);
3660 $this->stats_propale['customers'] = (int) $obj->nb_customers;
3661 $this->stats_propale['nb'] = (int) $obj->nb;
3662 $this->stats_propale['rows'] = (int) $obj->nb_rows;
3663 $this->stats_propale['qty'] = $obj->qty ? (float) $obj->qty : 0;
3664
3665 // if it's a virtual product, maybe it is in proposal by extension
3666 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3667 $TFather = $this->getFather();
3668 if (is_array($TFather) && !empty($TFather)) {
3669 foreach ($TFather as &$fatherData) {
3670 $pFather = new Product($this->db);
3671 $pFather->id = (int) $fatherData['id'];
3672 $qtyCoef = $fatherData['qty'];
3673
3674 if ($fatherData['incdec']) {
3675 $pFather->load_stats_propale($socid);
3676
3677 if (!empty($pFather->stats_propale)) {
3678 $this->stats_propale['customers'] += $pFather->stats_propale['customers'];
3679 $this->stats_propale['nb'] += $pFather->stats_propale['nb'];
3680 $this->stats_propale['rows'] += $pFather->stats_propale['rows'];
3681 $this->stats_propale['qty'] += $pFather->stats_propale['qty'] * $qtyCoef;
3682 }
3683 }
3684 }
3685 }
3686 }
3687
3688 $parameters = array('socid' => $socid);
3689 $reshook = $hookmanager->executeHooks('loadStatsCustomerProposal', $parameters, $this, $action);
3690 if ($reshook > 0) {
3691 $this->stats_propale = $hookmanager->resArray['stats_propale'];
3692 }
3693
3694 return 1;
3695 } else {
3696 $this->error = $this->db->error();
3697 return -1;
3698 }
3699 }
3700
3701
3702 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3709 public function load_stats_proposal_supplier($socid = 0)
3710 {
3711 // phpcs:enable
3712 global $user, $hookmanager, $action;
3713
3714 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,";
3715 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3716 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as pd";
3717 $sql .= ", ".$this->db->prefix()."supplier_proposal as p";
3718 $sql .= ", ".$this->db->prefix()."societe as s";
3719 $sql .= " WHERE p.rowid = pd.fk_supplier_proposal";
3720 $sql .= " AND p.fk_soc = s.rowid";
3721 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
3722 $sql .= " AND pd.fk_product = ".((int) $this->id);
3723 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3724 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = p.fk_soc AND sc.fk_user = ".((int) $user->id);
3725 }
3726 //$sql.= " AND pr.fk_statut != 0";
3727 if ($socid > 0) {
3728 $sql .= " AND p.fk_soc = ".((int) $socid);
3729 }
3730
3731 $result = $this->db->query($sql);
3732 if ($result) {
3733 $obj = $this->db->fetch_object($result);
3734 $this->stats_proposal_supplier['suppliers'] = $obj->nb_suppliers;
3735 $this->stats_proposal_supplier['nb'] = $obj->nb;
3736 $this->stats_proposal_supplier['rows'] = $obj->nb_rows;
3737 $this->stats_proposal_supplier['qty'] = $obj->qty ? $obj->qty : 0;
3738
3739 $parameters = array('socid' => $socid);
3740 $reshook = $hookmanager->executeHooks('loadStatsSupplierProposal', $parameters, $this, $action);
3741 if ($reshook > 0) {
3742 $this->stats_proposal_supplier = $hookmanager->resArray['stats_proposal_supplier'];
3743 }
3744
3745 return 1;
3746 } else {
3747 $this->error = $this->db->error();
3748 return -1;
3749 }
3750 }
3751
3752
3753 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3762 public function load_stats_commande($socid = 0, $filtrestatut = '', $forVirtualStock = 0)
3763 {
3764 // phpcs:enable
3765 global $user, $hookmanager, $action;
3766
3767
3768 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3769 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3770 $sql .= " FROM ".$this->db->prefix()."commandedet as cd";
3771 $sql .= ", ".$this->db->prefix()."commande as c";
3772 $sql .= ", ".$this->db->prefix()."societe as s";
3773 $sql .= " WHERE c.rowid = cd.fk_commande";
3774 $sql .= " AND c.fk_soc = s.rowid";
3775 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'commande').")";
3776 $sql .= " AND cd.fk_product = ".((int) $this->id);
3777 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3778 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3779 }
3780 if ($socid > 0) {
3781 $sql .= " AND c.fk_soc = ".((int) $socid);
3782 }
3783 if ($filtrestatut != '') {
3784 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3785 }
3786 // Add where from hooks
3787 $parameters = array('socid' => $socid, 'type_element' => 'order');
3788 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3789 $sql .= $hookmanager->resPrint;
3790 $result = $this->db->query($sql);
3791 if ($result) {
3792 $obj = $this->db->fetch_object($result);
3793 $this->stats_commande['customers'] = (int) $obj->nb_customers;
3794 $this->stats_commande['nb'] = (int) $obj->nb;
3795 $this->stats_commande['rows'] = (int) $obj->nb_rows;
3796 $this->stats_commande['qty'] = $obj->qty ? (float) $obj->qty : 0;
3797
3798 // if it's a virtual product, maybe it is in order by extension
3799 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3800 $TFather = $this->getFather();
3801 if (is_array($TFather) && !empty($TFather)) {
3802 foreach ($TFather as &$fatherData) {
3803 $pFather = new Product($this->db);
3804 $pFather->id = $fatherData['id'];
3805 $qtyCoef = $fatherData['qty'];
3806
3807 if ($fatherData['incdec']) {
3808 $pFather->load_stats_commande($socid, $filtrestatut);
3809
3810 $this->stats_commande['customers'] += $pFather->stats_commande['customers'];
3811 $this->stats_commande['nb'] += $pFather->stats_commande['nb'];
3812 $this->stats_commande['rows'] += $pFather->stats_commande['rows'];
3813 $this->stats_commande['qty'] += $pFather->stats_commande['qty'] * $qtyCoef;
3814 }
3815 }
3816 }
3817 }
3818
3819 // If stock decrease is on invoice validation, the theoretical stock continue to
3820 // count the orders lines containing product in theoretical stock when some are already removed by invoice validation.
3821 if ($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
3822 // Do the request using an UNION instead of a OR in the JOIN that is very slow
3823 if (getDolGlobalString('DECREASE_ONLY_UNINVOICEDPRODUCTS')) {
3824 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is on, we make a compensation but only if order not yet invoice.
3825 $adeduire = 0;
3826 $sql = "SELECT SUM(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3827 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3828 //$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'))";
3829 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_target = f.rowid AND el.targettype = 'facture' AND sourcetype = 'commande' ";
3830 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid";
3831 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3832 $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
3833
3834 $sql .= " UNION ";
3835
3836 $sql .= "SELECT SUM(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3837 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3838 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_source = f.rowid AND el.targettype = 'commande' AND sourcetype = 'facture' ";
3839 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_target = c.rowid";
3840 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3841 $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
3842 // Add where from hooks
3843 $parameters = array('socid' => $socid, 'type_element' => 'order');
3844 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3845 $sql .= $hookmanager->resPrint;
3846 $resql = $this->db->query($sql);
3847 if ($resql) {
3848 while ($obj = $this->db->fetch_object($resql)) {
3849 $adeduire += (float) $obj->count;
3850 }
3851 }
3852
3853 $this->stats_commande['qty'] -= $adeduire;
3854 } else {
3855 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is off, we make a compensation with lines of invoices linked to the order
3856 include_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
3857
3858 // For every order having invoice already validated we need to decrease stock cause it's in physical stock
3859 $adeduire = 0;
3860 $sql = "SELECT sum(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3861 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3862 //$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'))";
3863 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_target = f.rowid AND el.targettype = 'facture' AND sourcetype = 'commande' ";
3864 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid";
3865 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product = ".((int) $this->id);
3866 $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
3867
3868 $sql .= " UNION ALL ";
3869
3870 $sql .= "SELECT sum(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3871 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3872 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_source = f.rowid AND el.targettype = 'commande' AND sourcetype = 'facture' ";
3873 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_target = c.rowid";
3874 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product = ".((int) $this->id);
3875 $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
3876
3877 dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3878 // Add where from hooks
3879 $parameters = array('socid' => $socid, 'type_element' => 'order');
3880 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3881 $sql .= $hookmanager->resPrint;
3882 $resql = $this->db->query($sql);
3883 if ($resql) {
3884 while ($obj = $this->db->fetch_object($resql)) {
3885 $adeduire += (float) $obj->count;
3886 }
3887 } else {
3888 $this->error = $this->db->error();
3889 return -1;
3890 }
3891
3892 $this->stats_commande['qty'] -= $adeduire;
3893 }
3894 }
3895
3896 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3897 $reshook = $hookmanager->executeHooks('loadStatsCustomerOrder', $parameters, $this, $action);
3898 if ($reshook > 0) {
3899 $this->stats_commande = $hookmanager->resArray['stats_commande'];
3900 }
3901 return 1;
3902 } else {
3903 $this->error = $this->db->error();
3904 return -1;
3905 }
3906 }
3907
3908 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3918 public function load_stats_commande_fournisseur($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3919 {
3920 // phpcs:enable
3921 global $user, $hookmanager, $action;
3922
3923 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,";
3924 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3925 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as cd";
3926 $sql .= ", ".$this->db->prefix()."commande_fournisseur as c";
3927 $sql .= ", ".$this->db->prefix()."societe as s";
3928 $sql .= " WHERE c.rowid = cd.fk_commande";
3929 $sql .= " AND c.fk_soc = s.rowid";
3930 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
3931 $sql .= " AND cd.fk_product = ".((int) $this->id);
3932 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3933 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3934 }
3935 if ($socid > 0) {
3936 $sql .= " AND c.fk_soc = ".((int) $socid);
3937 }
3938 if ($filtrestatut != '') {
3939 $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")"; // Peut valoir 0
3940 }
3941 if (!empty($dateofvirtualstock)) {
3942 $sql .= " AND c.date_livraison <= '".$this->db->idate($dateofvirtualstock)."'";
3943 }
3944
3945 $result = $this->db->query($sql);
3946 if ($result) {
3947 $obj = $this->db->fetch_object($result);
3948 $this->stats_commande_fournisseur['suppliers'] = $obj->nb_suppliers;
3949 $this->stats_commande_fournisseur['nb'] = $obj->nb;
3950 $this->stats_commande_fournisseur['rows'] = $obj->nb_rows;
3951 $this->stats_commande_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3952
3953 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3954 $reshook = $hookmanager->executeHooks('loadStatsSupplierOrder', $parameters, $this, $action);
3955 if ($reshook > 0) {
3956 $this->stats_commande_fournisseur = $hookmanager->resArray['stats_commande_fournisseur'];
3957 }
3958
3959 return 1;
3960 } else {
3961 $this->error = $this->db->error().' sql='.$sql;
3962 return -1;
3963 }
3964 }
3965
3966 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3976 public function load_stats_sending($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $filterShipmentStatus = '')
3977 {
3978 // phpcs:enable
3979 global $user, $hookmanager, $action;
3980
3981 $sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,";
3982 $sql .= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty";
3983 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
3984 $sql .= ", ".$this->db->prefix()."commandedet as cd";
3985 $sql .= ", ".$this->db->prefix()."commande as c";
3986 $sql .= ", ".$this->db->prefix()."expedition as e";
3987 $sql .= ", ".$this->db->prefix()."societe as s";
3988 $sql .= " WHERE e.rowid = ed.fk_expedition";
3989 $sql .= " AND c.rowid = cd.fk_commande";
3990 $sql .= " AND e.fk_soc = s.rowid";
3991 $sql .= " AND e.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'expedition').")";
3992 $sql .= " AND ed.fk_elementdet = cd.rowid";
3993 $sql .= " AND cd.fk_product = ".((int) $this->id);
3994 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3995 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = e.fk_soc AND sc.fk_user = ".((int) $user->id);
3996 }
3997 if ($socid > 0) {
3998 $sql .= " AND e.fk_soc = ".((int) $socid);
3999 }
4000 if ($filtrestatut != '') {
4001 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
4002 }
4003 if (!empty($filterShipmentStatus)) {
4004 $sql .= " AND e.fk_statut IN (".$this->db->sanitize($filterShipmentStatus).")";
4005 }
4006
4007 $result = $this->db->query($sql);
4008 if ($result) {
4009 $obj = $this->db->fetch_object($result);
4010 $this->stats_expedition['customers'] = (int) $obj->nb_customers;
4011 $this->stats_expedition['nb'] = (int) $obj->nb;
4012 $this->stats_expedition['rows'] = (int) $obj->nb_rows;
4013 $this->stats_expedition['qty'] = $obj->qty ? (float) $obj->qty : 0;
4014
4015 // if it's a virtual product, maybe it is in sending by extension
4016 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4017 $TFather = $this->getFather();
4018 if (is_array($TFather) && !empty($TFather)) {
4019 foreach ($TFather as &$fatherData) {
4020 $pFather = new Product($this->db);
4021 $pFather->id = $fatherData['id'];
4022 $qtyCoef = $fatherData['qty'];
4023
4024 if ($fatherData['incdec']) {
4025 $pFather->load_stats_sending($socid, $filtrestatut, $forVirtualStock);
4026
4027 if (!empty($pFather->stats_expedition)) {
4028 $this->stats_expedition['customers'] += $pFather->stats_expedition['customers'];
4029 $this->stats_expedition['nb'] += $pFather->stats_expedition['nb'];
4030 $this->stats_expedition['rows'] += $pFather->stats_expedition['rows'];
4031 $this->stats_expedition['qty'] += $pFather->stats_expedition['qty'] * $qtyCoef;
4032 }
4033 }
4034 }
4035 }
4036 }
4037
4038 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock, 'filterShipmentStatus' => $filterShipmentStatus);
4039 $reshook = $hookmanager->executeHooks('loadStatsSending', $parameters, $this, $action);
4040 if ($reshook > 0) {
4041 $this->stats_expedition = $hookmanager->resArray['stats_expedition'];
4042 }
4043
4044 return 1;
4045 } else {
4046 $this->error = $this->db->error();
4047 return -1;
4048 }
4049 }
4050
4051 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4061 public function load_stats_reception($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
4062 {
4063 // phpcs:enable
4064 global $user, $hookmanager, $action;
4065
4066 $sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_suppliers, COUNT(DISTINCT cf.rowid) as nb,";
4067 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4068 $sql .= " FROM ".$this->db->prefix()."receptiondet_batch as fd";
4069 $sql .= ", ".$this->db->prefix()."commande_fournisseur as cf";
4070 $sql .= ", ".$this->db->prefix()."societe as s";
4071 $sql .= " WHERE cf.rowid = fd.fk_element";
4072 $sql .= " AND cf.fk_soc = s.rowid";
4073 $sql .= " AND cf.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
4074 $sql .= " AND fd.fk_product = ".((int) $this->id);
4075 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
4076 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = cf.fk_soc AND sc.fk_user = ".((int) $user->id);
4077 }
4078 if ($socid > 0) {
4079 $sql .= " AND cf.fk_soc = ".((int) $socid);
4080 }
4081 if ($filtrestatut != '') {
4082 $sql .= " AND cf.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
4083 }
4084 if (!empty($dateofvirtualstock)) {
4085 $sql .= " AND fd.datec <= '".$this->db->idate($dateofvirtualstock)."'";
4086 }
4087
4088 $result = $this->db->query($sql);
4089 if ($result) {
4090 $obj = $this->db->fetch_object($result);
4091 $this->stats_reception['suppliers'] = $obj->nb_suppliers;
4092 $this->stats_reception['nb'] = $obj->nb;
4093 $this->stats_reception['rows'] = $obj->nb_rows;
4094 $this->stats_reception['qty'] = $obj->qty ? $obj->qty : 0;
4095
4096 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
4097 $reshook = $hookmanager->executeHooks('loadStatsReception', $parameters, $this, $action);
4098 if ($reshook > 0) {
4099 $this->stats_reception = $hookmanager->resArray['stats_reception'];
4100 }
4101
4102 return 1;
4103 } else {
4104 $this->error = $this->db->error();
4105 return -1;
4106 }
4107 }
4108
4109 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4120 public function load_stats_inproduction($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null, $warehouseid = 0)
4121 {
4122 // phpcs:enable
4123 global $user, $hookmanager, $action;
4124
4125 $serviceStockIsEnabled = isModEnabled("service") && getDolGlobalString('STOCK_SUPPORTS_SERVICES');
4126
4127 $sql = "SELECT COUNT(DISTINCT m.fk_soc) as nb_customers, COUNT(DISTINCT m.rowid) as nb,";
4128 $sql .= " COUNT(mp.rowid) as nb_rows, SUM(mp.qty) as qty, role";
4129 $sql .= " FROM ".$this->db->prefix()."mrp_production as mp";
4130 $sql .= ", ".$this->db->prefix()."mrp_mo as m";
4131 $sql .= " LEFT JOIN ".$this->db->prefix()."societe as s ON s.rowid = m.fk_soc";
4132 $sql .= " WHERE m.rowid = mp.fk_mo";
4133 $sql .= " AND m.entity IN (".getEntity(($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE')) ? 'stock' : 'mrp').")";
4134 $sql .= " AND mp.fk_product = ".((int) $this->id);
4135 $sql .= " AND (mp.disable_stock_change IN (0) OR mp.disable_stock_change IS NULL)";
4136 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
4137 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = m.fk_soc AND sc.fk_user = ".((int) $user->id);
4138 }
4139 if ($socid > 0) {
4140 $sql .= " AND m.fk_soc = ".((int) $socid);
4141 }
4142 if ($filtrestatut != '') {
4143 $sql .= " AND m.status IN (".$this->db->sanitize($filtrestatut).")";
4144 }
4145 if (!empty($dateofvirtualstock)) {
4146 $sql .= " AND m.date_valid <= '".$this->db->idate($dateofvirtualstock)."'"; // better date to code ? end of production ?
4147 }
4148 if (!$serviceStockIsEnabled) {
4149 $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))";
4150 }
4151 if (!empty($warehouseid)) {
4152 $sql .= " AND m.fk_warehouse = ".((int) $warehouseid);
4153 }
4154 $sql .= " GROUP BY role";
4155
4156 if ($warehouseid) {
4157 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
4158 } else {
4159 $this->stats_mrptoconsume['customers'] = 0;
4160 $this->stats_mrptoconsume['nb'] = 0;
4161 $this->stats_mrptoconsume['rows'] = 0;
4162 $this->stats_mrptoconsume['qty'] = 0.0;
4163 $this->stats_mrptoproduce['customers'] = 0;
4164 $this->stats_mrptoproduce['nb'] = 0;
4165 $this->stats_mrptoproduce['rows'] = 0;
4166 $this->stats_mrptoproduce['qty'] = 0.0;
4167 }
4168
4169 $result = $this->db->query($sql);
4170 if ($result) {
4171 while ($obj = $this->db->fetch_object($result)) {
4172 if ($obj->role == 'toconsume' && empty($warehouseid)) {
4173 $this->stats_mrptoconsume['customers'] += (int) $obj->nb_customers;
4174 $this->stats_mrptoconsume['nb'] += (int) $obj->nb;
4175 $this->stats_mrptoconsume['rows'] += (int) $obj->nb_rows;
4176 $this->stats_mrptoconsume['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4177 }
4178 if ($obj->role == 'consumed' && empty($warehouseid)) {
4179 //$this->stats_mrptoconsume['customers'] += $obj->nb_customers;
4180 //$this->stats_mrptoconsume['nb'] += $obj->nb;
4181 //$this->stats_mrptoconsume['rows'] += $obj->nb_rows;
4182 $this->stats_mrptoconsume['qty'] -= ($obj->qty ? (float) $obj->qty : 0.0);
4183 }
4184 if ($obj->role == 'toproduce') {
4185 if ($warehouseid) {
4186 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4187 } else {
4188 $this->stats_mrptoproduce['customers'] += (int) $obj->nb_customers;
4189 $this->stats_mrptoproduce['nb'] += (int) $obj->nb;
4190 $this->stats_mrptoproduce['rows'] += (int) $obj->nb_rows;
4191 $this->stats_mrptoproduce['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4192 }
4193 }
4194 if ($obj->role == 'produced') {
4195 //$this->stats_mrptoproduce['customers'] += $obj->nb_customers;
4196 //$this->stats_mrptoproduce['nb'] += $obj->nb;
4197 //$this->stats_mrptoproduce['rows'] += $obj->nb_rows;
4198 if ($warehouseid) {
4199 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
4200 } else {
4201 $this->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
4202 }
4203 }
4204 }
4205
4206 // Clean data
4207 if ($warehouseid) {
4208 if ($this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] < 0) {
4209 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
4210 }
4211 } else {
4212 if ($this->stats_mrptoconsume['qty'] < 0) {
4213 $this->stats_mrptoconsume['qty'] = 0;
4214 }
4215 if ($this->stats_mrptoproduce['qty'] < 0) {
4216 $this->stats_mrptoproduce['qty'] = 0;
4217 }
4218 }
4219
4220 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
4221 $reshook = $hookmanager->executeHooks('loadStatsInProduction', $parameters, $this, $action);
4222 if ($reshook > 0) {
4223 $this->stats_mrptoproduce = $hookmanager->resArray['stats_mrptoproduce'];
4224 }
4225
4226 return 1;
4227 } else {
4228 $this->error = $this->db->error();
4229 return -1;
4230 }
4231 }
4232
4233 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4240 public function load_stats_contrat($socid = 0)
4241 {
4242 // phpcs:enable
4243 global $user, $hookmanager, $action;
4244
4245 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
4246 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
4247 $sql .= " FROM ".$this->db->prefix()."contratdet as cd";
4248 $sql .= ", ".$this->db->prefix()."contrat as c";
4249 $sql .= ", ".$this->db->prefix()."societe as s";
4250 $sql .= " WHERE c.rowid = cd.fk_contrat";
4251 $sql .= " AND c.fk_soc = s.rowid";
4252 $sql .= " AND c.entity IN (".getEntity('contract').")";
4253 $sql .= " AND cd.fk_product = ".((int) $this->id);
4254 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4255 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
4256 }
4257 //$sql.= " AND c.statut != 0";
4258 if ($socid > 0) {
4259 $sql .= " AND c.fk_soc = ".((int) $socid);
4260 }
4261
4262 $result = $this->db->query($sql);
4263 if ($result) {
4264 $obj = $this->db->fetch_object($result);
4265 $this->stats_contrat['customers'] = (int) $obj->nb_customers;
4266 $this->stats_contrat['nb'] = (int) $obj->nb;
4267 $this->stats_contrat['rows'] = (int) $obj->nb_rows;
4268 $this->stats_contrat['qty'] = $obj->qty ? (float) $obj->qty : 0;
4269
4270 // if it's a virtual product, maybe it is in contract by extension
4271 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4272 $TFather = $this->getFather();
4273 if (is_array($TFather) && !empty($TFather)) {
4274 foreach ($TFather as &$fatherData) {
4275 $pFather = new Product($this->db);
4276 $pFather->id = $fatherData['id'];
4277 $qtyCoef = $fatherData['qty'];
4278
4279 if ($fatherData['incdec']) {
4280 $pFather->load_stats_contrat($socid);
4281
4282 if (!empty($pFather->stats_contrat)) {
4283 $this->stats_contrat['customers'] += $pFather->stats_contrat['customers'];
4284 $this->stats_contrat['nb'] += $pFather->stats_contrat['nb'];
4285 $this->stats_contrat['rows'] += $pFather->stats_contrat['rows'];
4286 $this->stats_contrat['qty'] += $pFather->stats_contrat['qty'] * $qtyCoef;
4287 }
4288 }
4289 }
4290 }
4291 }
4292
4293 $parameters = array('socid' => $socid);
4294 $reshook = $hookmanager->executeHooks('loadStatsContract', $parameters, $this, $action);
4295 if ($reshook > 0) {
4296 $this->stats_contrat = $hookmanager->resArray['stats_contrat'];
4297 }
4298
4299 return 1;
4300 } else {
4301 $this->error = $this->db->error().' sql='.$sql;
4302 return -1;
4303 }
4304 }
4305
4306 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4313 public function load_stats_facture($socid = 0)
4314 {
4315 // phpcs:enable
4316 global $user, $hookmanager, $action;
4317
4318 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4319 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(".$this->db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1').") as qty";
4320 $sql .= " FROM ".$this->db->prefix()."facturedet as fd";
4321 $sql .= ", ".$this->db->prefix()."facture as f";
4322 $sql .= ", ".$this->db->prefix()."societe as s";
4323 $sql .= " WHERE f.rowid = fd.fk_facture";
4324 $sql .= " AND f.fk_soc = s.rowid";
4325 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4326 $sql .= " AND fd.fk_product = ".((int) $this->id);
4327 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4328 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4329 }
4330 //$sql.= " AND f.fk_statut != 0";
4331 if ($socid > 0) {
4332 $sql .= " AND f.fk_soc = ".((int) $socid);
4333 }
4334
4335 $result = $this->db->query($sql);
4336 if ($result) {
4337 $obj = $this->db->fetch_object($result);
4338 $this->stats_facture['customers'] = (int) $obj->nb_customers;
4339 $this->stats_facture['nb'] = (int) $obj->nb;
4340 $this->stats_facture['rows'] = (int) $obj->nb_rows;
4341 $this->stats_facture['qty'] = $obj->qty ? (float) $obj->qty : 0;
4342
4343 // if it's a virtual product, maybe it is in invoice by extension
4344 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4345 $TFather = $this->getFather();
4346 if (is_array($TFather) && !empty($TFather)) {
4347 foreach ($TFather as &$fatherData) {
4348 $pFather = new Product($this->db);
4349 $pFather->id = $fatherData['id'];
4350 $qtyCoef = $fatherData['qty'];
4351
4352 if ($fatherData['incdec']) {
4353 $pFather->load_stats_facture($socid);
4354
4355 if (!empty($pFather->stats_facture)) {
4356 $this->stats_facture['customers'] += $pFather->stats_facture['customers'];
4357 $this->stats_facture['nb'] += $pFather->stats_facture['nb'];
4358 $this->stats_facture['rows'] += $pFather->stats_facture['rows'];
4359 $this->stats_facture['qty'] += $pFather->stats_facture['qty'] * $qtyCoef;
4360 }
4361 }
4362 }
4363 }
4364 }
4365
4366 $parameters = array('socid' => $socid);
4367 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoice', $parameters, $this, $action);
4368 if ($reshook > 0) {
4369 $this->stats_facture = $hookmanager->resArray['stats_facture'];
4370 }
4371
4372 return 1;
4373 } else {
4374 $this->error = $this->db->error();
4375 return -1;
4376 }
4377 }
4378
4379
4380 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4387 public function load_stats_facturerec($socid = 0)
4388 {
4389 // phpcs:enable
4390 global $user, $hookmanager, $action;
4391
4392 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4393 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4394 $sql .= " FROM ".MAIN_DB_PREFIX."facturedet_rec as fd";
4395 $sql .= ", ".MAIN_DB_PREFIX."facture_rec as f";
4396 $sql .= ", ".MAIN_DB_PREFIX."societe as s";
4397 $sql .= " WHERE f.rowid = fd.fk_facture";
4398 $sql .= " AND f.fk_soc = s.rowid";
4399 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4400 $sql .= " AND fd.fk_product = ".((int) $this->id);
4401 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4402 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4403 }
4404 //$sql.= " AND f.fk_statut != 0";
4405 if ($socid > 0) {
4406 $sql .= " AND f.fk_soc = ".((int) $socid);
4407 }
4408
4409 $result = $this->db->query($sql);
4410 if ($result) {
4411 $obj = $this->db->fetch_object($result);
4412 $this->stats_facturerec['customers'] = (int) $obj->nb_customers;
4413 $this->stats_facturerec['nb'] = (int) $obj->nb;
4414 $this->stats_facturerec['rows'] = (int) $obj->nb_rows;
4415 $this->stats_facturerec['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4416
4417 // if it's a virtual product, maybe it is in invoice by extension
4418 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4419 $TFather = $this->getFather();
4420 if (is_array($TFather) && !empty($TFather)) {
4421 foreach ($TFather as &$fatherData) {
4422 $pFather = new Product($this->db);
4423 $pFather->id = $fatherData['id'];
4424 $qtyCoef = $fatherData['qty'];
4425
4426 if ($fatherData['incdec']) {
4427 $pFather->load_stats_facture($socid);
4428
4429 $this->stats_facturerec['customers'] += $pFather->stats_facturerec['customers'];
4430 $this->stats_facturerec['nb'] += $pFather->stats_facturerec['nb'];
4431 $this->stats_facturerec['rows'] += $pFather->stats_facturerec['rows'];
4432 $this->stats_facturerec['qty'] += $pFather->stats_facturerec['qty'] * $qtyCoef;
4433 }
4434 }
4435 }
4436 }
4437
4438 $parameters = array('socid' => $socid);
4439 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoiceRec', $parameters, $this, $action);
4440 if ($reshook > 0) {
4441 $this->stats_facturerec = $hookmanager->resArray['stats_facturerec'];
4442 }
4443
4444 return 1;
4445 } else {
4446 $this->error = $this->db->error();
4447 return -1;
4448 }
4449 }
4450
4451 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4458 public function load_stats_facture_fournisseur($socid = 0)
4459 {
4460 // phpcs:enable
4461 global $user, $hookmanager, $action;
4462
4463 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
4464 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4465 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as fd";
4466 $sql .= ", ".$this->db->prefix()."facture_fourn as f";
4467 $sql .= ", ".$this->db->prefix()."societe as s";
4468 $sql .= " WHERE f.rowid = fd.fk_facture_fourn";
4469 $sql .= " AND f.fk_soc = s.rowid";
4470 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4471 $sql .= " AND fd.fk_product = ".((int) $this->id);
4472 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4473 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4474 }
4475 //$sql.= " AND f.fk_statut != 0";
4476 if ($socid > 0) {
4477 $sql .= " AND f.fk_soc = ".((int) $socid);
4478 }
4479
4480 $result = $this->db->query($sql);
4481 if ($result) {
4482 $obj = $this->db->fetch_object($result);
4483 $this->stats_facture_fournisseur['suppliers'] = (int) $obj->nb_suppliers;
4484 $this->stats_facture_fournisseur['nb'] = (int) $obj->nb;
4485 $this->stats_facture_fournisseur['rows'] = (int) $obj->nb_rows;
4486 $this->stats_facture_fournisseur['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4487
4488 $parameters = array('socid' => $socid);
4489 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoice', $parameters, $this, $action);
4490 if ($reshook > 0) {
4491 $this->stats_facture_fournisseur = $hookmanager->resArray['stats_facture_fournisseur'];
4492 }
4493
4494 return 1;
4495 } else {
4496 $this->error = $this->db->error();
4497 return -1;
4498 }
4499 }
4500
4501 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4508 public function load_stats_facturefournrec($socid = 0)
4509 {
4510 // phpcs:enable
4511 global $user, $hookmanager, $action;
4512
4513 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
4514 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4515 $sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn_det_rec as fd";
4516 $sql .= ", ".MAIN_DB_PREFIX."facture_fourn_rec as f";
4517 $sql .= ", ".MAIN_DB_PREFIX."societe as s";
4518 if (!$user->hasRight('societe', 'client', 'voir')) {
4519 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
4520 }
4521 $sql .= " WHERE f.rowid = fd.fk_facture";
4522 $sql .= " AND f.fk_soc = s.rowid";
4523 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4524 $sql .= " AND fd.fk_product = ".((int) $this->id);
4525 if (!$user->hasRight('societe', 'client', 'voir')) {
4526 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4527 }
4528 //$sql.= " AND f.fk_statut != 0";
4529 if ($socid > 0) {
4530 $sql .= " AND f.fk_soc = ".((int) $socid);
4531 }
4532
4533 $result = $this->db->query($sql);
4534 if ($result) {
4535 $obj = $this->db->fetch_object($result);
4536 $this->stats_facturefournrec['suppliers'] = (int) $obj->nb_suppliers;
4537 $this->stats_facturefournrec['nb'] = (int) $obj->nb;
4538 $this->stats_facturefournrec['rows'] = (int) $obj->nb_rows;
4539 $this->stats_facturefournrec['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4540
4541 $parameters = array('socid' => $socid);
4542 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoiceRec', $parameters, $this, $action);
4543 if ($reshook > 0) {
4544 $this->stats_facturefournrec = $hookmanager->resArray['stats_facturefournrec'];
4545 }
4546
4547 return 1;
4548 } else {
4549 $this->error = $this->db->error();
4550 return -1;
4551 }
4552 }
4553
4554 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4563 private function _get_stats($sql, $mode, $year = 0)
4564 {
4565 // phpcs:enable
4566 $tab = array();
4567
4568 $resql = $this->db->query($sql);
4569 if ($resql) {
4570 $num = $this->db->num_rows($resql);
4571 $i = 0;
4572 while ($i < $num) {
4573 $arr = $this->db->fetch_array($resql);
4574 if (is_array($arr)) {
4575 $keyfortab = (string) $arr[1];
4576 if ($year == -1) {
4577 $keyfortab = substr($keyfortab, -2);
4578 }
4579
4580 if ($mode == 'byunit') {
4581 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[0]; // 1st field
4582 } elseif ($mode == 'bynumber') {
4583 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4584 } elseif ($mode == 'byamount') {
4585 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4586 } else {
4587 // Bad value for $mode
4588 return -1;
4589 }
4590 }
4591 $i++;
4592 }
4593 } else {
4594 $this->error = $this->db->error().' sql='.$sql;
4595 return -1;
4596 }
4597
4598 if (empty($year)) {
4599 $year = dol_print_date(time(), '%Y');
4600 $month = dol_print_date(time(), '%m');
4601 } elseif ($year == -1) {
4602 $year = '';
4603 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4604 } else {
4605 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4606 }
4607
4608 $result = array();
4609
4610 for ($j = 0; $j < 12; $j++) {
4611 // $idx is 'D', 'N', 'O', 'S', ... (First letter of month in user language)
4612 $idx = ucfirst(dol_trunc(dol_print_date(dol_mktime(12, 0, 0, $month, 1, 1970), "%b"), 1, 'right', 'UTF-8', 1));
4613
4614 // print $idx.'-'.$year.'-'.$month.'<br>';
4615 $result[$j] = array($idx, isset($tab[$year.$month]) ? $tab[$year.$month] : 0);
4616 // $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0);
4617
4618 $month = "0".($month - 1);
4619 if (dol_strlen($month) == 3) {
4620 $month = substr($month, 1);
4621 }
4622 if ($month == 0) {
4623 $month = 12;
4624 if ($year !== '') { // $year is '' when we want stats for all years
4625 $year -= 1;
4626 }
4627 }
4628 }
4629
4630 return array_reverse($result);
4631 }
4632
4633
4634 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4645 public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4646 {
4647 // phpcs:enable
4648 global $user;
4649
4650 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4651 if ($mode == 'bynumber') {
4652 $sql .= ", count(DISTINCT f.rowid)";
4653 }
4654 $sql .= ", sum(d.total_ht) as total_ht";
4655 $sql .= " FROM ".$this->db->prefix()."facturedet as d, ".$this->db->prefix()."facture as f, ".$this->db->prefix()."societe as s";
4656 if ($filteronproducttype >= 0) {
4657 $sql .= ", ".$this->db->prefix()."product as p";
4658 }
4659 if (!$user->hasRight('societe', 'client', 'voir')) {
4660 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4661 }
4662 $sql .= " WHERE f.rowid = d.fk_facture";
4663 if ($this->id > 0) {
4664 $sql .= " AND d.fk_product = ".((int) $this->id);
4665 } else {
4666 $sql .= " AND d.fk_product > 0";
4667 }
4668 if ($filteronproducttype >= 0) {
4669 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4670 }
4671 $sql .= " AND f.fk_soc = s.rowid";
4672 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4673 if (!$user->hasRight('societe', 'client', 'voir')) {
4674 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4675 }
4676 if ($socid > 0) {
4677 $sql .= " AND f.fk_soc = ".((int) $socid);
4678 }
4679 $sql .= $morefilter;
4680 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4681 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4682
4683 return $this->_get_stats($sql, $mode, $year);
4684 }
4685
4686
4687 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4698 public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4699 {
4700 // phpcs:enable
4701 global $user;
4702
4703 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4704 if ($mode == 'bynumber') {
4705 $sql .= ", count(DISTINCT f.rowid)";
4706 }
4707 $sql .= ", sum(d.total_ht) as total_ht";
4708 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as d, ".$this->db->prefix()."facture_fourn as f, ".$this->db->prefix()."societe as s";
4709 if ($filteronproducttype >= 0) {
4710 $sql .= ", ".$this->db->prefix()."product as p";
4711 }
4712 if (!$user->hasRight('societe', 'client', 'voir')) {
4713 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4714 }
4715 $sql .= " WHERE f.rowid = d.fk_facture_fourn";
4716 if ($this->id > 0) {
4717 $sql .= " AND d.fk_product = ".((int) $this->id);
4718 } else {
4719 $sql .= " AND d.fk_product > 0";
4720 }
4721 if ($filteronproducttype >= 0) {
4722 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4723 }
4724 $sql .= " AND f.fk_soc = s.rowid";
4725 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4726 if (!$user->hasRight('societe', 'client', 'voir')) {
4727 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4728 }
4729 if ($socid > 0) {
4730 $sql .= " AND f.fk_soc = ".((int) $socid);
4731 }
4732 $sql .= $morefilter;
4733 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4734 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4735
4736 return $this->_get_stats($sql, $mode, $year);
4737 }
4738
4739 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4750 public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4751 {
4752 // phpcs:enable
4753 global $user, $hookmanager;
4754
4755 $sql = "SELECT sum(d.qty) as qty, date_format(p.datep, '%Y%m')";
4756 if ($mode == 'bynumber') {
4757 $sql .= ", count(DISTINCT p.rowid)";
4758 }
4759 $sql .= ", sum(d.total_ht) as total_ht";
4760 $sql .= " FROM ".$this->db->prefix()."propaldet as d, ".$this->db->prefix()."propal as p, ".$this->db->prefix()."societe as s";
4761 if ($filteronproducttype >= 0) {
4762 $sql .= ", ".$this->db->prefix()."product as prod";
4763 }
4764 if (!$user->hasRight('societe', 'client', 'voir')) {
4765 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4766 }
4767 $sql .= " WHERE p.rowid = d.fk_propal";
4768 if ($this->id > 0) {
4769 $sql .= " AND d.fk_product = ".((int) $this->id);
4770 } else {
4771 $sql .= " AND d.fk_product > 0";
4772 }
4773 if ($filteronproducttype >= 0) {
4774 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4775 }
4776 $sql .= " AND p.fk_soc = s.rowid";
4777 $sql .= " AND p.entity IN (".getEntity('propal').")";
4778 if (!$user->hasRight('societe', 'client', 'voir')) {
4779 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4780 }
4781 if ($socid > 0) {
4782 $sql .= " AND p.fk_soc = ".((int) $socid);
4783 }
4784 $sql .= $morefilter;
4785 $parameters = array('socid' => $user->socid);
4786 $hookmanager->executeHooks('productGetNbPropal', $parameters, $this); // Note that $action and $object may have been modified by hook
4787 $sql .= $hookmanager->resPrint;
4788 $sql .= " GROUP BY date_format(p.datep,'%Y%m')";
4789 $sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC";
4790 return $this->_get_stats($sql, $mode, $year);
4791 }
4792
4793 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4804 public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4805 {
4806 // phpcs:enable
4807 global $user;
4808
4809 $sql = "SELECT sum(d.qty) as qty, date_format(p.date_valid, '%Y%m')";
4810 if ($mode == 'bynumber') {
4811 $sql .= ", count(DISTINCT p.rowid)";
4812 }
4813 $sql .= ", sum(d.total_ht) as total_ht";
4814 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as d, ".$this->db->prefix()."supplier_proposal as p, ".$this->db->prefix()."societe as s";
4815 if ($filteronproducttype >= 0) {
4816 $sql .= ", ".$this->db->prefix()."product as prod";
4817 }
4818 if (!$user->hasRight('societe', 'client', 'voir')) {
4819 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4820 }
4821 $sql .= " WHERE p.rowid = d.fk_supplier_proposal";
4822 if ($this->id > 0) {
4823 $sql .= " AND d.fk_product = ".((int) $this->id);
4824 } else {
4825 $sql .= " AND d.fk_product > 0";
4826 }
4827 if ($filteronproducttype >= 0) {
4828 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4829 }
4830 $sql .= " AND p.fk_soc = s.rowid";
4831 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
4832 if (!$user->hasRight('societe', 'client', 'voir')) {
4833 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4834 }
4835 if ($socid > 0) {
4836 $sql .= " AND p.fk_soc = ".((int) $socid);
4837 }
4838 $sql .= $morefilter;
4839 $sql .= " GROUP BY date_format(p.date_valid,'%Y%m')";
4840 $sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
4841
4842 return $this->_get_stats($sql, $mode, $year);
4843 }
4844
4845 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4856 public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4857 {
4858 // phpcs:enable
4859 global $user, $hookmanager;
4860
4861 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4862 if ($mode == 'bynumber') {
4863 $sql .= ", count(DISTINCT c.rowid)";
4864 }
4865 $sql .= ", sum(d.total_ht) as total_ht";
4866 $sql .= " FROM ".$this->db->prefix()."commandedet as d, ".$this->db->prefix()."commande as c, ".$this->db->prefix()."societe as s";
4867 if ($filteronproducttype >= 0) {
4868 $sql .= ", ".$this->db->prefix()."product as p";
4869 }
4870 if (!$user->hasRight('societe', 'client', 'voir')) {
4871 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4872 }
4873 $sql .= " WHERE c.rowid = d.fk_commande";
4874 if ($this->id > 0) {
4875 $sql .= " AND d.fk_product = ".((int) $this->id);
4876 } else {
4877 $sql .= " AND d.fk_product > 0";
4878 }
4879 if ($filteronproducttype >= 0) {
4880 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4881 }
4882 $sql .= " AND c.fk_soc = s.rowid";
4883 $sql .= " AND c.entity IN (".getEntity('commande').")";
4884 if (!$user->hasRight('societe', 'client', 'voir')) {
4885 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4886 }
4887 if ($socid > 0) {
4888 $sql .= " AND c.fk_soc = ".((int) $socid);
4889 }
4890 $sql .= $morefilter;
4891 $parameters = array('socid' => $user->socid);
4892 $hookmanager->executeHooks('productGetNbOrder', $parameters, $this); // Note that $action and $object may have been modified by hook
4893 $sql .= $hookmanager->resPrint;
4894 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4895 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4896
4897 return $this->_get_stats($sql, $mode, $year);
4898 }
4899
4900 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4911 public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4912 {
4913 // phpcs:enable
4914 global $user;
4915
4916 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4917 if ($mode == 'bynumber') {
4918 $sql .= ", count(DISTINCT c.rowid)";
4919 }
4920 $sql .= ", sum(d.total_ht) as total_ht";
4921 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as d, ".$this->db->prefix()."commande_fournisseur as c, ".$this->db->prefix()."societe as s";
4922 if ($filteronproducttype >= 0) {
4923 $sql .= ", ".$this->db->prefix()."product as p";
4924 }
4925 if (!$user->hasRight('societe', 'client', 'voir')) {
4926 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4927 }
4928 $sql .= " WHERE c.rowid = d.fk_commande";
4929 if ($this->id > 0) {
4930 $sql .= " AND d.fk_product = ".((int) $this->id);
4931 } else {
4932 $sql .= " AND d.fk_product > 0";
4933 }
4934 if ($filteronproducttype >= 0) {
4935 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4936 }
4937 $sql .= " AND c.fk_soc = s.rowid";
4938 $sql .= " AND c.entity IN (".getEntity('supplier_order').")";
4939 if (!$user->hasRight('societe', 'client', 'voir')) {
4940 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4941 }
4942 if ($socid > 0) {
4943 $sql .= " AND c.fk_soc = ".((int) $socid);
4944 }
4945 $sql .= $morefilter;
4946 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4947 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4948
4949 return $this->_get_stats($sql, $mode, $year);
4950 }
4951
4952 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4963 public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4964 {
4965 // phpcs:enable
4966 global $user;
4967
4968 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_contrat, '%Y%m')";
4969 if ($mode == 'bynumber') {
4970 $sql .= ", count(DISTINCT c.rowid)";
4971 }
4972 $sql .= ", sum(d.total_ht) as total_ht";
4973 $sql .= " FROM ".$this->db->prefix()."contratdet as d, ".$this->db->prefix()."contrat as c, ".$this->db->prefix()."societe as s";
4974 if ($filteronproducttype >= 0) {
4975 $sql .= ", ".$this->db->prefix()."product as p";
4976 }
4977 if (!$user->hasRight('societe', 'client', 'voir')) {
4978 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4979 }
4980 $sql .= " WHERE c.entity IN (".getEntity('contract').")";
4981 $sql .= " AND c.rowid = d.fk_contrat";
4982
4983 if ($this->id > 0) {
4984 $sql .= " AND d.fk_product = ".((int) $this->id);
4985 } else {
4986 $sql .= " AND d.fk_product > 0";
4987 }
4988 if ($filteronproducttype >= 0) {
4989 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4990 }
4991 $sql .= " AND c.fk_soc = s.rowid";
4992
4993 if (!$user->hasRight('societe', 'client', 'voir')) {
4994 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4995 }
4996 if ($socid > 0) {
4997 $sql .= " AND c.fk_soc = ".((int) $socid);
4998 }
4999 $sql .= $morefilter;
5000 $sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')";
5001 $sql .= " ORDER BY date_format(c.date_contrat,'%Y%m') DESC";
5002
5003 return $this->_get_stats($sql, $mode, $year);
5004 }
5005
5006 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5017 public function get_nb_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
5018 {
5019 // phpcs:enable
5020 global $user;
5021
5022 $sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')";
5023 if ($mode == 'bynumber') {
5024 $sql .= ", count(DISTINCT d.rowid)";
5025 }
5026 $sql .= " FROM ".$this->db->prefix()."mrp_mo as d LEFT JOIN ".$this->db->prefix()."societe as s ON d.fk_soc = s.rowid";
5027 if ($filteronproducttype >= 0) {
5028 $sql .= ", ".$this->db->prefix()."product as p";
5029 }
5030 if (!$user->hasRight('societe', 'client', 'voir')) {
5031 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
5032 }
5033
5034 $sql .= " WHERE d.entity IN (".getEntity('mo').")";
5035 $sql .= " AND d.status > 0";
5036
5037 if ($this->id > 0) {
5038 $sql .= " AND d.fk_product = ".((int) $this->id);
5039 } else {
5040 $sql .= " AND d.fk_product > 0";
5041 }
5042 if ($filteronproducttype >= 0) {
5043 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
5044 }
5045
5046 if (!$user->hasRight('societe', 'client', 'voir')) {
5047 $sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
5048 }
5049 if ($socid > 0) {
5050 $sql .= " AND d.fk_soc = ".((int) $socid);
5051 }
5052 $sql .= $morefilter;
5053 $sql .= " GROUP BY date_format(d.date_valid,'%Y%m')";
5054 $sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC";
5055
5056 return $this->_get_stats($sql, $mode, $year);
5057 }
5058
5059 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5070 public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
5071 {
5072 // phpcs:enable
5073 global $user;
5074
5075 // Clean parameters
5076 if (!is_numeric($id_pere)) {
5077 $id_pere = 0;
5078 }
5079 if (!is_numeric($id_fils)) {
5080 $id_fils = 0;
5081 }
5082 if (!is_numeric($incdec)) {
5083 $incdec = 0;
5084 }
5085
5086 $result = $this->del_sousproduit($id_pere, $id_fils);
5087 if ($result < 0) {
5088 return $result;
5089 }
5090
5091 // Check not already father of id_pere (to avoid father -> child -> father links)
5092 $sql = "SELECT fk_product_pere from ".$this->db->prefix()."product_association";
5093 $sql .= " WHERE fk_product_pere = ".((int) $id_fils)." AND fk_product_fils = ".((int) $id_pere);
5094 if (!$this->db->query($sql)) {
5095 dol_print_error($this->db);
5096 return -1;
5097 } else {
5098 //Selection of the highest row
5099 $sql = "SELECT MAX(rang) as max_rank FROM ".$this->db->prefix()."product_association";
5100 $sql .= " WHERE fk_product_pere = ".((int) $id_pere);
5101 $resql = $this->db->query($sql);
5102 if ($resql) {
5103 $obj = $this->db->fetch_object($resql);
5104 $rank = $obj->max_rank + 1;
5105 //Addition of a product with the highest rank +1
5106 $sql = "INSERT INTO ".$this->db->prefix()."product_association(fk_product_pere,fk_product_fils,qty,incdec,rang)";
5107 $sql .= " VALUES (".((int) $id_pere).", ".((int) $id_fils).", ".price2num($qty, 'MS').", ".((int) $incdec).", ".((int) $rank).")";
5108 if (! $this->db->query($sql)) {
5109 dol_print_error($this->db);
5110 return -1;
5111 } else {
5112 if (!$notrigger) {
5113 // Call trigger
5114 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_ADD', $user);
5115 if ($result < 0) {
5116 $this->error = $this->db->lasterror();
5117 dol_syslog(get_class($this).'::addSubproduct error='.$this->error, LOG_ERR);
5118 return -1;
5119 }
5120 }
5121 // End call triggers
5122
5123 return 1;
5124 }
5125 } else {
5126 dol_print_error($this->db);
5127 return -1;
5128 }
5129 }
5130 }
5131
5132 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5143 public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
5144 {
5145 // phpcs:enable
5146 global $user;
5147
5148 // Clean parameters
5149 if (!is_numeric($id_pere)) {
5150 $id_pere = 0;
5151 }
5152 if (!is_numeric($id_fils)) {
5153 $id_fils = 0;
5154 }
5155 if (!is_numeric($incdec)) {
5156 $incdec = 1;
5157 }
5158 if (!is_numeric($qty)) {
5159 $qty = 1;
5160 }
5161
5162 $sql = 'UPDATE '.$this->db->prefix().'product_association SET ';
5163 $sql .= 'qty = '.price2num($qty, 'MS');
5164 $sql .= ',incdec = '.((int) $incdec);
5165 $sql .= ' WHERE fk_product_pere = '.((int) $id_pere).' AND fk_product_fils = '.((int) $id_fils);
5166
5167 if (!$this->db->query($sql)) {
5168 dol_print_error($this->db);
5169 return -1;
5170 } else {
5171 if (!$notrigger) {
5172 // Call trigger
5173 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_UPDATE', $user);
5174 if ($result < 0) {
5175 $this->error = $this->db->lasterror();
5176 dol_syslog(get_class($this).'::updateSubproduct error='.$this->error, LOG_ERR);
5177 return -1;
5178 }
5179 // End call triggers
5180 }
5181
5182 return 1;
5183 }
5184 }
5185
5186 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5195 public function del_sousproduit($fk_parent, $fk_child, $notrigger = 0)
5196 {
5197 // phpcs:enable
5198 global $user;
5199
5200 if (!is_numeric($fk_parent)) {
5201 $fk_parent = 0;
5202 }
5203 if (!is_numeric($fk_child)) {
5204 $fk_child = 0;
5205 }
5206
5207 $sql = "DELETE FROM ".$this->db->prefix()."product_association";
5208 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5209 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5210
5211 dol_syslog(get_class($this).'::del_sousproduit', LOG_DEBUG);
5212 if (!$this->db->query($sql)) {
5213 dol_print_error($this->db);
5214 return -1;
5215 }
5216
5217 // Updated ranks so that none are missing
5218 $sqlrank = "SELECT rowid, rang FROM ".$this->db->prefix()."product_association";
5219 $sqlrank .= " WHERE fk_product_pere = ".((int) $fk_parent);
5220 $sqlrank .= " ORDER BY rang";
5221 $resqlrank = $this->db->query($sqlrank);
5222 if ($resqlrank) {
5223 $cpt = 0;
5224 while ($objrank = $this->db->fetch_object($resqlrank)) {
5225 $cpt++;
5226 $sql = "UPDATE ".$this->db->prefix()."product_association";
5227 $sql .= " SET rang = ".((int) $cpt);
5228 $sql .= " WHERE rowid = ".((int) $objrank->rowid);
5229 if (! $this->db->query($sql)) {
5230 dol_print_error($this->db);
5231 return -1;
5232 }
5233 }
5234 }
5235
5236 if (!$notrigger) {
5237 // Call trigger
5238 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_DELETE', $user);
5239 if ($result < 0) {
5240 $this->error = $this->db->lasterror();
5241 dol_syslog(get_class($this).'::delSubproduct error='.$this->error, LOG_ERR);
5242 return -1;
5243 }
5244 // End call triggers
5245 }
5246
5247 return 1;
5248 }
5249
5250 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5258 public function is_sousproduit($fk_parent, $fk_child)
5259 {
5260 // phpcs:enable
5261 $sql = "SELECT fk_product_pere, qty, incdec";
5262 $sql .= " FROM ".$this->db->prefix()."product_association";
5263 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5264 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5265
5266 $result = $this->db->query($sql);
5267 if ($result) {
5268 $num = $this->db->num_rows($result);
5269
5270 if ($num > 0) {
5271 $obj = $this->db->fetch_object($result);
5272
5273 $this->is_sousproduit_qty = $obj->qty;
5274 $this->is_sousproduit_incdec = $obj->incdec;
5275
5276 return 1;
5277 } else {
5278 return 0;
5279 }
5280 } else {
5281 dol_print_error($this->db);
5282 return -1;
5283 }
5284 }
5285
5286
5287 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5298 public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
5299 {
5300 // phpcs:enable
5301 global $conf;
5302
5303 $now = dol_now();
5304
5305 dol_syslog(get_class($this)."::add_fournisseur id_fourn = ".$id_fourn." ref_fourn=".$ref_fourn." quantity=".$quantity, LOG_DEBUG);
5306
5307 // Clean parameters
5308 $quantity = price2num($quantity, 'MS');
5309
5310 if ($ref_fourn) {
5311 // Check if ref is not already used
5312 $sql = "SELECT rowid, fk_product";
5313 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5314 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5315 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5316 $sql .= " AND fk_product <> ".((int) $this->id);
5317 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5318
5319 $resql = $this->db->query($sql);
5320 if ($resql) {
5321 $obj = $this->db->fetch_object($resql);
5322 if ($obj) {
5323 // If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
5324 $this->product_id_already_linked = $obj->fk_product;
5325 return -3;
5326 }
5327 $this->db->free($resql);
5328 }
5329 }
5330
5331 $sql = "SELECT rowid";
5332 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5333 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5334 if ($ref_fourn) {
5335 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5336 } else {
5337 $sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
5338 }
5339 $sql .= " AND quantity = ".((float) $quantity);
5340 $sql .= " AND fk_product = ".((int) $this->id);
5341 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5342
5343 $resql = $this->db->query($sql);
5344 if ($resql) {
5345 $obj = $this->db->fetch_object($resql);
5346
5347 // The reference supplier does not exist, we create it for this product.
5348 if (empty($obj)) {
5349 $sql = "INSERT INTO ".$this->db->prefix()."product_fournisseur_price(";
5350 $sql .= "datec";
5351 $sql .= ", entity";
5352 $sql .= ", fk_product";
5353 $sql .= ", fk_soc";
5354 $sql .= ", ref_fourn";
5355 $sql .= ", quantity";
5356 $sql .= ", fk_user";
5357 $sql .= ", tva_tx";
5358 $sql .= ") VALUES (";
5359 $sql .= "'".$this->db->idate($now)."'";
5360 $sql .= ", ".((int) $conf->entity);
5361 $sql .= ", ".((int) $this->id);
5362 $sql .= ", ".((int) $id_fourn);
5363 $sql .= ", '".$this->db->escape($ref_fourn)."'";
5364 $sql .= ", ".((float) $quantity);
5365 $sql .= ", ".((int) $user->id);
5366 $sql .= ", 0";
5367 $sql .= ")";
5368
5369 if ($this->db->query($sql)) {
5370 $this->product_fourn_price_id = $this->db->last_insert_id($this->db->prefix()."product_fournisseur_price");
5371 return 1;
5372 } else {
5373 $this->error = $this->db->lasterror();
5374 return -1;
5375 }
5376 } else {
5377 // If the supplier price already exists for this product and quantity
5378 $this->product_fourn_price_id = $obj->rowid;
5379 return 0;
5380 }
5381 } else {
5382 $this->error = $this->db->lasterror();
5383 return -2;
5384 }
5385 }
5386
5387
5388 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5394 public function list_suppliers()
5395 {
5396 // phpcs:enable
5397 global $conf;
5398
5399 $list = array();
5400
5401 $sql = "SELECT DISTINCT p.fk_soc";
5402 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as p";
5403 $sql .= " WHERE p.fk_product = ".((int) $this->id);
5404 $sql .= " AND p.entity = ".((int) $conf->entity);
5405
5406 $result = $this->db->query($sql);
5407 if ($result) {
5408 $num = $this->db->num_rows($result);
5409 $i = 0;
5410 while ($i < $num) {
5411 $obj = $this->db->fetch_object($result);
5412 $list[$i] = $obj->fk_soc;
5413 $i++;
5414 }
5415 }
5416
5417 return $list;
5418 }
5419
5420 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5428 public function clone_price($fromId, $toId)
5429 {
5430 // phpcs:enable
5431 global $user;
5432
5433 $now = dol_now();
5434
5435 $this->db->begin();
5436
5437 // prices
5438 $sql = "INSERT INTO ".$this->db->prefix()."product_price (";
5439 $sql .= " entity";
5440 $sql .= ", fk_product";
5441 $sql .= ", date_price";
5442 $sql .= ", price_level";
5443 $sql .= ", price";
5444 $sql .= ", price_ttc";
5445 $sql .= ", price_min";
5446 $sql .= ", price_min_ttc";
5447 $sql .= ", price_base_type";
5448 $sql .= ", price_label";
5449 $sql .= ", default_vat_code";
5450 $sql .= ", tva_tx";
5451 $sql .= ", recuperableonly";
5452 $sql .= ", localtax1_tx";
5453 $sql .= ", localtax1_type";
5454 $sql .= ", localtax2_tx";
5455 $sql .= ", localtax2_type";
5456 $sql .= ", fk_user_author";
5457 $sql .= ", tosell";
5458 $sql .= ", price_by_qty";
5459 $sql .= ", fk_price_expression";
5460 $sql .= ", fk_multicurrency";
5461 $sql .= ", multicurrency_code";
5462 $sql .= ", multicurrency_tx";
5463 $sql .= ", multicurrency_price";
5464 $sql .= ", multicurrency_price_ttc";
5465 $sql .= ")";
5466 $sql .= " SELECT";
5467 $sql .= " entity";
5468 $sql .= ", ".((int) $toId);
5469 $sql .= ", '".$this->db->idate($now)."'";
5470 $sql .= ", price_level";
5471 $sql .= ", price";
5472 $sql .= ", price_ttc";
5473 $sql .= ", price_min";
5474 $sql .= ", price_min_ttc";
5475 $sql .= ", price_base_type";
5476 $sql .= ", price_label";
5477 $sql .= ", default_vat_code";
5478 $sql .= ", tva_tx";
5479 $sql .= ", recuperableonly";
5480 $sql .= ", localtax1_tx";
5481 $sql .= ", localtax1_type";
5482 $sql .= ", localtax2_tx";
5483 $sql .= ", localtax2_type";
5484 $sql .= ", ".((int) $user->id);
5485 $sql .= ", tosell";
5486 $sql .= ", price_by_qty";
5487 $sql .= ", fk_price_expression";
5488 $sql .= ", fk_multicurrency";
5489 $sql .= ", multicurrency_code";
5490 $sql .= ", multicurrency_tx";
5491 $sql .= ", multicurrency_price";
5492 $sql .= ", multicurrency_price_ttc";
5493 $sql .= " FROM ".$this->db->prefix()."product_price ps";
5494 $sql .= " WHERE fk_product = ".((int) $fromId);
5495 $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)";
5496 $sql .= " ORDER BY date_price DESC";
5497
5498 dol_syslog(__METHOD__, LOG_DEBUG);
5499 $resql = $this->db->query($sql);
5500 if (!$resql) {
5501 $this->db->rollback();
5502 return -1;
5503 }
5504
5505 $this->db->commit();
5506 return 1;
5507 }
5508
5509 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5517 public function clone_associations($fromId, $toId)
5518 {
5519 // phpcs:enable
5520 $this->db->begin();
5521
5522 $sql = 'INSERT INTO '.$this->db->prefix().'product_association (fk_product_pere, fk_product_fils, qty, incdec)';
5523 $sql .= " SELECT ".((int) $toId).", fk_product_fils, qty, incdec FROM ".$this->db->prefix()."product_association";
5524 $sql .= " WHERE fk_product_pere = ".((int) $fromId);
5525
5526 dol_syslog(get_class($this).'::clone_association', LOG_DEBUG);
5527 if (!$this->db->query($sql)) {
5528 $this->db->rollback();
5529 return -1;
5530 }
5531
5532 $this->db->commit();
5533 return 1;
5534 }
5535
5536 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5544 public function clone_fournisseurs($fromId, $toId)
5545 {
5546 // phpcs:enable
5547 $this->db->begin();
5548
5549 $now = dol_now();
5550
5551 // les fournisseurs
5552 /*$sql = "INSERT ".$this->db->prefix()."product_fournisseur ("
5553 . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
5554 . " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, ref_fourn, fk_user_author"
5555 . " FROM ".$this->db->prefix()."product_fournisseur"
5556 . " WHERE fk_product = ".((int) $fromId);
5557
5558 if ( ! $this->db->query($sql ) )
5559 {
5560 $this->db->rollback();
5561 return -1;
5562 }*/
5563
5564 // les prix de fournisseurs.
5565 $sql = "INSERT ".$this->db->prefix()."product_fournisseur_price (";
5566 $sql .= " datec, fk_product, fk_soc, price, quantity, fk_user, tva_tx)";
5567 $sql .= " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, price, quantity, fk_user, tva_tx";
5568 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5569 $sql .= " WHERE fk_product = ".((int) $fromId);
5570
5571 dol_syslog(get_class($this).'::clone_fournisseurs', LOG_DEBUG);
5572 $resql = $this->db->query($sql);
5573 if (!$resql) {
5574 $this->db->rollback();
5575 return -1;
5576 } else {
5577 $this->db->commit();
5578 return 1;
5579 }
5580 }
5581
5582 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5595 public function fetch_prod_arbo($prod, $compl_path = '', $multiply = 1, $level = 1, $id_parent = 0, $ignore_stock_load = 0)
5596 {
5597 // phpcs:enable
5598 $tmpproduct = null;
5599
5600 if ($multiply < 1) {
5601 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);
5602 $multiply = 1;
5603 }
5604
5605 //var_dump($prod);
5606 foreach ($prod as $id_product => $desc_pere) { // $id_product is 0 (first call starting with root top) or an id of a sub_product
5607 if (is_array($desc_pere)) { // If desc_pere is an array, this means it's a child
5608 $id = (!empty($desc_pere[0]) ? $desc_pere[0] : '');
5609 $nb = (!empty($desc_pere[1]) ? $desc_pere[1] : '');
5610 $type = (!empty($desc_pere[2]) ? $desc_pere[2] : '');
5611 $label = (!empty($desc_pere[3]) ? $desc_pere[3] : '');
5612 $incdec = (!empty($desc_pere[4]) ? $desc_pere[4] : 0);
5613
5614 //print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n";
5615 if (is_null($tmpproduct)) {
5616 $tmpproduct = new Product($this->db); // So we initialize tmpproduct only once for all loop.
5617 }
5618 $tmpproduct->fetch($id); // Load product to get ->ref
5619
5620 if (empty($ignore_stock_load) && ($tmpproduct->isProduct() || getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) {
5621 $tmpproduct->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
5622 }
5623
5624 $this->res[] = array(
5625 'id' => $id, // Id product
5626 'id_parent' => $id_parent,
5627 'ref' => $tmpproduct->ref, // Ref product
5628 'nb' => $nb, // Nb of units that compose parent product
5629 'nb_total' => $nb * $multiply, // Nb of units for all nb of product
5630 'stock' => $tmpproduct->stock_reel, // Stock
5631 'stock_alert' => $tmpproduct->seuil_stock_alerte, // Stock alert
5632 'label' => $label,
5633 'fullpath' => $compl_path.$label, // Label
5634 'type' => $type, // Nb of units that compose parent product
5635 'desiredstock' => $tmpproduct->desiredstock,
5636 'level' => $level,
5637 'incdec' => $incdec,
5638 'entity' => $tmpproduct->entity
5639 );
5640
5641 // Recursive call if there child has children of its own
5642 if (isset($desc_pere['childs']) && is_array($desc_pere['childs'])) {
5643 if (!is_int($desc_pere[1] * $multiply)) {
5644 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);
5645 }
5646
5647 //print 'YYY We go down for '.$desc_pere[3]." -> \n";
5648 $this->fetch_prod_arbo($desc_pere['childs'], $compl_path.$desc_pere[3]." -> ", (int) ceil($desc_pere[1] * $multiply), $level + 1, $id, $ignore_stock_load);
5649 }
5650 }
5651 }
5652 }
5653
5654 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5663 public function get_arbo_each_prod($multiply = 1, $ignore_stock_load = 0)
5664 {
5665 // phpcs:enable
5666 $this->res = array();
5667 if (isset($this->sousprods) && is_array($this->sousprods)) {
5668 foreach ($this->sousprods as $prod_name => $desc_product) {
5669 if (is_array($desc_product)) {
5670 $this->fetch_prod_arbo($desc_product, "", $multiply, 1, $this->id, $ignore_stock_load); // This set $this->res
5671 }
5672 }
5673 }
5674 //var_dump($res);
5675 return $this->res;
5676 }
5677
5685 public function hasFatherOrChild($mode = 0)
5686 {
5687 $nb = 0;
5688
5689 $sql = "SELECT COUNT(pa.rowid) as nb";
5690 $sql .= " FROM ".$this->db->prefix()."product_association as pa";
5691 if ($mode == 0) {
5692 $sql .= " WHERE pa.fk_product_fils = ".((int) $this->id)." OR pa.fk_product_pere = ".((int) $this->id);
5693 } elseif ($mode == -1) {
5694 $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)
5695 } elseif ($mode == 1) {
5696 $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)
5697 }
5698
5699 $resql = $this->db->query($sql);
5700 if ($resql) {
5701 $obj = $this->db->fetch_object($resql);
5702 if ($obj) {
5703 $nb = (int) $obj->nb;
5704 }
5705 } else {
5706 return -1;
5707 }
5708
5709 return $nb;
5710 }
5711
5717 public function hasVariants()
5718 {
5719 $nb = 0;
5720 if (!isModEnabled('variants')) {
5721 return $nb;
5722 }
5723 $sql = "SELECT count(rowid) as nb FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_parent = ".((int) $this->id);
5724 $sql .= " AND entity IN (".getEntity('product').")";
5725
5726 $resql = $this->db->query($sql);
5727 if ($resql) {
5728 $obj = $this->db->fetch_object($resql);
5729 if ($obj) {
5730 $nb = (int) $obj->nb;
5731 }
5732 }
5733
5734 return $nb;
5735 }
5736
5737
5743 public function isVariant()
5744 {
5745 if (isModEnabled('variants')) {
5746 $sql = "SELECT rowid FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_child = ".((int) $this->id)." AND entity IN (".getEntity('product').")";
5747
5748 $query = $this->db->query($sql);
5749
5750 if ($query) {
5751 if (!$this->db->num_rows($query)) {
5752 return false;
5753 }
5754 return true;
5755 } else {
5756 dol_print_error($this->db);
5757 return -1;
5758 }
5759 } else {
5760 return false;
5761 }
5762 }
5763
5770 public function getFather()
5771 {
5772 $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";
5773 $sql .= ", p.tosell as status, p.tobuy as status_buy";
5774 $sql .= " FROM ".$this->db->prefix()."product_association as pa,";
5775 $sql .= " ".$this->db->prefix()."product as p";
5776 $sql .= " WHERE p.rowid = pa.fk_product_pere";
5777 $sql .= " AND pa.fk_product_fils = ".((int) $this->id);
5778
5779 $res = $this->db->query($sql);
5780 if ($res) {
5781 $prods = array();
5782 while ($record = $this->db->fetch_array($res)) {
5783 // $record['id'] = $record['rowid'] = id of father
5784 $prods[$record['id']] = array();
5785 $prods[$record['id']]['id'] = $record['rowid'];
5786 $prods[$record['id']]['ref'] = $record['ref'];
5787 $prods[$record['id']]['label'] = $record['label'];
5788 $prods[$record['id']]['qty'] = $record['qty'];
5789 $prods[$record['id']]['incdec'] = $record['incdec'];
5790 $prods[$record['id']]['fk_product_type'] = $record['fk_product_type'];
5791 $prods[$record['id']]['entity'] = $record['entity'];
5792 $prods[$record['id']]['status'] = $record['status'];
5793 $prods[$record['id']]['status_buy'] = $record['status_buy'];
5794 }
5795 return $prods;
5796 } else {
5797 dol_print_error($this->db);
5798 return -1;
5799 }
5800 }
5801
5802
5812 public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array())
5813 {
5814 if (empty($id)) {
5815 return array();
5816 }
5817
5818 $sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,";
5819 $sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec,";
5820 $sql .= " pa.rowid as fk_association, pa.rang";
5821 $sql .= " FROM ".$this->db->prefix()."product as p,";
5822 $sql .= " ".$this->db->prefix()."product_association as pa";
5823 $sql .= " WHERE p.rowid = pa.fk_product_fils";
5824 $sql .= " AND pa.fk_product_pere = ".((int) $id);
5825 $sql .= " AND pa.fk_product_fils <> ".((int) $id); // This should not happens, it is to avoid infinite loop if it happens
5826 $sql .= " ORDER BY pa.rang";
5827
5828 dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level. ' parents='.(is_array($parents) ? implode(',', $parents) : $parents), LOG_DEBUG);
5829
5830 // Protection against infinite loop
5831 if ($level > 30) {
5832 return array();
5833 }
5834
5835 $res = $this->db->query($sql);
5836 if ($res) {
5837 $prods = array();
5838 if ($this->db->num_rows($res) > 0) {
5839 $parents[] = $id;
5840 }
5841
5842 while ($rec = $this->db->fetch_array($res)) {
5843 if (in_array($rec['id'], $parents)) {
5844 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);
5845 continue; // We discard this child if it is already found at a higher level in tree in the same branch.
5846 }
5847
5848 $prods[$rec['rowid']] = array(
5849 0 => $rec['rowid'],
5850 1 => $rec['qty'],
5851 2 => $rec['fk_product_type'],
5852 3 => $this->db->escape($rec['label']),
5853 4 => $rec['incdec'],
5854 5 => $rec['ref'],
5855 6 => $rec['fk_association'],
5856 7 => $rec['rang']
5857 );
5858 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
5859 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
5860 if (empty($firstlevelonly)) {
5861 $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, $parents);
5862 foreach ($listofchilds as $keyChild => $valueChild) {
5863 $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
5864 }
5865 }
5866 }
5867
5868 return $prods;
5869 } else {
5870 dol_print_error($this->db);
5871 return -1;
5872 }
5873 }
5874
5875 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5882 public function get_sousproduits_arbo()
5883 {
5884 // phpcs:enable
5885 $parent = array();
5886
5887 foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) { // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
5888 $parent[$this->label][$keyChild] = $valueChild;
5889 }
5890 foreach ($parent as $key => $value) { // key=label, value is array of children
5891 $this->sousprods[$key] = $value; // @phan-suppress-current-line PhanTypeMismatchProperty
5892 }
5893 }
5894
5902 public function getTooltipContentArray($params)
5903 {
5904 global $conf, $langs, $user;
5905
5906 $langs->loadLangs(array('products', 'other'));
5907
5908 $datas = array();
5909 $nofetch = !empty($params['nofetch']);
5910
5911 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5912 return ['optimize' => $langs->trans("ShowProduct")];
5913 }
5914
5915 // Does user has permission to read product/service
5916 $permissiontoreadproduct = 0;
5917 if ($this->type == self::TYPE_PRODUCT && $user->hasRight('product', 'read')) {
5918 $permissiontoreadproduct = 1;
5919 }
5920 if ($this->type == self::TYPE_SERVICE && $user->hasRight('service', 'read')) {
5921 $permissiontoreadproduct = 1;
5922 }
5923
5924 if (!empty($this->entity) && $permissiontoreadproduct) {
5925 $tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80, 0, 0, 0, 0, '1');
5926 if ($this->nbphoto > 0) {
5927 $datas['photo'] = '<div class="photointooltip floatright">'."\n" . $tmpphoto . '</div>';
5928 }
5929 }
5930
5931 if ($this->isProduct()) {
5932 $datas['picto'] = img_picto('', 'product').' <u class="paddingrightonly">'.$langs->trans("Product").'</u>';
5933 } elseif ($this->isService()) {
5934 $datas['picto'] = img_picto('', 'service').' <u class="paddingrightonly">'.$langs->trans("Service").'</u>';
5935 }
5936 if (isset($this->status) && isset($this->status_buy)) {
5937 $datas['status'] = ' '.$this->getLibStatut(5, 0) . ' '.$this->getLibStatut(5, 1);
5938 }
5939
5940 if (!empty($this->ref)) {
5941 $datas['ref'] = '<br><b>'.$langs->trans('ProductRef').':</b> '.$this->ref;
5942 }
5943 if (!empty($this->label)) {
5944 $datas['label'] = '<br><b>'.$langs->trans('ProductLabel').':</b> '.$this->label.'<br>';
5945 }
5946
5947 if ($permissiontoreadproduct) {
5948 if (!empty($this->description) && getDolGlobalString('PRODUCT_SHOW_DESCRIPTION_IN_TOOLTIP')) {
5949 $datas['description'] = '<br><b>'.$langs->trans('ProductDescription').':</b> '.dolGetFirstLineOfText($this->description, 5);
5950 }
5951
5952 $datas['stockmanaged'] = "<br><b>".$langs->trans("StockableProduct").'</b>: '.yn($this->isStockManaged());
5953
5954 if ($this->isStockManaged()) {
5955 if (isModEnabled('productbatch')) {
5956 $langs->load("productbatch");
5957 $datas['batchstatus'] = "<br><b>".$langs->trans("ManageLotSerial").'</b>: '.$this->getLibStatut(0, 2);
5958 if ($this->status_batch) {
5959 $datas['batchdlc'] = "<br><b>".$langs->trans("BatchSellOrEatByMandatoryList", $langs->transnoentitiesnoconv("SellByDate"), $langs->transnoentitiesnoconv("EatByDate")).'</b>: '.$this->getSellOrEatByMandatoryLabel();
5960 }
5961 }
5962 }
5963 if (isModEnabled('barcode')) {
5964 $datas['barcode'] = '<br><b>'.$langs->trans('BarCode').':</b> '.$this->barcode;
5965 }
5966
5967 if ($this->isProduct()) {
5968 if ($this->weight) {
5969 $datas['weight'] = "<br><b>".$langs->trans("Weight").'</b>: '.$this->weight.' '.measuringUnitString(0, "weight", $this->weight_units);
5970 }
5971 $labelsize = "";
5972 if ($this->length) {
5973 $labelsize .= "<b>".$langs->trans("Length").'</b>: '.$this->length.' '.measuringUnitString(0, 'size', $this->length_units);
5974 }
5975 if ($this->width) {
5976 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Width").'</b>: '.$this->width.' '.measuringUnitString(0, 'size', $this->width_units);
5977 }
5978 if ($this->height) {
5979 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Height").'</b>: '.$this->height.' '.measuringUnitString(0, 'size', $this->height_units);
5980 }
5981 if ($labelsize) {
5982 $datas['size'] = "<br>".$labelsize;
5983 }
5984
5985 $labelsurfacevolume = "";
5986 if ($this->surface) {
5987 $labelsurfacevolume .= "<b>".$langs->trans("Surface").'</b>: '.$this->surface.' '.measuringUnitString(0, 'surface', $this->surface_units);
5988 }
5989 if ($this->volume) {
5990 $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Volume").'</b>: '.$this->volume.' '.measuringUnitString(0, 'volume', $this->volume_units);
5991 }
5992 if ($labelsurfacevolume) {
5993 $datas['surface'] = "<br>" . $labelsurfacevolume;
5994 }
5995 }
5996 if ($this->isService() && !empty($this->duration_value)) {
5997 // Duration
5998 $datas['duration'] = '<br><b>'.$langs->trans("Duration").':</b> '.$this->duration_value;
5999 if ($this->duration_value > 1) {
6000 $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"));
6001 } elseif ($this->duration_value > 0) {
6002 $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"));
6003 }
6004 $datas['duration'] .= (!empty($this->duration_unit) && isset($dur[$this->duration_unit]) ? "&nbsp;".$langs->trans($dur[$this->duration_unit]) : '');
6005 }
6006 if (empty($user->socid)) {
6007 if ($this->isStockManaged() && isset($this->pmp) && $this->pmp) {
6008 $datas['pmp'] = "<br><b>".$langs->trans("PMPValue").'</b>: '.price($this->pmp, 0, '', 1, -1, -1, $conf->currency);
6009 }
6010
6011 if (isModEnabled('accounting')) {
6012 if ($this->status && isset($this->accountancy_code_sell)) {
6013 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
6014 $selllabel = '<br>';
6015 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellCode').':</b> '.length_accountg($this->accountancy_code_sell);
6016 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellIntraCode').':</b> '.length_accountg($this->accountancy_code_sell_intra);
6017 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellExportCode').':</b> '.length_accountg($this->accountancy_code_sell_export);
6018 $datas['accountancysell'] = $selllabel;
6019 }
6020 if ($this->status_buy && isset($this->accountancy_code_buy)) {
6021 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
6022 $buylabel = '';
6023 if (empty($this->status)) {
6024 $buylabel .= '<br>';
6025 }
6026 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyCode').':</b> '.length_accountg($this->accountancy_code_buy);
6027 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyIntraCode').':</b> '.length_accountg($this->accountancy_code_buy_intra);
6028 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyExportCode').':</b> '.length_accountg($this->accountancy_code_buy_export);
6029 $datas['accountancybuy'] = $buylabel;
6030 }
6031 }
6032 }
6033 // show categories for this record only in ajax to not overload lists
6034 if (isModEnabled('category') && !$nofetch) {
6035 require_once DOL_DOCUMENT_ROOT . '/categories/class/categorie.class.php';
6036 $form = new Form($this->db);
6037 $datas['categories'] = '<br>' . $form->showCategories($this->id, Categorie::TYPE_PRODUCT, 1);
6038 }
6039 }
6040
6041 return $datas;
6042 }
6043
6057 public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0, $morecss = '', $add_label = 0, $sep = ' - ')
6058 {
6059 global $langs, $hookmanager;
6060
6061 include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
6062
6063 $result = '';
6064
6065 $newref = (string) $this->ref;
6066 if ($maxlength) {
6067 $newref = dol_trunc($newref, $maxlength, 'middle');
6068 }
6069 $params = [
6070 'id' => $this->id,
6071 'objecttype' => ($this->type == 1 ? 'service' : 'product'),
6072 'option' => $option,
6073 'nofetch' => 1,
6074 ];
6075 $classfortooltip = 'classfortooltip';
6076 $dataparams = '';
6077 if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
6078 $classfortooltip = 'classforajaxtooltip';
6079 $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
6080 $label = '';
6081 } else {
6082 $label = implode($this->getTooltipContentArray($params));
6083 }
6084
6085 $linkclose = '';
6086 if (empty($notooltip)) {
6087 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
6088 $label = $langs->trans("ShowProduct");
6089 $linkclose .= ' alt="'.dol_escape_htmltag($label, 1, 1).'"';
6090 }
6091 $linkclose .= ($label ? ' title="'.dol_escape_htmltag($label, 1, 1).'"' : ' title="tocomplete"');
6092 $linkclose .= $dataparams.' class="nowraponall '.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
6093 } else {
6094 $linkclose = ' class="nowraponall'.($morecss ? ' '.$morecss : '').'"';
6095 }
6096
6097 if ($option == 'supplier' || $option == 'category') {
6098 $url = DOL_URL_ROOT.'/product/price_suppliers.php?id='.$this->id;
6099 } elseif ($option == 'stock') {
6100 $url = DOL_URL_ROOT.'/product/stock/product.php?id='.$this->id;
6101 } elseif ($option == 'composition') {
6102 $url = DOL_URL_ROOT.'/product/composition/card.php?id='.$this->id;
6103 } else {
6104 $url = DOL_URL_ROOT.'/product/card.php?id='.$this->id;
6105 }
6106
6107 if ($option !== 'nolink') {
6108 // Add param to save lastsearch_values or not
6109 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
6110 if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
6111 $add_save_lastsearch_values = 1;
6112 }
6113 if ($add_save_lastsearch_values) {
6114 $url .= '&save_lastsearch_values=1';
6115 }
6116 }
6117
6118 $linkstart = '<a href="'.$url.'"';
6119 $linkstart .= $linkclose.'>';
6120 $linkend = '</a>';
6121
6122 $result .= $linkstart;
6123 if ($withpicto) {
6124 if ($this->isProduct()) {
6125 $result .= (img_object(($notooltip ? '' : $label), 'product', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
6126 }
6127 if ($this->isService()) {
6128 $result .= (img_object(($notooltip ? '' : $label), 'service', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
6129 }
6130 }
6131 $result .= '<span class="aaa">'.dol_escape_htmltag($newref).'</span>';
6132 $result .= $linkend;
6133 if ($withpicto != 2) {
6134 $result .= (($add_label && $this->label) ? $sep.dol_trunc($this->label, ($add_label > 1 ? $add_label : 0)) : '');
6135 }
6136
6137 global $action;
6138 $hookmanager->initHooks(array('productdao'));
6139 $parameters = array('id' => $this->id, 'getnomurl' => &$result, 'label' => &$label);
6140 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
6141 if ($reshook > 0) {
6142 $result = $hookmanager->resPrint;
6143 } else {
6144 $result .= $hookmanager->resPrint;
6145 }
6146
6147 return $result;
6148 }
6149
6150
6161 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
6162 {
6163 global $langs;
6164
6165 $langs->load("products");
6166 $outputlangs->load("products");
6167
6168 if (!dol_strlen($modele)) {
6169 $modele = ''; // No doc template/generation by default
6170
6171 if (!empty($this->model_pdf)) {
6172 $modele = $this->model_pdf;
6173 } elseif (getDolGlobalString('PRODUCT_ADDON_PDF')) {
6174 $modele = getDolGlobalString('PRODUCT_ADDON_PDF');
6175 }
6176 }
6177
6178 if (empty($modele)) {
6179 return 0;
6180 } else {
6181 $modelpath = "core/modules/product/doc/";
6182 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
6183 }
6184 }
6185
6193 public function getLibStatut($mode = 0, $type = 0)
6194 {
6195 switch ($type) {
6196 case 0:
6197 return $this->LibStatut($this->status, $mode, $type);
6198 case 1:
6199 return $this->LibStatut($this->status_buy, $mode, $type);
6200 case 2:
6201 return $this->LibStatut($this->status_batch, $mode, $type);
6202 default:
6203 //Simulate previous behavior but should return an error string
6204 return $this->LibStatut($this->status_buy, $mode, $type);
6205 }
6206 }
6207
6208 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6217 public function LibStatut($status, $mode = 0, $type = 0)
6218 {
6219 // phpcs:enable
6220 global $langs;
6221
6222 $labelStatus = $labelStatusShort = '';
6223
6224 $langs->load('products');
6225 if (isModEnabled('productbatch')) {
6226 $langs->load("productbatch");
6227 }
6228
6229 if ($type == 2) {
6230 switch ($mode) {
6231 case 0:
6232 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatch') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial')));
6233 return dolGetStatus($label);
6234 case 1:
6235 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort')));
6236 return dolGetStatus($label);
6237 case 2:
6238 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 1, 2);
6239 case 3:
6240 return dolGetStatus($langs->transnoentitiesnoconv('ProductStatusNotOnBatch'), '', '', empty($status) ? 'status5' : 'status4', 3, 'dot');
6241 case 4:
6242 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 0, 2);
6243 case 5:
6244 return $this->LibStatut($status, 1, 2).' '.$this->LibStatut($status, 3, 2);
6245 default:
6246 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'));
6247 }
6248 }
6249
6250 $statuttrans = empty($status) ? 'status5' : 'status4';
6251
6252 if ($status == 0) {
6253 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6254 if ($type == 0) {
6255 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnSellShort');
6256 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnSell');
6257 } elseif ($type == 1) {
6258 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBuyShort');
6259 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBuy');
6260 } elseif ($type == 2) {
6261 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBatch');
6262 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort');
6263 }
6264 } elseif ($status == 1) {
6265 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6266 if ($type == 0) {
6267 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSellShort');
6268 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSell');
6269 } elseif ($type == 1) {
6270 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBuyShort');
6271 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBuy');
6272 } elseif ($type == 2) {
6273 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBatch');
6274 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBatchShort');
6275 }
6276 } elseif ($type == 2 && $status == 2) {
6277 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSerial');
6278 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSerialShort');
6279 }
6280
6281 if ($mode > 6) {
6282 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'), '', '', 'status0', 0);
6283 } else {
6284 return dolGetStatus($labelStatus, $labelStatusShort, '', $statuttrans, $mode);
6285 }
6286 }
6287
6288
6294 public function getLibFinished()
6295 {
6296 global $langs;
6297
6298 $langs->load('products');
6299 $label = '';
6300
6301 if (isset($this->finished) && $this->finished >= 0) {
6302 $sql = "SELECT label, code FROM ".$this->db->prefix()."c_product_nature where code = ".((int) $this->finished)." AND active=1";
6303 $resql = $this->db->query($sql);
6304 if (!$resql) {
6305 $this->error = $this->db->error().' sql='.$sql;
6306 dol_syslog(__METHOD__.' Error '.$this->error, LOG_ERR);
6307 return -1;
6308 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
6309 $label = $langs->trans($res['label']);
6310 }
6311 $this->db->free($resql);
6312 }
6313
6314 return $label;
6315 }
6316
6317
6318 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6335 public function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null)
6336 {
6337 // phpcs:enable
6338 if ($id_entrepot) {
6339 $this->db->begin();
6340
6341 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
6342
6343 // Ensure $nbpiece is a number and positive
6344 $nbpiece = (float) $nbpiece;
6345 if ($nbpiece < 0) {
6346 if (!$movement) {
6347 $movement = 1;
6348 }
6349 $nbpiece = abs($nbpiece);
6350 }
6351 $op = array();
6352 $op[0] = $nbpiece;
6353 $op[1] = -$nbpiece;
6354
6355 $movementstock = new MouvementStock($this->db);
6356 $movementstock->setOrigin($origin_element, (int) $origin_id); // Set ->origin_type and ->origin_id
6357 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', '', '', '', false, 0, $disablestockchangeforsubproduct);
6358
6359 if ($result >= 0) {
6360 if ($extrafields) {
6361 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
6362 $movementstock->array_options = $array_options;
6363 $movementstock->insertExtraFields();
6364 }
6365 $this->db->commit();
6366 return 1;
6367 } else {
6368 $this->setErrorsFromObject($movementstock);
6369
6370 $this->db->rollback();
6371 return -1;
6372 }
6373 }
6374
6375 return -1;
6376 }
6377
6378 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6399 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)
6400 {
6401 // phpcs:enable
6402 if ($id_entrepot) {
6403 $this->db->begin();
6404
6405 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
6406
6407 // Ensure $nbpiece is a number and positive
6408 $nbpiece = (float) $nbpiece;
6409 if ($nbpiece < 0) {
6410 if (!$movement) {
6411 $movement = 1;
6412 }
6413 $nbpiece = abs($nbpiece);
6414 }
6415
6416 $op = array();
6417 $op[0] = $nbpiece;
6418 $op[1] = -$nbpiece;
6419
6420 $movementstock = new MouvementStock($this->db);
6421 $movementstock->setOrigin($origin_element, (int) $origin_id); // Set ->origin_type and ->fk_origin
6422 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', $dlc, $dluo, $lot, false, 0, $disablestockchangeforsubproduct, 0, $force_update_batch);
6423
6424 if ($result >= 0) {
6425 if ($extrafields) {
6426 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
6427 $movementstock->array_options = $array_options;
6428 $movementstock->insertExtraFields();
6429 }
6430 $this->db->commit();
6431 return 1;
6432 } else {
6433 $this->error = $movementstock->error;
6434 $this->errors = $movementstock->errors;
6435
6436 $this->db->rollback();
6437 return -1;
6438 }
6439 }
6440 return -1;
6441 }
6442
6443 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6456 public function load_stock($option = '', $includedraftpoforvirtual = null, $dateofvirtualstock = null)
6457 {
6458 // phpcs:enable
6459 $this->stock_reel = 0;
6460 $this->stock_warehouse = array();
6461 $this->stock_theorique = 0;
6462
6463 // Set filter on warehouse status
6464 $warehouseStatus = array();
6465 if (preg_match('/warehouseclosed/', $option)) {
6467 }
6468 if (preg_match('/warehouseopen/', $option)) {
6470 }
6471 if (preg_match('/warehouseinternal/', $option)) {
6472 if (getDolGlobalString('ENTREPOT_EXTRA_STATUS')) {
6474 } else {
6476 }
6477 }
6478
6479 $sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot";
6480 $sql .= " FROM ".$this->db->prefix()."product_stock as ps";
6481 $sql .= ", ".$this->db->prefix()."entrepot as w";
6482 $sql .= " WHERE w.entity IN (".getEntity('stock').")";
6483 $sql .= " AND w.rowid = ps.fk_entrepot";
6484 $sql .= " AND ps.fk_product = ".((int) $this->id);
6485 if (count($warehouseStatus)) {
6486 $sql .= " AND w.statut IN (".$this->db->sanitize(implode(',', $warehouseStatus)).")";
6487 }
6488
6489 $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;
6490
6491 dol_syslog(get_class($this)."::load_stock", LOG_DEBUG);
6492 $result = $this->db->query($sql);
6493 if ($result) {
6494 $num = $this->db->num_rows($result);
6495 $i = 0;
6496 if ($num > 0) {
6497 while ($i < $num) {
6498 $row = $this->db->fetch_object($result);
6499 $this->stock_warehouse[$row->fk_entrepot] = new stdClass();
6500 $this->stock_warehouse[$row->fk_entrepot]->real = $row->reel;
6501 $this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid;
6502 if ((!preg_match('/nobatch/', $option)) && $this->hasbatch()) {
6503 $this->stock_warehouse[$row->fk_entrepot]->detail_batch = Productbatch::findAll($this->db, $row->rowid, 1, $this->id);
6504 }
6505 $this->stock_reel += $row->reel;
6506 $i++;
6507 }
6508 $this->stock_reel = (float) price2num($this->stock_reel, 'MS');
6509 }
6510 $this->db->free($result);
6511
6512 if (!preg_match('/novirtual/', $option)) {
6513 $this->load_virtual_stock($includedraftpoforvirtual, $dateofvirtualstock); // This load stock_theorique and also load all arrays stats_xxx...
6514 }
6515
6516 return 1;
6517 } else {
6518 $this->error = $this->db->lasterror();
6519 return -1;
6520 }
6521 }
6522
6523
6524 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6534 public function load_virtual_stock($includedraftpoforvirtual = null, $dateofvirtualstock = null)
6535 {
6536 // phpcs:enable
6537 global $hookmanager, $action;
6538
6539 $stock_commande_client = 0;
6540 $stock_commande_fournisseur = 0;
6541 $stock_sending_client = 0;
6542 $stock_reception_fournisseur = 0;
6543 $stock_inproduction = 0;
6544
6545 //dol_syslog("load_virtual_stock");
6546
6547 if (isModEnabled('order')) {
6548 $result = $this->load_stats_commande(0, '1,2', 1);
6549 if ($result < 0) {
6550 dol_print_error($this->db, $this->error);
6551 }
6552 $stock_commande_client = $this->stats_commande['qty'];
6553 }
6554 if (isModEnabled("shipping")) {
6555 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
6556 $filterShipmentStatus = '';
6557 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
6558 $filterShipmentStatus = Expedition::STATUS_VALIDATED.','.Expedition::STATUS_CLOSED;
6559 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6560 $filterShipmentStatus = Expedition::STATUS_CLOSED;
6561 }
6562 $result = $this->load_stats_sending(0, '1,2', 1, $filterShipmentStatus);
6563 if ($result < 0) {
6564 dol_print_error($this->db, $this->error);
6565 }
6566 $stock_sending_client = $this->stats_expedition['qty'];
6567 }
6568 // Include supplier order lines
6569 if (isModEnabled("supplier_order")) {
6570 $filterStatus = getDolGlobalString('SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK', '3,4');
6571 if (isset($includedraftpoforvirtual)) {
6572 $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
6573 }
6574 $result = $this->load_stats_commande_fournisseur(0, $filterStatus, 1, $dateofvirtualstock);
6575 if ($result < 0) {
6576 dol_print_error($this->db, $this->error);
6577 }
6578 $stock_commande_fournisseur = $this->stats_commande_fournisseur['qty'];
6579 }
6580 // Include reception lines
6581 if (isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) {
6582 $filterStatus = '4';
6583 if (isset($includedraftpoforvirtual)) {
6584 $filterStatus = '0,'.$filterStatus;
6585 }
6586 $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock);
6587 if ($result < 0) {
6588 dol_print_error($this->db, $this->error);
6589 }
6590 $stock_reception_fournisseur = $this->stats_reception['qty'];
6591 }
6592 // Include manufacturing
6593 if (isModEnabled('mrp')) {
6594 $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock);
6595 if ($result < 0) {
6596 dol_print_error($this->db, $this->error);
6597 }
6598 $stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty'];
6599 }
6600
6601 $this->stock_theorique = $this->stock_reel + $stock_inproduction;
6602
6603 // $weBillOrderOrShipmentReception is set to 'order' or 'shipmentreception'. it will be used to know how to make virtual stock
6604 // calculation when we have a stock increase or decrease on billing. Do we have to count orders to bill or shipment/reception to bill ?
6605 $weBillOrderOrShipmentReception = getDolGlobalString('STOCK_DO_WE_BILL_ORDER_OR_SHIPMENTECEPTION_FOR_VIRTUALSTOCK', 'order');
6606
6607 // Stock decrease mode
6608 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') || getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6609 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6610 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_VALIDATE_ORDER')) {
6611 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
6612 $tmpnewprod = dol_clone($this, 1);
6613 $result = $tmpnewprod->load_stats_commande(0, '0', 1); // Get qty in draft orders
6614 $this->stock_theorique += $tmpnewprod->stats_commande['qty'];
6615 }
6616 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'order') {
6617 $this->stock_theorique -= $stock_commande_client;
6618 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6619 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6620 }
6621
6622 // Stock Increase mode
6623 if (getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION') || getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION_CLOSE')) {
6624 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6625 } 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
6626 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6627 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER')) { // Warning: stock change "on approval", not on validation !
6628 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
6629 $tmpnewprod = dol_clone($this, 1);
6630 $result = $tmpnewprod->load_stats_commande_fournisseur(0, '0', 1); // Get qty in draft orders
6631 $this->stock_theorique += $this->stats_commande_fournisseur['qty'];
6632 }
6633 $this->stock_theorique -= $stock_reception_fournisseur;
6634 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'order') {
6635 $this->stock_theorique += $stock_commande_fournisseur;
6636 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6637 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6638 }
6639
6640 $parameters = array('id' => $this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual);
6641 // Note that $action and $object may have been modified by some hooks
6642 $reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action);
6643 if ($reshook > 0) {
6644 $this->stock_theorique = $hookmanager->resArray['stock_theorique'];
6645 } elseif ($reshook == 0 && isset($hookmanager->resArray['stock_stats_hook'])) {
6646 $this->stock_theorique += $hookmanager->resArray['stock_stats_hook'];
6647 }
6648
6649 //Virtual Stock by Warehouse
6650 if (!empty($this->stock_warehouse) && getDolGlobalString('STOCK_ALLOW_VIRTUAL_STOCK_PER_WAREHOUSE')) {
6651 foreach ($this->stock_warehouse as $warehouseid => $stockwarehouse) {
6652 if (isModEnabled('mrp')) {
6653 $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock, $warehouseid);
6654 if ($result < 0) {
6655 dol_print_error($this->db, $this->error);
6656 }
6657 }
6658
6659 if ($this->fk_default_warehouse == $warehouseid) {
6660 $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']);
6661 } else {
6662 $this->stock_warehouse[$warehouseid]->virtual = $this->stock_warehouse[$warehouseid]->real + $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'];
6663 }
6664 }
6665 }
6666
6667 return 1;
6668 }
6669
6677 public function loadStockForVirtualProduct($option = '', $qtyWish = 1)
6678 {
6679 $this->stock_warehouse = array();
6680 $error = 0;
6681
6682 $this->get_sousproduits_arbo();
6683 $prods_arbo = $this->get_arbo_each_prod($qtyWish, 1);
6684 if (count($prods_arbo) > 0) {
6685 $productCachedList = array();
6686 $stockByComponentList = array();
6687
6688 foreach ($prods_arbo as $componentArr) {
6689 $componentId = $componentArr['id'];
6690 // only component whose manage stock
6691 if ($componentArr['incdec'] == 1) {
6692 if (!isset($productCachedList[$componentId])) {
6693 $componentStatic = new self($this->db);
6694 $componentStatic->fetch($componentId);
6695 // check if it's a sub-kit
6696 $childrenNb = $componentStatic->hasFatherOrChild(1);
6697 if ($childrenNb == 0) {
6698 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6699 if (!isset($stockByComponentList[$componentId])) {
6700 $stockByComponentList[$componentId] = array(
6701 'qty_need' => 0
6702 );
6703 }
6704 $stockByComponentList[$componentId]['qty_need'] += $componentArr['nb_total'];
6705 }
6706 $productCachedList[$componentId] = $componentStatic;
6707 }
6708 }
6709 }
6710
6711 if (!empty($stockByComponentList)) {
6712 foreach ($stockByComponentList as $componentId => $stockByComponentArr) {
6713 if (!isset($productCachedList[$componentId])) {
6714 $componentStatic = new self($this->db);
6715 $componentStatic->fetch($componentId);
6716 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6717 $productCachedList[$componentId] = $componentStatic;
6718 }
6719 $component = $productCachedList[$componentId];
6720
6721
6722 if ($component->stock_reel < $stockByComponentArr['qty_need']) {
6723 // not enough stock for this component to assemble this virtual product
6724 $error++;
6725 $this->error = 'Not enough component [id='.$componentId.'] in stock, real='.$component->stock_reel.' and need='.$stockByComponentArr['qty_need'];
6726 $this->errors[] = $this->error;
6727 dol_syslog(__METHOD__.' : '.$this->error, LOG_ERR);
6728 } else {
6729 if (!empty($component->stock_warehouse)) {
6730 foreach ($component->stock_warehouse as $warehouseId => $warehouseObj) {
6731 $kitWarehouseAvailable = new stdClass();
6732 $kitWarehouseAvailable->id = $warehouseObj->id;
6733 $kitWarehouseAvailable->real = $qtyWish;
6734 $this->stock_warehouse[$warehouseId] = $kitWarehouseAvailable;
6735 }
6736 }
6737 }
6738
6739 if ($error) {
6740 break;
6741 }
6742 }
6743 } elseif (getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE') && empty($productCachedList)) {
6744 // if all sub product are not stock managed when use parent stock
6745 $this->load_stock('warehouseopen');
6746 }
6747 }
6748
6749 if ($error) {
6750 return -1;
6751 } else {
6752 return 1;
6753 }
6754 }
6755
6763 public function loadBatchInfo($batch)
6764 {
6765 $result = array();
6766
6767 $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";
6768 $sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = ".((int) $this->id)." AND pb.batch = '".$this->db->escape($batch)."'";
6769 $sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby";
6770 dol_syslog(get_class($this)."::loadBatchInfo load first entry found for lot/serial = ".$batch, LOG_DEBUG);
6771 $resql = $this->db->query($sql);
6772 if ($resql) {
6773 $num = $this->db->num_rows($resql);
6774 $i = 0;
6775 while ($i < $num) {
6776 $obj = $this->db->fetch_object($resql);
6777 $result[] = array('batch' => $batch, 'eatby' => $this->db->jdate($obj->eatby), 'sellby' => $this->db->jdate($obj->sellby), 'qty' => $obj->qty);
6778 $i++;
6779 }
6780 return $result;
6781 } else {
6782 dol_print_error($this->db);
6783 $this->db->rollback();
6784 return array();
6785 }
6786 }
6787
6788 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6797 public function add_photo($sdir, $file)
6798 {
6799 // phpcs:enable
6800 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6801
6802 $result = 0;
6803
6804 $dir = $sdir;
6805 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6806 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos";
6807 } else {
6808 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product').dol_sanitizeFileName($this->ref);
6809 }
6810
6811 dol_mkdir($dir);
6812
6813 $dir_osencoded = $dir;
6814
6815 if (is_dir($dir_osencoded)) {
6816 $originImage = $dir.'/'.$file['name'];
6817
6818 // Cree fichier en taille origine
6819 $result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
6820
6821 if (file_exists(dol_osencode($originImage))) {
6822 // Create thumbs
6823 $this->addThumbs($originImage);
6824 }
6825 }
6826
6827 if (is_numeric($result) && $result > 0) {
6828 return 1;
6829 } else {
6830 return -1;
6831 }
6832 }
6833
6834 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6841 public function is_photo_available($sdir)
6842 {
6843 // phpcs:enable
6844 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6845 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6846
6847 $dir = $sdir;
6848 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6849 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos/";
6850 } else {
6851 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product');
6852 }
6853
6854 $dir_osencoded = dol_osencode($dir);
6855 if (file_exists($dir_osencoded)) {
6856 $handle = opendir($dir_osencoded);
6857 if (is_resource($handle)) {
6858 while (($file = readdir($handle)) !== false) {
6859 if (!utf8_check($file)) {
6860 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // To be sure data is stored in UTF8 in memory
6861 }
6862 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6863 return true;
6864 }
6865 }
6866 }
6867 }
6868
6869 return false;
6870 }
6871
6872 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6880 public function liste_photos($dir, $nbmax = 0)
6881 {
6882 // phpcs:enable
6883 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6884 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6885
6886 $nbphoto = 0;
6887 $tabobj = array();
6888
6889 $dir_osencoded = dol_osencode($dir);
6890 $handle = @opendir($dir_osencoded);
6891 if (is_resource($handle)) {
6892 while (($file = readdir($handle)) !== false) {
6893 if (!utf8_check($file)) {
6894 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // readdir returns ISO
6895 }
6896 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6897 $nbphoto++;
6898
6899 // We forge name of thumb.
6900 $photo = $file;
6901 $photo_vignette = '';
6902 $regs = array();
6903 if (preg_match('/('.$this->regeximgext.')$/i', $photo, $regs)) {
6904 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $photo).'_small'.$regs[0];
6905 }
6906
6907 $dirthumb = $dir.'thumbs/';
6908
6909 // Object
6910 $obj = array();
6911 $obj['photo'] = $photo;
6912 if ($photo_vignette && dol_is_file($dirthumb.$photo_vignette)) {
6913 $obj['photo_vignette'] = 'thumbs/'.$photo_vignette;
6914 } else {
6915 $obj['photo_vignette'] = "";
6916 }
6917
6918 $tabobj[$nbphoto - 1] = $obj;
6919
6920 // Do we have to continue with next photo ?
6921 if ($nbmax && $nbphoto >= $nbmax) {
6922 break;
6923 }
6924 }
6925 }
6926
6927 closedir($handle);
6928 }
6929
6930 return $tabobj;
6931 }
6932
6933 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6940 public function delete_photo($file)
6941 {
6942 // phpcs:enable
6943 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6944 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6945
6946 $dir = dirname($file).'/'; // Chemin du dossier contenant l'image d'origine
6947 $dirthumb = $dir.'/thumbs/'; // Chemin du dossier contenant la vignette
6948 $filename = preg_replace('/'.preg_quote($dir, '/').'/i', '', $file); // Nom du fichier
6949
6950 // On efface l'image d'origine
6951 dol_delete_file($file, 0, 0, 0, $this); // For triggers
6952
6953 // Si elle existe, on efface la vignette
6954 if (preg_match('/('.$this->regeximgext.')$/i', $filename, $regs)) {
6955 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_small'.$regs[0];
6956 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6957 dol_delete_file($dirthumb.$photo_vignette);
6958 }
6959
6960 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_mini'.$regs[0];
6961 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6962 dol_delete_file($dirthumb.$photo_vignette);
6963 }
6964 }
6965 }
6966
6967 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6974 public function get_image_size($file)
6975 {
6976 // phpcs:enable
6977 $file_osencoded = dol_osencode($file);
6978 $infoImg = getimagesize($file_osencoded); // Get information on image
6979 $this->imgWidth = $infoImg[0]; // Largeur de l'image
6980 $this->imgHeight = $infoImg[1]; // Hauteur de l'image
6981 }
6982
6988 public function loadStateBoard()
6989 {
6990 global $hookmanager;
6991
6992 $this->nb = array();
6993
6994 $sql = "SELECT count(p.rowid) as nb, fk_product_type";
6995 $sql .= " FROM ".$this->db->prefix()."product as p";
6996 $sql .= ' WHERE p.entity IN ('.getEntity($this->element, 1).')';
6997 // Add where from hooks
6998 if (is_object($hookmanager)) {
6999 $parameters = array();
7000 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
7001 $sql .= $hookmanager->resPrint;
7002 }
7003 $sql .= ' GROUP BY fk_product_type';
7004
7005 $resql = $this->db->query($sql);
7006 if ($resql) {
7007 while ($obj = $this->db->fetch_object($resql)) {
7008 if ($obj->fk_product_type == 1) {
7009 $this->nb["services"] = $obj->nb;
7010 } else {
7011 $this->nb["products"] = $obj->nb;
7012 }
7013 }
7014 $this->db->free($resql);
7015 return 1;
7016 } else {
7017 dol_print_error($this->db);
7018 $this->error = $this->db->error();
7019 return -1;
7020 }
7021 }
7022
7028 public function isProduct()
7029 {
7030 return $this->type == Product::TYPE_PRODUCT;
7031 }
7032
7038 public function isService()
7039 {
7040 return $this->type == Product::TYPE_SERVICE;
7041 }
7042
7048 public function isStockManaged()
7049 {
7050 return (($this->isProduct() || ($this->isService() && getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) && ($this->stockable_product > 0));
7051 }
7052
7058 public function isMandatoryPeriod()
7059 {
7060 return $this->mandatory_period == 1;
7061 }
7062
7068 public function hasbatch()
7069 {
7070 return $this->status_batch > 0;
7071 }
7072
7073
7074 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7083 public function get_barcode($object, $type = '')
7084 {
7085 // phpcs:enable
7086 global $conf;
7087
7088 $result = '';
7089 if (getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
7090 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
7091 foreach ($dirsociete as $dirroot) {
7092 $res = dol_include_once($dirroot . getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM').'.php');
7093 if ($res) {
7094 break;
7095 }
7096 }
7097 $var = getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM');
7098 $mod = new $var();
7099 '@phan-var-force ModeleNumRefBarCode $mod';
7100
7101 $result = $mod->getNextValue($object, $type);
7102
7103 dol_syslog(get_class($this)."::get_barcode barcode=".$result." module=".$var);
7104 }
7105 return $result;
7106 }
7107
7115 public function initAsSpecimen()
7116 {
7117 $now = dol_now();
7118
7119 // Initialize parameters
7120 $this->specimen = 1;
7121 $this->id = 0;
7122 $this->ref = 'PRODUCT_SPEC';
7123 $this->label = 'PRODUCT SPECIMEN';
7124 $this->description = 'This is description of this product specimen that was created the '.dol_print_date($now, 'dayhourlog').'.';
7125 $this->specimen = 1;
7126 $this->country_id = 1;
7127 $this->status = 1;
7128 $this->status_buy = 1;
7129 $this->tobatch = 0;
7130 $this->sell_or_eat_by_mandatory = 0;
7131 $this->note_private = 'This is a comment (private)';
7132 $this->note_public = 'This is a comment (public)';
7133 $this->date_creation = $now;
7134 $this->date_modification = $now;
7135
7136 $this->weight = 4;
7137 $this->weight_units = 3;
7138
7139 $this->length = 5;
7140 $this->length_units = 1;
7141 $this->width = 6;
7142 $this->width_units = 0;
7143 $this->height = null;
7144 $this->height_units = null;
7145
7146 $this->surface = 30;
7147 $this->surface_units = 0;
7148 $this->volume = 300;
7149 $this->volume_units = 0;
7150
7151 $this->barcode = -1; // Create barcode automatically
7152
7153 return 1;
7154 }
7155
7165 public function getLabelOfUnit($type = 'long', $outputlangs = null, $noentities = 0)
7166 {
7167 global $langs;
7168
7169 if (empty($this->fk_unit)) {
7170 return '';
7171 }
7172 if (empty($outputlangs)) {
7173 $outputlangs = $langs;
7174 }
7175
7176 $outputlangs->load('products');
7177 $label = '';
7178
7179 $sql = "SELECT code, label, short_label FROM ".$this->db->prefix()."c_units where rowid = ".((int) $this->fk_unit);
7180
7181 $resql = $this->db->query($sql);
7182 if (!$resql) {
7183 $this->error = $this->db->error();
7184 dol_syslog(get_class($this)."::getLabelOfUnit Error ".$this->error, LOG_ERR);
7185 return -1;
7186 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
7187 if ($type == 'short') {
7188 if ($noentities) {
7189 $label = $outputlangs->transnoentitiesnoconv($res['short_label']);
7190 } else {
7191 $label = $outputlangs->trans($res['short_label']);
7192 }
7193 } elseif ($type == 'code') {
7194 $label = $res['code'];
7195 } else {
7196 if ($outputlangs->trans('unit'.$res['code']) == 'unit'.$res['code']) {
7197 // No translation available
7198 $label = $res['label'];
7199 } else {
7200 // Return the translated value
7201 if ($noentities) {
7202 $label = $outputlangs->transnoentitiesnoconv('unit'.$res['code']);
7203 } else {
7204 $label = $outputlangs->trans('unit'.$res['code']);
7205 }
7206 }
7207 }
7208 }
7209 $this->db->free($resql);
7210
7211 return $label;
7212 }
7213
7214 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7220 public function min_recommended_price()
7221 {
7222 // phpcs:enable
7223 $maxpricesupplier = 0;
7224
7225 if (getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE')) {
7226 include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
7227 $product_fourn = new ProductFournisseur($this->db);
7228 $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', '');
7229
7230 if (is_array($product_fourn_list) && count($product_fourn_list) > 0) {
7231 foreach ($product_fourn_list as $productfourn) {
7232 if ($productfourn->fourn_unitprice > $maxpricesupplier) {
7233 $maxpricesupplier = $productfourn->fourn_unitprice;
7234 }
7235 }
7236
7237 $maxpricesupplier *= getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE');
7238 }
7239 }
7240
7241 return $maxpricesupplier;
7242 }
7243
7244
7255 public function setCategories($categories)
7256 {
7257 require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
7258 return parent::setCategoriesCommon($categories, Categorie::TYPE_PRODUCT);
7259 }
7260
7269 public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
7270 {
7271 /* we firstly delete origin product prices existing in destination before update */
7272 $sql = " DELETE FROM ".$dbs->prefix()."product_customer_price";
7273 $sql .= " WHERE fk_soc = ".(int) $origin_id;
7274 $sql .= " AND fk_product IN (SELECT fk_product FROM (SELECT fk_product FROM ".$dbs->prefix()."product_customer_price WHERE fk_soc = ".(int) $dest_id.") AS tmp)";
7275 //$sql .= ' AND EXISTS (SELECT 1 FROM '.$dbs->prefix().'product_customer_price p_new WHERE p_new.fk_product = p_old.fk_product AND p_new.fk_soc = '.(int) $dest_id.')';
7276
7277 if (!$dbs->query($sql)) {
7278 return false;
7279 }
7280
7281 $tables = array(
7282 'product_customer_price',
7283 'product_customer_price_log'
7284 );
7285
7286 return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
7287 }
7288
7300 public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
7301 {
7302 $sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM ".$this->db->prefix()."product_pricerules";
7303 $query = $this->db->query($sql);
7304
7305 $rules = array();
7306
7307 while ($result = $this->db->fetch_object($query)) {
7308 $rules[$result->level] = $result;
7309 }
7310
7311 //Because prices can be based on other level's prices, we temporarily store them
7312 $prices = array(
7313 1 => $baseprice
7314 );
7315
7316 $nbofproducts = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
7317 for ($i = 1; $i <= $nbofproducts; $i++) {
7318 $price = $baseprice;
7319 $price_min = $baseprice;
7320
7321 //We have to make sure it does exist and it is > 0
7322 //First price level only allows changing min_price
7323 if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) {
7324 $price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent / 100));
7325 }
7326
7327 $prices[$i] = $price;
7328
7329 //We have to make sure it does exist and it is > 0
7330 if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) {
7331 $price_min = $price * (1 - ($rules[$i]->var_min_percent / 100));
7332 }
7333
7334 //Little check to make sure the price is modified before triggering generation
7335 $check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i]));
7336 $check_type = ($baseprice == $this->multiprices_base_type[$i]);
7337
7338 if ($check_amount && $check_type) {
7339 continue;
7340 }
7341
7342 if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, 1) < 0) {
7343 return -1;
7344 }
7345 }
7346
7347 return 1;
7348 }
7349
7355 public function getRights()
7356 {
7357 global $user;
7358
7359 if ($this->isProduct()) {
7360 return $user->rights->produit;
7361 } else {
7362 return $user->rights->service;
7363 }
7364 }
7365
7372 public function info($id)
7373 {
7374 $sql = "SELECT p.rowid, p.ref, p.datec as date_creation, GREATEST(p.tms, pef.tms) as date_modification,";
7375 $sql .= " p.fk_user_author, p.fk_user_modif";
7376 $sql .= " FROM ".$this->db->prefix().$this->table_element." as p";
7377 $sql .= " LEFT JOIN ".$this->db->prefix().$this->table_element."_extrafields as pef ON pef.fk_object=p.rowid";
7378 $sql .= " WHERE p.rowid = ".((int) $id);
7379
7380 $result = $this->db->query($sql);
7381 if ($result) {
7382 if ($this->db->num_rows($result)) {
7383 $obj = $this->db->fetch_object($result);
7384
7385 $this->id = $obj->rowid;
7386 $this->ref = $obj->ref;
7387
7388 $this->user_creation_id = $obj->fk_user_author;
7389 $this->user_modification_id = $obj->fk_user_modif;
7390
7391 $this->date_creation = $this->db->jdate($obj->date_creation);
7392 $this->date_modification = $this->db->jdate($obj->date_modification);
7393 }
7394
7395 $this->db->free($result);
7396 } else {
7397 dol_print_error($this->db);
7398 }
7399 }
7400
7401
7407 public function getProductDurationHours()
7408 {
7409 if (empty($this->duration_value)) {
7410 $this->errors[] = 'ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
7411 return -1;
7412 }
7413 if ($this->duration_unit == 's') {
7414 $prodDurationHours = 1. / 3600;
7415 } elseif ($this->duration_unit == 'i' || $this->duration_unit == 'mn' || $this->duration_unit == 'min') {
7416 $prodDurationHours = 1. / 60;
7417 } elseif ($this->duration_unit == 'h') {
7418 $prodDurationHours = 1.;
7419 } elseif ($this->duration_unit == 'd') {
7420 $prodDurationHours = 24.;
7421 } elseif ($this->duration_unit == 'w') {
7422 $prodDurationHours = 24. * 7;
7423 } elseif ($this->duration_unit == 'm') {
7424 $prodDurationHours = 24. * 30;
7425 } elseif ($this->duration_unit == 'y') {
7426 $prodDurationHours = 24. * 365;
7427 } else {
7428 $prodDurationHours = 0.0;
7429 }
7430 $prodDurationHours *= $this->duration_value;
7431
7432 return $prodDurationHours;
7433 }
7434
7435
7443 public function getKanbanView($option = '', $arraydata = null)
7444 {
7445 global $langs, $conf;
7446
7447 $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
7448
7449 $return = '<div class="box-flex-item box-flex-grow-zero">';
7450 $return .= '<div class="info-box info-box-sm">';
7451 $return .= '<div class="info-box-img">';
7452 $label = '';
7453 if ($this->entity !== null && $this->is_photo_available($conf->product->multidir_output[$this->entity])) {
7454 $label .= $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 120, 160, 0, 0, 0, '', 'photoref photokanban');
7455 $return .= $label;
7456 } else {
7457 if ($this->isProduct()) {
7458 $label .= img_picto('', 'product');
7459 } elseif ($this->isService()) {
7460 $label .= img_picto('', 'service');
7461 }
7462 $return .= $label;
7463 }
7464 $return .= '</div>';
7465 $return .= '<div class="info-box-content">';
7466 $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">' . $this->getNomUrl() . '</span>';
7467 if ($selected >= 0) {
7468 $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
7469 }
7470 $return .= '<br><span class="info-box-label opacitymedium inline-block tdoverflowmax150 valignmiddle" title="'.dol_escape_htmltag($this->label).'">'.dol_escape_htmltag($this->label).'</span>';
7471 if ($this->price_base_type == 'TTC') {
7472 $return .= '<br><span class="info-box-status amount">'.price($this->price_ttc).' '.$langs->trans("TTC").'</span>';
7473 } else {
7474 if ($this->status) {
7475 $return .= '<br><span class="info-box-status amount">'.price($this->price).' '.$langs->trans("HT").'</span>';
7476 }
7477 }
7478 $br = 1;
7479 if ($this->isProduct()) {
7480 $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>';
7481 $br = 0;
7482 }
7483 if ($br) {
7484 $return .= '<br>';
7485 $return .= '<div class="info-box-status inline-block valignmiddle">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7486 } else {
7487 $return .= '<div class="info-box-status inline-block valignmiddle marginleftonly paddingleft">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7488 }
7489 $return .= '</div>';
7490 $return .= '</div>';
7491 $return .= '</div>';
7492 return $return;
7493 }
7494}
7495
7501{
7502 public $picto = 'service';
7503}
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)
Loads supplier reception statistics table for given product or 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:169
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.
dolGetStatus($statusLabel='', $statusLabelShort='', $html='', $statusType='status0', $displayMode=0, $url='', $params=array())
Output the badge of a status.
dol_clone($srcobject, $native=2)
Create a clone of instance of object (new instance with same value for each properties) With native =...
dol_print_date($time, $format='', $tzoutput='auto', $outputlangs=null, $encodetooutput=false, $decorate=0)
Output date in a string format according to outputlangs (or langs if not defined).
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
dol_trunc($string, $size=40, $trunc='right', $stringencoding='UTF-8', $nodot=0, $display=0)
Truncate a string to a particular length adding '...' if string larger than length.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
isModEnabled($module)
Is Dolibarr module enabled.
utf8_check($str)
Check if a string is in UTF8.
get_default_tva(Societe $thirdparty_seller, Societe $thirdparty_buyer, $idprod=0, $idprodfournprice=0)
Function that return vat rate of a product line (according to seller, buyer and product vat rate) VAT...
get_localtax($vatrate, $local, $thirdparty_buyer=null, $thirdparty_seller=null, $vatnpr=0)
Return localtax rate for a particular VAT rate, when selling a product with vat $vatrate,...
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
getEntity($element, $shared=1, $currentobject=null)
Get list of entity id to use.
dol_mkdir($dir, $dataroot='', $newmask='')
Creation of a directory (this can create recursive subdir)
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
Returns text escaped for inclusion in HTML alt or title or value tags, or into values of HTML input f...
image_format_supported($file, $acceptsvg=0)
Return if a filename is file name of a supported image format.
query($query, $usesavepoint=0, $type='auto', $result_mode=0)
Execute a SQL request and return the resultset.
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