dolibarr 25.0.0-alpha
product.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2001-2007 Rodolphe Quiedeville <rodolphe@quiedeville.org>
3 * Copyright (C) 2004-2014 Laurent Destailleur <eldy@users.sourceforge.net>
4 * Copyright (C) 2005-2015 Regis Houssin <regis.houssin@inodbox.com>
5 * Copyright (C) 2006 Andre Cianfarani <acianfa@free.fr>
6 * Copyright (C) 2007-2011 Jean Heimburger <jean@tiaris.info>
7 * Copyright (C) 2010-2018 Juanjo Menent <jmenent@2byte.es>
8 * Copyright (C) 2012 Cedric Salvador <csalvador@gpcsolutions.fr>
9 * Copyright (C) 2013-2014 Cedric GROSS <c.gross@kreiz-it.fr>
10 * Copyright (C) 2013-2016 Marcos García <marcosgdf@gmail.com>
11 * Copyright (C) 2011-2021 Open-DSI <support@open-dsi.fr>
12 * Copyright (C) 2014 Henry Florian <florian.henry@open-concept.pro>
13 * Copyright (C) 2014-2016 Philippe Grand <philippe.grand@atoo-net.com>
14 * Copyright (C) 2014 Ion agorria <ion@agorria.com>
15 * Copyright (C) 2016-2024 Ferran Marcet <fmarcet@2byte.es>
16 * Copyright (C) 2017 Gustavo Novaro
17 * Copyright (C) 2019-2026 Frédéric France <frederic.france@free.fr>
18 * Copyright (C) 2023 Benjamin Falière <benjamin.faliere@altairis.fr>
19 * Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
20 * Copyright (C) 2025 Lenin Rivas <lenin.rivas777@gmail.com>
21 * Copyright (C) 2026 Anthony Berton <anthony.berton@bb2a.fr>
22 * Copyright (C) 2026 Jose Martinez <jose.martinez@pichinov.com>
23 * Copyright (C) 2026 Mélina Joum <melina.joum@altairis.fr>
24 *
25 * This program is free software; you can redistribute it and/or modify
26 * it under the terms of the GNU General Public License as published by
27 * the Free Software Foundation; either version 3 of the License, or
28 * (at your option) any later version.
29 *
30 * This program is distributed in the hope that it will be useful,
31 * but WITHOUT ANY WARRANTY; without even the implied warranty of
32 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33 * GNU General Public License for more details.
34 *
35 * You should have received a copy of the GNU General Public License
36 * along with this program. If not, see <https://www.gnu.org/licenses/>.
37 */
38
44require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
45require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
46require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
47require_once DOL_DOCUMENT_ROOT.'/product/class/productlang.class.php';
48require_once DOL_DOCUMENT_ROOT.'/product/stock/class/productlot.class.php';
49require_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php';
50
54class Product extends CommonObject
55{
60 const SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY = 1;
61 const SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY = 2;
62 const SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT = 3;
63
68 public $TRIGGER_PREFIX = 'PRODUCT';
69
73 public $element = 'product';
74
78 public $table_element = 'product';
79
83 public $fk_element = 'fk_product';
84
88 protected $childtables = array(
89 'supplier_proposaldet' => array('name' => 'SupplierProposal', 'parent' => 'supplier_proposal', 'parentkey' => 'fk_supplier_proposal'),
90 'propaldet' => array('name' => 'Proposal', 'parent' => 'propal', 'parentkey' => 'fk_propal'),
91 'commandedet' => array('name' => 'Order', 'parent' => 'commande', 'parentkey' => 'fk_commande'),
92 'facturedet' => array('name' => 'Invoice', 'parent' => 'facture', 'parentkey' => 'fk_facture'),
93 'contratdet' => array('name' => 'Contract', 'parent' => 'contrat', 'parentkey' => 'fk_contrat'),
94 'facture_fourn_det' => array('name' => 'SupplierInvoice', 'parent' => 'facture_fourn', 'parentkey' => 'fk_facture_fourn'),
95 'commande_fournisseurdet' => array('name' => 'SupplierOrder', 'parent' => 'commande_fournisseur', 'parentkey' => 'fk_commande'),
96 'mrp_production' => array('name' => 'Mo', 'parent' => 'mrp_mo', 'parentkey' => 'fk_mo', 'enabled' => 'isModEnabled("mrp")'),
97 'bom_bom' => array('name' => 'BOM', 'enabled' => 'isModEnabled("bom")'),
98 'bom_bomline' => array('name' => 'BOMLine', 'parent' => 'bom_bom', 'parentkey' => 'fk_bom', 'enabled' => 'isModEnabled("bom")'),
99 );
100
106 public $picto = 'product';
107
111 protected $table_ref_field = 'ref';
112
117 public $regeximgext = '\.gif|\.jpg|\.jpeg|\.png|\.bmp|\.webp|\.xpm|\.xbm|\.avif';
118
124 public $ref;
125
131 public $libelle;
132
138 public $label;
139
145 public $description;
146
152 public $other;
153
159 public $type = self::TYPE_PRODUCT;
160
166 public $price;
167
171 public $price_formated; // used by takepos/ajax/ajax.php
172
178 public $price_ttc;
179
183 public $price_ttc_formated; // used by takepos/ajax/ajax.php
184
190 public $price_min;
191
197 public $price_min_ttc;
198
203 public $price_base_type;
207 public $price_label;
208
210
213 public $multiprices = array();
217 public $multiprices_ttc = array();
221 public $multiprices_base_type = array();
225 public $multiprices_default_vat_code = array();
229 public $multiprices_min = array();
233 public $multiprices_min_ttc = array();
237 public $multiprices_tva_tx = array();
241 public $multiprices_recuperableonly = array();
242
244
247 public $price_by_qty;
251 public $prices_by_qty = array();
255 public $prices_by_qty_id = array();
259 public $prices_by_qty_list = array();
260
264 public $level;
265
269 public $multilangs = array();
270
274 public $default_vat_code;
275
279 public $tva_tx;
280
284 public $tva_npr = 0;
285
289 public $remise_percent;
290
294 public $localtax1_tx;
298 public $localtax2_tx;
302 public $localtax1_type;
306 public $localtax2_type;
307
308 // Properties set by get_buyprice() for return
309
313 public $desc_supplier;
317 public $vatrate_supplier;
321 public $default_vat_code_supplier;
322
326 public $fourn_multicurrency_price;
327
331 public $fourn_multicurrency_unitprice;
335 public $fourn_multicurrency_tx;
339 public $fourn_multicurrency_id;
343 public $fourn_multicurrency_code;
344
348 public $packaging;
349
350
357 public $lifetime;
358
365 public $qc_frequency;
366
372 public $stock_reel = 0;
373
379 public $stock_theorique;
380
386 public $cost_price;
387
393 public $pmp;
394
400 public $seuil_stock_alerte = 0;
401
405 public $desiredstock = 0;
406
410 public $duration_value;
414 public $duration_unit;
418 public $duration;
419
423 public $fk_default_workstation;
424
430 public $status = 0;
431
438 public $tosell;
439
445 public $status_buy = 0;
446
453 public $tobuy;
454
460 public $finished;
461
467 public $fk_default_bom;
468
474 public $product_fourn_price_id;
475
481 public $buyprice;
482
488 public $tobatch;
489
490
496 public $status_batch = 0;
497
503 public $sell_or_eat_by_mandatory = 0;
504
510 public $batch_mask = '';
511
517 public $customcode;
518
524 public $url;
525
527
530 public $weight;
531
535 public $weight_units; // scale -3, 0, 3, 6
539 public $length;
543 public $length_units; // scale -3, 0, 3, 6
547 public $width;
551 public $width_units; // scale -3, 0, 3, 6
555 public $height;
559 public $height_units; // scale -3, 0, 3, 6
563 public $surface;
567 public $surface_units; // scale -3, 0, 3, 6
571 public $volume;
575 public $volume_units; // scale -3, 0, 3, 6
576
580 public $net_measure;
584 public $net_measure_units; // scale -3, 0, 3, 6
585
589 public $accountancy_code_sell;
593 public $accountancy_code_sell_intra;
597 public $accountancy_code_sell_export;
601 public $accountancy_code_buy;
605 public $accountancy_code_buy_intra;
609 public $accountancy_code_buy_export;
610
614 public $barcode;
615
619 public $barcode_type;
620
624 public $barcode_type_code;
625
629 public $stats_propale = array();
630
634 public $stats_commande = array();
635
639 public $stats_contrat = array();
640
644 public $stats_facture = array();
645
649 public $stats_proposal_supplier = array();
650
654 public $stats_commande_fournisseur = array();
655
659 public $stats_expedition = array();
660
664 public $stats_reception = array();
665
669 public $stats_mo = array();
670
674 public $stats_bom = array();
675
679 public $stats_mrptoconsume = array();
680
684 public $stats_mrptoproduce = array();
685
689 public $stats_facturerec = array();
690
694 public $stats_facture_fournisseur = array();
695
699 public $stats_facturefournrec = array();
700
704 public $imgWidth;
708 public $imgHeight;
709
714 public $product_fourn_id;
715
720 public $product_id_already_linked;
721
726 public $nbphoto = 0;
727
731 public $stock_warehouse = array();
732
736 public $fk_default_warehouse;
737
741 public $fk_price_expression;
742
747 public $fourn_qty;
748
753 public $fourn_pu;
754
759 public $fourn_price_base_type;
760
764 public $fourn_socid;
765
771 public $ref_fourn;
772
776 public $ref_supplier;
777
783 public $fk_unit;
784
790 public $price_autogen = 0;
791
797 public $sousprods = array();
798
802 public $res;
803
804
810 public $is_object_used;
811
821 public $is_sousproduit_qty;
822
833 public $is_sousproduit_incdec;
834
838 public $mandatory_period;
839
845 public $stockable_product = 1;
846
875 public $fields = array(
876 'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'index' => 1, 'position' => 1, 'comment' => 'Id'),
877 'entity' => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'visible' => 0, 'default' => '1', 'notnull' => 1, 'index' => 1, 'position' => 5),
878 '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'),
879 'label' => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'showoncombobox' => 2, 'position' => 11, 'csslist' => 'tdoverflowmax250'),
880 'barcode' => array('type' => 'varchar(255)', 'label' => 'Barcode', 'enabled' => 'isModEnabled("barcode")', 'position' => 20, 'visible' => -1, 'showoncombobox' => 3, 'cssview' => 'tdwordbreak', 'csslist' => 'tdoverflowmax125'),
881 'fk_barcode_type' => array('type' => 'integer', 'label' => 'BarcodeType', 'enabled' => 1, 'position' => 21, 'notnull' => 0, 'visible' => -1,),
882 'note_public' => array('type' => 'html', 'label' => 'NotePublic', 'enabled' => 1, 'visible' => 0, 'position' => 61),
883 'note' => array('type' => 'html', 'label' => 'NotePrivate', 'enabled' => 1, 'visible' => 0, 'position' => 62),
884 'datec' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 500),
885 'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 501),
886 //'date_valid' =>array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-2, 'position'=>502),
887 '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'),
888 'fk_user_modif' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'UserModif', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'position' => 511),
889 //'fk_user_valid' =>array('type'=>'integer', 'label'=>'UserValidation', 'enabled'=>1, 'visible'=>-1, 'position'=>512),
890 'localtax1_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax1tx', 'enabled' => 1, 'position' => 150, 'notnull' => 0, 'visible' => -1,),
891 'localtax1_type' => array('type' => 'varchar(10)', 'label' => 'Localtax1type', 'enabled' => 1, 'position' => 155, 'notnull' => 1, 'visible' => -1,),
892 'localtax2_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax2tx', 'enabled' => 1, 'position' => 160, 'notnull' => 0, 'visible' => -1,),
893 'localtax2_type' => array('type' => 'varchar(10)', 'label' => 'Localtax2type', 'enabled' => 1, 'position' => 165, 'notnull' => 1, 'visible' => -1,),
894 'last_main_doc' => array('type' => 'varchar(255)', 'label' => 'LastMainDoc', 'enabled' => 1, 'visible' => -1, 'position' => 170),
895 'import_key' => array('type' => 'varchar(14)', 'label' => 'ImportId', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'index' => 0, 'position' => 1000),
896 //'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')),
897 //'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')),
898 'mandatory_period' => array('type' => 'integer', 'label' => 'mandatoryperiod', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'default' => '0', 'index' => 1, 'position' => 1000),
899 'stockable_product' => array('type' => 'integer', 'label' => 'stockable_product', 'enabled' => 1, 'visible' => 1, 'default' => '1', 'notnull' => 1, 'index' => 1, 'position' => 502),
900 );
901
905 const TYPE_PRODUCT = 0;
909 const TYPE_SERVICE = 1;
910
914 const DISABLED_STOCK = 0;
915 const ENABLED_STOCK = 1;
916
922 public function __construct($db)
923 {
924 $this->db = $db;
925
926 $this->ismultientitymanaged = 1;
927 $this->isextrafieldmanaged = 1;
928
929 $this->canvas = '';
930 }
931
937 public function check()
938 {
939 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
940 $this->ref = trim((string) $this->ref);
941 } else {
942 $this->ref = dol_sanitizeFileName(stripslashes($this->ref));
943 }
944
945 $err = 0;
946 if (dol_strlen(trim((string) $this->ref)) == 0) {
947 $err++;
948 }
949
950 if (dol_strlen(trim((string) $this->label)) == 0) {
951 $err++;
952 }
953
954 if ($err > 0) {
955 return 0;
956 } else {
957 return 1;
958 }
959 }
960
968 public function create($user, $notrigger = 0)
969 {
970 global $conf, $langs;
971
972 $error = 0;
973
974 // Clean parameters
975 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
976 $this->ref = trim((string) $this->ref);
977 } else {
978 $this->ref = dol_sanitizeFileName(dol_string_nospecial(trim((string) $this->ref)));
979 }
980 $this->label = trim((string) $this->label);
981 $this->price_ttc = (float) price2num($this->price_ttc);
982 $this->price = (float) price2num($this->price);
983 $this->price_min_ttc = (float) price2num($this->price_min_ttc);
984 $this->price_min = (float) price2num($this->price_min);
985 $this->price_label = trim((string) $this->price_label);
986 if (empty($this->tva_tx)) {
987 $this->tva_tx = 0;
988 }
989 if (empty($this->tva_npr)) {
990 $this->tva_npr = 0;
991 }
992 // Local taxes
993 if (empty($this->localtax1_tx)) {
994 $this->localtax1_tx = 0;
995 }
996 if (empty($this->localtax2_tx)) {
997 $this->localtax2_tx = 0;
998 }
999 if (empty($this->localtax1_type)) {
1000 $this->localtax1_type = '0';
1001 }
1002 if (empty($this->localtax2_type)) {
1003 $this->localtax2_type = '0';
1004 }
1005 // Price
1006 if (empty($this->price_base_type) && getDolGlobalString('PRODUCT_PRICE_BASE_TYPE')) {
1007 $this->price_base_type = getDolGlobalString('PRODUCT_PRICE_BASE_TYPE');
1008 }
1009 if (empty($this->price)) {
1010 $this->price = 0;
1011 }
1012 if (empty($this->price_min)) {
1013 $this->price_min = 0;
1014 }
1015 // Price by quantity
1016 if (empty($this->price_by_qty)) {
1017 $this->price_by_qty = 0;
1018 }
1019
1020 if (empty($this->status)) {
1021 $this->status = 0;
1022 }
1023 if (empty($this->status_buy)) {
1024 $this->status_buy = 0;
1025 }
1026 if (empty($this->stockable_product)) {
1027 $this->stockable_product = 0;
1028 }
1029
1030 $price_ht = 0;
1031 $price_ttc = 0;
1032 $price_min_ht = 0;
1033 $price_min_ttc = 0;
1034
1035 //
1036 if ($this->price_base_type == 'TTC' && $this->price_ttc > 0) {
1037 $price_ttc = price2num($this->price_ttc, 'MU');
1038 $price_ht = price2num($this->price_ttc / (1 + ($this->tva_tx / 100)), 'MU');
1039 }
1040
1041 //
1042 if ($this->price_base_type != 'TTC' && $this->price > 0) {
1043 $price_ht = price2num($this->price, 'MU');
1044 $price_ttc = price2num($this->price * (1 + ($this->tva_tx / 100)), 'MU');
1045 }
1046
1047 //
1048 if (($this->price_min_ttc > 0) && ($this->price_base_type == 'TTC')) {
1049 $price_min_ttc = price2num($this->price_min_ttc, 'MU');
1050 $price_min_ht = price2num($this->price_min_ttc / (1 + ($this->tva_tx / 100)), 'MU');
1051 }
1052
1053 //
1054 if (($this->price_min > 0) && ($this->price_base_type != 'TTC')) {
1055 $price_min_ht = price2num($this->price_min, 'MU');
1056 $price_min_ttc = price2num($this->price_min * (1 + ($this->tva_tx / 100)), 'MU');
1057 }
1058
1059 $this->accountancy_code_buy = trim((string) $this->accountancy_code_buy);
1060 $this->accountancy_code_buy_intra = trim((string) $this->accountancy_code_buy_intra);
1061 $this->accountancy_code_buy_export = trim((string) $this->accountancy_code_buy_export);
1062 $this->accountancy_code_sell = trim((string) $this->accountancy_code_sell);
1063 $this->accountancy_code_sell_intra = trim((string) $this->accountancy_code_sell_intra);
1064 $this->accountancy_code_sell_export = trim((string) $this->accountancy_code_sell_export);
1065
1066 // Normalize the accountancy codes the way the admin dropdown does it, so an API client that
1067 // sends '606111000' ends up with the same '606111' value the GUI stores (see issue #32343).
1068 // When ACCOUNTING_MANAGE_ZERO is on, trailing zeros are part of the code and must be kept.
1069 if (!getDolGlobalString('ACCOUNTING_MANAGE_ZERO')) {
1070 require_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
1071 $this->accountancy_code_buy = clean_account($this->accountancy_code_buy);
1072 $this->accountancy_code_buy_intra = clean_account($this->accountancy_code_buy_intra);
1073 $this->accountancy_code_buy_export = clean_account($this->accountancy_code_buy_export);
1074 $this->accountancy_code_sell = clean_account($this->accountancy_code_sell);
1075 $this->accountancy_code_sell_intra = clean_account($this->accountancy_code_sell_intra);
1076 $this->accountancy_code_sell_export = clean_account($this->accountancy_code_sell_export);
1077 }
1078
1079 // Barcode value
1080 $this->barcode = trim((string) $this->barcode);
1081 $this->mandatory_period = empty($this->mandatory_period) ? 0 : $this->mandatory_period;
1082 // Check parameters
1083 if (empty($this->label)) {
1084 $langs->load('errors');
1085 $this->errors[] = $langs->trans('ErrorMandatoryParametersNotProvided');
1086 return -1;
1087 }
1088
1089 if (empty($this->ref) || $this->ref == 'auto') {
1090 // Load object modCodeProduct
1091 $module = getDolGlobalString('PRODUCT_CODEPRODUCT_ADDON', 'mod_codeproduct_leopard');
1092 if ($module != 'mod_codeproduct_leopard') { // Do not load module file for leopard
1093 if (substr($module, 0, 16) == 'mod_codeproduct_' && substr($module, -3) == 'php') {
1094 $module = substr($module, 0, dol_strlen($module) - 4);
1095 }
1096 dol_include_once('/core/modules/product/'.$module.'.php');
1097 $modCodeProduct = new $module();
1098 '@phan-var-force ModeleProductCode $modCodeProduct';
1099 if (!empty($modCodeProduct->code_auto)) {
1100 $this->ref = $modCodeProduct->getNextValue($this, $this->type);
1101 }
1102 unset($modCodeProduct);
1103 }
1104
1105 if (empty($this->ref)) {
1106 $this->error = 'ProductModuleNotSetupForAutoRef';
1107 return -2;
1108 }
1109 }
1110
1111 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);
1112
1113 $now = dol_now();
1114
1115 if (empty($this->date_creation)) {
1116 $this->date_creation = $now;
1117 }
1118
1119 $this->db->begin();
1120
1121 // For automatic creation during create action (not used by Dolibarr GUI, can be used by scripts)
1122 if ($this->barcode == '-1' || $this->barcode == 'auto') {
1123 $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1124 }
1125
1126 // Check more parameters
1127 // If error, this->errors[] is filled
1128 $result = $this->verify();
1129
1130 if ($result >= 0) {
1131 $sql = "SELECT count(*) as nb";
1132 $sql .= " FROM ".$this->db->prefix()."product";
1133 $sql .= " WHERE entity IN (".getEntity('product').")";
1134 $sql .= " AND ref = '".$this->db->escape($this->ref)."'";
1135
1136 $result = $this->db->query($sql);
1137 if ($result) {
1138 $obj = $this->db->fetch_object($result);
1139 if ($obj->nb == 0) {
1140 // Insert new product, no previous one found
1141 $sql = "INSERT INTO ".$this->db->prefix()."product (";
1142 $sql .= "datec";
1143 $sql .= ", entity";
1144 $sql .= ", ref";
1145 $sql .= ", ref_ext";
1146 $sql .= ", price_min";
1147 $sql .= ", price_min_ttc";
1148 $sql .= ", label";
1149 $sql .= ", fk_user_author";
1150 $sql .= ", fk_product_type";
1151 $sql .= ", price";
1152 $sql .= ", price_ttc";
1153 $sql .= ", price_base_type";
1154 $sql .= ", price_label";
1155 $sql .= ", tobuy";
1156 $sql .= ", tosell";
1157 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1158 $sql .= ", accountancy_code_buy";
1159 $sql .= ", accountancy_code_buy_intra";
1160 $sql .= ", accountancy_code_buy_export";
1161 $sql .= ", accountancy_code_sell";
1162 $sql .= ", accountancy_code_sell_intra";
1163 $sql .= ", accountancy_code_sell_export";
1164 }
1165 $sql .= ", canvas";
1166 $sql .= ", finished";
1167 $sql .= ", tobatch";
1168 $sql .= ", sell_or_eat_by_mandatory";
1169 $sql .= ", batch_mask";
1170 $sql .= ", fk_unit";
1171 $sql .= ", mandatory_period";
1172 $sql .= ", stockable_product";
1173 if (!empty($this->default_vat_code)) {
1174 $sql .= ", default_vat_code";
1175 }
1176 $sql .= ") VALUES (";
1177 $sql .= "'".$this->db->idate($this->date_creation)."'";
1178 $sql .= ", ".(!empty($this->entity) ? (int) $this->entity : (int) $conf->entity);
1179 $sql .= ", '".$this->db->escape($this->ref)."'";
1180 $sql .= ", ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
1181 $sql .= ", ".price2num($price_min_ht);
1182 $sql .= ", ".price2num($price_min_ttc);
1183 $sql .= ", ".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null");
1184 $sql .= ", ".((int) $user->id);
1185 $sql .= ", ".((int) $this->type);
1186 $sql .= ", ".price2num($price_ht, 'MT');
1187 $sql .= ", ".price2num($price_ttc, 'MT');
1188 $sql .= ", '".$this->db->escape($this->price_base_type)."'";
1189 $sql .= ", ".(!empty($this->price_label) ? "'".$this->db->escape($this->price_label)."'" : "null");
1190 $sql .= ", ".((int) $this->status);
1191 $sql .= ", ".((int) $this->status_buy);
1192 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1193 $sql .= ", '".$this->db->escape($this->accountancy_code_buy)."'";
1194 $sql .= ", '".$this->db->escape($this->accountancy_code_buy_intra)."'";
1195 $sql .= ", '".$this->db->escape($this->accountancy_code_buy_export)."'";
1196 $sql .= ", '".$this->db->escape($this->accountancy_code_sell)."'";
1197 $sql .= ", '".$this->db->escape($this->accountancy_code_sell_intra)."'";
1198 $sql .= ", '".$this->db->escape($this->accountancy_code_sell_export)."'";
1199 }
1200 $sql .= ", '".$this->db->escape($this->canvas)."'";
1201 $sql .= ", ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? 'NULL' : (int) $this->finished);
1202 $sql .= ", ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : ((int) $this->status_batch));
1203 $sql .= ", ".((empty($this->sell_or_eat_by_mandatory) || $this->sell_or_eat_by_mandatory < 0) ? 0 : ((int) $this->sell_or_eat_by_mandatory));
1204 $sql .= ", '".$this->db->escape($this->batch_mask)."'";
1205 $sql .= ", ".($this->fk_unit > 0 ? ((int) $this->fk_unit) : 'NULL');
1206 $sql .= ", '".$this->db->escape((string) $this->mandatory_period)."'";
1207 $sql .= ", ".((int) $this->stockable_product);
1208 if (!empty($this->default_vat_code)) {
1209 $sql .= ", '".$this->db->escape($this->default_vat_code)."'";
1210 }
1211 $sql .= ")";
1212 dol_syslog(get_class($this)."::Create", LOG_DEBUG);
1213
1214 $result = $this->db->query($sql);
1215 if ($result) {
1216 $id = $this->db->last_insert_id($this->db->prefix()."product");
1217
1218 if ($id > 0) {
1219 $this->id = $id;
1220 $this->price = $price_ht;
1221 $this->price_ttc = $price_ttc;
1222 $this->price_min = $price_min_ht;
1223 $this->price_min_ttc = $price_min_ttc;
1224
1225 $result = $this->_log_price($user);
1226 if ($result > 0) {
1227 if ($this->update($id, $user, 1, 'add') <= 0) {
1228 $error++;
1229 }
1230 } else {
1231 $error++;
1232 $this->error = $this->db->lasterror();
1233 }
1234
1235 // update accountancy for this entity
1236 if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1237 $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " .((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1238
1239 $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1240 $sql .= " fk_product";
1241 $sql .= ", entity";
1242 $sql .= ", accountancy_code_buy";
1243 $sql .= ", accountancy_code_buy_intra";
1244 $sql .= ", accountancy_code_buy_export";
1245 $sql .= ", accountancy_code_sell";
1246 $sql .= ", accountancy_code_sell_intra";
1247 $sql .= ", accountancy_code_sell_export";
1248 $sql .= ") VALUES (";
1249 $sql .= ((int) $this->id);
1250 $sql .= ", " . ((int) $conf->entity);
1251 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1252 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1253 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1254 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1255 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1256 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1257 $sql .= ")";
1258 $result = $this->db->query($sql);
1259 if (!$result) {
1260 $error++;
1261 $this->error = 'ErrorFailedToInsertAccountancyForEntity';
1262 }
1263 }
1264 } else {
1265 $error++;
1266 $this->error = 'ErrorFailedToGetInsertedId';
1267 }
1268 } else {
1269 $error++;
1270 $this->error = $this->db->lasterror();
1271 }
1272 } else {
1273 // Product already exists with this ref
1274 $langs->load("products");
1275 $error++;
1276 $this->error = "ErrorProductAlreadyExists";
1277 dol_syslog(get_class($this)."::Create fails, ref ".$this->ref." already exists");
1278 }
1279 } else {
1280 $error++;
1281 $this->error = $this->db->lasterror();
1282 }
1283
1284 if (!$error && !$notrigger) {
1285 // Call trigger
1286 $result = $this->call_trigger('PRODUCT_CREATE', $user);
1287 if ($result < 0) {
1288 $error++;
1289 }
1290 // End call triggers
1291 }
1292
1293 if (!$error) {
1294 $this->db->commit();
1295 return $this->id;
1296 } else {
1297 $this->db->rollback();
1298 return -$error;
1299 }
1300 } else {
1301 $this->db->rollback();
1302 dol_syslog(get_class($this)."::Create fails verify ".implode(',', $this->errors), LOG_WARNING);
1303 return -3;
1304 }
1305 }
1306
1307
1314 public function verify()
1315 {
1316 global $langs;
1317
1318 $this->errors = array();
1319
1320 $result = 0;
1321 $this->ref = trim((string) $this->ref);
1322
1323 if (!$this->ref) {
1324 $this->errors[] = 'ErrorBadRef';
1325 $result = -2;
1326 }
1327
1328 $arrayofnonnegativevalue = array('weight' => 'Weight', 'width' => 'Width', 'height' => 'Height', 'length' => 'Length', 'surface' => 'Surface', 'volume' => 'Volume');
1329 foreach ($arrayofnonnegativevalue as $key => $value) {
1330 if (property_exists($this, $key) && !empty($this->$key) && ($this->$key < 0)) {
1331 $langs->loadLangs(array("main", "other"));
1332 $this->error = $langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv($value));
1333 $this->errors[] = $this->error;
1334 $result = -4;
1335 }
1336 }
1337
1338 $rescode = $this->check_barcode($this->barcode, $this->barcode_type_code);
1339 if ($rescode) {
1340 if ($rescode == -1) {
1341 $this->errors[] = 'ErrorBadBarCodeSyntax';
1342 } elseif ($rescode == -2) {
1343 $this->errors[] = 'ErrorBarCodeRequired';
1344 } elseif ($rescode == -3) {
1345 // Note: Common usage is to have barcode unique. For variants, we should have a different barcode.
1346 $this->errors[] = 'ErrorBarCodeAlreadyUsed';
1347 }
1348
1349 $result = -3;
1350 }
1351
1352 return $result;
1353 }
1354
1355 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1366 public function check_barcode($valuetotest, $typefortest)
1367 {
1368 // phpcs:enable
1369 global $conf;
1370
1371 if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
1372 $module = strtolower(getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM'));
1373
1374 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
1375 foreach ($dirsociete as $dirroot) {
1376 $res = dol_include_once($dirroot.$module.'.php');
1377 if ($res) {
1378 break;
1379 }
1380 }
1381
1382 $mod = new $module();
1383 '@phan-var-force ModeleNumRefBarCode $mod';
1386 dol_syslog(get_class($this)."::check_barcode value=".$valuetotest." type=".$typefortest." module=".$module);
1387 $result = $mod->verif($this->db, $valuetotest, $this, 0, $typefortest);
1388 return $result;
1389 } else {
1390 return 0;
1391 }
1392 }
1393
1405 public function update($id, $user, $notrigger = 0, $action = 'update', $updatetype = false)
1406 {
1407 global $langs, $conf, $hookmanager;
1408
1409 $error = 0;
1410
1411 // Check parameters
1412 if (!$this->label) {
1413 $this->label = 'MISSING LABEL';
1414 }
1415
1416 // Clean parameters
1417 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
1418 $this->ref = trim((string) $this->ref);
1419 } else {
1420 $this->ref = dol_string_nospecial(trim((string) $this->ref));
1421 }
1422 $this->label = trim((string) $this->label);
1423 $this->description = trim((string) $this->description);
1424 $this->note_private = (isset($this->note_private) ? trim($this->note_private) : null);
1425 $this->note_public = (isset($this->note_public) ? trim($this->note_public) : null);
1426 $this->net_measure = price2num($this->net_measure);
1427 $this->net_measure_units = (!is_numeric($this->net_measure_units) ? null : (int) $this->net_measure_units);
1428 $this->weight = price2num($this->weight);
1429 $this->weight_units = (!is_numeric($this->weight_units) ? null : (int) $this->weight_units);
1430 $this->length = price2num($this->length);
1431 $this->length_units = (!is_numeric($this->length_units) ? null : (int) $this->length_units);
1432 $this->width = price2num($this->width);
1433 $this->width_units = (!is_numeric($this->width_units) ? null : (int) $this->width_units);
1434 $this->height = price2num($this->height);
1435 $this->height_units = (!is_numeric($this->height_units) ? null : (int) $this->height_units);
1436 $this->surface = price2num($this->surface);
1437 $this->surface_units = (!is_numeric($this->surface_units) ? null : (int) $this->surface_units);
1438 $this->volume = price2num($this->volume);
1439 $this->volume_units = (!is_numeric($this->volume_units) ? null : (int) $this->volume_units);
1440
1441 // set unit not defined
1442 if (is_numeric($this->length_units)) {
1443 $this->width_units = $this->length_units; // Not used yet
1444 }
1445 if (is_numeric($this->length_units)) {
1446 $this->height_units = $this->length_units; // Not used yet
1447 }
1448
1449 // Automated compute surface and volume if not filled
1450 if (empty($this->surface) && !empty($this->length) && !empty($this->width) && $this->length_units == $this->width_units) {
1451 $this->surface = (float) $this->length * (float) $this->width;
1452 $this->surface_units = measuring_units_squared((int) $this->length_units);
1453 }
1454 if (empty($this->volume) && !empty($this->surface) && !empty($this->height) && $this->length_units == $this->height_units) {
1455 $this->volume = $this->surface * (float) $this->height;
1456 $this->volume_units = measuring_units_cubed((int) $this->height_units);
1457 }
1458
1459 if (empty($this->tva_tx)) {
1460 $this->tva_tx = 0;
1461 }
1462 if (empty($this->tva_npr)) {
1463 $this->tva_npr = 0;
1464 }
1465 if (empty($this->localtax1_tx)) {
1466 $this->localtax1_tx = 0;
1467 }
1468 if (empty($this->localtax2_tx)) {
1469 $this->localtax2_tx = 0;
1470 }
1471 if (empty($this->localtax1_type)) {
1472 $this->localtax1_type = '0';
1473 }
1474 if (empty($this->localtax2_type)) {
1475 $this->localtax2_type = '0';
1476 }
1477 if (empty($this->status)) {
1478 $this->status = 0;
1479 }
1480 if (empty($this->status_buy)) {
1481 $this->status_buy = 0;
1482 }
1483
1484 if (empty($this->country_id)) {
1485 $this->country_id = 0;
1486 }
1487 if (empty($this->country_id) && !empty($this->country_code)) {
1488 $country_id = getCountry($this->country_code, '3');
1489 $this->country_id = is_int($country_id) ? $country_id : 0;
1490 }
1491
1492 if (empty($this->state_id)) {
1493 $this->state_id = 0;
1494 }
1495
1496 if (empty($this->stockable_product)) {
1497 $this->stockable_product = 0;
1498 }
1499
1500 // For automatic creation during update action (not used by Dolibarr GUI, can be used by scripts)
1501 if ($this->barcode == '-1' || $this->barcode == 'auto') {
1502 $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1503 }
1504
1505 // Barcode value
1506 $this->barcode = (empty($this->barcode) ? '' : trim($this->barcode));
1507
1508 $this->accountancy_code_buy = trim((string) $this->accountancy_code_buy);
1509 $this->accountancy_code_buy_intra = (!empty($this->accountancy_code_buy_intra) ? trim($this->accountancy_code_buy_intra) : '');
1510 $this->accountancy_code_buy_export = trim((string) $this->accountancy_code_buy_export);
1511 $this->accountancy_code_sell = trim((string) $this->accountancy_code_sell);
1512 $this->accountancy_code_sell_intra = trim((string) $this->accountancy_code_sell_intra);
1513 $this->accountancy_code_sell_export = trim((string) $this->accountancy_code_sell_export);
1514
1515 // Normalize the accountancy codes the way the admin dropdown does it, so an API client that
1516 // sends '606111000' ends up with the same '606111' value the GUI stores (see issue #32343).
1517 // When ACCOUNTING_MANAGE_ZERO is on, trailing zeros are part of the code and must be kept.
1518 if (!getDolGlobalString('ACCOUNTING_MANAGE_ZERO')) {
1519 require_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
1520 $this->accountancy_code_buy = clean_account($this->accountancy_code_buy);
1521 $this->accountancy_code_buy_intra = clean_account($this->accountancy_code_buy_intra);
1522 $this->accountancy_code_buy_export = clean_account($this->accountancy_code_buy_export);
1523 $this->accountancy_code_sell = clean_account($this->accountancy_code_sell);
1524 $this->accountancy_code_sell_intra = clean_account($this->accountancy_code_sell_intra);
1525 $this->accountancy_code_sell_export = clean_account($this->accountancy_code_sell_export);
1526 }
1527
1528 $this->db->begin();
1529
1530 $result = 0;
1531 // Check name is required and codes are ok or unique. If error, this->errors[] is filled
1532 if ($action != 'add') {
1533 $result = $this->verify(); // We don't check when update called during a create because verify was already done
1534 } else {
1535 // we can continue
1536 $result = 0;
1537 }
1538
1539 if ($result >= 0) {
1540 // $this->oldcopy should have been set by the caller of update (here properties were already modified)
1541 // Note that this->oldcopy must be a Product instance (not stdClass), otherwise the method
1542 // hasbatch() called below will fatal. Callers may set oldcopy via dol_clone($obj, 2) (which
1543 // returns a stdClass with scalar properties only) and then we cannot call methods on it,
1544 // so re-clone with native=1 in that case (see issues #38663, #38638).
1545 if (is_null($this->oldcopy) || (is_object($this->oldcopy) && empty($this->oldcopy->id)) || !($this->oldcopy instanceof Product)) {
1546 $this->oldcopy = dol_clone($this, 1); // 1 to clone with methods to avoid fatal error with $this->oldcopy->hasbatch()
1547 }
1548 // Test if batch management is activated on existing product
1549 // If yes, we create missing entries into product_batch
1550 if ($this->hasbatch() && !$this->oldcopy->hasbatch()) {
1551 //$valueforundefinedlot = 'Undefined'; // In previous version, 39 and lower
1552 $valueforundefinedlot = '000000';
1553 if (getDolGlobalString('STOCK_DEFAULT_BATCH')) {
1554 $valueforundefinedlot = getDolGlobalString('STOCK_DEFAULT_BATCH');
1555 }
1556
1557 dol_syslog("Flag batch of product id=".$this->id." is set to ON, so we will create missing records into product_batch");
1558
1559 $this->load_stock();
1560 foreach ($this->stock_warehouse as $idW => $ObjW) { // For each warehouse where we have stocks defined for this product (for each lines in product_stock)
1561 $qty_batch = 0;
1562 foreach ($ObjW->detail_batch as $detail) { // Each lines of detail in product_batch of the current $ObjW = product_stock
1563 if ($detail->batch == $valueforundefinedlot || $detail->batch == 'Undefined') {
1564 // We discard this line, we will create it later
1565 $sqlclean = "DELETE FROM ".$this->db->prefix()."product_batch WHERE batch in('Undefined', '".$this->db->escape($valueforundefinedlot)."') AND fk_product_stock = ".((int) $ObjW->id);
1566 $result = $this->db->query($sqlclean);
1567 if (!$result) {
1568 dol_print_error($this->db);
1569 exit;
1570 }
1571 continue;
1572 }
1573
1574 $qty_batch += $detail->qty;
1575 }
1576 // Quantities in batch details are not same as stock quantity,
1577 // so we add a default batch record to complete and get same qty in parent and child table
1578 if ($ObjW->real != $qty_batch) {
1579 $ObjBatch = new Productbatch($this->db);
1580 $ObjBatch->batch = $valueforundefinedlot;
1581 $ObjBatch->qty = ($ObjW->real - $qty_batch);
1582 $ObjBatch->fk_product_stock = (int) $ObjW->id;
1583
1584 if ($ObjBatch->create($user, 1) < 0) {
1585 $error++;
1586 $this->errors = $ObjBatch->errors;
1587 } else {
1588 // we also add lot record if not exist
1589 $ObjLot = new Productlot($this->db);
1590 // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1591 if ($ObjLot->fetch(0, $this->id, $valueforundefinedlot) == 0) {
1592 $ObjLot->fk_product = $this->id;
1593 $ObjLot->entity = (int) $this->entity;
1594 $ObjLot->fk_user_creat = $user->id;
1595 $ObjLot->batch = $valueforundefinedlot;
1596 if ($ObjLot->create($user, 1) < 0) {
1597 $error++;
1598 $this->errors = $ObjLot->errors;
1599 }
1600 }
1601 }
1602 }
1603 }
1604 }
1605
1606 $sql = "UPDATE ".$this->db->prefix()."product";
1607 $sql .= " SET label = '".$this->db->escape($this->label)."'";
1608
1609 if ($updatetype && ($this->isProduct() || $this->isService())) {
1610 $sql .= ", fk_product_type = ".((int) $this->type);
1611 }
1612
1613 $sql .= ", ref = '".$this->db->escape($this->ref)."'";
1614 $sql .= ", ref_ext = ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
1615 $sql .= ", default_vat_code = ".($this->default_vat_code ? "'".$this->db->escape($this->default_vat_code)."'" : "null");
1616 $sql .= ", tva_tx = ".((float) $this->tva_tx);
1617 $sql .= ", recuperableonly = ".((int) $this->tva_npr);
1618 $sql .= ", localtax1_tx = ".((float) $this->localtax1_tx);
1619 $sql .= ", localtax2_tx = ".((float) $this->localtax2_tx);
1620 $sql .= ", localtax1_type = ".($this->localtax1_type != '' ? "'".$this->db->escape($this->localtax1_type)."'" : "'0'");
1621 $sql .= ", localtax2_type = ".($this->localtax2_type != '' ? "'".$this->db->escape($this->localtax2_type)."'" : "'0'");
1622
1623 $sql .= ", barcode = ".(empty($this->barcode) ? "null" : "'".$this->db->escape($this->barcode)."'");
1624 $sql .= ", fk_barcode_type = ".(empty($this->barcode_type) ? "null" : $this->db->escape((string) $this->barcode_type));
1625
1626 $sql .= ", tosell = ".(int) $this->status;
1627 $sql .= ", tobuy = ".(int) $this->status_buy;
1628 $sql .= ", tobatch = ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : (int) $this->status_batch);
1629 $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);
1630 $sql .= ", batch_mask = '".$this->db->escape($this->batch_mask)."'";
1631
1632 $sql .= ", finished = ".((!isset($this->finished) || $this->finished < 0 || $this->finished === '') ? "null" : (int) $this->finished);
1633 $sql .= ", fk_default_bom = ".((!isset($this->fk_default_bom) || $this->fk_default_bom < 0 || $this->fk_default_bom == '') ? "null" : (int) $this->fk_default_bom);
1634 $sql .= ", net_measure = ".($this->net_measure != '' ? "'".$this->db->escape($this->net_measure)."'" : 'null');
1635 $sql .= ", net_measure_units = ".((string) $this->net_measure_units != '' ? ((int) $this->net_measure_units) : 'null');
1636 $sql .= ", weight = ".($this->weight != '' ? "'".$this->db->escape($this->weight)."'" : 'null');
1637 $sql .= ", weight_units = ".((string) $this->weight_units != '' ? ((int) $this->weight_units) : 'null');
1638 $sql .= ", length = ".($this->length != '' ? "'".$this->db->escape($this->length)."'" : 'null');
1639 $sql .= ", length_units = ".((string) $this->length_units != '' ? ((int) $this->length_units) : 'null');
1640 $sql .= ", width= ".($this->width != '' ? "'".$this->db->escape($this->width)."'" : 'null');
1641 $sql .= ", width_units = ".((string) $this->width_units != '' ? ((int) $this->width_units) : 'null');
1642 $sql .= ", height = ".($this->height != '' ? "'".$this->db->escape($this->height)."'" : 'null');
1643 $sql .= ", height_units = ".((string) $this->height_units != '' ? ((int) $this->height_units) : 'null');
1644 $sql .= ", surface = ".($this->surface != '' ? "'".$this->db->escape($this->surface)."'" : 'null');
1645 $sql .= ", surface_units = ".((string) $this->surface_units != '' ? ((int) $this->surface_units) : 'null');
1646 $sql .= ", volume = ".($this->volume != '' ? "'".$this->db->escape($this->volume)."'" : 'null');
1647 $sql .= ", volume_units = ".((string) $this->volume_units != '' ? ((int) $this->volume_units) : 'null');
1648 $sql .= ", fk_default_warehouse = ".($this->fk_default_warehouse > 0 ? ((int) $this->fk_default_warehouse) : 'null');
1649 $sql .= ", fk_default_workstation = ".($this->fk_default_workstation > 0 ? ((int) $this->fk_default_workstation) : 'null');
1650 $sql .= ", seuil_stock_alerte = ".((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (float) $this->seuil_stock_alerte : 'null');
1651 $sql .= ", description = '".$this->db->escape($this->description)."'";
1652 $sql .= ", url = ".($this->url ? "'".$this->db->escape($this->url)."'" : 'null');
1653 $sql .= ", customcode = '".$this->db->escape($this->customcode)."'";
1654 $sql .= ", fk_country = ".($this->country_id > 0 ? (int) $this->country_id : 'null');
1655 $sql .= ", fk_state = ".($this->state_id > 0 ? (int) $this->state_id : 'null');
1656 $sql .= ", lifetime = ".($this->lifetime > 0 ? (int) $this->lifetime : 'null');
1657 $sql .= ", qc_frequency = ".($this->qc_frequency > 0 ? (int) $this->qc_frequency : 'null');
1658 $sql .= ", note = ".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : 'null');
1659 $sql .= ", note_public = ".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : 'null');
1660 $sql .= ", duration = '".$this->db->escape($this->duration_value.$this->duration_unit)."'";
1661 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1662 $sql .= ", accountancy_code_buy = '" . $this->db->escape($this->accountancy_code_buy) . "'";
1663 $sql .= ", accountancy_code_buy_intra = '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1664 $sql .= ", accountancy_code_buy_export = '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1665 $sql .= ", accountancy_code_sell= '" . $this->db->escape($this->accountancy_code_sell) . "'";
1666 $sql .= ", accountancy_code_sell_intra= '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1667 $sql .= ", accountancy_code_sell_export= '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1668 }
1669 $sql .= ", desiredstock = ".((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (float) $this->desiredstock : "null");
1670 $sql .= ", cost_price = ".($this->cost_price != '' ? ((float) $this->cost_price) : 'null');
1671 $sql .= ", fk_unit= ".(!$this->fk_unit ? 'NULL' : (int) $this->fk_unit);
1672 $sql .= ", price_autogen = ".(!$this->price_autogen ? 0 : 1);
1673 $sql .= ", fk_price_expression = ".($this->fk_price_expression != 0 ? (int) $this->fk_price_expression : 'NULL');
1674 $sql .= ", fk_user_modif = ".($user->id > 0 ? (int) $user->id : 'NULL');
1675 $sql .= ", mandatory_period = ".((int) $this->mandatory_period);
1676 $sql .= ", stockable_product = ".(int) $this->stockable_product;
1677 if (getDolGlobalString('PRODUCT_USE_CUSTOMER_PACKAGING')) {
1678 $sql .= ", packaging = ".(float) $this->packaging;
1679 }
1680
1681 // stock field is not here because it is a denormalized value from product_stock.
1682 $sql .= " WHERE rowid = ".((int) $id);
1683
1684 dol_syslog(get_class($this)."::update", LOG_DEBUG);
1685
1686 $resql = $this->db->query($sql);
1687 if ($resql) {
1688 $this->id = $id;
1689
1690 // Multilangs
1691 if (getDolGlobalInt('MAIN_MULTILANGS')) {
1692 if ($this->setMultiLangs($user, $notrigger) < 0) {
1693 $this->db->rollback();
1694 return -2;
1695 }
1696 }
1697
1698 $action = 'update';
1699
1700 // update accountancy for this entity
1701 if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1702 $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1703
1704 $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1705 $sql .= " fk_product";
1706 $sql .= ", entity";
1707 $sql .= ", accountancy_code_buy";
1708 $sql .= ", accountancy_code_buy_intra";
1709 $sql .= ", accountancy_code_buy_export";
1710 $sql .= ", accountancy_code_sell";
1711 $sql .= ", accountancy_code_sell_intra";
1712 $sql .= ", accountancy_code_sell_export";
1713 $sql .= ") VALUES (";
1714 $sql .= ((int) $this->id);
1715 $sql .= ", " . ((int) $conf->entity);
1716 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1717 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1718 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1719 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1720 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1721 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1722 $sql .= ")";
1723 $result = $this->db->query($sql);
1724 if (!$result) {
1725 $error++;
1726 $this->error = 'ErrorFailedToUpdateAccountancyForEntity';
1727 }
1728 }
1729
1730 if (!$this->hasbatch() && $this->oldcopy->hasbatch()) {
1731 // Selection of all product stock movements that contains batchs
1732 $sql = 'SELECT pb.qty, ps.fk_entrepot, pb.batch FROM '.MAIN_DB_PREFIX.'product_batch as pb';
1733 $sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'product_stock as ps ON (ps.rowid = pb.fk_product_stock)';
1734 $sql .= ' WHERE ps.fk_product = '.(int) $this->id;
1735
1736 $resql = $this->db->query($sql);
1737 if ($resql) {
1738 $inventorycode = dol_print_date(dol_now(), '%Y%m%d%H%M%S');
1739
1740 while ($obj = $this->db->fetch_object($resql)) {
1741 $value = $obj->qty;
1742 $fk_entrepot = $obj->fk_entrepot;
1743 $price = 0;
1744 $dlc = '';
1745 $dluo = '';
1746 $batch = $obj->batch;
1747
1748 // To know how to revert stockMouvement (add or remove)
1749 $addOremove = $value > 0 ? 1 : 0; // 1 if remove, 0 if add
1750 $label = $langs->trans('BatchStockMouvementAddInGlobal');
1751 $res = $this->correct_stock_batch($user, $fk_entrepot, abs($value), $addOremove, $label, $price, $dlc, $dluo, $batch, $inventorycode, '', null, 0, null, true);
1752
1753 if ($res > 0) {
1754 $label = $langs->trans('BatchStockMouvementAddInGlobal');
1755 $res = $this->correct_stock($user, $fk_entrepot, abs($value), (int) empty($addOremove), $label, $price, $inventorycode, '', null, 0);
1756 if ($res < 0) {
1757 $error++;
1758 }
1759 } else {
1760 $error++;
1761 }
1762 }
1763 }
1764 }
1765
1766 // Actions on extra fields
1767 if (!$error) {
1768 $result = $this->insertExtraFields();
1769 if ($result < 0) {
1770 $error++;
1771 }
1772 }
1773
1774 if (!$error && !$notrigger) {
1775 // Call trigger
1776 $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1777 if ($result < 0) {
1778 $error++;
1779 }
1780 // End call triggers
1781 }
1782
1783 if (!$error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref)) {
1784 // We remove directory
1785 if ($conf->product->dir_output) {
1786 $olddir = $conf->product->dir_output."/".dol_sanitizeFileName($this->oldcopy->ref);
1787 $newdir = $conf->product->dir_output."/".dol_sanitizeFileName($this->ref);
1788 if (file_exists($olddir)) {
1789 // include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1790 // $res = dol_move($olddir, $newdir);
1791 // do not use dol_move with directory
1792 $res = @rename($olddir, $newdir);
1793 if (!$res) {
1794 $langs->load("errors");
1795 $this->error = $langs->trans('ErrorFailToRenameDir', $olddir, $newdir);
1796 $error++;
1797 } else {
1798 // to keep old entries with the new dir
1799 require_once DOL_DOCUMENT_ROOT . '/ecm/class/ecmfiles.class.php';
1800 $ecmfiles = new EcmFiles($this->db);
1801 $ecmfiles->updateAfterRename("produit/".dol_sanitizeFileName($this->oldcopy->ref), "produit/".dol_sanitizeFileName($this->ref));
1802 }
1803 }
1804 }
1805 }
1806
1807 if (!$error) {
1808 if (isModEnabled('variants')) {
1809 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1810
1811 $comb = new ProductCombination($this->db);
1812
1813 foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) {
1814 $currcomb->updateProperties($this, $user);
1815 }
1816 }
1817
1818 $this->db->commit();
1819 return 1;
1820 } else {
1821 $this->db->rollback();
1822 return -$error;
1823 }
1824 } else {
1825 if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1826 $langs->load("errors");
1827 if (empty($conf->barcode->enabled) || empty($this->barcode)) {
1828 $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductAlreadyExists", $this->ref);
1829 } else {
1830 $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductBarCodeAlreadyExists", $this->barcode);
1831 }
1832 $this->errors[] = $this->error;
1833 $this->db->rollback();
1834 return -1;
1835 } else {
1836 $this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1837 $this->errors[] = $this->error;
1838 $this->db->rollback();
1839 return -2;
1840 }
1841 }
1842 } else {
1843 $this->db->rollback();
1844 dol_syslog(get_class($this)."::Update fails verify ".implode(',', $this->errors), LOG_WARNING);
1845 return -3;
1846 }
1847 }
1848
1856 public function delete(User $user, $notrigger = 0)
1857 {
1858 global $conf;
1859 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1860
1861 $error = 0;
1862
1863 // Check parameters
1864 if (empty($this->id)) {
1865 $this->error = "Object must be fetched before calling delete";
1866 return -1;
1867 }
1868 if (($this->isProduct() && !$user->hasRight('produit', 'supprimer')) || ($this->isService() && !$user->hasRight('service', 'supprimer'))) {
1869 $this->error = "ErrorForbidden";
1870 return 0;
1871 }
1872
1873 $objectisused = $this->isObjectUsed($this->id);
1874 if (empty($objectisused)) {
1875 $this->db->begin();
1876
1877 if (empty($notrigger)) {
1878 // Call trigger
1879 $result = $this->call_trigger('PRODUCT_DELETE', $user);
1880 if ($result < 0) {
1881 $error++;
1882 }
1883 // End call triggers
1884 }
1885
1886 // Delete from product_batch on product delete
1887 if (!$error) {
1888 $sql = "DELETE FROM ".$this->db->prefix().'product_batch';
1889 $sql .= " WHERE fk_product_stock IN (";
1890 $sql .= "SELECT rowid FROM ".$this->db->prefix().'product_stock';
1891 $sql .= " WHERE fk_product = ".((int) $this->id).")";
1892
1893 $result = $this->db->query($sql);
1894 if (!$result) {
1895 $error++;
1896 $this->errors[] = $this->db->lasterror();
1897 }
1898 }
1899
1900 // Delete pricelogs so we don't have any orphan rows, see #38383
1901 if (!$error) {
1902 $sql = "DELETE FROM " . $this->db->prefix() . "product_fournisseur_price_log";
1903 $sql .= " WHERE fk_product_fournisseur IN (";
1904 $sql .= " SELECT rowid FROM " . $this->db->prefix() . "product_fournisseur_price";
1905 $sql .= " WHERE fk_product = " . ((int) $this->id);
1906 $sql .= " )";
1907
1908 $resql = $this->db->query($sql);
1909 if (!$resql) {
1910 $error++;
1911 $this->errors[] = $this->db->lasterror();
1912 }
1913 }
1914
1915 // Delete the extrafields of the translations (llx_product_lang_extrafields has no foreign key)
1916 if (!$error) {
1917 $sql = "DELETE FROM ".$this->db->prefix()."product_lang_extrafields";
1918 $sql .= " WHERE fk_object IN (SELECT rowid FROM ".$this->db->prefix()."product_lang WHERE fk_product = ".((int) $this->id).")";
1919 $result = $this->db->query($sql);
1920 if (!$result) {
1921 $error++;
1922 $this->errors[] = $this->db->lasterror();
1923 }
1924 }
1925
1926 // Delete all child tables
1927 if (!$error) {
1928 $elements = array('product_fournisseur_price', 'product_price', 'product_lang', 'categorie_product', 'product_stock', 'product_customer_price', 'product_lot'); // product_batch is done before
1929 foreach ($elements as $table) {
1930 if (!$error) {
1931 $sql = "DELETE FROM ".$this->db->prefix().$table;
1932 $sql .= " WHERE fk_product = ".(int) $this->id;
1933
1934 $result = $this->db->query($sql);
1935 if (!$result) {
1936 $error++;
1937 $this->errors[] = $this->db->lasterror();
1938 }
1939 }
1940 }
1941 }
1942
1943 if (!$error && isModEnabled('variants')) {
1944 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1945 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
1946
1947 // If it is a parent product, then we remove the association with child products
1948 $prodcomb = new ProductCombination($this->db);
1949
1950 if ($prodcomb->deleteByFkProductParent($user, $this->id) < 0) {
1951 $error++;
1952 $this->errors[] = 'Error deleting combinations';
1953 }
1954
1955 // We also check if it is a child product
1956 if (!$error && ($prodcomb->fetchByFkProductChild($this->id) > 0) && ($prodcomb->delete($user) < 0)) {
1957 $error++;
1958 $this->errors[] = 'Error deleting child combination';
1959 }
1960 }
1961
1962 // Rows may survive the deactivation of the module, and the foreign key on
1963 // fk_product_parent would then refuse to delete the product. Only the links are
1964 // removed here, never a product: deleting variant products silently is the job of the
1965 // block above, which runs when the user can actually see them. Missing tables are
1966 // tolerated by deleteLinksByProduct(): they only exist once the module was enabled.
1967 if (!$error) {
1968 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1969
1970 $prodcomb = new ProductCombination($this->db);
1971 if ($prodcomb->deleteLinksByProduct($this->id) < 0) {
1972 $error++;
1973 $this->errors[] = $prodcomb->error;
1974 }
1975 }
1976
1977 // Delete from product_association
1978 if (!$error) {
1979 $sql = "DELETE FROM ".$this->db->prefix()."product_association";
1980 $sql .= " WHERE fk_product_pere = ".(int) $this->id." OR fk_product_fils = ".(int) $this->id;
1981
1982 $result = $this->db->query($sql);
1983 if (!$result) {
1984 $error++;
1985 $this->errors[] = $this->db->lasterror();
1986 }
1987 }
1988
1989 // Remove extrafields
1990 if (!$error) {
1991 $result = $this->deleteExtraFields();
1992 if ($result < 0) {
1993 $error++;
1994 dol_syslog(get_class($this)."::delete error -4 ".$this->error, LOG_ERR);
1995 }
1996 }
1997
1998 // Delete product
1999 if (!$error) {
2000 $sqlz = "DELETE FROM ".$this->db->prefix()."product";
2001 $sqlz .= " WHERE rowid = ".(int) $this->id;
2002
2003 $resultz = $this->db->query($sqlz);
2004 if (!$resultz) {
2005 $error++;
2006 $this->errors[] = $this->db->lasterror();
2007 }
2008 }
2009
2010 // Delete record into ECM index and physically
2011 if (!$error) {
2012 $res = $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
2013 $res = $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
2014 if (!$res) {
2015 $error++;
2016 }
2017 }
2018
2019 if (!$error) {
2020 // We remove directory
2021 $ref = dol_sanitizeFileName($this->ref);
2022 if ($conf->product->dir_output) {
2023 $dir = $conf->product->dir_output."/".$ref;
2024 if (file_exists($dir)) {
2025 $res = @dol_delete_dir_recursive($dir);
2026 if (!$res) {
2027 $this->errors[] = 'ErrorFailToDeleteDir';
2028 $error++;
2029 }
2030 }
2031 }
2032 }
2033
2034 if (!$error) {
2035 $this->db->commit();
2036 return 1;
2037 } else {
2038 foreach ($this->errors as $errmsg) {
2039 dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
2040 $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
2041 }
2042 $this->db->rollback();
2043 return -$error;
2044 }
2045 } else {
2046 $this->error = "ErrorRecordIsUsedCantDelete";
2047 return 0;
2048 }
2049 }
2050
2056 public static function getSellOrEatByMandatoryList()
2057 {
2058 global $langs;
2059
2060 $sellByLabel = $langs->trans('SellByDate');
2061 $eatByLabel = $langs->trans('EatByDate');
2062 return array(
2063 self::SELL_OR_EAT_BY_MANDATORY_ID_NONE => $langs->trans('BatchSellOrEatByMandatoryNone'),
2064 self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY => $sellByLabel,
2065 self::SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY => $eatByLabel,
2066 self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT => $langs->trans('BatchSellOrEatByMandatoryAll', $sellByLabel, $eatByLabel),
2067 );
2068 }
2069
2076 {
2077 $sellOrEatByMandatoryLabel = '';
2078
2079 $sellOrEatByMandatoryList = self::getSellOrEatByMandatoryList();
2080 if (isset($sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory])) {
2081 $sellOrEatByMandatoryLabel = $sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory];
2082 }
2083
2084 return $sellOrEatByMandatoryLabel;
2085 }
2086
2094 public function setMultiLangs($user, $notrigger = 0)
2095 {
2096 global $langs;
2097
2098 $langs_available = $langs->get_available_languages(DOL_DOCUMENT_ROOT, 0, 2);
2099 $current_lang = $langs->getDefaultLang();
2100
2101 foreach ($langs_available as $key => $value) {
2102 if ($key == $current_lang) {
2103 $sql = "SELECT rowid";
2104 $sql .= " FROM ".$this->db->prefix()."product_lang";
2105 $sql .= " WHERE fk_product = ".((int) $this->id);
2106 $sql .= " AND lang = '".$this->db->escape($key)."'";
2107
2108 $result = $this->db->query($sql);
2109 $objlang = ($result ? $this->db->fetch_object($result) : null);
2110 $rowidlang = ($objlang ? (int) $objlang->rowid : 0);
2111
2112 if ($objlang) { // if there is already a description line for this language
2113 $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
2114 $sql2 .= " SET ";
2115 $sql2 .= " label='".$this->db->escape($this->label)."',";
2116 $sql2 .= " description='".$this->db->escape($this->description)."'";
2117 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2118 $sql2 .= ", note='".$this->db->escape($this->other)."'";
2119 }
2120 $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
2121 } else {
2122 $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
2123 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2124 $sql2 .= ", note";
2125 }
2126 $sql2 .= ")";
2127 $sql2 .= " VALUES(".((int) $this->id).",'".$this->db->escape($key)."','".$this->db->escape($this->label)."',";
2128 $sql2 .= " '".$this->db->escape($this->description)."'";
2129 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2130 $sql2 .= ", '".$this->db->escape($this->other)."'";
2131 }
2132 $sql2 .= ")";
2133 }
2134 dol_syslog(get_class($this).'::setMultiLangs key = current_lang = '.$key);
2135 if (!$this->db->query($sql2)) {
2136 $this->error = $this->db->lasterror();
2137 return -1;
2138 }
2139 if (empty($rowidlang)) {
2140 $rowidlang = (int) $this->db->last_insert_id($this->db->prefix()."product_lang");
2141 }
2142 if ($this->setMultiLangsExtrafields($key, $rowidlang) < 0) {
2143 return -1;
2144 }
2145 } elseif (isset($this->multilangs[$key])) {
2146 if (empty($this->multilangs[$key]["label"])) {
2147 $this->errors[] = $key . ' : ' . $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Label"));
2148 return -1;
2149 }
2150
2151 $sql = "SELECT rowid";
2152 $sql .= " FROM ".$this->db->prefix()."product_lang";
2153 $sql .= " WHERE fk_product = ".((int) $this->id);
2154 $sql .= " AND lang = '".$this->db->escape($key)."'";
2155
2156 $result = $this->db->query($sql);
2157 $objlang = ($result ? $this->db->fetch_object($result) : null);
2158 $rowidlang = ($objlang ? (int) $objlang->rowid : 0);
2159
2160 if ($objlang) { // if there is already a description line for this language
2161 $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
2162 $sql2 .= " SET ";
2163 $sql2 .= " label = '".$this->db->escape($this->multilangs["$key"]["label"])."',";
2164 $sql2 .= " description = '".$this->db->escape($this->multilangs["$key"]["description"])."'";
2165 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2166 // @phan-suppress-next-line PhanTypeInvalidDimOffset
2167 $sql2 .= ", note = '".$this->db->escape($this->multilangs["$key"]["other"])."'";
2168 }
2169 $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
2170 } else {
2171 $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
2172 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2173 $sql2 .= ", note";
2174 }
2175 $sql2 .= ")";
2176 $sql2 .= " VALUES(".((int) $this->id).",'".$this->db->escape($key)."','".$this->db->escape($this->multilangs["$key"]["label"])."',";
2177 $sql2 .= " '".$this->db->escape($this->multilangs["$key"]["description"])."'";
2178 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2179 // @phan-suppress-next-line PhanTypeInvalidDimOffset
2180 $sql2 .= ", '".$this->db->escape($this->multilangs["$key"]["other"])."'";
2181 }
2182 $sql2 .= ")";
2183 }
2184
2185 // We do not save if main fields are empty
2186 if ($this->multilangs["$key"]["label"] || $this->multilangs["$key"]["description"]) {
2187 if (!$this->db->query($sql2)) {
2188 $this->error = $this->db->lasterror();
2189 return -1;
2190 }
2191 if (empty($rowidlang)) {
2192 $rowidlang = (int) $this->db->last_insert_id($this->db->prefix()."product_lang");
2193 }
2194 if ($this->setMultiLangsExtrafields($key, $rowidlang) < 0) {
2195 return -1;
2196 }
2197 }
2198 } else {
2199 // language is not current language and we didn't provide a multilang description for this language
2200 }
2201 }
2202
2203 if (empty($notrigger)) {
2204 // Call trigger
2205 $result = $this->call_trigger('PRODUCT_SET_MULTILANGS', $user);
2206 if ($result < 0) {
2207 $this->error = $this->db->lasterror();
2208 return -1;
2209 }
2210 // End call triggers
2211 }
2212
2213 return 1;
2214 }
2215
2224 protected function setMultiLangsExtrafields($langcode, $rowidlang)
2225 {
2226 if (empty($rowidlang) || !isset($this->multilangs[$langcode]['array_options']) || !is_array($this->multilangs[$langcode]['array_options'])) {
2227 return 0;
2228 }
2229
2230 $productlang = new ProductLang($this->db);
2231 $productlang->id = $rowidlang;
2232 $productlang->array_options = $this->multilangs[$langcode]['array_options'];
2233 $result = $productlang->insertExtraFields();
2234 if ($result < 0) {
2235 $this->error = $productlang->error;
2236 $this->errors = array_merge($this->errors, $productlang->errors);
2237 dol_syslog(get_class($this).'::setMultiLangsExtrafields error='.$this->error, LOG_ERR);
2238 return -1;
2239 }
2240
2241 return 1;
2242 }
2243
2253 public function delMultiLangs($langtodelete, $user, $notrigger = 0)
2254 {
2255 dol_syslog(get_class($this).'::delMultiLangs', LOG_DEBUG);
2256
2257 // Delete the extrafields of the translation first (llx_product_lang_extrafields has no foreign key)
2258 $sql = "DELETE FROM ".$this->db->prefix()."product_lang_extrafields";
2259 $sql .= " WHERE fk_object IN (SELECT rowid FROM ".$this->db->prefix()."product_lang";
2260 $sql .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($langtodelete)."')";
2261 $result = $this->db->query($sql);
2262 if (!$result) {
2263 $this->error = $this->db->lasterror();
2264 dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
2265 return -1;
2266 }
2267
2268 $sql = "DELETE FROM ".$this->db->prefix()."product_lang";
2269 $sql .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($langtodelete)."'";
2270
2271 $result = $this->db->query($sql);
2272 if ($result) {
2273 if (empty($notrigger)) {
2274 // Call trigger
2275 $result = $this->call_trigger('PRODUCT_DEL_MULTILANGS', $user);
2276 if ($result < 0) {
2277 $this->error = $this->db->lasterror();
2278 dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
2279 return -1;
2280 }
2281 // End call triggers
2282 }
2283 unset($this->multilangs[$langtodelete]);
2284 return 1;
2285 } else {
2286 $this->error = $this->db->lasterror();
2287 dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
2288 return -1;
2289 }
2290 }
2291
2300 public function setAccountancyCode($type, $value)
2301 {
2302 global $user;
2303
2304 $error = 0;
2305
2306 $this->db->begin();
2307
2308 if ($type == 'buy') {
2309 $field = 'accountancy_code_buy';
2310 } elseif ($type == 'buy_intra') {
2311 $field = 'accountancy_code_buy_intra';
2312 } elseif ($type == 'buy_export') {
2313 $field = 'accountancy_code_buy_export';
2314 } elseif ($type == 'sell') {
2315 $field = 'accountancy_code_sell';
2316 } elseif ($type == 'sell_intra') {
2317 $field = 'accountancy_code_sell_intra';
2318 } elseif ($type == 'sell_export') {
2319 $field = 'accountancy_code_sell_export';
2320 } else {
2321 return -1;
2322 }
2323
2324 $sql = "UPDATE ".$this->db->prefix().$this->table_element." SET ";
2325 $sql .= "$field = '".$this->db->escape($value)."'";
2326 $sql .= " WHERE rowid = ".((int) $this->id);
2327
2328 dol_syslog(__METHOD__, LOG_DEBUG);
2329 $resql = $this->db->query($sql);
2330
2331 if ($resql) {
2332 // Call trigger
2333 $result = $this->call_trigger('PRODUCT_MODIFY', $user);
2334 if ($result < 0) {
2335 $error++;
2336 }
2337 // End call triggers
2338
2339 if ($error) {
2340 $this->db->rollback();
2341 return -1;
2342 }
2343
2344 $this->$field = $value;
2345
2346 $this->db->commit();
2347 return 1;
2348 } else {
2349 $this->error = $this->db->lasterror();
2350 $this->db->rollback();
2351 return -1;
2352 }
2353 }
2354
2361 public function getMultiLangs($loadextrafields = 0)
2362 {
2363 global $langs;
2364
2365 $current_lang = '';
2366 if ($langs instanceOf Translate) {
2367 $current_lang = $langs->getDefaultLang();
2368 }
2369
2370 $sql = "SELECT rowid, lang, label, description, note as other";
2371 $sql .= " FROM ".$this->db->prefix()."product_lang";
2372 $sql .= " WHERE fk_product = ".((int) $this->id);
2373
2374 $result = $this->db->query($sql);
2375 if ($result) {
2376 while ($obj = $this->db->fetch_object($result)) {
2377 //print 'lang='.$obj->lang.' current='.$current_lang.'<br>';
2378 if ($obj->lang == $current_lang) { // si on a les traduct. dans la langue courante on les charge en infos principales.
2379 $this->label = $obj->label;
2380 $this->description = $obj->description;
2381 $this->other = $obj->other;
2382 }
2383 $this->multilangs[(string) $obj->lang]["rowid"] = (int) $obj->rowid;
2384 $this->multilangs[(string) $obj->lang]["label"] = $obj->label;
2385 $this->multilangs[(string) $obj->lang]["description"] = $obj->description;
2386 $this->multilangs[(string) $obj->lang]["other"] = $obj->other;
2387 }
2388
2389 // Load the extrafields of each translation, only when asked, because this method is called by fetch()
2390 if ($loadextrafields && !empty($this->multilangs)) {
2391 $extrafields = new ExtraFields($this->db);
2392 $extrafields->fetch_name_optionals_label('product_lang');
2393 if (!empty($extrafields->attributes['product_lang']['count'])) {
2394 $productlang = new ProductLang($this->db);
2395 foreach ($this->multilangs as $langcode => $value) {
2396 $productlang->id = $value['rowid'];
2397 $productlang->array_options = array();
2398 $productlang->fetch_optionals();
2399 $this->multilangs[$langcode]['array_options'] = $productlang->array_options;
2400 }
2401 }
2402 }
2403
2404 return 1;
2405 } else {
2406 $this->error = "Error: ".$this->db->lasterror()." - ".$sql;
2407 return -1;
2408 }
2409 }
2410
2417 private function getArrayForPriceCompare($level = 0)
2418 {
2419 $testExit = array('multiprices','multiprices_ttc','multiprices_base_type','multiprices_min','multiprices_min_ttc','multiprices_tva_tx','multiprices_recuperableonly');
2420
2421 foreach ($testExit as $field) {
2422 if (!isset($this->$field)) {
2423 return array();
2424 }
2425 $tmparray = $this->$field;
2426 if (!isset($tmparray[$level])) {
2427 return array();
2428 }
2429 }
2430
2431 $lastPrice = array(
2432 'level' => $level ? $level : 1,
2433 'multiprices' => (float) $this->multiprices[$level],
2434 'multiprices_ttc' => (float) $this->multiprices_ttc[$level],
2435 'multiprices_base_type' => $this->multiprices_base_type[$level],
2436 'multiprices_min' => (float) $this->multiprices_min[$level],
2437 'multiprices_min_ttc' => (float) $this->multiprices_min_ttc[$level],
2438 'multiprices_tva_tx' => (float) $this->multiprices_tva_tx[$level],
2439 'multiprices_recuperableonly' => (float) $this->multiprices_recuperableonly[$level],
2440 );
2441
2442 return $lastPrice;
2443 }
2444
2445
2446 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2454 private function _log_price($user, $level = 0)
2455 {
2456 // phpcs:enable
2457 global $conf;
2458
2459 $now = dol_now();
2460
2461 // Clean parameters
2462 if (empty($this->price_by_qty)) {
2463 $this->price_by_qty = 0;
2464 }
2465
2466 // Add new price
2467 $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,";
2468 $sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, price_min,price_min_ttc,price_by_qty,entity,fk_price_expression) ";
2469 $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)."'").",";
2470 $sql .= " ".((float) price2num($this->price)).", ".((float) price2num($this->price_ttc)).",'".$this->db->escape($this->price_base_type)."',".((int) $this->status).", ".((float) price2num($this->tva_tx)).",";
2471 $sql .= " ".($this->default_vat_code ? ("'".$this->db->escape($this->default_vat_code)."'") : "null").", ".((int) $this->tva_npr).",";
2472 $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');
2473 $sql .= ")";
2474
2475 dol_syslog(get_class($this)."::_log_price", LOG_DEBUG);
2476 $resql = $this->db->query($sql);
2477 if (!$resql) {
2478 $this->error = $this->db->lasterror();
2479 dol_print_error($this->db);
2480 return -1;
2481 } else {
2482 return 1;
2483 }
2484 }
2485
2486
2487 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2495 public function log_price_delete($user, $rowid)
2496 {
2497 // phpcs:enable
2498 $sql = "DELETE FROM ".$this->db->prefix()."product_price_by_qty";
2499 $sql .= " WHERE fk_product_price = ".((int) $rowid);
2500 $sql .= " AND fk_product_price IN (SELECT pp.rowid FROM ".$this->db->prefix()."product_price as pp WHERE pp.fk_product = ".((int) $this->id).")";
2501 $resql = $this->db->query($sql);
2502
2503 $sql = "DELETE FROM ".$this->db->prefix()."product_price_extrafields";
2504 $sql .= " WHERE fk_object = ".((int) $rowid);
2505 $resql = $this->db->query($sql);
2506
2507 $sql = "DELETE FROM ".$this->db->prefix()."product_price";
2508 $sql .= " WHERE rowid = ".((int) $rowid);
2509 $sql .= " AND fk_product = ".((int) $this->id);
2510 $resql = $this->db->query($sql);
2511 if ($resql) {
2512 return 1;
2513 } else {
2514 $this->error = $this->db->lasterror();
2515 return -1;
2516 }
2517 }
2518
2527 public function fetchAllPriceLogs($id)
2528 {
2529 dol_syslog(__METHOD__, LOG_DEBUG);
2530
2531 $logs = array();
2532
2533 $sql = "SELECT";
2534 $sql .= " pl.rowid as log_rowid,";
2535 $sql .= " pl.datec,";
2536 $sql .= " pl.price,";
2537 $sql .= " pl.quantity,";
2538 $sql .= " pl.fk_user,";
2539 $sql .= " pl.multicurrency_code,";
2540 $sql .= " pl.multicurrency_price,";
2541 // Supplier info
2542 $sql .= " pp.fk_soc as supplier_id,";
2543 $sql .= " pp.ref_fourn as supplier_ref,";
2544 $sql .= " pp.entity as entity";
2545
2546 $sql .= " FROM " . $this->db->prefix() . "product_fournisseur_price_log as pl";
2547 $sql .= " INNER JOIN " . $this->db->prefix() . "product_fournisseur_price as pp ON pl.fk_product_fournisseur = pp.rowid";
2548
2549 $sql .= " WHERE pp.fk_product = " . ((int) $id);
2550 $sql .= " AND pp.entity IN (" . getEntity('product') . ")";
2551 $sql .= " ORDER BY pl.datec DESC";
2552
2553 $resql = $this->db->query($sql);
2554 if ($resql) {
2555 while ($obj = $this->db->fetch_object($resql)) {
2556 if (is_string($obj->datec)) {
2557 $obj->datec = strtotime($obj->datec);
2558 }
2559 $logs[] = $obj;
2560 }
2561 $this->db->free($resql);
2562 } else {
2563 $this->error = $this->db->lasterror();
2564 }
2565
2566 return $logs;
2567 }
2568
2578 public function getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp = 0)
2579 {
2580 global $hookmanager, $action;
2581
2582 // Call hook if any
2583 if (is_object($hookmanager)) {
2584 $parameters = array('thirdparty_seller' => $thirdparty_seller, 'thirdparty_buyer' => $thirdparty_buyer, 'pqp' => $pqp);
2585 // Note that $action and $object may have been modified by some hooks
2586 $reshook = $hookmanager->executeHooks('getSellPrice', $parameters, $this, $action);
2587 if ($reshook > 0) {
2588 return $hookmanager->resArray;
2589 }
2590 }
2591
2592 // Update if prices fields are defined
2593 $tva_tx = get_default_tva($thirdparty_seller, $thirdparty_buyer, $this->id);
2594 $tva_npr = get_default_npr($thirdparty_seller, $thirdparty_buyer, $this->id);
2595 if (empty($tva_tx)) {
2596 $tva_npr = 0;
2597 }
2598
2599 $pu_ht = $this->price;
2600 $pu_ttc = $this->price_ttc;
2601 $price_min = $this->price_min;
2602 $price_min_ttc = $this->price_min_ttc;
2603 $price_base_type = (empty($this->price_base_type) ? 'HT' : $this->price_base_type);
2604
2605 // if price by customer / level
2606 if (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) {
2607 require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
2608
2609 $prodcustprice = new ProductCustomerPrice($this->db);
2610
2611 $filter = array('t.fk_product' => (string) $this->id, 't.fk_soc' => (string) $thirdparty_buyer->id);
2612
2613 // If a price per customer exist
2614 $pricebycustomerexist = false;
2615 $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
2616 if ($result) {
2617 if (count($prodcustprice->lines) > 0) {
2618 $date_now = (int) floor(dol_now() / 86400) * 86400; // date without hours
2619 foreach ($prodcustprice->lines as $k => $custprice_line) {
2620 if ($custprice_line->date_begin <= $date_now && (empty($custprice_line->date_end) || $date_now <= $custprice_line->date_end)) {
2621 $pricebycustomerexist = true;
2622 $pu_ht = price($custprice_line->price);
2623 $price_min = price($custprice_line->price_min);
2624 $price_min_ttc = price($custprice_line->price_min_ttc);
2625 $pu_ttc = price($custprice_line->price_ttc);
2626 $price_base_type = $custprice_line->price_base_type;
2627 $tva_tx = $custprice_line->tva_tx;
2628 if ($custprice_line->default_vat_code && !preg_match('/\‍(.*\‍)/', $tva_tx)) {
2629 $tva_tx .= ' (' . $custprice_line->default_vat_code . ')';
2630 }
2631 $tva_npr = $custprice_line->recuperableonly;
2632 if (empty($tva_tx)) {
2633 $tva_npr = 0;
2634 }
2635 break;
2636 }
2637 }
2638 }
2639 }
2640
2641 if (!$pricebycustomerexist && !empty($thirdparty_buyer->price_level)) {
2642 $pu_ht = isset($this->multiprices[$thirdparty_buyer->price_level]) ? $this->multiprices[$thirdparty_buyer->price_level] : 0;
2643 $pu_ttc = isset($this->multiprices_ttc[$thirdparty_buyer->price_level]) ? $this->multiprices_ttc[$thirdparty_buyer->price_level] : 0;
2644 $price_min = isset($this->multiprices_min[$thirdparty_buyer->price_level]) ? $this->multiprices_min[$thirdparty_buyer->price_level] : 0;
2645 $price_min_ttc = isset($this->multiprices_min_ttc[$thirdparty_buyer->price_level]) ? $this->multiprices_min_ttc[$thirdparty_buyer->price_level] : 0;
2646 $price_base_type = isset($this->multiprices_base_type[$thirdparty_buyer->price_level]) ? $this->multiprices_base_type[$thirdparty_buyer->price_level] : 'HT';
2647 if (getDolGlobalString('PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL')) {
2648 // using this option is a bug. kept for backward compatibility
2649 if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
2650 $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
2651 }
2652 if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
2653 $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
2654 }
2655 if (empty($tva_tx)) {
2656 $tva_npr = 0;
2657 }
2658 }
2659 }
2660 } elseif (getDolGlobalString('PRODUIT_MULTIPRICES') && !empty($thirdparty_buyer->price_level)) { // // If price per segment
2661 $pu_ht = isset($this->multiprices[$thirdparty_buyer->price_level]) ? $this->multiprices[$thirdparty_buyer->price_level] : 0;
2662 $pu_ttc = isset($this->multiprices_ttc[$thirdparty_buyer->price_level]) ? $this->multiprices_ttc[$thirdparty_buyer->price_level] : 0;
2663 $price_min = isset($this->multiprices_min[$thirdparty_buyer->price_level]) ? $this->multiprices_min[$thirdparty_buyer->price_level] : 0;
2664 $price_min_ttc = isset($this->multiprices_min_ttc[$thirdparty_buyer->price_level]) ? $this->multiprices_min_ttc[$thirdparty_buyer->price_level] : 0;
2665 $price_base_type = isset($this->multiprices_base_type[$thirdparty_buyer->price_level]) ? $this->multiprices_base_type[$thirdparty_buyer->price_level] : 'HT';
2666 if (getDolGlobalString('PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL')) {
2667 // using this option is a bug. kept for backward compatibility
2668 if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
2669 $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
2670 }
2671 if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
2672 $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
2673 }
2674 if (empty($tva_tx)) {
2675 $tva_npr = 0;
2676 }
2677 }
2678 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES')) {
2679 // If price per customer
2680 require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
2681
2682 $prodcustprice = new ProductCustomerPrice($this->db);
2683
2684 $filter = array('t.fk_product' => (string) $this->id, 't.fk_soc' => (string) $thirdparty_buyer->id);
2685
2686 $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
2687 if ($result) {
2688 if (count($prodcustprice->lines) > 0) {
2689 $date_now = (int) floor(dol_now() / 86400) * 86400; // date without hours
2690 foreach ($prodcustprice->lines as $k => $custprice_line) {
2691 if ($custprice_line->date_begin <= $date_now && (empty($custprice_line->date_end) || $date_now <= $custprice_line->date_end)) {
2692 $pu_ht = price($custprice_line->price);
2693 $price_min = price($custprice_line->price_min);
2694 $price_min_ttc = price($custprice_line->price_min_ttc);
2695 $pu_ttc = price($custprice_line->price_ttc);
2696 $price_base_type = $custprice_line->price_base_type;
2697 $tva_tx = $custprice_line->tva_tx;
2698 if ($custprice_line->default_vat_code && !preg_match('/\‍(.*\‍)/', $tva_tx)) {
2699 $tva_tx .= ' (' . $custprice_line->default_vat_code . ')';
2700 }
2701 $tva_npr = $custprice_line->recuperableonly;
2702 if (empty($tva_tx)) {
2703 $tva_npr = 0;
2704 }
2705 break;
2706 }
2707 }
2708 }
2709 }
2710 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY')) {
2711 // If price per quantity
2712 if (!empty($this->prices_by_qty[0])) {
2713 // yes, this product has some prices per quantity
2714 // Search price into product_price_by_qty from $this->id
2715 foreach ($this->prices_by_qty_list[0] as $priceforthequantityarray) {
2716 if ($priceforthequantityarray['rowid'] != $pqp) {
2717 continue;
2718 }
2719 // We found the price
2720 if ($priceforthequantityarray['price_base_type'] == 'HT') {
2721 $pu_ht = $priceforthequantityarray['unitprice'];
2722 } else {
2723 $pu_ttc = $priceforthequantityarray['unitprice'];
2724 }
2725 break;
2726 }
2727 }
2728 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) {
2729 // If price per quantity and customer
2730 if (!empty($this->prices_by_qty[$thirdparty_buyer->price_level])) {
2731 // yes, this product has some prices per quantity
2732 // Search price into product_price_by_qty from $this->id
2733 foreach ($this->prices_by_qty_list[$thirdparty_buyer->price_level] as $priceforthequantityarray) {
2734 if ($priceforthequantityarray['rowid'] != $pqp) {
2735 continue;
2736 }
2737 // We found the price
2738 if ($priceforthequantityarray['price_base_type'] == 'HT') {
2739 $pu_ht = $priceforthequantityarray['unitprice'];
2740 } else {
2741 $pu_ttc = $priceforthequantityarray['unitprice'];
2742 }
2743 break;
2744 }
2745 }
2746 }
2747
2748 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);
2749 }
2750
2751 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2765 public function get_buyprice($prodfournprice, $qty, $product_id = 0, $fourn_ref = '', $fk_soc = 0)
2766 {
2767 // phpcs:enable
2768 global $action, $hookmanager;
2769
2770 // Call hook if any
2771 if (is_object($hookmanager)) {
2772 $parameters = array(
2773 'prodfournprice' => $prodfournprice,
2774 'qty' => $qty,
2775 'product_id' => $product_id,
2776 'fourn_ref' => $fourn_ref,
2777 'fk_soc' => $fk_soc,
2778 );
2779 // Note that $action and $object may have been modified by some hooks
2780 $reshook = $hookmanager->executeHooks('getBuyPrice', $parameters, $this, $action);
2781 if ($reshook > 0) {
2782 return $hookmanager->resArray;
2783 }
2784 }
2785
2786 $result = 0;
2787
2788 // 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)
2789 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2790 $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,";
2791 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2792 $sql .= " pfp.packaging";
2793 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2794 $sql .= " WHERE pfp.rowid = ".((int) $prodfournprice);
2795 if ($qty > 0) {
2796 $sql .= " AND pfp.quantity <= ".((float) $qty);
2797 }
2798 $sql .= " ORDER BY pfp.quantity DESC";
2799
2800 dol_syslog(get_class($this)."::get_buyprice first search by prodfournprice/qty", LOG_DEBUG);
2801 $resql = $this->db->query($sql);
2802 if ($resql) {
2803 $obj = $this->db->fetch_object($resql);
2804 if ($obj && $obj->quantity > 0) { // If we found a supplier prices from the id of supplier price
2805 if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2806 $prod_supplier = new ProductFournisseur($this->db);
2807 $prod_supplier->product_fourn_price_id = $obj->rowid;
2808 $prod_supplier->id = $obj->fk_product;
2809 $prod_supplier->fourn_qty = $obj->quantity;
2810 $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2811 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2812
2813 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2814 $priceparser = new PriceParser($this->db);
2815 $price_result = $priceparser->parseProductSupplier($prod_supplier);
2816 if ($price_result >= 0) {
2817 $obj->price = $price_result;
2818 }
2819 }
2820 $this->product_fourn_price_id = $obj->rowid;
2821 $this->buyprice = $obj->price; // deprecated
2822 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product of supplier
2823 $this->fourn_price_base_type = 'HT'; // Price base type
2824 $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2825 $this->ref_fourn = $obj->ref_supplier; // deprecated
2826 $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2827 $this->desc_supplier = $obj->desc_supplier; // desc supplier
2828 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2829 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2830 $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2831 $this->fourn_multicurrency_price = $obj->multicurrency_price;
2832 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2833 $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2834 $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2835 $this->fourn_multicurrency_code = $obj->multicurrency_code;
2836 if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2837 $this->packaging = (float) $obj->packaging;
2838 }
2839 // Expose the supplier minimum purchase quantity so callers can enforce qty_min
2840 // on top of packaging-multiple rounding (#38783). pfp.quantity is the lowest
2841 // qty for which this price line is valid; for "below-min" callers this is the
2842 // floor they must round up to.
2843 $this->fourn_qty = $obj->quantity;
2844 $result = $obj->fk_product;
2845 return $result;
2846 } else { // If not found
2847 // 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.
2848 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2849 $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,";
2850 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2851 $sql .= " pfp.packaging";
2852 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2853 $sql .= " WHERE 1 = 1";
2854 if ($product_id > 0) {
2855 $sql .= " AND pfp.fk_product = ".((int) $product_id);
2856 }
2857 if ($fourn_ref != 'none') {
2858 $sql .= " AND pfp.ref_fourn = '".$this->db->escape($fourn_ref)."'";
2859 }
2860 if ($fk_soc > 0) {
2861 $sql .= " AND pfp.fk_soc = ".((int) $fk_soc);
2862 }
2863 if ($qty > 0) {
2864 $sql .= " AND pfp.quantity <= ".((float) $qty);
2865 }
2866 $sql .= " ORDER BY pfp.quantity DESC";
2867 $sql .= " LIMIT 1";
2868
2869 dol_syslog(get_class($this)."::get_buyprice second search from qty/ref/product_id", LOG_DEBUG);
2870 $resql = $this->db->query($sql);
2871 if ($resql) {
2872 $obj = $this->db->fetch_object($resql);
2873 if ($obj && $obj->quantity > 0) { // If found
2874 if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2875 $prod_supplier = new ProductFournisseur($this->db);
2876 $prod_supplier->product_fourn_price_id = $obj->rowid;
2877 $prod_supplier->id = $obj->fk_product;
2878 $prod_supplier->fourn_qty = $obj->quantity;
2879 $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2880 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2881
2882 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2883 $priceparser = new PriceParser($this->db);
2884 $price_result = $priceparser->parseProductSupplier($prod_supplier);
2885 if ($price_result >= 0) {
2886 $obj->price = $price_result;
2887 }
2888 }
2889 $this->product_fourn_price_id = $obj->rowid;
2890 $this->buyprice = $obj->price; // deprecated
2891 $this->fourn_qty = $obj->quantity; // min quantity for price for a virtual supplier
2892 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product for a virtual supplier
2893 $this->fourn_price_base_type = 'HT'; // Price base type for a virtual supplier
2894 $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2895 $this->ref_fourn = $obj->ref_supplier; // deprecated
2896 $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2897 $this->desc_supplier = $obj->desc_supplier; // desc supplier
2898 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2899 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2900 $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2901 $this->fourn_multicurrency_price = $obj->multicurrency_price;
2902 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2903 $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2904 $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2905 $this->fourn_multicurrency_code = $obj->multicurrency_code;
2906 if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2907 $this->packaging = (float) $obj->packaging;
2908 }
2909 // Expose pfp.quantity so callers can enforce qty_min on top of packaging
2910 // rounding (#38783).
2911 $this->fourn_qty = $obj->quantity;
2912 $result = $obj->fk_product;
2913 return $result;
2914 } else {
2915 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.
2916 }
2917 } else {
2918 $this->error = $this->db->lasterror();
2919 return -3;
2920 }
2921 }
2922 } else {
2923 $this->error = $this->db->lasterror();
2924 return -2;
2925 }
2926 }
2927
2928
2947 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)
2948 {
2949 $lastPriceData = $this->getArrayForPriceCompare($level); // temporary store current price before update
2950
2951 $id = $this->id;
2952
2953 dol_syslog(get_class($this)."::update_price id=".$id." newprice=".$newprice." newpricebase=".$newpricebase." newminprice=".$newminprice." level=".$level." npr=".$newnpr." newdefaultvatcode=".$newdefaultvatcode);
2954
2955 // Clean parameters
2956 if (empty($this->tva_tx)) {
2957 $this->tva_tx = 0.0;
2958 }
2959 if (empty($newnpr)) {
2960 $newnpr = 0.0;
2961 }
2962 if (empty($newminprice)) {
2963 $newminprice = 0.0;
2964 }
2965
2966 // Check parameters
2967 if ($newvat === null || $newvat == '') { // Maintain '' for backwards compatibility
2968 $newvat = (float) $this->tva_tx;
2969 }
2970
2971 $localtaxtype1 = '';
2972 $localtaxtype2 = '';
2973
2974 // If multiprices are enabled, then we check if the current product is subject to price autogeneration
2975 // Price will be modified ONLY when the first one is the one that is being modified
2976 if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && !$ignore_autogen && $this->price_autogen && ($level == 1)) {
2977 return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpbq);
2978 }
2979
2980 if (!empty($newminprice) && ($newminprice > $newprice)) {
2981 $this->error = 'ErrorPriceCantBeLowerThanMinPrice';
2982 return -1;
2983 }
2984
2985 if ($newprice === 0 || $newprice !== '') {
2986 if ($newpricebase == 'TTC') {
2987 $price_ttc = (float) price2num($newprice, 'MU');
2988 $price = (float) price2num($newprice) / (1 + ((float) $newvat / 100));
2989 $price = (float) price2num($price, 'MU');
2990
2991 if ((string) $newminprice != '0') {
2992 $price_min_ttc = (float) price2num($newminprice, 'MU');
2993 $price_min = (float) price2num($newminprice) / (1 + ($newvat / 100));
2994 $price_min = (float) price2num($price_min, 'MU');
2995 } else {
2996 $price_min = 0.0;
2997 $price_min_ttc = 0.0;
2998 }
2999 } else {
3000 $price = (float) price2num($newprice, 'MU');
3001 $price_ttc = ($newnpr != 1) ? (float) price2num($newprice) * (1 + ($newvat / 100)) : $price;
3002 $price_ttc = (float) price2num($price_ttc, 'MU');
3003
3004 if ((string) $newminprice != '0') {
3005 $price_min = (float) price2num($newminprice, 'MU');
3006 $price_min_ttc = (float) price2num($newminprice) * (1 + ($newvat / 100));
3007 $price_min_ttc = (float) price2num($price_min_ttc, 'MU');
3008 //print 'X'.$newminprice.'-'.$price_min;
3009 } else {
3010 $price_min = 0.0;
3011 $price_min_ttc = 0.0;
3012 }
3013 }
3014 //print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc;
3015 if (count($localtaxes_array) > 0) {
3016 $localtaxtype1 = $localtaxes_array['0'];
3017 $localtax1 = $localtaxes_array['1'];
3018 $localtaxtype2 = $localtaxes_array['2'];
3019 $localtax2 = $localtaxes_array['3'];
3020 } else {
3021 // if array empty, we try to use the vat code
3022 if (!empty($newdefaultvatcode)) {
3023 global $mysoc;
3024 // Get record from code
3025 $sql = "SELECT t.rowid, t.code, t.recuperableonly as tva_npr, t.localtax1, t.localtax2, t.localtax1_type, t.localtax2_type";
3026 $sql .= " FROM ".MAIN_DB_PREFIX."c_tva as t, ".MAIN_DB_PREFIX."c_country as c";
3027 $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '".$this->db->escape($mysoc->country_code)."'";
3028 $sql .= " AND t.taux = ".((float) $newdefaultvatcode)." AND t.active = 1";
3029 $sql .= " AND t.code = '".$this->db->escape($newdefaultvatcode)."'";
3030 $resql = $this->db->query($sql);
3031 if ($resql) {
3032 $obj = $this->db->fetch_object($resql);
3033 if ($obj) {
3034 $npr = $obj->tva_npr;
3035 $localtax1 = $obj->localtax1;
3036 $localtax2 = $obj->localtax2;
3037 $localtaxtype1 = $obj->localtax1_type;
3038 $localtaxtype2 = $obj->localtax2_type;
3039 }
3040 }
3041 } else {
3042 // old method. deprecated because we can't retrieve type
3043 $localtaxtype1 = '0';
3044 $localtax1 = get_localtax($newvat, 1);
3045 $localtaxtype2 = '0';
3046 $localtax2 = get_localtax($newvat, 2);
3047 }
3048 }
3049 if (empty($localtax1)) {
3050 $localtax1 = 0; // If = '' then = 0
3051 }
3052 if (empty($localtax2)) {
3053 $localtax2 = 0; // If = '' then = 0
3054 }
3055
3056 $this->db->begin();
3057
3058 // The price columns of the table product hold the default price of the product, that is the price of the level 1
3059 // when multiprices are enabled (level is 0 when they are not). So they must not be overwritten by another level.
3060 // The vat columns are however shared by all levels, so they are always updated.
3061 $updatedefaultprice = (empty($level) || $level == 1);
3062
3063 // Don't put quotes here on decimal numbers.
3064 // This causes storage with base rounding instead of exact values.
3065 $sql = "UPDATE ".$this->db->prefix()."product SET";
3066 if ($updatedefaultprice) {
3067 $sql .= " price_base_type = '".$this->db->escape($newpricebase)."',";
3068 $sql .= " price = ".(float) $price.",";
3069 $sql .= " price_ttc = ".(float) $price_ttc.",";
3070 $sql .= " price_min = ".(float) $price_min.",";
3071 $sql .= " price_min_ttc = ".(float) $price_min_ttc.",";
3072 $sql .= " price_label = ".(!empty($price_label) ? "'".$this->db->escape($price_label)."'" : "null").",";
3073 }
3074 $sql .= " localtax1_tx = ".($localtax1 >= 0 ? (float) $localtax1 : 'NULL').",";
3075 $sql .= " localtax2_tx = ".($localtax2 >= 0 ? (float) $localtax2 : 'NULL').",";
3076 $sql .= " localtax1_type = ".($localtaxtype1 != '' ? "'".$this->db->escape($localtaxtype1)."'" : "'0'").",";
3077 $sql .= " localtax2_type = ".($localtaxtype2 != '' ? "'".$this->db->escape($localtaxtype2)."'" : "'0'").",";
3078 $sql .= " default_vat_code = ".($newdefaultvatcode ? "'".$this->db->escape($newdefaultvatcode)."'" : "null").",";
3079 $sql .= " tva_tx = ".(float) price2num($newvat).",";
3080 $sql .= " recuperableonly = '".$this->db->escape((string) $newnpr)."'";
3081 $sql .= " WHERE rowid = ".((int) $id);
3082
3083 dol_syslog(get_class($this)."::update_price", LOG_DEBUG);
3084 $resql = $this->db->query($sql);
3085 if ($resql) {
3086 $this->multiprices[$level] = $price;
3087 $this->multiprices_ttc[$level] = $price_ttc;
3088 $this->multiprices_min[$level] = $price_min;
3089 $this->multiprices_min_ttc[$level] = $price_min_ttc;
3090 $this->multiprices_base_type[$level] = $newpricebase;
3091 $this->multiprices_default_vat_code[$level] = $newdefaultvatcode;
3092 $this->multiprices_tva_tx[$level] = $newvat;
3093 $this->multiprices_recuperableonly[$level] = $newnpr;
3094
3095 // Save the default price of the product to restore it after the log if we are updating another level
3096 // (_log_price() reads the price into the properties of the object to save the price of the level).
3097 $savdefaultprice = array($this->price, $this->price_label, $this->price_ttc, $this->price_min, $this->price_min_ttc, $this->price_base_type);
3098
3099 $this->price = $price;
3100 $this->price_label = $price_label;
3101 $this->price_ttc = $price_ttc;
3102 $this->price_min = $price_min;
3103 $this->price_min_ttc = $price_min_ttc;
3104 $this->price_base_type = $newpricebase;
3105 $this->default_vat_code = $newdefaultvatcode;
3106 $this->tva_tx = $newvat;
3107 $this->tva_npr = $newnpr;
3108
3109 //Local taxes
3110 $this->localtax1_tx = $localtax1;
3111 $this->localtax2_tx = $localtax2;
3112 $this->localtax1_type = $localtaxtype1;
3113 $this->localtax2_type = $localtaxtype2;
3114
3115 // Price by quantity
3116 $this->price_by_qty = $newpbq;
3117
3118 // check if price have really change before log
3119 $newPriceData = $this->getArrayForPriceCompare($level);
3120 if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || (!getDolGlobalString('PRODUIT_MULTIPRICES') && !getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES'))) {
3121 $this->_log_price($user, $level); // Save price for level into table product_price
3122 }
3123
3124 if (!$updatedefaultprice) {
3125 // The price of the level is now saved, so we can restore the default price of the product
3126 list($this->price, $this->price_label, $this->price_ttc, $this->price_min, $this->price_min_ttc, $this->price_base_type) = $savdefaultprice;
3127 }
3128
3129 $this->level = $level; // Store level of price edited for trigger
3130
3131 // Call trigger
3132 if (!$notrigger) {
3133 $result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user);
3134 if ($result < 0) {
3135 $this->db->rollback();
3136 return -1;
3137 }
3138 }
3139 // End call triggers
3140
3141 $this->db->commit();
3142 } else {
3143 $this->db->rollback();
3144 $this->error = $this->db->lasterror();
3145 return -1;
3146 }
3147 }
3148
3149 return 1;
3150 }
3151
3159 public function setPriceExpression($expression_id)
3160 {
3161 global $user;
3162
3163 $this->fk_price_expression = $expression_id;
3164
3165 return $this->update($this->id, $user);
3166 }
3167
3180 public function fetch($id = 0, $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0)
3181 {
3182 include_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
3183
3184 global $conf;
3185
3186 dol_syslog(get_class($this)."::fetch id=".$id." ref=".$ref." ref_ext=".$ref_ext);
3187
3188 // Check parameters
3189 if (!$id && !$ref && !$ref_ext && !$barcode) {
3190 $this->error = 'ErrorWrongParameters';
3191 dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR);
3192 return -1;
3193 }
3194
3195 $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,";
3196 $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,";
3197 $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,";
3198 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.last_main_doc,";
3199 $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,";
3200 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
3201 $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,";
3202 } else {
3203 $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,";
3204 }
3205
3206 // For MultiCompany
3207 // PMP per entity & Stocks Sharings stock_reel includes only stocks shared with this entity
3208 $separatedEntityPMP = false; // Set to true to get the AWP from table llx_product_perentity instead of field 'pmp' into llx_product.
3209 $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.
3210 $visibleWarehousesEntities = $conf->entity;
3211 if (getDolGlobalString('MULTICOMPANY_PRODUCT_SHARING_ENABLED')) {
3212 if (getDolGlobalString('MULTICOMPANY_PMP_PER_ENTITY_ENABLED')) {
3213 $checkPMPPerEntity = $this->db->query("SELECT pmp FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = ".((int) $id)." AND entity = ".(int) $conf->entity);
3214 if ($this->db->num_rows($checkPMPPerEntity) > 0) {
3215 $separatedEntityPMP = true;
3216 }
3217 }
3218 global $mc;
3219 $separatedStock = true;
3220 if (isset($mc->sharings['stock']) && !empty($mc->sharings['stock'])) {
3221 $visibleWarehousesEntities .= "," . implode(",", $mc->sharings['stock']);
3222 }
3223 }
3224 if ($separatedEntityPMP) {
3225 $sql .= " ppe.pmp,";
3226 } else {
3227 $sql .= " p.pmp,";
3228 }
3229 $sql .= " p.datec, GREATEST(p.tms, pef.tms) AS tms, p.import_key, p.entity, p.desiredstock, p.tobatch, p.sell_or_eat_by_mandatory, p.batch_mask, p.fk_unit,";
3230 $sql .= " p.fk_price_expression, p.price_autogen, p.stockable_product, p.model_pdf,";
3231 $sql .= " p.price_label,";
3232 if ($separatedStock) {
3233 $sql .= " SUM(sp.reel) as stock";
3234 } else {
3235 $sql .= " p.stock";
3236 }
3237 $sql .= " FROM ".$this->db->prefix()."product as p";
3238 $sql .= " LEFT JOIN ".$this->db->prefix()."product_extrafields as pef ON pef.fk_object=p.rowid";
3239 if (getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED') || $separatedEntityPMP) {
3240 $sql .= " LEFT JOIN " . $this->db->prefix() . "product_perentity as ppe ON ppe.fk_product = p.rowid AND ppe.entity = " . ((int) $conf->entity);
3241 }
3242 if ($separatedStock) {
3243 $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)."))";
3244 }
3245
3246 if ($id) {
3247 $sql .= " WHERE p.rowid = ".((int) $id);
3248 } else {
3249 $sql .= " WHERE p.entity IN (".getEntity($this->element).")";
3250 if ($ref) {
3251 $sql .= " AND p.ref = '".$this->db->escape($ref)."'";
3252 } elseif ($ref_ext) {
3253 $sql .= " AND p.ref_ext = '".$this->db->escape($ref_ext)."'";
3254 } elseif ($barcode) {
3255 $sql .= " AND p.barcode = '".$this->db->escape($barcode)."'";
3256 }
3257 }
3258 if ($separatedStock) {
3259 $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,";
3260 $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,";
3261 $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,";
3262 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
3263 $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,";
3264 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
3265 $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,";
3266 } else {
3267 $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,";
3268 }
3269 if ($separatedEntityPMP) {
3270 $sql .= " ppe.pmp,";
3271 } else {
3272 $sql .= " p.pmp,";
3273 }
3274 $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,";
3275 $sql .= " p.fk_price_expression, p.price_autogen, p.stockable_product, p.model_pdf,";
3276 $sql .= " p.price_label";
3277 // can't be false TODO fix
3278 // if (!$separatedStock) {
3279 // $sql .= ", p.stock";
3280 // }
3281 }
3282
3283 $resql = $this->db->query($sql);
3284 if ($resql) {
3285 unset($this->oldcopy);
3286
3287 if ($this->db->num_rows($resql) > 0) {
3288 $obj = $this->db->fetch_object($resql);
3289
3290 $this->id = $obj->rowid;
3291 $this->ref = $obj->ref;
3292 $this->ref_ext = $obj->ref_ext;
3293 $this->label = $obj->label;
3294 $this->description = $obj->description;
3295 $this->url = $obj->url;
3296 $this->note_public = $obj->note_public;
3297 $this->note_private = $obj->note_private;
3298 $this->note = $obj->note_private; // deprecated
3299
3300 $this->type = $obj->fk_product_type;
3301 $this->price_label = $obj->price_label;
3302 $this->status = $obj->tosell;
3303 $this->status_buy = $obj->tobuy;
3304 $this->status_batch = $obj->tobatch;
3305 $this->sell_or_eat_by_mandatory = $obj->sell_or_eat_by_mandatory;
3306 $this->batch_mask = $obj->batch_mask;
3307
3308 $this->customcode = $obj->customcode;
3309 $this->country_id = $obj->fk_country;
3310 $this->country_code = getCountry($this->country_id, '2', $this->db);
3311 $this->state_id = $obj->fk_state;
3312 $this->lifetime = $obj->lifetime;
3313 $this->qc_frequency = $obj->qc_frequency;
3314 $this->price = $obj->price;
3315 $this->price_ttc = $obj->price_ttc;
3316 $this->price_min = $obj->price_min;
3317 $this->price_min_ttc = $obj->price_min_ttc;
3318 $this->price_base_type = (empty($obj->price_base_type) ? 'HT' : $obj->price_base_type);
3319 $this->cost_price = isset($obj->cost_price) ? (float) $obj->cost_price : null;
3320 $this->default_vat_code = $obj->default_vat_code;
3321 $this->tva_tx = $obj->tva_tx;
3323 $this->tva_npr = $obj->tva_npr;
3325 $this->localtax1_tx = $obj->localtax1_tx;
3326 $this->localtax2_tx = $obj->localtax2_tx;
3327 $this->localtax1_type = $obj->localtax1_type;
3328 $this->localtax2_type = $obj->localtax2_type;
3329
3330 $this->finished = $obj->finished;
3331 $this->fk_default_bom = $obj->fk_default_bom;
3332
3333 $this->duration = $obj->duration;
3334 $matches = [];
3335 preg_match('/([\d.]+)(\w+)/', $obj->duration, $matches);
3336 $this->duration_value = !empty($matches[1]) ? (float) $matches[1] : 0;
3337 $this->duration_unit = !empty($matches[2]) ? (string) $matches[2] : null;
3338 $this->canvas = $obj->canvas;
3339 $this->net_measure = $obj->net_measure;
3340 $this->net_measure_units = $obj->net_measure_units;
3341 $this->weight = $obj->weight;
3342 $this->weight_units = (is_null($obj->weight_units) ? 0 : $obj->weight_units);
3343 $this->length = $obj->length;
3344 $this->length_units = (is_null($obj->length_units) ? 0 : $obj->length_units);
3345 $this->width = $obj->width;
3346 $this->width_units = (is_null($obj->width_units) ? 0 : $obj->width_units);
3347 $this->height = $obj->height;
3348 $this->height_units = (is_null($obj->height_units) ? 0 : $obj->height_units);
3349
3350 $this->surface = $obj->surface;
3351 $this->surface_units = (is_null($obj->surface_units) ? 0 : $obj->surface_units);
3352 $this->volume = $obj->volume;
3353 $this->volume_units = (is_null($obj->volume_units) ? 0 : $obj->volume_units);
3354 $this->barcode = $obj->barcode;
3355 $this->barcode_type = $obj->fk_barcode_type;
3356
3357 $this->accountancy_code_buy = $obj->accountancy_code_buy;
3358 $this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra;
3359 $this->accountancy_code_buy_export = $obj->accountancy_code_buy_export;
3360 $this->accountancy_code_sell = $obj->accountancy_code_sell;
3361 $this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra;
3362 $this->accountancy_code_sell_export = $obj->accountancy_code_sell_export;
3363
3364 $this->fk_default_warehouse = $obj->fk_default_warehouse;
3365 $this->fk_default_workstation = $obj->fk_default_workstation;
3366 $this->seuil_stock_alerte = $obj->seuil_stock_alerte;
3367 $this->desiredstock = $obj->desiredstock;
3368 $this->stock_reel = $obj->stock;
3369 $this->stockable_product = $obj->stockable_product;
3370 $this->pmp = $obj->pmp;
3371
3372 $this->date_creation = $this->db->jdate($obj->datec);
3373 $this->date_modification = $this->db->jdate($obj->tms);
3374
3375 $this->import_key = $obj->import_key;
3376 $this->entity = $obj->entity;
3377
3378 $this->ref_ext = $obj->ref_ext;
3379 $this->fk_price_expression = $obj->fk_price_expression;
3380 $this->fk_unit = $obj->fk_unit;
3381 $this->price_autogen = $obj->price_autogen;
3382 $this->model_pdf = $obj->model_pdf;
3383 $this->last_main_doc = $obj->last_main_doc;
3384
3385 $this->mandatory_period = $obj->mandatory_period;
3386
3387 if (getDolGlobalString('PRODUCT_USE_CUSTOMER_PACKAGING')) {
3388 $this->packaging = (float) $obj->packaging;
3389 }
3390
3391 $this->db->free($resql);
3392
3393 // fetch optionals attributes and labels
3394 $this->fetch_optionals();
3395
3396 // Multilangs
3397 if (getDolGlobalInt('MAIN_MULTILANGS') && empty($ignore_lang_load)) {
3398 $this->getMultiLangs();
3399 }
3400
3401 // Load multiprices array
3402 if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && empty($ignore_price_load)) { // prices per segment
3403 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
3404 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
3405 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3406 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
3407 $sql .= " ,price_label";
3408 $sql .= " FROM ".$this->db->prefix()."product_price";
3409 $sql .= " WHERE entity IN (".getEntity('productprice').")";
3410 $sql .= " AND price_level=".((int) $i);
3411 $sql .= " AND fk_product = ".((int) $this->id);
3412 $sql .= " ORDER BY date_price DESC, rowid DESC"; // Get the most recent line
3413 $sql .= " LIMIT 1"; // Only the first one
3414 $resql = $this->db->query($sql);
3415 if ($resql) {
3416 $result = $this->db->fetch_array($resql);
3417
3418 $this->multiprices[$i] = $result ? $result["price"] : null;
3419 $this->multiprices_ttc[$i] = $result ? $result["price_ttc"] : null;
3420 $this->multiprices_min[$i] = $result ? $result["price_min"] : null;
3421 $this->multiprices_min_ttc[$i] = $result ? $result["price_min_ttc"] : null;
3422 $this->multiprices_base_type[$i] = $result ? $result["price_base_type"] : null;
3423 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
3424 $this->multiprices_tva_tx[$i] = $result ? $result["tva_tx"].(!empty($result['default_vat_code']) ? ' ('.$result['default_vat_code'].')' : '') : null;
3425 $this->multiprices_recuperableonly[$i] = $result ? $result["recuperableonly"] : null;
3426
3427 // Price by quantity
3428 /*
3429 $this->prices_by_qty[$i]=$result["price_by_qty"];
3430 $this->prices_by_qty_id[$i]=$result["rowid"];
3431 // Get the pricelist according to the qty if flag is set
3432 if ($this->prices_by_qty[$i] == 1)
3433 {
3434 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
3435 $sql.= " FROM ".$this->db->prefix()."product_price_by_qty";
3436 $sql.= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
3437 $sql.= " ORDER BY quantity ASC";
3438
3439 $resql = $this->db->query($sql);
3440 if ($resql)
3441 {
3442 $resultat=array();
3443 $ii=0;
3444 while ($result= $this->db->fetch_array($resql)) {
3445 $resultat[$ii]=array();
3446 $resultat[$ii]["rowid"]=$result["rowid"];
3447 $resultat[$ii]["price"]= $result["price"];
3448 $resultat[$ii]["unitprice"]= $result["unitprice"];
3449 $resultat[$ii]["quantity"]= $result["quantity"];
3450 $resultat[$ii]["remise_percent"]= $result["remise_percent"];
3451 $resultat[$ii]["remise"]= $result["remise"]; // deprecated
3452 $resultat[$ii]["price_base_type"]= $result["price_base_type"];
3453 $ii++;
3454 }
3455 $this->prices_by_qty_list[$i]=$resultat;
3456 }
3457 else
3458 {
3459 dol_print_error($this->db);
3460 return -1;
3461 }
3462 }*/
3463 } else {
3464 $this->error = $this->db->lasterror;
3465 return -1;
3466 }
3467 }
3468 } elseif ((getDolGlobalString('PRODUIT_CUSTOMER_PRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && empty($ignore_price_load)) { // prices per customers
3469 // Nothing loaded by default. List may be very long.
3470 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY') && empty($ignore_price_load)) { // prices per quantity
3471 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3472 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid";
3473 $sql .= " FROM ".$this->db->prefix()."product_price";
3474 $sql .= " WHERE fk_product = ".((int) $this->id);
3475 $sql .= " ORDER BY date_price DESC, rowid DESC";
3476 $sql .= " LIMIT 1";
3477
3478 $resql = $this->db->query($sql);
3479 if ($resql) {
3480 $result = $this->db->fetch_array($resql);
3481
3482 if ($result) {
3483 // Price by quantity
3484 $this->prices_by_qty[0] = $result["price_by_qty"];
3485 $this->prices_by_qty_id[0] = $result["rowid"];
3486 // Get the pricelist according to the qty if flag is set
3487 if ($this->prices_by_qty[0] == 1) {
3488 $sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise, remise, price_base_type";
3489 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
3490 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[0]);
3491 $sql .= " ORDER BY quantity ASC";
3492
3493 $resql = $this->db->query($sql);
3494 if ($resql) {
3495 $resultat = array();
3496 $ii = 0;
3497 while ($result = $this->db->fetch_array($resql)) {
3498 $resultat[$ii] = array();
3499 $resultat[$ii]["rowid"] = $result["rowid"];
3500 $resultat[$ii]["price"] = $result["price"];
3501 $resultat[$ii]["unitprice"] = $result["unitprice"];
3502 $resultat[$ii]["quantity"] = $result["quantity"];
3503 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
3504 //$resultat[$ii]["remise"]= $result["remise"]; // deprecated
3505 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
3506 $ii++;
3507 }
3508 $this->prices_by_qty_list[0] = $resultat;
3509 } else {
3510 $this->error = $this->db->lasterror;
3511 return -1;
3512 }
3513 }
3514 }
3515 } else {
3516 $this->error = $this->db->lasterror;
3517 return -1;
3518 }
3519 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') && empty($ignore_price_load)) { // prices per customer and quantity
3520 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
3521 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
3522 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3523 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
3524 $sql .= " FROM ".$this->db->prefix()."product_price";
3525 $sql .= " WHERE entity IN (".getEntity('productprice').")";
3526 $sql .= " AND price_level=".((int) $i);
3527 $sql .= " AND fk_product = ".((int) $this->id);
3528 $sql .= " ORDER BY date_price DESC, rowid DESC";
3529 $sql .= " LIMIT 1";
3530 $resql = $this->db->query($sql);
3531 if (!$resql) {
3532 $this->error = $this->db->lasterror;
3533 return -1;
3534 } else {
3535 $result = $this->db->fetch_array($resql);
3536 $this->multiprices[$i] = (!empty($result["price"]) ? $result["price"] : 0);
3537 $this->multiprices_ttc[$i] = (!empty($result["price_ttc"]) ? $result["price_ttc"] : 0);
3538 $this->multiprices_min[$i] = (!empty($result["price_min"]) ? $result["price_min"] : 0);
3539 $this->multiprices_min_ttc[$i] = (!empty($result["price_min_ttc"]) ? $result["price_min_ttc"] : 0);
3540 $this->multiprices_base_type[$i] = (!empty($result["price_base_type"]) ? $result["price_base_type"] : '');
3541 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
3542 $this->multiprices_tva_tx[$i] = (!empty($result["tva_tx"]) ? $result["tva_tx"] : 0); // TODO Add ' ('.$result['default_vat_code'].')'
3543 $this->multiprices_recuperableonly[$i] = (!empty($result["recuperableonly"]) ? $result["recuperableonly"] : 0);
3544
3545 // Price by quantity
3546 $this->prices_by_qty[$i] = (!empty($result["price_by_qty"]) ? $result["price_by_qty"] : 0);
3547 $this->prices_by_qty_id[$i] = (!empty($result["rowid"]) ? $result["rowid"] : 0);
3548 // Get the pricelist according to the qty if flag is set
3549 if ($this->prices_by_qty[$i] == 1) {
3550 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
3551 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
3552 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
3553 $sql .= " ORDER BY quantity ASC";
3554
3555 $resql = $this->db->query($sql);
3556 if ($resql) {
3557 $resultat = array();
3558 $ii = 0;
3559 while ($result = $this->db->fetch_array($resql)) {
3560 $resultat[$ii] = array();
3561 $resultat[$ii]["rowid"] = $result["rowid"];
3562 $resultat[$ii]["price"] = $result["price"];
3563 $resultat[$ii]["unitprice"] = $result["unitprice"];
3564 $resultat[$ii]["quantity"] = $result["quantity"];
3565 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
3566 $resultat[$ii]["remise"] = $result["remise"]; // deprecated
3567 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
3568 $ii++;
3569 }
3570 $this->prices_by_qty_list[$i] = $resultat;
3571 } else {
3572 $this->error = $this->db->lasterror;
3573 return -1;
3574 }
3575 }
3576 }
3577 }
3578 }
3579
3580 if (isModEnabled('dynamicprices') && !empty($this->fk_price_expression) && empty($ignore_expression)) {
3581 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
3582 $priceparser = new PriceParser($this->db);
3583 $price_result = $priceparser->parseProduct($this);
3584 if ($price_result >= 0) {
3585 $this->price = $price_result;
3586 // Calculate the VAT
3587 $this->price_ttc = (float) price2num($this->price) * (1 + ($this->tva_tx / 100));
3588 $this->price_ttc = (float) price2num($this->price_ttc, 'MU');
3589 }
3590 }
3591
3592 // We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product.
3593 // Instead we just init the stock_warehouse array
3594 $this->stock_warehouse = array();
3595
3596 return 1;
3597 } else {
3598 return 0;
3599 }
3600 } else {
3601 $this->error = $this->db->lasterror();
3602 return -1;
3603 }
3604 }
3605
3606 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3613 public function load_stats_mo($socid = 0)
3614 {
3615 // phpcs:enable
3616 global $user, $hookmanager, $action;
3617
3618 $error = 0;
3619
3620 foreach (array('toconsume', 'consumed', 'toproduce', 'produced') as $role) {
3621 $this->stats_mo['customers_'.$role] = 0;
3622 $this->stats_mo['nb_'.$role] = 0;
3623 $this->stats_mo['qty_'.$role] = 0;
3624
3625 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3626 $sql .= " SUM(mp.qty) as qty";
3627 $sql .= " FROM ".$this->db->prefix()."mrp_mo as c";
3628 $sql .= " INNER JOIN ".$this->db->prefix()."mrp_production as mp ON mp.fk_mo=c.rowid";
3629 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3630 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3631 }
3632 $sql .= " WHERE ";
3633 $sql .= " c.entity IN (".getEntity('mo').")";
3634
3635 $sql .= " AND mp.fk_product = ".((int) $this->id);
3636 $sql .= " AND mp.role ='".$this->db->escape($role)."'";
3637 if ($socid > 0) {
3638 $sql .= " AND c.fk_soc = ".((int) $socid);
3639 }
3640
3641 $result = $this->db->query($sql);
3642 if ($result) {
3643 $obj = $this->db->fetch_object($result);
3644 $this->stats_mo['customers_'.$role] = $obj->nb_customers ? $obj->nb_customers : 0;
3645 $this->stats_mo['nb_'.$role] = $obj->nb ? $obj->nb : 0;
3646 $this->stats_mo['qty_'.$role] = $obj->qty ? price2num($obj->qty, 'MS') : 0; // qty may be a float due to the SUM()
3647 } else {
3648 $this->error = $this->db->error();
3649 $error++;
3650 }
3651 }
3652
3653 if (!empty($error)) {
3654 return -1;
3655 }
3656
3657 $parameters = array('socid' => $socid);
3658 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3659 if ($reshook > 0) {
3660 $this->stats_mo = $hookmanager->resArray['stats_mo'];
3661 }
3662
3663 return 1;
3664 }
3665
3666 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3673 public function load_stats_bom($socid = 0)
3674 {
3675 // phpcs:enable
3676 global $hookmanager, $action;
3677
3678 $error = 0;
3679
3680 $this->stats_bom['nb_toproduce'] = 0;
3681 $this->stats_bom['nb_toconsume'] = 0;
3682 $this->stats_bom['qty_toproduce'] = 0;
3683 $this->stats_bom['qty_toconsume'] = 0;
3684
3685 $sql = "SELECT COUNT(DISTINCT b.rowid) as nb_toproduce,";
3686 $sql .= " SUM(b.qty) as qty_toproduce";
3687 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3688 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom = b.rowid";
3689 $sql .= " WHERE ";
3690 $sql .= " b.entity IN (".getEntity('bom').")";
3691 $sql .= " AND b.fk_product =".((int) $this->id);
3692 $sql .= " GROUP BY b.rowid";
3693
3694 $result = $this->db->query($sql);
3695 if ($result) {
3696 $obj = $this->db->fetch_object($result);
3697 $this->stats_bom['nb_toproduce'] = !empty($obj->nb_toproduce) ? $obj->nb_toproduce : 0;
3698 $this->stats_bom['qty_toproduce'] = !empty($obj->qty_toproduce) ? price2num($obj->qty_toproduce) : 0;
3699 } else {
3700 $this->error = $this->db->error();
3701 $error++;
3702 }
3703
3704 $sql = "SELECT COUNT(DISTINCT bl.rowid) as nb_toconsume,";
3705 $sql .= " SUM(bl.qty) as qty_toconsume";
3706 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3707 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
3708 $sql .= " WHERE ";
3709 $sql .= " b.entity IN (".getEntity('bom').")";
3710 $sql .= " AND bl.fk_product =".((int) $this->id);
3711
3712 $result = $this->db->query($sql);
3713 if ($result) {
3714 $obj = $this->db->fetch_object($result);
3715 $this->stats_bom['nb_toconsume'] = !empty($obj->nb_toconsume) ? $obj->nb_toconsume : 0;
3716 $this->stats_bom['qty_toconsume'] = !empty($obj->qty_toconsume) ? price2num($obj->qty_toconsume) : 0;
3717 } else {
3718 $this->error = $this->db->error();
3719 $error++;
3720 }
3721
3722 if (!empty($error)) {
3723 return -1;
3724 }
3725
3726 $parameters = array('socid' => $socid);
3727 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3728 if ($reshook > 0) {
3729 $this->stats_bom = $hookmanager->resArray['stats_bom'];
3730 }
3731
3732 return 1;
3733 }
3734
3735 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3742 public function load_stats_propale($socid = 0)
3743 {
3744 // phpcs:enable
3745 global $user, $hookmanager, $action;
3746
3747 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,";
3748 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3749 $sql .= " FROM ".$this->db->prefix()."propaldet as pd";
3750 $sql .= ", ".$this->db->prefix()."propal as p";
3751 $sql .= ", ".$this->db->prefix()."societe as s";
3752 $sql .= " WHERE p.rowid = pd.fk_propal";
3753 $sql .= " AND p.fk_soc = s.rowid";
3754 $sql .= " AND p.entity IN (".getEntity('propal').")";
3755 $sql .= " AND pd.fk_product = ".((int) $this->id);
3756 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3757 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = p.fk_soc AND sc.fk_user = ".((int) $user->id);
3758 }
3759 //$sql.= " AND pr.fk_statut != 0";
3760 if ($socid > 0) {
3761 $sql .= " AND p.fk_soc = ".((int) $socid);
3762 }
3763 // Add where from hooks
3764 $parameters = array('socid' => $socid, 'type_element' => 'propal');
3765 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3766 $sql .= $hookmanager->resPrint;
3767 $result = $this->db->query($sql);
3768 if ($result) {
3769 $obj = $this->db->fetch_object($result);
3770 $this->stats_propale['customers'] = (int) $obj->nb_customers;
3771 $this->stats_propale['nb'] = (int) $obj->nb;
3772 $this->stats_propale['rows'] = (int) $obj->nb_rows;
3773 $this->stats_propale['qty'] = $obj->qty ? (float) $obj->qty : 0;
3774
3775 // if it's a virtual product, maybe it is in proposal by extension
3776 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3777 $TFather = $this->getFather();
3778 if (is_array($TFather) && !empty($TFather)) {
3779 foreach ($TFather as &$fatherData) {
3780 $pFather = new Product($this->db);
3781 $pFather->id = (int) $fatherData['id'];
3782 $qtyCoef = $fatherData['qty'];
3783
3784 if ($fatherData['incdec']) {
3785 $pFather->load_stats_propale($socid);
3786
3787 if (!empty($pFather->stats_propale)) {
3788 $this->stats_propale['customers'] += $pFather->stats_propale['customers'];
3789 $this->stats_propale['nb'] += $pFather->stats_propale['nb'];
3790 $this->stats_propale['rows'] += $pFather->stats_propale['rows'];
3791 $this->stats_propale['qty'] += $pFather->stats_propale['qty'] * $qtyCoef;
3792 }
3793 }
3794 }
3795 }
3796 }
3797
3798 $parameters = array('socid' => $socid);
3799 $reshook = $hookmanager->executeHooks('loadStatsCustomerProposal', $parameters, $this, $action);
3800 if ($reshook > 0) {
3801 $this->stats_propale = $hookmanager->resArray['stats_propale'];
3802 }
3803
3804 return 1;
3805 } else {
3806 $this->error = $this->db->error();
3807 return -1;
3808 }
3809 }
3810
3811
3812 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3819 public function load_stats_proposal_supplier($socid = 0)
3820 {
3821 // phpcs:enable
3822 global $user, $hookmanager, $action;
3823
3824 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,";
3825 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3826 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as pd";
3827 $sql .= ", ".$this->db->prefix()."supplier_proposal as p";
3828 $sql .= ", ".$this->db->prefix()."societe as s";
3829 $sql .= " WHERE p.rowid = pd.fk_supplier_proposal";
3830 $sql .= " AND p.fk_soc = s.rowid";
3831 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
3832 $sql .= " AND pd.fk_product = ".((int) $this->id);
3833 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3834 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = p.fk_soc AND sc.fk_user = ".((int) $user->id);
3835 }
3836 //$sql.= " AND pr.fk_statut != 0";
3837 if ($socid > 0) {
3838 $sql .= " AND p.fk_soc = ".((int) $socid);
3839 }
3840
3841 $result = $this->db->query($sql);
3842 if ($result) {
3843 $obj = $this->db->fetch_object($result);
3844 $this->stats_proposal_supplier['suppliers'] = $obj->nb_suppliers;
3845 $this->stats_proposal_supplier['nb'] = $obj->nb;
3846 $this->stats_proposal_supplier['rows'] = $obj->nb_rows;
3847 $this->stats_proposal_supplier['qty'] = $obj->qty ? $obj->qty : 0;
3848
3849 $parameters = array('socid' => $socid);
3850 $reshook = $hookmanager->executeHooks('loadStatsSupplierProposal', $parameters, $this, $action);
3851 if ($reshook > 0) {
3852 $this->stats_proposal_supplier = $hookmanager->resArray['stats_proposal_supplier'];
3853 }
3854
3855 return 1;
3856 } else {
3857 $this->error = $this->db->error();
3858 return -1;
3859 }
3860 }
3861
3862
3863 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3872 public function load_stats_commande($socid = 0, $filtrestatut = '', $forVirtualStock = 0)
3873 {
3874 // phpcs:enable
3875 global $user, $hookmanager, $action;
3876
3877
3878 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3879 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3880 $sql .= " FROM ".$this->db->prefix()."commandedet as cd";
3881 $sql .= ", ".$this->db->prefix()."commande as c";
3882 $sql .= ", ".$this->db->prefix()."societe as s";
3883 $sql .= " WHERE c.rowid = cd.fk_commande";
3884 $sql .= " AND c.fk_soc = s.rowid";
3885 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'commande').")";
3886 $sql .= " AND cd.fk_product = ".((int) $this->id);
3887 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3888 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3889 }
3890 if ($socid > 0) {
3891 $sql .= " AND c.fk_soc = ".((int) $socid);
3892 }
3893 if ($filtrestatut != '') {
3894 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3895 }
3896 // Add where from hooks
3897 $parameters = array('socid' => $socid, 'type_element' => 'order');
3898 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3899 $sql .= $hookmanager->resPrint;
3900 $result = $this->db->query($sql);
3901 if ($result) {
3902 $obj = $this->db->fetch_object($result);
3903 $this->stats_commande['customers'] = (int) $obj->nb_customers;
3904 $this->stats_commande['nb'] = (int) $obj->nb;
3905 $this->stats_commande['rows'] = (int) $obj->nb_rows;
3906 $this->stats_commande['qty'] = $obj->qty ? (float) $obj->qty : 0;
3907
3908 // if it's a virtual product, maybe it is in order by extension
3909 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3910 $TFather = $this->getFather();
3911 if (is_array($TFather) && !empty($TFather)) {
3912 foreach ($TFather as &$fatherData) {
3913 $pFather = new Product($this->db);
3914 $pFather->id = $fatherData['id'];
3915 $qtyCoef = $fatherData['qty'];
3916
3917 if ($fatherData['incdec']) {
3918 $pFather->load_stats_commande($socid, $filtrestatut);
3919
3920 $this->stats_commande['customers'] += $pFather->stats_commande['customers'];
3921 $this->stats_commande['nb'] += $pFather->stats_commande['nb'];
3922 $this->stats_commande['rows'] += $pFather->stats_commande['rows'];
3923 $this->stats_commande['qty'] += $pFather->stats_commande['qty'] * $qtyCoef;
3924 }
3925 }
3926 }
3927 }
3928
3929 // If stock decrease is on invoice validation, the theoretical stock continue to
3930 // count the orders lines containing product in theoretical stock when some are already removed by invoice validation.
3931 if ($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
3932 // Do the request using an UNION instead of a OR in the JOIN that is very slow
3933 if (getDolGlobalString('DECREASE_ONLY_UNINVOICEDPRODUCTS')) {
3934 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is on, we make a compensation but only if order not yet invoice.
3935 $adeduire = 0;
3936 $sql = "SELECT SUM(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3937 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3938 //$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'))";
3939 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_target = f.rowid AND el.targettype = 'facture' AND sourcetype = 'commande' ";
3940 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid";
3941 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3942 $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
3943
3944 $sql .= " UNION ";
3945
3946 $sql .= "SELECT SUM(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3947 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3948 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_source = f.rowid AND el.targettype = 'commande' AND sourcetype = 'facture' ";
3949 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_target = c.rowid";
3950 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3951 $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
3952 // Add where from hooks
3953 $parameters = array('socid' => $socid, 'type_element' => 'order');
3954 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3955 $sql .= $hookmanager->resPrint;
3956 $resql = $this->db->query($sql);
3957 if ($resql) {
3958 while ($obj = $this->db->fetch_object($resql)) {
3959 $adeduire += (float) $obj->count;
3960 }
3961 }
3962
3963 $this->stats_commande['qty'] -= $adeduire;
3964 } else {
3965 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is off, we make a compensation with lines of invoices linked to the order
3966 include_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
3967
3968 // For every order having invoice already validated we need to decrease stock cause it's in physical stock
3969 $adeduire = 0;
3970 $sql = "SELECT sum(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3971 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3972 //$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'))";
3973 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_target = f.rowid AND el.targettype = 'facture' AND sourcetype = 'commande' ";
3974 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid";
3975 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product = ".((int) $this->id);
3976 $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
3977
3978 $sql .= " UNION ALL ";
3979
3980 $sql .= "SELECT sum(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3981 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3982 $sql .= " JOIN ".$this->db->prefix()."element_element as el ON el.fk_source = f.rowid AND el.targettype = 'commande' AND sourcetype = 'facture' ";
3983 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_target = c.rowid";
3984 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product = ".((int) $this->id);
3985 $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
3986
3987 dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3988 // Add where from hooks
3989 $parameters = array('socid' => $socid, 'type_element' => 'order');
3990 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
3991 $sql .= $hookmanager->resPrint;
3992 $resql = $this->db->query($sql);
3993 if ($resql) {
3994 while ($obj = $this->db->fetch_object($resql)) {
3995 $adeduire += (float) $obj->count;
3996 }
3997 } else {
3998 $this->error = $this->db->error();
3999 return -1;
4000 }
4001
4002 $this->stats_commande['qty'] -= $adeduire;
4003 }
4004 }
4005
4006 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
4007 $reshook = $hookmanager->executeHooks('loadStatsCustomerOrder', $parameters, $this, $action);
4008 if ($reshook > 0) {
4009 $this->stats_commande = $hookmanager->resArray['stats_commande'];
4010 }
4011 return 1;
4012 } else {
4013 $this->error = $this->db->error();
4014 return -1;
4015 }
4016 }
4017
4018 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4029 public function load_stats_commande_fournisseur($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null, $dateofvirtualstockmin = null)
4030 {
4031 // phpcs:enable
4032 global $user, $hookmanager, $action;
4033
4034 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,";
4035 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
4036 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as cd";
4037 $sql .= ", ".$this->db->prefix()."commande_fournisseur as c";
4038 $sql .= ", ".$this->db->prefix()."societe as s";
4039 $sql .= " WHERE c.rowid = cd.fk_commande";
4040 $sql .= " AND c.fk_soc = s.rowid";
4041 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
4042 $sql .= " AND cd.fk_product = ".((int) $this->id);
4043 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
4044 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
4045 }
4046 if ($socid > 0) {
4047 $sql .= " AND c.fk_soc = ".((int) $socid);
4048 }
4049 if ($filtrestatut != '') {
4050 $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")"; // Peut valoir 0
4051 }
4052 if (!empty($dateofvirtualstock)) {
4053 // Window of the horizon. The lower bound, when set, drops the orders that did not arrive as planned: their date has
4054 // passed, so they are no longer a credible supply. Orders with no planned delivery date fall outside the window as
4055 // well, since their arrival is unknown, unless the option says to keep them.
4056 $sqlwindow = "c.date_livraison <= '".$this->db->idate($dateofvirtualstock)."'";
4057 if (!empty($dateofvirtualstockmin)) {
4058 $sqlwindow = "c.date_livraison >= '".$this->db->idate($dateofvirtualstockmin)."' AND ".$sqlwindow;
4059 }
4060 if (getDolGlobalInt('STOCK_VIRTUAL_HORIZON_INCLUDE_UNDATED_ORDERS')) {
4061 $sqlwindow .= " OR c.date_livraison IS NULL";
4062 }
4063 $sql .= " AND (".$sqlwindow.")";
4064 }
4065
4066 $result = $this->db->query($sql);
4067 if ($result) {
4068 $obj = $this->db->fetch_object($result);
4069 $this->stats_commande_fournisseur['suppliers'] = $obj->nb_suppliers;
4070 $this->stats_commande_fournisseur['nb'] = $obj->nb;
4071 $this->stats_commande_fournisseur['rows'] = $obj->nb_rows;
4072 $this->stats_commande_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
4073
4074 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
4075 $reshook = $hookmanager->executeHooks('loadStatsSupplierOrder', $parameters, $this, $action);
4076 if ($reshook > 0) {
4077 $this->stats_commande_fournisseur = $hookmanager->resArray['stats_commande_fournisseur'];
4078 }
4079
4080 return 1;
4081 } else {
4082 $this->error = $this->db->error().' sql='.$sql;
4083 return -1;
4084 }
4085 }
4086
4087 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4097 public function load_stats_sending($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $filterShipmentStatus = '')
4098 {
4099 // phpcs:enable
4100 global $user, $hookmanager, $action;
4101
4102 $sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,";
4103 $sql .= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty";
4104 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
4105 $sql .= ", ".$this->db->prefix()."commandedet as cd";
4106 $sql .= ", ".$this->db->prefix()."commande as c";
4107 $sql .= ", ".$this->db->prefix()."expedition as e";
4108 $sql .= ", ".$this->db->prefix()."societe as s";
4109 $sql .= " WHERE e.rowid = ed.fk_expedition";
4110 $sql .= " AND c.rowid = cd.fk_commande";
4111 $sql .= " AND e.fk_soc = s.rowid";
4112 $sql .= " AND e.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'expedition').")";
4113 $sql .= " AND ed.fk_elementdet = cd.rowid";
4114 $sql .= " AND cd.fk_product = ".((int) $this->id);
4115 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
4116 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = e.fk_soc AND sc.fk_user = ".((int) $user->id);
4117 }
4118 if ($socid > 0) {
4119 $sql .= " AND e.fk_soc = ".((int) $socid);
4120 }
4121 if ($filtrestatut != '') {
4122 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
4123 }
4124 if (!empty($filterShipmentStatus)) {
4125 $sql .= " AND e.fk_statut IN (".$this->db->sanitize($filterShipmentStatus).")";
4126 }
4127
4128 $result = $this->db->query($sql);
4129 if ($result) {
4130 $obj = $this->db->fetch_object($result);
4131 $this->stats_expedition['customers'] = (int) $obj->nb_customers;
4132 $this->stats_expedition['nb'] = (int) $obj->nb;
4133 $this->stats_expedition['rows'] = (int) $obj->nb_rows;
4134 $this->stats_expedition['qty'] = $obj->qty ? (float) $obj->qty : 0;
4135
4136 // if it's a virtual product, maybe it is in sending by extension
4137 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4138 $TFather = $this->getFather();
4139 if (is_array($TFather) && !empty($TFather)) {
4140 foreach ($TFather as &$fatherData) {
4141 $pFather = new Product($this->db);
4142 $pFather->id = $fatherData['id'];
4143 $qtyCoef = $fatherData['qty'];
4144
4145 if ($fatherData['incdec']) {
4146 $pFather->load_stats_sending($socid, $filtrestatut, $forVirtualStock);
4147
4148 if (!empty($pFather->stats_expedition)) {
4149 $this->stats_expedition['customers'] += $pFather->stats_expedition['customers'];
4150 $this->stats_expedition['nb'] += $pFather->stats_expedition['nb'];
4151 $this->stats_expedition['rows'] += $pFather->stats_expedition['rows'];
4152 $this->stats_expedition['qty'] += $pFather->stats_expedition['qty'] * $qtyCoef;
4153 }
4154 }
4155 }
4156 }
4157 }
4158
4159 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock, 'filterShipmentStatus' => $filterShipmentStatus);
4160 $reshook = $hookmanager->executeHooks('loadStatsSending', $parameters, $this, $action);
4161 if ($reshook > 0) {
4162 $this->stats_expedition = $hookmanager->resArray['stats_expedition'];
4163 }
4164
4165 return 1;
4166 } else {
4167 $this->error = $this->db->error();
4168 return -1;
4169 }
4170 }
4171
4172 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4183 public function load_stats_reception($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null, $dateofvirtualstockmin = null)
4184 {
4185 // phpcs:enable
4186 global $user, $hookmanager, $action;
4187
4188 $sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_suppliers, COUNT(DISTINCT cf.rowid) as nb,";
4189 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4190 $sql .= " FROM ".$this->db->prefix()."receptiondet_batch as fd";
4191 $sql .= ", ".$this->db->prefix()."commande_fournisseur as cf";
4192 $sql .= ", ".$this->db->prefix()."societe as s";
4193 $sql .= " WHERE cf.rowid = fd.fk_element";
4194 $sql .= " AND cf.fk_soc = s.rowid";
4195 $sql .= " AND cf.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
4196 $sql .= " AND fd.fk_product = ".((int) $this->id);
4197 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
4198 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = cf.fk_soc AND sc.fk_user = ".((int) $user->id);
4199 }
4200 if ($socid > 0) {
4201 $sql .= " AND cf.fk_soc = ".((int) $socid);
4202 }
4203 if ($filtrestatut != '') {
4204 $sql .= " AND cf.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
4205 }
4206 if (!empty($dateofvirtualstock)) {
4207 $sql .= " AND fd.datec <= '".$this->db->idate($dateofvirtualstock)."'";
4208 }
4209 if (!empty($dateofvirtualstockmin)) {
4210 // Same window as the supplier order side: both halves of the subtraction must always run on the same population of
4211 // orders, otherwise an order dropped from one half while its receptions are still subtracted in the other one would
4212 // push the theoretical stock below the real stock.
4213 $sqlwindow = "cf.date_livraison >= '".$this->db->idate($dateofvirtualstockmin)."' AND cf.date_livraison <= '".$this->db->idate($dateofvirtualstock)."'";
4214 if (getDolGlobalInt('STOCK_VIRTUAL_HORIZON_INCLUDE_UNDATED_ORDERS')) {
4215 $sqlwindow .= " OR cf.date_livraison IS NULL";
4216 }
4217 $sql .= " AND (".$sqlwindow.")";
4218 }
4219
4220 $result = $this->db->query($sql);
4221 if ($result) {
4222 $obj = $this->db->fetch_object($result);
4223 $this->stats_reception['suppliers'] = $obj->nb_suppliers;
4224 $this->stats_reception['nb'] = $obj->nb;
4225 $this->stats_reception['rows'] = $obj->nb_rows;
4226 $this->stats_reception['qty'] = $obj->qty ? $obj->qty : 0;
4227
4228 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
4229 $reshook = $hookmanager->executeHooks('loadStatsReception', $parameters, $this, $action);
4230 if ($reshook > 0) {
4231 $this->stats_reception = $hookmanager->resArray['stats_reception'];
4232 }
4233
4234 return 1;
4235 } else {
4236 $this->error = $this->db->error();
4237 return -1;
4238 }
4239 }
4240
4241 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4253 public function load_stats_inproduction($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null, $warehouseid = 0, $dateofvirtualstockmin = null)
4254 {
4255 // phpcs:enable
4256 global $user, $hookmanager, $action;
4257
4258 $serviceStockIsEnabled = isModEnabled("service") && getDolGlobalString('STOCK_SUPPORTS_SERVICES');
4259
4260 $sql = "SELECT COUNT(DISTINCT m.fk_soc) as nb_customers, COUNT(DISTINCT m.rowid) as nb,";
4261 $sql .= " COUNT(mp.rowid) as nb_rows, SUM(mp.qty) as qty, role";
4262 $sql .= " FROM ".$this->db->prefix()."mrp_production as mp";
4263 $sql .= ", ".$this->db->prefix()."mrp_mo as m";
4264 $sql .= " LEFT JOIN ".$this->db->prefix()."societe as s ON s.rowid = m.fk_soc";
4265 $sql .= " WHERE m.rowid = mp.fk_mo";
4266 $sql .= " AND m.entity IN (".getEntity(($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE')) ? 'stock' : 'mrp').")";
4267 $sql .= " AND mp.fk_product = ".((int) $this->id);
4268 $sql .= " AND (mp.disable_stock_change IN (0) OR mp.disable_stock_change IS NULL)";
4269 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
4270 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = m.fk_soc AND sc.fk_user = ".((int) $user->id);
4271 }
4272 if ($socid > 0) {
4273 $sql .= " AND m.fk_soc = ".((int) $socid);
4274 }
4275 if ($filtrestatut != '') {
4276 $sql .= " AND m.status IN (".$this->db->sanitize($filtrestatut).")";
4277 }
4278 if (!empty($dateofvirtualstockmin)) {
4279 // Window of the horizon. Here the relevant date is the planned end of production, not the validation date: a
4280 // manufacturing order validated months ago but never completed is no longer a credible source of goods, and its
4281 // components are no longer a credible consumption either. The validation date is kept as a fallback for the
4282 // orders that carry no planned end date.
4283 $sql .= " AND COALESCE(m.date_end_planned, m.date_valid) >= '".$this->db->idate($dateofvirtualstockmin)."'";
4284 $sql .= " AND COALESCE(m.date_end_planned, m.date_valid) <= '".$this->db->idate($dateofvirtualstock)."'";
4285 } elseif (!empty($dateofvirtualstock)) {
4286 $sql .= " AND m.date_valid <= '".$this->db->idate($dateofvirtualstock)."'"; // better date to code ? end of production ?
4287 }
4288 if (!$serviceStockIsEnabled) {
4289 $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))";
4290 }
4291 if (!empty($warehouseid)) {
4292 $sql .= " AND m.fk_warehouse = ".((int) $warehouseid);
4293 }
4294 $sql .= " GROUP BY role";
4295
4296 if ($warehouseid) {
4297 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
4298 } else {
4299 $this->stats_mrptoconsume['customers'] = 0;
4300 $this->stats_mrptoconsume['nb'] = 0;
4301 $this->stats_mrptoconsume['rows'] = 0;
4302 $this->stats_mrptoconsume['qty'] = 0.0;
4303 $this->stats_mrptoproduce['customers'] = 0;
4304 $this->stats_mrptoproduce['nb'] = 0;
4305 $this->stats_mrptoproduce['rows'] = 0;
4306 $this->stats_mrptoproduce['qty'] = 0.0;
4307 }
4308
4309 $result = $this->db->query($sql);
4310 if ($result) {
4311 while ($obj = $this->db->fetch_object($result)) {
4312 if ($obj->role == 'toconsume' && empty($warehouseid)) {
4313 $this->stats_mrptoconsume['customers'] += (int) $obj->nb_customers;
4314 $this->stats_mrptoconsume['nb'] += (int) $obj->nb;
4315 $this->stats_mrptoconsume['rows'] += (int) $obj->nb_rows;
4316 $this->stats_mrptoconsume['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4317 }
4318 if ($obj->role == 'consumed' && empty($warehouseid)) {
4319 //$this->stats_mrptoconsume['customers'] += $obj->nb_customers;
4320 //$this->stats_mrptoconsume['nb'] += $obj->nb;
4321 //$this->stats_mrptoconsume['rows'] += $obj->nb_rows;
4322 $this->stats_mrptoconsume['qty'] -= ($obj->qty ? (float) $obj->qty : 0.0);
4323 }
4324 if ($obj->role == 'toproduce') {
4325 if ($warehouseid) {
4326 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4327 } else {
4328 $this->stats_mrptoproduce['customers'] += (int) $obj->nb_customers;
4329 $this->stats_mrptoproduce['nb'] += (int) $obj->nb;
4330 $this->stats_mrptoproduce['rows'] += (int) $obj->nb_rows;
4331 $this->stats_mrptoproduce['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4332 }
4333 }
4334 if ($obj->role == 'produced') {
4335 //$this->stats_mrptoproduce['customers'] += $obj->nb_customers;
4336 //$this->stats_mrptoproduce['nb'] += $obj->nb;
4337 //$this->stats_mrptoproduce['rows'] += $obj->nb_rows;
4338 if ($warehouseid) {
4339 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
4340 } else {
4341 $this->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
4342 }
4343 }
4344 }
4345
4346 // Clean data
4347 if ($warehouseid) {
4348 if ($this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] < 0) {
4349 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
4350 }
4351 } else {
4352 if ($this->stats_mrptoconsume['qty'] < 0) {
4353 $this->stats_mrptoconsume['qty'] = 0;
4354 }
4355 if ($this->stats_mrptoproduce['qty'] < 0) {
4356 $this->stats_mrptoproduce['qty'] = 0;
4357 }
4358 }
4359
4360 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
4361 $reshook = $hookmanager->executeHooks('loadStatsInProduction', $parameters, $this, $action);
4362 if ($reshook > 0) {
4363 $this->stats_mrptoproduce = $hookmanager->resArray['stats_mrptoproduce'];
4364 }
4365
4366 return 1;
4367 } else {
4368 $this->error = $this->db->error();
4369 return -1;
4370 }
4371 }
4372
4373 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4380 public function load_stats_contrat($socid = 0)
4381 {
4382 // phpcs:enable
4383 global $user, $hookmanager, $action;
4384
4385 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
4386 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
4387 $sql .= " FROM ".$this->db->prefix()."contratdet as cd";
4388 $sql .= ", ".$this->db->prefix()."contrat as c";
4389 $sql .= ", ".$this->db->prefix()."societe as s";
4390 $sql .= " WHERE c.rowid = cd.fk_contrat";
4391 $sql .= " AND c.fk_soc = s.rowid";
4392 $sql .= " AND c.entity IN (".getEntity('contract').")";
4393 $sql .= " AND cd.fk_product = ".((int) $this->id);
4394 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4395 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
4396 }
4397 //$sql.= " AND c.statut != 0";
4398 if ($socid > 0) {
4399 $sql .= " AND c.fk_soc = ".((int) $socid);
4400 }
4401
4402 $result = $this->db->query($sql);
4403 if ($result) {
4404 $obj = $this->db->fetch_object($result);
4405 $this->stats_contrat['customers'] = (int) $obj->nb_customers;
4406 $this->stats_contrat['nb'] = (int) $obj->nb;
4407 $this->stats_contrat['rows'] = (int) $obj->nb_rows;
4408 $this->stats_contrat['qty'] = $obj->qty ? (float) $obj->qty : 0;
4409
4410 // if it's a virtual product, maybe it is in contract by extension
4411 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4412 $TFather = $this->getFather();
4413 if (is_array($TFather) && !empty($TFather)) {
4414 foreach ($TFather as &$fatherData) {
4415 $pFather = new Product($this->db);
4416 $pFather->id = $fatherData['id'];
4417 $qtyCoef = $fatherData['qty'];
4418
4419 if ($fatherData['incdec']) {
4420 $pFather->load_stats_contrat($socid);
4421
4422 if (!empty($pFather->stats_contrat)) {
4423 $this->stats_contrat['customers'] += $pFather->stats_contrat['customers'];
4424 $this->stats_contrat['nb'] += $pFather->stats_contrat['nb'];
4425 $this->stats_contrat['rows'] += $pFather->stats_contrat['rows'];
4426 $this->stats_contrat['qty'] += $pFather->stats_contrat['qty'] * $qtyCoef;
4427 }
4428 }
4429 }
4430 }
4431 }
4432
4433 $parameters = array('socid' => $socid);
4434 $reshook = $hookmanager->executeHooks('loadStatsContract', $parameters, $this, $action);
4435 if ($reshook > 0) {
4436 $this->stats_contrat = $hookmanager->resArray['stats_contrat'];
4437 }
4438
4439 return 1;
4440 } else {
4441 $this->error = $this->db->error().' sql='.$sql;
4442 return -1;
4443 }
4444 }
4445
4446 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4453 public function load_stats_facture($socid = 0)
4454 {
4455 // phpcs:enable
4456 global $user, $hookmanager, $action;
4457
4458 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4459 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(".$this->db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1').") as qty";
4460 $sql .= " FROM ".$this->db->prefix()."facturedet as fd";
4461 $sql .= ", ".$this->db->prefix()."facture as f";
4462 $sql .= ", ".$this->db->prefix()."societe as s";
4463 $sql .= " WHERE f.rowid = fd.fk_facture";
4464 $sql .= " AND f.fk_soc = s.rowid";
4465 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4466 $sql .= " AND fd.fk_product = ".((int) $this->id);
4467 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4468 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4469 }
4470 //$sql.= " AND f.fk_statut != 0";
4471 if ($socid > 0) {
4472 $sql .= " AND f.fk_soc = ".((int) $socid);
4473 }
4474
4475 $result = $this->db->query($sql);
4476 if ($result) {
4477 $obj = $this->db->fetch_object($result);
4478 $this->stats_facture['customers'] = (int) $obj->nb_customers;
4479 $this->stats_facture['nb'] = (int) $obj->nb;
4480 $this->stats_facture['rows'] = (int) $obj->nb_rows;
4481 $this->stats_facture['qty'] = $obj->qty ? (float) $obj->qty : 0;
4482
4483 // if it's a virtual product, maybe it is in invoice by extension
4484 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4485 $TFather = $this->getFather();
4486 if (is_array($TFather) && !empty($TFather)) {
4487 foreach ($TFather as &$fatherData) {
4488 $pFather = new Product($this->db);
4489 $pFather->id = $fatherData['id'];
4490 $qtyCoef = $fatherData['qty'];
4491
4492 if ($fatherData['incdec']) {
4493 $pFather->load_stats_facture($socid);
4494
4495 if (!empty($pFather->stats_facture)) {
4496 $this->stats_facture['customers'] += $pFather->stats_facture['customers'];
4497 $this->stats_facture['nb'] += $pFather->stats_facture['nb'];
4498 $this->stats_facture['rows'] += $pFather->stats_facture['rows'];
4499 $this->stats_facture['qty'] += $pFather->stats_facture['qty'] * $qtyCoef;
4500 }
4501 }
4502 }
4503 }
4504 }
4505
4506 $parameters = array('socid' => $socid);
4507 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoice', $parameters, $this, $action);
4508 if ($reshook > 0) {
4509 $this->stats_facture = $hookmanager->resArray['stats_facture'];
4510 }
4511
4512 return 1;
4513 } else {
4514 $this->error = $this->db->error();
4515 return -1;
4516 }
4517 }
4518
4519
4520 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4527 public function load_stats_facturerec($socid = 0)
4528 {
4529 // phpcs:enable
4530 global $user, $hookmanager, $action;
4531
4532 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4533 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4534 $sql .= " FROM ".MAIN_DB_PREFIX."facturedet_rec as fd";
4535 $sql .= ", ".MAIN_DB_PREFIX."facture_rec as f";
4536 $sql .= ", ".MAIN_DB_PREFIX."societe as s";
4537 $sql .= " WHERE f.rowid = fd.fk_facture";
4538 $sql .= " AND f.fk_soc = s.rowid";
4539 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4540 $sql .= " AND fd.fk_product = ".((int) $this->id);
4541 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4542 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4543 }
4544 //$sql.= " AND f.fk_statut != 0";
4545 if ($socid > 0) {
4546 $sql .= " AND f.fk_soc = ".((int) $socid);
4547 }
4548
4549 $result = $this->db->query($sql);
4550 if ($result) {
4551 $obj = $this->db->fetch_object($result);
4552 $this->stats_facturerec['customers'] = (int) $obj->nb_customers;
4553 $this->stats_facturerec['nb'] = (int) $obj->nb;
4554 $this->stats_facturerec['rows'] = (int) $obj->nb_rows;
4555 $this->stats_facturerec['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4556
4557 // if it's a virtual product, maybe it is in invoice by extension
4558 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4559 $TFather = $this->getFather();
4560 if (is_array($TFather) && !empty($TFather)) {
4561 foreach ($TFather as &$fatherData) {
4562 $pFather = new Product($this->db);
4563 $pFather->id = $fatherData['id'];
4564 $qtyCoef = $fatherData['qty'];
4565
4566 if ($fatherData['incdec']) {
4567 $pFather->load_stats_facture($socid);
4568
4569 $this->stats_facturerec['customers'] += $pFather->stats_facturerec['customers'];
4570 $this->stats_facturerec['nb'] += $pFather->stats_facturerec['nb'];
4571 $this->stats_facturerec['rows'] += $pFather->stats_facturerec['rows'];
4572 $this->stats_facturerec['qty'] += $pFather->stats_facturerec['qty'] * $qtyCoef;
4573 }
4574 }
4575 }
4576 }
4577
4578 $parameters = array('socid' => $socid);
4579 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoiceRec', $parameters, $this, $action);
4580 if ($reshook > 0) {
4581 $this->stats_facturerec = $hookmanager->resArray['stats_facturerec'];
4582 }
4583
4584 return 1;
4585 } else {
4586 $this->error = $this->db->error();
4587 return -1;
4588 }
4589 }
4590
4591 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4598 public function load_stats_facture_fournisseur($socid = 0)
4599 {
4600 // phpcs:enable
4601 global $user, $hookmanager, $action;
4602
4603 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
4604 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4605 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as fd";
4606 $sql .= ", ".$this->db->prefix()."facture_fourn as f";
4607 $sql .= ", ".$this->db->prefix()."societe as s";
4608 $sql .= " WHERE f.rowid = fd.fk_facture_fourn";
4609 $sql .= " AND f.fk_soc = s.rowid";
4610 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4611 $sql .= " AND fd.fk_product = ".((int) $this->id);
4612 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4613 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4614 }
4615 //$sql.= " AND f.fk_statut != 0";
4616 if ($socid > 0) {
4617 $sql .= " AND f.fk_soc = ".((int) $socid);
4618 }
4619
4620 $result = $this->db->query($sql);
4621 if ($result) {
4622 $obj = $this->db->fetch_object($result);
4623 $this->stats_facture_fournisseur['suppliers'] = (int) $obj->nb_suppliers;
4624 $this->stats_facture_fournisseur['nb'] = (int) $obj->nb;
4625 $this->stats_facture_fournisseur['rows'] = (int) $obj->nb_rows;
4626 $this->stats_facture_fournisseur['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4627
4628 $parameters = array('socid' => $socid);
4629 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoice', $parameters, $this, $action);
4630 if ($reshook > 0) {
4631 $this->stats_facture_fournisseur = $hookmanager->resArray['stats_facture_fournisseur'];
4632 }
4633
4634 return 1;
4635 } else {
4636 $this->error = $this->db->error();
4637 return -1;
4638 }
4639 }
4640
4641 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4648 public function load_stats_facturefournrec($socid = 0)
4649 {
4650 // phpcs:enable
4651 global $user, $hookmanager, $action;
4652
4653 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
4654 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4655 $sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn_det_rec as fd";
4656 $sql .= ", ".MAIN_DB_PREFIX."facture_fourn_rec as f";
4657 $sql .= ", ".MAIN_DB_PREFIX."societe as s";
4658 if (!$user->hasRight('societe', 'client', 'voir')) {
4659 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
4660 }
4661 $sql .= " WHERE f.rowid = fd.fk_facture";
4662 $sql .= " AND f.fk_soc = s.rowid";
4663 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4664 $sql .= " AND fd.fk_product = ".((int) $this->id);
4665 if (!$user->hasRight('societe', 'client', 'voir')) {
4666 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4667 }
4668 //$sql.= " AND f.fk_statut != 0";
4669 if ($socid > 0) {
4670 $sql .= " AND f.fk_soc = ".((int) $socid);
4671 }
4672
4673 $result = $this->db->query($sql);
4674 if ($result) {
4675 $obj = $this->db->fetch_object($result);
4676 $this->stats_facturefournrec['suppliers'] = (int) $obj->nb_suppliers;
4677 $this->stats_facturefournrec['nb'] = (int) $obj->nb;
4678 $this->stats_facturefournrec['rows'] = (int) $obj->nb_rows;
4679 $this->stats_facturefournrec['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4680
4681 $parameters = array('socid' => $socid);
4682 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoiceRec', $parameters, $this, $action);
4683 if ($reshook > 0) {
4684 $this->stats_facturefournrec = $hookmanager->resArray['stats_facturefournrec'];
4685 }
4686
4687 return 1;
4688 } else {
4689 $this->error = $this->db->error();
4690 return -1;
4691 }
4692 }
4693
4694 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4703 private function _get_stats($sql, $mode, $year = 0)
4704 {
4705 // phpcs:enable
4706 $tab = array();
4707
4708 $resql = $this->db->query($sql);
4709 if ($resql) {
4710 $num = $this->db->num_rows($resql);
4711 $i = 0;
4712 while ($i < $num) {
4713 $arr = $this->db->fetch_array($resql);
4714 if (is_array($arr)) {
4715 $keyfortab = (string) $arr[1];
4716 if ($year == -1) {
4717 $keyfortab = substr($keyfortab, -2);
4718 }
4719
4720 if ($mode == 'byunit') {
4721 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[0]; // 1st field
4722 } elseif ($mode == 'bynumber') {
4723 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4724 } elseif ($mode == 'byamount') {
4725 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4726 } else {
4727 // Bad value for $mode
4728 return -1;
4729 }
4730 }
4731 $i++;
4732 }
4733 } else {
4734 $this->error = $this->db->error().' sql='.$sql;
4735 return -1;
4736 }
4737
4738 if (empty($year)) {
4739 $year = dol_print_date(dol_now(), '%Y');
4740 $month = dol_print_date(dol_now(), '%m');
4741 } elseif ($year == -1) {
4742 $year = '';
4743 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4744 } else {
4745 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4746 }
4747
4748 $result = array();
4749
4750 for ($j = 0; $j < 12; $j++) {
4751 // $idx is 'D', 'N', 'O', 'S', ... (First letter of month in user language)
4752 $idx = ucfirst(dol_trunc(dol_print_date(dol_mktime(12, 0, 0, $month, 1, 1970), "%b"), 1, 'right', 'UTF-8', 1));
4753
4754 // print $idx.'-'.$year.'-'.$month.'<br>';
4755 $result[$j] = array($idx, isset($tab[$year.$month]) ? $tab[$year.$month] : 0);
4756 // $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0);
4757
4758 $month = "0".($month - 1);
4759 if (dol_strlen($month) == 3) {
4760 $month = substr($month, 1);
4761 }
4762 if ($month == 0) {
4763 $month = 12;
4764 if ($year !== '') { // $year is '' when we want stats for all years
4765 $year -= 1;
4766 }
4767 }
4768 }
4769
4770 return array_reverse($result);
4771 }
4772
4773
4774 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4785 public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4786 {
4787 // phpcs:enable
4788 global $user;
4789
4790 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4791 if ($mode == 'bynumber') {
4792 $sql .= ", count(DISTINCT f.rowid)";
4793 }
4794 $sql .= ", sum(d.total_ht) as total_ht";
4795 $sql .= " FROM ".$this->db->prefix()."facturedet as d, ".$this->db->prefix()."facture as f, ".$this->db->prefix()."societe as s";
4796 if ($filteronproducttype >= 0) {
4797 $sql .= ", ".$this->db->prefix()."product as p";
4798 }
4799 if (!$user->hasRight('societe', 'client', 'voir')) {
4800 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4801 }
4802 $sql .= " WHERE f.rowid = d.fk_facture";
4803 if ($this->id > 0) {
4804 $sql .= " AND d.fk_product = ".((int) $this->id);
4805 } else {
4806 $sql .= " AND d.fk_product > 0";
4807 }
4808 if ($filteronproducttype >= 0) {
4809 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4810 }
4811 $sql .= " AND f.fk_soc = s.rowid";
4812 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4813 if (!$user->hasRight('societe', 'client', 'voir')) {
4814 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4815 }
4816 if ($socid > 0) {
4817 $sql .= " AND f.fk_soc = ".((int) $socid);
4818 }
4819 $sql .= $morefilter;
4820 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4821 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4822
4823 return $this->_get_stats($sql, $mode, $year);
4824 }
4825
4826
4827 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4838 public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4839 {
4840 // phpcs:enable
4841 global $user;
4842
4843 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4844 if ($mode == 'bynumber') {
4845 $sql .= ", count(DISTINCT f.rowid)";
4846 }
4847 $sql .= ", sum(d.total_ht) as total_ht";
4848 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as d, ".$this->db->prefix()."facture_fourn as f, ".$this->db->prefix()."societe as s";
4849 if ($filteronproducttype >= 0) {
4850 $sql .= ", ".$this->db->prefix()."product as p";
4851 }
4852 if (!$user->hasRight('societe', 'client', 'voir')) {
4853 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4854 }
4855 $sql .= " WHERE f.rowid = d.fk_facture_fourn";
4856 if ($this->id > 0) {
4857 $sql .= " AND d.fk_product = ".((int) $this->id);
4858 } else {
4859 $sql .= " AND d.fk_product > 0";
4860 }
4861 if ($filteronproducttype >= 0) {
4862 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4863 }
4864 $sql .= " AND f.fk_soc = s.rowid";
4865 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4866 if (!$user->hasRight('societe', 'client', 'voir')) {
4867 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4868 }
4869 if ($socid > 0) {
4870 $sql .= " AND f.fk_soc = ".((int) $socid);
4871 }
4872 $sql .= $morefilter;
4873 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4874 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4875
4876 return $this->_get_stats($sql, $mode, $year);
4877 }
4878
4879 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4890 public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4891 {
4892 // phpcs:enable
4893 global $user, $hookmanager;
4894
4895 $sql = "SELECT sum(d.qty) as qty, date_format(p.datep, '%Y%m')";
4896 if ($mode == 'bynumber') {
4897 $sql .= ", count(DISTINCT p.rowid)";
4898 }
4899 $sql .= ", sum(d.total_ht) as total_ht";
4900 $sql .= " FROM ".$this->db->prefix()."propaldet as d, ".$this->db->prefix()."propal as p, ".$this->db->prefix()."societe as s";
4901 if ($filteronproducttype >= 0) {
4902 $sql .= ", ".$this->db->prefix()."product as prod";
4903 }
4904 if (!$user->hasRight('societe', 'client', 'voir')) {
4905 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4906 }
4907 $sql .= " WHERE p.rowid = d.fk_propal";
4908 if ($this->id > 0) {
4909 $sql .= " AND d.fk_product = ".((int) $this->id);
4910 } else {
4911 $sql .= " AND d.fk_product > 0";
4912 }
4913 if ($filteronproducttype >= 0) {
4914 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4915 }
4916 $sql .= " AND p.fk_soc = s.rowid";
4917 $sql .= " AND p.entity IN (".getEntity('propal').")";
4918 if (!$user->hasRight('societe', 'client', 'voir')) {
4919 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4920 }
4921 if ($socid > 0) {
4922 $sql .= " AND p.fk_soc = ".((int) $socid);
4923 }
4924 $sql .= $morefilter;
4925 $parameters = array('socid' => $user->socid);
4926 $hookmanager->executeHooks('productGetNbPropal', $parameters, $this); // Note that $action and $object may have been modified by hook
4927 $sql .= $hookmanager->resPrint;
4928 $sql .= " GROUP BY date_format(p.datep,'%Y%m')";
4929 $sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC";
4930 return $this->_get_stats($sql, $mode, $year);
4931 }
4932
4933 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4944 public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4945 {
4946 // phpcs:enable
4947 global $user;
4948
4949 $sql = "SELECT sum(d.qty) as qty, date_format(p.date_valid, '%Y%m')";
4950 if ($mode == 'bynumber') {
4951 $sql .= ", count(DISTINCT p.rowid)";
4952 }
4953 $sql .= ", sum(d.total_ht) as total_ht";
4954 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as d, ".$this->db->prefix()."supplier_proposal as p, ".$this->db->prefix()."societe as s";
4955 if ($filteronproducttype >= 0) {
4956 $sql .= ", ".$this->db->prefix()."product as prod";
4957 }
4958 if (!$user->hasRight('societe', 'client', 'voir')) {
4959 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4960 }
4961 $sql .= " WHERE p.rowid = d.fk_supplier_proposal";
4962 if ($this->id > 0) {
4963 $sql .= " AND d.fk_product = ".((int) $this->id);
4964 } else {
4965 $sql .= " AND d.fk_product > 0";
4966 }
4967 if ($filteronproducttype >= 0) {
4968 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4969 }
4970 $sql .= " AND p.fk_soc = s.rowid";
4971 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
4972 if (!$user->hasRight('societe', 'client', 'voir')) {
4973 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4974 }
4975 if ($socid > 0) {
4976 $sql .= " AND p.fk_soc = ".((int) $socid);
4977 }
4978 $sql .= $morefilter;
4979 $sql .= " GROUP BY date_format(p.date_valid,'%Y%m')";
4980 $sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
4981
4982 return $this->_get_stats($sql, $mode, $year);
4983 }
4984
4985 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4996 public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4997 {
4998 // phpcs:enable
4999 global $user, $hookmanager;
5000
5001 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
5002 if ($mode == 'bynumber') {
5003 $sql .= ", count(DISTINCT c.rowid)";
5004 }
5005 $sql .= ", sum(d.total_ht) as total_ht";
5006 $sql .= " FROM ".$this->db->prefix()."commandedet as d, ".$this->db->prefix()."commande as c, ".$this->db->prefix()."societe as s";
5007 if ($filteronproducttype >= 0) {
5008 $sql .= ", ".$this->db->prefix()."product as p";
5009 }
5010 if (!$user->hasRight('societe', 'client', 'voir')) {
5011 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
5012 }
5013 $sql .= " WHERE c.rowid = d.fk_commande";
5014 if ($this->id > 0) {
5015 $sql .= " AND d.fk_product = ".((int) $this->id);
5016 } else {
5017 $sql .= " AND d.fk_product > 0";
5018 }
5019 if ($filteronproducttype >= 0) {
5020 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
5021 }
5022 $sql .= " AND c.fk_soc = s.rowid";
5023 $sql .= " AND c.entity IN (".getEntity('commande').")";
5024 if (!$user->hasRight('societe', 'client', 'voir')) {
5025 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
5026 }
5027 if ($socid > 0) {
5028 $sql .= " AND c.fk_soc = ".((int) $socid);
5029 }
5030 $sql .= $morefilter;
5031 $parameters = array('socid' => $user->socid);
5032 $hookmanager->executeHooks('productGetNbOrder', $parameters, $this); // Note that $action and $object may have been modified by hook
5033 $sql .= $hookmanager->resPrint;
5034 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
5035 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
5036
5037 return $this->_get_stats($sql, $mode, $year);
5038 }
5039
5040 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5051 public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
5052 {
5053 // phpcs:enable
5054 global $user;
5055
5056 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
5057 if ($mode == 'bynumber') {
5058 $sql .= ", count(DISTINCT c.rowid)";
5059 }
5060 $sql .= ", sum(d.total_ht) as total_ht";
5061 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as d, ".$this->db->prefix()."commande_fournisseur as c, ".$this->db->prefix()."societe as s";
5062 if ($filteronproducttype >= 0) {
5063 $sql .= ", ".$this->db->prefix()."product as p";
5064 }
5065 if (!$user->hasRight('societe', 'client', 'voir')) {
5066 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
5067 }
5068 $sql .= " WHERE c.rowid = d.fk_commande";
5069 if ($this->id > 0) {
5070 $sql .= " AND d.fk_product = ".((int) $this->id);
5071 } else {
5072 $sql .= " AND d.fk_product > 0";
5073 }
5074 if ($filteronproducttype >= 0) {
5075 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
5076 }
5077 $sql .= " AND c.fk_soc = s.rowid";
5078 $sql .= " AND c.entity IN (".getEntity('supplier_order').")";
5079 if (!$user->hasRight('societe', 'client', 'voir')) {
5080 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
5081 }
5082 if ($socid > 0) {
5083 $sql .= " AND c.fk_soc = ".((int) $socid);
5084 }
5085 $sql .= $morefilter;
5086 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
5087 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
5088
5089 return $this->_get_stats($sql, $mode, $year);
5090 }
5091
5092 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5103 public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
5104 {
5105 // phpcs:enable
5106 global $user;
5107
5108 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_contrat, '%Y%m')";
5109 if ($mode == 'bynumber') {
5110 $sql .= ", count(DISTINCT c.rowid)";
5111 }
5112 $sql .= ", sum(d.total_ht) as total_ht";
5113 $sql .= " FROM ".$this->db->prefix()."contratdet as d, ".$this->db->prefix()."contrat as c, ".$this->db->prefix()."societe as s";
5114 if ($filteronproducttype >= 0) {
5115 $sql .= ", ".$this->db->prefix()."product as p";
5116 }
5117 if (!$user->hasRight('societe', 'client', 'voir')) {
5118 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
5119 }
5120 $sql .= " WHERE c.entity IN (".getEntity('contract').")";
5121 $sql .= " AND c.rowid = d.fk_contrat";
5122
5123 if ($this->id > 0) {
5124 $sql .= " AND d.fk_product = ".((int) $this->id);
5125 } else {
5126 $sql .= " AND d.fk_product > 0";
5127 }
5128 if ($filteronproducttype >= 0) {
5129 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
5130 }
5131 $sql .= " AND c.fk_soc = s.rowid";
5132
5133 if (!$user->hasRight('societe', 'client', 'voir')) {
5134 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
5135 }
5136 if ($socid > 0) {
5137 $sql .= " AND c.fk_soc = ".((int) $socid);
5138 }
5139 $sql .= $morefilter;
5140 $sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')";
5141 $sql .= " ORDER BY date_format(c.date_contrat,'%Y%m') DESC";
5142
5143 return $this->_get_stats($sql, $mode, $year);
5144 }
5145
5146 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5157 public function get_nb_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
5158 {
5159 // phpcs:enable
5160 global $user;
5161
5162 $sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')";
5163 if ($mode == 'bynumber') {
5164 $sql .= ", count(DISTINCT d.rowid)";
5165 }
5166 $sql .= " FROM ".$this->db->prefix()."mrp_mo as d LEFT JOIN ".$this->db->prefix()."societe as s ON d.fk_soc = s.rowid";
5167 if ($filteronproducttype >= 0) {
5168 $sql .= ", ".$this->db->prefix()."product as p";
5169 }
5170 if (!$user->hasRight('societe', 'client', 'voir')) {
5171 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
5172 }
5173
5174 $sql .= " WHERE d.entity IN (".getEntity('mo').")";
5175 $sql .= " AND d.status > 0";
5176
5177 if ($this->id > 0) {
5178 $sql .= " AND d.fk_product = ".((int) $this->id);
5179 } else {
5180 $sql .= " AND d.fk_product > 0";
5181 }
5182 if ($filteronproducttype >= 0) {
5183 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
5184 }
5185
5186 if (!$user->hasRight('societe', 'client', 'voir')) {
5187 $sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
5188 }
5189 if ($socid > 0) {
5190 $sql .= " AND d.fk_soc = ".((int) $socid);
5191 }
5192 $sql .= $morefilter;
5193 $sql .= " GROUP BY date_format(d.date_valid,'%Y%m')";
5194 $sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC";
5195
5196 return $this->_get_stats($sql, $mode, $year);
5197 }
5198
5199 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5210 public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
5211 {
5212 // phpcs:enable
5213 global $user;
5214
5215 // Clean parameters
5216 if (!is_numeric($id_pere)) {
5217 $id_pere = 0;
5218 }
5219 if (!is_numeric($id_fils)) {
5220 $id_fils = 0;
5221 }
5222 if (!is_numeric($incdec)) {
5223 $incdec = 0;
5224 }
5225
5226 $result = $this->del_sousproduit($id_pere, $id_fils);
5227 if ($result < 0) {
5228 return $result;
5229 }
5230
5231 // Check not already father of id_pere (to avoid father -> child -> father links)
5232 $sql = "SELECT fk_product_pere from ".$this->db->prefix()."product_association";
5233 $sql .= " WHERE fk_product_pere = ".((int) $id_fils)." AND fk_product_fils = ".((int) $id_pere);
5234 if (!$this->db->query($sql)) {
5235 dol_print_error($this->db);
5236 return -1;
5237 } else {
5238 //Selection of the highest row
5239 $sql = "SELECT MAX(rang) as max_rank FROM ".$this->db->prefix()."product_association";
5240 $sql .= " WHERE fk_product_pere = ".((int) $id_pere);
5241 $resql = $this->db->query($sql);
5242 if ($resql) {
5243 $obj = $this->db->fetch_object($resql);
5244 $rank = $obj->max_rank + 1;
5245 //Addition of a product with the highest rank +1
5246 $sql = "INSERT INTO ".$this->db->prefix()."product_association(fk_product_pere,fk_product_fils,qty,incdec,rang)";
5247 $sql .= " VALUES (".((int) $id_pere).", ".((int) $id_fils).", ".price2num($qty, 'MS').", ".((int) $incdec).", ".((int) $rank).")";
5248 if (! $this->db->query($sql)) {
5249 dol_print_error($this->db);
5250 return -1;
5251 } else {
5252 if (!$notrigger) {
5253 // Call trigger
5254 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_ADD', $user);
5255 if ($result < 0) {
5256 $this->error = $this->db->lasterror();
5257 dol_syslog(get_class($this).'::addSubproduct error='.$this->error, LOG_ERR);
5258 return -1;
5259 }
5260 }
5261 // End call triggers
5262
5263 return 1;
5264 }
5265 } else {
5266 dol_print_error($this->db);
5267 return -1;
5268 }
5269 }
5270 }
5271
5272 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5283 public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
5284 {
5285 // phpcs:enable
5286 global $user;
5287
5288 // Clean parameters
5289 if (!is_numeric($id_pere)) {
5290 $id_pere = 0;
5291 }
5292 if (!is_numeric($id_fils)) {
5293 $id_fils = 0;
5294 }
5295 if (!is_numeric($incdec)) {
5296 $incdec = 1;
5297 }
5298 if (!is_numeric($qty)) {
5299 $qty = 1;
5300 }
5301
5302 $sql = 'UPDATE '.$this->db->prefix().'product_association SET ';
5303 $sql .= 'qty = '.price2num($qty, 'MS');
5304 $sql .= ',incdec = '.((int) $incdec);
5305 $sql .= ' WHERE fk_product_pere = '.((int) $id_pere).' AND fk_product_fils = '.((int) $id_fils);
5306
5307 if (!$this->db->query($sql)) {
5308 dol_print_error($this->db);
5309 return -1;
5310 } else {
5311 if (!$notrigger) {
5312 // Call trigger
5313 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_UPDATE', $user);
5314 if ($result < 0) {
5315 $this->error = $this->db->lasterror();
5316 dol_syslog(get_class($this).'::updateSubproduct error='.$this->error, LOG_ERR);
5317 return -1;
5318 }
5319 // End call triggers
5320 }
5321
5322 return 1;
5323 }
5324 }
5325
5326 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5335 public function del_sousproduit($fk_parent, $fk_child, $notrigger = 0)
5336 {
5337 // phpcs:enable
5338 global $user;
5339
5340 if (!is_numeric($fk_parent)) {
5341 $fk_parent = 0;
5342 }
5343 if (!is_numeric($fk_child)) {
5344 $fk_child = 0;
5345 }
5346
5347 $sql = "DELETE FROM ".$this->db->prefix()."product_association";
5348 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5349 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5350
5351 dol_syslog(get_class($this).'::del_sousproduit', LOG_DEBUG);
5352 if (!$this->db->query($sql)) {
5353 dol_print_error($this->db);
5354 return -1;
5355 }
5356
5357 // Updated ranks so that none are missing
5358 $sqlrank = "SELECT rowid, rang FROM ".$this->db->prefix()."product_association";
5359 $sqlrank .= " WHERE fk_product_pere = ".((int) $fk_parent);
5360 $sqlrank .= " ORDER BY rang";
5361 $resqlrank = $this->db->query($sqlrank);
5362 if ($resqlrank) {
5363 $cpt = 0;
5364 while ($objrank = $this->db->fetch_object($resqlrank)) {
5365 $cpt++;
5366 $sql = "UPDATE ".$this->db->prefix()."product_association";
5367 $sql .= " SET rang = ".((int) $cpt);
5368 $sql .= " WHERE rowid = ".((int) $objrank->rowid);
5369 if (! $this->db->query($sql)) {
5370 dol_print_error($this->db);
5371 return -1;
5372 }
5373 }
5374 }
5375
5376 if (!$notrigger) {
5377 // Call trigger
5378 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_DELETE', $user);
5379 if ($result < 0) {
5380 $this->error = $this->db->lasterror();
5381 dol_syslog(get_class($this).'::delSubproduct error='.$this->error, LOG_ERR);
5382 return -1;
5383 }
5384 // End call triggers
5385 }
5386
5387 return 1;
5388 }
5389
5390 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5398 public function is_sousproduit($fk_parent, $fk_child)
5399 {
5400 // phpcs:enable
5401 $sql = "SELECT fk_product_pere, qty, incdec";
5402 $sql .= " FROM ".$this->db->prefix()."product_association";
5403 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5404 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5405
5406 $result = $this->db->query($sql);
5407 if ($result) {
5408 $num = $this->db->num_rows($result);
5409
5410 if ($num > 0) {
5411 $obj = $this->db->fetch_object($result);
5412
5413 $this->is_sousproduit_qty = $obj->qty;
5414 $this->is_sousproduit_incdec = $obj->incdec;
5415
5416 return 1;
5417 } else {
5418 return 0;
5419 }
5420 } else {
5421 dol_print_error($this->db);
5422 return -1;
5423 }
5424 }
5425
5426
5427 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5438 public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
5439 {
5440 // phpcs:enable
5441 global $conf;
5442
5443 $now = dol_now();
5444
5445 dol_syslog(get_class($this)."::add_fournisseur id_fourn = ".$id_fourn." ref_fourn=".$ref_fourn." quantity=".$quantity, LOG_DEBUG);
5446
5447 // Clean parameters
5448 $quantity = price2num($quantity, 'MS');
5449
5450 if ($ref_fourn) {
5451 // Check if ref is not already used
5452 $sql = "SELECT rowid, fk_product";
5453 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5454 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5455 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5456 $sql .= " AND fk_product <> ".((int) $this->id);
5457 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5458
5459 $resql = $this->db->query($sql);
5460 if ($resql) {
5461 $obj = $this->db->fetch_object($resql);
5462 if ($obj) {
5463 // If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
5464 $this->product_id_already_linked = $obj->fk_product;
5465 return -3;
5466 }
5467 $this->db->free($resql);
5468 }
5469 }
5470
5471 $sql = "SELECT rowid";
5472 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5473 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5474 if ($ref_fourn) {
5475 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5476 } else {
5477 $sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
5478 }
5479 $sql .= " AND quantity = ".((float) $quantity);
5480 $sql .= " AND fk_product = ".((int) $this->id);
5481 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5482
5483 $resql = $this->db->query($sql);
5484 if ($resql) {
5485 $obj = $this->db->fetch_object($resql);
5486
5487 // The reference supplier does not exist, we create it for this product.
5488 if (empty($obj)) {
5489 $sql = "INSERT INTO ".$this->db->prefix()."product_fournisseur_price(";
5490 $sql .= "datec";
5491 $sql .= ", entity";
5492 $sql .= ", fk_product";
5493 $sql .= ", fk_soc";
5494 $sql .= ", ref_fourn";
5495 $sql .= ", quantity";
5496 $sql .= ", fk_user";
5497 $sql .= ", tva_tx";
5498 $sql .= ") VALUES (";
5499 $sql .= "'".$this->db->idate($now)."'";
5500 $sql .= ", ".((int) $conf->entity);
5501 $sql .= ", ".((int) $this->id);
5502 $sql .= ", ".((int) $id_fourn);
5503 $sql .= ", '".$this->db->escape($ref_fourn)."'";
5504 $sql .= ", ".((float) $quantity);
5505 $sql .= ", ".((int) $user->id);
5506 $sql .= ", 0";
5507 $sql .= ")";
5508
5509 if ($this->db->query($sql)) {
5510 $this->product_fourn_price_id = $this->db->last_insert_id($this->db->prefix()."product_fournisseur_price");
5511 return 1;
5512 } else {
5513 $this->error = $this->db->lasterror();
5514 return -1;
5515 }
5516 } else {
5517 // If the supplier price already exists for this product and quantity
5518 $this->product_fourn_price_id = $obj->rowid;
5519 return 0;
5520 }
5521 } else {
5522 $this->error = $this->db->lasterror();
5523 return -2;
5524 }
5525 }
5526
5527
5528 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5534 public function list_suppliers()
5535 {
5536 // phpcs:enable
5537 global $conf;
5538
5539 $list = array();
5540
5541 $sql = "SELECT DISTINCT p.fk_soc";
5542 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as p";
5543 $sql .= " WHERE p.fk_product = ".((int) $this->id);
5544 $sql .= " AND p.entity = ".((int) $conf->entity);
5545
5546 $result = $this->db->query($sql);
5547 if ($result) {
5548 $num = $this->db->num_rows($result);
5549 $i = 0;
5550 while ($i < $num) {
5551 $obj = $this->db->fetch_object($result);
5552 $list[$i] = $obj->fk_soc;
5553 $i++;
5554 }
5555 }
5556
5557 return $list;
5558 }
5559
5560 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5568 public function clone_price($fromId, $toId)
5569 {
5570 // phpcs:enable
5571 global $user;
5572
5573 $now = dol_now();
5574
5575 $this->db->begin();
5576
5577 // Default selling price and price levels
5578 $sql = "INSERT INTO ".$this->db->prefix()."product_price (";
5579 $sql .= " entity";
5580 $sql .= ", fk_product";
5581 $sql .= ", date_price";
5582 $sql .= ", price_level";
5583 $sql .= ", price";
5584 $sql .= ", price_ttc";
5585 $sql .= ", price_min";
5586 $sql .= ", price_min_ttc";
5587 $sql .= ", price_base_type";
5588 $sql .= ", price_label";
5589 $sql .= ", default_vat_code";
5590 $sql .= ", tva_tx";
5591 $sql .= ", recuperableonly";
5592 $sql .= ", localtax1_tx";
5593 $sql .= ", localtax1_type";
5594 $sql .= ", localtax2_tx";
5595 $sql .= ", localtax2_type";
5596 $sql .= ", fk_user_author";
5597 $sql .= ", tosell";
5598 $sql .= ", price_by_qty";
5599 $sql .= ", fk_price_expression";
5600 $sql .= ", fk_multicurrency";
5601 $sql .= ", multicurrency_code";
5602 $sql .= ", multicurrency_tx";
5603 $sql .= ", multicurrency_price";
5604 $sql .= ", multicurrency_price_ttc";
5605 $sql .= ")";
5606 $sql .= " SELECT";
5607 $sql .= " entity";
5608 $sql .= ", ".((int) $toId);
5609 $sql .= ", '".$this->db->idate($now)."'";
5610 $sql .= ", price_level";
5611 $sql .= ", price";
5612 $sql .= ", price_ttc";
5613 $sql .= ", price_min";
5614 $sql .= ", price_min_ttc";
5615 $sql .= ", price_base_type";
5616 $sql .= ", price_label";
5617 $sql .= ", default_vat_code";
5618 $sql .= ", tva_tx";
5619 $sql .= ", recuperableonly";
5620 $sql .= ", localtax1_tx";
5621 $sql .= ", localtax1_type";
5622 $sql .= ", localtax2_tx";
5623 $sql .= ", localtax2_type";
5624 $sql .= ", ".((int) $user->id);
5625 $sql .= ", tosell";
5626 $sql .= ", price_by_qty";
5627 $sql .= ", fk_price_expression";
5628 $sql .= ", fk_multicurrency";
5629 $sql .= ", multicurrency_code";
5630 $sql .= ", multicurrency_tx";
5631 $sql .= ", multicurrency_price";
5632 $sql .= ", multicurrency_price_ttc";
5633 $sql .= " FROM ".$this->db->prefix()."product_price as ps";
5634 $sql .= " WHERE ps.fk_product = ".((int) $fromId);
5635 $sql .= " AND ps.date_price IN (SELECT MAX(pd.date_price) FROM ".$this->db->prefix()."product_price as pd WHERE pd.fk_product = ".((int) $fromId)." AND pd.price_level = ps.price_level)";
5636 $sql .= " ORDER BY ps.date_price DESC";
5637
5638 dol_syslog(__METHOD__, LOG_DEBUG);
5639 $resql = $this->db->query($sql);
5640 if (!$resql) {
5641 $this->db->rollback();
5642 return -1;
5643 }
5644 $this->db->free($resql);
5645
5646 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
5647 $this->cloneMultipriceExtra($fromId, $toId); // Add of level price extrafields
5648 }
5649 if (getDolGlobalString('PRODUIT_CUSTOMER_PRICES')) {
5650 $this->cloneCustomerPriceAndExtra($fromId, $toId); // Price per customer
5651 }
5652 if (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) {
5653 // Add of level price extrafields and price per customer
5654 $this->cloneMultipriceExtra($fromId, $toId); // Add of level price extrafields
5655 $this->cloneCustomerPriceAndExtra($fromId, $toId); // Price per customer
5656 }
5657
5658 $this->db->commit();
5659 return 1;
5660 }
5661
5662 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5670 public function clone_associations($fromId, $toId)
5671 {
5672 // phpcs:enable
5673 $this->db->begin();
5674
5675 $sql = 'INSERT INTO '.$this->db->prefix().'product_association (fk_product_pere, fk_product_fils, qty, incdec)';
5676 $sql .= " SELECT ".((int) $toId).", fk_product_fils, qty, incdec FROM ".$this->db->prefix()."product_association";
5677 $sql .= " WHERE fk_product_pere = ".((int) $fromId);
5678
5679 dol_syslog(get_class($this).'::clone_association', LOG_DEBUG);
5680 if (!$this->db->query($sql)) {
5681 $this->db->rollback();
5682 return -1;
5683 }
5684
5685 $this->db->commit();
5686 return 1;
5687 }
5688
5689 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5697 public function clone_fournisseurs($fromId, $toId)
5698 {
5699 // phpcs:enable
5700 $this->db->begin();
5701
5702 $now = dol_now();
5703
5704 // les fournisseurs
5705 /*$sql = "INSERT ".$this->db->prefix()."product_fournisseur ("
5706 . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
5707 . " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, ref_fourn, fk_user_author"
5708 . " FROM ".$this->db->prefix()."product_fournisseur"
5709 . " WHERE fk_product = ".((int) $fromId);
5710
5711 if ( ! $this->db->query($sql ) )
5712 {
5713 $this->db->rollback();
5714 return -1;
5715 }*/
5716
5717 // les prix de fournisseurs.
5718 $sql = "INSERT ".$this->db->prefix()."product_fournisseur_price (";
5719 $sql .= " datec, fk_product, fk_soc, price, quantity, fk_user, tva_tx)";
5720 $sql .= " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, price, quantity, fk_user, tva_tx";
5721 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5722 $sql .= " WHERE fk_product = ".((int) $fromId);
5723
5724 dol_syslog(get_class($this).'::clone_fournisseurs', LOG_DEBUG);
5725 $resql = $this->db->query($sql);
5726 if (!$resql) {
5727 $this->db->rollback();
5728 return -1;
5729 } else {
5730 $this->db->commit();
5731 return 1;
5732 }
5733 }
5734
5735 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5748 public function fetch_prod_arbo($prod, $compl_path = '', $multiply = 1, $level = 1, $id_parent = 0, $ignore_stock_load = 0)
5749 {
5750 // phpcs:enable
5751 $tmpproduct = null;
5752
5753 if ($multiply < 1) {
5754 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);
5755 $multiply = 1;
5756 }
5757
5758 foreach ($prod as $id_product => $desc_pere) { // $id_product is 0 (first call starting with root top) or an id of a sub_product
5759 if (is_array($desc_pere)) { // If desc_pere is an array, this means it's a child
5760 $id = (!empty($desc_pere[0]) ? $desc_pere[0] : '');
5761 $nb = (!empty($desc_pere[1]) ? $desc_pere[1] : '');
5762 $type = (!empty($desc_pere[2]) ? $desc_pere[2] : '');
5763 $label = (!empty($desc_pere[3]) ? $desc_pere[3] : '');
5764 $incdec = (!empty($desc_pere[4]) ? $desc_pere[4] : 0);
5765
5766 //print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n";
5767 if (is_null($tmpproduct)) {
5768 $tmpproduct = new Product($this->db); // So we initialize tmpproduct only once for all loop.
5769 }
5770 $tmpproduct->fetch($id); // Load product to get ->ref
5771
5772 if (empty($ignore_stock_load) && ($tmpproduct->isProduct() || getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) {
5773 $tmpproduct->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
5774 }
5775
5776 $this->res[] = array(
5777 'id' => $id, // Id product
5778 'id_parent' => $id_parent,
5779 'ref' => $tmpproduct->ref, // Ref product
5780 'nb' => $nb, // Nb of units that compose parent product
5781 'nb_total' => $nb * $multiply, // Nb of units for all nb of product
5782 'stock' => $tmpproduct->stock_reel, // Stock
5783 'stock_alert' => $tmpproduct->seuil_stock_alerte, // Stock alert
5784 'label' => $label,
5785 'fullpath' => $compl_path.$label, // Label
5786 'type' => $type, // Nb of units that compose parent product
5787 'desiredstock' => $tmpproduct->desiredstock,
5788 'level' => $level,
5789 'incdec' => $incdec,
5790 'entity' => $tmpproduct->entity
5791 );
5792
5793 // Recursive call if there child has children of its own
5794 if (isset($desc_pere['childs']) && is_array($desc_pere['childs'])) {
5795 if (!is_int($desc_pere[1] * $multiply)) {
5796 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);
5797 }
5798
5799 //print 'YYY We go down for '.$desc_pere[3]." -> \n";
5800 $this->fetch_prod_arbo($desc_pere['childs'], $compl_path.$desc_pere[3]." -> ", (int) ceil($desc_pere[1] * $multiply), $level + 1, $id, $ignore_stock_load);
5801 }
5802 }
5803 }
5804 }
5805
5806 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5815 public function get_arbo_each_prod($multiply = 1, $ignore_stock_load = 0)
5816 {
5817 // phpcs:enable
5818 $this->res = array();
5819 if (isset($this->sousprods) && is_array($this->sousprods)) {
5820 foreach ($this->sousprods as $prod_name => $desc_product) {
5821 if (is_array($desc_product)) {
5822 $this->fetch_prod_arbo($desc_product, "", $multiply, 1, $this->id, $ignore_stock_load); // This set $this->res
5823 }
5824 }
5825 }
5826 return $this->res;
5827 }
5828
5836 public function hasFatherOrChild($mode = 0)
5837 {
5838 $nb = 0;
5839
5840 $sql = "SELECT COUNT(pa.rowid) as nb";
5841 $sql .= " FROM ".$this->db->prefix()."product_association as pa";
5842 if ($mode == 0) {
5843 $sql .= " WHERE pa.fk_product_fils = ".((int) $this->id)." OR pa.fk_product_pere = ".((int) $this->id);
5844 } elseif ($mode == -1) {
5845 $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)
5846 } elseif ($mode == 1) {
5847 $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)
5848 }
5849
5850 $resql = $this->db->query($sql);
5851 if ($resql) {
5852 $obj = $this->db->fetch_object($resql);
5853 if ($obj) {
5854 $nb = (int) $obj->nb;
5855 }
5856 } else {
5857 return -1;
5858 }
5859
5860 return $nb;
5861 }
5862
5868 public function hasVariants()
5869 {
5870 $nb = 0;
5871 if (!isModEnabled('variants')) {
5872 return $nb;
5873 }
5874 $sql = "SELECT count(rowid) as nb FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_parent = ".((int) $this->id);
5875 $sql .= " AND entity IN (".getEntity('product').")";
5876
5877 $resql = $this->db->query($sql);
5878 if ($resql) {
5879 $obj = $this->db->fetch_object($resql);
5880 if ($obj) {
5881 $nb = (int) $obj->nb;
5882 }
5883 }
5884
5885 return $nb;
5886 }
5887
5888
5894 public function isVariant()
5895 {
5896 if (isModEnabled('variants')) {
5897 $sql = "SELECT rowid FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_child = ".((int) $this->id)." AND entity IN (".getEntity('product').")";
5898
5899 $query = $this->db->query($sql);
5900
5901 if ($query) {
5902 if (!$this->db->num_rows($query)) {
5903 return false;
5904 }
5905 return true;
5906 } else {
5907 dol_print_error($this->db);
5908 return -1;
5909 }
5910 } else {
5911 return false;
5912 }
5913 }
5914
5921 public function getFather()
5922 {
5923 $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";
5924 $sql .= ", p.tosell as status, p.tobuy as status_buy";
5925 $sql .= " FROM ".$this->db->prefix()."product_association as pa,";
5926 $sql .= " ".$this->db->prefix()."product as p";
5927 $sql .= " WHERE p.rowid = pa.fk_product_pere";
5928 $sql .= " AND pa.fk_product_fils = ".((int) $this->id);
5929
5930 $res = $this->db->query($sql);
5931 if ($res) {
5932 $prods = array();
5933 while ($record = $this->db->fetch_array($res)) {
5934 // $record['id'] = $record['rowid'] = id of father
5935 $prods[$record['id']] = array();
5936 $prods[$record['id']]['id'] = $record['rowid'];
5937 $prods[$record['id']]['ref'] = $record['ref'];
5938 $prods[$record['id']]['label'] = $record['label'];
5939 $prods[$record['id']]['qty'] = $record['qty'];
5940 $prods[$record['id']]['incdec'] = $record['incdec'];
5941 $prods[$record['id']]['fk_product_type'] = $record['fk_product_type'];
5942 $prods[$record['id']]['entity'] = $record['entity'];
5943 $prods[$record['id']]['status'] = $record['status'];
5944 $prods[$record['id']]['status_buy'] = $record['status_buy'];
5945 }
5946 return $prods;
5947 } else {
5948 dol_print_error($this->db);
5949 return -1;
5950 }
5951 }
5952
5953
5963 public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array())
5964 {
5965 if (empty($id)) {
5966 return array();
5967 }
5968
5969 $sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,";
5970 $sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec,";
5971 $sql .= " pa.rowid as fk_association, pa.rang";
5972 $sql .= " FROM ".$this->db->prefix()."product as p,";
5973 $sql .= " ".$this->db->prefix()."product_association as pa";
5974 $sql .= " WHERE p.rowid = pa.fk_product_fils";
5975 $sql .= " AND pa.fk_product_pere = ".((int) $id);
5976 $sql .= " AND pa.fk_product_fils <> ".((int) $id); // This should not happens, it is to avoid infinite loop if it happens
5977 $sql .= " ORDER BY pa.rang";
5978
5979 dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level. ' parents='.(is_array($parents) ? implode(',', $parents) : $parents), LOG_DEBUG);
5980
5981 // Protection against infinite loop
5982 if ($level > 30) {
5983 return array();
5984 }
5985
5986 $res = $this->db->query($sql);
5987 if ($res) {
5988 $prods = array();
5989 if ($this->db->num_rows($res) > 0) {
5990 $parents[] = $id;
5991 }
5992
5993 while ($rec = $this->db->fetch_array($res)) {
5994 if (in_array($rec['id'], $parents)) {
5995 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);
5996 continue; // We discard this child if it is already found at a higher level in tree in the same branch.
5997 }
5998
5999 $prods[$rec['rowid']] = array(
6000 0 => $rec['rowid'],
6001 1 => $rec['qty'],
6002 2 => $rec['fk_product_type'],
6003 3 => $this->db->escape($rec['label']),
6004 4 => $rec['incdec'],
6005 5 => $rec['ref'],
6006 6 => $rec['fk_association'],
6007 7 => $rec['rang']
6008 );
6009 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
6010 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
6011 if (empty($firstlevelonly)) {
6012 $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, $parents);
6013 foreach ($listofchilds as $keyChild => $valueChild) {
6014 $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
6015 }
6016 }
6017 }
6018
6019 return $prods;
6020 } else {
6021 dol_print_error($this->db);
6022 return -1;
6023 }
6024 }
6025
6026 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6033 public function get_sousproduits_arbo()
6034 {
6035 // phpcs:enable
6036 $parent = array();
6037
6038 foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) { // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
6039 $parent[$this->label][$keyChild] = $valueChild;
6040 }
6041 foreach ($parent as $key => $value) { // key=label, value is array of children
6042 $this->sousprods[$key] = $value; // @phan-suppress-current-line PhanTypeMismatchProperty
6043 }
6044 }
6045
6053 public function getTooltipContentArray($params)
6054 {
6055 global $conf, $langs, $user;
6056
6057 $langs->loadLangs(array('products', 'other'));
6058
6059 $datas = array();
6060 $nofetch = !empty($params['nofetch']);
6061
6062 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
6063 return ['optimize' => $langs->trans("ShowProduct")];
6064 }
6065
6066 // Does user has permission to read product/service
6067 $permissiontoreadproduct = 0;
6068 if ($this->type == self::TYPE_PRODUCT && $user->hasRight('product', 'read')) {
6069 $permissiontoreadproduct = 1;
6070 }
6071 if ($this->type == self::TYPE_SERVICE && $user->hasRight('service', 'read')) {
6072 $permissiontoreadproduct = 1;
6073 }
6074
6075 if (!empty($this->entity) && $permissiontoreadproduct) {
6076 $tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80, 0, 0, 0, 0, '1');
6077 if ($this->nbphoto > 0) {
6078 $datas['photo'] = '<div class="photointooltip floatright">'."\n" . $tmpphoto . '</div>';
6079 }
6080 }
6081
6082 if ($this->isProduct()) {
6083 $datas['picto'] = img_picto('', 'product').' <u class="paddingrightonly">'.$langs->trans("Product").'</u>';
6084 } elseif ($this->isService()) {
6085 $datas['picto'] = img_picto('', 'service').' <u class="paddingrightonly">'.$langs->trans("Service").'</u>';
6086 }
6087 if (isset($this->status) && isset($this->status_buy)) {
6088 $datas['status'] = ' '.$this->getLibStatut(5, 0) . ' '.$this->getLibStatut(5, 1);
6089 }
6090
6091 if (!empty($this->ref)) {
6092 $datas['ref'] = '<br><b>'.$langs->trans('ProductRef').':</b> '.$this->ref;
6093 }
6094 if (!empty($this->label)) {
6095 $datas['label'] = '<br><b>'.$langs->trans('ProductLabel').':</b> '.$this->label.'<br>';
6096 }
6097
6098 if ($permissiontoreadproduct) {
6099 if (!empty($this->description) && getDolGlobalString('PRODUCT_SHOW_DESCRIPTION_IN_TOOLTIP')) {
6100 $datas['description'] = '<br><b>'.$langs->trans('ProductDescription').':</b> '.dolGetFirstLineOfText($this->description, 5);
6101 }
6102
6103 $datas['stockmanaged'] = "<br><b>".$langs->trans("StockableProduct").'</b>: '.yn($this->isStockManaged());
6104
6105 if ($this->isStockManaged()) {
6106 if (isModEnabled('productbatch')) {
6107 $langs->load("productbatch");
6108 $datas['batchstatus'] = "<br><b>".$langs->trans("ManageLotSerial").'</b>: '.$this->getLibStatut(0, 2);
6109 if ($this->status_batch) {
6110 $datas['batchdlc'] = "<br><b>".$langs->trans("BatchSellOrEatByMandatoryList", $langs->transnoentitiesnoconv("SellByDate"), $langs->transnoentitiesnoconv("EatByDate")).'</b>: '.$this->getSellOrEatByMandatoryLabel();
6111 }
6112 }
6113 }
6114 if (isModEnabled('barcode')) {
6115 $datas['barcode'] = '<br><b>'.$langs->trans('BarCode').':</b> '.$this->barcode;
6116 }
6117
6118 if ($this->isProduct()) {
6119 if ($this->weight) {
6120 $datas['weight'] = "<br><b>".$langs->trans("Weight").'</b>: '.$this->weight.' '.measuringUnitString(0, "weight", $this->weight_units);
6121 }
6122 $labelsize = "";
6123 if ($this->length) {
6124 $labelsize .= "<b>".$langs->trans("Length").'</b>: '.$this->length.' '.measuringUnitString(0, 'size', $this->length_units);
6125 }
6126 if ($this->width) {
6127 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Width").'</b>: '.$this->width.' '.measuringUnitString(0, 'size', $this->width_units);
6128 }
6129 if ($this->height) {
6130 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Height").'</b>: '.$this->height.' '.measuringUnitString(0, 'size', $this->height_units);
6131 }
6132 if ($labelsize) {
6133 $datas['size'] = "<br>".$labelsize;
6134 }
6135
6136 $labelsurfacevolume = "";
6137 if ($this->surface) {
6138 $labelsurfacevolume .= "<b>".$langs->trans("Surface").'</b>: '.$this->surface.' '.measuringUnitString(0, 'surface', $this->surface_units);
6139 }
6140 if ($this->volume) {
6141 $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Volume").'</b>: '.$this->volume.' '.measuringUnitString(0, 'volume', $this->volume_units);
6142 }
6143 if ($labelsurfacevolume) {
6144 $datas['surface'] = "<br>" . $labelsurfacevolume;
6145 }
6146 }
6147 if ($this->isService() && !empty($this->duration_value)) {
6148 // Duration
6149 $datas['duration'] = '<br><b>'.$langs->trans("Duration").':</b> '.$this->duration_value;
6150 if ($this->duration_value > 1) {
6151 $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"));
6152 } elseif ($this->duration_value > 0) {
6153 $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"));
6154 }
6155 $datas['duration'] .= (!empty($this->duration_unit) && isset($dur[$this->duration_unit]) ? "&nbsp;".$langs->trans($dur[$this->duration_unit]) : '');
6156 }
6157 if (empty($user->socid)) {
6158 if ($this->isStockManaged() && isset($this->pmp) && $this->pmp) {
6159 $datas['pmp'] = "<br><b>".$langs->trans("PMPValue").'</b>: '.price($this->pmp, 0, '', 1, -1, -1, $conf->currency);
6160 }
6161
6162 if (isModEnabled('accounting')) {
6163 if ($this->status && isset($this->accountancy_code_sell)) {
6164 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
6165 $selllabel = '<br>';
6166 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellCode').':</b> '.length_accountg($this->accountancy_code_sell);
6167 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellIntraCode').':</b> '.length_accountg($this->accountancy_code_sell_intra);
6168 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellExportCode').':</b> '.length_accountg($this->accountancy_code_sell_export);
6169 $datas['accountancysell'] = $selllabel;
6170 }
6171 if ($this->status_buy && isset($this->accountancy_code_buy)) {
6172 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
6173 $buylabel = '';
6174 if (empty($this->status)) {
6175 $buylabel .= '<br>';
6176 }
6177 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyCode').':</b> '.length_accountg($this->accountancy_code_buy);
6178 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyIntraCode').':</b> '.length_accountg($this->accountancy_code_buy_intra);
6179 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyExportCode').':</b> '.length_accountg($this->accountancy_code_buy_export);
6180 $datas['accountancybuy'] = $buylabel;
6181 }
6182 }
6183 }
6184 // show categories for this record only in ajax to not overload lists
6185 if (isModEnabled('category') && !$nofetch) {
6186 require_once DOL_DOCUMENT_ROOT . '/categories/class/categorie.class.php';
6187 $form = new Form($this->db);
6188 $datas['categories'] = '<br>' . $form->showCategories($this->id, Categorie::TYPE_PRODUCT, 1);
6189 }
6190 }
6191
6192 return $datas;
6193 }
6194
6209 public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0, $morecss = '', $add_label = 0, $sep = ' - ', $addlinktonotes = 0)
6210 {
6211 global $langs, $hookmanager, $user;
6212
6213 include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
6214
6215 $result = '';
6216
6217 $newref = (string) $this->ref;
6218 if ($maxlength) {
6219 $newref = dol_trunc($newref, $maxlength, 'middle');
6220 }
6221 $params = [
6222 'id' => $this->id,
6223 'objecttype' => ($this->type == 1 ? 'service' : 'product'),
6224 'option' => $option,
6225 'nofetch' => 1,
6226 ];
6227 $classfortooltip = 'classfortooltip';
6228 $dataparams = '';
6229 if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
6230 $classfortooltip = 'classforajaxtooltip';
6231 $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
6232 $label = '';
6233 } else {
6234 $label = implode($this->getTooltipContentArray($params));
6235 }
6236
6237 $linkclose = '';
6238 if (empty($notooltip)) {
6239 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
6240 $label = $langs->trans("ShowProduct");
6241 $linkclose .= ' alt="'.dol_escape_htmltag($label, 1, 1).'"';
6242 }
6243 $linkclose .= ($label ? ' title="'.dol_escape_htmltag($label, 1, 1).'"' : ' title="tocomplete"');
6244 $linkclose .= $dataparams.' class="nowraponall '.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
6245 } else {
6246 $linkclose = ' class="nowraponall'.($morecss ? ' '.$morecss : '').'"';
6247 }
6248
6249 if ($option == 'supplier' || $option == 'category') {
6250 $baseurl = DOL_URL_ROOT.'/product/price_suppliers.php';
6251 } elseif ($option == 'stock') {
6252 $baseurl = DOL_URL_ROOT.'/product/stock/product.php';
6253 } elseif ($option == 'composition') {
6254 $baseurl = DOL_URL_ROOT.'/product/composition/card.php';
6255 } else {
6256 $baseurl = DOL_URL_ROOT.'/product/card.php';
6257 }
6258 $query = ['id' => $this->id];
6259
6260 if ($option !== 'nolink') {
6261 // Add param to save lastsearch_values or not
6262 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
6263 if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
6264 $add_save_lastsearch_values = 1;
6265 }
6266 if ($add_save_lastsearch_values) {
6267 $query = array_merge($query, ['save_lastsearch_values' => 1]);
6268 }
6269 }
6270 $url = dolBuildUrl($baseurl, $query);
6271
6272 $linkstart = '<a href="'.$url.'"';
6273 $linkstart .= $linkclose.'>';
6274 $linkend = '</a>';
6275
6276 $result .= $linkstart;
6277 if ($withpicto) {
6278 if ($this->isProduct()) {
6279 $result .= (img_object(($notooltip ? '' : $label), 'product', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
6280 }
6281 if ($this->isService()) {
6282 $result .= (img_object(($notooltip ? '' : $label), 'service', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
6283 }
6284 }
6285 $result .= '<span class="aaa">'.dol_escape_htmltag($newref).'</span>';
6286 $result .= $linkend;
6287 if ($withpicto != 2) {
6288 $result .= (($add_label && $this->label) ? $sep.dol_trunc($this->label, ($add_label > 1 ? $add_label : 0)) : '');
6289 }
6290
6291 if ($addlinktonotes) {
6292 $txttoshow = ($user->socid > 0 ? $this->note_public : $this->note_private);
6293 if ($txttoshow) {
6294 $notetoshow = $langs->trans("ViewPrivateNote").':<br>'.dol_string_nohtmltag($txttoshow, 1);
6295 $result .= ' <span class="note inline-block">';
6296 $result .= '<a href="'.DOL_URL_ROOT.'/product/note.php?id='.$this->id.'" class="classfortooltip" title="'.dol_escape_htmltag($notetoshow).'">';
6297 $result .= img_picto('', 'note');
6298 $result .= '</a>';
6299 $result .= '</span>';
6300 }
6301 }
6302
6303 global $action;
6304 $hookmanager->initHooks(array('productdao'));
6305 $parameters = array('id' => $this->id, 'getnomurl' => &$result, 'label' => &$label);
6306 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
6307 if ($reshook > 0) {
6308 $result = $hookmanager->resPrint;
6309 } else {
6310 $result .= $hookmanager->resPrint;
6311 }
6312
6313 return $result;
6314 }
6315
6316
6327 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
6328 {
6329 global $langs;
6330
6331 $langs->load("products");
6332 $outputlangs->load("products");
6333
6334 // Set the model to the name of the model to use
6335 if (!dol_strlen($modele)) {
6336 $modele = ''; // No doc template/generation by default
6337
6338 if (!empty($this->model_pdf)) {
6339 $modele = $this->model_pdf;
6340 } elseif (getDolGlobalString('PRODUCT_ADDON_PDF')) {
6341 $modele = getDolGlobalString('PRODUCT_ADDON_PDF');
6342 }
6343 }
6344
6345 if (empty($modele)) {
6346 return 0;
6347 } else {
6348 $modelpath = "core/modules/product/doc/";
6349 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
6350 }
6351 }
6352
6360 public function getLibStatut($mode = 0, $type = 0)
6361 {
6362 switch ($type) {
6363 case 0:
6364 return $this->LibStatut($this->status, $mode, $type);
6365 case 1:
6366 return $this->LibStatut($this->status_buy, $mode, $type);
6367 case 2:
6368 return $this->LibStatut($this->status_batch, $mode, $type);
6369 default:
6370 //Simulate previous behavior but should return an error string
6371 return $this->LibStatut($this->status_buy, $mode, $type);
6372 }
6373 }
6374
6375 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6384 public function LibStatut($status, $mode = 0, $type = 0)
6385 {
6386 // phpcs:enable
6387 global $langs;
6388
6389 $labelStatus = $labelStatusShort = '';
6390
6391 $langs->load('products');
6392 if (isModEnabled('productbatch')) {
6393 $langs->load("productbatch");
6394 }
6395
6396 if ($type == 2) {
6397 switch ($mode) {
6398 case 0:
6399 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatch') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial')));
6400 return dolGetStatus($label);
6401 case 1:
6402 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort')));
6403 return dolGetStatus($label);
6404 case 2:
6405 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 1, 2);
6406 case 3:
6407 return dolGetStatus($langs->transnoentitiesnoconv('ProductStatusNotOnBatch'), '', '', empty($status) ? 'status5' : 'status4', 3, 'dot');
6408 case 4:
6409 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 0, 2);
6410 case 5:
6411 return $this->LibStatut($status, 1, 2).' '.$this->LibStatut($status, 3, 2);
6412 default:
6413 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'));
6414 }
6415 }
6416
6417 $statuttrans = empty($status) ? 'status5' : 'status4';
6418
6419 if ($status == 0) {
6420 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6421 if ($type == 0) {
6422 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnSellShort');
6423 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnSell');
6424 } elseif ($type == 1) {
6425 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBuyShort');
6426 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBuy');
6427 } elseif ($type == 2) {
6428 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBatch');
6429 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort');
6430 }
6431 } elseif ($status == 1) {
6432 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6433 if ($type == 0) {
6434 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSellShort');
6435 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSell');
6436 } elseif ($type == 1) {
6437 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBuyShort');
6438 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBuy');
6439 } elseif ($type == 2) {
6440 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBatch');
6441 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBatchShort');
6442 }
6443 } elseif ($type == 2 && $status == 2) {
6444 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSerial');
6445 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSerialShort');
6446 }
6447
6448 if ($mode > 6) {
6449 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'), '', '', 'status0', 0);
6450 } else {
6451 return dolGetStatus($labelStatus, $labelStatusShort, '', $statuttrans, $mode);
6452 }
6453 }
6454
6455
6461 public function getLibFinished()
6462 {
6463 global $langs;
6464
6465 $langs->load('products');
6466 $label = '';
6467
6468 if (isset($this->finished) && $this->finished >= 0) {
6469 $sql = "SELECT label, code FROM ".$this->db->prefix()."c_product_nature where code = ".((int) $this->finished)." AND active=1";
6470 $resql = $this->db->query($sql);
6471 if (!$resql) {
6472 $this->error = $this->db->error().' sql='.$sql;
6473 dol_syslog(__METHOD__.' Error '.$this->error, LOG_ERR);
6474 return -1;
6475 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
6476 $label = $langs->trans($res['label']);
6477 }
6478 $this->db->free($resql);
6479 }
6480
6481 return $label;
6482 }
6483
6484
6485 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6502 public function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null)
6503 {
6504 // phpcs:enable
6505 if ($id_entrepot) {
6506 $this->db->begin();
6507
6508 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
6509
6510 // Ensure $nbpiece is a number and positive
6511 $nbpiece = (float) $nbpiece;
6512 if ($nbpiece < 0) {
6513 if (!$movement) {
6514 $movement = 1;
6515 }
6516 $nbpiece = abs($nbpiece);
6517 }
6518 $op = array();
6519 $op[0] = $nbpiece;
6520 $op[1] = -$nbpiece;
6521
6522 $movementstock = new MouvementStock($this->db);
6523 $movementstock->setOrigin($origin_element, (int) $origin_id); // Set ->origin_type and ->origin_id
6524 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', '', '', '', false, 0, $disablestockchangeforsubproduct);
6525
6526 if ($result >= 0) {
6527 if ($extrafields) {
6528 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
6529 $movementstock->array_options = $array_options;
6530 $movementstock->insertExtraFields();
6531 }
6532 $this->db->commit();
6533 return 1;
6534 } else {
6535 $this->setErrorsFromObject($movementstock);
6536
6537 $this->db->rollback();
6538 return -1;
6539 }
6540 }
6541
6542 return -1;
6543 }
6544
6545 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6566 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)
6567 {
6568 // phpcs:enable
6569 if ($id_entrepot) {
6570 $this->db->begin();
6571
6572 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
6573
6574 // Ensure $nbpiece is a number and positive
6575 $nbpiece = (float) $nbpiece;
6576 if ($nbpiece < 0) {
6577 if (!$movement) {
6578 $movement = 1;
6579 }
6580 $nbpiece = abs($nbpiece);
6581 }
6582
6583 $op = array();
6584 $op[0] = $nbpiece;
6585 $op[1] = -$nbpiece;
6586
6587 $movementstock = new MouvementStock($this->db);
6588 $movementstock->setOrigin($origin_element, (int) $origin_id); // Set ->origin_type and ->fk_origin
6589 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', $dlc, $dluo, $lot, false, 0, $disablestockchangeforsubproduct, 0, $force_update_batch);
6590
6591 if ($result >= 0) {
6592 if ($extrafields) {
6593 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
6594 $movementstock->array_options = $array_options;
6595 $movementstock->insertExtraFields();
6596 }
6597 $this->db->commit();
6598 return 1;
6599 } else {
6600 $this->error = $movementstock->error;
6601 $this->errors = $movementstock->errors;
6602
6603 $this->db->rollback();
6604 return -1;
6605 }
6606 }
6607 return -1;
6608 }
6609
6610 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6623 public function load_stock($option = '', $includedraftpoforvirtual = null, $dateofvirtualstock = null)
6624 {
6625 // phpcs:enable
6626 $this->stock_reel = 0;
6627 $this->stock_warehouse = array();
6628 $this->stock_theorique = 0;
6629
6630 // Set filter on warehouse status
6631 $warehouseStatus = array();
6632 if (preg_match('/warehouseclosed/', $option)) {
6634 }
6635 if (preg_match('/warehouseopen/', $option)) {
6637 }
6638 if (preg_match('/warehouseinternal/', $option)) {
6639 if (getDolGlobalString('ENTREPOT_EXTRA_STATUS')) {
6641 } else {
6643 }
6644 }
6645
6646 $sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot";
6647 $sql .= " FROM ".$this->db->prefix()."product_stock as ps";
6648 $sql .= ", ".$this->db->prefix()."entrepot as w";
6649 $sql .= " WHERE w.entity IN (".getEntity('stock').")";
6650 $sql .= " AND w.rowid = ps.fk_entrepot";
6651 $sql .= " AND ps.fk_product = ".((int) $this->id);
6652 if (count($warehouseStatus)) {
6653 $sql .= " AND w.statut IN (".$this->db->sanitize(implode(',', $warehouseStatus)).")";
6654 }
6655
6656 $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;
6657
6658 dol_syslog(get_class($this)."::load_stock", LOG_DEBUG);
6659 $result = $this->db->query($sql);
6660 if ($result) {
6661 $num = $this->db->num_rows($result);
6662 $i = 0;
6663 if ($num > 0) {
6664 while ($i < $num) {
6665 $row = $this->db->fetch_object($result);
6666 $this->stock_warehouse[$row->fk_entrepot] = new stdClass();
6667 $this->stock_warehouse[$row->fk_entrepot]->real = $row->reel;
6668 $this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid;
6669 if ((!preg_match('/nobatch/', $option)) && $this->hasbatch()) {
6670 $this->stock_warehouse[$row->fk_entrepot]->detail_batch = Productbatch::findAll($this->db, $row->rowid, 1, $this->id);
6671 }
6672 $this->stock_reel += $row->reel;
6673 $i++;
6674 }
6675 $this->stock_reel = (float) price2num($this->stock_reel, 'MS');
6676 }
6677 $this->db->free($result);
6678
6679 if (!preg_match('/novirtual/', $option)) {
6680 $this->load_virtual_stock($includedraftpoforvirtual, $dateofvirtualstock); // This load stock_theorique and also load all arrays stats_xxx...
6681 }
6682
6683 return 1;
6684 } else {
6685 $this->error = $this->db->lasterror();
6686 return -1;
6687 }
6688 }
6689
6690
6691 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6701 public function load_virtual_stock($includedraftpoforvirtual = null, $dateofvirtualstock = null)
6702 {
6703 // phpcs:enable
6704 global $hookmanager, $action;
6705
6706 $stock_commande_client = 0;
6707 $stock_commande_fournisseur = 0;
6708 $stock_sending_client = 0;
6709 $stock_reception_fournisseur = 0;
6710 $stock_inproduction = 0;
6711
6712 //dol_syslog("load_virtual_stock");
6713
6714 // If the caller did not provide a date, apply the virtual stock horizon (option STOCK_VIRTUAL_HORIZON_IN_DAYS):
6715 // incoming supply expected after this horizon is ignored, so the theoretical stock does not promise goods that are still far away.
6716 // An empty option means no horizon at all (default). A value of 0 is a valid horizon: only supply already due is counted.
6717 $horizoninDays = getDolGlobalString('STOCK_VIRTUAL_HORIZON_IN_DAYS');
6718 $dateofvirtualstockmin = 0;
6719 if (empty($dateofvirtualstock) && $horizoninDays !== '') {
6720 include_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php'; // dol_time_plus_duree/dol_get_first_hour are not loaded in CLI/webservice contexts
6721 $dateofvirtualstock = dol_time_plus_duree(dol_now(), max(0, (int) $horizoninDays), 'd');
6722 $dateofvirtualstockmin = dol_get_first_hour(dol_now()); // The horizon is a window: supply expected before today did not arrive as planned
6723 }
6724
6725 if (isModEnabled('order')) {
6726 $result = $this->load_stats_commande(0, '1,2', 1);
6727 if ($result < 0) {
6728 dol_print_error($this->db, $this->error);
6729 }
6730 $stock_commande_client = $this->stats_commande['qty'];
6731 }
6732 if (isModEnabled("shipping")) {
6733 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
6734 $filterShipmentStatus = '';
6735 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
6736 $filterShipmentStatus = Expedition::STATUS_VALIDATED.','.Expedition::STATUS_CLOSED;
6737 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6738 $filterShipmentStatus = Expedition::STATUS_CLOSED;
6739 }
6740 $result = $this->load_stats_sending(0, '1,2', 1, $filterShipmentStatus);
6741 if ($result < 0) {
6742 dol_print_error($this->db, $this->error);
6743 }
6744 $stock_sending_client = $this->stats_expedition['qty'];
6745 }
6746 // Include supplier order lines
6747 if (isModEnabled("supplier_order")) {
6748 $filterStatus = getDolGlobalString('SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK', '3,4');
6749 if (isset($includedraftpoforvirtual)) {
6750 $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
6751 }
6752 $result = $this->load_stats_commande_fournisseur(0, $filterStatus, 1, $dateofvirtualstock, $dateofvirtualstockmin);
6753 if ($result < 0) {
6754 dol_print_error($this->db, $this->error);
6755 }
6756 $stock_commande_fournisseur = $this->stats_commande_fournisseur['qty'];
6757 }
6758 // Include reception lines
6759 if (isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) {
6760 $filterStatus = getDolGlobalString('SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK', '4');
6761 if (isset($includedraftpoforvirtual)) {
6762 $filterStatus = '0,'.$filterStatus;
6763 }
6764 $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock, $dateofvirtualstockmin);
6765 if ($result < 0) {
6766 dol_print_error($this->db, $this->error);
6767 }
6768 $stock_reception_fournisseur = $this->stats_reception['qty'];
6769 }
6770 // Include manufacturing
6771 if (isModEnabled('mrp')) {
6772 $filterStatus = getDolGlobalString('MO_STATUS_FOR_VIRTUAL_STOCK', '1,2');
6773 $result = $this->load_stats_inproduction(0, $filterStatus, 1, $dateofvirtualstock, 0, $dateofvirtualstockmin);
6774 if ($result < 0) {
6775 dol_print_error($this->db, $this->error);
6776 }
6777 $stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty'];
6778 }
6779
6780 $this->stock_theorique = $this->stock_reel + $stock_inproduction;
6781
6782 // $weBillOrderOrShipmentReception is set to 'order' or 'shipmentreception'. it will be used to know how to make virtual stock
6783 // calculation when we have a stock increase or decrease on billing. Do we have to count orders to bill or shipment/reception to bill ?
6784 $weBillOrderOrShipmentReception = getDolGlobalString('STOCK_DO_WE_BILL_ORDER_OR_SHIPMENTECEPTION_FOR_VIRTUALSTOCK', 'order');
6785
6786 // Stock decrease mode
6787 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') || getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6788 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6789 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_VALIDATE_ORDER')) {
6790 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
6791 $tmpnewprod = dol_clone($this, 1);
6792 $result = $tmpnewprod->load_stats_commande(0, '0', 1); // Get qty in draft orders
6793 $this->stock_theorique += $tmpnewprod->stats_commande['qty'];
6794 }
6795 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'order') {
6796 $this->stock_theorique -= $stock_commande_client;
6797 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6798 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6799 }
6800
6801 // Stock Increase mode
6802 if (getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION') || getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION_CLOSE')) {
6803 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6804 } 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
6805 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6806 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER')) { // Warning: stock change "on approval", not on validation !
6807 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
6808 $tmpnewprod = dol_clone($this, 1);
6809 $result = $tmpnewprod->load_stats_commande_fournisseur(0, '0', 1); // Get qty in draft orders
6810 $this->stock_theorique += $this->stats_commande_fournisseur['qty'];
6811 }
6812 $this->stock_theorique -= $stock_reception_fournisseur;
6813 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'order') {
6814 $this->stock_theorique += $stock_commande_fournisseur;
6815 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6816 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6817 }
6818
6819 $parameters = array('id' => $this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual);
6820 // Note that $action and $object may have been modified by some hooks
6821 $reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action);
6822 if ($reshook > 0) {
6823 $this->stock_theorique = $hookmanager->resArray['stock_theorique'];
6824 } elseif ($reshook == 0 && isset($hookmanager->resArray['stock_stats_hook'])) {
6825 $this->stock_theorique += $hookmanager->resArray['stock_stats_hook'];
6826 }
6827
6828 //Virtual Stock by Warehouse
6829 if (!empty($this->stock_warehouse) && getDolGlobalString('STOCK_ALLOW_VIRTUAL_STOCK_PER_WAREHOUSE')) {
6830 foreach ($this->stock_warehouse as $warehouseid => $stockwarehouse) {
6831 if (isModEnabled('mrp')) {
6832 $result = $this->load_stats_inproduction(0, getDolGlobalString('MO_STATUS_FOR_VIRTUAL_STOCK', '1,2'), 1, $dateofvirtualstock, $warehouseid, $dateofvirtualstockmin);
6833 if ($result < 0) {
6834 dol_print_error($this->db, $this->error);
6835 }
6836 }
6837
6838 if ($this->fk_default_warehouse == $warehouseid) {
6839 $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']);
6840 } else {
6841 $this->stock_warehouse[$warehouseid]->virtual = $this->stock_warehouse[$warehouseid]->real + $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'];
6842 }
6843 }
6844 }
6845
6846 return 1;
6847 }
6848
6856 public function loadStockForVirtualProduct($option = '', $qtyWish = 1)
6857 {
6858 $this->stock_warehouse = array();
6859 $error = 0;
6860
6861 $this->get_sousproduits_arbo();
6862 $prods_arbo = $this->get_arbo_each_prod($qtyWish, 1);
6863 if (count($prods_arbo) > 0) {
6864 $productCachedList = array();
6865 $stockByComponentList = array();
6866
6867 foreach ($prods_arbo as $componentArr) {
6868 $componentId = $componentArr['id'];
6869 // only component whose manage stock
6870 if ($componentArr['incdec'] == 1) {
6871 if (!isset($productCachedList[$componentId])) {
6872 $componentStatic = new self($this->db);
6873 $componentStatic->fetch($componentId);
6874 // check if it's a sub-kit
6875 $childrenNb = $componentStatic->hasFatherOrChild(1);
6876 if ($childrenNb == 0) {
6877 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6878 if (!isset($stockByComponentList[$componentId])) {
6879 $stockByComponentList[$componentId] = array(
6880 'qty_need' => 0
6881 );
6882 }
6883 $stockByComponentList[$componentId]['qty_need'] += $componentArr['nb_total'];
6884 }
6885 $productCachedList[$componentId] = $componentStatic;
6886 }
6887 }
6888 }
6889
6890 if (!empty($stockByComponentList)) {
6891 foreach ($stockByComponentList as $componentId => $stockByComponentArr) {
6892 if (!isset($productCachedList[$componentId])) {
6893 $componentStatic = new self($this->db);
6894 $componentStatic->fetch($componentId);
6895 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6896 $productCachedList[$componentId] = $componentStatic;
6897 }
6898 $component = $productCachedList[$componentId];
6899
6900
6901 if ($component->stock_reel < $stockByComponentArr['qty_need']) {
6902 // not enough stock for this component to assemble this virtual product
6903 $error++;
6904 $this->error = 'Not enough component [id='.$componentId.'] in stock, real='.$component->stock_reel.' and need='.$stockByComponentArr['qty_need'];
6905 $this->errors[] = $this->error;
6906 dol_syslog(__METHOD__.' : '.$this->error, LOG_ERR);
6907 } else {
6908 if (!empty($component->stock_warehouse)) {
6909 foreach ($component->stock_warehouse as $warehouseId => $warehouseObj) {
6910 $kitWarehouseAvailable = new stdClass();
6911 $kitWarehouseAvailable->id = $warehouseObj->id;
6912 $kitWarehouseAvailable->real = $qtyWish;
6913 $this->stock_warehouse[$warehouseId] = $kitWarehouseAvailable;
6914 }
6915 }
6916 }
6917
6918 if ($error) {
6919 break;
6920 }
6921 }
6922 } elseif (getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE') && empty($productCachedList)) {
6923 // if all sub product are not stock managed when use parent stock
6924 $this->load_stock('warehouseopen');
6925 }
6926 }
6927
6928 if ($error) {
6929 return -1;
6930 } else {
6931 return 1;
6932 }
6933 }
6934
6942 public function loadBatchInfo($batch)
6943 {
6944 $result = array();
6945
6946 $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";
6947 $sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = ".((int) $this->id)." AND pb.batch = '".$this->db->escape($batch)."'";
6948 $sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby";
6949 dol_syslog(get_class($this)."::loadBatchInfo load first entry found for lot/serial = ".$batch, LOG_DEBUG);
6950 $resql = $this->db->query($sql);
6951 if ($resql) {
6952 $num = $this->db->num_rows($resql);
6953 $i = 0;
6954 while ($i < $num) {
6955 $obj = $this->db->fetch_object($resql);
6956 $result[] = array('batch' => $batch, 'eatby' => $this->db->jdate($obj->eatby), 'sellby' => $this->db->jdate($obj->sellby), 'qty' => $obj->qty);
6957 $i++;
6958 }
6959 return $result;
6960 } else {
6961 dol_print_error($this->db);
6962 $this->db->rollback();
6963 return array();
6964 }
6965 }
6966
6967 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6976 public function add_photo($sdir, $file)
6977 {
6978 // phpcs:enable
6979 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6980
6981 $result = 0;
6982
6983 $dir = $sdir;
6984 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6985 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos";
6986 } else {
6987 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product').dol_sanitizeFileName($this->ref);
6988 }
6989
6990 dol_mkdir($dir);
6991
6992 $dir_osencoded = $dir;
6993
6994 if (is_dir($dir_osencoded)) {
6995 $originImage = $dir.'/'.$file['name'];
6996
6997 // Cree fichier en taille origine
6998 $result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
6999
7000 if (file_exists(dol_osencode($originImage))) {
7001 // Create thumbs
7002 $this->addThumbs($originImage);
7003 }
7004 }
7005
7006 if (is_numeric($result) && $result > 0) {
7007 return 1;
7008 } else {
7009 return -1;
7010 }
7011 }
7012
7013 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7020 public function is_photo_available($sdir)
7021 {
7022 // phpcs:enable
7023 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
7024 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
7025
7026 $dir = $sdir;
7027 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
7028 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos/";
7029 } else {
7030 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product');
7031 }
7032
7033 $dir_osencoded = dol_osencode($dir);
7034 if (file_exists($dir_osencoded)) {
7035 $handle = opendir($dir_osencoded);
7036 if (is_resource($handle)) {
7037 while (($file = readdir($handle)) !== false) {
7038 if (!utf8_check($file)) {
7039 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // To be sure data is stored in UTF8 in memory
7040 }
7041 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
7042 return true;
7043 }
7044 }
7045 }
7046 }
7047
7048 return false;
7049 }
7050
7051 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7059 public function liste_photos($dir, $nbmax = 0)
7060 {
7061 // phpcs:enable
7062 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
7063 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
7064
7065 $nbphoto = 0;
7066 $tabobj = array();
7067
7068 $dir_osencoded = dol_osencode($dir);
7069 $handle = @opendir($dir_osencoded);
7070 if (is_resource($handle)) {
7071 while (($file = readdir($handle)) !== false) {
7072 if (!utf8_check($file)) {
7073 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // readdir returns ISO
7074 }
7075 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
7076 $nbphoto++;
7077
7078 // We forge name of thumb.
7079 $photo = $file;
7080 $photo_vignette = '';
7081 $regs = array();
7082 if (preg_match('/('.$this->regeximgext.')$/i', $photo, $regs)) {
7083 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $photo).'_small'.$regs[0];
7084 }
7085
7086 $dirthumb = $dir.'thumbs/';
7087
7088 // Object
7089 $obj = array();
7090 $obj['photo'] = $photo;
7091 if ($photo_vignette && dol_is_file($dirthumb.$photo_vignette)) {
7092 $obj['photo_vignette'] = 'thumbs/'.$photo_vignette;
7093 } else {
7094 $obj['photo_vignette'] = "";
7095 }
7096
7097 $tabobj[$nbphoto - 1] = $obj;
7098
7099 // Do we have to continue with next photo ?
7100 if ($nbmax && $nbphoto >= $nbmax) {
7101 break;
7102 }
7103 }
7104 }
7105
7106 closedir($handle);
7107 }
7108
7109 return $tabobj;
7110 }
7111
7112 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7119 public function delete_photo($file)
7120 {
7121 // phpcs:enable
7122 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
7123 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
7124
7125 $dir = dirname($file).'/'; // Chemin du dossier contenant l'image d'origine
7126 $dirthumb = $dir.'/thumbs/'; // Chemin du dossier contenant la vignette
7127 $filename = preg_replace('/'.preg_quote($dir, '/').'/i', '', $file); // Nom du fichier
7128
7129 // On efface l'image d'origine
7130 dol_delete_file($file, 0, 0, 0, $this); // For triggers
7131
7132 // Si elle existe, on efface la vignette
7133 if (preg_match('/('.$this->regeximgext.')$/i', $filename, $regs)) {
7134 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_small'.$regs[0];
7135 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
7136 dol_delete_file($dirthumb.$photo_vignette);
7137 }
7138
7139 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_mini'.$regs[0];
7140 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
7141 dol_delete_file($dirthumb.$photo_vignette);
7142 }
7143 }
7144 }
7145
7146 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7153 public function get_image_size($file)
7154 {
7155 // phpcs:enable
7156 $file_osencoded = dol_osencode($file);
7157 $infoImg = getimagesize($file_osencoded); // Get information on image
7158 $this->imgWidth = $infoImg[0]; // Largeur de l'image
7159 $this->imgHeight = $infoImg[1]; // Hauteur de l'image
7160 }
7161
7167 public function loadStateBoard()
7168 {
7169 global $hookmanager;
7170
7171 $this->nb = array();
7172
7173 $sql = "SELECT count(p.rowid) as nb, fk_product_type";
7174 $sql .= " FROM ".$this->db->prefix()."product as p";
7175 $sql .= ' WHERE p.entity IN ('.getEntity($this->element, 1).')';
7176 // Add where from hooks
7177 if (is_object($hookmanager)) {
7178 $parameters = array();
7179 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
7180 $sql .= $hookmanager->resPrint;
7181 }
7182 $sql .= ' GROUP BY fk_product_type';
7183
7184 $resql = $this->db->query($sql);
7185 if ($resql) {
7186 while ($obj = $this->db->fetch_object($resql)) {
7187 if ($obj->fk_product_type == 1) {
7188 $this->nb["services"] = $obj->nb;
7189 } else {
7190 $this->nb["products"] = $obj->nb;
7191 }
7192 }
7193 $this->db->free($resql);
7194 return 1;
7195 } else {
7196 dol_print_error($this->db);
7197 $this->error = $this->db->error();
7198 return -1;
7199 }
7200 }
7201
7207 public function isProduct()
7208 {
7209 return $this->type == Product::TYPE_PRODUCT;
7210 }
7211
7217 public function isService()
7218 {
7219 return $this->type == Product::TYPE_SERVICE;
7220 }
7221
7227 public function isStockManaged()
7228 {
7229 return (($this->isProduct() || ($this->isService() && getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) && ($this->stockable_product > 0));
7230 }
7231
7237 public function isMandatoryPeriod()
7238 {
7239 return $this->mandatory_period == 1;
7240 }
7241
7247 public function hasbatch()
7248 {
7249 return $this->status_batch > 0;
7250 }
7251
7252
7253 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7262 public function get_barcode($object, $type = '')
7263 {
7264 // phpcs:enable
7265 global $conf;
7266
7267 $result = '';
7268 if (getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
7269 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
7270 foreach ($dirsociete as $dirroot) {
7271 $res = dol_include_once($dirroot . getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM').'.php');
7272 if ($res) {
7273 break;
7274 }
7275 }
7276 $var = getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM');
7277 $mod = new $var();
7278 '@phan-var-force ModeleNumRefBarCode $mod';
7279
7280 $result = $mod->getNextValue($object, $type);
7281
7282 dol_syslog(get_class($this)."::get_barcode barcode=".$result." module=".$var);
7283 }
7284 return $result;
7285 }
7286
7294 public function initAsSpecimen()
7295 {
7296 $now = dol_now();
7297
7298 // Initialize parameters
7299 $this->specimen = 1;
7300 $this->id = 0;
7301 $this->ref = 'PRODUCT_SPEC';
7302 $this->label = 'PRODUCT SPECIMEN';
7303 $this->description = 'This is description of this product specimen that was created the '.dol_print_date($now, 'dayhourlog').'.';
7304 $this->specimen = 1;
7305 $this->country_id = 1;
7306 $this->status = 1;
7307 $this->status_buy = 1;
7308 $this->tobatch = 0;
7309 $this->sell_or_eat_by_mandatory = 0;
7310 $this->note_private = 'This is a comment (private)';
7311 $this->note_public = 'This is a comment (public)';
7312 $this->date_creation = $now;
7313 $this->date_modification = $now;
7314
7315 $this->weight = 4;
7316 $this->weight_units = 3;
7317
7318 $this->length = 5;
7319 $this->length_units = 1;
7320 $this->width = 6;
7321 $this->width_units = 0;
7322 $this->height = null;
7323 $this->height_units = null;
7324
7325 $this->surface = 30;
7326 $this->surface_units = 0;
7327 $this->volume = 300;
7328 $this->volume_units = 0;
7329
7330 $this->barcode = -1; // Create barcode automatically
7331
7332 return 1;
7333 }
7334
7344 public function getLabelOfUnit($type = 'long', $outputlangs = null, $noentities = 0)
7345 {
7346 global $langs;
7347
7348 if (empty($this->fk_unit)) {
7349 return '';
7350 }
7351 if (empty($outputlangs)) {
7352 $outputlangs = $langs;
7353 }
7354
7355 $outputlangs->load('products');
7356 $label = '';
7357
7358 $sql = "SELECT code, label, short_label FROM ".$this->db->prefix()."c_units where rowid = ".((int) $this->fk_unit);
7359
7360 $resql = $this->db->query($sql);
7361 if (!$resql) {
7362 $this->error = $this->db->error();
7363 dol_syslog(get_class($this)."::getLabelOfUnit Error ".$this->error, LOG_ERR);
7364 return -1;
7365 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
7366 if ($type == 'short') {
7367 if ($noentities) {
7368 $label = $outputlangs->transnoentitiesnoconv($res['short_label']);
7369 } else {
7370 $label = $outputlangs->trans($res['short_label']);
7371 }
7372 } elseif ($type == 'code') {
7373 $label = $res['code'];
7374 } else {
7375 if ($outputlangs->trans('unit'.$res['code']) == 'unit'.$res['code']) {
7376 // No translation available
7377 $label = $res['label'];
7378 } else {
7379 // Return the translated value
7380 if ($noentities) {
7381 $label = $outputlangs->transnoentitiesnoconv('unit'.$res['code']);
7382 } else {
7383 $label = $outputlangs->trans('unit'.$res['code']);
7384 }
7385 }
7386 }
7387 }
7388 $this->db->free($resql);
7389
7390 return $label;
7391 }
7392
7393 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7399 public function min_recommended_price()
7400 {
7401 // phpcs:enable
7402 $maxpricesupplier = 0;
7403
7404 if (getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE')) {
7405 include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
7406 $product_fourn = new ProductFournisseur($this->db);
7407 $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', '');
7408
7409 if (is_array($product_fourn_list) && count($product_fourn_list) > 0) {
7410 foreach ($product_fourn_list as $productfourn) {
7411 if ($productfourn->fourn_unitprice > $maxpricesupplier) {
7412 $maxpricesupplier = $productfourn->fourn_unitprice;
7413 }
7414 }
7415
7416 $maxpricesupplier *= getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE');
7417 }
7418 }
7419
7420 return $maxpricesupplier;
7421 }
7422
7423
7434 public function setCategories($categories)
7435 {
7436 require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
7437 return parent::setCategoriesCommon($categories, Categorie::TYPE_PRODUCT);
7438 }
7439
7448 public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
7449 {
7450 /* we firstly delete origin product prices existing in destination before update */
7451 $sql = " DELETE FROM ".$dbs->prefix()."product_customer_price";
7452 $sql .= " WHERE fk_soc = ".(int) $origin_id;
7453 $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)";
7454 //$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.')';
7455
7456 if (!$dbs->query($sql)) {
7457 return false;
7458 }
7459
7460 $tables = array(
7461 'product_customer_price',
7462 'product_customer_price_log'
7463 );
7464
7465 return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
7466 }
7467
7479 public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
7480 {
7481 $sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM ".$this->db->prefix()."product_pricerules";
7482 $query = $this->db->query($sql);
7483
7484 $rules = array();
7485
7486 while ($result = $this->db->fetch_object($query)) {
7487 $rules[$result->level] = $result;
7488 }
7489
7490 //Because prices can be based on other level's prices, we temporarily store them
7491 $prices = array(
7492 1 => $baseprice
7493 );
7494
7495 $nbofproducts = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
7496 for ($i = 1; $i <= $nbofproducts; $i++) {
7497 $price = $baseprice;
7498 $price_min = $baseprice;
7499
7500 //We have to make sure it does exist and it is > 0
7501 //First price level only allows changing min_price
7502 if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) {
7503 $price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent / 100));
7504 }
7505
7506 $prices[$i] = $price;
7507
7508 //We have to make sure it does exist and it is > 0
7509 if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) {
7510 $price_min = $price * (1 - ($rules[$i]->var_min_percent / 100));
7511 }
7512
7513 //Little check to make sure the price is modified before triggering generation
7514 $check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i]));
7515 $check_type = ($baseprice == $this->multiprices_base_type[$i]);
7516
7517 if ($check_amount && $check_type) {
7518 continue;
7519 }
7520
7521 if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, 1) < 0) {
7522 return -1;
7523 }
7524 }
7525
7526 return 1;
7527 }
7528
7534 public function getRights()
7535 {
7536 global $user;
7537
7538 if ($this->isProduct()) {
7539 return $user->rights->produit;
7540 } else {
7541 return $user->rights->service;
7542 }
7543 }
7544
7551 public function info($id)
7552 {
7553 $sql = "SELECT p.rowid, p.ref, p.datec as date_creation, GREATEST(p.tms, pef.tms) as date_modification,";
7554 $sql .= " p.fk_user_author, p.fk_user_modif";
7555 $sql .= " FROM ".$this->db->prefix().$this->table_element." as p";
7556 $sql .= " LEFT JOIN ".$this->db->prefix().$this->table_element."_extrafields as pef ON pef.fk_object=p.rowid";
7557 $sql .= " WHERE p.rowid = ".((int) $id);
7558
7559 $result = $this->db->query($sql);
7560 if ($result) {
7561 if ($this->db->num_rows($result)) {
7562 $obj = $this->db->fetch_object($result);
7563
7564 $this->id = $obj->rowid;
7565 $this->ref = $obj->ref;
7566
7567 $this->user_creation_id = $obj->fk_user_author;
7568 $this->user_modification_id = $obj->fk_user_modif;
7569
7570 $this->date_creation = $this->db->jdate($obj->date_creation);
7571 $this->date_modification = $this->db->jdate($obj->date_modification);
7572 }
7573
7574 $this->db->free($result);
7575 } else {
7576 dol_print_error($this->db);
7577 }
7578 }
7579
7580
7586 public function getProductDurationHours()
7587 {
7588 if (empty($this->duration_value)) {
7589 $this->errors[] = 'ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
7590 return -1;
7591 }
7592 if ($this->duration_unit == 's') {
7593 $prodDurationHours = 1. / 3600;
7594 } elseif ($this->duration_unit == 'i' || $this->duration_unit == 'mn' || $this->duration_unit == 'min') {
7595 $prodDurationHours = 1. / 60;
7596 } elseif ($this->duration_unit == 'h') {
7597 $prodDurationHours = 1.;
7598 } elseif ($this->duration_unit == 'd') {
7599 $prodDurationHours = 24.;
7600 } elseif ($this->duration_unit == 'w') {
7601 $prodDurationHours = 24. * 7;
7602 } elseif ($this->duration_unit == 'm') {
7603 $prodDurationHours = 24. * 30;
7604 } elseif ($this->duration_unit == 'y') {
7605 $prodDurationHours = 24. * 365;
7606 } else {
7607 // Unknown unit: the duration cannot be converted. Returning 0 silently made callers
7608 // compute a zero duration, and time.php divides by this value (#40805).
7609 $this->errors[] = 'ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
7610 return -1;
7611 }
7612 $prodDurationHours *= $this->duration_value;
7613
7614 return $prodDurationHours;
7615 }
7616
7617
7625 public function getKanbanView($option = '', $arraydata = null)
7626 {
7627 global $langs, $conf;
7628
7629 $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
7630
7631 $return = '<div class="box-flex-item box-flex-grow-zero">';
7632 $return .= '<div class="info-box info-box-sm">';
7633 $return .= '<div class="info-box-img">';
7634 $label = '';
7635 if ($this->entity !== null && $this->is_photo_available($conf->product->multidir_output[$this->entity])) {
7636 $label .= $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 120, 160, 0, 0, 0, '', 'photoref photokanban');
7637 $return .= $label;
7638 } else {
7639 if ($this->isProduct()) {
7640 $label .= img_picto('', 'product');
7641 } elseif ($this->isService()) {
7642 $label .= img_picto('', 'service');
7643 }
7644 $return .= $label;
7645 }
7646 $return .= '</div>';
7647 $return .= '<div class="info-box-content">';
7648 $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">' . $this->getNomUrl() . '</span>';
7649 if ($selected >= 0) {
7650 $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
7651 }
7652 $return .= '<br><span class="info-box-label opacitymedium inline-block tdoverflowmax150 valignmiddle" title="'.dol_escape_htmltag($this->label).'">'.dol_escape_htmltag($this->label).'</span>';
7653 if ($this->price_base_type == 'TTC') {
7654 $return .= '<br><span class="info-box-status amount">'.price($this->price_ttc).' '.$langs->trans("TTC").'</span>';
7655 } else {
7656 if ($this->status) {
7657 $return .= '<br><span class="info-box-status amount">'.price($this->price).' '.$langs->trans("HT").'</span>';
7658 }
7659 }
7660 $br = 1;
7661 if ($this->isProduct()) {
7662 $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>';
7663 $br = 0;
7664 }
7665 if ($br) {
7666 $return .= '<br>';
7667 $return .= '<div class="info-box-status inline-block valignmiddle">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7668 } else {
7669 $return .= '<div class="info-box-status inline-block valignmiddle marginleftonly paddingleft">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7670 }
7671 $return .= '</div>';
7672 $return .= '</div>';
7673 $return .= '</div>';
7674 return $return;
7675 }
7676
7684 private function cloneCustomerPriceAndExtra($fromId, $toId)
7685 {
7686 global $user;
7687
7688 $now = dol_now();
7689
7690 // Price per customer
7691 $sql = "INSERT INTO ".$this->db->prefix()."product_customer_price (";
7692 $sql .= " entity";
7693 $sql .= ", fk_product";
7694 $sql .= ", fk_soc";
7695 $sql .= ", datec";
7696 $sql .= ", price";
7697 $sql .= ", price_ttc";
7698 $sql .= ", price_min";
7699 $sql .= ", price_min_ttc";
7700 $sql .= ", price_base_type";
7701 $sql .= ", default_vat_code";
7702 $sql .= ", tva_tx";
7703 $sql .= ", recuperableonly";
7704 $sql .= ", localtax1_tx";
7705 $sql .= ", localtax1_type";
7706 $sql .= ", localtax2_tx";
7707 $sql .= ", localtax2_type";
7708 $sql .= ", fk_user";
7709 $sql .= ")";
7710 $sql .= " SELECT";
7711 $sql .= " entity";
7712 $sql .= ", ".((int) $toId);
7713 $sql .= ", fk_soc";
7714 $sql .= ", '".$this->db->idate($now)."'";
7715 $sql .= ", price";
7716 $sql .= ", price_ttc";
7717 $sql .= ", price_min";
7718 $sql .= ", price_min_ttc";
7719 $sql .= ", price_base_type";
7720 $sql .= ", default_vat_code";
7721 $sql .= ", tva_tx";
7722 $sql .= ", recuperableonly";
7723 $sql .= ", localtax1_tx";
7724 $sql .= ", localtax1_type";
7725 $sql .= ", localtax2_tx";
7726 $sql .= ", localtax2_type";
7727 $sql .= ", ".((int) $user->id);
7728 $sql .= " FROM ".$this->db->prefix()."product_customer_price pc";
7729 $sql .= " WHERE pc.fk_product = ".((int) $fromId);
7730 $sql .= " AND pc.datec IN (";
7731 $sql .= " SELECT MAX(pd.datec)";
7732 $sql .= " FROM ".$this->db->prefix()."product_customer_price pd";
7733 $sql .= " WHERE pd.fk_product = ".((int) $fromId);
7734 $sql .= " AND pd.fk_soc = pc.fk_soc";
7735 $sql .= " )";
7736
7737 dol_syslog(__METHOD__, LOG_DEBUG);
7738
7739 $resql = $this->db->query($sql);
7740 if (!$resql) {
7741 $this->db->rollback();
7742 return -1;
7743 }
7744 $this->db->free($resql);
7745
7746 // Add of customer price extrafields
7747 // Retrieving the list of extrafields
7748 $sql_fields = "SHOW COLUMNS FROM ".MAIN_DB_PREFIX."product_customer_price_extrafields";
7749 $res_fields = $this->db->query($sql_fields);
7750 if (!$res_fields) {
7751 $this->db->rollback();
7752 return -1;
7753 }
7754 $fields = array();
7755 while ($field = $this->db->fetch_object($res_fields)) {
7756 if (!in_array($field->Field, array('rowid', 'tms', 'fk_object'))) {
7757 $fields[] = $field->Field;
7758 }
7759 }
7760 if (!empty($fields)) {
7761 // Matching old price, new price
7762 $sql = "SELECT";
7763 $sql .= " oldp.rowid AS old_price_id,";
7764 $sql .= " newp.rowid AS new_price_id";
7765 $sql .= " FROM ".MAIN_DB_PREFIX."product_customer_price AS oldp";
7766 $sql .= " INNER JOIN ".MAIN_DB_PREFIX."product_customer_price AS newp";
7767 $sql .= " ON newp.fk_soc = oldp.fk_soc";
7768 $sql .= " WHERE oldp.fk_product = ".((int) $fromId);
7769 $sql .= " AND newp.fk_product = ".((int) $toId);
7770 $sql .= " AND newp.datec = '".$this->db->idate($now)."'";
7771 $resql = $this->db->query($sql);
7772 if (!$resql) {
7773 $this->db->rollback();
7774 return -1;
7775 }
7776 while ($obj = $this->db->fetch_object($resql)) {
7777 $sql_copy = "INSERT INTO ".MAIN_DB_PREFIX."product_customer_price_extrafields";
7778 $sql_copy .= " (fk_object";
7779 foreach ($fields as $field) {
7780 $sql_copy .= ",".$field;
7781 }
7782 $sql_copy .= ")";
7783 $sql_copy .= " SELECT ";
7784 $sql_copy .= ((int) $obj->new_price_id);
7785 foreach ($fields as $field) {
7786 $sql_copy .= ",".$field;
7787 }
7788 $sql_copy .= " FROM ".MAIN_DB_PREFIX."product_customer_price_extrafields";
7789 $sql_copy .= " WHERE fk_object = ".((int) $obj->old_price_id);
7790 $resql_copy = $this->db->query($sql_copy);
7791 if (!$resql_copy) {
7792 $this->db->rollback();
7793 return -1;
7794 }
7795 }
7796 $this->db->free($resql);
7797 }
7798
7799 $this->db->free($res_fields);
7800
7801 return 1;
7802 }
7803
7811 private function cloneMultipriceExtra($fromId, $toId)
7812 {
7813 global $user;
7814
7815 $now = dol_now();
7816
7817 // Retrieving the list of extrafields
7818 $sql_fields = "SHOW COLUMNS FROM ".MAIN_DB_PREFIX."product_price_extrafields";
7819 $res_fields = $this->db->query($sql_fields);
7820 if (!$res_fields) {
7821 $this->db->rollback();
7822 return -1;
7823 }
7824 $fields = array();
7825 while ($field = $this->db->fetch_object($res_fields)) {
7826 if (!in_array($field->Field, array('rowid', 'tms', 'fk_object'))) {
7827 $fields[] = $field->Field;
7828 }
7829 }
7830 if (!empty($fields)) {
7831 // Matching old price, new price
7832 $sql = "SELECT";
7833 $sql .= " oldp.rowid AS old_price_id,";
7834 $sql .= " newp.rowid AS new_price_id";
7835 $sql .= " FROM ".MAIN_DB_PREFIX."product_price AS oldp";
7836 $sql .= " INNER JOIN ".MAIN_DB_PREFIX."product_price AS newp";
7837 $sql .= " ON newp.price_level = oldp.price_level";
7838 $sql .= " WHERE oldp.fk_product = ".((int) $fromId);
7839 $sql .= " AND newp.fk_product = ".((int) $toId);
7840 $sql .= " AND newp.date_price = '".$this->db->idate($now)."'";
7841 $resql = $this->db->query($sql);
7842 if (!$resql) {
7843 $this->db->rollback();
7844 return -1;
7845 }
7846 while ($obj = $this->db->fetch_object($resql)) {
7847 $sql_copy = "INSERT INTO ".MAIN_DB_PREFIX."product_price_extrafields";
7848 $sql_copy .= " (fk_object";
7849 foreach ($fields as $field) {
7850 $sql_copy .= ",".$field;
7851 }
7852 $sql_copy .= ")";
7853 $sql_copy .= " SELECT ";
7854 $sql_copy .= ((int) $obj->new_price_id);
7855 foreach ($fields as $field) {
7856 $sql_copy .= ",".$field;
7857 }
7858 $sql_copy .= " FROM ".MAIN_DB_PREFIX."product_price_extrafields";
7859 $sql_copy .= " WHERE fk_object = ".((int) $obj->old_price_id);
7860 $resql_copy = $this->db->query($sql_copy);
7861 if (!$resql_copy) {
7862 $this->db->rollback();
7863 return -1;
7864 }
7865 }
7866
7867 $this->db->free($resql);
7868 }
7869
7870 $this->db->free($res_fields);
7871
7872 return 1;
7873 }
7874}
7875
7881{
7882 public $picto = 'service';
7883}
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.
$propal type
'integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter[:Sortfield]]]',...
Definition propal.php:280
$object ref
Definition info.php:90
fetch_optionals($rowid=null, $optionsArray=null)
Function to get extra fields of an object into $this->array_options This method is in most cases call...
deleteEcmFiles($mode=0)
Delete related files of object in database.
commonGenerateDocument($modelspath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams=null)
Common function for all objects extending CommonObject for generating documents.
setErrorsFromObject($object)
setErrorsFromObject
isObjectUsed($id=0, $entity=0)
Function to check if an object is used by others (by children).
deleteExtraFields()
Delete all extra fields values for the current object.
static commonReplaceThirdparty(DoliDB $dbs, $origin_id, $dest_id, array $tables, $ignoreerrors=0)
Function used to replace a thirdparty id with another one.
show_photos($modulepart, $sdir, $size=0, $nbmax=0, $nbbyrow=5, $showfilename=0, $showaction=0, $maxHeight=120, $maxWidth=160, $nolink=0, $overwritetitle=0, $usesharelink=0, $cache='', $addphotorefcss='photoref')
Show photos of an object (nbmax maximum), into several columns.
insertExtraFields($trigger='', $userused=null)
Add/Update all extra fields values for the current object.
addThumbs($file, $quality=50)
Build thumb.
Class to manage Dolibarr database access.
Class to manage ECM files.
const STATUS_OPEN_INTERNAL
Warehouse open and only operations for stock transfers/corrections allowed (not for customer shipping...
const STATUS_OPEN_ALL
Warehouse open and any operations are allowed (customer shipping, supplier dispatch,...
const STATUS_CLOSED
Warehouse closed, inactive.
const STATUS_CLOSED
Closed status -> parcel was received by customer / end of process prev status : validated or shipment...
const STATUS_VALIDATED
Validated status -> parcel is ready to be sent prev status : draft next status : closed or shipment_i...
Class to manage standard extra fields.
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.
load_stats_inproduction($socid=0, $filtrestatut='', $forVirtualStock=0, $dateofvirtualstock=null, $warehouseid=0, $dateofvirtualstockmin=null)
Charge tableau des stats production pour le produit/service.
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.
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.
load_stats_reception($socid=0, $filtrestatut='', $forVirtualStock=0, $dateofvirtualstock=null, $dateofvirtualstockmin=null)
Loads supplier reception statistics table for given product or service.
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_commande_fournisseur($socid=0, $filtrestatut='', $forVirtualStock=0, $dateofvirtualstock=null, $dateofvirtualstockmin=null)
Charge tableau des stats commande fournisseur pour le produit/service.
load_stats_facture($socid=0)
Charge tableau des stats facture pour le produit/service.
setCategories($categories)
Sets object to supplied categories.
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.
getNomUrl($withpicto=0, $option='', $maxlength=0, $save_lastsearch_value=-1, $notooltip=0, $morecss='', $add_label=0, $sep=' - ', $addlinktonotes=0)
Return clickable link of object (with eventually picto)
cloneMultipriceExtra($fromId, $toId)
Clone price level extrafields.
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.
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)
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)
setMultiLangsExtrafields($langcode, $rowidlang)
Save the extrafields of a translation of the product (table llx_product_lang_extrafields).
min_recommended_price()
Return minimum product recommended price.
cloneCustomerPriceAndExtra($fromId, $toId)
Clone customer prices using extra fields.
_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.
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.
getMultiLangs($loadextrafields=0)
Load array this->multilangs.
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 of a translation of a product (a row of llx_product_lang).
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 translations.
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
dol_get_first_hour($date, $gm='tzserver')
Return GMT time for first hour of a given GMT date (it removes hours, min and second part)
Definition date.lib.php:676
dol_time_plus_duree($time, $duration_value, $duration_unit, $ruleforendofmonth=0)
Add a delay to a date.
Definition date.lib.php:126
print $script_file $mode $langs defaultlang(is_numeric($duration_value) ? " delay=". $duration_value :"").(is_numeric($duration_value2) ? " after cd cd cd description as description
Only used if Module[ID]Desc translation string is not found.
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
dol_delete_file($file, $disableglob=0, $nophperrors=0, $nohook=0, $object=null, $allowdotdot=false, $indexdatabase=1, $nolog=0)
Remove a file or several files with a mask.
dol_delete_dir_recursive($dir, $count=0, $nophperrors=0, $onlysub=0, &$countdeleted=0, $indexdatabase=1, $nolog=0, $level=0)
Remove a directory $dir and its subdirectories (or only files and subdirectories)
dol_is_file($pathoffile)
Return if path is a file.
dol_move_uploaded_file($src_file, $dest_file, $allowoverwrite, $disablevirusscan=0, $uploaderrorcode=0, $nohook=0, $keyforsourcefile='addedfile', $upload_dir='', $mode=0)
Check validity of a file upload from an GUI page, and move it to its final destination.
dol_now($mode='gmt')
Return date for now.
dol_mktime($hour, $minute, $second, $month, $day, $year, $gm='auto', $check=1)
Return a timestamp date built from detailed information (by default a local PHP server timestamp) Rep...
dolGetFirstLineOfText($text, $nboflines=1, $charset='UTF-8')
Return first line of text.
dol_osencode($str)
Return a string encoded into OS filesystem encoding.
dol_string_nohtmltag($stringtoclean, $removelinefeed=1, $pagecodeto='UTF-8', $strip_tags=0, $removedoublespaces=1)
Clean a string from all HTML tags and entities.
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
dolBuildUrl($url, $params=[], $addtoken=false, $anchor='')
Return path of url.
dol_string_nospecial($str, $newstr='_', $badcharstoreplace='', $badcharstoremove='', $keepspaces=0)
Clean a string from all punctuation characters to use it as a ref or login.
dol_sanitizeFileName($str, $newstr='_', $unaccent=1, $includequotes=0, $allowdash=0)
Clean a string to use it as a file name.
dol_strlen($string, $stringencoding='UTF-8')
Make a strlen call.
price($amount, $form=0, $outlangs='', $trunc=1, $rounding=-1, $forcerounding=-1, $currency_code='')
Function to format a value into an amount for visual output Function used into PDF and HTML pages.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
if(!function_exists( 'dol_getprefix')) dol_include_once($relpath, $classname='')
Make an include_once using default root and alternate root if it fails.
get_default_npr(Societe $thirdparty_seller, Societe $thirdparty_buyer, $idprod=0, $idprodfournprice=0)
Function that returns whether VAT must be recoverable collected VAT (e.g.: VAT NPR in France)
dol_clone($srcobject, $native=2)
Create a clone of instance of object (new instance with same value for each properties) With native =...
dol_print_date($time, $format='', $tzoutput='auto', $outputlangs=null, $encodetooutput=false, $decorate=0)
Output date in a string format according to outputlangs (or langs if not defined).
dol_trunc($string, $size=40, $trunc='right', $stringencoding='UTF-8', $nodot=0, $display=0)
Truncate a string to a particular length adding '...' if string larger than length.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
isModEnabled($module)
Is Dolibarr module enabled.
utf8_check($str)
Check if a string is in UTF8.
get_default_tva(Societe $thirdparty_seller, Societe $thirdparty_buyer, $idprod=0, $idprodfournprice=0)
Function that return vat rate of a product line (according to seller, buyer and product vat rate) VAT...
get_localtax($vatrate, $local, $thirdparty_buyer=null, $thirdparty_seller=null, $vatnpr=0)
Return localtax rate for a particular VAT rate, when selling a product with vat $vatrate,...
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
getEntity($element, $shared=1, $currentobject=null)
Get list of entity id to use.
dol_mkdir($dir, $dataroot='', $newmask='')
Creation of a directory (this can create recursive subdir)
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
Definition html.lib.php:181
image_format_supported($file, $acceptsvg=0)
Return if a filename is file name of a supported image format.
query($query, $usesavepoint=0, $type='auto', $result_mode=0)
Execute a SQL request and return the resultset.
measuring_units_squared($unitscale)
Transform a given unit scale into the square of that unit, if known.
measuringUnitString($unitid, $measuring_style='', $unitscale=null, $use_short_label=0, $outputlangs=null)
Return translation label of a unit key.
measuring_units_cubed($unit)
Transform a given unit scale into the cube of that unit, if known.