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