dolibarr 25.0.0-alpha
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 * Copyright (C) 2026 Jose Martinez <jose.martinez@pichinov.com>
23 *
24 * This program is free software; you can redistribute it and/or modify
25 * it under the terms of the GNU General Public License as published by
26 * the Free Software Foundation; either version 3 of the License, or
27 * (at your option) any later version.
28 *
29 * This program is distributed in the hope that it will be useful,
30 * but WITHOUT ANY WARRANTY; without even the implied warranty of
31 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32 * GNU General Public License for more details.
33 *
34 * You should have received a copy of the GNU General Public License
35 * along with this program. If not, see <https://www.gnu.org/licenses/>.
36 */
37
43require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
44require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
45require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
46require_once DOL_DOCUMENT_ROOT.'/product/stock/class/productlot.class.php';
47require_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php';
48
52class Product extends CommonObject
53{
58 const SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY = 1;
59 const SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY = 2;
60 const SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT = 3;
61
66 public $TRIGGER_PREFIX = 'PRODUCT';
67
71 public $element = 'product';
72
76 public $table_element = 'product';
77
81 public $fk_element = 'fk_product';
82
86 protected $childtables = array(
87 'supplier_proposaldet' => array('name' => 'SupplierProposal', 'parent' => 'supplier_proposal', 'parentkey' => 'fk_supplier_proposal'),
88 'propaldet' => array('name' => 'Proposal', 'parent' => 'propal', 'parentkey' => 'fk_propal'),
89 'commandedet' => array('name' => 'Order', 'parent' => 'commande', 'parentkey' => 'fk_commande'),
90 'facturedet' => array('name' => 'Invoice', 'parent' => 'facture', 'parentkey' => 'fk_facture'),
91 'contratdet' => array('name' => 'Contract', 'parent' => 'contrat', 'parentkey' => 'fk_contrat'),
92 'facture_fourn_det' => array('name' => 'SupplierInvoice', 'parent' => 'facture_fourn', 'parentkey' => 'fk_facture_fourn'),
93 'commande_fournisseurdet' => array('name' => 'SupplierOrder', 'parent' => 'commande_fournisseur', 'parentkey' => 'fk_commande'),
94 'mrp_production' => array('name' => 'Mo', 'parent' => 'mrp_mo', 'parentkey' => 'fk_mo', 'enabled' => 'isModEnabled("mrp")'),
95 'bom_bom' => array('name' => 'BOM', 'enabled' => 'isModEnabled("bom")'),
96 'bom_bomline' => array('name' => 'BOMLine', 'parent' => 'bom_bom', 'parentkey' => 'fk_bom', 'enabled' => 'isModEnabled("bom")'),
97 );
98
104 public $picto = 'product';
105
109 protected $table_ref_field = 'ref';
110
115 public $regeximgext = '\.gif|\.jpg|\.jpeg|\.png|\.bmp|\.webp|\.xpm|\.xbm|\.avif';
116
122 public $ref;
123
129 public $libelle;
130
136 public $label;
137
143 public $description;
144
150 public $other;
151
157 public $type = self::TYPE_PRODUCT;
158
164 public $price;
165
169 public $price_formated; // used by takepos/ajax/ajax.php
170
176 public $price_ttc;
177
181 public $price_ttc_formated; // used by takepos/ajax/ajax.php
182
188 public $price_min;
189
195 public $price_min_ttc;
196
201 public $price_base_type;
205 public $price_label;
206
208
211 public $multiprices = array();
215 public $multiprices_ttc = array();
219 public $multiprices_base_type = array();
223 public $multiprices_default_vat_code = array();
227 public $multiprices_min = array();
231 public $multiprices_min_ttc = array();
235 public $multiprices_tva_tx = array();
239 public $multiprices_recuperableonly = array();
240
242
245 public $price_by_qty;
249 public $prices_by_qty = array();
253 public $prices_by_qty_id = array();
257 public $prices_by_qty_list = array();
258
262 public $level;
263
267 public $multilangs = array();
268
272 public $default_vat_code;
273
277 public $tva_tx;
278
282 public $tva_npr = 0;
283
287 public $remise_percent;
288
292 public $localtax1_tx;
296 public $localtax2_tx;
300 public $localtax1_type;
304 public $localtax2_type;
305
306 // Properties set by get_buyprice() for return
307
311 public $desc_supplier;
315 public $vatrate_supplier;
319 public $default_vat_code_supplier;
320
324 public $fourn_multicurrency_price;
325
329 public $fourn_multicurrency_unitprice;
333 public $fourn_multicurrency_tx;
337 public $fourn_multicurrency_id;
341 public $fourn_multicurrency_code;
342
346 public $packaging;
347
348
355 public $lifetime;
356
363 public $qc_frequency;
364
370 public $stock_reel = 0;
371
377 public $stock_theorique;
378
384 public $cost_price;
385
391 public $pmp;
392
398 public $seuil_stock_alerte = 0;
399
403 public $desiredstock = 0;
404
408 public $duration_value;
412 public $duration_unit;
416 public $duration;
417
421 public $fk_default_workstation;
422
428 public $status = 0;
429
436 public $tosell;
437
443 public $status_buy = 0;
444
451 public $tobuy;
452
458 public $finished;
459
465 public $fk_default_bom;
466
472 public $product_fourn_price_id;
473
479 public $buyprice;
480
486 public $tobatch;
487
488
494 public $status_batch = 0;
495
501 public $sell_or_eat_by_mandatory = 0;
502
508 public $batch_mask = '';
509
515 public $customcode;
516
522 public $url;
523
525
528 public $weight;
529
533 public $weight_units; // scale -3, 0, 3, 6
537 public $length;
541 public $length_units; // scale -3, 0, 3, 6
545 public $width;
549 public $width_units; // scale -3, 0, 3, 6
553 public $height;
557 public $height_units; // scale -3, 0, 3, 6
561 public $surface;
565 public $surface_units; // scale -3, 0, 3, 6
569 public $volume;
573 public $volume_units; // scale -3, 0, 3, 6
574
578 public $net_measure;
582 public $net_measure_units; // scale -3, 0, 3, 6
583
587 public $accountancy_code_sell;
591 public $accountancy_code_sell_intra;
595 public $accountancy_code_sell_export;
599 public $accountancy_code_buy;
603 public $accountancy_code_buy_intra;
607 public $accountancy_code_buy_export;
608
612 public $barcode;
613
617 public $barcode_type;
618
622 public $barcode_type_code;
623
627 public $stats_propale = array();
628
632 public $stats_commande = array();
633
637 public $stats_contrat = array();
638
642 public $stats_facture = array();
643
647 public $stats_proposal_supplier = array();
648
652 public $stats_commande_fournisseur = array();
653
657 public $stats_expedition = array();
658
662 public $stats_reception = array();
663
667 public $stats_mo = array();
668
672 public $stats_bom = array();
673
677 public $stats_mrptoconsume = array();
678
682 public $stats_mrptoproduce = array();
683
687 public $stats_facturerec = array();
688
692 public $stats_facture_fournisseur = array();
693
697 public $stats_facturefournrec = array();
698
702 public $imgWidth;
706 public $imgHeight;
707
712 public $product_fourn_id;
713
718 public $product_id_already_linked;
719
724 public $nbphoto = 0;
725
729 public $stock_warehouse = array();
730
734 public $fk_default_warehouse;
735
739 public $fk_price_expression;
740
745 public $fourn_qty;
746
751 public $fourn_pu;
752
757 public $fourn_price_base_type;
758
762 public $fourn_socid;
763
769 public $ref_fourn;
770
774 public $ref_supplier;
775
781 public $fk_unit;
782
788 public $price_autogen = 0;
789
795 public $sousprods = array();
796
800 public $res;
801
802
808 public $is_object_used;
809
819 public $is_sousproduit_qty;
820
831 public $is_sousproduit_incdec;
832
836 public $mandatory_period;
837
843 public $stockable_product = 1;
844
873 public $fields = array(
874 'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'index' => 1, 'position' => 1, 'comment' => 'Id'),
875 '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'),
876 'entity' => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'visible' => 0, 'default' => '1', 'notnull' => 1, 'index' => 1, 'position' => 5),
877 'label' => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'showoncombobox' => 2, 'position' => 15, 'csslist' => 'tdoverflowmax250'),
878 'barcode' => array('type' => 'varchar(255)', 'label' => 'Barcode', 'enabled' => 'isModEnabled("barcode")', 'position' => 20, 'visible' => -1, 'showoncombobox' => 3, 'cssview' => 'tdwordbreak', 'csslist' => 'tdoverflowmax125'),
879 'fk_barcode_type' => array('type' => 'integer', 'label' => 'BarcodeType', 'enabled' => 1, 'position' => 21, 'notnull' => 0, 'visible' => -1,),
880 'note_public' => array('type' => 'html', 'label' => 'NotePublic', 'enabled' => 1, 'visible' => 0, 'position' => 61),
881 'note' => array('type' => 'html', 'label' => 'NotePrivate', 'enabled' => 1, 'visible' => 0, 'position' => 62),
882 'datec' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 500),
883 'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 501),
884 //'date_valid' =>array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-2, 'position'=>502),
885 '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'),
886 'fk_user_modif' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'UserModif', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'position' => 511),
887 //'fk_user_valid' =>array('type'=>'integer', 'label'=>'UserValidation', 'enabled'=>1, 'visible'=>-1, 'position'=>512),
888 'localtax1_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax1tx', 'enabled' => 1, 'position' => 150, 'notnull' => 0, 'visible' => -1,),
889 'localtax1_type' => array('type' => 'varchar(10)', 'label' => 'Localtax1type', 'enabled' => 1, 'position' => 155, 'notnull' => 1, 'visible' => -1,),
890 'localtax2_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax2tx', 'enabled' => 1, 'position' => 160, 'notnull' => 0, 'visible' => -1,),
891 'localtax2_type' => array('type' => 'varchar(10)', 'label' => 'Localtax2type', 'enabled' => 1, 'position' => 165, 'notnull' => 1, 'visible' => -1,),
892 'last_main_doc' => array('type' => 'varchar(255)', 'label' => 'LastMainDoc', 'enabled' => 1, 'visible' => -1, 'position' => 170),
893 'import_key' => array('type' => 'varchar(14)', 'label' => 'ImportId', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'index' => 0, 'position' => 1000),
894 //'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')),
895 //'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')),
896 'mandatory_period' => array('type' => 'integer', 'label' => 'mandatoryperiod', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'default' => '0', 'index' => 1, 'position' => 1000),
897 'stockable_product' => array('type' => 'integer', 'label' => 'stockable_product', 'enabled' => 1, 'visible' => 1, 'default' => '1', 'notnull' => 1, 'index' => 1, 'position' => 502),
898 );
899
903 const TYPE_PRODUCT = 0;
907 const TYPE_SERVICE = 1;
908
912 const DISABLED_STOCK = 0;
913 const ENABLED_STOCK = 1;
914
920 public function __construct($db)
921 {
922 $this->db = $db;
923
924 $this->ismultientitymanaged = 1;
925 $this->isextrafieldmanaged = 1;
926
927 $this->canvas = '';
928 }
929
935 public function check()
936 {
937 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
938 $this->ref = trim($this->ref);
939 } else {
940 $this->ref = dol_sanitizeFileName(stripslashes($this->ref));
941 }
942
943 $err = 0;
944 if (dol_strlen(trim($this->ref)) == 0) {
945 $err++;
946 }
947
948 if (dol_strlen(trim($this->label)) == 0) {
949 $err++;
950 }
951
952 if ($err > 0) {
953 return 0;
954 } else {
955 return 1;
956 }
957 }
958
966 public function create($user, $notrigger = 0)
967 {
968 global $conf, $langs;
969
970 $error = 0;
971
972 // Clean parameters
973 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
974 $this->ref = trim($this->ref);
975 } else {
976 $this->ref = dol_sanitizeFileName(dol_string_nospecial(trim($this->ref)));
977 }
978 $this->label = trim($this->label);
979 $this->price_ttc = (float) price2num($this->price_ttc);
980 $this->price = (float) price2num($this->price);
981 $this->price_min_ttc = (float) price2num($this->price_min_ttc);
982 $this->price_min = (float) price2num($this->price_min);
983 $this->price_label = trim($this->price_label);
984 if (empty($this->tva_tx)) {
985 $this->tva_tx = 0;
986 }
987 if (empty($this->tva_npr)) {
988 $this->tva_npr = 0;
989 }
990 // Local taxes
991 if (empty($this->localtax1_tx)) {
992 $this->localtax1_tx = 0;
993 }
994 if (empty($this->localtax2_tx)) {
995 $this->localtax2_tx = 0;
996 }
997 if (empty($this->localtax1_type)) {
998 $this->localtax1_type = '0';
999 }
1000 if (empty($this->localtax2_type)) {
1001 $this->localtax2_type = '0';
1002 }
1003 // Price
1004 if (empty($this->price_base_type) && getDolGlobalString('PRODUCT_PRICE_BASE_TYPE')) {
1005 $this->price_base_type = getDolGlobalString('PRODUCT_PRICE_BASE_TYPE');
1006 }
1007 if (empty($this->price)) {
1008 $this->price = 0;
1009 }
1010 if (empty($this->price_min)) {
1011 $this->price_min = 0;
1012 }
1013 // Price by quantity
1014 if (empty($this->price_by_qty)) {
1015 $this->price_by_qty = 0;
1016 }
1017
1018 if (empty($this->status)) {
1019 $this->status = 0;
1020 }
1021 if (empty($this->status_buy)) {
1022 $this->status_buy = 0;
1023 }
1024 if (empty($this->stockable_product)) {
1025 $this->stockable_product = 0;
1026 }
1027
1028 $price_ht = 0;
1029 $price_ttc = 0;
1030 $price_min_ht = 0;
1031 $price_min_ttc = 0;
1032
1033 //
1034 if ($this->price_base_type == 'TTC' && $this->price_ttc > 0) {
1035 $price_ttc = price2num($this->price_ttc, 'MU');
1036 $price_ht = price2num($this->price_ttc / (1 + ($this->tva_tx / 100)), 'MU');
1037 }
1038
1039 //
1040 if ($this->price_base_type != 'TTC' && $this->price > 0) {
1041 $price_ht = price2num($this->price, 'MU');
1042 $price_ttc = price2num($this->price * (1 + ($this->tva_tx / 100)), 'MU');
1043 }
1044
1045 //
1046 if (($this->price_min_ttc > 0) && ($this->price_base_type == 'TTC')) {
1047 $price_min_ttc = price2num($this->price_min_ttc, 'MU');
1048 $price_min_ht = price2num($this->price_min_ttc / (1 + ($this->tva_tx / 100)), 'MU');
1049 }
1050
1051 //
1052 if (($this->price_min > 0) && ($this->price_base_type != 'TTC')) {
1053 $price_min_ht = price2num($this->price_min, 'MU');
1054 $price_min_ttc = price2num($this->price_min * (1 + ($this->tva_tx / 100)), 'MU');
1055 }
1056
1057 $this->accountancy_code_buy = trim((string) $this->accountancy_code_buy);
1058 $this->accountancy_code_buy_intra = trim((string) $this->accountancy_code_buy_intra);
1059 $this->accountancy_code_buy_export = trim((string) $this->accountancy_code_buy_export);
1060 $this->accountancy_code_sell = trim((string) $this->accountancy_code_sell);
1061 $this->accountancy_code_sell_intra = trim((string) $this->accountancy_code_sell_intra);
1062 $this->accountancy_code_sell_export = trim((string) $this->accountancy_code_sell_export);
1063
1064 // Normalize the accountancy codes the way the admin dropdown does it, so an API client that
1065 // sends '606111000' ends up with the same '606111' value the GUI stores (see issue #32343).
1066 // When ACCOUNTING_MANAGE_ZERO is on, trailing zeros are part of the code and must be kept.
1067 if (!getDolGlobalString('ACCOUNTING_MANAGE_ZERO')) {
1068 require_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
1069 $this->accountancy_code_buy = clean_account($this->accountancy_code_buy);
1070 $this->accountancy_code_buy_intra = clean_account($this->accountancy_code_buy_intra);
1071 $this->accountancy_code_buy_export = clean_account($this->accountancy_code_buy_export);
1072 $this->accountancy_code_sell = clean_account($this->accountancy_code_sell);
1073 $this->accountancy_code_sell_intra = clean_account($this->accountancy_code_sell_intra);
1074 $this->accountancy_code_sell_export = clean_account($this->accountancy_code_sell_export);
1075 }
1076
1077 // Barcode value
1078 $this->barcode = trim($this->barcode);
1079 $this->mandatory_period = empty($this->mandatory_period) ? 0 : $this->mandatory_period;
1080 // Check parameters
1081 if (empty($this->label)) {
1082 $langs->load('errors');
1083 $this->errors[] = $langs->trans('ErrorMandatoryParametersNotProvided');
1084 return -1;
1085 }
1086
1087 if (empty($this->ref) || $this->ref == 'auto') {
1088 // Load object modCodeProduct
1089 $module = getDolGlobalString('PRODUCT_CODEPRODUCT_ADDON', 'mod_codeproduct_leopard');
1090 if ($module != 'mod_codeproduct_leopard') { // Do not load module file for leopard
1091 if (substr($module, 0, 16) == 'mod_codeproduct_' && substr($module, -3) == 'php') {
1092 $module = substr($module, 0, dol_strlen($module) - 4);
1093 }
1094 dol_include_once('/core/modules/product/'.$module.'.php');
1095 $modCodeProduct = new $module();
1096 '@phan-var-force ModeleProductCode $modCodeProduct';
1097 if (!empty($modCodeProduct->code_auto)) {
1098 $this->ref = $modCodeProduct->getNextValue($this, $this->type);
1099 }
1100 unset($modCodeProduct);
1101 }
1102
1103 if (empty($this->ref)) {
1104 $this->error = 'ProductModuleNotSetupForAutoRef';
1105 return -2;
1106 }
1107 }
1108
1109 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);
1110
1111 $now = dol_now();
1112
1113 if (empty($this->date_creation)) {
1114 $this->date_creation = $now;
1115 }
1116
1117 $this->db->begin();
1118
1119 // For automatic creation during create action (not used by Dolibarr GUI, can be used by scripts)
1120 if ($this->barcode == '-1' || $this->barcode == 'auto') {
1121 $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1122 }
1123
1124 // Check more parameters
1125 // If error, this->errors[] is filled
1126 $result = $this->verify();
1127
1128 if ($result >= 0) {
1129 $sql = "SELECT count(*) as nb";
1130 $sql .= " FROM ".$this->db->prefix()."product";
1131 $sql .= " WHERE entity IN (".getEntity('product').")";
1132 $sql .= " AND ref = '".$this->db->escape($this->ref)."'";
1133
1134 $result = $this->db->query($sql);
1135 if ($result) {
1136 $obj = $this->db->fetch_object($result);
1137 if ($obj->nb == 0) {
1138 // Insert new product, no previous one found
1139 $sql = "INSERT INTO ".$this->db->prefix()."product (";
1140 $sql .= "datec";
1141 $sql .= ", entity";
1142 $sql .= ", ref";
1143 $sql .= ", ref_ext";
1144 $sql .= ", price_min";
1145 $sql .= ", price_min_ttc";
1146 $sql .= ", label";
1147 $sql .= ", fk_user_author";
1148 $sql .= ", fk_product_type";
1149 $sql .= ", price";
1150 $sql .= ", price_ttc";
1151 $sql .= ", price_base_type";
1152 $sql .= ", price_label";
1153 $sql .= ", tobuy";
1154 $sql .= ", tosell";
1155 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1156 $sql .= ", accountancy_code_buy";
1157 $sql .= ", accountancy_code_buy_intra";
1158 $sql .= ", accountancy_code_buy_export";
1159 $sql .= ", accountancy_code_sell";
1160 $sql .= ", accountancy_code_sell_intra";
1161 $sql .= ", accountancy_code_sell_export";
1162 }
1163 $sql .= ", canvas";
1164 $sql .= ", finished";
1165 $sql .= ", tobatch";
1166 $sql .= ", sell_or_eat_by_mandatory";
1167 $sql .= ", batch_mask";
1168 $sql .= ", fk_unit";
1169 $sql .= ", mandatory_period";
1170 $sql .= ", stockable_product";
1171 if (!empty($this->default_vat_code)) {
1172 $sql .= ", default_vat_code";
1173 }
1174 $sql .= ") VALUES (";
1175 $sql .= "'".$this->db->idate($this->date_creation)."'";
1176 $sql .= ", ".(!empty($this->entity) ? (int) $this->entity : (int) $conf->entity);
1177 $sql .= ", '".$this->db->escape($this->ref)."'";
1178 $sql .= ", ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
1179 $sql .= ", ".price2num($price_min_ht);
1180 $sql .= ", ".price2num($price_min_ttc);
1181 $sql .= ", ".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null");
1182 $sql .= ", ".((int) $user->id);
1183 $sql .= ", ".((int) $this->type);
1184 $sql .= ", ".price2num($price_ht, 'MT');
1185 $sql .= ", ".price2num($price_ttc, 'MT');
1186 $sql .= ", '".$this->db->escape($this->price_base_type)."'";
1187 $sql .= ", ".(!empty($this->price_label) ? "'".$this->db->escape($this->price_label)."'" : "null");
1188 $sql .= ", ".((int) $this->status);
1189 $sql .= ", ".((int) $this->status_buy);
1190 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1191 $sql .= ", '".$this->db->escape($this->accountancy_code_buy)."'";
1192 $sql .= ", '".$this->db->escape($this->accountancy_code_buy_intra)."'";
1193 $sql .= ", '".$this->db->escape($this->accountancy_code_buy_export)."'";
1194 $sql .= ", '".$this->db->escape($this->accountancy_code_sell)."'";
1195 $sql .= ", '".$this->db->escape($this->accountancy_code_sell_intra)."'";
1196 $sql .= ", '".$this->db->escape($this->accountancy_code_sell_export)."'";
1197 }
1198 $sql .= ", '".$this->db->escape($this->canvas)."'";
1199 $sql .= ", ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? 'NULL' : (int) $this->finished);
1200 $sql .= ", ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : ((int) $this->status_batch));
1201 $sql .= ", ".((empty($this->sell_or_eat_by_mandatory) || $this->sell_or_eat_by_mandatory < 0) ? 0 : ((int) $this->sell_or_eat_by_mandatory));
1202 $sql .= ", '".$this->db->escape($this->batch_mask)."'";
1203 $sql .= ", ".($this->fk_unit > 0 ? ((int) $this->fk_unit) : 'NULL');
1204 $sql .= ", '".$this->db->escape((string) $this->mandatory_period)."'";
1205 $sql .= ", ".((int) $this->stockable_product);
1206 if (!empty($this->default_vat_code)) {
1207 $sql .= ", '".$this->db->escape($this->default_vat_code)."'";
1208 }
1209 $sql .= ")";
1210 dol_syslog(get_class($this)."::Create", LOG_DEBUG);
1211
1212 $result = $this->db->query($sql);
1213 if ($result) {
1214 $id = $this->db->last_insert_id($this->db->prefix()."product");
1215
1216 if ($id > 0) {
1217 $this->id = $id;
1218 $this->price = $price_ht;
1219 $this->price_ttc = $price_ttc;
1220 $this->price_min = $price_min_ht;
1221 $this->price_min_ttc = $price_min_ttc;
1222
1223 $result = $this->_log_price($user);
1224 if ($result > 0) {
1225 if ($this->update($id, $user, 1, 'add') <= 0) {
1226 $error++;
1227 }
1228 } else {
1229 $error++;
1230 $this->error = $this->db->lasterror();
1231 }
1232
1233 // update accountancy for this entity
1234 if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1235 $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " .((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1236
1237 $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1238 $sql .= " fk_product";
1239 $sql .= ", entity";
1240 $sql .= ", accountancy_code_buy";
1241 $sql .= ", accountancy_code_buy_intra";
1242 $sql .= ", accountancy_code_buy_export";
1243 $sql .= ", accountancy_code_sell";
1244 $sql .= ", accountancy_code_sell_intra";
1245 $sql .= ", accountancy_code_sell_export";
1246 $sql .= ") VALUES (";
1247 $sql .= ((int) $this->id);
1248 $sql .= ", " . ((int) $conf->entity);
1249 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1250 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1251 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1252 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1253 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1254 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1255 $sql .= ")";
1256 $result = $this->db->query($sql);
1257 if (!$result) {
1258 $error++;
1259 $this->error = 'ErrorFailedToInsertAccountancyForEntity';
1260 }
1261 }
1262 } else {
1263 $error++;
1264 $this->error = 'ErrorFailedToGetInsertedId';
1265 }
1266 } else {
1267 $error++;
1268 $this->error = $this->db->lasterror();
1269 }
1270 } else {
1271 // Product already exists with this ref
1272 $langs->load("products");
1273 $error++;
1274 $this->error = "ErrorProductAlreadyExists";
1275 dol_syslog(get_class($this)."::Create fails, ref ".$this->ref." already exists");
1276 }
1277 } else {
1278 $error++;
1279 $this->error = $this->db->lasterror();
1280 }
1281
1282 if (!$error && !$notrigger) {
1283 // Call trigger
1284 $result = $this->call_trigger('PRODUCT_CREATE', $user);
1285 if ($result < 0) {
1286 $error++;
1287 }
1288 // End call triggers
1289 }
1290
1291 if (!$error) {
1292 $this->db->commit();
1293 return $this->id;
1294 } else {
1295 $this->db->rollback();
1296 return -$error;
1297 }
1298 } else {
1299 $this->db->rollback();
1300 dol_syslog(get_class($this)."::Create fails verify ".implode(',', $this->errors), LOG_WARNING);
1301 return -3;
1302 }
1303 }
1304
1305
1312 public function verify()
1313 {
1314 global $langs;
1315
1316 $this->errors = array();
1317
1318 $result = 0;
1319 $this->ref = trim($this->ref);
1320
1321 if (!$this->ref) {
1322 $this->errors[] = 'ErrorBadRef';
1323 $result = -2;
1324 }
1325
1326 $arrayofnonnegativevalue = array('weight' => 'Weight', 'width' => 'Width', 'height' => 'Height', 'length' => 'Length', 'surface' => 'Surface', 'volume' => 'Volume');
1327 foreach ($arrayofnonnegativevalue as $key => $value) {
1328 if (property_exists($this, $key) && !empty($this->$key) && ($this->$key < 0)) {
1329 $langs->loadLangs(array("main", "other"));
1330 $this->error = $langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv($value));
1331 $this->errors[] = $this->error;
1332 $result = -4;
1333 }
1334 }
1335
1336 $rescode = $this->check_barcode($this->barcode, $this->barcode_type_code);
1337 if ($rescode) {
1338 if ($rescode == -1) {
1339 $this->errors[] = 'ErrorBadBarCodeSyntax';
1340 } elseif ($rescode == -2) {
1341 $this->errors[] = 'ErrorBarCodeRequired';
1342 } elseif ($rescode == -3) {
1343 // Note: Common usage is to have barcode unique. For variants, we should have a different barcode.
1344 $this->errors[] = 'ErrorBarCodeAlreadyUsed';
1345 }
1346
1347 $result = -3;
1348 }
1349
1350 return $result;
1351 }
1352
1353 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1364 public function check_barcode($valuetotest, $typefortest)
1365 {
1366 // phpcs:enable
1367 global $conf;
1368
1369 if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
1370 $module = strtolower(getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM'));
1371
1372 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
1373 foreach ($dirsociete as $dirroot) {
1374 $res = dol_include_once($dirroot.$module.'.php');
1375 if ($res) {
1376 break;
1377 }
1378 }
1379
1380 $mod = new $module();
1381 '@phan-var-force ModeleNumRefBarCode $mod';
1384 dol_syslog(get_class($this)."::check_barcode value=".$valuetotest." type=".$typefortest." module=".$module);
1385 $result = $mod->verif($this->db, $valuetotest, $this, 0, $typefortest);
1386 return $result;
1387 } else {
1388 return 0;
1389 }
1390 }
1391
1403 public function update($id, $user, $notrigger = 0, $action = 'update', $updatetype = false)
1404 {
1405 global $langs, $conf, $hookmanager;
1406
1407 $error = 0;
1408
1409 // Check parameters
1410 if (!$this->label) {
1411 $this->label = 'MISSING LABEL';
1412 }
1413
1414 // Clean parameters
1415 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
1416 $this->ref = trim($this->ref);
1417 } else {
1418 $this->ref = dol_string_nospecial(trim($this->ref));
1419 }
1420 $this->label = trim($this->label);
1421 $this->description = trim($this->description);
1422 $this->note_private = (isset($this->note_private) ? trim($this->note_private) : null);
1423 $this->note_public = (isset($this->note_public) ? trim($this->note_public) : null);
1424 $this->net_measure = price2num($this->net_measure);
1425 $this->net_measure_units = (!is_numeric($this->net_measure_units) ? null : (int) $this->net_measure_units);
1426 $this->weight = price2num($this->weight);
1427 $this->weight_units = (!is_numeric($this->weight_units) ? null : (int) $this->weight_units);
1428 $this->length = price2num($this->length);
1429 $this->length_units = (!is_numeric($this->length_units) ? null : (int) $this->length_units);
1430 $this->width = price2num($this->width);
1431 $this->width_units = (!is_numeric($this->width_units) ? null : (int) $this->width_units);
1432 $this->height = price2num($this->height);
1433 $this->height_units = (!is_numeric($this->height_units) ? null : (int) $this->height_units);
1434 $this->surface = price2num($this->surface);
1435 $this->surface_units = (!is_numeric($this->surface_units) ? null : (int) $this->surface_units);
1436 $this->volume = price2num($this->volume);
1437 $this->volume_units = (!is_numeric($this->volume_units) ? null : (int) $this->volume_units);
1438
1439 // set unit not defined
1440 if (is_numeric($this->length_units)) {
1441 $this->width_units = $this->length_units; // Not used yet
1442 }
1443 if (is_numeric($this->length_units)) {
1444 $this->height_units = $this->length_units; // Not used yet
1445 }
1446
1447 // Automated compute surface and volume if not filled
1448 if (empty($this->surface) && !empty($this->length) && !empty($this->width) && $this->length_units == $this->width_units) {
1449 $this->surface = (float) $this->length * (float) $this->width;
1450 $this->surface_units = measuring_units_squared((int) $this->length_units);
1451 }
1452 if (empty($this->volume) && !empty($this->surface) && !empty($this->height) && $this->length_units == $this->height_units) {
1453 $this->volume = $this->surface * (float) $this->height;
1454 $this->volume_units = measuring_units_cubed((int) $this->height_units);
1455 }
1456
1457 if (empty($this->tva_tx)) {
1458 $this->tva_tx = 0;
1459 }
1460 if (empty($this->tva_npr)) {
1461 $this->tva_npr = 0;
1462 }
1463 if (empty($this->localtax1_tx)) {
1464 $this->localtax1_tx = 0;
1465 }
1466 if (empty($this->localtax2_tx)) {
1467 $this->localtax2_tx = 0;
1468 }
1469 if (empty($this->localtax1_type)) {
1470 $this->localtax1_type = '0';
1471 }
1472 if (empty($this->localtax2_type)) {
1473 $this->localtax2_type = '0';
1474 }
1475 if (empty($this->status)) {
1476 $this->status = 0;
1477 }
1478 if (empty($this->status_buy)) {
1479 $this->status_buy = 0;
1480 }
1481
1482 if (empty($this->country_id)) {
1483 $this->country_id = 0;
1484 }
1485 if (empty($this->country_id) && !empty($this->country_code)) {
1486 $country_id = getCountry($this->country_code, '3');
1487 $this->country_id = is_int($country_id) ? $country_id : 0;
1488 }
1489
1490 if (empty($this->state_id)) {
1491 $this->state_id = 0;
1492 }
1493
1494 if (empty($this->stockable_product)) {
1495 $this->stockable_product = 0;
1496 }
1497
1498 // For automatic creation during update action (not used by Dolibarr GUI, can be used by scripts)
1499 if ($this->barcode == '-1' || $this->barcode == 'auto') {
1500 $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1501 }
1502
1503 // Barcode value
1504 $this->barcode = (empty($this->barcode) ? '' : trim($this->barcode));
1505
1506 $this->accountancy_code_buy = trim($this->accountancy_code_buy);
1507 $this->accountancy_code_buy_intra = (!empty($this->accountancy_code_buy_intra) ? trim($this->accountancy_code_buy_intra) : '');
1508 $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
1509 $this->accountancy_code_sell = trim($this->accountancy_code_sell);
1510 $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
1511 $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
1512
1513 // Normalize the accountancy codes the way the admin dropdown does it, so an API client that
1514 // sends '606111000' ends up with the same '606111' value the GUI stores (see issue #32343).
1515 // When ACCOUNTING_MANAGE_ZERO is on, trailing zeros are part of the code and must be kept.
1516 if (!getDolGlobalString('ACCOUNTING_MANAGE_ZERO')) {
1517 require_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
1518 $this->accountancy_code_buy = clean_account($this->accountancy_code_buy);
1519 $this->accountancy_code_buy_intra = clean_account($this->accountancy_code_buy_intra);
1520 $this->accountancy_code_buy_export = clean_account($this->accountancy_code_buy_export);
1521 $this->accountancy_code_sell = clean_account($this->accountancy_code_sell);
1522 $this->accountancy_code_sell_intra = clean_account($this->accountancy_code_sell_intra);
1523 $this->accountancy_code_sell_export = clean_account($this->accountancy_code_sell_export);
1524 }
1525
1526 $this->db->begin();
1527
1528 $result = 0;
1529 // Check name is required and codes are ok or unique. If error, this->errors[] is filled
1530 if ($action != 'add') {
1531 $result = $this->verify(); // We don't check when update called during a create because verify was already done
1532 } else {
1533 // we can continue
1534 $result = 0;
1535 }
1536
1537 if ($result >= 0) {
1538 // $this->oldcopy should have been set by the caller of update (here properties were already modified)
1539 // Note that this->oldcopy must be a Product instance (not stdClass), otherwise the method
1540 // hasbatch() called below will fatal. Callers may set oldcopy via dol_clone($obj, 2) (which
1541 // returns a stdClass with scalar properties only) and then we cannot call methods on it,
1542 // so re-clone with native=1 in that case (see issues #38663, #38638).
1543 if (is_null($this->oldcopy) || (is_object($this->oldcopy) && empty($this->oldcopy->id)) || !($this->oldcopy instanceof Product)) {
1544 $this->oldcopy = dol_clone($this, 1); // 1 to clone with methods to avoid fatal error with $this->oldcopy->hasbatch()
1545 }
1546 // Test if batch management is activated on existing product
1547 // If yes, we create missing entries into product_batch
1548 if ($this->hasbatch() && !$this->oldcopy->hasbatch()) {
1549 //$valueforundefinedlot = 'Undefined'; // In previous version, 39 and lower
1550 $valueforundefinedlot = '000000';
1551 if (getDolGlobalString('STOCK_DEFAULT_BATCH')) {
1552 $valueforundefinedlot = getDolGlobalString('STOCK_DEFAULT_BATCH');
1553 }
1554
1555 dol_syslog("Flag batch of product id=".$this->id." is set to ON, so we will create missing records into product_batch");
1556
1557 $this->load_stock();
1558 foreach ($this->stock_warehouse as $idW => $ObjW) { // For each warehouse where we have stocks defined for this product (for each lines in product_stock)
1559 $qty_batch = 0;
1560 foreach ($ObjW->detail_batch as $detail) { // Each lines of detail in product_batch of the current $ObjW = product_stock
1561 if ($detail->batch == $valueforundefinedlot || $detail->batch == 'Undefined') {
1562 // We discard this line, we will create it later
1563 $sqlclean = "DELETE FROM ".$this->db->prefix()."product_batch WHERE batch in('Undefined', '".$this->db->escape($valueforundefinedlot)."') AND fk_product_stock = ".((int) $ObjW->id);
1564 $result = $this->db->query($sqlclean);
1565 if (!$result) {
1566 dol_print_error($this->db);
1567 exit;
1568 }
1569 continue;
1570 }
1571
1572 $qty_batch += $detail->qty;
1573 }
1574 // Quantities in batch details are not same as stock quantity,
1575 // so we add a default batch record to complete and get same qty in parent and child table
1576 if ($ObjW->real != $qty_batch) {
1577 $ObjBatch = new Productbatch($this->db);
1578 $ObjBatch->batch = $valueforundefinedlot;
1579 $ObjBatch->qty = ($ObjW->real - $qty_batch);
1580 $ObjBatch->fk_product_stock = (int) $ObjW->id;
1581
1582 if ($ObjBatch->create($user, 1) < 0) {
1583 $error++;
1584 $this->errors = $ObjBatch->errors;
1585 } else {
1586 // we also add lot record if not exist
1587 $ObjLot = new Productlot($this->db);
1588 // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1589 if ($ObjLot->fetch(0, $this->id, $valueforundefinedlot) == 0) {
1590 $ObjLot->fk_product = $this->id;
1591 $ObjLot->entity = (int) $this->entity;
1592 $ObjLot->fk_user_creat = $user->id;
1593 $ObjLot->batch = $valueforundefinedlot;
1594 if ($ObjLot->create($user, 1) < 0) {
1595 $error++;
1596 $this->errors = $ObjLot->errors;
1597 }
1598 }
1599 }
1600 }
1601 }
1602 }
1603
1604 $sql = "UPDATE ".$this->db->prefix()."product";
1605 $sql .= " SET label = '".$this->db->escape($this->label)."'";
1606
1607 if ($updatetype && ($this->isProduct() || $this->isService())) {
1608 $sql .= ", fk_product_type = ".((int) $this->type);
1609 }
1610
1611 $sql .= ", ref = '".$this->db->escape($this->ref)."'";
1612 $sql .= ", ref_ext = ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
1613 $sql .= ", default_vat_code = ".($this->default_vat_code ? "'".$this->db->escape($this->default_vat_code)."'" : "null");
1614 $sql .= ", tva_tx = ".((float) $this->tva_tx);
1615 $sql .= ", recuperableonly = ".((int) $this->tva_npr);
1616 $sql .= ", localtax1_tx = ".((float) $this->localtax1_tx);
1617 $sql .= ", localtax2_tx = ".((float) $this->localtax2_tx);
1618 $sql .= ", localtax1_type = ".($this->localtax1_type != '' ? "'".$this->db->escape($this->localtax1_type)."'" : "'0'");
1619 $sql .= ", localtax2_type = ".($this->localtax2_type != '' ? "'".$this->db->escape($this->localtax2_type)."'" : "'0'");
1620
1621 $sql .= ", barcode = ".(empty($this->barcode) ? "null" : "'".$this->db->escape($this->barcode)."'");
1622 $sql .= ", fk_barcode_type = ".(empty($this->barcode_type) ? "null" : $this->db->escape((string) $this->barcode_type));
1623
1624 $sql .= ", tosell = ".(int) $this->status;
1625 $sql .= ", tobuy = ".(int) $this->status_buy;
1626 $sql .= ", tobatch = ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : (int) $this->status_batch);
1627 $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);
1628 $sql .= ", batch_mask = '".$this->db->escape($this->batch_mask)."'";
1629
1630 $sql .= ", finished = ".((!isset($this->finished) || $this->finished < 0 || $this->finished === '') ? "null" : (int) $this->finished);
1631 $sql .= ", fk_default_bom = ".((!isset($this->fk_default_bom) || $this->fk_default_bom < 0 || $this->fk_default_bom == '') ? "null" : (int) $this->fk_default_bom);
1632 $sql .= ", net_measure = ".($this->net_measure != '' ? "'".$this->db->escape($this->net_measure)."'" : 'null');
1633 $sql .= ", net_measure_units = ".((string) $this->net_measure_units != '' ? ((int) $this->net_measure_units) : 'null');
1634 $sql .= ", weight = ".($this->weight != '' ? "'".$this->db->escape($this->weight)."'" : 'null');
1635 $sql .= ", weight_units = ".((string) $this->weight_units != '' ? ((int) $this->weight_units) : 'null');
1636 $sql .= ", length = ".($this->length != '' ? "'".$this->db->escape($this->length)."'" : 'null');
1637 $sql .= ", length_units = ".((string) $this->length_units != '' ? ((int) $this->length_units) : 'null');
1638 $sql .= ", width= ".($this->width != '' ? "'".$this->db->escape($this->width)."'" : 'null');
1639 $sql .= ", width_units = ".((string) $this->width_units != '' ? ((int) $this->width_units) : 'null');
1640 $sql .= ", height = ".($this->height != '' ? "'".$this->db->escape($this->height)."'" : 'null');
1641 $sql .= ", height_units = ".((string) $this->height_units != '' ? ((int) $this->height_units) : 'null');
1642 $sql .= ", surface = ".($this->surface != '' ? "'".$this->db->escape($this->surface)."'" : 'null');
1643 $sql .= ", surface_units = ".((string) $this->surface_units != '' ? ((int) $this->surface_units) : 'null');
1644 $sql .= ", volume = ".($this->volume != '' ? "'".$this->db->escape($this->volume)."'" : 'null');
1645 $sql .= ", volume_units = ".((string) $this->volume_units != '' ? ((int) $this->volume_units) : 'null');
1646 $sql .= ", fk_default_warehouse = ".($this->fk_default_warehouse > 0 ? ((int) $this->fk_default_warehouse) : 'null');
1647 $sql .= ", fk_default_workstation = ".($this->fk_default_workstation > 0 ? ((int) $this->fk_default_workstation) : 'null');
1648 $sql .= ", seuil_stock_alerte = ".((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (float) $this->seuil_stock_alerte : 'null');
1649 $sql .= ", description = '".$this->db->escape($this->description)."'";
1650 $sql .= ", url = ".($this->url ? "'".$this->db->escape($this->url)."'" : 'null');
1651 $sql .= ", customcode = '".$this->db->escape($this->customcode)."'";
1652 $sql .= ", fk_country = ".($this->country_id > 0 ? (int) $this->country_id : 'null');
1653 $sql .= ", fk_state = ".($this->state_id > 0 ? (int) $this->state_id : 'null');
1654 $sql .= ", lifetime = ".($this->lifetime > 0 ? (int) $this->lifetime : 'null');
1655 $sql .= ", qc_frequency = ".($this->qc_frequency > 0 ? (int) $this->qc_frequency : 'null');
1656 $sql .= ", note = ".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : 'null');
1657 $sql .= ", note_public = ".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : 'null');
1658 $sql .= ", duration = '".$this->db->escape($this->duration_value.$this->duration_unit)."'";
1659 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1660 $sql .= ", accountancy_code_buy = '" . $this->db->escape($this->accountancy_code_buy) . "'";
1661 $sql .= ", accountancy_code_buy_intra = '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1662 $sql .= ", accountancy_code_buy_export = '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1663 $sql .= ", accountancy_code_sell= '" . $this->db->escape($this->accountancy_code_sell) . "'";
1664 $sql .= ", accountancy_code_sell_intra= '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1665 $sql .= ", accountancy_code_sell_export= '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1666 }
1667 $sql .= ", desiredstock = ".((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (float) $this->desiredstock : "null");
1668 $sql .= ", cost_price = ".($this->cost_price != '' ? ((float) $this->cost_price) : 'null');
1669 $sql .= ", fk_unit= ".(!$this->fk_unit ? 'NULL' : (int) $this->fk_unit);
1670 $sql .= ", price_autogen = ".(!$this->price_autogen ? 0 : 1);
1671 $sql .= ", fk_price_expression = ".($this->fk_price_expression != 0 ? (int) $this->fk_price_expression : 'NULL');
1672 $sql .= ", fk_user_modif = ".($user->id > 0 ? (int) $user->id : 'NULL');
1673 $sql .= ", mandatory_period = ".((int) $this->mandatory_period);
1674 $sql .= ", stockable_product = ".(int) $this->stockable_product;
1675 if (getDolGlobalString('PRODUCT_USE_CUSTOMER_PACKAGING')) {
1676 $sql .= ", packaging = ".(float) $this->packaging;
1677 }
1678
1679 // stock field is not here because it is a denormalized value from product_stock.
1680 $sql .= " WHERE rowid = ".((int) $id);
1681
1682 dol_syslog(get_class($this)."::update", LOG_DEBUG);
1683
1684 $resql = $this->db->query($sql);
1685 if ($resql) {
1686 $this->id = $id;
1687
1688 // Multilangs
1689 if (getDolGlobalInt('MAIN_MULTILANGS')) {
1690 if ($this->setMultiLangs($user, $notrigger) < 0) {
1691 $this->db->rollback();
1692 return -2;
1693 }
1694 }
1695
1696 $action = 'update';
1697
1698 // update accountancy for this entity
1699 if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1700 $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1701
1702 $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1703 $sql .= " fk_product";
1704 $sql .= ", entity";
1705 $sql .= ", accountancy_code_buy";
1706 $sql .= ", accountancy_code_buy_intra";
1707 $sql .= ", accountancy_code_buy_export";
1708 $sql .= ", accountancy_code_sell";
1709 $sql .= ", accountancy_code_sell_intra";
1710 $sql .= ", accountancy_code_sell_export";
1711 $sql .= ") VALUES (";
1712 $sql .= ((int) $this->id);
1713 $sql .= ", " . ((int) $conf->entity);
1714 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1715 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1716 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1717 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1718 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1719 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1720 $sql .= ")";
1721 $result = $this->db->query($sql);
1722 if (!$result) {
1723 $error++;
1724 $this->error = 'ErrorFailedToUpdateAccountancyForEntity';
1725 }
1726 }
1727
1728 if (!$this->hasbatch() && $this->oldcopy->hasbatch()) {
1729 // Selection of all product stock movements that contains batchs
1730 $sql = 'SELECT pb.qty, ps.fk_entrepot, pb.batch FROM '.MAIN_DB_PREFIX.'product_batch as pb';
1731 $sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'product_stock as ps ON (ps.rowid = pb.fk_product_stock)';
1732 $sql .= ' WHERE ps.fk_product = '.(int) $this->id;
1733
1734 $resql = $this->db->query($sql);
1735 if ($resql) {
1736 $inventorycode = dol_print_date(dol_now(), '%Y%m%d%H%M%S');
1737
1738 while ($obj = $this->db->fetch_object($resql)) {
1739 $value = $obj->qty;
1740 $fk_entrepot = $obj->fk_entrepot;
1741 $price = 0;
1742 $dlc = '';
1743 $dluo = '';
1744 $batch = $obj->batch;
1745
1746 // To know how to revert stockMouvement (add or remove)
1747 $addOremove = $value > 0 ? 1 : 0; // 1 if remove, 0 if add
1748 $label = $langs->trans('BatchStockMouvementAddInGlobal');
1749 $res = $this->correct_stock_batch($user, $fk_entrepot, abs($value), $addOremove, $label, $price, $dlc, $dluo, $batch, $inventorycode, '', null, 0, null, true);
1750
1751 if ($res > 0) {
1752 $label = $langs->trans('BatchStockMouvementAddInGlobal');
1753 $res = $this->correct_stock($user, $fk_entrepot, abs($value), (int) empty($addOremove), $label, $price, $inventorycode, '', null, 0);
1754 if ($res < 0) {
1755 $error++;
1756 }
1757 } else {
1758 $error++;
1759 }
1760 }
1761 }
1762 }
1763
1764 // Actions on extra fields
1765 if (!$error) {
1766 $result = $this->insertExtraFields();
1767 if ($result < 0) {
1768 $error++;
1769 }
1770 }
1771
1772 if (!$error && !$notrigger) {
1773 // Call trigger
1774 $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1775 if ($result < 0) {
1776 $error++;
1777 }
1778 // End call triggers
1779 }
1780
1781 if (!$error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref)) {
1782 // We remove directory
1783 if ($conf->product->dir_output) {
1784 $olddir = $conf->product->dir_output."/".dol_sanitizeFileName($this->oldcopy->ref);
1785 $newdir = $conf->product->dir_output."/".dol_sanitizeFileName($this->ref);
1786 if (file_exists($olddir)) {
1787 // include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1788 // $res = dol_move($olddir, $newdir);
1789 // do not use dol_move with directory
1790 $res = @rename($olddir, $newdir);
1791 if (!$res) {
1792 $langs->load("errors");
1793 $this->error = $langs->trans('ErrorFailToRenameDir', $olddir, $newdir);
1794 $error++;
1795 } else {
1796 // to keep old entries with the new dir
1797 require_once DOL_DOCUMENT_ROOT . '/ecm/class/ecmfiles.class.php';
1798 $ecmfiles = new EcmFiles($this->db);
1799 $ecmfiles->updateAfterRename("produit/".dol_sanitizeFileName($this->oldcopy->ref), "produit/".dol_sanitizeFileName($this->ref));
1800 }
1801 }
1802 }
1803 }
1804
1805 if (!$error) {
1806 if (isModEnabled('variants')) {
1807 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1808
1809 $comb = new ProductCombination($this->db);
1810
1811 foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) {
1812 $currcomb->updateProperties($this, $user);
1813 }
1814 }
1815
1816 $this->db->commit();
1817 return 1;
1818 } else {
1819 $this->db->rollback();
1820 return -$error;
1821 }
1822 } else {
1823 if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1824 $langs->load("errors");
1825 if (empty($conf->barcode->enabled) || empty($this->barcode)) {
1826 $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductAlreadyExists", $this->ref);
1827 } else {
1828 $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductBarCodeAlreadyExists", $this->barcode);
1829 }
1830 $this->errors[] = $this->error;
1831 $this->db->rollback();
1832 return -1;
1833 } else {
1834 $this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1835 $this->errors[] = $this->error;
1836 $this->db->rollback();
1837 return -2;
1838 }
1839 }
1840 } else {
1841 $this->db->rollback();
1842 dol_syslog(get_class($this)."::Update fails verify ".implode(',', $this->errors), LOG_WARNING);
1843 return -3;
1844 }
1845 }
1846
1854 public function delete(User $user, $notrigger = 0)
1855 {
1856 global $conf;
1857 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1858
1859 $error = 0;
1860
1861 // Check parameters
1862 if (empty($this->id)) {
1863 $this->error = "Object must be fetched before calling delete";
1864 return -1;
1865 }
1866 if (($this->isProduct() && !$user->hasRight('produit', 'supprimer')) || ($this->isService() && !$user->hasRight('service', 'supprimer'))) {
1867 $this->error = "ErrorForbidden";
1868 return 0;
1869 }
1870
1871 $objectisused = $this->isObjectUsed($this->id);
1872 if (empty($objectisused)) {
1873 $this->db->begin();
1874
1875 if (empty($notrigger)) {
1876 // Call trigger
1877 $result = $this->call_trigger('PRODUCT_DELETE', $user);
1878 if ($result < 0) {
1879 $error++;
1880 }
1881 // End call triggers
1882 }
1883
1884 // Delete from product_batch on product delete
1885 if (!$error) {
1886 $sql = "DELETE FROM ".$this->db->prefix().'product_batch';
1887 $sql .= " WHERE fk_product_stock IN (";
1888 $sql .= "SELECT rowid FROM ".$this->db->prefix().'product_stock';
1889 $sql .= " WHERE fk_product = ".((int) $this->id).")";
1890
1891 $result = $this->db->query($sql);
1892 if (!$result) {
1893 $error++;
1894 $this->errors[] = $this->db->lasterror();
1895 }
1896 }
1897
1898 // Delete pricelogs so we don't have any orphan rows, see #38383
1899 if (!$error) {
1900 $sql = "DELETE FROM " . $this->db->prefix() . "product_fournisseur_price_log";
1901 $sql .= " WHERE fk_product_fournisseur IN (";
1902 $sql .= " SELECT rowid FROM " . $this->db->prefix() . "product_fournisseur_price";
1903 $sql .= " WHERE fk_product = " . ((int) $this->id);
1904 $sql .= " )";
1905
1906 $resql = $this->db->query($sql);
1907 if (!$resql) {
1908 $error++;
1909 $this->errors[] = $this->db->lasterror();
1910 }
1911 }
1912
1913 // Delete all child tables
1914 if (!$error) {
1915 $elements = array('product_fournisseur_price', 'product_price', 'product_lang', 'categorie_product', 'product_stock', 'product_customer_price', 'product_lot'); // product_batch is done before
1916 foreach ($elements as $table) {
1917 if (!$error) {
1918 $sql = "DELETE FROM ".$this->db->prefix().$table;
1919 $sql .= " WHERE fk_product = ".(int) $this->id;
1920
1921 $result = $this->db->query($sql);
1922 if (!$result) {
1923 $error++;
1924 $this->errors[] = $this->db->lasterror();
1925 }
1926 }
1927 }
1928 }
1929
1930 if (!$error && isModEnabled('variants')) {
1931 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1932 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
1933
1934 // If it is a parent product, then we remove the association with child products
1935 $prodcomb = new ProductCombination($this->db);
1936
1937 if ($prodcomb->deleteByFkProductParent($user, $this->id) < 0) {
1938 $error++;
1939 $this->errors[] = 'Error deleting combinations';
1940 }
1941
1942 // We also check if it is a child product
1943 if (!$error && ($prodcomb->fetchByFkProductChild($this->id) > 0) && ($prodcomb->delete($user) < 0)) {
1944 $error++;
1945 $this->errors[] = 'Error deleting child combination';
1946 }
1947 }
1948
1949 // Delete from product_association
1950 if (!$error) {
1951 $sql = "DELETE FROM ".$this->db->prefix()."product_association";
1952 $sql .= " WHERE fk_product_pere = ".(int) $this->id." OR fk_product_fils = ".(int) $this->id;
1953
1954 $result = $this->db->query($sql);
1955 if (!$result) {
1956 $error++;
1957 $this->errors[] = $this->db->lasterror();
1958 }
1959 }
1960
1961 // Remove extrafields
1962 if (!$error) {
1963 $result = $this->deleteExtraFields();
1964 if ($result < 0) {
1965 $error++;
1966 dol_syslog(get_class($this)."::delete error -4 ".$this->error, LOG_ERR);
1967 }
1968 }
1969
1970 // Delete product
1971 if (!$error) {
1972 $sqlz = "DELETE FROM ".$this->db->prefix()."product";
1973 $sqlz .= " WHERE rowid = ".(int) $this->id;
1974
1975 $resultz = $this->db->query($sqlz);
1976 if (!$resultz) {
1977 $error++;
1978 $this->errors[] = $this->db->lasterror();
1979 }
1980 }
1981
1982 // Delete record into ECM index and physically
1983 if (!$error) {
1984 $res = $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
1985 $res = $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
1986 if (!$res) {
1987 $error++;
1988 }
1989 }
1990
1991 if (!$error) {
1992 // We remove directory
1993 $ref = dol_sanitizeFileName($this->ref);
1994 if ($conf->product->dir_output) {
1995 $dir = $conf->product->dir_output."/".$ref;
1996 if (file_exists($dir)) {
1997 $res = @dol_delete_dir_recursive($dir);
1998 if (!$res) {
1999 $this->errors[] = 'ErrorFailToDeleteDir';
2000 $error++;
2001 }
2002 }
2003 }
2004 }
2005
2006 if (!$error) {
2007 $this->db->commit();
2008 return 1;
2009 } else {
2010 foreach ($this->errors as $errmsg) {
2011 dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
2012 $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
2013 }
2014 $this->db->rollback();
2015 return -$error;
2016 }
2017 } else {
2018 $this->error = "ErrorRecordIsUsedCantDelete";
2019 return 0;
2020 }
2021 }
2022
2028 public static function getSellOrEatByMandatoryList()
2029 {
2030 global $langs;
2031
2032 $sellByLabel = $langs->trans('SellByDate');
2033 $eatByLabel = $langs->trans('EatByDate');
2034 return array(
2035 self::SELL_OR_EAT_BY_MANDATORY_ID_NONE => $langs->trans('BatchSellOrEatByMandatoryNone'),
2036 self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY => $sellByLabel,
2037 self::SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY => $eatByLabel,
2038 self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT => $langs->trans('BatchSellOrEatByMandatoryAll', $sellByLabel, $eatByLabel),
2039 );
2040 }
2041
2048 {
2049 $sellOrEatByMandatoryLabel = '';
2050
2051 $sellOrEatByMandatoryList = self::getSellOrEatByMandatoryList();
2052 if (isset($sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory])) {
2053 $sellOrEatByMandatoryLabel = $sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory];
2054 }
2055
2056 return $sellOrEatByMandatoryLabel;
2057 }
2058
2066 public function setMultiLangs($user, $notrigger = 0)
2067 {
2068 global $langs;
2069
2070 $langs_available = $langs->get_available_languages(DOL_DOCUMENT_ROOT, 0, 2);
2071 $current_lang = $langs->getDefaultLang();
2072
2073 foreach ($langs_available as $key => $value) {
2074 if ($key == $current_lang) {
2075 $sql = "SELECT rowid";
2076 $sql .= " FROM ".$this->db->prefix()."product_lang";
2077 $sql .= " WHERE fk_product = ".((int) $this->id);
2078 $sql .= " AND lang = '".$this->db->escape($key)."'";
2079
2080 $result = $this->db->query($sql);
2081
2082 if ($this->db->num_rows($result)) { // if there is already a description line for this language
2083 $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
2084 $sql2 .= " SET ";
2085 $sql2 .= " label='".$this->db->escape($this->label)."',";
2086 $sql2 .= " description='".$this->db->escape($this->description)."'";
2087 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2088 $sql2 .= ", note='".$this->db->escape($this->other)."'";
2089 }
2090 $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
2091 } else {
2092 $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
2093 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2094 $sql2 .= ", note";
2095 }
2096 $sql2 .= ")";
2097 $sql2 .= " VALUES(".((int) $this->id).",'".$this->db->escape($key)."','".$this->db->escape($this->label)."',";
2098 $sql2 .= " '".$this->db->escape($this->description)."'";
2099 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2100 $sql2 .= ", '".$this->db->escape($this->other)."'";
2101 }
2102 $sql2 .= ")";
2103 }
2104 dol_syslog(get_class($this).'::setMultiLangs key = current_lang = '.$key);
2105 if (!$this->db->query($sql2)) {
2106 $this->error = $this->db->lasterror();
2107 return -1;
2108 }
2109 } elseif (isset($this->multilangs[$key])) {
2110 if (empty($this->multilangs[$key]["label"])) {
2111 $this->errors[] = $key . ' : ' . $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Label"));
2112 return -1;
2113 }
2114
2115 $sql = "SELECT rowid";
2116 $sql .= " FROM ".$this->db->prefix()."product_lang";
2117 $sql .= " WHERE fk_product = ".((int) $this->id);
2118 $sql .= " AND lang = '".$this->db->escape($key)."'";
2119
2120 $result = $this->db->query($sql);
2121
2122 if ($this->db->num_rows($result)) { // if there is already a description line for this language
2123 $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
2124 $sql2 .= " SET ";
2125 $sql2 .= " label = '".$this->db->escape($this->multilangs["$key"]["label"])."',";
2126 $sql2 .= " description = '".$this->db->escape($this->multilangs["$key"]["description"])."'";
2127 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2128 // @phan-suppress-next-line PhanTypeInvalidDimOffset
2129 $sql2 .= ", note = '".$this->db->escape($this->multilangs["$key"]["other"])."'";
2130 }
2131 $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
2132 } else {
2133 $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
2134 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2135 $sql2 .= ", note";
2136 }
2137 $sql2 .= ")";
2138 $sql2 .= " VALUES(".((int) $this->id).",'".$this->db->escape($key)."','".$this->db->escape($this->multilangs["$key"]["label"])."',";
2139 $sql2 .= " '".$this->db->escape($this->multilangs["$key"]["description"])."'";
2140 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2141 // @phan-suppress-next-line PhanTypeInvalidDimOffset
2142 $sql2 .= ", '".$this->db->escape($this->multilangs["$key"]["other"])."'";
2143 }
2144 $sql2 .= ")";
2145 }
2146
2147 // We do not save if main fields are empty
2148 if ($this->multilangs["$key"]["label"] || $this->multilangs["$key"]["description"]) {
2149 if (!$this->db->query($sql2)) {
2150 $this->error = $this->db->lasterror();
2151 return -1;
2152 }
2153 }
2154 } else {
2155 // language is not current language and we didn't provide a multilang description for this language
2156 }
2157 }
2158
2159 if (empty($notrigger)) {
2160 // Call trigger
2161 $result = $this->call_trigger('PRODUCT_SET_MULTILANGS', $user);
2162 if ($result < 0) {
2163 $this->error = $this->db->lasterror();
2164 return -1;
2165 }
2166 // End call triggers
2167 }
2168
2169 return 1;
2170 }
2171
2181 public function delMultiLangs($langtodelete, $user, $notrigger = 0)
2182 {
2183 $sql = "DELETE FROM ".$this->db->prefix()."product_lang";
2184 $sql .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($langtodelete)."'";
2185
2186 dol_syslog(get_class($this).'::delMultiLangs', LOG_DEBUG);
2187 $result = $this->db->query($sql);
2188 if ($result) {
2189 if (empty($notrigger)) {
2190 // Call trigger
2191 $result = $this->call_trigger('PRODUCT_DEL_MULTILANGS', $user);
2192 if ($result < 0) {
2193 $this->error = $this->db->lasterror();
2194 dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
2195 return -1;
2196 }
2197 // End call triggers
2198 }
2199 unset($this->multilangs[$langtodelete]);
2200 return 1;
2201 } else {
2202 $this->error = $this->db->lasterror();
2203 dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
2204 return -1;
2205 }
2206 }
2207
2216 public function setAccountancyCode($type, $value)
2217 {
2218 global $user;
2219
2220 $error = 0;
2221
2222 $this->db->begin();
2223
2224 if ($type == 'buy') {
2225 $field = 'accountancy_code_buy';
2226 } elseif ($type == 'buy_intra') {
2227 $field = 'accountancy_code_buy_intra';
2228 } elseif ($type == 'buy_export') {
2229 $field = 'accountancy_code_buy_export';
2230 } elseif ($type == 'sell') {
2231 $field = 'accountancy_code_sell';
2232 } elseif ($type == 'sell_intra') {
2233 $field = 'accountancy_code_sell_intra';
2234 } elseif ($type == 'sell_export') {
2235 $field = 'accountancy_code_sell_export';
2236 } else {
2237 return -1;
2238 }
2239
2240 $sql = "UPDATE ".$this->db->prefix().$this->table_element." SET ";
2241 $sql .= "$field = '".$this->db->escape($value)."'";
2242 $sql .= " WHERE rowid = ".((int) $this->id);
2243
2244 dol_syslog(__METHOD__, LOG_DEBUG);
2245 $resql = $this->db->query($sql);
2246
2247 if ($resql) {
2248 // Call trigger
2249 $result = $this->call_trigger('PRODUCT_MODIFY', $user);
2250 if ($result < 0) {
2251 $error++;
2252 }
2253 // End call triggers
2254
2255 if ($error) {
2256 $this->db->rollback();
2257 return -1;
2258 }
2259
2260 $this->$field = $value;
2261
2262 $this->db->commit();
2263 return 1;
2264 } else {
2265 $this->error = $this->db->lasterror();
2266 $this->db->rollback();
2267 return -1;
2268 }
2269 }
2270
2276 public function getMultiLangs()
2277 {
2278 global $langs;
2279
2280 $current_lang = $langs->getDefaultLang();
2281
2282 $sql = "SELECT lang, label, description, note as other";
2283 $sql .= " FROM ".$this->db->prefix()."product_lang";
2284 $sql .= " WHERE fk_product = ".((int) $this->id);
2285
2286 $result = $this->db->query($sql);
2287 if ($result) {
2288 while ($obj = $this->db->fetch_object($result)) {
2289 //print 'lang='.$obj->lang.' current='.$current_lang.'<br>';
2290 if ($obj->lang == $current_lang) { // si on a les traduct. dans la langue courante on les charge en infos principales.
2291 $this->label = $obj->label;
2292 $this->description = $obj->description;
2293 $this->other = $obj->other;
2294 }
2295 $this->multilangs[(string) $obj->lang]["label"] = $obj->label;
2296 $this->multilangs[(string) $obj->lang]["description"] = $obj->description;
2297 $this->multilangs[(string) $obj->lang]["other"] = $obj->other;
2298 }
2299 return 1;
2300 } else {
2301 $this->error = "Error: ".$this->db->lasterror()." - ".$sql;
2302 return -1;
2303 }
2304 }
2305
2312 private function getArrayForPriceCompare($level = 0)
2313 {
2314 $testExit = array('multiprices','multiprices_ttc','multiprices_base_type','multiprices_min','multiprices_min_ttc','multiprices_tva_tx','multiprices_recuperableonly');
2315
2316 foreach ($testExit as $field) {
2317 if (!isset($this->$field)) {
2318 return array();
2319 }
2320 $tmparray = $this->$field;
2321 if (!isset($tmparray[$level])) {
2322 return array();
2323 }
2324 }
2325
2326 $lastPrice = array(
2327 'level' => $level ? $level : 1,
2328 'multiprices' => (float) $this->multiprices[$level],
2329 'multiprices_ttc' => (float) $this->multiprices_ttc[$level],
2330 'multiprices_base_type' => $this->multiprices_base_type[$level],
2331 'multiprices_min' => (float) $this->multiprices_min[$level],
2332 'multiprices_min_ttc' => (float) $this->multiprices_min_ttc[$level],
2333 'multiprices_tva_tx' => (float) $this->multiprices_tva_tx[$level],
2334 'multiprices_recuperableonly' => (float) $this->multiprices_recuperableonly[$level],
2335 );
2336
2337 return $lastPrice;
2338 }
2339
2340
2341 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2349 private function _log_price($user, $level = 0)
2350 {
2351 // phpcs:enable
2352 global $conf;
2353
2354 $now = dol_now();
2355
2356 // Clean parameters
2357 if (empty($this->price_by_qty)) {
2358 $this->price_by_qty = 0;
2359 }
2360
2361 // Add new price
2362 $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,";
2363 $sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, price_min,price_min_ttc,price_by_qty,entity,fk_price_expression) ";
2364 $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)."'").",";
2365 $sql .= " ".((float) price2num($this->price)).", ".((float) price2num($this->price_ttc)).",'".$this->db->escape($this->price_base_type)."',".((int) $this->status).", ".((float) price2num($this->tva_tx)).",";
2366 $sql .= " ".($this->default_vat_code ? ("'".$this->db->escape($this->default_vat_code)."'") : "null").", ".((int) $this->tva_npr).",";
2367 $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');
2368 $sql .= ")";
2369
2370 dol_syslog(get_class($this)."::_log_price", LOG_DEBUG);
2371 $resql = $this->db->query($sql);
2372 if (!$resql) {
2373 $this->error = $this->db->lasterror();
2374 dol_print_error($this->db);
2375 return -1;
2376 } else {
2377 return 1;
2378 }
2379 }
2380
2381
2382 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2390 public function log_price_delete($user, $rowid)
2391 {
2392 // phpcs:enable
2393 $sql = "DELETE FROM ".$this->db->prefix()."product_price_by_qty";
2394 $sql .= " WHERE fk_product_price = ".((int) $rowid);
2395 $resql = $this->db->query($sql);
2396
2397 $sql = "DELETE FROM ".$this->db->prefix()."product_price_extrafields";
2398 $sql .= " WHERE fk_object = ".((int) $rowid);
2399 $resql = $this->db->query($sql);
2400
2401 $sql = "DELETE FROM ".$this->db->prefix()."product_price";
2402 $sql .= " WHERE rowid = ".((int) $rowid);
2403 $resql = $this->db->query($sql);
2404 if ($resql) {
2405 return 1;
2406 } else {
2407 $this->error = $this->db->lasterror();
2408 return -1;
2409 }
2410 }
2411
2420 public function fetchAllPriceLogs($id)
2421 {
2422 dol_syslog(__METHOD__, LOG_DEBUG);
2423
2424 $logs = array();
2425
2426 $sql = "SELECT";
2427 $sql .= " pl.rowid as log_rowid,";
2428 $sql .= " pl.datec,";
2429 $sql .= " pl.price,";
2430 $sql .= " pl.quantity,";
2431 $sql .= " pl.fk_user,";
2432 $sql .= " pl.multicurrency_code,";
2433 $sql .= " pl.multicurrency_price,";
2434 // Supplier info
2435 $sql .= " pp.fk_soc as supplier_id,";
2436 $sql .= " pp.ref_fourn as supplier_ref,";
2437 $sql .= " pp.entity as entity";
2438
2439 $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price_log as pl";
2440 $sql .= " INNER JOIN " . $this->db->prefix() . "product_fournisseur_price as pp ON pl.fk_product_fournisseur = pp.rowid";
2441
2442 $sql .= " WHERE pp.fk_product = " . ((int) $id);
2443 $sql .= " AND pp.entity IN (" . getEntity('product') . ")";
2444 $sql .= " ORDER BY pl.datec DESC";
2445
2446 $resql = $this->db->query($sql);
2447 if ($resql) {
2448 while ($obj = $this->db->fetch_object($resql)) {
2449 if (is_string($obj->datec)) {
2450 $obj->datec = strtotime($obj->datec);
2451 }
2452 $logs[] = $obj;
2453 }
2454 $this->db->free($resql);
2455 } else {
2456 $this->error = $this->db->lasterror();
2457 }
2458
2459 return $logs;
2460 }
2461
2471 public function getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp = 0)
2472 {
2473 global $hookmanager, $action;
2474
2475 // Call hook if any
2476 if (is_object($hookmanager)) {
2477 $parameters = array('thirdparty_seller' => $thirdparty_seller, 'thirdparty_buyer' => $thirdparty_buyer, 'pqp' => $pqp);
2478 // Note that $action and $object may have been modified by some hooks
2479 $reshook = $hookmanager->executeHooks('getSellPrice', $parameters, $this, $action);
2480 if ($reshook > 0) {
2481 return $hookmanager->resArray;
2482 }
2483 }
2484
2485 // Update if prices fields are defined
2486 $tva_tx = get_default_tva($thirdparty_seller, $thirdparty_buyer, $this->id);
2487 $tva_npr = get_default_npr($thirdparty_seller, $thirdparty_buyer, $this->id);
2488 if (empty($tva_tx)) {
2489 $tva_npr = 0;
2490 }
2491
2492 $pu_ht = $this->price;
2493 $pu_ttc = $this->price_ttc;
2494 $price_min = $this->price_min;
2495 $price_min_ttc = $this->price_min_ttc;
2496 $price_base_type = (empty($this->price_base_type) ? 'HT' : $this->price_base_type);
2497
2498 // if price by customer / level
2499 if (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) {
2500 require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
2501
2502 $prodcustprice = new ProductCustomerPrice($this->db);
2503
2504 $filter = array('t.fk_product' => (string) $this->id, 't.fk_soc' => (string) $thirdparty_buyer->id);
2505
2506 // If a price per customer exist
2507 $pricebycustomerexist = false;
2508 $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
2509 if ($result) {
2510 if (count($prodcustprice->lines) > 0) {
2511 $date_now = (int) floor(dol_now() / 86400) * 86400; // date without hours
2512 foreach ($prodcustprice->lines as $k => $custprice_line) {
2513 if ($custprice_line->date_begin <= $date_now && (empty($custprice_line->date_end) || $date_now <= $custprice_line->date_end)) {
2514 $pricebycustomerexist = true;
2515 $pu_ht = price($custprice_line->price);
2516 $price_min = price($custprice_line->price_min);
2517 $price_min_ttc = price($custprice_line->price_min_ttc);
2518 $pu_ttc = price($custprice_line->price_ttc);
2519 $price_base_type = $custprice_line->price_base_type;
2520 $tva_tx = $custprice_line->tva_tx;
2521 if ($custprice_line->default_vat_code && !preg_match('/\‍(.*\‍)/', $tva_tx)) {
2522 $tva_tx .= ' (' . $custprice_line->default_vat_code . ')';
2523 }
2524 $tva_npr = $custprice_line->recuperableonly;
2525 if (empty($tva_tx)) {
2526 $tva_npr = 0;
2527 }
2528 break;
2529 }
2530 }
2531 }
2532 }
2533
2534 if (!$pricebycustomerexist && !empty($thirdparty_buyer->price_level)) {
2535 $pu_ht = isset($this->multiprices[$thirdparty_buyer->price_level]) ? $this->multiprices[$thirdparty_buyer->price_level] : 0;
2536 $pu_ttc = isset($this->multiprices_ttc[$thirdparty_buyer->price_level]) ? $this->multiprices_ttc[$thirdparty_buyer->price_level] : 0;
2537 $price_min = isset($this->multiprices_min[$thirdparty_buyer->price_level]) ? $this->multiprices_min[$thirdparty_buyer->price_level] : 0;
2538 $price_min_ttc = isset($this->multiprices_min_ttc[$thirdparty_buyer->price_level]) ? $this->multiprices_min_ttc[$thirdparty_buyer->price_level] : 0;
2539 $price_base_type = isset($this->multiprices_base_type[$thirdparty_buyer->price_level]) ? $this->multiprices_base_type[$thirdparty_buyer->price_level] : 'HT';
2540 if (getDolGlobalString('PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL')) {
2541 // using this option is a bug. kept for backward compatibility
2542 if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
2543 $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
2544 }
2545 if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
2546 $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
2547 }
2548 if (empty($tva_tx)) {
2549 $tva_npr = 0;
2550 }
2551 }
2552 }
2553 } elseif (getDolGlobalString('PRODUIT_MULTIPRICES') && !empty($thirdparty_buyer->price_level)) { // // If price per segment
2554 $pu_ht = isset($this->multiprices[$thirdparty_buyer->price_level]) ? $this->multiprices[$thirdparty_buyer->price_level] : 0;
2555 $pu_ttc = isset($this->multiprices_ttc[$thirdparty_buyer->price_level]) ? $this->multiprices_ttc[$thirdparty_buyer->price_level] : 0;
2556 $price_min = isset($this->multiprices_min[$thirdparty_buyer->price_level]) ? $this->multiprices_min[$thirdparty_buyer->price_level] : 0;
2557 $price_min_ttc = isset($this->multiprices_min_ttc[$thirdparty_buyer->price_level]) ? $this->multiprices_min_ttc[$thirdparty_buyer->price_level] : 0;
2558 $price_base_type = isset($this->multiprices_base_type[$thirdparty_buyer->price_level]) ? $this->multiprices_base_type[$thirdparty_buyer->price_level] : 'HT';
2559 if (getDolGlobalString('PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL')) {
2560 // using this option is a bug. kept for backward compatibility
2561 if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
2562 $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
2563 }
2564 if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
2565 $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
2566 }
2567 if (empty($tva_tx)) {
2568 $tva_npr = 0;
2569 }
2570 }
2571 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES')) {
2572 // If price per customer
2573 require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
2574
2575 $prodcustprice = new ProductCustomerPrice($this->db);
2576
2577 $filter = array('t.fk_product' => (string) $this->id, 't.fk_soc' => (string) $thirdparty_buyer->id);
2578
2579 $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
2580 if ($result) {
2581 if (count($prodcustprice->lines) > 0) {
2582 $date_now = (int) floor(dol_now() / 86400) * 86400; // date without hours
2583 foreach ($prodcustprice->lines as $k => $custprice_line) {
2584 if ($custprice_line->date_begin <= $date_now && (empty($custprice_line->date_end) || $date_now <= $custprice_line->date_end)) {
2585 $pu_ht = price($custprice_line->price);
2586 $price_min = price($custprice_line->price_min);
2587 $price_min_ttc = price($custprice_line->price_min_ttc);
2588 $pu_ttc = price($custprice_line->price_ttc);
2589 $price_base_type = $custprice_line->price_base_type;
2590 $tva_tx = $custprice_line->tva_tx;
2591 if ($custprice_line->default_vat_code && !preg_match('/\‍(.*\‍)/', $tva_tx)) {
2592 $tva_tx .= ' (' . $custprice_line->default_vat_code . ')';
2593 }
2594 $tva_npr = $custprice_line->recuperableonly;
2595 if (empty($tva_tx)) {
2596 $tva_npr = 0;
2597 }
2598 break;
2599 }
2600 }
2601 }
2602 }
2603 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY')) {
2604 // If price per quantity
2605 if (!empty($this->prices_by_qty[0])) {
2606 // yes, this product has some prices per quantity
2607 // Search price into product_price_by_qty from $this->id
2608 foreach ($this->prices_by_qty_list[0] as $priceforthequantityarray) {
2609 if ($priceforthequantityarray['rowid'] != $pqp) {
2610 continue;
2611 }
2612 // We found the price
2613 if ($priceforthequantityarray['price_base_type'] == 'HT') {
2614 $pu_ht = $priceforthequantityarray['unitprice'];
2615 } else {
2616 $pu_ttc = $priceforthequantityarray['unitprice'];
2617 }
2618 break;
2619 }
2620 }
2621 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) {
2622 // If price per quantity and customer
2623 if (!empty($this->prices_by_qty[$thirdparty_buyer->price_level])) {
2624 // yes, this product has some prices per quantity
2625 // Search price into product_price_by_qty from $this->id
2626 foreach ($this->prices_by_qty_list[$thirdparty_buyer->price_level] as $priceforthequantityarray) {
2627 if ($priceforthequantityarray['rowid'] != $pqp) {
2628 continue;
2629 }
2630 // We found the price
2631 if ($priceforthequantityarray['price_base_type'] == 'HT') {
2632 $pu_ht = $priceforthequantityarray['unitprice'];
2633 } else {
2634 $pu_ttc = $priceforthequantityarray['unitprice'];
2635 }
2636 break;
2637 }
2638 }
2639 }
2640
2641 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);
2642 }
2643
2644 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2658 public function get_buyprice($prodfournprice, $qty, $product_id = 0, $fourn_ref = '', $fk_soc = 0)
2659 {
2660 // phpcs:enable
2661 global $action, $hookmanager;
2662
2663 // Call hook if any
2664 if (is_object($hookmanager)) {
2665 $parameters = array(
2666 'prodfournprice' => $prodfournprice,
2667 'qty' => $qty,
2668 'product_id' => $product_id,
2669 'fourn_ref' => $fourn_ref,
2670 'fk_soc' => $fk_soc,
2671 );
2672 // Note that $action and $object may have been modified by some hooks
2673 $reshook = $hookmanager->executeHooks('getBuyPrice', $parameters, $this, $action);
2674 if ($reshook > 0) {
2675 return $hookmanager->resArray;
2676 }
2677 }
2678
2679 $result = 0;
2680
2681 // 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)
2682 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2683 $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,";
2684 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2685 $sql .= " pfp.packaging";
2686 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2687 $sql .= " WHERE pfp.rowid = ".((int) $prodfournprice);
2688 if ($qty > 0) {
2689 $sql .= " AND pfp.quantity <= ".((float) $qty);
2690 }
2691 $sql .= " ORDER BY pfp.quantity DESC";
2692
2693 dol_syslog(get_class($this)."::get_buyprice first search by prodfournprice/qty", LOG_DEBUG);
2694 $resql = $this->db->query($sql);
2695 if ($resql) {
2696 $obj = $this->db->fetch_object($resql);
2697 if ($obj && $obj->quantity > 0) { // If we found a supplier prices from the id of supplier price
2698 if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2699 $prod_supplier = new ProductFournisseur($this->db);
2700 $prod_supplier->product_fourn_price_id = $obj->rowid;
2701 $prod_supplier->id = $obj->fk_product;
2702 $prod_supplier->fourn_qty = $obj->quantity;
2703 $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2704 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2705
2706 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2707 $priceparser = new PriceParser($this->db);
2708 $price_result = $priceparser->parseProductSupplier($prod_supplier);
2709 if ($price_result >= 0) {
2710 $obj->price = $price_result;
2711 }
2712 }
2713 $this->product_fourn_price_id = $obj->rowid;
2714 $this->buyprice = $obj->price; // deprecated
2715 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product of supplier
2716 $this->fourn_price_base_type = 'HT'; // Price base type
2717 $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2718 $this->ref_fourn = $obj->ref_supplier; // deprecated
2719 $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2720 $this->desc_supplier = $obj->desc_supplier; // desc supplier
2721 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2722 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2723 $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2724 $this->fourn_multicurrency_price = $obj->multicurrency_price;
2725 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2726 $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2727 $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2728 $this->fourn_multicurrency_code = $obj->multicurrency_code;
2729 if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2730 $this->packaging = (float) $obj->packaging;
2731 }
2732 // Expose the supplier minimum purchase quantity so callers can enforce qty_min
2733 // on top of packaging-multiple rounding (#38783). pfp.quantity is the lowest
2734 // qty for which this price line is valid; for "below-min" callers this is the
2735 // floor they must round up to.
2736 $this->fourn_qty = $obj->quantity;
2737 $result = $obj->fk_product;
2738 return $result;
2739 } else { // If not found
2740 // 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.
2741 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2742 $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,";
2743 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2744 $sql .= " pfp.packaging";
2745 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2746 $sql .= " WHERE 1 = 1";
2747 if ($product_id > 0) {
2748 $sql .= " AND pfp.fk_product = ".((int) $product_id);
2749 }
2750 if ($fourn_ref != 'none') {
2751 $sql .= " AND pfp.ref_fourn = '".$this->db->escape($fourn_ref)."'";
2752 }
2753 if ($fk_soc > 0) {
2754 $sql .= " AND pfp.fk_soc = ".((int) $fk_soc);
2755 }
2756 if ($qty > 0) {
2757 $sql .= " AND pfp.quantity <= ".((float) $qty);
2758 }
2759 $sql .= " ORDER BY pfp.quantity DESC";
2760 $sql .= " LIMIT 1";
2761
2762 dol_syslog(get_class($this)."::get_buyprice second search from qty/ref/product_id", LOG_DEBUG);
2763 $resql = $this->db->query($sql);
2764 if ($resql) {
2765 $obj = $this->db->fetch_object($resql);
2766 if ($obj && $obj->quantity > 0) { // If found
2767 if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2768 $prod_supplier = new ProductFournisseur($this->db);
2769 $prod_supplier->product_fourn_price_id = $obj->rowid;
2770 $prod_supplier->id = $obj->fk_product;
2771 $prod_supplier->fourn_qty = $obj->quantity;
2772 $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2773 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2774
2775 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2776 $priceparser = new PriceParser($this->db);
2777 $price_result = $priceparser->parseProductSupplier($prod_supplier);
2778 if ($price_result >= 0) {
2779 $obj->price = $price_result;
2780 }
2781 }
2782 $this->product_fourn_price_id = $obj->rowid;
2783 $this->buyprice = $obj->price; // deprecated
2784 $this->fourn_qty = $obj->quantity; // min quantity for price for a virtual supplier
2785 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product for a virtual supplier
2786 $this->fourn_price_base_type = 'HT'; // Price base type for a virtual supplier
2787 $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2788 $this->ref_fourn = $obj->ref_supplier; // deprecated
2789 $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2790 $this->desc_supplier = $obj->desc_supplier; // desc supplier
2791 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2792 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2793 $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2794 $this->fourn_multicurrency_price = $obj->multicurrency_price;
2795 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2796 $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2797 $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2798 $this->fourn_multicurrency_code = $obj->multicurrency_code;
2799 if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2800 $this->packaging = (float) $obj->packaging;
2801 }
2802 // Expose pfp.quantity so callers can enforce qty_min on top of packaging
2803 // rounding (#38783).
2804 $this->fourn_qty = $obj->quantity;
2805 $result = $obj->fk_product;
2806 return $result;
2807 } else {
2808 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.
2809 }
2810 } else {
2811 $this->error = $this->db->lasterror();
2812 return -3;
2813 }
2814 }
2815 } else {
2816 $this->error = $this->db->lasterror();
2817 return -2;
2818 }
2819 }
2820
2821
2840 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)
2841 {
2842 $lastPriceData = $this->getArrayForPriceCompare($level); // temporary store current price before update
2843
2844 $id = $this->id;
2845
2846 dol_syslog(get_class($this)."::update_price id=".$id." newprice=".$newprice." newpricebase=".$newpricebase." newminprice=".$newminprice." level=".$level." npr=".$newnpr." newdefaultvatcode=".$newdefaultvatcode);
2847
2848 // Clean parameters
2849 if (empty($this->tva_tx)) {
2850 $this->tva_tx = 0.0;
2851 }
2852 if (empty($newnpr)) {
2853 $newnpr = 0.0;
2854 }
2855 if (empty($newminprice)) {
2856 $newminprice = 0.0;
2857 }
2858
2859 // Check parameters
2860 if ($newvat === null || $newvat == '') { // Maintain '' for backwards compatibility
2861 $newvat = (float) $this->tva_tx;
2862 }
2863
2864 $localtaxtype1 = '';
2865 $localtaxtype2 = '';
2866
2867 // If multiprices are enabled, then we check if the current product is subject to price autogeneration
2868 // Price will be modified ONLY when the first one is the one that is being modified
2869 if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && !$ignore_autogen && $this->price_autogen && ($level == 1)) {
2870 return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpbq);
2871 }
2872
2873 if (!empty($newminprice) && ($newminprice > $newprice)) {
2874 $this->error = 'ErrorPriceCantBeLowerThanMinPrice';
2875 return -1;
2876 }
2877
2878 if ($newprice === 0 || $newprice !== '') {
2879 if ($newpricebase == 'TTC') {
2880 $price_ttc = (float) price2num($newprice, 'MU');
2881 $price = (float) price2num($newprice) / (1 + ((float) $newvat / 100));
2882 $price = (float) price2num($price, 'MU');
2883
2884 if ((string) $newminprice != '0') {
2885 $price_min_ttc = (float) price2num($newminprice, 'MU');
2886 $price_min = (float) price2num($newminprice) / (1 + ($newvat / 100));
2887 $price_min = (float) price2num($price_min, 'MU');
2888 } else {
2889 $price_min = 0.0;
2890 $price_min_ttc = 0.0;
2891 }
2892 } else {
2893 $price = (float) price2num($newprice, 'MU');
2894 $price_ttc = ($newnpr != 1) ? (float) price2num($newprice) * (1 + ($newvat / 100)) : $price;
2895 $price_ttc = (float) price2num($price_ttc, 'MU');
2896
2897 if ((string) $newminprice != '0') {
2898 $price_min = (float) price2num($newminprice, 'MU');
2899 $price_min_ttc = (float) price2num($newminprice) * (1 + ($newvat / 100));
2900 $price_min_ttc = (float) price2num($price_min_ttc, 'MU');
2901 //print 'X'.$newminprice.'-'.$price_min;
2902 } else {
2903 $price_min = 0.0;
2904 $price_min_ttc = 0.0;
2905 }
2906 }
2907 //print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc;
2908 if (count($localtaxes_array) > 0) {
2909 $localtaxtype1 = $localtaxes_array['0'];
2910 $localtax1 = $localtaxes_array['1'];
2911 $localtaxtype2 = $localtaxes_array['2'];
2912 $localtax2 = $localtaxes_array['3'];
2913 } else {
2914 // if array empty, we try to use the vat code
2915 if (!empty($newdefaultvatcode)) {
2916 global $mysoc;
2917 // Get record from code
2918 $sql = "SELECT t.rowid, t.code, t.recuperableonly as tva_npr, t.localtax1, t.localtax2, t.localtax1_type, t.localtax2_type";
2919 $sql .= " FROM ".MAIN_DB_PREFIX."c_tva as t, ".MAIN_DB_PREFIX."c_country as c";
2920 $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '".$this->db->escape($mysoc->country_code)."'";
2921 $sql .= " AND t.taux = ".((float) $newdefaultvatcode)." AND t.active = 1";
2922 $sql .= " AND t.code = '".$this->db->escape($newdefaultvatcode)."'";
2923 $resql = $this->db->query($sql);
2924 if ($resql) {
2925 $obj = $this->db->fetch_object($resql);
2926 if ($obj) {
2927 $npr = $obj->tva_npr;
2928 $localtax1 = $obj->localtax1;
2929 $localtax2 = $obj->localtax2;
2930 $localtaxtype1 = $obj->localtax1_type;
2931 $localtaxtype2 = $obj->localtax2_type;
2932 }
2933 }
2934 } else {
2935 // old method. deprecated because we can't retrieve type
2936 $localtaxtype1 = '0';
2937 $localtax1 = get_localtax($newvat, 1);
2938 $localtaxtype2 = '0';
2939 $localtax2 = get_localtax($newvat, 2);
2940 }
2941 }
2942 if (empty($localtax1)) {
2943 $localtax1 = 0; // If = '' then = 0
2944 }
2945 if (empty($localtax2)) {
2946 $localtax2 = 0; // If = '' then = 0
2947 }
2948
2949 $this->db->begin();
2950
2951 // The price columns of the table product hold the default price of the product, that is the price of the level 1
2952 // when multiprices are enabled (level is 0 when they are not). So they must not be overwritten by another level.
2953 // The vat columns are however shared by all levels, so they are always updated.
2954 $updatedefaultprice = (empty($level) || $level == 1);
2955
2956 // Don't put quotes here on decimal numbers.
2957 // This causes storage with base rounding instead of exact values.
2958 $sql = "UPDATE ".$this->db->prefix()."product SET";
2959 if ($updatedefaultprice) {
2960 $sql .= " price_base_type = '".$this->db->escape($newpricebase)."',";
2961 $sql .= " price = ".(float) $price.",";
2962 $sql .= " price_ttc = ".(float) $price_ttc.",";
2963 $sql .= " price_min = ".(float) $price_min.",";
2964 $sql .= " price_min_ttc = ".(float) $price_min_ttc.",";
2965 $sql .= " price_label = ".(!empty($price_label) ? "'".$this->db->escape($price_label)."'" : "null").",";
2966 }
2967 $sql .= " localtax1_tx = ".($localtax1 >= 0 ? (float) $localtax1 : 'NULL').",";
2968 $sql .= " localtax2_tx = ".($localtax2 >= 0 ? (float) $localtax2 : 'NULL').",";
2969 $sql .= " localtax1_type = ".($localtaxtype1 != '' ? "'".$this->db->escape($localtaxtype1)."'" : "'0'").",";
2970 $sql .= " localtax2_type = ".($localtaxtype2 != '' ? "'".$this->db->escape($localtaxtype2)."'" : "'0'").",";
2971 $sql .= " default_vat_code = ".($newdefaultvatcode ? "'".$this->db->escape($newdefaultvatcode)."'" : "null").",";
2972 $sql .= " tva_tx = ".(float) price2num($newvat).",";
2973 $sql .= " recuperableonly = '".$this->db->escape((string) $newnpr)."'";
2974 $sql .= " WHERE rowid = ".((int) $id);
2975
2976 dol_syslog(get_class($this)."::update_price", LOG_DEBUG);
2977 $resql = $this->db->query($sql);
2978 if ($resql) {
2979 $this->multiprices[$level] = $price;
2980 $this->multiprices_ttc[$level] = $price_ttc;
2981 $this->multiprices_min[$level] = $price_min;
2982 $this->multiprices_min_ttc[$level] = $price_min_ttc;
2983 $this->multiprices_base_type[$level] = $newpricebase;
2984 $this->multiprices_default_vat_code[$level] = $newdefaultvatcode;
2985 $this->multiprices_tva_tx[$level] = $newvat;
2986 $this->multiprices_recuperableonly[$level] = $newnpr;
2987
2988 // Save the default price of the product to restore it after the log if we are updating another level
2989 // (_log_price() reads the price into the properties of the object to save the price of the level).
2990 $savdefaultprice = array($this->price, $this->price_label, $this->price_ttc, $this->price_min, $this->price_min_ttc, $this->price_base_type);
2991
2992 $this->price = $price;
2993 $this->price_label = $price_label;
2994 $this->price_ttc = $price_ttc;
2995 $this->price_min = $price_min;
2996 $this->price_min_ttc = $price_min_ttc;
2997 $this->price_base_type = $newpricebase;
2998 $this->default_vat_code = $newdefaultvatcode;
2999 $this->tva_tx = $newvat;
3000 $this->tva_npr = $newnpr;
3001
3002 //Local taxes
3003 $this->localtax1_tx = $localtax1;
3004 $this->localtax2_tx = $localtax2;
3005 $this->localtax1_type = $localtaxtype1;
3006 $this->localtax2_type = $localtaxtype2;
3007
3008 // Price by quantity
3009 $this->price_by_qty = $newpbq;
3010
3011 // check if price have really change before log
3012 $newPriceData = $this->getArrayForPriceCompare($level);
3013 if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || (!getDolGlobalString('PRODUIT_MULTIPRICES') && !getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES'))) {
3014 $this->_log_price($user, $level); // Save price for level into table product_price
3015 }
3016
3017 if (!$updatedefaultprice) {
3018 // The price of the level is now saved, so we can restore the default price of the product
3019 list($this->price, $this->price_label, $this->price_ttc, $this->price_min, $this->price_min_ttc, $this->price_base_type) = $savdefaultprice;
3020 }
3021
3022 $this->level = $level; // Store level of price edited for trigger
3023
3024 // Call trigger
3025 if (!$notrigger) {
3026 $result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user);
3027 if ($result < 0) {
3028 $this->db->rollback();
3029 return -1;
3030 }
3031 }
3032 // End call triggers
3033
3034 $this->db->commit();
3035 } else {
3036 $this->db->rollback();
3037 $this->error = $this->db->lasterror();
3038 return -1;
3039 }
3040 }
3041
3042 return 1;
3043 }
3044
3052 public function setPriceExpression($expression_id)
3053 {
3054 global $user;
3055
3056 $this->fk_price_expression = $expression_id;
3057
3058 return $this->update($this->id, $user);
3059 }
3060
3073 public function fetch($id = 0, $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0)
3074 {
3075 include_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
3076
3077 global $conf;
3078
3079 dol_syslog(get_class($this)."::fetch id=".$id." ref=".$ref." ref_ext=".$ref_ext);
3080
3081 // Check parameters
3082 if (!$id && !$ref && !$ref_ext && !$barcode) {
3083 $this->error = 'ErrorWrongParameters';
3084 dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR);
3085 return -1;
3086 }
3087
3088 $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,";
3089 $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,";
3090 $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,";
3091 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.last_main_doc,";
3092 $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,";
3093 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
3094 $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,";
3095 } else {
3096 $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,";
3097 }
3098
3099 // For MultiCompany
3100 // PMP per entity & Stocks Sharings stock_reel includes only stocks shared with this entity
3101 $separatedEntityPMP = false; // Set to true to get the AWP from table llx_product_perentity instead of field 'pmp' into llx_product.
3102 $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.
3103 $visibleWarehousesEntities = $conf->entity;
3104 if (getDolGlobalString('MULTICOMPANY_PRODUCT_SHARING_ENABLED')) {
3105 if (getDolGlobalString('MULTICOMPANY_PMP_PER_ENTITY_ENABLED')) {
3106 $checkPMPPerEntity = $this->db->query("SELECT pmp FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = ".((int) $id)." AND entity = ".(int) $conf->entity);
3107 if ($this->db->num_rows($checkPMPPerEntity) > 0) {
3108 $separatedEntityPMP = true;
3109 }
3110 }
3111 global $mc;
3112 $separatedStock = true;
3113 if (isset($mc->sharings['stock']) && !empty($mc->sharings['stock'])) {
3114 $visibleWarehousesEntities .= "," . implode(",", $mc->sharings['stock']);
3115 }
3116 }
3117 if ($separatedEntityPMP) {
3118 $sql .= " ppe.pmp,";
3119 } else {
3120 $sql .= " p.pmp,";
3121 }
3122 $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,";
3123 $sql .= " p.fk_price_expression, p.price_autogen, p.stockable_product, p.model_pdf,";
3124 $sql .= " p.price_label,";
3125 if ($separatedStock) {
3126 $sql .= " SUM(sp.reel) as stock";
3127 } else {
3128 $sql .= " p.stock";
3129 }
3130 $sql .= " FROM ".$this->db->prefix()."product as p";
3131 $sql .= " LEFT JOIN ".$this->db->prefix()."product_extrafields as pef ON pef.fk_object=p.rowid";
3132 if (getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED') || $separatedEntityPMP) {
3133 $sql .= " LEFT JOIN " . $this->db->prefix() . "product_perentity as ppe ON ppe.fk_product = p.rowid AND ppe.entity = " . ((int) $conf->entity);
3134 }
3135 if ($separatedStock) {
3136 $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)."))";
3137 }
3138
3139 if ($id) {
3140 $sql .= " WHERE p.rowid = ".((int) $id);
3141 } else {
3142 $sql .= " WHERE p.entity IN (".getEntity($this->element).")";
3143 if ($ref) {
3144 $sql .= " AND p.ref = '".$this->db->escape($ref)."'";
3145 } elseif ($ref_ext) {
3146 $sql .= " AND p.ref_ext = '".$this->db->escape($ref_ext)."'";
3147 } elseif ($barcode) {
3148 $sql .= " AND p.barcode = '".$this->db->escape($barcode)."'";
3149 }
3150 }
3151 if ($separatedStock) {
3152 $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,";
3153 $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,";
3154 $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,";
3155 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
3156 $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,";
3157 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
3158 $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,";
3159 } else {
3160 $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,";
3161 }
3162 if ($separatedEntityPMP) {
3163 $sql .= " ppe.pmp,";
3164 } else {
3165 $sql .= " p.pmp,";
3166 }
3167 $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,";
3168 $sql .= " p.fk_price_expression, p.price_autogen, p.stockable_product, p.model_pdf,";
3169 $sql .= " p.price_label";
3170 // can't be false TODO fix
3171 // if (!$separatedStock) {
3172 // $sql .= ", p.stock";
3173 // }
3174 }
3175
3176 $resql = $this->db->query($sql);
3177 if ($resql) {
3178 unset($this->oldcopy);
3179
3180 if ($this->db->num_rows($resql) > 0) {
3181 $obj = $this->db->fetch_object($resql);
3182
3183 $this->id = $obj->rowid;
3184 $this->ref = $obj->ref;
3185 $this->ref_ext = $obj->ref_ext;
3186 $this->label = $obj->label;
3187 $this->description = $obj->description;
3188 $this->url = $obj->url;
3189 $this->note_public = $obj->note_public;
3190 $this->note_private = $obj->note_private;
3191 $this->note = $obj->note_private; // deprecated
3192
3193 $this->type = $obj->fk_product_type;
3194 $this->price_label = $obj->price_label;
3195 $this->status = $obj->tosell;
3196 $this->status_buy = $obj->tobuy;
3197 $this->status_batch = $obj->tobatch;
3198 $this->sell_or_eat_by_mandatory = $obj->sell_or_eat_by_mandatory;
3199 $this->batch_mask = $obj->batch_mask;
3200
3201 $this->customcode = $obj->customcode;
3202 $this->country_id = $obj->fk_country;
3203 $this->country_code = getCountry($this->country_id, '2', $this->db);
3204 $this->state_id = $obj->fk_state;
3205 $this->lifetime = $obj->lifetime;
3206 $this->qc_frequency = $obj->qc_frequency;
3207 $this->price = $obj->price;
3208 $this->price_ttc = $obj->price_ttc;
3209 $this->price_min = $obj->price_min;
3210 $this->price_min_ttc = $obj->price_min_ttc;
3211 $this->price_base_type = (empty($obj->price_base_type) ? 'HT' : $obj->price_base_type);
3212 $this->cost_price = isset($obj->cost_price) ? (float) $obj->cost_price : null;
3213 $this->default_vat_code = $obj->default_vat_code;
3214 $this->tva_tx = $obj->tva_tx;
3216 $this->tva_npr = $obj->tva_npr;
3218 $this->localtax1_tx = $obj->localtax1_tx;
3219 $this->localtax2_tx = $obj->localtax2_tx;
3220 $this->localtax1_type = $obj->localtax1_type;
3221 $this->localtax2_type = $obj->localtax2_type;
3222
3223 $this->finished = $obj->finished;
3224 $this->fk_default_bom = $obj->fk_default_bom;
3225
3226 $this->duration = $obj->duration;
3227 $matches = [];
3228 preg_match('/([\d.]+)(\w+)/', $obj->duration, $matches);
3229 $this->duration_value = !empty($matches[1]) ? (float) $matches[1] : 0;
3230 $this->duration_unit = !empty($matches[2]) ? (string) $matches[2] : null;
3231 $this->canvas = $obj->canvas;
3232 $this->net_measure = $obj->net_measure;
3233 $this->net_measure_units = $obj->net_measure_units;
3234 $this->weight = $obj->weight;
3235 $this->weight_units = (is_null($obj->weight_units) ? 0 : $obj->weight_units);
3236 $this->length = $obj->length;
3237 $this->length_units = (is_null($obj->length_units) ? 0 : $obj->length_units);
3238 $this->width = $obj->width;
3239 $this->width_units = (is_null($obj->width_units) ? 0 : $obj->width_units);
3240 $this->height = $obj->height;
3241 $this->height_units = (is_null($obj->height_units) ? 0 : $obj->height_units);
3242
3243 $this->surface = $obj->surface;
3244 $this->surface_units = (is_null($obj->surface_units) ? 0 : $obj->surface_units);
3245 $this->volume = $obj->volume;
3246 $this->volume_units = (is_null($obj->volume_units) ? 0 : $obj->volume_units);
3247 $this->barcode = $obj->barcode;
3248 $this->barcode_type = $obj->fk_barcode_type;
3249
3250 $this->accountancy_code_buy = $obj->accountancy_code_buy;
3251 $this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra;
3252 $this->accountancy_code_buy_export = $obj->accountancy_code_buy_export;
3253 $this->accountancy_code_sell = $obj->accountancy_code_sell;
3254 $this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra;
3255 $this->accountancy_code_sell_export = $obj->accountancy_code_sell_export;
3256
3257 $this->fk_default_warehouse = $obj->fk_default_warehouse;
3258 $this->fk_default_workstation = $obj->fk_default_workstation;
3259 $this->seuil_stock_alerte = $obj->seuil_stock_alerte;
3260 $this->desiredstock = $obj->desiredstock;
3261 $this->stock_reel = $obj->stock;
3262 $this->stockable_product = $obj->stockable_product;
3263 $this->pmp = $obj->pmp;
3264
3265 $this->date_creation = $this->db->jdate($obj->datec);
3266 $this->date_modification = $this->db->jdate($obj->tms);
3267
3268 $this->import_key = $obj->import_key;
3269 $this->entity = $obj->entity;
3270
3271 $this->ref_ext = $obj->ref_ext;
3272 $this->fk_price_expression = $obj->fk_price_expression;
3273 $this->fk_unit = $obj->fk_unit;
3274 $this->price_autogen = $obj->price_autogen;
3275 $this->model_pdf = $obj->model_pdf;
3276 $this->last_main_doc = $obj->last_main_doc;
3277
3278 $this->mandatory_period = $obj->mandatory_period;
3279
3280 if (getDolGlobalString('PRODUCT_USE_CUSTOMER_PACKAGING')) {
3281 $this->packaging = (float) $obj->packaging;
3282 }
3283
3284 $this->db->free($resql);
3285
3286 // fetch optionals attributes and labels
3287 $this->fetch_optionals();
3288
3289 // Multilangs
3290 if (getDolGlobalInt('MAIN_MULTILANGS') && empty($ignore_lang_load)) {
3291 $this->getMultiLangs();
3292 }
3293
3294 // Load multiprices array
3295 if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && empty($ignore_price_load)) { // prices per segment
3296 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
3297 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
3298 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3299 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
3300 $sql .= " ,price_label";
3301 $sql .= " FROM ".$this->db->prefix()."product_price";
3302 $sql .= " WHERE entity IN (".getEntity('productprice').")";
3303 $sql .= " AND price_level=".((int) $i);
3304 $sql .= " AND fk_product = ".((int) $this->id);
3305 $sql .= " ORDER BY date_price DESC, rowid DESC"; // Get the most recent line
3306 $sql .= " LIMIT 1"; // Only the first one
3307 $resql = $this->db->query($sql);
3308 if ($resql) {
3309 $result = $this->db->fetch_array($resql);
3310
3311 $this->multiprices[$i] = $result ? $result["price"] : null;
3312 $this->multiprices_ttc[$i] = $result ? $result["price_ttc"] : null;
3313 $this->multiprices_min[$i] = $result ? $result["price_min"] : null;
3314 $this->multiprices_min_ttc[$i] = $result ? $result["price_min_ttc"] : null;
3315 $this->multiprices_base_type[$i] = $result ? $result["price_base_type"] : null;
3316 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
3317 $this->multiprices_tva_tx[$i] = $result ? $result["tva_tx"].(!empty($result['default_vat_code']) ? ' ('.$result['default_vat_code'].')' : '') : null;
3318 $this->multiprices_recuperableonly[$i] = $result ? $result["recuperableonly"] : null;
3319
3320 // Price by quantity
3321 /*
3322 $this->prices_by_qty[$i]=$result["price_by_qty"];
3323 $this->prices_by_qty_id[$i]=$result["rowid"];
3324 // Get the pricelist according to the qty if flag is set
3325 if ($this->prices_by_qty[$i] == 1)
3326 {
3327 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
3328 $sql.= " FROM ".$this->db->prefix()."product_price_by_qty";
3329 $sql.= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
3330 $sql.= " ORDER BY quantity ASC";
3331
3332 $resql = $this->db->query($sql);
3333 if ($resql)
3334 {
3335 $resultat=array();
3336 $ii=0;
3337 while ($result= $this->db->fetch_array($resql)) {
3338 $resultat[$ii]=array();
3339 $resultat[$ii]["rowid"]=$result["rowid"];
3340 $resultat[$ii]["price"]= $result["price"];
3341 $resultat[$ii]["unitprice"]= $result["unitprice"];
3342 $resultat[$ii]["quantity"]= $result["quantity"];
3343 $resultat[$ii]["remise_percent"]= $result["remise_percent"];
3344 $resultat[$ii]["remise"]= $result["remise"]; // deprecated
3345 $resultat[$ii]["price_base_type"]= $result["price_base_type"];
3346 $ii++;
3347 }
3348 $this->prices_by_qty_list[$i]=$resultat;
3349 }
3350 else
3351 {
3352 dol_print_error($this->db);
3353 return -1;
3354 }
3355 }*/
3356 } else {
3357 $this->error = $this->db->lasterror;
3358 return -1;
3359 }
3360 }
3361 } elseif ((getDolGlobalString('PRODUIT_CUSTOMER_PRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && empty($ignore_price_load)) { // prices per customers
3362 // Nothing loaded by default. List may be very long.
3363 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY') && empty($ignore_price_load)) { // prices per quantity
3364 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3365 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid";
3366 $sql .= " FROM ".$this->db->prefix()."product_price";
3367 $sql .= " WHERE fk_product = ".((int) $this->id);
3368 $sql .= " ORDER BY date_price DESC, rowid DESC";
3369 $sql .= " LIMIT 1";
3370
3371 $resql = $this->db->query($sql);
3372 if ($resql) {
3373 $result = $this->db->fetch_array($resql);
3374
3375 if ($result) {
3376 // Price by quantity
3377 $this->prices_by_qty[0] = $result["price_by_qty"];
3378 $this->prices_by_qty_id[0] = $result["rowid"];
3379 // Get the pricelist according to the qty if flag is set
3380 if ($this->prices_by_qty[0] == 1) {
3381 $sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise, remise, price_base_type";
3382 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
3383 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[0]);
3384 $sql .= " ORDER BY quantity ASC";
3385
3386 $resql = $this->db->query($sql);
3387 if ($resql) {
3388 $resultat = array();
3389 $ii = 0;
3390 while ($result = $this->db->fetch_array($resql)) {
3391 $resultat[$ii] = array();
3392 $resultat[$ii]["rowid"] = $result["rowid"];
3393 $resultat[$ii]["price"] = $result["price"];
3394 $resultat[$ii]["unitprice"] = $result["unitprice"];
3395 $resultat[$ii]["quantity"] = $result["quantity"];
3396 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
3397 //$resultat[$ii]["remise"]= $result["remise"]; // deprecated
3398 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
3399 $ii++;
3400 }
3401 $this->prices_by_qty_list[0] = $resultat;
3402 } else {
3403 $this->error = $this->db->lasterror;
3404 return -1;
3405 }
3406 }
3407 }
3408 } else {
3409 $this->error = $this->db->lasterror;
3410 return -1;
3411 }
3412 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') && empty($ignore_price_load)) { // prices per customer and quantity
3413 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
3414 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
3415 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3416 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
3417 $sql .= " FROM ".$this->db->prefix()."product_price";
3418 $sql .= " WHERE entity IN (".getEntity('productprice').")";
3419 $sql .= " AND price_level=".((int) $i);
3420 $sql .= " AND fk_product = ".((int) $this->id);
3421 $sql .= " ORDER BY date_price DESC, rowid DESC";
3422 $sql .= " LIMIT 1";
3423 $resql = $this->db->query($sql);
3424 if (!$resql) {
3425 $this->error = $this->db->lasterror;
3426 return -1;
3427 } else {
3428 $result = $this->db->fetch_array($resql);
3429 $this->multiprices[$i] = (!empty($result["price"]) ? $result["price"] : 0);
3430 $this->multiprices_ttc[$i] = (!empty($result["price_ttc"]) ? $result["price_ttc"] : 0);
3431 $this->multiprices_min[$i] = (!empty($result["price_min"]) ? $result["price_min"] : 0);
3432 $this->multiprices_min_ttc[$i] = (!empty($result["price_min_ttc"]) ? $result["price_min_ttc"] : 0);
3433 $this->multiprices_base_type[$i] = (!empty($result["price_base_type"]) ? $result["price_base_type"] : '');
3434 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
3435 $this->multiprices_tva_tx[$i] = (!empty($result["tva_tx"]) ? $result["tva_tx"] : 0); // TODO Add ' ('.$result['default_vat_code'].')'
3436 $this->multiprices_recuperableonly[$i] = (!empty($result["recuperableonly"]) ? $result["recuperableonly"] : 0);
3437
3438 // Price by quantity
3439 $this->prices_by_qty[$i] = (!empty($result["price_by_qty"]) ? $result["price_by_qty"] : 0);
3440 $this->prices_by_qty_id[$i] = (!empty($result["rowid"]) ? $result["rowid"] : 0);
3441 // Get the pricelist according to the qty if flag is set
3442 if ($this->prices_by_qty[$i] == 1) {
3443 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
3444 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
3445 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
3446 $sql .= " ORDER BY quantity ASC";
3447
3448 $resql = $this->db->query($sql);
3449 if ($resql) {
3450 $resultat = array();
3451 $ii = 0;
3452 while ($result = $this->db->fetch_array($resql)) {
3453 $resultat[$ii] = array();
3454 $resultat[$ii]["rowid"] = $result["rowid"];
3455 $resultat[$ii]["price"] = $result["price"];
3456 $resultat[$ii]["unitprice"] = $result["unitprice"];
3457 $resultat[$ii]["quantity"] = $result["quantity"];
3458 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
3459 $resultat[$ii]["remise"] = $result["remise"]; // deprecated
3460 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
3461 $ii++;
3462 }
3463 $this->prices_by_qty_list[$i] = $resultat;
3464 } else {
3465 $this->error = $this->db->lasterror;
3466 return -1;
3467 }
3468 }
3469 }
3470 }
3471 }
3472
3473 if (isModEnabled('dynamicprices') && !empty($this->fk_price_expression) && empty($ignore_expression)) {
3474 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
3475 $priceparser = new PriceParser($this->db);
3476 $price_result = $priceparser->parseProduct($this);
3477 if ($price_result >= 0) {
3478 $this->price = $price_result;
3479 // Calculate the VAT
3480 $this->price_ttc = (float) price2num($this->price) * (1 + ($this->tva_tx / 100));
3481 $this->price_ttc = (float) price2num($this->price_ttc, 'MU');
3482 }
3483 }
3484
3485 // We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product.
3486 // Instead we just init the stock_warehouse array
3487 $this->stock_warehouse = array();
3488
3489 return 1;
3490 } else {
3491 return 0;
3492 }
3493 } else {
3494 $this->error = $this->db->lasterror();
3495 return -1;
3496 }
3497 }
3498
3499 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3506 public function load_stats_mo($socid = 0)
3507 {
3508 // phpcs:enable
3509 global $user, $hookmanager, $action;
3510
3511 $error = 0;
3512
3513 foreach (array('toconsume', 'consumed', 'toproduce', 'produced') as $role) {
3514 $this->stats_mo['customers_'.$role] = 0;
3515 $this->stats_mo['nb_'.$role] = 0;
3516 $this->stats_mo['qty_'.$role] = 0;
3517
3518 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3519 $sql .= " SUM(mp.qty) as qty";
3520 $sql .= " FROM ".$this->db->prefix()."mrp_mo as c";
3521 $sql .= " INNER JOIN ".$this->db->prefix()."mrp_production as mp ON mp.fk_mo=c.rowid";
3522 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3523 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3524 }
3525 $sql .= " WHERE ";
3526 $sql .= " c.entity IN (".getEntity('mo').")";
3527
3528 $sql .= " AND mp.fk_product = ".((int) $this->id);
3529 $sql .= " AND mp.role ='".$this->db->escape($role)."'";
3530 if ($socid > 0) {
3531 $sql .= " AND c.fk_soc = ".((int) $socid);
3532 }
3533
3534 $result = $this->db->query($sql);
3535 if ($result) {
3536 $obj = $this->db->fetch_object($result);
3537 $this->stats_mo['customers_'.$role] = $obj->nb_customers ? $obj->nb_customers : 0;
3538 $this->stats_mo['nb_'.$role] = $obj->nb ? $obj->nb : 0;
3539 $this->stats_mo['qty_'.$role] = $obj->qty ? price2num($obj->qty, 'MS') : 0; // qty may be a float due to the SUM()
3540 } else {
3541 $this->error = $this->db->error();
3542 $error++;
3543 }
3544 }
3545
3546 if (!empty($error)) {
3547 return -1;
3548 }
3549
3550 $parameters = array('socid' => $socid);
3551 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3552 if ($reshook > 0) {
3553 $this->stats_mo = $hookmanager->resArray['stats_mo'];
3554 }
3555
3556 return 1;
3557 }
3558
3559 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3566 public function load_stats_bom($socid = 0)
3567 {
3568 // phpcs:enable
3569 global $hookmanager, $action;
3570
3571 $error = 0;
3572
3573 $this->stats_bom['nb_toproduce'] = 0;
3574 $this->stats_bom['nb_toconsume'] = 0;
3575 $this->stats_bom['qty_toproduce'] = 0;
3576 $this->stats_bom['qty_toconsume'] = 0;
3577
3578 $sql = "SELECT COUNT(DISTINCT b.rowid) as nb_toproduce,";
3579 $sql .= " SUM(b.qty) as qty_toproduce";
3580 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3581 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom = b.rowid";
3582 $sql .= " WHERE ";
3583 $sql .= " b.entity IN (".getEntity('bom').")";
3584 $sql .= " AND b.fk_product =".((int) $this->id);
3585 $sql .= " GROUP BY b.rowid";
3586
3587 $result = $this->db->query($sql);
3588 if ($result) {
3589 $obj = $this->db->fetch_object($result);
3590 $this->stats_bom['nb_toproduce'] = !empty($obj->nb_toproduce) ? $obj->nb_toproduce : 0;
3591 $this->stats_bom['qty_toproduce'] = !empty($obj->qty_toproduce) ? price2num($obj->qty_toproduce) : 0;
3592 } else {
3593 $this->error = $this->db->error();
3594 $error++;
3595 }
3596
3597 $sql = "SELECT COUNT(DISTINCT bl.rowid) as nb_toconsume,";
3598 $sql .= " SUM(bl.qty) as qty_toconsume";
3599 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3600 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
3601 $sql .= " WHERE ";
3602 $sql .= " b.entity IN (".getEntity('bom').")";
3603 $sql .= " AND bl.fk_product =".((int) $this->id);
3604
3605 $result = $this->db->query($sql);
3606 if ($result) {
3607 $obj = $this->db->fetch_object($result);
3608 $this->stats_bom['nb_toconsume'] = !empty($obj->nb_toconsume) ? $obj->nb_toconsume : 0;
3609 $this->stats_bom['qty_toconsume'] = !empty($obj->qty_toconsume) ? price2num($obj->qty_toconsume) : 0;
3610 } else {
3611 $this->error = $this->db->error();
3612 $error++;
3613 }
3614
3615 if (!empty($error)) {
3616 return -1;
3617 }
3618
3619 $parameters = array('socid' => $socid);
3620 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3621 if ($reshook > 0) {
3622 $this->stats_bom = $hookmanager->resArray['stats_bom'];
3623 }
3624
3625 return 1;
3626 }
3627
3628 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3635 public function load_stats_propale($socid = 0)
3636 {
3637 // phpcs:enable
3638 global $user, $hookmanager, $action;
3639
3640 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,";
3641 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3642 $sql .= " FROM ".$this->db->prefix()."propaldet as pd";
3643 $sql .= ", ".$this->db->prefix()."propal as p";
3644 $sql .= ", ".$this->db->prefix()."societe as s";
3645 $sql .= " WHERE p.rowid = pd.fk_propal";
3646 $sql .= " AND p.fk_soc = s.rowid";
3647 $sql .= " AND p.entity IN (".getEntity('propal').")";
3648 $sql .= " AND pd.fk_product = ".((int) $this->id);
3649 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3650 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = p.fk_soc AND sc.fk_user = ".((int) $user->id);
3651 }
3652 //$sql.= " AND pr.fk_statut != 0";
3653 if ($socid > 0) {
3654 $sql .= " AND p.fk_soc = ".((int) $socid);
3655 }
3656 // Add where from hooks
3657 $parameters = array('socid' => $socid, 'type_element' => 'propal');
3658 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3659 $sql .= $hookmanager->resPrint;
3660 $result = $this->db->query($sql);
3661 if ($result) {
3662 $obj = $this->db->fetch_object($result);
3663 $this->stats_propale['customers'] = (int) $obj->nb_customers;
3664 $this->stats_propale['nb'] = (int) $obj->nb;
3665 $this->stats_propale['rows'] = (int) $obj->nb_rows;
3666 $this->stats_propale['qty'] = $obj->qty ? (float) $obj->qty : 0;
3667
3668 // if it's a virtual product, maybe it is in proposal by extension
3669 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3670 $TFather = $this->getFather();
3671 if (is_array($TFather) && !empty($TFather)) {
3672 foreach ($TFather as &$fatherData) {
3673 $pFather = new Product($this->db);
3674 $pFather->id = (int) $fatherData['id'];
3675 $qtyCoef = $fatherData['qty'];
3676
3677 if ($fatherData['incdec']) {
3678 $pFather->load_stats_propale($socid);
3679
3680 if (!empty($pFather->stats_propale)) {
3681 $this->stats_propale['customers'] += $pFather->stats_propale['customers'];
3682 $this->stats_propale['nb'] += $pFather->stats_propale['nb'];
3683 $this->stats_propale['rows'] += $pFather->stats_propale['rows'];
3684 $this->stats_propale['qty'] += $pFather->stats_propale['qty'] * $qtyCoef;
3685 }
3686 }
3687 }
3688 }
3689 }
3690
3691 $parameters = array('socid' => $socid);
3692 $reshook = $hookmanager->executeHooks('loadStatsCustomerProposal', $parameters, $this, $action);
3693 if ($reshook > 0) {
3694 $this->stats_propale = $hookmanager->resArray['stats_propale'];
3695 }
3696
3697 return 1;
3698 } else {
3699 $this->error = $this->db->error();
3700 return -1;
3701 }
3702 }
3703
3704
3705 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3712 public function load_stats_proposal_supplier($socid = 0)
3713 {
3714 // phpcs:enable
3715 global $user, $hookmanager, $action;
3716
3717 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,";
3718 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3719 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as pd";
3720 $sql .= ", ".$this->db->prefix()."supplier_proposal as p";
3721 $sql .= ", ".$this->db->prefix()."societe as s";
3722 $sql .= " WHERE p.rowid = pd.fk_supplier_proposal";
3723 $sql .= " AND p.fk_soc = s.rowid";
3724 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
3725 $sql .= " AND pd.fk_product = ".((int) $this->id);
3726 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3727 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = p.fk_soc AND sc.fk_user = ".((int) $user->id);
3728 }
3729 //$sql.= " AND pr.fk_statut != 0";
3730 if ($socid > 0) {
3731 $sql .= " AND p.fk_soc = ".((int) $socid);
3732 }
3733
3734 $result = $this->db->query($sql);
3735 if ($result) {
3736 $obj = $this->db->fetch_object($result);
3737 $this->stats_proposal_supplier['suppliers'] = $obj->nb_suppliers;
3738 $this->stats_proposal_supplier['nb'] = $obj->nb;
3739 $this->stats_proposal_supplier['rows'] = $obj->nb_rows;
3740 $this->stats_proposal_supplier['qty'] = $obj->qty ? $obj->qty : 0;
3741
3742 $parameters = array('socid' => $socid);
3743 $reshook = $hookmanager->executeHooks('loadStatsSupplierProposal', $parameters, $this, $action);
3744 if ($reshook > 0) {
3745 $this->stats_proposal_supplier = $hookmanager->resArray['stats_proposal_supplier'];
3746 }
3747
3748 return 1;
3749 } else {
3750 $this->error = $this->db->error();
3751 return -1;
3752 }
3753 }
3754
3755
3756 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3765 public function load_stats_commande($socid = 0, $filtrestatut = '', $forVirtualStock = 0)
3766 {
3767 // phpcs:enable
3768 global $user, $hookmanager, $action;
3769
3770
3771 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3772 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3773 $sql .= " FROM ".$this->db->prefix()."commandedet as cd";
3774 $sql .= ", ".$this->db->prefix()."commande as c";
3775 $sql .= ", ".$this->db->prefix()."societe as s";
3776 $sql .= " WHERE c.rowid = cd.fk_commande";
3777 $sql .= " AND c.fk_soc = s.rowid";
3778 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'commande').")";
3779 $sql .= " AND cd.fk_product = ".((int) $this->id);
3780 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3781 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3782 }
3783 if ($socid > 0) {
3784 $sql .= " AND c.fk_soc = ".((int) $socid);
3785 }
3786 if ($filtrestatut != '') {
3787 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3788 }
3789 // Add where from hooks
3790 $parameters = array('socid' => $socid, 'type_element' => 'order');
3791 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3792 $sql .= $hookmanager->resPrint;
3793 $result = $this->db->query($sql);
3794 if ($result) {
3795 $obj = $this->db->fetch_object($result);
3796 $this->stats_commande['customers'] = (int) $obj->nb_customers;
3797 $this->stats_commande['nb'] = (int) $obj->nb;
3798 $this->stats_commande['rows'] = (int) $obj->nb_rows;
3799 $this->stats_commande['qty'] = $obj->qty ? (float) $obj->qty : 0;
3800
3801 // if it's a virtual product, maybe it is in order by extension
3802 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3803 $TFather = $this->getFather();
3804 if (is_array($TFather) && !empty($TFather)) {
3805 foreach ($TFather as &$fatherData) {
3806 $pFather = new Product($this->db);
3807 $pFather->id = $fatherData['id'];
3808 $qtyCoef = $fatherData['qty'];
3809
3810 if ($fatherData['incdec']) {
3811 $pFather->load_stats_commande($socid, $filtrestatut);
3812
3813 $this->stats_commande['customers'] += $pFather->stats_commande['customers'];
3814 $this->stats_commande['nb'] += $pFather->stats_commande['nb'];
3815 $this->stats_commande['rows'] += $pFather->stats_commande['rows'];
3816 $this->stats_commande['qty'] += $pFather->stats_commande['qty'] * $qtyCoef;
3817 }
3818 }
3819 }
3820 }
3821
3822 // If stock decrease is on invoice validation, the theoretical stock continue to
3823 // count the orders lines containing product in theoretical stock when some are already removed by invoice validation.
3824 if ($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
3825 // Do the request using an UNION instead of a OR in the JOIN that is very slow
3826 if (getDolGlobalString('DECREASE_ONLY_UNINVOICEDPRODUCTS')) {
3827 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is on, we make a compensation but only if order not yet invoice.
3828 $adeduire = 0;
3829 $sql = "SELECT SUM(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3830 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3831 //$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'))";
3832 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_target = f.rowid AND el.targettype = 'facture' AND sourcetype = 'commande' ";
3833 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid";
3834 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3835 $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
3836
3837 $sql .= " UNION ";
3838
3839 $sql .= "SELECT SUM(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3840 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3841 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_source = f.rowid AND el.targettype = 'commande' AND sourcetype = 'facture' ";
3842 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_target = c.rowid";
3843 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3844 $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
3845 // Add where from hooks
3846 $parameters = array('socid' => $socid, 'type_element' => 'order');
3847 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3848 $sql .= $hookmanager->resPrint;
3849 $resql = $this->db->query($sql);
3850 if ($resql) {
3851 while ($obj = $this->db->fetch_object($resql)) {
3852 $adeduire += (float) $obj->count;
3853 }
3854 }
3855
3856 $this->stats_commande['qty'] -= $adeduire;
3857 } else {
3858 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is off, we make a compensation with lines of invoices linked to the order
3859 include_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
3860
3861 // For every order having invoice already validated we need to decrease stock cause it's in physical stock
3862 $adeduire = 0;
3863 $sql = "SELECT sum(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3864 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3865 //$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'))";
3866 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_target = f.rowid AND el.targettype = 'facture' AND sourcetype = 'commande' ";
3867 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid";
3868 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product = ".((int) $this->id);
3869 $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
3870
3871 $sql .= " UNION ALL ";
3872
3873 $sql .= "SELECT sum(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3874 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3875 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_source = f.rowid AND el.targettype = 'commande' AND sourcetype = 'facture' ";
3876 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_target = c.rowid";
3877 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product = ".((int) $this->id);
3878 $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
3879
3880 dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3881 // Add where from hooks
3882 $parameters = array('socid' => $socid, 'type_element' => 'order');
3883 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3884 $sql .= $hookmanager->resPrint;
3885 $resql = $this->db->query($sql);
3886 if ($resql) {
3887 while ($obj = $this->db->fetch_object($resql)) {
3888 $adeduire += (float) $obj->count;
3889 }
3890 } else {
3891 $this->error = $this->db->error();
3892 return -1;
3893 }
3894
3895 $this->stats_commande['qty'] -= $adeduire;
3896 }
3897 }
3898
3899 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3900 $reshook = $hookmanager->executeHooks('loadStatsCustomerOrder', $parameters, $this, $action);
3901 if ($reshook > 0) {
3902 $this->stats_commande = $hookmanager->resArray['stats_commande'];
3903 }
3904 return 1;
3905 } else {
3906 $this->error = $this->db->error();
3907 return -1;
3908 }
3909 }
3910
3911 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3921 public function load_stats_commande_fournisseur($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3922 {
3923 // phpcs:enable
3924 global $user, $hookmanager, $action;
3925
3926 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,";
3927 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3928 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as cd";
3929 $sql .= ", ".$this->db->prefix()."commande_fournisseur as c";
3930 $sql .= ", ".$this->db->prefix()."societe as s";
3931 $sql .= " WHERE c.rowid = cd.fk_commande";
3932 $sql .= " AND c.fk_soc = s.rowid";
3933 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
3934 $sql .= " AND cd.fk_product = ".((int) $this->id);
3935 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3936 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3937 }
3938 if ($socid > 0) {
3939 $sql .= " AND c.fk_soc = ".((int) $socid);
3940 }
3941 if ($filtrestatut != '') {
3942 $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")"; // Peut valoir 0
3943 }
3944 if (!empty($dateofvirtualstock)) {
3945 $sql .= " AND c.date_livraison <= '".$this->db->idate($dateofvirtualstock)."'";
3946 }
3947
3948 $result = $this->db->query($sql);
3949 if ($result) {
3950 $obj = $this->db->fetch_object($result);
3951 $this->stats_commande_fournisseur['suppliers'] = $obj->nb_suppliers;
3952 $this->stats_commande_fournisseur['nb'] = $obj->nb;
3953 $this->stats_commande_fournisseur['rows'] = $obj->nb_rows;
3954 $this->stats_commande_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3955
3956 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3957 $reshook = $hookmanager->executeHooks('loadStatsSupplierOrder', $parameters, $this, $action);
3958 if ($reshook > 0) {
3959 $this->stats_commande_fournisseur = $hookmanager->resArray['stats_commande_fournisseur'];
3960 }
3961
3962 return 1;
3963 } else {
3964 $this->error = $this->db->error().' sql='.$sql;
3965 return -1;
3966 }
3967 }
3968
3969 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3979 public function load_stats_sending($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $filterShipmentStatus = '')
3980 {
3981 // phpcs:enable
3982 global $user, $hookmanager, $action;
3983
3984 $sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,";
3985 $sql .= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty";
3986 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
3987 $sql .= ", ".$this->db->prefix()."commandedet as cd";
3988 $sql .= ", ".$this->db->prefix()."commande as c";
3989 $sql .= ", ".$this->db->prefix()."expedition as e";
3990 $sql .= ", ".$this->db->prefix()."societe as s";
3991 $sql .= " WHERE e.rowid = ed.fk_expedition";
3992 $sql .= " AND c.rowid = cd.fk_commande";
3993 $sql .= " AND e.fk_soc = s.rowid";
3994 $sql .= " AND e.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'expedition').")";
3995 $sql .= " AND ed.fk_elementdet = cd.rowid";
3996 $sql .= " AND cd.fk_product = ".((int) $this->id);
3997 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3998 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = e.fk_soc AND sc.fk_user = ".((int) $user->id);
3999 }
4000 if ($socid > 0) {
4001 $sql .= " AND e.fk_soc = ".((int) $socid);
4002 }
4003 if ($filtrestatut != '') {
4004 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
4005 }
4006 if (!empty($filterShipmentStatus)) {
4007 $sql .= " AND e.fk_statut IN (".$this->db->sanitize($filterShipmentStatus).")";
4008 }
4009
4010 $result = $this->db->query($sql);
4011 if ($result) {
4012 $obj = $this->db->fetch_object($result);
4013 $this->stats_expedition['customers'] = (int) $obj->nb_customers;
4014 $this->stats_expedition['nb'] = (int) $obj->nb;
4015 $this->stats_expedition['rows'] = (int) $obj->nb_rows;
4016 $this->stats_expedition['qty'] = $obj->qty ? (float) $obj->qty : 0;
4017
4018 // if it's a virtual product, maybe it is in sending by extension
4019 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4020 $TFather = $this->getFather();
4021 if (is_array($TFather) && !empty($TFather)) {
4022 foreach ($TFather as &$fatherData) {
4023 $pFather = new Product($this->db);
4024 $pFather->id = $fatherData['id'];
4025 $qtyCoef = $fatherData['qty'];
4026
4027 if ($fatherData['incdec']) {
4028 $pFather->load_stats_sending($socid, $filtrestatut, $forVirtualStock);
4029
4030 if (!empty($pFather->stats_expedition)) {
4031 $this->stats_expedition['customers'] += $pFather->stats_expedition['customers'];
4032 $this->stats_expedition['nb'] += $pFather->stats_expedition['nb'];
4033 $this->stats_expedition['rows'] += $pFather->stats_expedition['rows'];
4034 $this->stats_expedition['qty'] += $pFather->stats_expedition['qty'] * $qtyCoef;
4035 }
4036 }
4037 }
4038 }
4039 }
4040
4041 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock, 'filterShipmentStatus' => $filterShipmentStatus);
4042 $reshook = $hookmanager->executeHooks('loadStatsSending', $parameters, $this, $action);
4043 if ($reshook > 0) {
4044 $this->stats_expedition = $hookmanager->resArray['stats_expedition'];
4045 }
4046
4047 return 1;
4048 } else {
4049 $this->error = $this->db->error();
4050 return -1;
4051 }
4052 }
4053
4054 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4064 public function load_stats_reception($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
4065 {
4066 // phpcs:enable
4067 global $user, $hookmanager, $action;
4068
4069 $sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_suppliers, COUNT(DISTINCT cf.rowid) as nb,";
4070 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4071 $sql .= " FROM ".$this->db->prefix()."receptiondet_batch as fd";
4072 $sql .= ", ".$this->db->prefix()."commande_fournisseur as cf";
4073 $sql .= ", ".$this->db->prefix()."societe as s";
4074 $sql .= " WHERE cf.rowid = fd.fk_element";
4075 $sql .= " AND cf.fk_soc = s.rowid";
4076 $sql .= " AND cf.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
4077 $sql .= " AND fd.fk_product = ".((int) $this->id);
4078 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
4079 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = cf.fk_soc AND sc.fk_user = ".((int) $user->id);
4080 }
4081 if ($socid > 0) {
4082 $sql .= " AND cf.fk_soc = ".((int) $socid);
4083 }
4084 if ($filtrestatut != '') {
4085 $sql .= " AND cf.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
4086 }
4087 if (!empty($dateofvirtualstock)) {
4088 $sql .= " AND fd.datec <= '".$this->db->idate($dateofvirtualstock)."'";
4089 }
4090
4091 $result = $this->db->query($sql);
4092 if ($result) {
4093 $obj = $this->db->fetch_object($result);
4094 $this->stats_reception['suppliers'] = $obj->nb_suppliers;
4095 $this->stats_reception['nb'] = $obj->nb;
4096 $this->stats_reception['rows'] = $obj->nb_rows;
4097 $this->stats_reception['qty'] = $obj->qty ? $obj->qty : 0;
4098
4099 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
4100 $reshook = $hookmanager->executeHooks('loadStatsReception', $parameters, $this, $action);
4101 if ($reshook > 0) {
4102 $this->stats_reception = $hookmanager->resArray['stats_reception'];
4103 }
4104
4105 return 1;
4106 } else {
4107 $this->error = $this->db->error();
4108 return -1;
4109 }
4110 }
4111
4112 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4123 public function load_stats_inproduction($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null, $warehouseid = 0)
4124 {
4125 // phpcs:enable
4126 global $user, $hookmanager, $action;
4127
4128 $serviceStockIsEnabled = isModEnabled("service") && getDolGlobalString('STOCK_SUPPORTS_SERVICES');
4129
4130 $sql = "SELECT COUNT(DISTINCT m.fk_soc) as nb_customers, COUNT(DISTINCT m.rowid) as nb,";
4131 $sql .= " COUNT(mp.rowid) as nb_rows, SUM(mp.qty) as qty, role";
4132 $sql .= " FROM ".$this->db->prefix()."mrp_production as mp";
4133 $sql .= ", ".$this->db->prefix()."mrp_mo as m";
4134 $sql .= " LEFT JOIN ".$this->db->prefix()."societe as s ON s.rowid = m.fk_soc";
4135 $sql .= " WHERE m.rowid = mp.fk_mo";
4136 $sql .= " AND m.entity IN (".getEntity(($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE')) ? 'stock' : 'mrp').")";
4137 $sql .= " AND mp.fk_product = ".((int) $this->id);
4138 $sql .= " AND (mp.disable_stock_change IN (0) OR mp.disable_stock_change IS NULL)";
4139 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
4140 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = m.fk_soc AND sc.fk_user = ".((int) $user->id);
4141 }
4142 if ($socid > 0) {
4143 $sql .= " AND m.fk_soc = ".((int) $socid);
4144 }
4145 if ($filtrestatut != '') {
4146 $sql .= " AND m.status IN (".$this->db->sanitize($filtrestatut).")";
4147 }
4148 if (!empty($dateofvirtualstock)) {
4149 $sql .= " AND m.date_valid <= '".$this->db->idate($dateofvirtualstock)."'"; // better date to code ? end of production ?
4150 }
4151 if (!$serviceStockIsEnabled) {
4152 $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))";
4153 }
4154 if (!empty($warehouseid)) {
4155 $sql .= " AND m.fk_warehouse = ".((int) $warehouseid);
4156 }
4157 $sql .= " GROUP BY role";
4158
4159 if ($warehouseid) {
4160 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
4161 } else {
4162 $this->stats_mrptoconsume['customers'] = 0;
4163 $this->stats_mrptoconsume['nb'] = 0;
4164 $this->stats_mrptoconsume['rows'] = 0;
4165 $this->stats_mrptoconsume['qty'] = 0.0;
4166 $this->stats_mrptoproduce['customers'] = 0;
4167 $this->stats_mrptoproduce['nb'] = 0;
4168 $this->stats_mrptoproduce['rows'] = 0;
4169 $this->stats_mrptoproduce['qty'] = 0.0;
4170 }
4171
4172 $result = $this->db->query($sql);
4173 if ($result) {
4174 while ($obj = $this->db->fetch_object($result)) {
4175 if ($obj->role == 'toconsume' && empty($warehouseid)) {
4176 $this->stats_mrptoconsume['customers'] += (int) $obj->nb_customers;
4177 $this->stats_mrptoconsume['nb'] += (int) $obj->nb;
4178 $this->stats_mrptoconsume['rows'] += (int) $obj->nb_rows;
4179 $this->stats_mrptoconsume['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4180 }
4181 if ($obj->role == 'consumed' && empty($warehouseid)) {
4182 //$this->stats_mrptoconsume['customers'] += $obj->nb_customers;
4183 //$this->stats_mrptoconsume['nb'] += $obj->nb;
4184 //$this->stats_mrptoconsume['rows'] += $obj->nb_rows;
4185 $this->stats_mrptoconsume['qty'] -= ($obj->qty ? (float) $obj->qty : 0.0);
4186 }
4187 if ($obj->role == 'toproduce') {
4188 if ($warehouseid) {
4189 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4190 } else {
4191 $this->stats_mrptoproduce['customers'] += (int) $obj->nb_customers;
4192 $this->stats_mrptoproduce['nb'] += (int) $obj->nb;
4193 $this->stats_mrptoproduce['rows'] += (int) $obj->nb_rows;
4194 $this->stats_mrptoproduce['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4195 }
4196 }
4197 if ($obj->role == 'produced') {
4198 //$this->stats_mrptoproduce['customers'] += $obj->nb_customers;
4199 //$this->stats_mrptoproduce['nb'] += $obj->nb;
4200 //$this->stats_mrptoproduce['rows'] += $obj->nb_rows;
4201 if ($warehouseid) {
4202 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
4203 } else {
4204 $this->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
4205 }
4206 }
4207 }
4208
4209 // Clean data
4210 if ($warehouseid) {
4211 if ($this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] < 0) {
4212 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
4213 }
4214 } else {
4215 if ($this->stats_mrptoconsume['qty'] < 0) {
4216 $this->stats_mrptoconsume['qty'] = 0;
4217 }
4218 if ($this->stats_mrptoproduce['qty'] < 0) {
4219 $this->stats_mrptoproduce['qty'] = 0;
4220 }
4221 }
4222
4223 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
4224 $reshook = $hookmanager->executeHooks('loadStatsInProduction', $parameters, $this, $action);
4225 if ($reshook > 0) {
4226 $this->stats_mrptoproduce = $hookmanager->resArray['stats_mrptoproduce'];
4227 }
4228
4229 return 1;
4230 } else {
4231 $this->error = $this->db->error();
4232 return -1;
4233 }
4234 }
4235
4236 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4243 public function load_stats_contrat($socid = 0)
4244 {
4245 // phpcs:enable
4246 global $user, $hookmanager, $action;
4247
4248 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
4249 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
4250 $sql .= " FROM ".$this->db->prefix()."contratdet as cd";
4251 $sql .= ", ".$this->db->prefix()."contrat as c";
4252 $sql .= ", ".$this->db->prefix()."societe as s";
4253 $sql .= " WHERE c.rowid = cd.fk_contrat";
4254 $sql .= " AND c.fk_soc = s.rowid";
4255 $sql .= " AND c.entity IN (".getEntity('contract').")";
4256 $sql .= " AND cd.fk_product = ".((int) $this->id);
4257 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4258 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
4259 }
4260 //$sql.= " AND c.statut != 0";
4261 if ($socid > 0) {
4262 $sql .= " AND c.fk_soc = ".((int) $socid);
4263 }
4264
4265 $result = $this->db->query($sql);
4266 if ($result) {
4267 $obj = $this->db->fetch_object($result);
4268 $this->stats_contrat['customers'] = (int) $obj->nb_customers;
4269 $this->stats_contrat['nb'] = (int) $obj->nb;
4270 $this->stats_contrat['rows'] = (int) $obj->nb_rows;
4271 $this->stats_contrat['qty'] = $obj->qty ? (float) $obj->qty : 0;
4272
4273 // if it's a virtual product, maybe it is in contract by extension
4274 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4275 $TFather = $this->getFather();
4276 if (is_array($TFather) && !empty($TFather)) {
4277 foreach ($TFather as &$fatherData) {
4278 $pFather = new Product($this->db);
4279 $pFather->id = $fatherData['id'];
4280 $qtyCoef = $fatherData['qty'];
4281
4282 if ($fatherData['incdec']) {
4283 $pFather->load_stats_contrat($socid);
4284
4285 if (!empty($pFather->stats_contrat)) {
4286 $this->stats_contrat['customers'] += $pFather->stats_contrat['customers'];
4287 $this->stats_contrat['nb'] += $pFather->stats_contrat['nb'];
4288 $this->stats_contrat['rows'] += $pFather->stats_contrat['rows'];
4289 $this->stats_contrat['qty'] += $pFather->stats_contrat['qty'] * $qtyCoef;
4290 }
4291 }
4292 }
4293 }
4294 }
4295
4296 $parameters = array('socid' => $socid);
4297 $reshook = $hookmanager->executeHooks('loadStatsContract', $parameters, $this, $action);
4298 if ($reshook > 0) {
4299 $this->stats_contrat = $hookmanager->resArray['stats_contrat'];
4300 }
4301
4302 return 1;
4303 } else {
4304 $this->error = $this->db->error().' sql='.$sql;
4305 return -1;
4306 }
4307 }
4308
4309 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4316 public function load_stats_facture($socid = 0)
4317 {
4318 // phpcs:enable
4319 global $user, $hookmanager, $action;
4320
4321 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4322 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(".$this->db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1').") as qty";
4323 $sql .= " FROM ".$this->db->prefix()."facturedet as fd";
4324 $sql .= ", ".$this->db->prefix()."facture as f";
4325 $sql .= ", ".$this->db->prefix()."societe as s";
4326 $sql .= " WHERE f.rowid = fd.fk_facture";
4327 $sql .= " AND f.fk_soc = s.rowid";
4328 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4329 $sql .= " AND fd.fk_product = ".((int) $this->id);
4330 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4331 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4332 }
4333 //$sql.= " AND f.fk_statut != 0";
4334 if ($socid > 0) {
4335 $sql .= " AND f.fk_soc = ".((int) $socid);
4336 }
4337
4338 $result = $this->db->query($sql);
4339 if ($result) {
4340 $obj = $this->db->fetch_object($result);
4341 $this->stats_facture['customers'] = (int) $obj->nb_customers;
4342 $this->stats_facture['nb'] = (int) $obj->nb;
4343 $this->stats_facture['rows'] = (int) $obj->nb_rows;
4344 $this->stats_facture['qty'] = $obj->qty ? (float) $obj->qty : 0;
4345
4346 // if it's a virtual product, maybe it is in invoice by extension
4347 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4348 $TFather = $this->getFather();
4349 if (is_array($TFather) && !empty($TFather)) {
4350 foreach ($TFather as &$fatherData) {
4351 $pFather = new Product($this->db);
4352 $pFather->id = $fatherData['id'];
4353 $qtyCoef = $fatherData['qty'];
4354
4355 if ($fatherData['incdec']) {
4356 $pFather->load_stats_facture($socid);
4357
4358 if (!empty($pFather->stats_facture)) {
4359 $this->stats_facture['customers'] += $pFather->stats_facture['customers'];
4360 $this->stats_facture['nb'] += $pFather->stats_facture['nb'];
4361 $this->stats_facture['rows'] += $pFather->stats_facture['rows'];
4362 $this->stats_facture['qty'] += $pFather->stats_facture['qty'] * $qtyCoef;
4363 }
4364 }
4365 }
4366 }
4367 }
4368
4369 $parameters = array('socid' => $socid);
4370 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoice', $parameters, $this, $action);
4371 if ($reshook > 0) {
4372 $this->stats_facture = $hookmanager->resArray['stats_facture'];
4373 }
4374
4375 return 1;
4376 } else {
4377 $this->error = $this->db->error();
4378 return -1;
4379 }
4380 }
4381
4382
4383 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4390 public function load_stats_facturerec($socid = 0)
4391 {
4392 // phpcs:enable
4393 global $user, $hookmanager, $action;
4394
4395 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4396 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4397 $sql .= " FROM ".MAIN_DB_PREFIX."facturedet_rec as fd";
4398 $sql .= ", ".MAIN_DB_PREFIX."facture_rec as f";
4399 $sql .= ", ".MAIN_DB_PREFIX."societe as s";
4400 $sql .= " WHERE f.rowid = fd.fk_facture";
4401 $sql .= " AND f.fk_soc = s.rowid";
4402 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4403 $sql .= " AND fd.fk_product = ".((int) $this->id);
4404 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4405 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4406 }
4407 //$sql.= " AND f.fk_statut != 0";
4408 if ($socid > 0) {
4409 $sql .= " AND f.fk_soc = ".((int) $socid);
4410 }
4411
4412 $result = $this->db->query($sql);
4413 if ($result) {
4414 $obj = $this->db->fetch_object($result);
4415 $this->stats_facturerec['customers'] = (int) $obj->nb_customers;
4416 $this->stats_facturerec['nb'] = (int) $obj->nb;
4417 $this->stats_facturerec['rows'] = (int) $obj->nb_rows;
4418 $this->stats_facturerec['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4419
4420 // if it's a virtual product, maybe it is in invoice by extension
4421 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4422 $TFather = $this->getFather();
4423 if (is_array($TFather) && !empty($TFather)) {
4424 foreach ($TFather as &$fatherData) {
4425 $pFather = new Product($this->db);
4426 $pFather->id = $fatherData['id'];
4427 $qtyCoef = $fatherData['qty'];
4428
4429 if ($fatherData['incdec']) {
4430 $pFather->load_stats_facture($socid);
4431
4432 $this->stats_facturerec['customers'] += $pFather->stats_facturerec['customers'];
4433 $this->stats_facturerec['nb'] += $pFather->stats_facturerec['nb'];
4434 $this->stats_facturerec['rows'] += $pFather->stats_facturerec['rows'];
4435 $this->stats_facturerec['qty'] += $pFather->stats_facturerec['qty'] * $qtyCoef;
4436 }
4437 }
4438 }
4439 }
4440
4441 $parameters = array('socid' => $socid);
4442 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoiceRec', $parameters, $this, $action);
4443 if ($reshook > 0) {
4444 $this->stats_facturerec = $hookmanager->resArray['stats_facturerec'];
4445 }
4446
4447 return 1;
4448 } else {
4449 $this->error = $this->db->error();
4450 return -1;
4451 }
4452 }
4453
4454 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4461 public function load_stats_facture_fournisseur($socid = 0)
4462 {
4463 // phpcs:enable
4464 global $user, $hookmanager, $action;
4465
4466 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
4467 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4468 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as fd";
4469 $sql .= ", ".$this->db->prefix()."facture_fourn as f";
4470 $sql .= ", ".$this->db->prefix()."societe as s";
4471 $sql .= " WHERE f.rowid = fd.fk_facture_fourn";
4472 $sql .= " AND f.fk_soc = s.rowid";
4473 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4474 $sql .= " AND fd.fk_product = ".((int) $this->id);
4475 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4476 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4477 }
4478 //$sql.= " AND f.fk_statut != 0";
4479 if ($socid > 0) {
4480 $sql .= " AND f.fk_soc = ".((int) $socid);
4481 }
4482
4483 $result = $this->db->query($sql);
4484 if ($result) {
4485 $obj = $this->db->fetch_object($result);
4486 $this->stats_facture_fournisseur['suppliers'] = (int) $obj->nb_suppliers;
4487 $this->stats_facture_fournisseur['nb'] = (int) $obj->nb;
4488 $this->stats_facture_fournisseur['rows'] = (int) $obj->nb_rows;
4489 $this->stats_facture_fournisseur['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4490
4491 $parameters = array('socid' => $socid);
4492 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoice', $parameters, $this, $action);
4493 if ($reshook > 0) {
4494 $this->stats_facture_fournisseur = $hookmanager->resArray['stats_facture_fournisseur'];
4495 }
4496
4497 return 1;
4498 } else {
4499 $this->error = $this->db->error();
4500 return -1;
4501 }
4502 }
4503
4504 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4511 public function load_stats_facturefournrec($socid = 0)
4512 {
4513 // phpcs:enable
4514 global $user, $hookmanager, $action;
4515
4516 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
4517 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4518 $sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn_det_rec as fd";
4519 $sql .= ", ".MAIN_DB_PREFIX."facture_fourn_rec as f";
4520 $sql .= ", ".MAIN_DB_PREFIX."societe as s";
4521 if (!$user->hasRight('societe', 'client', 'voir')) {
4522 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
4523 }
4524 $sql .= " WHERE f.rowid = fd.fk_facture";
4525 $sql .= " AND f.fk_soc = s.rowid";
4526 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4527 $sql .= " AND fd.fk_product = ".((int) $this->id);
4528 if (!$user->hasRight('societe', 'client', 'voir')) {
4529 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4530 }
4531 //$sql.= " AND f.fk_statut != 0";
4532 if ($socid > 0) {
4533 $sql .= " AND f.fk_soc = ".((int) $socid);
4534 }
4535
4536 $result = $this->db->query($sql);
4537 if ($result) {
4538 $obj = $this->db->fetch_object($result);
4539 $this->stats_facturefournrec['suppliers'] = (int) $obj->nb_suppliers;
4540 $this->stats_facturefournrec['nb'] = (int) $obj->nb;
4541 $this->stats_facturefournrec['rows'] = (int) $obj->nb_rows;
4542 $this->stats_facturefournrec['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4543
4544 $parameters = array('socid' => $socid);
4545 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoiceRec', $parameters, $this, $action);
4546 if ($reshook > 0) {
4547 $this->stats_facturefournrec = $hookmanager->resArray['stats_facturefournrec'];
4548 }
4549
4550 return 1;
4551 } else {
4552 $this->error = $this->db->error();
4553 return -1;
4554 }
4555 }
4556
4557 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4566 private function _get_stats($sql, $mode, $year = 0)
4567 {
4568 // phpcs:enable
4569 $tab = array();
4570
4571 $resql = $this->db->query($sql);
4572 if ($resql) {
4573 $num = $this->db->num_rows($resql);
4574 $i = 0;
4575 while ($i < $num) {
4576 $arr = $this->db->fetch_array($resql);
4577 if (is_array($arr)) {
4578 $keyfortab = (string) $arr[1];
4579 if ($year == -1) {
4580 $keyfortab = substr($keyfortab, -2);
4581 }
4582
4583 if ($mode == 'byunit') {
4584 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[0]; // 1st field
4585 } elseif ($mode == 'bynumber') {
4586 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4587 } elseif ($mode == 'byamount') {
4588 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4589 } else {
4590 // Bad value for $mode
4591 return -1;
4592 }
4593 }
4594 $i++;
4595 }
4596 } else {
4597 $this->error = $this->db->error().' sql='.$sql;
4598 return -1;
4599 }
4600
4601 if (empty($year)) {
4602 $year = dol_print_date(time(), '%Y');
4603 $month = dol_print_date(time(), '%m');
4604 } elseif ($year == -1) {
4605 $year = '';
4606 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4607 } else {
4608 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4609 }
4610
4611 $result = array();
4612
4613 for ($j = 0; $j < 12; $j++) {
4614 // $idx is 'D', 'N', 'O', 'S', ... (First letter of month in user language)
4615 $idx = ucfirst(dol_trunc(dol_print_date(dol_mktime(12, 0, 0, $month, 1, 1970), "%b"), 1, 'right', 'UTF-8', 1));
4616
4617 // print $idx.'-'.$year.'-'.$month.'<br>';
4618 $result[$j] = array($idx, isset($tab[$year.$month]) ? $tab[$year.$month] : 0);
4619 // $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0);
4620
4621 $month = "0".($month - 1);
4622 if (dol_strlen($month) == 3) {
4623 $month = substr($month, 1);
4624 }
4625 if ($month == 0) {
4626 $month = 12;
4627 $year -= 1;
4628 }
4629 }
4630
4631 return array_reverse($result);
4632 }
4633
4634
4635 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4646 public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4647 {
4648 // phpcs:enable
4649 global $user;
4650
4651 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4652 if ($mode == 'bynumber') {
4653 $sql .= ", count(DISTINCT f.rowid)";
4654 }
4655 $sql .= ", sum(d.total_ht) as total_ht";
4656 $sql .= " FROM ".$this->db->prefix()."facturedet as d, ".$this->db->prefix()."facture as f, ".$this->db->prefix()."societe as s";
4657 if ($filteronproducttype >= 0) {
4658 $sql .= ", ".$this->db->prefix()."product as p";
4659 }
4660 if (!$user->hasRight('societe', 'client', 'voir')) {
4661 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4662 }
4663 $sql .= " WHERE f.rowid = d.fk_facture";
4664 if ($this->id > 0) {
4665 $sql .= " AND d.fk_product = ".((int) $this->id);
4666 } else {
4667 $sql .= " AND d.fk_product > 0";
4668 }
4669 if ($filteronproducttype >= 0) {
4670 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4671 }
4672 $sql .= " AND f.fk_soc = s.rowid";
4673 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4674 if (!$user->hasRight('societe', 'client', 'voir')) {
4675 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4676 }
4677 if ($socid > 0) {
4678 $sql .= " AND f.fk_soc = ".((int) $socid);
4679 }
4680 $sql .= $morefilter;
4681 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4682 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4683
4684 return $this->_get_stats($sql, $mode, $year);
4685 }
4686
4687
4688 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4699 public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4700 {
4701 // phpcs:enable
4702 global $user;
4703
4704 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4705 if ($mode == 'bynumber') {
4706 $sql .= ", count(DISTINCT f.rowid)";
4707 }
4708 $sql .= ", sum(d.total_ht) as total_ht";
4709 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as d, ".$this->db->prefix()."facture_fourn as f, ".$this->db->prefix()."societe as s";
4710 if ($filteronproducttype >= 0) {
4711 $sql .= ", ".$this->db->prefix()."product as p";
4712 }
4713 if (!$user->hasRight('societe', 'client', 'voir')) {
4714 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4715 }
4716 $sql .= " WHERE f.rowid = d.fk_facture_fourn";
4717 if ($this->id > 0) {
4718 $sql .= " AND d.fk_product = ".((int) $this->id);
4719 } else {
4720 $sql .= " AND d.fk_product > 0";
4721 }
4722 if ($filteronproducttype >= 0) {
4723 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4724 }
4725 $sql .= " AND f.fk_soc = s.rowid";
4726 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4727 if (!$user->hasRight('societe', 'client', 'voir')) {
4728 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4729 }
4730 if ($socid > 0) {
4731 $sql .= " AND f.fk_soc = ".((int) $socid);
4732 }
4733 $sql .= $morefilter;
4734 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4735 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4736
4737 return $this->_get_stats($sql, $mode, $year);
4738 }
4739
4740 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4751 public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4752 {
4753 // phpcs:enable
4754 global $user, $hookmanager;
4755
4756 $sql = "SELECT sum(d.qty) as qty, date_format(p.datep, '%Y%m')";
4757 if ($mode == 'bynumber') {
4758 $sql .= ", count(DISTINCT p.rowid)";
4759 }
4760 $sql .= ", sum(d.total_ht) as total_ht";
4761 $sql .= " FROM ".$this->db->prefix()."propaldet as d, ".$this->db->prefix()."propal as p, ".$this->db->prefix()."societe as s";
4762 if ($filteronproducttype >= 0) {
4763 $sql .= ", ".$this->db->prefix()."product as prod";
4764 }
4765 if (!$user->hasRight('societe', 'client', 'voir')) {
4766 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4767 }
4768 $sql .= " WHERE p.rowid = d.fk_propal";
4769 if ($this->id > 0) {
4770 $sql .= " AND d.fk_product = ".((int) $this->id);
4771 } else {
4772 $sql .= " AND d.fk_product > 0";
4773 }
4774 if ($filteronproducttype >= 0) {
4775 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4776 }
4777 $sql .= " AND p.fk_soc = s.rowid";
4778 $sql .= " AND p.entity IN (".getEntity('propal').")";
4779 if (!$user->hasRight('societe', 'client', 'voir')) {
4780 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4781 }
4782 if ($socid > 0) {
4783 $sql .= " AND p.fk_soc = ".((int) $socid);
4784 }
4785 $sql .= $morefilter;
4786 $parameters = array('socid' => $user->socid);
4787 $hookmanager->executeHooks('productGetNbPropal', $parameters, $this); // Note that $action and $object may have been modified by hook
4788 $sql .= $hookmanager->resPrint;
4789 $sql .= " GROUP BY date_format(p.datep,'%Y%m')";
4790 $sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC";
4791 return $this->_get_stats($sql, $mode, $year);
4792 }
4793
4794 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4805 public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4806 {
4807 // phpcs:enable
4808 global $user;
4809
4810 $sql = "SELECT sum(d.qty) as qty, date_format(p.date_valid, '%Y%m')";
4811 if ($mode == 'bynumber') {
4812 $sql .= ", count(DISTINCT p.rowid)";
4813 }
4814 $sql .= ", sum(d.total_ht) as total_ht";
4815 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as d, ".$this->db->prefix()."supplier_proposal as p, ".$this->db->prefix()."societe as s";
4816 if ($filteronproducttype >= 0) {
4817 $sql .= ", ".$this->db->prefix()."product as prod";
4818 }
4819 if (!$user->hasRight('societe', 'client', 'voir')) {
4820 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4821 }
4822 $sql .= " WHERE p.rowid = d.fk_supplier_proposal";
4823 if ($this->id > 0) {
4824 $sql .= " AND d.fk_product = ".((int) $this->id);
4825 } else {
4826 $sql .= " AND d.fk_product > 0";
4827 }
4828 if ($filteronproducttype >= 0) {
4829 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4830 }
4831 $sql .= " AND p.fk_soc = s.rowid";
4832 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
4833 if (!$user->hasRight('societe', 'client', 'voir')) {
4834 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4835 }
4836 if ($socid > 0) {
4837 $sql .= " AND p.fk_soc = ".((int) $socid);
4838 }
4839 $sql .= $morefilter;
4840 $sql .= " GROUP BY date_format(p.date_valid,'%Y%m')";
4841 $sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
4842
4843 return $this->_get_stats($sql, $mode, $year);
4844 }
4845
4846 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4857 public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4858 {
4859 // phpcs:enable
4860 global $user, $hookmanager;
4861
4862 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4863 if ($mode == 'bynumber') {
4864 $sql .= ", count(DISTINCT c.rowid)";
4865 }
4866 $sql .= ", sum(d.total_ht) as total_ht";
4867 $sql .= " FROM ".$this->db->prefix()."commandedet as d, ".$this->db->prefix()."commande as c, ".$this->db->prefix()."societe as s";
4868 if ($filteronproducttype >= 0) {
4869 $sql .= ", ".$this->db->prefix()."product as p";
4870 }
4871 if (!$user->hasRight('societe', 'client', 'voir')) {
4872 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4873 }
4874 $sql .= " WHERE c.rowid = d.fk_commande";
4875 if ($this->id > 0) {
4876 $sql .= " AND d.fk_product = ".((int) $this->id);
4877 } else {
4878 $sql .= " AND d.fk_product > 0";
4879 }
4880 if ($filteronproducttype >= 0) {
4881 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4882 }
4883 $sql .= " AND c.fk_soc = s.rowid";
4884 $sql .= " AND c.entity IN (".getEntity('commande').")";
4885 if (!$user->hasRight('societe', 'client', 'voir')) {
4886 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4887 }
4888 if ($socid > 0) {
4889 $sql .= " AND c.fk_soc = ".((int) $socid);
4890 }
4891 $sql .= $morefilter;
4892 $parameters = array('socid' => $user->socid);
4893 $hookmanager->executeHooks('productGetNbOrder', $parameters, $this); // Note that $action and $object may have been modified by hook
4894 $sql .= $hookmanager->resPrint;
4895 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4896 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4897
4898 return $this->_get_stats($sql, $mode, $year);
4899 }
4900
4901 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4912 public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4913 {
4914 // phpcs:enable
4915 global $user;
4916
4917 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4918 if ($mode == 'bynumber') {
4919 $sql .= ", count(DISTINCT c.rowid)";
4920 }
4921 $sql .= ", sum(d.total_ht) as total_ht";
4922 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as d, ".$this->db->prefix()."commande_fournisseur as c, ".$this->db->prefix()."societe as s";
4923 if ($filteronproducttype >= 0) {
4924 $sql .= ", ".$this->db->prefix()."product as p";
4925 }
4926 if (!$user->hasRight('societe', 'client', 'voir')) {
4927 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4928 }
4929 $sql .= " WHERE c.rowid = d.fk_commande";
4930 if ($this->id > 0) {
4931 $sql .= " AND d.fk_product = ".((int) $this->id);
4932 } else {
4933 $sql .= " AND d.fk_product > 0";
4934 }
4935 if ($filteronproducttype >= 0) {
4936 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4937 }
4938 $sql .= " AND c.fk_soc = s.rowid";
4939 $sql .= " AND c.entity IN (".getEntity('supplier_order').")";
4940 if (!$user->hasRight('societe', 'client', 'voir')) {
4941 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4942 }
4943 if ($socid > 0) {
4944 $sql .= " AND c.fk_soc = ".((int) $socid);
4945 }
4946 $sql .= $morefilter;
4947 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4948 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4949
4950 return $this->_get_stats($sql, $mode, $year);
4951 }
4952
4953 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4964 public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4965 {
4966 // phpcs:enable
4967 global $user;
4968
4969 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_contrat, '%Y%m')";
4970 if ($mode == 'bynumber') {
4971 $sql .= ", count(DISTINCT c.rowid)";
4972 }
4973 $sql .= ", sum(d.total_ht) as total_ht";
4974 $sql .= " FROM ".$this->db->prefix()."contratdet as d, ".$this->db->prefix()."contrat as c, ".$this->db->prefix()."societe as s";
4975 if ($filteronproducttype >= 0) {
4976 $sql .= ", ".$this->db->prefix()."product as p";
4977 }
4978 if (!$user->hasRight('societe', 'client', 'voir')) {
4979 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4980 }
4981 $sql .= " WHERE c.entity IN (".getEntity('contract').")";
4982 $sql .= " AND c.rowid = d.fk_contrat";
4983
4984 if ($this->id > 0) {
4985 $sql .= " AND d.fk_product = ".((int) $this->id);
4986 } else {
4987 $sql .= " AND d.fk_product > 0";
4988 }
4989 if ($filteronproducttype >= 0) {
4990 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4991 }
4992 $sql .= " AND c.fk_soc = s.rowid";
4993
4994 if (!$user->hasRight('societe', 'client', 'voir')) {
4995 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4996 }
4997 if ($socid > 0) {
4998 $sql .= " AND c.fk_soc = ".((int) $socid);
4999 }
5000 $sql .= $morefilter;
5001 $sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')";
5002 $sql .= " ORDER BY date_format(c.date_contrat,'%Y%m') DESC";
5003
5004 return $this->_get_stats($sql, $mode, $year);
5005 }
5006
5007 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5018 public function get_nb_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
5019 {
5020 // phpcs:enable
5021 global $user;
5022
5023 $sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')";
5024 if ($mode == 'bynumber') {
5025 $sql .= ", count(DISTINCT d.rowid)";
5026 }
5027 $sql .= " FROM ".$this->db->prefix()."mrp_mo as d LEFT JOIN ".$this->db->prefix()."societe as s ON d.fk_soc = s.rowid";
5028 if ($filteronproducttype >= 0) {
5029 $sql .= ", ".$this->db->prefix()."product as p";
5030 }
5031 if (!$user->hasRight('societe', 'client', 'voir')) {
5032 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
5033 }
5034
5035 $sql .= " WHERE d.entity IN (".getEntity('mo').")";
5036 $sql .= " AND d.status > 0";
5037
5038 if ($this->id > 0) {
5039 $sql .= " AND d.fk_product = ".((int) $this->id);
5040 } else {
5041 $sql .= " AND d.fk_product > 0";
5042 }
5043 if ($filteronproducttype >= 0) {
5044 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
5045 }
5046
5047 if (!$user->hasRight('societe', 'client', 'voir')) {
5048 $sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
5049 }
5050 if ($socid > 0) {
5051 $sql .= " AND d.fk_soc = ".((int) $socid);
5052 }
5053 $sql .= $morefilter;
5054 $sql .= " GROUP BY date_format(d.date_valid,'%Y%m')";
5055 $sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC";
5056
5057 return $this->_get_stats($sql, $mode, $year);
5058 }
5059
5060 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5071 public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
5072 {
5073 // phpcs:enable
5074 global $user;
5075
5076 // Clean parameters
5077 if (!is_numeric($id_pere)) {
5078 $id_pere = 0;
5079 }
5080 if (!is_numeric($id_fils)) {
5081 $id_fils = 0;
5082 }
5083 if (!is_numeric($incdec)) {
5084 $incdec = 0;
5085 }
5086
5087 $result = $this->del_sousproduit($id_pere, $id_fils);
5088 if ($result < 0) {
5089 return $result;
5090 }
5091
5092 // Check not already father of id_pere (to avoid father -> child -> father links)
5093 $sql = "SELECT fk_product_pere from ".$this->db->prefix()."product_association";
5094 $sql .= " WHERE fk_product_pere = ".((int) $id_fils)." AND fk_product_fils = ".((int) $id_pere);
5095 if (!$this->db->query($sql)) {
5096 dol_print_error($this->db);
5097 return -1;
5098 } else {
5099 //Selection of the highest row
5100 $sql = "SELECT MAX(rang) as max_rank FROM ".$this->db->prefix()."product_association";
5101 $sql .= " WHERE fk_product_pere = ".((int) $id_pere);
5102 $resql = $this->db->query($sql);
5103 if ($resql) {
5104 $obj = $this->db->fetch_object($resql);
5105 $rank = $obj->max_rank + 1;
5106 //Addition of a product with the highest rank +1
5107 $sql = "INSERT INTO ".$this->db->prefix()."product_association(fk_product_pere,fk_product_fils,qty,incdec,rang)";
5108 $sql .= " VALUES (".((int) $id_pere).", ".((int) $id_fils).", ".price2num($qty, 'MS').", ".((int) $incdec).", ".((int) $rank).")";
5109 if (! $this->db->query($sql)) {
5110 dol_print_error($this->db);
5111 return -1;
5112 } else {
5113 if (!$notrigger) {
5114 // Call trigger
5115 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_ADD', $user);
5116 if ($result < 0) {
5117 $this->error = $this->db->lasterror();
5118 dol_syslog(get_class($this).'::addSubproduct error='.$this->error, LOG_ERR);
5119 return -1;
5120 }
5121 }
5122 // End call triggers
5123
5124 return 1;
5125 }
5126 } else {
5127 dol_print_error($this->db);
5128 return -1;
5129 }
5130 }
5131 }
5132
5133 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5144 public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
5145 {
5146 // phpcs:enable
5147 global $user;
5148
5149 // Clean parameters
5150 if (!is_numeric($id_pere)) {
5151 $id_pere = 0;
5152 }
5153 if (!is_numeric($id_fils)) {
5154 $id_fils = 0;
5155 }
5156 if (!is_numeric($incdec)) {
5157 $incdec = 1;
5158 }
5159 if (!is_numeric($qty)) {
5160 $qty = 1;
5161 }
5162
5163 $sql = 'UPDATE '.$this->db->prefix().'product_association SET ';
5164 $sql .= 'qty = '.price2num($qty, 'MS');
5165 $sql .= ',incdec = '.((int) $incdec);
5166 $sql .= ' WHERE fk_product_pere = '.((int) $id_pere).' AND fk_product_fils = '.((int) $id_fils);
5167
5168 if (!$this->db->query($sql)) {
5169 dol_print_error($this->db);
5170 return -1;
5171 } else {
5172 if (!$notrigger) {
5173 // Call trigger
5174 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_UPDATE', $user);
5175 if ($result < 0) {
5176 $this->error = $this->db->lasterror();
5177 dol_syslog(get_class($this).'::updateSubproduct error='.$this->error, LOG_ERR);
5178 return -1;
5179 }
5180 // End call triggers
5181 }
5182
5183 return 1;
5184 }
5185 }
5186
5187 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5196 public function del_sousproduit($fk_parent, $fk_child, $notrigger = 0)
5197 {
5198 // phpcs:enable
5199 global $user;
5200
5201 if (!is_numeric($fk_parent)) {
5202 $fk_parent = 0;
5203 }
5204 if (!is_numeric($fk_child)) {
5205 $fk_child = 0;
5206 }
5207
5208 $sql = "DELETE FROM ".$this->db->prefix()."product_association";
5209 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5210 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5211
5212 dol_syslog(get_class($this).'::del_sousproduit', LOG_DEBUG);
5213 if (!$this->db->query($sql)) {
5214 dol_print_error($this->db);
5215 return -1;
5216 }
5217
5218 // Updated ranks so that none are missing
5219 $sqlrank = "SELECT rowid, rang FROM ".$this->db->prefix()."product_association";
5220 $sqlrank .= " WHERE fk_product_pere = ".((int) $fk_parent);
5221 $sqlrank .= " ORDER BY rang";
5222 $resqlrank = $this->db->query($sqlrank);
5223 if ($resqlrank) {
5224 $cpt = 0;
5225 while ($objrank = $this->db->fetch_object($resqlrank)) {
5226 $cpt++;
5227 $sql = "UPDATE ".$this->db->prefix()."product_association";
5228 $sql .= " SET rang = ".((int) $cpt);
5229 $sql .= " WHERE rowid = ".((int) $objrank->rowid);
5230 if (! $this->db->query($sql)) {
5231 dol_print_error($this->db);
5232 return -1;
5233 }
5234 }
5235 }
5236
5237 if (!$notrigger) {
5238 // Call trigger
5239 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_DELETE', $user);
5240 if ($result < 0) {
5241 $this->error = $this->db->lasterror();
5242 dol_syslog(get_class($this).'::delSubproduct error='.$this->error, LOG_ERR);
5243 return -1;
5244 }
5245 // End call triggers
5246 }
5247
5248 return 1;
5249 }
5250
5251 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5259 public function is_sousproduit($fk_parent, $fk_child)
5260 {
5261 // phpcs:enable
5262 $sql = "SELECT fk_product_pere, qty, incdec";
5263 $sql .= " FROM ".$this->db->prefix()."product_association";
5264 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5265 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5266
5267 $result = $this->db->query($sql);
5268 if ($result) {
5269 $num = $this->db->num_rows($result);
5270
5271 if ($num > 0) {
5272 $obj = $this->db->fetch_object($result);
5273
5274 $this->is_sousproduit_qty = $obj->qty;
5275 $this->is_sousproduit_incdec = $obj->incdec;
5276
5277 return 1;
5278 } else {
5279 return 0;
5280 }
5281 } else {
5282 dol_print_error($this->db);
5283 return -1;
5284 }
5285 }
5286
5287
5288 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5299 public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
5300 {
5301 // phpcs:enable
5302 global $conf;
5303
5304 $now = dol_now();
5305
5306 dol_syslog(get_class($this)."::add_fournisseur id_fourn = ".$id_fourn." ref_fourn=".$ref_fourn." quantity=".$quantity, LOG_DEBUG);
5307
5308 // Clean parameters
5309 $quantity = price2num($quantity, 'MS');
5310
5311 if ($ref_fourn) {
5312 // Check if ref is not already used
5313 $sql = "SELECT rowid, fk_product";
5314 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5315 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5316 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5317 $sql .= " AND fk_product <> ".((int) $this->id);
5318 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5319
5320 $resql = $this->db->query($sql);
5321 if ($resql) {
5322 $obj = $this->db->fetch_object($resql);
5323 if ($obj) {
5324 // If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
5325 $this->product_id_already_linked = $obj->fk_product;
5326 return -3;
5327 }
5328 $this->db->free($resql);
5329 }
5330 }
5331
5332 $sql = "SELECT rowid";
5333 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5334 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5335 if ($ref_fourn) {
5336 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5337 } else {
5338 $sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
5339 }
5340 $sql .= " AND quantity = ".((float) $quantity);
5341 $sql .= " AND fk_product = ".((int) $this->id);
5342 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5343
5344 $resql = $this->db->query($sql);
5345 if ($resql) {
5346 $obj = $this->db->fetch_object($resql);
5347
5348 // The reference supplier does not exist, we create it for this product.
5349 if (empty($obj)) {
5350 $sql = "INSERT INTO ".$this->db->prefix()."product_fournisseur_price(";
5351 $sql .= "datec";
5352 $sql .= ", entity";
5353 $sql .= ", fk_product";
5354 $sql .= ", fk_soc";
5355 $sql .= ", ref_fourn";
5356 $sql .= ", quantity";
5357 $sql .= ", fk_user";
5358 $sql .= ", tva_tx";
5359 $sql .= ") VALUES (";
5360 $sql .= "'".$this->db->idate($now)."'";
5361 $sql .= ", ".((int) $conf->entity);
5362 $sql .= ", ".((int) $this->id);
5363 $sql .= ", ".((int) $id_fourn);
5364 $sql .= ", '".$this->db->escape($ref_fourn)."'";
5365 $sql .= ", ".((float) $quantity);
5366 $sql .= ", ".((int) $user->id);
5367 $sql .= ", 0";
5368 $sql .= ")";
5369
5370 if ($this->db->query($sql)) {
5371 $this->product_fourn_price_id = $this->db->last_insert_id($this->db->prefix()."product_fournisseur_price");
5372 return 1;
5373 } else {
5374 $this->error = $this->db->lasterror();
5375 return -1;
5376 }
5377 } else {
5378 // If the supplier price already exists for this product and quantity
5379 $this->product_fourn_price_id = $obj->rowid;
5380 return 0;
5381 }
5382 } else {
5383 $this->error = $this->db->lasterror();
5384 return -2;
5385 }
5386 }
5387
5388
5389 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5395 public function list_suppliers()
5396 {
5397 // phpcs:enable
5398 global $conf;
5399
5400 $list = array();
5401
5402 $sql = "SELECT DISTINCT p.fk_soc";
5403 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as p";
5404 $sql .= " WHERE p.fk_product = ".((int) $this->id);
5405 $sql .= " AND p.entity = ".((int) $conf->entity);
5406
5407 $result = $this->db->query($sql);
5408 if ($result) {
5409 $num = $this->db->num_rows($result);
5410 $i = 0;
5411 while ($i < $num) {
5412 $obj = $this->db->fetch_object($result);
5413 $list[$i] = $obj->fk_soc;
5414 $i++;
5415 }
5416 }
5417
5418 return $list;
5419 }
5420
5421 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5429 public function clone_price($fromId, $toId)
5430 {
5431 // phpcs:enable
5432 global $user;
5433
5434 $now = dol_now();
5435
5436 $this->db->begin();
5437
5438 // prices
5439 $sql = "INSERT INTO ".$this->db->prefix()."product_price (";
5440 $sql .= " entity";
5441 $sql .= ", fk_product";
5442 $sql .= ", date_price";
5443 $sql .= ", price_level";
5444 $sql .= ", price";
5445 $sql .= ", price_ttc";
5446 $sql .= ", price_min";
5447 $sql .= ", price_min_ttc";
5448 $sql .= ", price_base_type";
5449 $sql .= ", price_label";
5450 $sql .= ", default_vat_code";
5451 $sql .= ", tva_tx";
5452 $sql .= ", recuperableonly";
5453 $sql .= ", localtax1_tx";
5454 $sql .= ", localtax1_type";
5455 $sql .= ", localtax2_tx";
5456 $sql .= ", localtax2_type";
5457 $sql .= ", fk_user_author";
5458 $sql .= ", tosell";
5459 $sql .= ", price_by_qty";
5460 $sql .= ", fk_price_expression";
5461 $sql .= ", fk_multicurrency";
5462 $sql .= ", multicurrency_code";
5463 $sql .= ", multicurrency_tx";
5464 $sql .= ", multicurrency_price";
5465 $sql .= ", multicurrency_price_ttc";
5466 $sql .= ")";
5467 $sql .= " SELECT";
5468 $sql .= " entity";
5469 $sql .= ", ".((int) $toId);
5470 $sql .= ", '".$this->db->idate($now)."'";
5471 $sql .= ", price_level";
5472 $sql .= ", price";
5473 $sql .= ", price_ttc";
5474 $sql .= ", price_min";
5475 $sql .= ", price_min_ttc";
5476 $sql .= ", price_base_type";
5477 $sql .= ", price_label";
5478 $sql .= ", default_vat_code";
5479 $sql .= ", tva_tx";
5480 $sql .= ", recuperableonly";
5481 $sql .= ", localtax1_tx";
5482 $sql .= ", localtax1_type";
5483 $sql .= ", localtax2_tx";
5484 $sql .= ", localtax2_type";
5485 $sql .= ", ".((int) $user->id);
5486 $sql .= ", tosell";
5487 $sql .= ", price_by_qty";
5488 $sql .= ", fk_price_expression";
5489 $sql .= ", fk_multicurrency";
5490 $sql .= ", multicurrency_code";
5491 $sql .= ", multicurrency_tx";
5492 $sql .= ", multicurrency_price";
5493 $sql .= ", multicurrency_price_ttc";
5494 $sql .= " FROM ".$this->db->prefix()."product_price ps";
5495 $sql .= " WHERE fk_product = ".((int) $fromId);
5496 $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)";
5497 $sql .= " ORDER BY date_price DESC";
5498
5499 dol_syslog(__METHOD__, LOG_DEBUG);
5500 $resql = $this->db->query($sql);
5501 if (!$resql) {
5502 $this->db->rollback();
5503 return -1;
5504 }
5505
5506 $this->db->commit();
5507 return 1;
5508 }
5509
5510 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5518 public function clone_associations($fromId, $toId)
5519 {
5520 // phpcs:enable
5521 $this->db->begin();
5522
5523 $sql = 'INSERT INTO '.$this->db->prefix().'product_association (fk_product_pere, fk_product_fils, qty, incdec)';
5524 $sql .= " SELECT ".((int) $toId).", fk_product_fils, qty, incdec FROM ".$this->db->prefix()."product_association";
5525 $sql .= " WHERE fk_product_pere = ".((int) $fromId);
5526
5527 dol_syslog(get_class($this).'::clone_association', LOG_DEBUG);
5528 if (!$this->db->query($sql)) {
5529 $this->db->rollback();
5530 return -1;
5531 }
5532
5533 $this->db->commit();
5534 return 1;
5535 }
5536
5537 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5545 public function clone_fournisseurs($fromId, $toId)
5546 {
5547 // phpcs:enable
5548 $this->db->begin();
5549
5550 $now = dol_now();
5551
5552 // les fournisseurs
5553 /*$sql = "INSERT ".$this->db->prefix()."product_fournisseur ("
5554 . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
5555 . " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, ref_fourn, fk_user_author"
5556 . " FROM ".$this->db->prefix()."product_fournisseur"
5557 . " WHERE fk_product = ".((int) $fromId);
5558
5559 if ( ! $this->db->query($sql ) )
5560 {
5561 $this->db->rollback();
5562 return -1;
5563 }*/
5564
5565 // les prix de fournisseurs.
5566 $sql = "INSERT ".$this->db->prefix()."product_fournisseur_price (";
5567 $sql .= " datec, fk_product, fk_soc, price, quantity, fk_user, tva_tx)";
5568 $sql .= " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, price, quantity, fk_user, tva_tx";
5569 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5570 $sql .= " WHERE fk_product = ".((int) $fromId);
5571
5572 dol_syslog(get_class($this).'::clone_fournisseurs', LOG_DEBUG);
5573 $resql = $this->db->query($sql);
5574 if (!$resql) {
5575 $this->db->rollback();
5576 return -1;
5577 } else {
5578 $this->db->commit();
5579 return 1;
5580 }
5581 }
5582
5583 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5596 public function fetch_prod_arbo($prod, $compl_path = '', $multiply = 1, $level = 1, $id_parent = 0, $ignore_stock_load = 0)
5597 {
5598 // phpcs:enable
5599 $tmpproduct = null;
5600
5601 if ($multiply < 1) {
5602 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);
5603 $multiply = 1;
5604 }
5605
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 return $this->res;
5675 }
5676
5684 public function hasFatherOrChild($mode = 0)
5685 {
5686 $nb = 0;
5687
5688 $sql = "SELECT COUNT(pa.rowid) as nb";
5689 $sql .= " FROM ".$this->db->prefix()."product_association as pa";
5690 if ($mode == 0) {
5691 $sql .= " WHERE pa.fk_product_fils = ".((int) $this->id)." OR pa.fk_product_pere = ".((int) $this->id);
5692 } elseif ($mode == -1) {
5693 $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)
5694 } elseif ($mode == 1) {
5695 $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)
5696 }
5697
5698 $resql = $this->db->query($sql);
5699 if ($resql) {
5700 $obj = $this->db->fetch_object($resql);
5701 if ($obj) {
5702 $nb = (int) $obj->nb;
5703 }
5704 } else {
5705 return -1;
5706 }
5707
5708 return $nb;
5709 }
5710
5716 public function hasVariants()
5717 {
5718 $nb = 0;
5719 if (!isModEnabled('variants')) {
5720 return $nb;
5721 }
5722 $sql = "SELECT count(rowid) as nb FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_parent = ".((int) $this->id);
5723 $sql .= " AND entity IN (".getEntity('product').")";
5724
5725 $resql = $this->db->query($sql);
5726 if ($resql) {
5727 $obj = $this->db->fetch_object($resql);
5728 if ($obj) {
5729 $nb = (int) $obj->nb;
5730 }
5731 }
5732
5733 return $nb;
5734 }
5735
5736
5742 public function isVariant()
5743 {
5744 if (isModEnabled('variants')) {
5745 $sql = "SELECT rowid FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_child = ".((int) $this->id)." AND entity IN (".getEntity('product').")";
5746
5747 $query = $this->db->query($sql);
5748
5749 if ($query) {
5750 if (!$this->db->num_rows($query)) {
5751 return false;
5752 }
5753 return true;
5754 } else {
5755 dol_print_error($this->db);
5756 return -1;
5757 }
5758 } else {
5759 return false;
5760 }
5761 }
5762
5769 public function getFather()
5770 {
5771 $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";
5772 $sql .= ", p.tosell as status, p.tobuy as status_buy";
5773 $sql .= " FROM ".$this->db->prefix()."product_association as pa,";
5774 $sql .= " ".$this->db->prefix()."product as p";
5775 $sql .= " WHERE p.rowid = pa.fk_product_pere";
5776 $sql .= " AND pa.fk_product_fils = ".((int) $this->id);
5777
5778 $res = $this->db->query($sql);
5779 if ($res) {
5780 $prods = array();
5781 while ($record = $this->db->fetch_array($res)) {
5782 // $record['id'] = $record['rowid'] = id of father
5783 $prods[$record['id']] = array();
5784 $prods[$record['id']]['id'] = $record['rowid'];
5785 $prods[$record['id']]['ref'] = $record['ref'];
5786 $prods[$record['id']]['label'] = $record['label'];
5787 $prods[$record['id']]['qty'] = $record['qty'];
5788 $prods[$record['id']]['incdec'] = $record['incdec'];
5789 $prods[$record['id']]['fk_product_type'] = $record['fk_product_type'];
5790 $prods[$record['id']]['entity'] = $record['entity'];
5791 $prods[$record['id']]['status'] = $record['status'];
5792 $prods[$record['id']]['status_buy'] = $record['status_buy'];
5793 }
5794 return $prods;
5795 } else {
5796 dol_print_error($this->db);
5797 return -1;
5798 }
5799 }
5800
5801
5811 public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array())
5812 {
5813 if (empty($id)) {
5814 return array();
5815 }
5816
5817 $sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,";
5818 $sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec,";
5819 $sql .= " pa.rowid as fk_association, pa.rang";
5820 $sql .= " FROM ".$this->db->prefix()."product as p,";
5821 $sql .= " ".$this->db->prefix()."product_association as pa";
5822 $sql .= " WHERE p.rowid = pa.fk_product_fils";
5823 $sql .= " AND pa.fk_product_pere = ".((int) $id);
5824 $sql .= " AND pa.fk_product_fils <> ".((int) $id); // This should not happens, it is to avoid infinite loop if it happens
5825 $sql .= " ORDER BY pa.rang";
5826
5827 dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level. ' parents='.(is_array($parents) ? implode(',', $parents) : $parents), LOG_DEBUG);
5828
5829 // Protection against infinite loop
5830 if ($level > 30) {
5831 return array();
5832 }
5833
5834 $res = $this->db->query($sql);
5835 if ($res) {
5836 $prods = array();
5837 if ($this->db->num_rows($res) > 0) {
5838 $parents[] = $id;
5839 }
5840
5841 while ($rec = $this->db->fetch_array($res)) {
5842 if (in_array($rec['id'], $parents)) {
5843 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);
5844 continue; // We discard this child if it is already found at a higher level in tree in the same branch.
5845 }
5846
5847 $prods[$rec['rowid']] = array(
5848 0 => $rec['rowid'],
5849 1 => $rec['qty'],
5850 2 => $rec['fk_product_type'],
5851 3 => $this->db->escape($rec['label']),
5852 4 => $rec['incdec'],
5853 5 => $rec['ref'],
5854 6 => $rec['fk_association'],
5855 7 => $rec['rang']
5856 );
5857 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
5858 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
5859 if (empty($firstlevelonly)) {
5860 $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, $parents);
5861 foreach ($listofchilds as $keyChild => $valueChild) {
5862 $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
5863 }
5864 }
5865 }
5866
5867 return $prods;
5868 } else {
5869 dol_print_error($this->db);
5870 return -1;
5871 }
5872 }
5873
5874 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5881 public function get_sousproduits_arbo()
5882 {
5883 // phpcs:enable
5884 $parent = array();
5885
5886 foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) { // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
5887 $parent[$this->label][$keyChild] = $valueChild;
5888 }
5889 foreach ($parent as $key => $value) { // key=label, value is array of children
5890 $this->sousprods[$key] = $value; // @phan-suppress-current-line PhanTypeMismatchProperty
5891 }
5892 }
5893
5901 public function getTooltipContentArray($params)
5902 {
5903 global $conf, $langs, $user;
5904
5905 $langs->loadLangs(array('products', 'other'));
5906
5907 $datas = array();
5908 $nofetch = !empty($params['nofetch']);
5909
5910 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5911 return ['optimize' => $langs->trans("ShowProduct")];
5912 }
5913
5914 // Does user has permission to read product/service
5915 $permissiontoreadproduct = 0;
5916 if ($this->type == self::TYPE_PRODUCT && $user->hasRight('product', 'read')) {
5917 $permissiontoreadproduct = 1;
5918 }
5919 if ($this->type == self::TYPE_SERVICE && $user->hasRight('service', 'read')) {
5920 $permissiontoreadproduct = 1;
5921 }
5922
5923 if (!empty($this->entity) && $permissiontoreadproduct) {
5924 $tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80, 0, 0, 0, 0, '1');
5925 if ($this->nbphoto > 0) {
5926 $datas['photo'] = '<div class="photointooltip floatright">'."\n" . $tmpphoto . '</div>';
5927 }
5928 }
5929
5930 if ($this->isProduct()) {
5931 $datas['picto'] = img_picto('', 'product').' <u class="paddingrightonly">'.$langs->trans("Product").'</u>';
5932 } elseif ($this->isService()) {
5933 $datas['picto'] = img_picto('', 'service').' <u class="paddingrightonly">'.$langs->trans("Service").'</u>';
5934 }
5935 if (isset($this->status) && isset($this->status_buy)) {
5936 $datas['status'] = ' '.$this->getLibStatut(5, 0) . ' '.$this->getLibStatut(5, 1);
5937 }
5938
5939 if (!empty($this->ref)) {
5940 $datas['ref'] = '<br><b>'.$langs->trans('ProductRef').':</b> '.$this->ref;
5941 }
5942 if (!empty($this->label)) {
5943 $datas['label'] = '<br><b>'.$langs->trans('ProductLabel').':</b> '.$this->label.'<br>';
5944 }
5945
5946 if ($permissiontoreadproduct) {
5947 if (!empty($this->description) && getDolGlobalString('PRODUCT_SHOW_DESCRIPTION_IN_TOOLTIP')) {
5948 $datas['description'] = '<br><b>'.$langs->trans('ProductDescription').':</b> '.dolGetFirstLineOfText($this->description, 5);
5949 }
5950
5951 $datas['stockmanaged'] = "<br><b>".$langs->trans("StockableProduct").'</b>: '.yn($this->isStockManaged());
5952
5953 if ($this->isStockManaged()) {
5954 if (isModEnabled('productbatch')) {
5955 $langs->load("productbatch");
5956 $datas['batchstatus'] = "<br><b>".$langs->trans("ManageLotSerial").'</b>: '.$this->getLibStatut(0, 2);
5957 if ($this->status_batch) {
5958 $datas['batchdlc'] = "<br><b>".$langs->trans("BatchSellOrEatByMandatoryList", $langs->transnoentitiesnoconv("SellByDate"), $langs->transnoentitiesnoconv("EatByDate")).'</b>: '.$this->getSellOrEatByMandatoryLabel();
5959 }
5960 }
5961 }
5962 if (isModEnabled('barcode')) {
5963 $datas['barcode'] = '<br><b>'.$langs->trans('BarCode').':</b> '.$this->barcode;
5964 }
5965
5966 if ($this->isProduct()) {
5967 if ($this->weight) {
5968 $datas['weight'] = "<br><b>".$langs->trans("Weight").'</b>: '.$this->weight.' '.measuringUnitString(0, "weight", $this->weight_units);
5969 }
5970 $labelsize = "";
5971 if ($this->length) {
5972 $labelsize .= "<b>".$langs->trans("Length").'</b>: '.$this->length.' '.measuringUnitString(0, 'size', $this->length_units);
5973 }
5974 if ($this->width) {
5975 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Width").'</b>: '.$this->width.' '.measuringUnitString(0, 'size', $this->width_units);
5976 }
5977 if ($this->height) {
5978 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Height").'</b>: '.$this->height.' '.measuringUnitString(0, 'size', $this->height_units);
5979 }
5980 if ($labelsize) {
5981 $datas['size'] = "<br>".$labelsize;
5982 }
5983
5984 $labelsurfacevolume = "";
5985 if ($this->surface) {
5986 $labelsurfacevolume .= "<b>".$langs->trans("Surface").'</b>: '.$this->surface.' '.measuringUnitString(0, 'surface', $this->surface_units);
5987 }
5988 if ($this->volume) {
5989 $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Volume").'</b>: '.$this->volume.' '.measuringUnitString(0, 'volume', $this->volume_units);
5990 }
5991 if ($labelsurfacevolume) {
5992 $datas['surface'] = "<br>" . $labelsurfacevolume;
5993 }
5994 }
5995 if ($this->isService() && !empty($this->duration_value)) {
5996 // Duration
5997 $datas['duration'] = '<br><b>'.$langs->trans("Duration").':</b> '.$this->duration_value;
5998 if ($this->duration_value > 1) {
5999 $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"));
6000 } elseif ($this->duration_value > 0) {
6001 $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"));
6002 }
6003 $datas['duration'] .= (!empty($this->duration_unit) && isset($dur[$this->duration_unit]) ? "&nbsp;".$langs->trans($dur[$this->duration_unit]) : '');
6004 }
6005 if (empty($user->socid)) {
6006 if ($this->isStockManaged() && isset($this->pmp) && $this->pmp) {
6007 $datas['pmp'] = "<br><b>".$langs->trans("PMPValue").'</b>: '.price($this->pmp, 0, '', 1, -1, -1, $conf->currency);
6008 }
6009
6010 if (isModEnabled('accounting')) {
6011 if ($this->status && isset($this->accountancy_code_sell)) {
6012 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
6013 $selllabel = '<br>';
6014 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellCode').':</b> '.length_accountg($this->accountancy_code_sell);
6015 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellIntraCode').':</b> '.length_accountg($this->accountancy_code_sell_intra);
6016 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellExportCode').':</b> '.length_accountg($this->accountancy_code_sell_export);
6017 $datas['accountancysell'] = $selllabel;
6018 }
6019 if ($this->status_buy && isset($this->accountancy_code_buy)) {
6020 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
6021 $buylabel = '';
6022 if (empty($this->status)) {
6023 $buylabel .= '<br>';
6024 }
6025 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyCode').':</b> '.length_accountg($this->accountancy_code_buy);
6026 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyIntraCode').':</b> '.length_accountg($this->accountancy_code_buy_intra);
6027 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyExportCode').':</b> '.length_accountg($this->accountancy_code_buy_export);
6028 $datas['accountancybuy'] = $buylabel;
6029 }
6030 }
6031 }
6032 // show categories for this record only in ajax to not overload lists
6033 if (isModEnabled('category') && !$nofetch) {
6034 require_once DOL_DOCUMENT_ROOT . '/categories/class/categorie.class.php';
6035 $form = new Form($this->db);
6036 $datas['categories'] = '<br>' . $form->showCategories($this->id, Categorie::TYPE_PRODUCT, 1);
6037 }
6038 }
6039
6040 return $datas;
6041 }
6042
6056 public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0, $morecss = '', $add_label = 0, $sep = ' - ')
6057 {
6058 global $langs, $hookmanager;
6059
6060 include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
6061
6062 $result = '';
6063
6064 $newref = (string) $this->ref;
6065 if ($maxlength) {
6066 $newref = dol_trunc($newref, $maxlength, 'middle');
6067 }
6068 $params = [
6069 'id' => $this->id,
6070 'objecttype' => ($this->type == 1 ? 'service' : 'product'),
6071 'option' => $option,
6072 'nofetch' => 1,
6073 ];
6074 $classfortooltip = 'classfortooltip';
6075 $dataparams = '';
6076 if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
6077 $classfortooltip = 'classforajaxtooltip';
6078 $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
6079 $label = '';
6080 } else {
6081 $label = implode($this->getTooltipContentArray($params));
6082 }
6083
6084 $linkclose = '';
6085 if (empty($notooltip)) {
6086 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
6087 $label = $langs->trans("ShowProduct");
6088 $linkclose .= ' alt="'.dol_escape_htmltag($label, 1, 1).'"';
6089 }
6090 $linkclose .= ($label ? ' title="'.dol_escape_htmltag($label, 1, 1).'"' : ' title="tocomplete"');
6091 $linkclose .= $dataparams.' class="nowraponall '.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
6092 } else {
6093 $linkclose = ' class="nowraponall'.($morecss ? ' '.$morecss : '').'"';
6094 }
6095
6096 if ($option == 'supplier' || $option == 'category') {
6097 $url = DOL_URL_ROOT.'/product/price_suppliers.php?id='.$this->id;
6098 } elseif ($option == 'stock') {
6099 $url = DOL_URL_ROOT.'/product/stock/product.php?id='.$this->id;
6100 } elseif ($option == 'composition') {
6101 $url = DOL_URL_ROOT.'/product/composition/card.php?id='.$this->id;
6102 } else {
6103 $url = DOL_URL_ROOT.'/product/card.php?id='.$this->id;
6104 }
6105
6106 if ($option !== 'nolink') {
6107 // Add param to save lastsearch_values or not
6108 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
6109 if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
6110 $add_save_lastsearch_values = 1;
6111 }
6112 if ($add_save_lastsearch_values) {
6113 $url .= '&save_lastsearch_values=1';
6114 }
6115 }
6116
6117 $linkstart = '<a href="'.$url.'"';
6118 $linkstart .= $linkclose.'>';
6119 $linkend = '</a>';
6120
6121 $result .= $linkstart;
6122 if ($withpicto) {
6123 if ($this->isProduct()) {
6124 $result .= (img_object(($notooltip ? '' : $label), 'product', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
6125 }
6126 if ($this->isService()) {
6127 $result .= (img_object(($notooltip ? '' : $label), 'service', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
6128 }
6129 }
6130 $result .= '<span class="aaa">'.dol_escape_htmltag($newref).'</span>';
6131 $result .= $linkend;
6132 if ($withpicto != 2) {
6133 $result .= (($add_label && $this->label) ? $sep.dol_trunc($this->label, ($add_label > 1 ? $add_label : 0)) : '');
6134 }
6135
6136 global $action;
6137 $hookmanager->initHooks(array('productdao'));
6138 $parameters = array('id' => $this->id, 'getnomurl' => &$result, 'label' => &$label);
6139 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
6140 if ($reshook > 0) {
6141 $result = $hookmanager->resPrint;
6142 } else {
6143 $result .= $hookmanager->resPrint;
6144 }
6145
6146 return $result;
6147 }
6148
6149
6160 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
6161 {
6162 global $langs;
6163
6164 $langs->load("products");
6165 $outputlangs->load("products");
6166
6167 // Set the model to the name of the model to use
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 $filterStatus = getDolGlobalString('MO_STATUS_FOR_VIRTUAL_STOCK', '1,2');
6595 $result = $this->load_stats_inproduction(0, $filterStatus, 1, $dateofvirtualstock);
6596 if ($result < 0) {
6597 dol_print_error($this->db, $this->error);
6598 }
6599 $stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty'];
6600 }
6601
6602 $this->stock_theorique = $this->stock_reel + $stock_inproduction;
6603
6604 // $weBillOrderOrShipmentReception is set to 'order' or 'shipmentreception'. it will be used to know how to make virtual stock
6605 // calculation when we have a stock increase or decrease on billing. Do we have to count orders to bill or shipment/reception to bill ?
6606 $weBillOrderOrShipmentReception = getDolGlobalString('STOCK_DO_WE_BILL_ORDER_OR_SHIPMENTECEPTION_FOR_VIRTUALSTOCK', 'order');
6607
6608 // Stock decrease mode
6609 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') || getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6610 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6611 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_VALIDATE_ORDER')) {
6612 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
6613 $tmpnewprod = dol_clone($this, 1);
6614 $result = $tmpnewprod->load_stats_commande(0, '0', 1); // Get qty in draft orders
6615 $this->stock_theorique += $tmpnewprod->stats_commande['qty'];
6616 }
6617 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'order') {
6618 $this->stock_theorique -= $stock_commande_client;
6619 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6620 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6621 }
6622
6623 // Stock Increase mode
6624 if (getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION') || getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION_CLOSE')) {
6625 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6626 } 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
6627 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6628 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER')) { // Warning: stock change "on approval", not on validation !
6629 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
6630 $tmpnewprod = dol_clone($this, 1);
6631 $result = $tmpnewprod->load_stats_commande_fournisseur(0, '0', 1); // Get qty in draft orders
6632 $this->stock_theorique += $this->stats_commande_fournisseur['qty'];
6633 }
6634 $this->stock_theorique -= $stock_reception_fournisseur;
6635 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'order') {
6636 $this->stock_theorique += $stock_commande_fournisseur;
6637 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6638 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6639 }
6640
6641 $parameters = array('id' => $this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual);
6642 // Note that $action and $object may have been modified by some hooks
6643 $reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action);
6644 if ($reshook > 0) {
6645 $this->stock_theorique = $hookmanager->resArray['stock_theorique'];
6646 } elseif ($reshook == 0 && isset($hookmanager->resArray['stock_stats_hook'])) {
6647 $this->stock_theorique += $hookmanager->resArray['stock_stats_hook'];
6648 }
6649
6650 //Virtual Stock by Warehouse
6651 if (!empty($this->stock_warehouse) && getDolGlobalString('STOCK_ALLOW_VIRTUAL_STOCK_PER_WAREHOUSE')) {
6652 foreach ($this->stock_warehouse as $warehouseid => $stockwarehouse) {
6653 if (isModEnabled('mrp')) {
6654 $result = $this->load_stats_inproduction(0, getDolGlobalString('MO_STATUS_FOR_VIRTUAL_STOCK', '1,2'), 1, $dateofvirtualstock, $warehouseid);
6655 if ($result < 0) {
6656 dol_print_error($this->db, $this->error);
6657 }
6658 }
6659
6660 if ($this->fk_default_warehouse == $warehouseid) {
6661 $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']);
6662 } else {
6663 $this->stock_warehouse[$warehouseid]->virtual = $this->stock_warehouse[$warehouseid]->real + $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'];
6664 }
6665 }
6666 }
6667
6668 return 1;
6669 }
6670
6678 public function loadStockForVirtualProduct($option = '', $qtyWish = 1)
6679 {
6680 $this->stock_warehouse = array();
6681 $error = 0;
6682
6683 $this->get_sousproduits_arbo();
6684 $prods_arbo = $this->get_arbo_each_prod($qtyWish, 1);
6685 if (count($prods_arbo) > 0) {
6686 $productCachedList = array();
6687 $stockByComponentList = array();
6688
6689 foreach ($prods_arbo as $componentArr) {
6690 $componentId = $componentArr['id'];
6691 // only component whose manage stock
6692 if ($componentArr['incdec'] == 1) {
6693 if (!isset($productCachedList[$componentId])) {
6694 $componentStatic = new self($this->db);
6695 $componentStatic->fetch($componentId);
6696 // check if it's a sub-kit
6697 $childrenNb = $componentStatic->hasFatherOrChild(1);
6698 if ($childrenNb == 0) {
6699 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6700 if (!isset($stockByComponentList[$componentId])) {
6701 $stockByComponentList[$componentId] = array(
6702 'qty_need' => 0
6703 );
6704 }
6705 $stockByComponentList[$componentId]['qty_need'] += $componentArr['nb_total'];
6706 }
6707 $productCachedList[$componentId] = $componentStatic;
6708 }
6709 }
6710 }
6711
6712 if (!empty($stockByComponentList)) {
6713 foreach ($stockByComponentList as $componentId => $stockByComponentArr) {
6714 if (!isset($productCachedList[$componentId])) {
6715 $componentStatic = new self($this->db);
6716 $componentStatic->fetch($componentId);
6717 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6718 $productCachedList[$componentId] = $componentStatic;
6719 }
6720 $component = $productCachedList[$componentId];
6721
6722
6723 if ($component->stock_reel < $stockByComponentArr['qty_need']) {
6724 // not enough stock for this component to assemble this virtual product
6725 $error++;
6726 $this->error = 'Not enough component [id='.$componentId.'] in stock, real='.$component->stock_reel.' and need='.$stockByComponentArr['qty_need'];
6727 $this->errors[] = $this->error;
6728 dol_syslog(__METHOD__.' : '.$this->error, LOG_ERR);
6729 } else {
6730 if (!empty($component->stock_warehouse)) {
6731 foreach ($component->stock_warehouse as $warehouseId => $warehouseObj) {
6732 $kitWarehouseAvailable = new stdClass();
6733 $kitWarehouseAvailable->id = $warehouseObj->id;
6734 $kitWarehouseAvailable->real = $qtyWish;
6735 $this->stock_warehouse[$warehouseId] = $kitWarehouseAvailable;
6736 }
6737 }
6738 }
6739
6740 if ($error) {
6741 break;
6742 }
6743 }
6744 } elseif (getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE') && empty($productCachedList)) {
6745 // if all sub product are not stock managed when use parent stock
6746 $this->load_stock('warehouseopen');
6747 }
6748 }
6749
6750 if ($error) {
6751 return -1;
6752 } else {
6753 return 1;
6754 }
6755 }
6756
6764 public function loadBatchInfo($batch)
6765 {
6766 $result = array();
6767
6768 $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";
6769 $sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = ".((int) $this->id)." AND pb.batch = '".$this->db->escape($batch)."'";
6770 $sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby";
6771 dol_syslog(get_class($this)."::loadBatchInfo load first entry found for lot/serial = ".$batch, LOG_DEBUG);
6772 $resql = $this->db->query($sql);
6773 if ($resql) {
6774 $num = $this->db->num_rows($resql);
6775 $i = 0;
6776 while ($i < $num) {
6777 $obj = $this->db->fetch_object($resql);
6778 $result[] = array('batch' => $batch, 'eatby' => $this->db->jdate($obj->eatby), 'sellby' => $this->db->jdate($obj->sellby), 'qty' => $obj->qty);
6779 $i++;
6780 }
6781 return $result;
6782 } else {
6783 dol_print_error($this->db);
6784 $this->db->rollback();
6785 return array();
6786 }
6787 }
6788
6789 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6798 public function add_photo($sdir, $file)
6799 {
6800 // phpcs:enable
6801 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6802
6803 $result = 0;
6804
6805 $dir = $sdir;
6806 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6807 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos";
6808 } else {
6809 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product').dol_sanitizeFileName($this->ref);
6810 }
6811
6812 dol_mkdir($dir);
6813
6814 $dir_osencoded = $dir;
6815
6816 if (is_dir($dir_osencoded)) {
6817 $originImage = $dir.'/'.$file['name'];
6818
6819 // Cree fichier en taille origine
6820 $result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
6821
6822 if (file_exists(dol_osencode($originImage))) {
6823 // Create thumbs
6824 $this->addThumbs($originImage);
6825 }
6826 }
6827
6828 if (is_numeric($result) && $result > 0) {
6829 return 1;
6830 } else {
6831 return -1;
6832 }
6833 }
6834
6835 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6842 public function is_photo_available($sdir)
6843 {
6844 // phpcs:enable
6845 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6846 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6847
6848 $dir = $sdir;
6849 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6850 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos/";
6851 } else {
6852 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product');
6853 }
6854
6855 $dir_osencoded = dol_osencode($dir);
6856 if (file_exists($dir_osencoded)) {
6857 $handle = opendir($dir_osencoded);
6858 if (is_resource($handle)) {
6859 while (($file = readdir($handle)) !== false) {
6860 if (!utf8_check($file)) {
6861 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // To be sure data is stored in UTF8 in memory
6862 }
6863 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6864 return true;
6865 }
6866 }
6867 }
6868 }
6869
6870 return false;
6871 }
6872
6873 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6881 public function liste_photos($dir, $nbmax = 0)
6882 {
6883 // phpcs:enable
6884 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6885 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6886
6887 $nbphoto = 0;
6888 $tabobj = array();
6889
6890 $dir_osencoded = dol_osencode($dir);
6891 $handle = @opendir($dir_osencoded);
6892 if (is_resource($handle)) {
6893 while (($file = readdir($handle)) !== false) {
6894 if (!utf8_check($file)) {
6895 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // readdir returns ISO
6896 }
6897 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6898 $nbphoto++;
6899
6900 // We forge name of thumb.
6901 $photo = $file;
6902 $photo_vignette = '';
6903 $regs = array();
6904 if (preg_match('/('.$this->regeximgext.')$/i', $photo, $regs)) {
6905 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $photo).'_small'.$regs[0];
6906 }
6907
6908 $dirthumb = $dir.'thumbs/';
6909
6910 // Object
6911 $obj = array();
6912 $obj['photo'] = $photo;
6913 if ($photo_vignette && dol_is_file($dirthumb.$photo_vignette)) {
6914 $obj['photo_vignette'] = 'thumbs/'.$photo_vignette;
6915 } else {
6916 $obj['photo_vignette'] = "";
6917 }
6918
6919 $tabobj[$nbphoto - 1] = $obj;
6920
6921 // Do we have to continue with next photo ?
6922 if ($nbmax && $nbphoto >= $nbmax) {
6923 break;
6924 }
6925 }
6926 }
6927
6928 closedir($handle);
6929 }
6930
6931 return $tabobj;
6932 }
6933
6934 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6941 public function delete_photo($file)
6942 {
6943 // phpcs:enable
6944 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6945 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6946
6947 $dir = dirname($file).'/'; // Chemin du dossier contenant l'image d'origine
6948 $dirthumb = $dir.'/thumbs/'; // Chemin du dossier contenant la vignette
6949 $filename = preg_replace('/'.preg_quote($dir, '/').'/i', '', $file); // Nom du fichier
6950
6951 // On efface l'image d'origine
6952 dol_delete_file($file, 0, 0, 0, $this); // For triggers
6953
6954 // Si elle existe, on efface la vignette
6955 if (preg_match('/('.$this->regeximgext.')$/i', $filename, $regs)) {
6956 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_small'.$regs[0];
6957 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6958 dol_delete_file($dirthumb.$photo_vignette);
6959 }
6960
6961 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_mini'.$regs[0];
6962 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6963 dol_delete_file($dirthumb.$photo_vignette);
6964 }
6965 }
6966 }
6967
6968 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6975 public function get_image_size($file)
6976 {
6977 // phpcs:enable
6978 $file_osencoded = dol_osencode($file);
6979 $infoImg = getimagesize($file_osencoded); // Get information on image
6980 $this->imgWidth = $infoImg[0]; // Largeur de l'image
6981 $this->imgHeight = $infoImg[1]; // Hauteur de l'image
6982 }
6983
6989 public function loadStateBoard()
6990 {
6991 global $hookmanager;
6992
6993 $this->nb = array();
6994
6995 $sql = "SELECT count(p.rowid) as nb, fk_product_type";
6996 $sql .= " FROM ".$this->db->prefix()."product as p";
6997 $sql .= ' WHERE p.entity IN ('.getEntity($this->element, 1).')';
6998 // Add where from hooks
6999 if (is_object($hookmanager)) {
7000 $parameters = array();
7001 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
7002 $sql .= $hookmanager->resPrint;
7003 }
7004 $sql .= ' GROUP BY fk_product_type';
7005
7006 $resql = $this->db->query($sql);
7007 if ($resql) {
7008 while ($obj = $this->db->fetch_object($resql)) {
7009 if ($obj->fk_product_type == 1) {
7010 $this->nb["services"] = $obj->nb;
7011 } else {
7012 $this->nb["products"] = $obj->nb;
7013 }
7014 }
7015 $this->db->free($resql);
7016 return 1;
7017 } else {
7018 dol_print_error($this->db);
7019 $this->error = $this->db->error();
7020 return -1;
7021 }
7022 }
7023
7029 public function isProduct()
7030 {
7031 return $this->type == Product::TYPE_PRODUCT;
7032 }
7033
7039 public function isService()
7040 {
7041 return $this->type == Product::TYPE_SERVICE;
7042 }
7043
7049 public function isStockManaged()
7050 {
7051 return (($this->isProduct() || ($this->isService() && getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) && ($this->stockable_product > 0));
7052 }
7053
7059 public function isMandatoryPeriod()
7060 {
7061 return $this->mandatory_period == 1;
7062 }
7063
7069 public function hasbatch()
7070 {
7071 return $this->status_batch > 0;
7072 }
7073
7074
7075 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7084 public function get_barcode($object, $type = '')
7085 {
7086 // phpcs:enable
7087 global $conf;
7088
7089 $result = '';
7090 if (getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
7091 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
7092 foreach ($dirsociete as $dirroot) {
7093 $res = dol_include_once($dirroot . getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM').'.php');
7094 if ($res) {
7095 break;
7096 }
7097 }
7098 $var = getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM');
7099 $mod = new $var();
7100 '@phan-var-force ModeleNumRefBarCode $mod';
7101
7102 $result = $mod->getNextValue($object, $type);
7103
7104 dol_syslog(get_class($this)."::get_barcode barcode=".$result." module=".$var);
7105 }
7106 return $result;
7107 }
7108
7116 public function initAsSpecimen()
7117 {
7118 $now = dol_now();
7119
7120 // Initialize parameters
7121 $this->specimen = 1;
7122 $this->id = 0;
7123 $this->ref = 'PRODUCT_SPEC';
7124 $this->label = 'PRODUCT SPECIMEN';
7125 $this->description = 'This is description of this product specimen that was created the '.dol_print_date($now, 'dayhourlog').'.';
7126 $this->specimen = 1;
7127 $this->country_id = 1;
7128 $this->status = 1;
7129 $this->status_buy = 1;
7130 $this->tobatch = 0;
7131 $this->sell_or_eat_by_mandatory = 0;
7132 $this->note_private = 'This is a comment (private)';
7133 $this->note_public = 'This is a comment (public)';
7134 $this->date_creation = $now;
7135 $this->date_modification = $now;
7136
7137 $this->weight = 4;
7138 $this->weight_units = 3;
7139
7140 $this->length = 5;
7141 $this->length_units = 1;
7142 $this->width = 6;
7143 $this->width_units = 0;
7144 $this->height = null;
7145 $this->height_units = null;
7146
7147 $this->surface = 30;
7148 $this->surface_units = 0;
7149 $this->volume = 300;
7150 $this->volume_units = 0;
7151
7152 $this->barcode = -1; // Create barcode automatically
7153
7154 return 1;
7155 }
7156
7166 public function getLabelOfUnit($type = 'long', $outputlangs = null, $noentities = 0)
7167 {
7168 global $langs;
7169
7170 if (empty($this->fk_unit)) {
7171 return '';
7172 }
7173 if (empty($outputlangs)) {
7174 $outputlangs = $langs;
7175 }
7176
7177 $outputlangs->load('products');
7178 $label = '';
7179
7180 $sql = "SELECT code, label, short_label FROM ".$this->db->prefix()."c_units where rowid = ".((int) $this->fk_unit);
7181
7182 $resql = $this->db->query($sql);
7183 if (!$resql) {
7184 $this->error = $this->db->error();
7185 dol_syslog(get_class($this)."::getLabelOfUnit Error ".$this->error, LOG_ERR);
7186 return -1;
7187 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
7188 if ($type == 'short') {
7189 if ($noentities) {
7190 $label = $outputlangs->transnoentitiesnoconv($res['short_label']);
7191 } else {
7192 $label = $outputlangs->trans($res['short_label']);
7193 }
7194 } elseif ($type == 'code') {
7195 $label = $res['code'];
7196 } else {
7197 if ($outputlangs->trans('unit'.$res['code']) == 'unit'.$res['code']) {
7198 // No translation available
7199 $label = $res['label'];
7200 } else {
7201 // Return the translated value
7202 if ($noentities) {
7203 $label = $outputlangs->transnoentitiesnoconv('unit'.$res['code']);
7204 } else {
7205 $label = $outputlangs->trans('unit'.$res['code']);
7206 }
7207 }
7208 }
7209 }
7210 $this->db->free($resql);
7211
7212 return $label;
7213 }
7214
7215 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7221 public function min_recommended_price()
7222 {
7223 // phpcs:enable
7224 $maxpricesupplier = 0;
7225
7226 if (getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE')) {
7227 include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
7228 $product_fourn = new ProductFournisseur($this->db);
7229 $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', '');
7230
7231 if (is_array($product_fourn_list) && count($product_fourn_list) > 0) {
7232 foreach ($product_fourn_list as $productfourn) {
7233 if ($productfourn->fourn_unitprice > $maxpricesupplier) {
7234 $maxpricesupplier = $productfourn->fourn_unitprice;
7235 }
7236 }
7237
7238 $maxpricesupplier *= getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE');
7239 }
7240 }
7241
7242 return $maxpricesupplier;
7243 }
7244
7245
7256 public function setCategories($categories)
7257 {
7258 require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
7259 return parent::setCategoriesCommon($categories, Categorie::TYPE_PRODUCT);
7260 }
7261
7270 public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
7271 {
7272 /* we firstly delete origin product prices existing in destination before update */
7273 $sql = " DELETE FROM ".$dbs->prefix()."product_customer_price";
7274 $sql .= " WHERE fk_soc = ".(int) $origin_id;
7275 $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)";
7276 //$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.')';
7277
7278 if (!$dbs->query($sql)) {
7279 return false;
7280 }
7281
7282 $tables = array(
7283 'product_customer_price',
7284 'product_customer_price_log'
7285 );
7286
7287 return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
7288 }
7289
7301 public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
7302 {
7303 $sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM ".$this->db->prefix()."product_pricerules";
7304 $query = $this->db->query($sql);
7305
7306 $rules = array();
7307
7308 while ($result = $this->db->fetch_object($query)) {
7309 $rules[$result->level] = $result;
7310 }
7311
7312 //Because prices can be based on other level's prices, we temporarily store them
7313 $prices = array(
7314 1 => $baseprice
7315 );
7316
7317 $nbofproducts = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
7318 for ($i = 1; $i <= $nbofproducts; $i++) {
7319 $price = $baseprice;
7320 $price_min = $baseprice;
7321
7322 //We have to make sure it does exist and it is > 0
7323 //First price level only allows changing min_price
7324 if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) {
7325 $price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent / 100));
7326 }
7327
7328 $prices[$i] = $price;
7329
7330 //We have to make sure it does exist and it is > 0
7331 if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) {
7332 $price_min = $price * (1 - ($rules[$i]->var_min_percent / 100));
7333 }
7334
7335 //Little check to make sure the price is modified before triggering generation
7336 $check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i]));
7337 $check_type = ($baseprice == $this->multiprices_base_type[$i]);
7338
7339 if ($check_amount && $check_type) {
7340 continue;
7341 }
7342
7343 if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, 1) < 0) {
7344 return -1;
7345 }
7346 }
7347
7348 return 1;
7349 }
7350
7356 public function getRights()
7357 {
7358 global $user;
7359
7360 if ($this->isProduct()) {
7361 return $user->rights->produit;
7362 } else {
7363 return $user->rights->service;
7364 }
7365 }
7366
7373 public function info($id)
7374 {
7375 $sql = "SELECT p.rowid, p.ref, p.datec as date_creation, GREATEST(p.tms, pef.tms) as date_modification,";
7376 $sql .= " p.fk_user_author, p.fk_user_modif";
7377 $sql .= " FROM ".$this->db->prefix().$this->table_element." as p";
7378 $sql .= " LEFT JOIN ".$this->db->prefix().$this->table_element."_extrafields as pef ON pef.fk_object=p.rowid";
7379 $sql .= " WHERE p.rowid = ".((int) $id);
7380
7381 $result = $this->db->query($sql);
7382 if ($result) {
7383 if ($this->db->num_rows($result)) {
7384 $obj = $this->db->fetch_object($result);
7385
7386 $this->id = $obj->rowid;
7387 $this->ref = $obj->ref;
7388
7389 $this->user_creation_id = $obj->fk_user_author;
7390 $this->user_modification_id = $obj->fk_user_modif;
7391
7392 $this->date_creation = $this->db->jdate($obj->date_creation);
7393 $this->date_modification = $this->db->jdate($obj->date_modification);
7394 }
7395
7396 $this->db->free($result);
7397 } else {
7398 dol_print_error($this->db);
7399 }
7400 }
7401
7402
7408 public function getProductDurationHours()
7409 {
7410 if (empty($this->duration_value)) {
7411 $this->errors[] = 'ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
7412 return -1;
7413 }
7414 if ($this->duration_unit == 's') {
7415 $prodDurationHours = 1. / 3600;
7416 } elseif ($this->duration_unit == 'i' || $this->duration_unit == 'mn' || $this->duration_unit == 'min') {
7417 $prodDurationHours = 1. / 60;
7418 } elseif ($this->duration_unit == 'h') {
7419 $prodDurationHours = 1.;
7420 } elseif ($this->duration_unit == 'd') {
7421 $prodDurationHours = 24.;
7422 } elseif ($this->duration_unit == 'w') {
7423 $prodDurationHours = 24. * 7;
7424 } elseif ($this->duration_unit == 'm') {
7425 $prodDurationHours = 24. * 30;
7426 } elseif ($this->duration_unit == 'y') {
7427 $prodDurationHours = 24. * 365;
7428 } else {
7429 $prodDurationHours = 0.0;
7430 }
7431 $prodDurationHours *= $this->duration_value;
7432
7433 return $prodDurationHours;
7434 }
7435
7436
7444 public function getKanbanView($option = '', $arraydata = null)
7445 {
7446 global $langs, $conf;
7447
7448 $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
7449
7450 $return = '<div class="box-flex-item box-flex-grow-zero">';
7451 $return .= '<div class="info-box info-box-sm">';
7452 $return .= '<div class="info-box-img">';
7453 $label = '';
7454 if ($this->entity !== null && $this->is_photo_available($conf->product->multidir_output[$this->entity])) {
7455 $label .= $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 120, 160, 0, 0, 0, '', 'photoref photokanban');
7456 $return .= $label;
7457 } else {
7458 if ($this->isProduct()) {
7459 $label .= img_picto('', 'product');
7460 } elseif ($this->isService()) {
7461 $label .= img_picto('', 'service');
7462 }
7463 $return .= $label;
7464 }
7465 $return .= '</div>';
7466 $return .= '<div class="info-box-content">';
7467 $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">' . $this->getNomUrl() . '</span>';
7468 if ($selected >= 0) {
7469 $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
7470 }
7471 $return .= '<br><span class="info-box-label opacitymedium inline-block tdoverflowmax150 valignmiddle" title="'.dol_escape_htmltag($this->label).'">'.dol_escape_htmltag($this->label).'</span>';
7472 if ($this->price_base_type == 'TTC') {
7473 $return .= '<br><span class="info-box-status amount">'.price($this->price_ttc).' '.$langs->trans("TTC").'</span>';
7474 } else {
7475 if ($this->status) {
7476 $return .= '<br><span class="info-box-status amount">'.price($this->price).' '.$langs->trans("HT").'</span>';
7477 }
7478 }
7479 $br = 1;
7480 if ($this->isProduct()) {
7481 $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>';
7482 $br = 0;
7483 }
7484 if ($br) {
7485 $return .= '<br>';
7486 $return .= '<div class="info-box-status inline-block valignmiddle">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7487 } else {
7488 $return .= '<div class="info-box-status inline-block valignmiddle marginleftonly paddingleft">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7489 }
7490 $return .= '</div>';
7491 $return .= '</div>';
7492 $return .= '</div>';
7493 return $return;
7494 }
7495}
7496
7502{
7503 public $picto = 'service';
7504}
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
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...
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.
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)
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_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)
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)
img_object($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $allowothertags=array())
Show a picto called object_picto (generic function)
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_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
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...
Definition html.lib.php:172
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
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