dolibarr 22.0.5
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-2025 Frédéric France <frederic.france@free.fr>
18 * Copyright (C) 2023 Benjamin Falière <benjamin.faliere@altairis.fr>
19 * Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
20 * Copyright (C) 2025 Lenin Rivas <lenin.rivas777@gmail.com>
21 * Copyright (C) 2026 Anthony Berton <anthony.berton@bb2a.fr>
22 *
23 * This program is free software; you can redistribute it and/or modify
24 * it under the terms of the GNU General Public License as published by
25 * the Free Software Foundation; either version 3 of the License, or
26 * (at your option) any later version.
27 *
28 * This program is distributed in the hope that it will be useful,
29 * but WITHOUT ANY WARRANTY; without even the implied warranty of
30 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 * GNU General Public License for more details.
32 *
33 * You should have received a copy of the GNU General Public License
34 * along with this program. If not, see <https://www.gnu.org/licenses/>.
35 */
36
42require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
43require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
44require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
45require_once DOL_DOCUMENT_ROOT.'/product/stock/class/productlot.class.php';
46require_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php';
47
51class Product extends CommonObject
52{
57 const SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY = 1;
58 const SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY = 2;
59 const SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT = 3;
60
64 public $element = 'product';
65
69 public $table_element = 'product';
70
74 public $fk_element = 'fk_product';
75
79 protected $childtables = array(
80 'supplier_proposaldet' => array('name' => 'SupplierProposal', 'parent' => 'supplier_proposal', 'parentkey' => 'fk_supplier_proposal'),
81 'propaldet' => array('name' => 'Proposal', 'parent' => 'propal', 'parentkey' => 'fk_propal'),
82 'commandedet' => array('name' => 'Order', 'parent' => 'commande', 'parentkey' => 'fk_commande'),
83 'facturedet' => array('name' => 'Invoice', 'parent' => 'facture', 'parentkey' => 'fk_facture'),
84 'contratdet' => array('name' => 'Contract', 'parent' => 'contrat', 'parentkey' => 'fk_contrat'),
85 'facture_fourn_det' => array('name' => 'SupplierInvoice', 'parent' => 'facture_fourn', 'parentkey' => 'fk_facture_fourn'),
86 'commande_fournisseurdet' => array('name' => 'SupplierOrder', 'parent' => 'commande_fournisseur', 'parentkey' => 'fk_commande'),
87 'mrp_production' => array('name' => 'Mo', 'parent' => 'mrp_mo', 'parentkey' => 'fk_mo', 'enabled' => 'isModEnabled("mrp")'),
88 'bom_bom' => array('name' => 'BOM', 'enabled' => 'isModEnabled("bom")'),
89 'bom_bomline' => array('name' => 'BOMLine', 'parent' => 'bom_bom', 'parentkey' => 'fk_bom', 'enabled' => 'isModEnabled("bom")'),
90 );
91
97 public $picto = 'product';
98
102 protected $table_ref_field = 'ref';
103
108 public $regeximgext = '\.gif|\.jpg|\.jpeg|\.png|\.bmp|\.webp|\.xpm|\.xbm';
109
115 public $libelle;
116
122 public $label;
123
129 public $description;
130
136 public $other;
137
143 public $type = self::TYPE_PRODUCT;
144
150 public $price;
151
155 public $price_formated; // used by takepos/ajax/ajax.php
156
162 public $price_ttc;
163
167 public $price_ttc_formated; // used by takepos/ajax/ajax.php
168
174 public $price_min;
175
181 public $price_min_ttc;
182
187 public $price_base_type;
191 public $price_label;
192
194
197 public $multiprices = array();
201 public $multiprices_ttc = array();
205 public $multiprices_base_type = array();
209 public $multiprices_default_vat_code = array();
213 public $multiprices_min = array();
217 public $multiprices_min_ttc = array();
221 public $multiprices_tva_tx = array();
225 public $multiprices_recuperableonly = array();
226
228
231 public $price_by_qty;
235 public $prices_by_qty = array();
239 public $prices_by_qty_id = array();
243 public $prices_by_qty_list = array();
244
248 public $level;
249
253 public $multilangs = array();
254
258 public $default_vat_code;
259
263 public $tva_tx;
264
268 public $tva_npr = 0;
269
273 public $remise_percent;
274
278 public $localtax1_tx;
282 public $localtax2_tx;
286 public $localtax1_type;
290 public $localtax2_type;
291
292 // Properties set by get_buyprice() for return
293
297 public $desc_supplier;
301 public $vatrate_supplier;
305 public $default_vat_code_supplier;
306
310 public $fourn_multicurrency_price;
314 public $fourn_multicurrency_unitprice;
318 public $fourn_multicurrency_tx;
322 public $fourn_multicurrency_id;
326 public $fourn_multicurrency_code;
327
331 public $packaging;
332
333
340 public $lifetime;
341
348 public $qc_frequency;
349
355 public $stock_reel = 0;
356
362 public $stock_theorique;
363
369 public $cost_price;
370
376 public $pmp;
377
383 public $seuil_stock_alerte = 0;
384
388 public $desiredstock = 0;
389
393 public $duration_value;
397 public $duration_unit;
401 public $duration;
402
406 public $fk_default_workstation;
407
413 public $status = 0;
414
421 public $tosell;
422
428 public $status_buy = 0;
429
436 public $tobuy;
437
443 public $finished;
444
450 public $fk_default_bom;
451
457 public $product_fourn_price_id;
458
464 public $buyprice;
465
471 public $tobatch;
472
473
479 public $status_batch = 0;
480
486 public $sell_or_eat_by_mandatory = 0;
487
493 public $batch_mask = '';
494
500 public $customcode;
501
507 public $url;
508
510
513 public $weight;
514
518 public $weight_units; // scale -3, 0, 3, 6
522 public $length;
526 public $length_units; // scale -3, 0, 3, 6
530 public $width;
534 public $width_units; // scale -3, 0, 3, 6
538 public $height;
542 public $height_units; // scale -3, 0, 3, 6
546 public $surface;
550 public $surface_units; // scale -3, 0, 3, 6
554 public $volume;
558 public $volume_units; // scale -3, 0, 3, 6
559
563 public $net_measure;
567 public $net_measure_units; // scale -3, 0, 3, 6
568
572 public $accountancy_code_sell;
576 public $accountancy_code_sell_intra;
580 public $accountancy_code_sell_export;
584 public $accountancy_code_buy;
588 public $accountancy_code_buy_intra;
592 public $accountancy_code_buy_export;
593
597 public $barcode;
598
602 public $barcode_type;
603
607 public $barcode_type_code;
608
612 public $stats_propale = array();
613
617 public $stats_commande = array();
618
622 public $stats_contrat = array();
623
627 public $stats_facture = array();
628
632 public $stats_proposal_supplier = array();
633
637 public $stats_commande_fournisseur = array();
638
642 public $stats_expedition = array();
643
647 public $stats_reception = array();
648
652 public $stats_mo = array();
653
657 public $stats_bom = array();
658
662 public $stats_mrptoconsume = array();
663
667 public $stats_mrptoproduce = array();
668
672 public $stats_facturerec = array();
673
677 public $stats_facture_fournisseur = array();
678
682 public $stats_facturefournrec = array();
683
687 public $imgWidth;
691 public $imgHeight;
692
697 public $product_fourn_id;
698
703 public $product_id_already_linked;
704
709 public $nbphoto = 0;
710
714 public $stock_warehouse = array();
715
719 public $fk_default_warehouse;
720
724 public $fk_price_expression;
725
730 public $fourn_qty;
731
736 public $fourn_pu;
737
742 public $fourn_price_base_type;
743
747 public $fourn_socid;
748
754 public $ref_fourn;
755
759 public $ref_supplier;
760
766 public $fk_unit;
767
773 public $price_autogen = 0;
774
780 public $supplierprices;
781
787 public $sousprods = array();
788
792 public $res;
793
794
800 public $is_object_used;
801
811 public $is_sousproduit_qty;
812
823 public $is_sousproduit_incdec;
824
828 public $mandatory_period;
829
835 public $stockable_product = 1;
836
865 public $fields = array(
866 'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'index' => 1, 'position' => 1, 'comment' => 'Id'),
867 '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'),
868 'entity' => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'visible' => 0, 'default' => '1', 'notnull' => 1, 'index' => 1, 'position' => 5),
869 'label' => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'showoncombobox' => 2, 'position' => 15, 'csslist' => 'tdoverflowmax250'),
870 'barcode' => array('type' => 'varchar(255)', 'label' => 'Barcode', 'enabled' => 'isModEnabled("barcode")', 'position' => 20, 'visible' => -1, 'showoncombobox' => 3, 'cssview' => 'tdwordbreak', 'csslist' => 'tdoverflowmax125'),
871 'fk_barcode_type' => array('type' => 'integer', 'label' => 'BarcodeType', 'enabled' => 1, 'position' => 21, 'notnull' => 0, 'visible' => -1,),
872 'note_public' => array('type' => 'html', 'label' => 'NotePublic', 'enabled' => 1, 'visible' => 0, 'position' => 61),
873 'note' => array('type' => 'html', 'label' => 'NotePrivate', 'enabled' => 1, 'visible' => 0, 'position' => 62),
874 'datec' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 500),
875 'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 501),
876 //'date_valid' =>array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-2, 'position'=>502),
877 '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'),
878 'fk_user_modif' => array('type' => 'integer:User:user/class/user.class.php', 'label' => 'UserModif', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'position' => 511),
879 //'fk_user_valid' =>array('type'=>'integer', 'label'=>'UserValidation', 'enabled'=>1, 'visible'=>-1, 'position'=>512),
880 'localtax1_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax1tx', 'enabled' => 1, 'position' => 150, 'notnull' => 0, 'visible' => -1,),
881 'localtax1_type' => array('type' => 'varchar(10)', 'label' => 'Localtax1type', 'enabled' => 1, 'position' => 155, 'notnull' => 1, 'visible' => -1,),
882 'localtax2_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax2tx', 'enabled' => 1, 'position' => 160, 'notnull' => 0, 'visible' => -1,),
883 'localtax2_type' => array('type' => 'varchar(10)', 'label' => 'Localtax2type', 'enabled' => 1, 'position' => 165, 'notnull' => 1, 'visible' => -1,),
884 'last_main_doc' => array('type' => 'varchar(255)', 'label' => 'LastMainDoc', 'enabled' => 1, 'visible' => -1, 'position' => 170),
885 'import_key' => array('type' => 'varchar(14)', 'label' => 'ImportId', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'index' => 0, 'position' => 1000),
886 //'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')),
887 //'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')),
888 'mandatory_period' => array('type' => 'integer', 'label' => 'mandatoryperiod', 'enabled' => 1, 'visible' => -1, 'notnull' => 1, 'default' => '0', 'index' => 1, 'position' => 1000),
889 'stockable_product' =>array('type' => 'integer', 'label' => 'stockable_product', 'enabled' => 1, 'visible' => 1, 'default' => '1', 'notnull' => 1, 'index' => 1, 'position' => 502),
890 );
891
895 const TYPE_PRODUCT = 0;
899 const TYPE_SERVICE = 1;
900
904 const DISABLED_STOCK = 0;
905 const ENABLED_STOCK = 1;
906
912 public function __construct($db)
913 {
914 $this->db = $db;
915
916 $this->ismultientitymanaged = 1;
917 $this->isextrafieldmanaged = 1;
918
919 $this->canvas = '';
920 }
921
927 public function check()
928 {
929 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
930 $this->ref = trim($this->ref);
931 } else {
932 $this->ref = dol_sanitizeFileName(stripslashes($this->ref));
933 }
934
935 $err = 0;
936 if (dol_strlen(trim($this->ref)) == 0) {
937 $err++;
938 }
939
940 if (dol_strlen(trim($this->label)) == 0) {
941 $err++;
942 }
943
944 if ($err > 0) {
945 return 0;
946 } else {
947 return 1;
948 }
949 }
950
958 public function create($user, $notrigger = 0)
959 {
960 global $conf, $langs;
961
962 $error = 0;
963
964 // Clean parameters
965 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
966 $this->ref = trim($this->ref);
967 } else {
968 $this->ref = dol_sanitizeFileName(dol_string_nospecial(trim($this->ref)));
969 }
970 $this->label = trim($this->label);
971 $this->price_ttc = (float) price2num($this->price_ttc);
972 $this->price = (float) price2num($this->price);
973 $this->price_min_ttc = (float) price2num($this->price_min_ttc);
974 $this->price_min = (float) price2num($this->price_min);
975 $this->price_label = trim($this->price_label);
976 if (empty($this->tva_tx)) {
977 $this->tva_tx = 0;
978 }
979 if (empty($this->tva_npr)) {
980 $this->tva_npr = 0;
981 }
982 // Local taxes
983 if (empty($this->localtax1_tx)) {
984 $this->localtax1_tx = 0;
985 }
986 if (empty($this->localtax2_tx)) {
987 $this->localtax2_tx = 0;
988 }
989 if (empty($this->localtax1_type)) {
990 $this->localtax1_type = '0';
991 }
992 if (empty($this->localtax2_type)) {
993 $this->localtax2_type = '0';
994 }
995 // Price
996 if (empty($this->price_base_type) && getDolGlobalString('PRODUCT_PRICE_BASE_TYPE')) {
997 $this->price_base_type = getDolGlobalString('PRODUCT_PRICE_BASE_TYPE');
998 }
999 if (empty($this->price)) {
1000 $this->price = 0;
1001 }
1002 if (empty($this->price_min)) {
1003 $this->price_min = 0;
1004 }
1005 // Price by quantity
1006 if (empty($this->price_by_qty)) {
1007 $this->price_by_qty = 0;
1008 }
1009
1010 if (empty($this->status)) {
1011 $this->status = 0;
1012 }
1013 if (empty($this->status_buy)) {
1014 $this->status_buy = 0;
1015 }
1016 if (empty($this->stockable_product)) {
1017 $this->stockable_product = 0;
1018 }
1019
1020 $price_ht = 0;
1021 $price_ttc = 0;
1022 $price_min_ht = 0;
1023 $price_min_ttc = 0;
1024
1025 //
1026 if ($this->price_base_type == 'TTC' && $this->price_ttc > 0) {
1027 $price_ttc = price2num($this->price_ttc, 'MU');
1028 $price_ht = price2num($this->price_ttc / (1 + ($this->tva_tx / 100)), 'MU');
1029 }
1030
1031 //
1032 if ($this->price_base_type != 'TTC' && $this->price > 0) {
1033 $price_ht = price2num($this->price, 'MU');
1034 $price_ttc = price2num($this->price * (1 + ($this->tva_tx / 100)), 'MU');
1035 }
1036
1037 //
1038 if (($this->price_min_ttc > 0) && ($this->price_base_type == 'TTC')) {
1039 $price_min_ttc = price2num($this->price_min_ttc, 'MU');
1040 $price_min_ht = price2num($this->price_min_ttc / (1 + ($this->tva_tx / 100)), 'MU');
1041 }
1042
1043 //
1044 if (($this->price_min > 0) && ($this->price_base_type != 'TTC')) {
1045 $price_min_ht = price2num($this->price_min, 'MU');
1046 $price_min_ttc = price2num($this->price_min * (1 + ($this->tva_tx / 100)), 'MU');
1047 }
1048
1049 $this->accountancy_code_buy = trim($this->accountancy_code_buy);
1050 $this->accountancy_code_buy_intra = trim($this->accountancy_code_buy_intra);
1051 $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
1052 $this->accountancy_code_sell = trim($this->accountancy_code_sell);
1053 $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
1054 $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
1055
1056 // Normalize the accountancy codes the way the admin dropdown does it, so an API client that
1057 // sends '606111000' ends up with the same '606111' value the GUI stores (see issue #32343).
1058 // When ACCOUNTING_MANAGE_ZERO is on, trailing zeros are part of the code and must be kept.
1059 if (!getDolGlobalString('ACCOUNTING_MANAGE_ZERO')) {
1060 require_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
1061 $this->accountancy_code_buy = clean_account($this->accountancy_code_buy);
1062 $this->accountancy_code_buy_intra = clean_account($this->accountancy_code_buy_intra);
1063 $this->accountancy_code_buy_export = clean_account($this->accountancy_code_buy_export);
1064 $this->accountancy_code_sell = clean_account($this->accountancy_code_sell);
1065 $this->accountancy_code_sell_intra = clean_account($this->accountancy_code_sell_intra);
1066 $this->accountancy_code_sell_export = clean_account($this->accountancy_code_sell_export);
1067 }
1068
1069 // Barcode value
1070 $this->barcode = trim($this->barcode);
1071 $this->mandatory_period = empty($this->mandatory_period) ? 0 : $this->mandatory_period;
1072 // Check parameters
1073 if (empty($this->label)) {
1074 $langs->load('errors');
1075 $this->errors[] = $langs->trans('ErrorMandatoryParametersNotProvided');
1076 return -1;
1077 }
1078
1079 if (empty($this->ref) || $this->ref == 'auto') {
1080 // Load object modCodeProduct
1081 $module = getDolGlobalString('PRODUCT_CODEPRODUCT_ADDON', 'mod_codeproduct_leopard');
1082 if ($module != 'mod_codeproduct_leopard') { // Do not load module file for leopard
1083 if (substr($module, 0, 16) == 'mod_codeproduct_' && substr($module, -3) == 'php') {
1084 $module = substr($module, 0, dol_strlen($module) - 4);
1085 }
1086 dol_include_once('/core/modules/product/'.$module.'.php');
1087 $modCodeProduct = new $module();
1088 '@phan-var-force ModeleProductCode $modCodeProduct';
1089 if (!empty($modCodeProduct->code_auto)) {
1090 $this->ref = $modCodeProduct->getNextValue($this, $this->type);
1091 }
1092 unset($modCodeProduct);
1093 }
1094
1095 if (empty($this->ref)) {
1096 $this->error = 'ProductModuleNotSetupForAutoRef';
1097 return -2;
1098 }
1099 }
1100
1101 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);
1102
1103 $now = dol_now();
1104
1105 if (empty($this->date_creation)) {
1106 $this->date_creation = $now;
1107 }
1108
1109 $this->db->begin();
1110
1111 // For automatic creation during create action (not used by Dolibarr GUI, can be used by scripts)
1112 if ($this->barcode == '-1' || $this->barcode == 'auto') {
1113 $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1114 }
1115
1116 // Check more parameters
1117 // If error, this->errors[] is filled
1118 $result = $this->verify();
1119
1120 if ($result >= 0) {
1121 $sql = "SELECT count(*) as nb";
1122 $sql .= " FROM ".$this->db->prefix()."product";
1123 $sql .= " WHERE entity IN (".getEntity('product').")";
1124 $sql .= " AND ref = '".$this->db->escape($this->ref)."'";
1125
1126 $result = $this->db->query($sql);
1127 if ($result) {
1128 $obj = $this->db->fetch_object($result);
1129 if ($obj->nb == 0) {
1130 // Insert new product, no previous one found
1131 $sql = "INSERT INTO ".$this->db->prefix()."product (";
1132 $sql .= "datec";
1133 $sql .= ", entity";
1134 $sql .= ", ref";
1135 $sql .= ", ref_ext";
1136 $sql .= ", price_min";
1137 $sql .= ", price_min_ttc";
1138 $sql .= ", label";
1139 $sql .= ", fk_user_author";
1140 $sql .= ", fk_product_type";
1141 $sql .= ", price";
1142 $sql .= ", price_ttc";
1143 $sql .= ", price_base_type";
1144 $sql .= ", price_label";
1145 $sql .= ", tobuy";
1146 $sql .= ", tosell";
1147 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1148 $sql .= ", accountancy_code_buy";
1149 $sql .= ", accountancy_code_buy_intra";
1150 $sql .= ", accountancy_code_buy_export";
1151 $sql .= ", accountancy_code_sell";
1152 $sql .= ", accountancy_code_sell_intra";
1153 $sql .= ", accountancy_code_sell_export";
1154 }
1155 $sql .= ", canvas";
1156 $sql .= ", finished";
1157 $sql .= ", tobatch";
1158 $sql .= ", sell_or_eat_by_mandatory";
1159 $sql .= ", batch_mask";
1160 $sql .= ", fk_unit";
1161 $sql .= ", mandatory_period";
1162 $sql .= ", stockable_product";
1163 if (!empty($this->default_vat_code)) $sql.=", default_vat_code";
1164 $sql .= ") VALUES (";
1165 $sql .= "'".$this->db->idate($this->date_creation)."'";
1166 $sql .= ", ".(!empty($this->entity) ? (int) $this->entity : (int) $conf->entity);
1167 $sql .= ", '".$this->db->escape($this->ref)."'";
1168 $sql .= ", ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
1169 $sql .= ", ".price2num($price_min_ht);
1170 $sql .= ", ".price2num($price_min_ttc);
1171 $sql .= ", ".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null");
1172 $sql .= ", ".((int) $user->id);
1173 $sql .= ", ".((int) $this->type);
1174 $sql .= ", ".price2num($price_ht, 'MT');
1175 $sql .= ", ".price2num($price_ttc, 'MT');
1176 $sql .= ", '".$this->db->escape($this->price_base_type)."'";
1177 $sql .= ", ".(!empty($this->price_label) ? "'".$this->db->escape($this->price_label)."'" : "null");
1178 $sql .= ", ".((int) $this->status);
1179 $sql .= ", ".((int) $this->status_buy);
1180 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1181 $sql .= ", '".$this->db->escape($this->accountancy_code_buy)."'";
1182 $sql .= ", '".$this->db->escape($this->accountancy_code_buy_intra)."'";
1183 $sql .= ", '".$this->db->escape($this->accountancy_code_buy_export)."'";
1184 $sql .= ", '".$this->db->escape($this->accountancy_code_sell)."'";
1185 $sql .= ", '".$this->db->escape($this->accountancy_code_sell_intra)."'";
1186 $sql .= ", '".$this->db->escape($this->accountancy_code_sell_export)."'";
1187 }
1188 $sql .= ", '".$this->db->escape($this->canvas)."'";
1189 $sql .= ", ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? 'NULL' : (int) $this->finished);
1190 $sql .= ", ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : ((int) $this->status_batch));
1191 $sql .= ", ".((empty($this->sell_or_eat_by_mandatory) || $this->sell_or_eat_by_mandatory < 0) ? 0 : ((int) $this->sell_or_eat_by_mandatory));
1192 $sql .= ", '".$this->db->escape($this->batch_mask)."'";
1193 $sql .= ", ".($this->fk_unit > 0 ? ((int) $this->fk_unit) : 'NULL');
1194 $sql .= ", '".$this->db->escape((string) $this->mandatory_period)."'";
1195 $sql .= ", ".((int) $this->stockable_product);
1196 if (!empty($this->default_vat_code)) $sql.=", '".$this->db->escape($this->default_vat_code)."'";
1197 $sql .= ")";
1198 dol_syslog(get_class($this)."::Create", LOG_DEBUG);
1199
1200 $result = $this->db->query($sql);
1201 if ($result) {
1202 $id = $this->db->last_insert_id($this->db->prefix()."product");
1203
1204 if ($id > 0) {
1205 $this->id = $id;
1206 $this->price = $price_ht;
1207 $this->price_ttc = $price_ttc;
1208 $this->price_min = $price_min_ht;
1209 $this->price_min_ttc = $price_min_ttc;
1210
1211 $result = $this->_log_price($user);
1212 if ($result > 0) {
1213 if ($this->update($id, $user, 1, 'add') <= 0) {
1214 $error++;
1215 }
1216 } else {
1217 $error++;
1218 $this->error = $this->db->lasterror();
1219 }
1220
1221 // update accountancy for this entity
1222 if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1223 $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " .((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1224
1225 $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1226 $sql .= " fk_product";
1227 $sql .= ", entity";
1228 $sql .= ", accountancy_code_buy";
1229 $sql .= ", accountancy_code_buy_intra";
1230 $sql .= ", accountancy_code_buy_export";
1231 $sql .= ", accountancy_code_sell";
1232 $sql .= ", accountancy_code_sell_intra";
1233 $sql .= ", accountancy_code_sell_export";
1234 $sql .= ") VALUES (";
1235 $sql .= $this->id;
1236 $sql .= ", " . ((int) $conf->entity);
1237 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1238 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1239 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1240 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1241 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1242 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1243 $sql .= ")";
1244 $result = $this->db->query($sql);
1245 if (!$result) {
1246 $error++;
1247 $this->error = 'ErrorFailedToInsertAccountancyForEntity';
1248 }
1249 }
1250 } else {
1251 $error++;
1252 $this->error = 'ErrorFailedToGetInsertedId';
1253 }
1254 } else {
1255 $error++;
1256 $this->error = $this->db->lasterror();
1257 }
1258 } else {
1259 // Product already exists with this ref
1260 $langs->load("products");
1261 $error++;
1262 $this->error = "ErrorProductAlreadyExists";
1263 dol_syslog(get_class($this)."::Create fails, ref ".$this->ref." already exists");
1264 }
1265 } else {
1266 $error++;
1267 $this->error = $this->db->lasterror();
1268 }
1269
1270 if (!$error && !$notrigger) {
1271 // Call trigger
1272 $result = $this->call_trigger('PRODUCT_CREATE', $user);
1273 if ($result < 0) {
1274 $error++;
1275 }
1276 // End call triggers
1277 }
1278
1279 if (!$error) {
1280 $this->db->commit();
1281 return $this->id;
1282 } else {
1283 $this->db->rollback();
1284 return -$error;
1285 }
1286 } else {
1287 $this->db->rollback();
1288 dol_syslog(get_class($this)."::Create fails verify ".implode(',', $this->errors), LOG_WARNING);
1289 return -3;
1290 }
1291 }
1292
1293
1300 public function verify()
1301 {
1302 global $langs;
1303
1304 $this->errors = array();
1305
1306 $result = 0;
1307 $this->ref = trim($this->ref);
1308
1309 if (!$this->ref) {
1310 $this->errors[] = 'ErrorBadRef';
1311 $result = -2;
1312 }
1313
1314 $arrayofnonnegativevalue = array('weight' => 'Weight', 'width' => 'Width', 'height' => 'Height', 'length' => 'Length', 'surface' => 'Surface', 'volume' => 'Volume');
1315 foreach ($arrayofnonnegativevalue as $key => $value) {
1316 if (property_exists($this, $key) && !empty($this->$key) && ($this->$key < 0)) {
1317 $langs->loadLangs(array("main", "other"));
1318 $this->error = $langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv($value));
1319 $this->errors[] = $this->error;
1320 $result = -4;
1321 }
1322 }
1323
1324 $rescode = $this->check_barcode($this->barcode, $this->barcode_type_code);
1325 if ($rescode) {
1326 if ($rescode == -1) {
1327 $this->errors[] = 'ErrorBadBarCodeSyntax';
1328 } elseif ($rescode == -2) {
1329 $this->errors[] = 'ErrorBarCodeRequired';
1330 } elseif ($rescode == -3) {
1331 // Note: Common usage is to have barcode unique. For variants, we should have a different barcode.
1332 $this->errors[] = 'ErrorBarCodeAlreadyUsed';
1333 }
1334
1335 $result = -3;
1336 }
1337
1338 return $result;
1339 }
1340
1341 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1352 public function check_barcode($valuetotest, $typefortest)
1353 {
1354 // phpcs:enable
1355 global $conf;
1356
1357 if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
1358 $module = strtolower(getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM'));
1359
1360 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
1361 foreach ($dirsociete as $dirroot) {
1362 $res = dol_include_once($dirroot.$module.'.php');
1363 if ($res) {
1364 break;
1365 }
1366 }
1367
1368 $mod = new $module();
1369 '@phan-var-force ModeleNumRefBarCode $mod';
1370
1371 dol_syslog(get_class($this)."::check_barcode value=".$valuetotest." type=".$typefortest." module=".$module);
1372 $result = $mod->verif($this->db, $valuetotest, $this, 0, $typefortest);
1373 return $result;
1374 } else {
1375 return 0;
1376 }
1377 }
1378
1390 public function update($id, $user, $notrigger = 0, $action = 'update', $updatetype = false)
1391 {
1392 global $langs, $conf, $hookmanager;
1393
1394 $error = 0;
1395
1396 // Check parameters
1397 if (!$this->label) {
1398 $this->label = 'MISSING LABEL';
1399 }
1400
1401 // Clean parameters
1402 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
1403 $this->ref = trim($this->ref);
1404 } else {
1405 $this->ref = dol_string_nospecial(trim($this->ref));
1406 }
1407 $this->label = trim($this->label);
1408 $this->description = trim($this->description);
1409 $this->note_private = (isset($this->note_private) ? trim($this->note_private) : null);
1410 $this->note_public = (isset($this->note_public) ? trim($this->note_public) : null);
1411 $this->net_measure = price2num($this->net_measure);
1412 $this->net_measure_units = (!is_numeric($this->net_measure_units) ? null : (int) $this->net_measure_units);
1413 $this->weight = price2num($this->weight);
1414 $this->weight_units = (!is_numeric($this->weight_units) ? null : (int) $this->weight_units);
1415 $this->length = price2num($this->length);
1416 $this->length_units = (!is_numeric($this->length_units) ? null : (int) $this->length_units);
1417 $this->width = price2num($this->width);
1418 $this->width_units = (!is_numeric($this->width_units) ? null : (int) $this->width_units);
1419 $this->height = price2num($this->height);
1420 $this->height_units = (!is_numeric($this->height_units) ? null : (int) $this->height_units);
1421 $this->surface = price2num($this->surface);
1422 $this->surface_units = (!is_numeric($this->surface_units) ? null : (int) $this->surface_units);
1423 $this->volume = price2num($this->volume);
1424 $this->volume_units = (!is_numeric($this->volume_units) ? null : (int) $this->volume_units);
1425
1426 // set unit not defined
1427 if (is_numeric($this->length_units)) {
1428 $this->width_units = $this->length_units; // Not used yet
1429 }
1430 if (is_numeric($this->length_units)) {
1431 $this->height_units = $this->length_units; // Not used yet
1432 }
1433
1434 // Automated compute surface and volume if not filled
1435 if (empty($this->surface) && !empty($this->length) && !empty($this->width) && $this->length_units == $this->width_units) {
1436 $this->surface = (float) $this->length * (float) $this->width;
1437 $this->surface_units = measuring_units_squared((int) $this->length_units);
1438 }
1439 if (empty($this->volume) && !empty($this->surface) && !empty($this->height) && $this->length_units == $this->height_units) {
1440 $this->volume = $this->surface * (float) $this->height;
1441 $this->volume_units = measuring_units_cubed((int) $this->height_units);
1442 }
1443
1444 if (empty($this->tva_tx)) {
1445 $this->tva_tx = 0;
1446 }
1447 if (empty($this->tva_npr)) {
1448 $this->tva_npr = 0;
1449 }
1450 if (empty($this->localtax1_tx)) {
1451 $this->localtax1_tx = 0;
1452 }
1453 if (empty($this->localtax2_tx)) {
1454 $this->localtax2_tx = 0;
1455 }
1456 if (empty($this->localtax1_type)) {
1457 $this->localtax1_type = '0';
1458 }
1459 if (empty($this->localtax2_type)) {
1460 $this->localtax2_type = '0';
1461 }
1462 if (empty($this->status)) {
1463 $this->status = 0;
1464 }
1465 if (empty($this->status_buy)) {
1466 $this->status_buy = 0;
1467 }
1468
1469 if (empty($this->country_id)) {
1470 $this->country_id = 0;
1471 }
1472 if (empty($this->country_id) && !empty($this->country_code)) {
1473 $country_id = getCountry($this->country_code, '3');
1474 $this->country_id = is_int($country_id) ? $country_id : 0;
1475 }
1476
1477 if (empty($this->state_id)) {
1478 $this->state_id = 0;
1479 }
1480
1481 if (empty($this->stockable_product)) {
1482 $this->stockable_product = 0;
1483 }
1484
1485 // Barcode value
1486 $this->barcode = (empty($this->barcode) ? '' : trim($this->barcode));
1487
1488 $this->accountancy_code_buy = trim($this->accountancy_code_buy);
1489 $this->accountancy_code_buy_intra = (!empty($this->accountancy_code_buy_intra) ? trim($this->accountancy_code_buy_intra) : '');
1490 $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
1491 $this->accountancy_code_sell = trim($this->accountancy_code_sell);
1492 $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
1493 $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
1494
1495 // Normalize the accountancy codes the way the admin dropdown does it, so an API client that
1496 // sends '606111000' ends up with the same '606111' value the GUI stores (see issue #32343).
1497 // When ACCOUNTING_MANAGE_ZERO is on, trailing zeros are part of the code and must be kept.
1498 if (!getDolGlobalString('ACCOUNTING_MANAGE_ZERO')) {
1499 require_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
1500 $this->accountancy_code_buy = clean_account($this->accountancy_code_buy);
1501 $this->accountancy_code_buy_intra = clean_account($this->accountancy_code_buy_intra);
1502 $this->accountancy_code_buy_export = clean_account($this->accountancy_code_buy_export);
1503 $this->accountancy_code_sell = clean_account($this->accountancy_code_sell);
1504 $this->accountancy_code_sell_intra = clean_account($this->accountancy_code_sell_intra);
1505 $this->accountancy_code_sell_export = clean_account($this->accountancy_code_sell_export);
1506 }
1507
1508 $this->db->begin();
1509
1510 $result = 0;
1511 // Check name is required and codes are ok or unique. If error, this->errors[] is filled
1512 if ($action != 'add') {
1513 $result = $this->verify(); // We don't check when update called during a create because verify was already done
1514 } else {
1515 // we can continue
1516 $result = 0;
1517 }
1518
1519 if ($result >= 0) {
1520 // $this->oldcopy should have been set by the caller of update (here properties were already modified)
1521 // Note that this->oldcopy must be object and not stdClass, if not the method hasbatch() will not work.
1522 if (is_null($this->oldcopy) || (is_object($this->oldcopy) && empty($this->oldcopy->id))) {
1523 $this->oldcopy = dol_clone($this, 1); // 1 to clone with methods to avoid fatal error with $this->oldcopy->hasbatch()
1524 }
1525 // Test if batch management is activated on existing product
1526 // If yes, we create missing entries into product_batch
1527 if ($this->hasbatch() && !$this->oldcopy->hasbatch()) {
1528 //$valueforundefinedlot = 'Undefined'; // In previous version, 39 and lower
1529 $valueforundefinedlot = '000000';
1530 if (getDolGlobalString('STOCK_DEFAULT_BATCH')) {
1531 $valueforundefinedlot = getDolGlobalString('STOCK_DEFAULT_BATCH');
1532 }
1533
1534 dol_syslog("Flag batch of product id=".$this->id." is set to ON, so we will create missing records into product_batch");
1535
1536 $this->load_stock();
1537 foreach ($this->stock_warehouse as $idW => $ObjW) { // For each warehouse where we have stocks defined for this product (for each lines in product_stock)
1538 $qty_batch = 0;
1539 foreach ($ObjW->detail_batch as $detail) { // Each lines of detail in product_batch of the current $ObjW = product_stock
1540 if ($detail->batch == $valueforundefinedlot || $detail->batch == 'Undefined') {
1541 // We discard this line, we will create it later
1542 $sqlclean = "DELETE FROM ".$this->db->prefix()."product_batch WHERE batch in('Undefined', '".$this->db->escape($valueforundefinedlot)."') AND fk_product_stock = ".((int) $ObjW->id);
1543 $result = $this->db->query($sqlclean);
1544 if (!$result) {
1545 dol_print_error($this->db);
1546 exit;
1547 }
1548 continue;
1549 }
1550
1551 $qty_batch += $detail->qty;
1552 }
1553 // Quantities in batch details are not same as stock quantity,
1554 // so we add a default batch record to complete and get same qty in parent and child table
1555 if ($ObjW->real != $qty_batch) {
1556 $ObjBatch = new Productbatch($this->db);
1557 $ObjBatch->batch = $valueforundefinedlot;
1558 $ObjBatch->qty = ($ObjW->real - $qty_batch);
1559 $ObjBatch->fk_product_stock = (int) $ObjW->id;
1560
1561 if ($ObjBatch->create($user, 1) < 0) {
1562 $error++;
1563 $this->errors = $ObjBatch->errors;
1564 } else {
1565 // we also add lot record if not exist
1566 $ObjLot = new Productlot($this->db);
1567 // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1568 if ($ObjLot->fetch(0, $this->id, $valueforundefinedlot) == 0) {
1569 $ObjLot->fk_product = $this->id;
1570 $ObjLot->entity = (int) $this->entity;
1571 $ObjLot->fk_user_creat = $user->id;
1572 $ObjLot->batch = $valueforundefinedlot;
1573 if ($ObjLot->create($user, 1) < 0) {
1574 $error++;
1575 $this->errors = $ObjLot->errors;
1576 }
1577 }
1578 }
1579 }
1580 }
1581 }
1582
1583 // For automatic creation
1584 if ($this->barcode == -1) {
1585 $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1586 }
1587
1588 $sql = "UPDATE ".$this->db->prefix()."product";
1589 $sql .= " SET label = '".$this->db->escape($this->label)."'";
1590
1591 if ($updatetype && ($this->isProduct() || $this->isService())) {
1592 $sql .= ", fk_product_type = ".((int) $this->type);
1593 }
1594
1595 $sql .= ", ref = '".$this->db->escape($this->ref)."'";
1596 $sql .= ", ref_ext = ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
1597 $sql .= ", default_vat_code = ".($this->default_vat_code ? "'".$this->db->escape($this->default_vat_code)."'" : "null");
1598 $sql .= ", tva_tx = ".((float) $this->tva_tx);
1599 $sql .= ", recuperableonly = ".((int) $this->tva_npr);
1600 $sql .= ", localtax1_tx = ".((float) $this->localtax1_tx);
1601 $sql .= ", localtax2_tx = ".((float) $this->localtax2_tx);
1602 $sql .= ", localtax1_type = ".($this->localtax1_type != '' ? "'".$this->db->escape($this->localtax1_type)."'" : "'0'");
1603 $sql .= ", localtax2_type = ".($this->localtax2_type != '' ? "'".$this->db->escape($this->localtax2_type)."'" : "'0'");
1604
1605 $sql .= ", barcode = ".(empty($this->barcode) ? "null" : "'".$this->db->escape($this->barcode)."'");
1606 $sql .= ", fk_barcode_type = ".(empty($this->barcode_type) ? "null" : $this->db->escape((string) $this->barcode_type));
1607
1608 $sql .= ", tosell = ".(int) $this->status;
1609 $sql .= ", tobuy = ".(int) $this->status_buy;
1610 $sql .= ", tobatch = ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : (int) $this->status_batch);
1611 $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);
1612 $sql .= ", batch_mask = '".$this->db->escape($this->batch_mask)."'";
1613
1614 $sql .= ", finished = ".((!isset($this->finished) || $this->finished < 0 || $this->finished === '') ? "null" : (int) $this->finished);
1615 $sql .= ", fk_default_bom = ".((!isset($this->fk_default_bom) || $this->fk_default_bom < 0 || $this->fk_default_bom == '') ? "null" : (int) $this->fk_default_bom);
1616 $sql .= ", net_measure = ".($this->net_measure != '' ? "'".$this->db->escape($this->net_measure)."'" : 'null');
1617 $sql .= ", net_measure_units = ".((string) $this->net_measure_units != '' ? ((int) $this->net_measure_units) : 'null');
1618 $sql .= ", weight = ".($this->weight != '' ? "'".$this->db->escape($this->weight)."'" : 'null');
1619 $sql .= ", weight_units = ".((string) $this->weight_units != '' ? ((int) $this->weight_units) : 'null');
1620 $sql .= ", length = ".($this->length != '' ? "'".$this->db->escape($this->length)."'" : 'null');
1621 $sql .= ", length_units = ".((string) $this->length_units != '' ? ((int) $this->length_units) : 'null');
1622 $sql .= ", width= ".($this->width != '' ? "'".$this->db->escape($this->width)."'" : 'null');
1623 $sql .= ", width_units = ".((string) $this->width_units != '' ? ((int) $this->width_units) : 'null');
1624 $sql .= ", height = ".($this->height != '' ? "'".$this->db->escape($this->height)."'" : 'null');
1625 $sql .= ", height_units = ".((string) $this->height_units != '' ? ((int) $this->height_units) : 'null');
1626 $sql .= ", surface = ".($this->surface != '' ? "'".$this->db->escape($this->surface)."'" : 'null');
1627 $sql .= ", surface_units = ".((string) $this->surface_units != '' ? ((int) $this->surface_units) : 'null');
1628 $sql .= ", volume = ".($this->volume != '' ? "'".$this->db->escape($this->volume)."'" : 'null');
1629 $sql .= ", volume_units = ".((string) $this->volume_units != '' ? ((int) $this->volume_units) : 'null');
1630 $sql .= ", fk_default_warehouse = ".($this->fk_default_warehouse > 0 ? ((int) $this->fk_default_warehouse) : 'null');
1631 $sql .= ", fk_default_workstation = ".($this->fk_default_workstation > 0 ? ((int) $this->fk_default_workstation) : 'null');
1632 $sql .= ", seuil_stock_alerte = ".((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (float) $this->seuil_stock_alerte : 'null');
1633 $sql .= ", description = '".$this->db->escape($this->description)."'";
1634 $sql .= ", url = ".($this->url ? "'".$this->db->escape($this->url)."'" : 'null');
1635 $sql .= ", customcode = '".$this->db->escape($this->customcode)."'";
1636 $sql .= ", fk_country = ".($this->country_id > 0 ? (int) $this->country_id : 'null');
1637 $sql .= ", fk_state = ".($this->state_id > 0 ? (int) $this->state_id : 'null');
1638 $sql .= ", lifetime = ".($this->lifetime > 0 ? (int) $this->lifetime : 'null');
1639 $sql .= ", qc_frequency = ".($this->qc_frequency > 0 ? (int) $this->qc_frequency : 'null');
1640 $sql .= ", note = ".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : 'null');
1641 $sql .= ", note_public = ".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : 'null');
1642 $sql .= ", duration = '".$this->db->escape($this->duration_value.$this->duration_unit)."'";
1643 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1644 $sql .= ", accountancy_code_buy = '" . $this->db->escape($this->accountancy_code_buy) . "'";
1645 $sql .= ", accountancy_code_buy_intra = '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1646 $sql .= ", accountancy_code_buy_export = '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1647 $sql .= ", accountancy_code_sell= '" . $this->db->escape($this->accountancy_code_sell) . "'";
1648 $sql .= ", accountancy_code_sell_intra= '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1649 $sql .= ", accountancy_code_sell_export= '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1650 }
1651 $sql .= ", desiredstock = ".((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (float) $this->desiredstock : "null");
1652 $sql .= ", cost_price = ".($this->cost_price != '' ? ((float) $this->cost_price) : 'null');
1653 $sql .= ", fk_unit= ".(!$this->fk_unit ? 'NULL' : (int) $this->fk_unit);
1654 $sql .= ", price_autogen = ".(!$this->price_autogen ? 0 : 1);
1655 $sql .= ", fk_price_expression = ".($this->fk_price_expression != 0 ? (int) $this->fk_price_expression : 'NULL');
1656 $sql .= ", fk_user_modif = ".($user->id > 0 ? (int) $user->id : 'NULL');
1657 $sql .= ", mandatory_period = ".((int) $this->mandatory_period);
1658 $sql .= ", stockable_product = ".(int) $this->stockable_product;
1659 if (getDolGlobalString('PRODUCT_USE_CUSTOMER_PACKAGING')) {
1660 $sql .= ", packaging = ".(float) $this->packaging;
1661 }
1662
1663 // stock field is not here because it is a denormalized value from product_stock.
1664 $sql .= " WHERE rowid = ".((int) $id);
1665
1666 dol_syslog(get_class($this)."::update", LOG_DEBUG);
1667
1668 $resql = $this->db->query($sql);
1669 if ($resql) {
1670 $this->id = $id;
1671
1672 // Multilangs
1673 if (getDolGlobalInt('MAIN_MULTILANGS')) {
1674 if ($this->setMultiLangs($user) < 0) {
1675 $this->db->rollback();
1676 return -2;
1677 }
1678 }
1679
1680 $action = 'update';
1681
1682 // update accountancy for this entity
1683 if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1684 $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1685
1686 $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1687 $sql .= " fk_product";
1688 $sql .= ", entity";
1689 $sql .= ", accountancy_code_buy";
1690 $sql .= ", accountancy_code_buy_intra";
1691 $sql .= ", accountancy_code_buy_export";
1692 $sql .= ", accountancy_code_sell";
1693 $sql .= ", accountancy_code_sell_intra";
1694 $sql .= ", accountancy_code_sell_export";
1695 $sql .= ") VALUES (";
1696 $sql .= ((int) $this->id);
1697 $sql .= ", " . ((int) $conf->entity);
1698 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1699 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1700 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1701 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1702 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1703 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1704 $sql .= ")";
1705 $result = $this->db->query($sql);
1706 if (!$result) {
1707 $error++;
1708 $this->error = 'ErrorFailedToUpdateAccountancyForEntity';
1709 }
1710 }
1711
1712 if (!$this->hasbatch() && $this->oldcopy->hasbatch()) {
1713 // Selection of all product stock movements that contains batchs
1714 $sql = 'SELECT pb.qty, ps.fk_entrepot, pb.batch FROM '.MAIN_DB_PREFIX.'product_batch as pb';
1715 $sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'product_stock as ps ON (ps.rowid = pb.fk_product_stock)';
1716 $sql .= ' WHERE ps.fk_product = '.(int) $this->id;
1717
1718 $resql = $this->db->query($sql);
1719 if ($resql) {
1720 $inventorycode = dol_print_date(dol_now(), '%Y%m%d%H%M%S');
1721
1722 while ($obj = $this->db->fetch_object($resql)) {
1723 $value = $obj->qty;
1724 $fk_entrepot = $obj->fk_entrepot;
1725 $price = 0;
1726 $dlc = '';
1727 $dluo = '';
1728 $batch = $obj->batch;
1729
1730 // To know how to revert stockMouvement (add or remove)
1731 $addOremove = $value > 0 ? 1 : 0; // 1 if remove, 0 if add
1732 $label = $langs->trans('BatchStockMouvementAddInGlobal');
1733 $res = $this->correct_stock_batch($user, $fk_entrepot, abs($value), $addOremove, $label, $price, $dlc, $dluo, $batch, $inventorycode, '', null, 0, null, true);
1734
1735 if ($res > 0) {
1736 $label = $langs->trans('BatchStockMouvementAddInGlobal');
1737 $res = $this->correct_stock($user, $fk_entrepot, abs($value), (int) empty($addOremove), $label, $price, $inventorycode, '', null, 0);
1738 if ($res < 0) {
1739 $error++;
1740 }
1741 } else {
1742 $error++;
1743 }
1744 }
1745 }
1746 }
1747
1748 // Actions on extra fields
1749 if (!$error) {
1750 $result = $this->insertExtraFields();
1751 if ($result < 0) {
1752 $error++;
1753 }
1754 }
1755
1756 if (!$error && !$notrigger) {
1757 // Call trigger
1758 $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1759 if ($result < 0) {
1760 $error++;
1761 }
1762 // End call triggers
1763 }
1764
1765 if (!$error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref)) {
1766 // We remove directory
1767 if ($conf->product->dir_output) {
1768 $olddir = $conf->product->dir_output."/".dol_sanitizeFileName($this->oldcopy->ref);
1769 $newdir = $conf->product->dir_output."/".dol_sanitizeFileName($this->ref);
1770 if (file_exists($olddir)) {
1771 // include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1772 // $res = dol_move($olddir, $newdir);
1773 // do not use dol_move with directory
1774 $res = @rename($olddir, $newdir);
1775 if (!$res) {
1776 $langs->load("errors");
1777 $this->error = $langs->trans('ErrorFailToRenameDir', $olddir, $newdir);
1778 $error++;
1779 } else {
1780 // to keep old entries with the new dir
1781 require_once DOL_DOCUMENT_ROOT . '/ecm/class/ecmfiles.class.php';
1782 $ecmfiles = new EcmFiles($this->db);
1783 $ecmfiles->updateAfterRename("produit/".dol_sanitizeFileName($this->oldcopy->ref), "produit/".dol_sanitizeFileName($this->ref));
1784 }
1785 }
1786 }
1787 }
1788
1789 if (!$error) {
1790 if (isModEnabled('variants')) {
1791 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1792
1793 $comb = new ProductCombination($this->db);
1794
1795 foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) {
1796 $currcomb->updateProperties($this, $user);
1797 }
1798 }
1799
1800 $this->db->commit();
1801 return 1;
1802 } else {
1803 $this->db->rollback();
1804 return -$error;
1805 }
1806 } else {
1807 if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1808 $langs->load("errors");
1809 if (empty($conf->barcode->enabled) || empty($this->barcode)) {
1810 $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductAlreadyExists", $this->ref);
1811 } else {
1812 $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductBarCodeAlreadyExists", $this->barcode);
1813 }
1814 $this->errors[] = $this->error;
1815 $this->db->rollback();
1816 return -1;
1817 } else {
1818 $this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1819 $this->errors[] = $this->error;
1820 $this->db->rollback();
1821 return -2;
1822 }
1823 }
1824 } else {
1825 $this->db->rollback();
1826 dol_syslog(get_class($this)."::Update fails verify ".implode(',', $this->errors), LOG_WARNING);
1827 return -3;
1828 }
1829 }
1830
1838 public function delete(User $user, $notrigger = 0)
1839 {
1840 global $conf;
1841 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1842
1843 $error = 0;
1844
1845 // Check parameters
1846 if (empty($this->id)) {
1847 $this->error = "Object must be fetched before calling delete";
1848 return -1;
1849 }
1850 if (($this->isProduct() && !$user->hasRight('produit', 'supprimer')) || ($this->isService() && !$user->hasRight('service', 'supprimer'))) {
1851 $this->error = "ErrorForbidden";
1852 return 0;
1853 }
1854
1855 $objectisused = $this->isObjectUsed($this->id);
1856 if (empty($objectisused)) {
1857 $this->db->begin();
1858
1859 if (!$error && empty($notrigger)) {
1860 // Call trigger
1861 $result = $this->call_trigger('PRODUCT_DELETE', $user);
1862 if ($result < 0) {
1863 $error++;
1864 }
1865 // End call triggers
1866 }
1867
1868 // Delete from product_batch on product delete
1869 if (!$error) {
1870 $sql = "DELETE FROM ".$this->db->prefix().'product_batch';
1871 $sql .= " WHERE fk_product_stock IN (";
1872 $sql .= "SELECT rowid FROM ".$this->db->prefix().'product_stock';
1873 $sql .= " WHERE fk_product = ".((int) $this->id).")";
1874
1875 $result = $this->db->query($sql);
1876 if (!$result) {
1877 $error++;
1878 $this->errors[] = $this->db->lasterror();
1879 }
1880 }
1881
1882 // Delete all child tables
1883 if (!$error) {
1884 $elements = array('product_fournisseur_price', 'product_price', 'product_lang', 'categorie_product', 'product_stock', 'product_customer_price', 'product_lot'); // product_batch is done before
1885 foreach ($elements as $table) {
1886 if (!$error) {
1887 $sql = "DELETE FROM ".$this->db->prefix().$table;
1888 $sql .= " WHERE fk_product = ".(int) $this->id;
1889
1890 $result = $this->db->query($sql);
1891 if (!$result) {
1892 $error++;
1893 $this->errors[] = $this->db->lasterror();
1894 }
1895 }
1896 }
1897 }
1898
1899 if (!$error) {
1900 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1901 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
1902
1903 //If it is a parent product, then we remove the association with child products
1904 $prodcomb = new ProductCombination($this->db);
1905
1906 if ($prodcomb->deleteByFkProductParent($user, $this->id) < 0) {
1907 $error++;
1908 $this->errors[] = 'Error deleting combinations';
1909 }
1910
1911 //We also check if it is a child product
1912 if (!$error && ($prodcomb->fetchByFkProductChild($this->id) > 0) && ($prodcomb->delete($user) < 0)) {
1913 $error++;
1914 $this->errors[] = 'Error deleting child combination';
1915 }
1916 }
1917
1918 // Delete from product_association
1919 if (!$error) {
1920 $sql = "DELETE FROM ".$this->db->prefix()."product_association";
1921 $sql .= " WHERE fk_product_pere = ".(int) $this->id." OR fk_product_fils = ".(int) $this->id;
1922
1923 $result = $this->db->query($sql);
1924 if (!$result) {
1925 $error++;
1926 $this->errors[] = $this->db->lasterror();
1927 }
1928 }
1929
1930 // Remove extrafields
1931 if (!$error) {
1932 $result = $this->deleteExtraFields();
1933 if ($result < 0) {
1934 $error++;
1935 dol_syslog(get_class($this)."::delete error -4 ".$this->error, LOG_ERR);
1936 }
1937 }
1938
1939 // Delete product
1940 if (!$error) {
1941 $sqlz = "DELETE FROM ".$this->db->prefix()."product";
1942 $sqlz .= " WHERE rowid = ".(int) $this->id;
1943
1944 $resultz = $this->db->query($sqlz);
1945 if (!$resultz) {
1946 $error++;
1947 $this->errors[] = $this->db->lasterror();
1948 }
1949 }
1950
1951 // Delete record into ECM index and physically
1952 if (!$error) {
1953 $res = $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
1954 $res = $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
1955 if (!$res) {
1956 $error++;
1957 }
1958 }
1959
1960 if (!$error) {
1961 // We remove directory
1962 $ref = dol_sanitizeFileName($this->ref);
1963 if ($conf->product->dir_output) {
1964 $dir = $conf->product->dir_output."/".$ref;
1965 if (file_exists($dir)) {
1966 $res = @dol_delete_dir_recursive($dir);
1967 if (!$res) {
1968 $this->errors[] = 'ErrorFailToDeleteDir';
1969 $error++;
1970 }
1971 }
1972 }
1973 }
1974
1975 if (!$error) {
1976 $this->db->commit();
1977 return 1;
1978 } else {
1979 foreach ($this->errors as $errmsg) {
1980 dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
1981 $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
1982 }
1983 $this->db->rollback();
1984 return -$error;
1985 }
1986 } else {
1987 $this->error = "ErrorRecordIsUsedCantDelete";
1988 return 0;
1989 }
1990 }
1991
1997 public static function getSellOrEatByMandatoryList()
1998 {
1999 global $langs;
2000
2001 $sellByLabel = $langs->trans('SellByDate');
2002 $eatByLabel = $langs->trans('EatByDate');
2003 return array(
2004 self::SELL_OR_EAT_BY_MANDATORY_ID_NONE => $langs->trans('BatchSellOrEatByMandatoryNone'),
2005 self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY => $sellByLabel,
2006 self::SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY => $eatByLabel,
2007 self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT => $langs->trans('BatchSellOrEatByMandatoryAll', $sellByLabel, $eatByLabel),
2008 );
2009 }
2010
2017 {
2018 $sellOrEatByMandatoryLabel = '';
2019
2020 $sellOrEatByMandatoryList = self::getSellOrEatByMandatoryList();
2021 if (isset($sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory])) {
2022 $sellOrEatByMandatoryLabel = $sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory];
2023 }
2024
2025 return $sellOrEatByMandatoryLabel;
2026 }
2027
2034 public function setMultiLangs($user)
2035 {
2036 global $langs;
2037
2038 $langs_available = $langs->get_available_languages(DOL_DOCUMENT_ROOT, 0, 2);
2039 $current_lang = $langs->getDefaultLang();
2040
2041 foreach ($langs_available as $key => $value) {
2042 if ($key == $current_lang) {
2043 $sql = "SELECT rowid";
2044 $sql .= " FROM ".$this->db->prefix()."product_lang";
2045 $sql .= " WHERE fk_product = ".((int) $this->id);
2046 $sql .= " AND lang = '".$this->db->escape($key)."'";
2047
2048 $result = $this->db->query($sql);
2049
2050 if ($this->db->num_rows($result)) { // if there is already a description line for this language
2051 $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
2052 $sql2 .= " SET ";
2053 $sql2 .= " label='".$this->db->escape($this->label)."',";
2054 $sql2 .= " description='".$this->db->escape($this->description)."'";
2055 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2056 $sql2 .= ", note='".$this->db->escape($this->other)."'";
2057 }
2058 $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
2059 } else {
2060 $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
2061 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2062 $sql2 .= ", note";
2063 }
2064 $sql2 .= ")";
2065 $sql2 .= " VALUES(".((int) $this->id).",'".$this->db->escape($key)."','".$this->db->escape($this->label)."',";
2066 $sql2 .= " '".$this->db->escape($this->description)."'";
2067 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2068 $sql2 .= ", '".$this->db->escape($this->other)."'";
2069 }
2070 $sql2 .= ")";
2071 }
2072 dol_syslog(get_class($this).'::setMultiLangs key = current_lang = '.$key);
2073 if (!$this->db->query($sql2)) {
2074 $this->error = $this->db->lasterror();
2075 return -1;
2076 }
2077 } elseif (isset($this->multilangs[$key])) {
2078 if (empty($this->multilangs[$key]["label"])) {
2079 $this->errors[] = $key . ' : ' . $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Label"));
2080 return -1;
2081 }
2082
2083 $sql = "SELECT rowid";
2084 $sql .= " FROM ".$this->db->prefix()."product_lang";
2085 $sql .= " WHERE fk_product = ".((int) $this->id);
2086 $sql .= " AND lang = '".$this->db->escape($key)."'";
2087
2088 $result = $this->db->query($sql);
2089
2090 if ($this->db->num_rows($result)) { // if there is already a description line for this language
2091 $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
2092 $sql2 .= " SET ";
2093 $sql2 .= " label = '".$this->db->escape($this->multilangs["$key"]["label"])."',";
2094 $sql2 .= " description = '".$this->db->escape($this->multilangs["$key"]["description"])."'";
2095 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2096 // @phan-suppress-next-line PhanTypeInvalidDimOffset
2097 $sql2 .= ", note = '".$this->db->escape($this->multilangs["$key"]["other"])."'";
2098 }
2099 $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
2100 } else {
2101 $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
2102 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2103 $sql2 .= ", note";
2104 }
2105 $sql2 .= ")";
2106 $sql2 .= " VALUES(".((int) $this->id).",'".$this->db->escape($key)."','".$this->db->escape($this->multilangs["$key"]["label"])."',";
2107 $sql2 .= " '".$this->db->escape($this->multilangs["$key"]["description"])."'";
2108 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
2109 // @phan-suppress-next-line PhanTypeInvalidDimOffset
2110 $sql2 .= ", '".$this->db->escape($this->multilangs["$key"]["other"])."'";
2111 }
2112 $sql2 .= ")";
2113 }
2114
2115 // We do not save if main fields are empty
2116 if ($this->multilangs["$key"]["label"] || $this->multilangs["$key"]["description"]) {
2117 if (!$this->db->query($sql2)) {
2118 $this->error = $this->db->lasterror();
2119 return -1;
2120 }
2121 }
2122 } else {
2123 // language is not current language and we didn't provide a multilang description for this language
2124 }
2125 }
2126
2127 // Call trigger
2128 $result = $this->call_trigger('PRODUCT_SET_MULTILANGS', $user);
2129 if ($result < 0) {
2130 $this->error = $this->db->lasterror();
2131 return -1;
2132 }
2133 // End call triggers
2134
2135 return 1;
2136 }
2137
2146 public function delMultiLangs($langtodelete, $user)
2147 {
2148 $sql = "DELETE FROM ".$this->db->prefix()."product_lang";
2149 $sql .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($langtodelete)."'";
2150
2151 dol_syslog(get_class($this).'::delMultiLangs', LOG_DEBUG);
2152 $result = $this->db->query($sql);
2153 if ($result) {
2154 // Call trigger
2155 $result = $this->call_trigger('PRODUCT_DEL_MULTILANGS', $user);
2156 if ($result < 0) {
2157 $this->error = $this->db->lasterror();
2158 dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
2159 return -1;
2160 }
2161 // End call triggers
2162 unset($this->multilangs[$langtodelete]);
2163 return 1;
2164 } else {
2165 $this->error = $this->db->lasterror();
2166 dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
2167 return -1;
2168 }
2169 }
2170
2179 public function setAccountancyCode($type, $value)
2180 {
2181 global $user;
2182
2183 $error = 0;
2184
2185 $this->db->begin();
2186
2187 if ($type == 'buy') {
2188 $field = 'accountancy_code_buy';
2189 } elseif ($type == 'buy_intra') {
2190 $field = 'accountancy_code_buy_intra';
2191 } elseif ($type == 'buy_export') {
2192 $field = 'accountancy_code_buy_export';
2193 } elseif ($type == 'sell') {
2194 $field = 'accountancy_code_sell';
2195 } elseif ($type == 'sell_intra') {
2196 $field = 'accountancy_code_sell_intra';
2197 } elseif ($type == 'sell_export') {
2198 $field = 'accountancy_code_sell_export';
2199 } else {
2200 return -1;
2201 }
2202
2203 $sql = "UPDATE ".$this->db->prefix().$this->table_element." SET ";
2204 $sql .= "$field = '".$this->db->escape($value)."'";
2205 $sql .= " WHERE rowid = ".((int) $this->id);
2206
2207 dol_syslog(__METHOD__, LOG_DEBUG);
2208 $resql = $this->db->query($sql);
2209
2210 if ($resql) {
2211 // Call trigger
2212 $result = $this->call_trigger('PRODUCT_MODIFY', $user);
2213 if ($result < 0) {
2214 $error++;
2215 }
2216 // End call triggers
2217
2218 if ($error) {
2219 $this->db->rollback();
2220 return -1;
2221 }
2222
2223 $this->$field = $value;
2224
2225 $this->db->commit();
2226 return 1;
2227 } else {
2228 $this->error = $this->db->lasterror();
2229 $this->db->rollback();
2230 return -1;
2231 }
2232 }
2233
2239 public function getMultiLangs()
2240 {
2241 global $langs;
2242
2243 $current_lang = $langs->getDefaultLang();
2244
2245 $sql = "SELECT lang, label, description, note as other";
2246 $sql .= " FROM ".$this->db->prefix()."product_lang";
2247 $sql .= " WHERE fk_product = ".((int) $this->id);
2248
2249 $result = $this->db->query($sql);
2250 if ($result) {
2251 while ($obj = $this->db->fetch_object($result)) {
2252 //print 'lang='.$obj->lang.' current='.$current_lang.'<br>';
2253 if ($obj->lang == $current_lang) { // si on a les traduct. dans la langue courante on les charge en infos principales.
2254 $this->label = $obj->label;
2255 $this->description = $obj->description;
2256 $this->other = $obj->other;
2257 }
2258 $this->multilangs[(string) $obj->lang]["label"] = $obj->label;
2259 $this->multilangs[(string) $obj->lang]["description"] = $obj->description;
2260 $this->multilangs[(string) $obj->lang]["other"] = $obj->other;
2261 }
2262 return 1;
2263 } else {
2264 $this->error = "Error: ".$this->db->lasterror()." - ".$sql;
2265 return -1;
2266 }
2267 }
2268
2275 private function getArrayForPriceCompare($level = 0)
2276 {
2277 $testExit = array('multiprices','multiprices_ttc','multiprices_base_type','multiprices_min','multiprices_min_ttc','multiprices_tva_tx','multiprices_recuperableonly');
2278
2279 foreach ($testExit as $field) {
2280 if (!isset($this->$field)) {
2281 return array();
2282 }
2283 $tmparray = $this->$field;
2284 if (!isset($tmparray[$level])) {
2285 return array();
2286 }
2287 }
2288
2289 $lastPrice = array(
2290 'level' => $level ? $level : 1,
2291 'multiprices' => (float) $this->multiprices[$level],
2292 'multiprices_ttc' => (float) $this->multiprices_ttc[$level],
2293 'multiprices_base_type' => $this->multiprices_base_type[$level],
2294 'multiprices_min' => (float) $this->multiprices_min[$level],
2295 'multiprices_min_ttc' => (float) $this->multiprices_min_ttc[$level],
2296 'multiprices_tva_tx' => (float) $this->multiprices_tva_tx[$level],
2297 'multiprices_recuperableonly' => (float) $this->multiprices_recuperableonly[$level],
2298 );
2299
2300 return $lastPrice;
2301 }
2302
2303
2304 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2312 private function _log_price($user, $level = 0)
2313 {
2314 // phpcs:enable
2315 global $conf;
2316
2317 $now = dol_now();
2318
2319 // Clean parameters
2320 if (empty($this->price_by_qty)) {
2321 $this->price_by_qty = 0;
2322 }
2323
2324 // Add new price
2325 $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,";
2326 $sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, price_min,price_min_ttc,price_by_qty,entity,fk_price_expression) ";
2327 $sql .= " VALUES(".($level ? ((int) $level) : 1).", '".$this->db->idate($now)."', ".((int) $this->id).", ".((int) $user->id).", ".(empty($this->price_label) ? "null" : "'".$this->db->escape($this->price_label)."'").", ".((float) price2num($this->price)).", ".((float) price2num($this->price_ttc)).",'".$this->db->escape($this->price_base_type)."',".((int) $this->status).", ".((float) price2num($this->tva_tx)).", ".($this->default_vat_code ? ("'".$this->db->escape($this->default_vat_code)."'") : "null").", ".((int) $this->tva_npr).",";
2328 $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');
2329 $sql .= ")";
2330
2331 dol_syslog(get_class($this)."::_log_price", LOG_DEBUG);
2332 $resql = $this->db->query($sql);
2333 if (!$resql) {
2334 $this->error = $this->db->lasterror();
2335 dol_print_error($this->db);
2336 return -1;
2337 } else {
2338 return 1;
2339 }
2340 }
2341
2342
2343 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2351 public function log_price_delete($user, $rowid)
2352 {
2353 // phpcs:enable
2354 $sql = "DELETE FROM ".$this->db->prefix()."product_price_by_qty";
2355 $sql .= " WHERE fk_product_price = ".((int) $rowid);
2356 $resql = $this->db->query($sql);
2357
2358 $sql = "DELETE FROM ".$this->db->prefix()."product_price";
2359 $sql .= " WHERE rowid=".((int) $rowid);
2360 $resql = $this->db->query($sql);
2361 if ($resql) {
2362 return 1;
2363 } else {
2364 $this->error = $this->db->lasterror();
2365 return -1;
2366 }
2367 }
2368
2369
2379 public function getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp = 0)
2380 {
2381 global $hookmanager, $action;
2382
2383 // Call hook if any
2384 if (is_object($hookmanager)) {
2385 $parameters = array('thirdparty_seller' => $thirdparty_seller, 'thirdparty_buyer' => $thirdparty_buyer, 'pqp' => $pqp);
2386 // Note that $action and $object may have been modified by some hooks
2387 $reshook = $hookmanager->executeHooks('getSellPrice', $parameters, $this, $action);
2388 if ($reshook > 0) {
2389 return $hookmanager->resArray;
2390 }
2391 }
2392
2393 // Update if prices fields are defined
2394 $tva_tx = get_default_tva($thirdparty_seller, $thirdparty_buyer, $this->id);
2395 $tva_npr = get_default_npr($thirdparty_seller, $thirdparty_buyer, $this->id);
2396 if (empty($tva_tx)) {
2397 $tva_npr = 0;
2398 }
2399
2400 $pu_ht = $this->price;
2401 $pu_ttc = $this->price_ttc;
2402 $price_min = $this->price_min;
2403 $price_min_ttc = $this->price_min_ttc;
2404 $price_base_type = $this->price_base_type;
2405
2406 // if price by customer / level
2407 if (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) {
2408 require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
2409
2410 $prodcustprice = new ProductCustomerPrice($this->db);
2411
2412 $filter = array('t.fk_product' => (string) $this->id, 't.fk_soc' => (string) $thirdparty_buyer->id);
2413
2414 // If a price per customer exist
2415 $pricebycustomerexist = false;
2416 $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
2417 if ($result) {
2418 if (count($prodcustprice->lines) > 0) {
2419 $date_now = (int) floor(dol_now() / 86400) * 86400; // date without hours
2420 foreach ($prodcustprice->lines as $k => $custprice_line) {
2421 if ($custprice_line->date_begin <= $date_now && (empty($custprice_line->date_end) || $date_now <= $custprice_line->date_end)) {
2422 $pricebycustomerexist = true;
2423 $pu_ht = price($custprice_line->price);
2424 $price_min = price($custprice_line->price_min);
2425 $price_min_ttc = price($custprice_line->price_min_ttc);
2426 $pu_ttc = price($custprice_line->price_ttc);
2427 $price_base_type = $custprice_line->price_base_type;
2428 $tva_tx = $custprice_line->tva_tx;
2429 if ($custprice_line->default_vat_code && !preg_match('/\‍(.*\‍)/', $tva_tx)) {
2430 $tva_tx .= ' (' . $custprice_line->default_vat_code . ')';
2431 }
2432 $tva_npr = $custprice_line->recuperableonly;
2433 if (empty($tva_tx)) {
2434 $tva_npr = 0;
2435 }
2436 break;
2437 }
2438 }
2439 }
2440 }
2441
2442 if (!$pricebycustomerexist && !empty($thirdparty_buyer->price_level)) {
2443 $pu_ht = $this->multiprices[$thirdparty_buyer->price_level];
2444 $pu_ttc = $this->multiprices_ttc[$thirdparty_buyer->price_level];
2445 $price_min = $this->multiprices_min[$thirdparty_buyer->price_level];
2446 $price_min_ttc = $this->multiprices_min_ttc[$thirdparty_buyer->price_level];
2447 $price_base_type = $this->multiprices_base_type[$thirdparty_buyer->price_level];
2448 if (getDolGlobalString('PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL')) {
2449 // using this option is a bug. kept for backward compatibility
2450 if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
2451 $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
2452 }
2453 if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
2454 $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
2455 }
2456 if (empty($tva_tx)) {
2457 $tva_npr = 0;
2458 }
2459 }
2460 }
2461 } elseif (getDolGlobalString('PRODUIT_MULTIPRICES') && !empty($thirdparty_buyer->price_level)) { // // If price per segment
2462 $pu_ht = $this->multiprices[$thirdparty_buyer->price_level];
2463 $pu_ttc = $this->multiprices_ttc[$thirdparty_buyer->price_level];
2464 $price_min = $this->multiprices_min[$thirdparty_buyer->price_level];
2465 $price_min_ttc = $this->multiprices_min_ttc[$thirdparty_buyer->price_level];
2466 $price_base_type = $this->multiprices_base_type[$thirdparty_buyer->price_level];
2467 if (getDolGlobalString('PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL')) { // using this option is a bug. kept for backward compatibility
2468 if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
2469 $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
2470 }
2471 if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
2472 $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
2473 }
2474 if (empty($tva_tx)) {
2475 $tva_npr = 0;
2476 }
2477 }
2478 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES')) {
2479 // If price per customer
2480 require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
2481
2482 $prodcustprice = new ProductCustomerPrice($this->db);
2483
2484 $filter = array('t.fk_product' => (string) $this->id, 't.fk_soc' => (string) $thirdparty_buyer->id);
2485
2486 $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
2487 if ($result) {
2488 if (count($prodcustprice->lines) > 0) {
2489 $date_now = (int) floor(dol_now() / 86400) * 86400; // date without hours
2490 foreach ($prodcustprice->lines as $k => $custprice_line) {
2491 if ($custprice_line->date_begin <= $date_now && (empty($custprice_line->date_end) || $date_now <= $custprice_line->date_end)) {
2492 $pu_ht = price($custprice_line->price);
2493 $price_min = price($custprice_line->price_min);
2494 $price_min_ttc = price($custprice_line->price_min_ttc);
2495 $pu_ttc = price($custprice_line->price_ttc);
2496 $price_base_type = $custprice_line->price_base_type;
2497 $tva_tx = $custprice_line->tva_tx;
2498 if ($custprice_line->default_vat_code && !preg_match('/\‍(.*\‍)/', $tva_tx)) {
2499 $tva_tx .= ' (' . $custprice_line->default_vat_code . ')';
2500 }
2501 $tva_npr = $custprice_line->recuperableonly;
2502 if (empty($tva_tx)) {
2503 $tva_npr = 0;
2504 }
2505 break;
2506 }
2507 }
2508 }
2509 }
2510 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY')) {
2511 // If price per quantity
2512 if ($this->prices_by_qty[0]) {
2513 // yes, this product has some prices per quantity
2514 // Search price into product_price_by_qty from $this->id
2515 foreach ($this->prices_by_qty_list[0] as $priceforthequantityarray) {
2516 if ($priceforthequantityarray['rowid'] != $pqp) {
2517 continue;
2518 }
2519 // We found the price
2520 if ($priceforthequantityarray['price_base_type'] == 'HT') {
2521 $pu_ht = $priceforthequantityarray['unitprice'];
2522 } else {
2523 $pu_ttc = $priceforthequantityarray['unitprice'];
2524 }
2525 break;
2526 }
2527 }
2528 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) {
2529 // If price per quantity and customer
2530 if ($this->prices_by_qty[$thirdparty_buyer->price_level]) {
2531 // yes, this product has some prices per quantity
2532 // Search price into product_price_by_qty from $this->id
2533 foreach ($this->prices_by_qty_list[$thirdparty_buyer->price_level] as $priceforthequantityarray) {
2534 if ($priceforthequantityarray['rowid'] != $pqp) {
2535 continue;
2536 }
2537 // We found the price
2538 if ($priceforthequantityarray['price_base_type'] == 'HT') {
2539 $pu_ht = $priceforthequantityarray['unitprice'];
2540 } else {
2541 $pu_ttc = $priceforthequantityarray['unitprice'];
2542 }
2543 break;
2544 }
2545 }
2546 }
2547
2548 $localtax1 = get_localtax((string) $tva_tx, 1, $thirdparty_buyer, $thirdparty_seller, $tva_npr);
2549 $localtax2 = get_localtax((string) $tva_tx, 2, $thirdparty_buyer, $thirdparty_seller, $tva_npr);
2550
2551 return array('pu_ht' => $pu_ht, 'pu_ttc' => $pu_ttc, 'price_min' => $price_min, 'price_min_ttc' => $price_min_ttc, 'price_base_type' => $price_base_type, 'tva_tx' => $tva_tx, 'tva_npr' => $tva_npr, 'localtax1' => $localtax1, 'localtax2' => $localtax2);
2552 }
2553
2554 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2568 public function get_buyprice($prodfournprice, $qty, $product_id = 0, $fourn_ref = '', $fk_soc = 0)
2569 {
2570 // phpcs:enable
2571 global $action, $hookmanager;
2572
2573 // Call hook if any
2574 if (is_object($hookmanager)) {
2575 $parameters = array(
2576 'prodfournprice' => $prodfournprice,
2577 'qty' => $qty,
2578 'product_id' => $product_id,
2579 'fourn_ref' => $fourn_ref,
2580 'fk_soc' => $fk_soc,
2581 );
2582 // Note that $action and $object may have been modified by some hooks
2583 $reshook = $hookmanager->executeHooks('getBuyPrice', $parameters, $this, $action);
2584 if ($reshook > 0) {
2585 return $hookmanager->resArray;
2586 }
2587 }
2588
2589 $result = 0;
2590
2591 // 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)
2592 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2593 $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,";
2594 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2595 $sql .= " pfp.packaging";
2596 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2597 $sql .= " WHERE pfp.rowid = ".((int) $prodfournprice);
2598 if ($qty > 0) {
2599 $sql .= " AND pfp.quantity <= ".((float) $qty);
2600 }
2601 $sql .= " ORDER BY pfp.quantity DESC";
2602
2603 dol_syslog(get_class($this)."::get_buyprice first search by prodfournprice/qty", LOG_DEBUG);
2604 $resql = $this->db->query($sql);
2605 if ($resql) {
2606 $obj = $this->db->fetch_object($resql);
2607 if ($obj && $obj->quantity > 0) { // If we found a supplier prices from the id of supplier price
2608 if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2609 $prod_supplier = new ProductFournisseur($this->db);
2610 $prod_supplier->product_fourn_price_id = $obj->rowid;
2611 $prod_supplier->id = $obj->fk_product;
2612 $prod_supplier->fourn_qty = $obj->quantity;
2613 $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2614 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2615
2616 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2617 $priceparser = new PriceParser($this->db);
2618 $price_result = $priceparser->parseProductSupplier($prod_supplier);
2619 if ($price_result >= 0) {
2620 $obj->price = $price_result;
2621 }
2622 }
2623 $this->product_fourn_price_id = $obj->rowid;
2624 $this->buyprice = $obj->price; // deprecated
2625 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product of supplier
2626 $this->fourn_price_base_type = 'HT'; // Price base type
2627 $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2628 $this->ref_fourn = $obj->ref_supplier; // deprecated
2629 $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2630 $this->desc_supplier = $obj->desc_supplier; // desc supplier
2631 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2632 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2633 $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2634 $this->fourn_multicurrency_price = $obj->multicurrency_price;
2635 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2636 $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2637 $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2638 $this->fourn_multicurrency_code = $obj->multicurrency_code;
2639 if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2640 $this->packaging = (float) $obj->packaging;
2641 }
2642 $result = $obj->fk_product;
2643 return $result;
2644 } else { // If not found
2645 // 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.
2646 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2647 $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,";
2648 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2649 $sql .= " pfp.packaging";
2650 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2651 $sql .= " WHERE 1 = 1";
2652 if ($product_id > 0) {
2653 $sql .= " AND pfp.fk_product = ".((int) $product_id);
2654 }
2655 if ($fourn_ref != 'none') {
2656 $sql .= " AND pfp.ref_fourn = '".$this->db->escape($fourn_ref)."'";
2657 }
2658 if ($fk_soc > 0) {
2659 $sql .= " AND pfp.fk_soc = ".((int) $fk_soc);
2660 }
2661 if ($qty > 0) {
2662 $sql .= " AND pfp.quantity <= ".((float) $qty);
2663 }
2664 $sql .= " ORDER BY pfp.quantity DESC";
2665 $sql .= " LIMIT 1";
2666
2667 dol_syslog(get_class($this)."::get_buyprice second search from qty/ref/product_id", LOG_DEBUG);
2668 $resql = $this->db->query($sql);
2669 if ($resql) {
2670 $obj = $this->db->fetch_object($resql);
2671 if ($obj && $obj->quantity > 0) { // If found
2672 if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2673 $prod_supplier = new ProductFournisseur($this->db);
2674 $prod_supplier->product_fourn_price_id = $obj->rowid;
2675 $prod_supplier->id = $obj->fk_product;
2676 $prod_supplier->fourn_qty = $obj->quantity;
2677 $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2678 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2679
2680 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2681 $priceparser = new PriceParser($this->db);
2682 $price_result = $priceparser->parseProductSupplier($prod_supplier);
2683 if ($price_result >= 0) {
2684 $obj->price = $price_result;
2685 }
2686 }
2687 $this->product_fourn_price_id = $obj->rowid;
2688 $this->buyprice = $obj->price; // deprecated
2689 $this->fourn_qty = $obj->quantity; // min quantity for price for a virtual supplier
2690 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product for a virtual supplier
2691 $this->fourn_price_base_type = 'HT'; // Price base type for a virtual supplier
2692 $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2693 $this->ref_fourn = $obj->ref_supplier; // deprecated
2694 $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2695 $this->desc_supplier = $obj->desc_supplier; // desc supplier
2696 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2697 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2698 $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2699 $this->fourn_multicurrency_price = $obj->multicurrency_price;
2700 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2701 $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2702 $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2703 $this->fourn_multicurrency_code = $obj->multicurrency_code;
2704 if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2705 $this->packaging = (float) $obj->packaging;
2706 }
2707 $result = $obj->fk_product;
2708 return $result;
2709 } else {
2710 return -1; // Ce produit n'existe pas avec cet id tarif fournisseur ou existe mais qte insuffisante, ni pour le couple produit/ref fournisseur dans la quantité.
2711 }
2712 } else {
2713 $this->error = $this->db->lasterror();
2714 return -3;
2715 }
2716 }
2717 } else {
2718 $this->error = $this->db->lasterror();
2719 return -2;
2720 }
2721 }
2722
2723
2742 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)
2743 {
2744 $lastPriceData = $this->getArrayForPriceCompare($level); // temporary store current price before update
2745
2746 $id = $this->id;
2747
2748 dol_syslog(get_class($this)."::update_price id=".$id." newprice=".$newprice." newpricebase=".$newpricebase." newminprice=".$newminprice." level=".$level." npr=".$newnpr." newdefaultvatcode=".$newdefaultvatcode);
2749
2750 // Clean parameters
2751 if (empty($this->tva_tx)) {
2752 $this->tva_tx = 0.0;
2753 }
2754 if (empty($newnpr)) {
2755 $newnpr = 0.0;
2756 }
2757 if (empty($newminprice)) {
2758 $newminprice = 0.0;
2759 }
2760
2761 // Check parameters
2762 if ($newvat === null || $newvat == '') { // Maintain '' for backwards compatibility
2763 $newvat = (float) $this->tva_tx;
2764 }
2765
2766 $localtaxtype1 = '';
2767 $localtaxtype2 = '';
2768
2769 // If multiprices are enabled, then we check if the current product is subject to price autogeneration
2770 // Price will be modified ONLY when the first one is the one that is being modified
2771 if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && !$ignore_autogen && $this->price_autogen && ($level == 1)) {
2772 return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpbq);
2773 }
2774
2775 if (!empty($newminprice) && ($newminprice > $newprice)) {
2776 $this->error = 'ErrorPriceCantBeLowerThanMinPrice';
2777 return -1;
2778 }
2779
2780 if ($newprice === 0 || $newprice !== '') {
2781 if ($newpricebase == 'TTC') {
2782 $price_ttc = (float) price2num($newprice, 'MU');
2783 $price = (float) price2num($newprice) / (1 + ((float) $newvat / 100));
2784 $price = (float) price2num($price, 'MU');
2785
2786 if ((string) $newminprice != '0') {
2787 $price_min_ttc = (float) price2num($newminprice, 'MU');
2788 $price_min = (float) price2num($newminprice) / (1 + ($newvat / 100));
2789 $price_min = (float) price2num($price_min, 'MU');
2790 } else {
2791 $price_min = 0.0;
2792 $price_min_ttc = 0.0;
2793 }
2794 } else {
2795 $price = (float) price2num($newprice, 'MU');
2796 $price_ttc = ($newnpr != 1) ? (float) price2num($newprice) * (1 + ($newvat / 100)) : $price;
2797 $price_ttc = (float) price2num($price_ttc, 'MU');
2798
2799 if ((string) $newminprice != '0') {
2800 $price_min = (float) price2num($newminprice, 'MU');
2801 $price_min_ttc = (float) price2num($newminprice) * (1 + ($newvat / 100));
2802 $price_min_ttc = (float) price2num($price_min_ttc, 'MU');
2803 //print 'X'.$newminprice.'-'.$price_min;
2804 } else {
2805 $price_min = 0.0;
2806 $price_min_ttc = 0.0;
2807 }
2808 }
2809 //print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc;
2810 if (count($localtaxes_array) > 0) {
2811 $localtaxtype1 = $localtaxes_array['0'];
2812 $localtax1 = $localtaxes_array['1'];
2813 $localtaxtype2 = $localtaxes_array['2'];
2814 $localtax2 = $localtaxes_array['3'];
2815 } else {
2816 // if array empty, we try to use the vat code
2817 if (!empty($newdefaultvatcode)) {
2818 global $mysoc;
2819 // Get record from code
2820 $sql = "SELECT t.rowid, t.code, t.recuperableonly as tva_npr, t.localtax1, t.localtax2, t.localtax1_type, t.localtax2_type";
2821 $sql .= " FROM ".MAIN_DB_PREFIX."c_tva as t, ".MAIN_DB_PREFIX."c_country as c";
2822 $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '".$this->db->escape($mysoc->country_code)."'";
2823 $sql .= " AND t.taux = ".((float) $newdefaultvatcode)." AND t.active = 1";
2824 $sql .= " AND t.code = '".$this->db->escape($newdefaultvatcode)."'";
2825 $resql = $this->db->query($sql);
2826 if ($resql) {
2827 $obj = $this->db->fetch_object($resql);
2828 if ($obj) {
2829 $npr = $obj->tva_npr;
2830 $localtax1 = $obj->localtax1;
2831 $localtax2 = $obj->localtax2;
2832 $localtaxtype1 = $obj->localtax1_type;
2833 $localtaxtype2 = $obj->localtax2_type;
2834 }
2835 }
2836 } else {
2837 // old method. deprecated because we can't retrieve type
2838 $localtaxtype1 = '0';
2839 $localtax1 = get_localtax($newvat, 1);
2840 $localtaxtype2 = '0';
2841 $localtax2 = get_localtax($newvat, 2);
2842 }
2843 }
2844 if (empty($localtax1)) {
2845 $localtax1 = 0; // If = '' then = 0
2846 }
2847 if (empty($localtax2)) {
2848 $localtax2 = 0; // If = '' then = 0
2849 }
2850
2851 $this->db->begin();
2852
2853 // Ne pas mettre de quote sur les numeriques decimaux.
2854 // Ceci provoque des stockages avec arrondis en base au lieu des valeurs exactes.
2855 $sql = "UPDATE ".$this->db->prefix()."product SET";
2856 $sql .= " price_base_type = '".$this->db->escape($newpricebase)."',";
2857 $sql .= " price = ".(float) $price.",";
2858 $sql .= " price_ttc = ".(float) $price_ttc.",";
2859 $sql .= " price_min = ".(float) $price_min.",";
2860 $sql .= " price_min_ttc = ".(float) $price_min_ttc.",";
2861 $sql .= " localtax1_tx = ".($localtax1 >= 0 ? (float) $localtax1 : 'NULL').",";
2862 $sql .= " localtax2_tx = ".($localtax2 >= 0 ? (float) $localtax2 : 'NULL').",";
2863 $sql .= " localtax1_type = ".($localtaxtype1 != '' ? "'".$this->db->escape($localtaxtype1)."'" : "'0'").",";
2864 $sql .= " localtax2_type = ".($localtaxtype2 != '' ? "'".$this->db->escape($localtaxtype2)."'" : "'0'").",";
2865 $sql .= " default_vat_code = ".($newdefaultvatcode ? "'".$this->db->escape($newdefaultvatcode)."'" : "null").",";
2866 $sql .= " price_label = ".(!empty($price_label) ? "'".$this->db->escape($price_label)."'" : "null").",";
2867 $sql .= " tva_tx = ".(float) price2num($newvat).",";
2868 $sql .= " recuperableonly = '".$this->db->escape((string) $newnpr)."'";
2869 $sql .= " WHERE rowid = ".((int) $id);
2870
2871 dol_syslog(get_class($this)."::update_price", LOG_DEBUG);
2872 $resql = $this->db->query($sql);
2873 if ($resql) {
2874 $this->multiprices[$level] = $price;
2875 $this->multiprices_ttc[$level] = $price_ttc;
2876 $this->multiprices_min[$level] = $price_min;
2877 $this->multiprices_min_ttc[$level] = $price_min_ttc;
2878 $this->multiprices_base_type[$level] = $newpricebase;
2879 $this->multiprices_default_vat_code[$level] = $newdefaultvatcode;
2880 $this->multiprices_tva_tx[$level] = $newvat;
2881 $this->multiprices_recuperableonly[$level] = $newnpr;
2882
2883 $this->price = $price;
2884 $this->price_label = $price_label;
2885 $this->price_ttc = $price_ttc;
2886 $this->price_min = $price_min;
2887 $this->price_min_ttc = $price_min_ttc;
2888 $this->price_base_type = $newpricebase;
2889 $this->default_vat_code = $newdefaultvatcode;
2890 $this->tva_tx = $newvat;
2891 $this->tva_npr = $newnpr;
2892
2893 //Local taxes
2894 $this->localtax1_tx = $localtax1;
2895 $this->localtax2_tx = $localtax2;
2896 $this->localtax1_type = $localtaxtype1;
2897 $this->localtax2_type = $localtaxtype2;
2898
2899 // Price by quantity
2900 $this->price_by_qty = $newpbq;
2901
2902 // check if price have really change before log
2903 $newPriceData = $this->getArrayForPriceCompare($level);
2904 if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || !getDolGlobalString('PRODUIT_MULTIPRICES')) {
2905 $this->_log_price($user, $level); // Save price for level into table product_price
2906 }
2907
2908 $this->level = $level; // Store level of price edited for trigger
2909
2910 // Call trigger
2911 if (!$notrigger) {
2912 $result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user);
2913 if ($result < 0) {
2914 $this->db->rollback();
2915 return -1;
2916 }
2917 }
2918 // End call triggers
2919
2920 $this->db->commit();
2921 } else {
2922 $this->db->rollback();
2923 $this->error = $this->db->lasterror();
2924 return -1;
2925 }
2926 }
2927
2928 return 1;
2929 }
2930
2938 public function setPriceExpression($expression_id)
2939 {
2940 global $user;
2941
2942 $this->fk_price_expression = $expression_id;
2943
2944 return $this->update($this->id, $user);
2945 }
2946
2959 public function fetch($id = 0, $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0)
2960 {
2961 include_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
2962
2963 global $conf;
2964
2965 dol_syslog(get_class($this)."::fetch id=".$id." ref=".$ref." ref_ext=".$ref_ext);
2966
2967 // Check parameters
2968 if (!$id && !$ref && !$ref_ext && !$barcode) {
2969 $this->error = 'ErrorWrongParameters';
2970 dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR);
2971 return -1;
2972 }
2973
2974 $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,";
2975 $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,";
2976 $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,";
2977 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.last_main_doc,";
2978 $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,";
2979 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
2980 $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,";
2981 } else {
2982 $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,";
2983 }
2984
2985 // For MultiCompany
2986 // PMP per entity & Stocks Sharings stock_reel includes only stocks shared with this entity
2987 $separatedEntityPMP = false; // Set to true to get the AWP from table llx_product_perentity instead of field 'pmp' into llx_product.
2988 $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.
2989 $visibleWarehousesEntities = $conf->entity;
2990 if (getDolGlobalString('MULTICOMPANY_PRODUCT_SHARING_ENABLED')) {
2991 if (getDolGlobalString('MULTICOMPANY_PMP_PER_ENTITY_ENABLED')) {
2992 $checkPMPPerEntity = $this->db->query("SELECT pmp FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = ".((int) $id)." AND entity = ".(int) $conf->entity);
2993 if ($this->db->num_rows($checkPMPPerEntity) > 0) {
2994 $separatedEntityPMP = true;
2995 }
2996 }
2997 global $mc;
2998 $separatedStock = true;
2999 if (isset($mc->sharings['stock']) && !empty($mc->sharings['stock'])) {
3000 $visibleWarehousesEntities .= "," . implode(",", $mc->sharings['stock']);
3001 }
3002 }
3003 if ($separatedEntityPMP) {
3004 $sql .= " ppe.pmp,";
3005 } else {
3006 $sql .= " p.pmp,";
3007 }
3008 $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,";
3009 $sql .= " p.fk_price_expression, p.price_autogen, p.stockable_product, p.model_pdf,";
3010 $sql .= " p.price_label,";
3011 if ($separatedStock) {
3012 $sql .= " SUM(sp.reel) as stock";
3013 } else {
3014 $sql .= " p.stock";
3015 }
3016 $sql .= " FROM ".$this->db->prefix()."product as p";
3017 if (getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED') || $separatedEntityPMP) {
3018 $sql .= " LEFT JOIN " . $this->db->prefix() . "product_perentity as ppe ON ppe.fk_product = p.rowid AND ppe.entity = " . ((int) $conf->entity);
3019 }
3020 if ($separatedStock) {
3021 $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)."))";
3022 }
3023
3024 if ($id) {
3025 $sql .= " WHERE p.rowid = ".((int) $id);
3026 } else {
3027 $sql .= " WHERE p.entity IN (".getEntity($this->element).")";
3028 if ($ref) {
3029 $sql .= " AND p.ref = '".$this->db->escape($ref)."'";
3030 } elseif ($ref_ext) {
3031 $sql .= " AND p.ref_ext = '".$this->db->escape($ref_ext)."'";
3032 } elseif ($barcode) {
3033 $sql .= " AND p.barcode = '".$this->db->escape($barcode)."'";
3034 }
3035 }
3036 if ($separatedStock) {
3037 $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,";
3038 $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,";
3039 $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,";
3040 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
3041 $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,";
3042 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
3043 $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,";
3044 } else {
3045 $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,";
3046 }
3047 if ($separatedEntityPMP) {
3048 $sql .= " ppe.pmp,";
3049 } else {
3050 $sql .= " p.pmp,";
3051 }
3052 $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,";
3053 $sql .= " p.fk_price_expression, p.price_autogen, p.stockable_product, p.model_pdf,";
3054 $sql .= " p.price_label";
3055 if (!$separatedStock) {
3056 $sql .= ", p.stock";
3057 }
3058 }
3059
3060 $resql = $this->db->query($sql);
3061 if ($resql) {
3062 unset($this->oldcopy);
3063
3064 if ($this->db->num_rows($resql) > 0) {
3065 $obj = $this->db->fetch_object($resql);
3066
3067 $this->id = $obj->rowid;
3068 $this->ref = $obj->ref;
3069 $this->ref_ext = $obj->ref_ext;
3070 $this->label = $obj->label;
3071 $this->description = $obj->description;
3072 $this->url = $obj->url;
3073 $this->note_public = $obj->note_public;
3074 $this->note_private = $obj->note_private;
3075 $this->note = $obj->note_private; // deprecated
3076
3077 $this->type = $obj->fk_product_type;
3078 $this->price_label = $obj->price_label;
3079 $this->status = $obj->tosell;
3080 $this->status_buy = $obj->tobuy;
3081 $this->status_batch = $obj->tobatch;
3082 $this->sell_or_eat_by_mandatory = $obj->sell_or_eat_by_mandatory;
3083 $this->batch_mask = $obj->batch_mask;
3084
3085 $this->customcode = $obj->customcode;
3086 $this->country_id = $obj->fk_country;
3087 $this->country_code = getCountry($this->country_id, '2', $this->db);
3088 $this->state_id = $obj->fk_state;
3089 $this->lifetime = $obj->lifetime;
3090 $this->qc_frequency = $obj->qc_frequency;
3091 $this->price = $obj->price;
3092 $this->price_ttc = $obj->price_ttc;
3093 $this->price_min = $obj->price_min;
3094 $this->price_min_ttc = $obj->price_min_ttc;
3095 $this->price_base_type = $obj->price_base_type;
3096 $this->cost_price = isset($obj->cost_price) ? (float) $obj->cost_price : null;
3097 $this->default_vat_code = $obj->default_vat_code;
3098 $this->tva_tx = $obj->tva_tx;
3100 $this->tva_npr = $obj->tva_npr;
3102 $this->localtax1_tx = $obj->localtax1_tx;
3103 $this->localtax2_tx = $obj->localtax2_tx;
3104 $this->localtax1_type = $obj->localtax1_type;
3105 $this->localtax2_type = $obj->localtax2_type;
3106
3107 $this->finished = $obj->finished;
3108 $this->fk_default_bom = $obj->fk_default_bom;
3109
3110 $this->duration = $obj->duration;
3111 $matches = [];
3112 preg_match('/([\d.]+)(\w+)/', $obj->duration, $matches);
3113 $this->duration_value = !empty($matches[1]) ? (float) $matches[1] : 0;
3114 $this->duration_unit = !empty($matches[2]) ? (string) $matches[2] : null;
3115 $this->canvas = $obj->canvas;
3116 $this->net_measure = $obj->net_measure;
3117 $this->net_measure_units = $obj->net_measure_units;
3118 $this->weight = $obj->weight;
3119 $this->weight_units = (is_null($obj->weight_units) ? 0 : $obj->weight_units);
3120 $this->length = $obj->length;
3121 $this->length_units = (is_null($obj->length_units) ? 0 : $obj->length_units);
3122 $this->width = $obj->width;
3123 $this->width_units = (is_null($obj->width_units) ? 0 : $obj->width_units);
3124 $this->height = $obj->height;
3125 $this->height_units = (is_null($obj->height_units) ? 0 : $obj->height_units);
3126
3127 $this->surface = $obj->surface;
3128 $this->surface_units = (is_null($obj->surface_units) ? 0 : $obj->surface_units);
3129 $this->volume = $obj->volume;
3130 $this->volume_units = (is_null($obj->volume_units) ? 0 : $obj->volume_units);
3131 $this->barcode = $obj->barcode;
3132 $this->barcode_type = $obj->fk_barcode_type;
3133
3134 $this->accountancy_code_buy = $obj->accountancy_code_buy;
3135 $this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra;
3136 $this->accountancy_code_buy_export = $obj->accountancy_code_buy_export;
3137 $this->accountancy_code_sell = $obj->accountancy_code_sell;
3138 $this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra;
3139 $this->accountancy_code_sell_export = $obj->accountancy_code_sell_export;
3140
3141 $this->fk_default_warehouse = $obj->fk_default_warehouse;
3142 $this->fk_default_workstation = $obj->fk_default_workstation;
3143 $this->seuil_stock_alerte = $obj->seuil_stock_alerte;
3144 $this->desiredstock = $obj->desiredstock;
3145 $this->stock_reel = $obj->stock;
3146 $this->stockable_product = $obj->stockable_product;
3147 $this->pmp = $obj->pmp;
3148
3149 $this->date_creation = $this->db->jdate($obj->datec);
3150 $this->date_modification = $this->db->jdate($obj->tms);
3151
3152 $this->import_key = $obj->import_key;
3153 $this->entity = $obj->entity;
3154
3155 $this->ref_ext = $obj->ref_ext;
3156 $this->fk_price_expression = $obj->fk_price_expression;
3157 $this->fk_unit = $obj->fk_unit;
3158 $this->price_autogen = $obj->price_autogen;
3159 $this->model_pdf = $obj->model_pdf;
3160 $this->last_main_doc = $obj->last_main_doc;
3161
3162 $this->mandatory_period = $obj->mandatory_period;
3163
3164 if (getDolGlobalString('PRODUCT_USE_CUSTOMER_PACKAGING')) {
3165 $this->packaging = (float) $obj->packaging;
3166 }
3167
3168 $this->db->free($resql);
3169
3170 // fetch optionals attributes and labels
3171 $this->fetch_optionals();
3172
3173 // Multilangs
3174 if (getDolGlobalInt('MAIN_MULTILANGS') && empty($ignore_lang_load)) {
3175 $this->getMultiLangs();
3176 }
3177
3178 // Load multiprices array
3179 if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && empty($ignore_price_load)) { // prices per segment
3180 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
3181 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
3182 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3183 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
3184 $sql .= " ,price_label";
3185 $sql .= " FROM ".$this->db->prefix()."product_price";
3186 $sql .= " WHERE entity IN (".getEntity('productprice').")";
3187 $sql .= " AND price_level=".((int) $i);
3188 $sql .= " AND fk_product = ".((int) $this->id);
3189 $sql .= " ORDER BY date_price DESC, rowid DESC"; // Get the most recent line
3190 $sql .= " LIMIT 1"; // Only the first one
3191 $resql = $this->db->query($sql);
3192 if ($resql) {
3193 $result = $this->db->fetch_array($resql);
3194
3195 $this->multiprices[$i] = $result ? $result["price"] : null;
3196 $this->multiprices_ttc[$i] = $result ? $result["price_ttc"] : null;
3197 $this->multiprices_min[$i] = $result ? $result["price_min"] : null;
3198 $this->multiprices_min_ttc[$i] = $result ? $result["price_min_ttc"] : null;
3199 $this->multiprices_base_type[$i] = $result ? $result["price_base_type"] : null;
3200 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
3201 $this->multiprices_tva_tx[$i] = $result ? $result["tva_tx"].(!empty($result['default_vat_code']) ? ' ('.$result['default_vat_code'].')' : '') : null;
3202 $this->multiprices_recuperableonly[$i] = $result ? $result["recuperableonly"] : null;
3203
3204 // Price by quantity
3205 /*
3206 $this->prices_by_qty[$i]=$result["price_by_qty"];
3207 $this->prices_by_qty_id[$i]=$result["rowid"];
3208 // Récuperation de la liste des prix selon qty si flag positionné
3209 if ($this->prices_by_qty[$i] == 1)
3210 {
3211 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
3212 $sql.= " FROM ".$this->db->prefix()."product_price_by_qty";
3213 $sql.= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
3214 $sql.= " ORDER BY quantity ASC";
3215
3216 $resql = $this->db->query($sql);
3217 if ($resql)
3218 {
3219 $resultat=array();
3220 $ii=0;
3221 while ($result= $this->db->fetch_array($resql)) {
3222 $resultat[$ii]=array();
3223 $resultat[$ii]["rowid"]=$result["rowid"];
3224 $resultat[$ii]["price"]= $result["price"];
3225 $resultat[$ii]["unitprice"]= $result["unitprice"];
3226 $resultat[$ii]["quantity"]= $result["quantity"];
3227 $resultat[$ii]["remise_percent"]= $result["remise_percent"];
3228 $resultat[$ii]["remise"]= $result["remise"]; // deprecated
3229 $resultat[$ii]["price_base_type"]= $result["price_base_type"];
3230 $ii++;
3231 }
3232 $this->prices_by_qty_list[$i]=$resultat;
3233 }
3234 else
3235 {
3236 dol_print_error($this->db);
3237 return -1;
3238 }
3239 }*/
3240 } else {
3241 $this->error = $this->db->lasterror;
3242 return -1;
3243 }
3244 }
3245 } elseif ((getDolGlobalString('PRODUIT_CUSTOMER_PRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && empty($ignore_price_load)) { // prices per customers
3246 // Nothing loaded by default. List may be very long.
3247 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY') && empty($ignore_price_load)) { // prices per quantity
3248 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3249 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid";
3250 $sql .= " FROM ".$this->db->prefix()."product_price";
3251 $sql .= " WHERE fk_product = ".((int) $this->id);
3252 $sql .= " ORDER BY date_price DESC, rowid DESC";
3253 $sql .= " LIMIT 1";
3254
3255 $resql = $this->db->query($sql);
3256 if ($resql) {
3257 $result = $this->db->fetch_array($resql);
3258
3259 if ($result) {
3260 // Price by quantity
3261 $this->prices_by_qty[0] = $result["price_by_qty"];
3262 $this->prices_by_qty_id[0] = $result["rowid"];
3263 // Récuperation de la liste des prix selon qty si flag positionné
3264 if ($this->prices_by_qty[0] == 1) {
3265 $sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise, remise, price_base_type";
3266 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
3267 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[0]);
3268 $sql .= " ORDER BY quantity ASC";
3269
3270 $resql = $this->db->query($sql);
3271 if ($resql) {
3272 $resultat = array();
3273 $ii = 0;
3274 while ($result = $this->db->fetch_array($resql)) {
3275 $resultat[$ii] = array();
3276 $resultat[$ii]["rowid"] = $result["rowid"];
3277 $resultat[$ii]["price"] = $result["price"];
3278 $resultat[$ii]["unitprice"] = $result["unitprice"];
3279 $resultat[$ii]["quantity"] = $result["quantity"];
3280 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
3281 //$resultat[$ii]["remise"]= $result["remise"]; // deprecated
3282 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
3283 $ii++;
3284 }
3285 $this->prices_by_qty_list[0] = $resultat;
3286 } else {
3287 $this->error = $this->db->lasterror;
3288 return -1;
3289 }
3290 }
3291 }
3292 } else {
3293 $this->error = $this->db->lasterror;
3294 return -1;
3295 }
3296 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') && empty($ignore_price_load)) { // prices per customer and quantity
3297 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
3298 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
3299 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3300 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
3301 $sql .= " FROM ".$this->db->prefix()."product_price";
3302 $sql .= " WHERE entity IN (".getEntity('productprice').")";
3303 $sql .= " AND price_level=".((int) $i);
3304 $sql .= " AND fk_product = ".((int) $this->id);
3305 $sql .= " ORDER BY date_price DESC, rowid DESC";
3306 $sql .= " LIMIT 1";
3307 $resql = $this->db->query($sql);
3308 if (!$resql) {
3309 $this->error = $this->db->lasterror;
3310 return -1;
3311 } else {
3312 $result = $this->db->fetch_array($resql);
3313 $this->multiprices[$i] = (!empty($result["price"]) ? $result["price"] : 0);
3314 $this->multiprices_ttc[$i] = (!empty($result["price_ttc"]) ? $result["price_ttc"] : 0);
3315 $this->multiprices_min[$i] = (!empty($result["price_min"]) ? $result["price_min"] : 0);
3316 $this->multiprices_min_ttc[$i] = (!empty($result["price_min_ttc"]) ? $result["price_min_ttc"] : 0);
3317 $this->multiprices_base_type[$i] = (!empty($result["price_base_type"]) ? $result["price_base_type"] : '');
3318 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
3319 $this->multiprices_tva_tx[$i] = (!empty($result["tva_tx"]) ? $result["tva_tx"] : 0); // TODO Add ' ('.$result['default_vat_code'].')'
3320 $this->multiprices_recuperableonly[$i] = (!empty($result["recuperableonly"]) ? $result["recuperableonly"] : 0);
3321
3322 // Price by quantity
3323 $this->prices_by_qty[$i] = (!empty($result["price_by_qty"]) ? $result["price_by_qty"] : 0);
3324 $this->prices_by_qty_id[$i] = (!empty($result["rowid"]) ? $result["rowid"] : 0);
3325 // Récuperation de la liste des prix selon qty si flag positionné
3326 if ($this->prices_by_qty[$i] == 1) {
3327 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
3328 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
3329 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
3330 $sql .= " ORDER BY quantity ASC";
3331
3332 $resql = $this->db->query($sql);
3333 if ($resql) {
3334 $resultat = array();
3335 $ii = 0;
3336 while ($result = $this->db->fetch_array($resql)) {
3337 $resultat[$ii] = array();
3338 $resultat[$ii]["rowid"] = $result["rowid"];
3339 $resultat[$ii]["price"] = $result["price"];
3340 $resultat[$ii]["unitprice"] = $result["unitprice"];
3341 $resultat[$ii]["quantity"] = $result["quantity"];
3342 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
3343 $resultat[$ii]["remise"] = $result["remise"]; // deprecated
3344 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
3345 $ii++;
3346 }
3347 $this->prices_by_qty_list[$i] = $resultat;
3348 } else {
3349 $this->error = $this->db->lasterror;
3350 return -1;
3351 }
3352 }
3353 }
3354 }
3355 }
3356
3357 if (isModEnabled('dynamicprices') && !empty($this->fk_price_expression) && empty($ignore_expression)) {
3358 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
3359 $priceparser = new PriceParser($this->db);
3360 $price_result = $priceparser->parseProduct($this);
3361 if ($price_result >= 0) {
3362 $this->price = $price_result;
3363 // Calculate the VAT
3364 $this->price_ttc = (float) price2num($this->price) * (1 + ($this->tva_tx / 100));
3365 $this->price_ttc = (float) price2num($this->price_ttc, 'MU');
3366 }
3367 }
3368
3369 // We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product.
3370 // Instead we just init the stock_warehouse array
3371 $this->stock_warehouse = array();
3372
3373 return 1;
3374 } else {
3375 return 0;
3376 }
3377 } else {
3378 $this->error = $this->db->lasterror();
3379 return -1;
3380 }
3381 }
3382
3383 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3390 public function load_stats_mo($socid = 0)
3391 {
3392 // phpcs:enable
3393 global $user, $hookmanager, $action;
3394
3395 $error = 0;
3396
3397 foreach (array('toconsume', 'consumed', 'toproduce', 'produced') as $role) {
3398 $this->stats_mo['customers_'.$role] = 0;
3399 $this->stats_mo['nb_'.$role] = 0;
3400 $this->stats_mo['qty_'.$role] = 0;
3401
3402 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3403 $sql .= " SUM(mp.qty) as qty";
3404 $sql .= " FROM ".$this->db->prefix()."mrp_mo as c";
3405 $sql .= " INNER JOIN ".$this->db->prefix()."mrp_production as mp ON mp.fk_mo=c.rowid";
3406 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3407 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3408 }
3409 $sql .= " WHERE ";
3410 $sql .= " c.entity IN (".getEntity('mo').")";
3411
3412 $sql .= " AND mp.fk_product = ".((int) $this->id);
3413 $sql .= " AND mp.role ='".$this->db->escape($role)."'";
3414 if ($socid > 0) {
3415 $sql .= " AND c.fk_soc = ".((int) $socid);
3416 }
3417
3418 $result = $this->db->query($sql);
3419 if ($result) {
3420 $obj = $this->db->fetch_object($result);
3421 $this->stats_mo['customers_'.$role] = $obj->nb_customers ? $obj->nb_customers : 0;
3422 $this->stats_mo['nb_'.$role] = $obj->nb ? $obj->nb : 0;
3423 $this->stats_mo['qty_'.$role] = $obj->qty ? price2num($obj->qty, 'MS') : 0; // qty may be a float due to the SUM()
3424 } else {
3425 $this->error = $this->db->error();
3426 $error++;
3427 }
3428 }
3429
3430 if (!empty($error)) {
3431 return -1;
3432 }
3433
3434 $parameters = array('socid' => $socid);
3435 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3436 if ($reshook > 0) {
3437 $this->stats_mo = $hookmanager->resArray['stats_mo'];
3438 }
3439
3440 return 1;
3441 }
3442
3443 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3450 public function load_stats_bom($socid = 0)
3451 {
3452 // phpcs:enable
3453 global $hookmanager, $action;
3454
3455 $error = 0;
3456
3457 $this->stats_bom['nb_toproduce'] = 0;
3458 $this->stats_bom['nb_toconsume'] = 0;
3459 $this->stats_bom['qty_toproduce'] = 0;
3460 $this->stats_bom['qty_toconsume'] = 0;
3461
3462 $sql = "SELECT COUNT(DISTINCT b.rowid) as nb_toproduce,";
3463 $sql .= " SUM(b.qty) as qty_toproduce";
3464 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3465 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom = b.rowid";
3466 $sql .= " WHERE ";
3467 $sql .= " b.entity IN (".getEntity('bom').")";
3468 $sql .= " AND b.fk_product =".((int) $this->id);
3469 $sql .= " GROUP BY b.rowid";
3470
3471 $result = $this->db->query($sql);
3472 if ($result) {
3473 $obj = $this->db->fetch_object($result);
3474 $this->stats_bom['nb_toproduce'] = !empty($obj->nb_toproduce) ? $obj->nb_toproduce : 0;
3475 $this->stats_bom['qty_toproduce'] = !empty($obj->qty_toproduce) ? price2num($obj->qty_toproduce) : 0;
3476 } else {
3477 $this->error = $this->db->error();
3478 $error++;
3479 }
3480
3481 $sql = "SELECT COUNT(DISTINCT bl.rowid) as nb_toconsume,";
3482 $sql .= " SUM(bl.qty) as qty_toconsume";
3483 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3484 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
3485 $sql .= " WHERE ";
3486 $sql .= " b.entity IN (".getEntity('bom').")";
3487 $sql .= " AND bl.fk_product =".((int) $this->id);
3488
3489 $result = $this->db->query($sql);
3490 if ($result) {
3491 $obj = $this->db->fetch_object($result);
3492 $this->stats_bom['nb_toconsume'] = !empty($obj->nb_toconsume) ? $obj->nb_toconsume : 0;
3493 $this->stats_bom['qty_toconsume'] = !empty($obj->qty_toconsume) ? price2num($obj->qty_toconsume) : 0;
3494 } else {
3495 $this->error = $this->db->error();
3496 $error++;
3497 }
3498
3499 if (!empty($error)) {
3500 return -1;
3501 }
3502
3503 $parameters = array('socid' => $socid);
3504 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3505 if ($reshook > 0) {
3506 $this->stats_bom = $hookmanager->resArray['stats_bom'];
3507 }
3508
3509 return 1;
3510 }
3511
3512 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3519 public function load_stats_propale($socid = 0)
3520 {
3521 // phpcs:enable
3522 global $user, $hookmanager, $action;
3523
3524 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,";
3525 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3526 $sql .= " FROM ".$this->db->prefix()."propaldet as pd";
3527 $sql .= ", ".$this->db->prefix()."propal as p";
3528 $sql .= ", ".$this->db->prefix()."societe as s";
3529 $sql .= " WHERE p.rowid = pd.fk_propal";
3530 $sql .= " AND p.fk_soc = s.rowid";
3531 $sql .= " AND p.entity IN (".getEntity('propal').")";
3532 $sql .= " AND pd.fk_product = ".((int) $this->id);
3533 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3534 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = p.fk_soc AND sc.fk_user = ".((int) $user->id);
3535 }
3536 //$sql.= " AND pr.fk_statut != 0";
3537 if ($socid > 0) {
3538 $sql .= " AND p.fk_soc = ".((int) $socid);
3539 }
3540
3541 $result = $this->db->query($sql);
3542 if ($result) {
3543 $obj = $this->db->fetch_object($result);
3544 $this->stats_propale['customers'] = $obj->nb_customers;
3545 $this->stats_propale['nb'] = $obj->nb;
3546 $this->stats_propale['rows'] = $obj->nb_rows;
3547 $this->stats_propale['qty'] = $obj->qty ? $obj->qty : 0;
3548
3549 // if it's a virtual product, maybe it is in proposal by extension
3550 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3551 $TFather = $this->getFather();
3552 if (is_array($TFather) && !empty($TFather)) {
3553 foreach ($TFather as &$fatherData) {
3554 $pFather = new Product($this->db);
3555 $pFather->id = $fatherData['id'];
3556 $qtyCoef = $fatherData['qty'];
3557
3558 if ($fatherData['incdec']) {
3559 $pFather->load_stats_propale($socid);
3560
3561 $this->stats_propale['customers'] += $pFather->stats_propale['customers'];
3562 $this->stats_propale['nb'] += $pFather->stats_propale['nb'];
3563 $this->stats_propale['rows'] += $pFather->stats_propale['rows'];
3564 $this->stats_propale['qty'] += $pFather->stats_propale['qty'] * $qtyCoef;
3565 }
3566 }
3567 }
3568 }
3569
3570 $parameters = array('socid' => $socid);
3571 $reshook = $hookmanager->executeHooks('loadStatsCustomerProposal', $parameters, $this, $action);
3572 if ($reshook > 0) {
3573 $this->stats_propale = $hookmanager->resArray['stats_propale'];
3574 }
3575
3576 return 1;
3577 } else {
3578 $this->error = $this->db->error();
3579 return -1;
3580 }
3581 }
3582
3583
3584 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3591 public function load_stats_proposal_supplier($socid = 0)
3592 {
3593 // phpcs:enable
3594 global $user, $hookmanager, $action;
3595
3596 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,";
3597 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3598 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as pd";
3599 $sql .= ", ".$this->db->prefix()."supplier_proposal as p";
3600 $sql .= ", ".$this->db->prefix()."societe as s";
3601 $sql .= " WHERE p.rowid = pd.fk_supplier_proposal";
3602 $sql .= " AND p.fk_soc = s.rowid";
3603 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
3604 $sql .= " AND pd.fk_product = ".((int) $this->id);
3605 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3606 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = p.fk_soc AND sc.fk_user = ".((int) $user->id);
3607 }
3608 //$sql.= " AND pr.fk_statut != 0";
3609 if ($socid > 0) {
3610 $sql .= " AND p.fk_soc = ".((int) $socid);
3611 }
3612
3613 $result = $this->db->query($sql);
3614 if ($result) {
3615 $obj = $this->db->fetch_object($result);
3616 $this->stats_proposal_supplier['suppliers'] = $obj->nb_suppliers;
3617 $this->stats_proposal_supplier['nb'] = $obj->nb;
3618 $this->stats_proposal_supplier['rows'] = $obj->nb_rows;
3619 $this->stats_proposal_supplier['qty'] = $obj->qty ? $obj->qty : 0;
3620
3621 $parameters = array('socid' => $socid);
3622 $reshook = $hookmanager->executeHooks('loadStatsSupplierProposal', $parameters, $this, $action);
3623 if ($reshook > 0) {
3624 $this->stats_proposal_supplier = $hookmanager->resArray['stats_proposal_supplier'];
3625 }
3626
3627 return 1;
3628 } else {
3629 $this->error = $this->db->error();
3630 return -1;
3631 }
3632 }
3633
3634
3635 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3644 public function load_stats_commande($socid = 0, $filtrestatut = '', $forVirtualStock = 0)
3645 {
3646 // phpcs:enable
3647 global $user, $hookmanager, $action;
3648
3649 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3650 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3651 $sql .= " FROM ".$this->db->prefix()."commandedet as cd";
3652 $sql .= ", ".$this->db->prefix()."commande as c";
3653 $sql .= ", ".$this->db->prefix()."societe as s";
3654 $sql .= " WHERE c.rowid = cd.fk_commande";
3655 $sql .= " AND c.fk_soc = s.rowid";
3656 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'commande').")";
3657 $sql .= " AND cd.fk_product = ".((int) $this->id);
3658 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3659 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3660 }
3661 if ($socid > 0) {
3662 $sql .= " AND c.fk_soc = ".((int) $socid);
3663 }
3664 if ($filtrestatut != '') {
3665 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3666 }
3667
3668 $result = $this->db->query($sql);
3669 if ($result) {
3670 $obj = $this->db->fetch_object($result);
3671 $this->stats_commande['customers'] = $obj->nb_customers;
3672 $this->stats_commande['nb'] = $obj->nb;
3673 $this->stats_commande['rows'] = $obj->nb_rows;
3674 $this->stats_commande['qty'] = $obj->qty ? $obj->qty : 0;
3675
3676 // if it's a virtual product, maybe it is in order by extension
3677 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3678 $TFather = $this->getFather();
3679 if (is_array($TFather) && !empty($TFather)) {
3680 foreach ($TFather as &$fatherData) {
3681 $pFather = new Product($this->db);
3682 $pFather->id = $fatherData['id'];
3683 $qtyCoef = $fatherData['qty'];
3684
3685 if ($fatherData['incdec']) {
3686 $pFather->load_stats_commande($socid, $filtrestatut);
3687
3688 $this->stats_commande['customers'] += $pFather->stats_commande['customers'];
3689 $this->stats_commande['nb'] += $pFather->stats_commande['nb'];
3690 $this->stats_commande['rows'] += $pFather->stats_commande['rows'];
3691 $this->stats_commande['qty'] += $pFather->stats_commande['qty'] * $qtyCoef;
3692 }
3693 }
3694 }
3695 }
3696
3697 // If stock decrease is on invoice validation, the theoretical stock continue to
3698 // count the orders lines in theoretical stock when some are already removed by invoice validation.
3699 if ($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
3700 if (getDolGlobalString('DECREASE_ONLY_UNINVOICEDPRODUCTS')) {
3701 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is on, we make a compensation but only if order not yet invoice.
3702 $adeduire = 0;
3703 $sql = "SELECT SUM(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3704 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3705 $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'))";
3706 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid";
3707 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3708
3709 dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3710 $resql = $this->db->query($sql);
3711 if ($resql) {
3712 if ($this->db->num_rows($resql) > 0) {
3713 $obj = $this->db->fetch_object($resql);
3714 $adeduire += $obj->count;
3715 }
3716 }
3717
3718 $this->stats_commande['qty'] -= $adeduire;
3719 } else {
3720 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is off, we make a compensation with lines of invoices linked to the order
3721 include_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
3722
3723 // For every order having invoice already validated we need to decrease stock cause it's in physical stock
3724 $adeduire = 0;
3725 $sql = "SELECT sum(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count";
3726 $sql .= " FROM " . $this->db->prefix() . "facturedet as fd";
3727 $sql .= " JOIN " . $this->db->prefix() . "facture as f ON fd.fk_facture = f.rowid";
3728 $sql .= " JOIN " . $this->db->prefix() . "element_element as el ON el.fk_target = f.rowid AND el.targettype = 'facture' AND el.sourcetype = 'commande'";
3729 $sql .= " JOIN " . $this->db->prefix() . "commande as c ON el.fk_source = c.rowid";
3730 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product = ".((int) $this->id);
3731 $sql .= " UNION ALL";
3732 $sql .= " SELECT sum(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count";
3733 $sql .= " FROM " . $this->db->prefix() . "facturedet as fd";
3734 $sql .= " JOIN " . $this->db->prefix() . "facture as f ON fd.fk_facture = f.rowid";
3735 $sql .= " JOIN " . $this->db->prefix() . "element_element as el ON el.fk_source = f.rowid AND el.targettype = 'commande' AND el.sourcetype = 'facture'";
3736 $sql .= " JOIN " . $this->db->prefix() . "commande as c ON el.fk_source = c.rowid";
3737 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product =".((int) $this->id);
3738 dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3739 $resql = $this->db->query($sql);
3740 if ($resql) {
3741 if ($this->db->num_rows($resql) > 0) {
3742 $obj = $this->db->fetch_object($resql);
3743 $adeduire += $obj->count;
3744 }
3745 } else {
3746 $this->error = $this->db->error();
3747 return -1;
3748 }
3749
3750 $this->stats_commande['qty'] -= $adeduire;
3751 }
3752 }
3753
3754 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3755 $reshook = $hookmanager->executeHooks('loadStatsCustomerOrder', $parameters, $this, $action);
3756 if ($reshook > 0) {
3757 $this->stats_commande = $hookmanager->resArray['stats_commande'];
3758 }
3759 return 1;
3760 } else {
3761 $this->error = $this->db->error();
3762 return -1;
3763 }
3764 }
3765
3766 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3776 public function load_stats_commande_fournisseur($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3777 {
3778 // phpcs:enable
3779 global $user, $hookmanager, $action;
3780
3781 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,";
3782 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3783 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as cd";
3784 $sql .= ", ".$this->db->prefix()."commande_fournisseur as c";
3785 $sql .= ", ".$this->db->prefix()."societe as s";
3786 $sql .= " WHERE c.rowid = cd.fk_commande";
3787 $sql .= " AND c.fk_soc = s.rowid";
3788 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
3789 $sql .= " AND cd.fk_product = ".((int) $this->id);
3790 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3791 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3792 }
3793 if ($socid > 0) {
3794 $sql .= " AND c.fk_soc = ".((int) $socid);
3795 }
3796 if ($filtrestatut != '') {
3797 $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")"; // Peut valoir 0
3798 }
3799 if (!empty($dateofvirtualstock)) {
3800 $sql .= " AND c.date_livraison <= '".$this->db->idate($dateofvirtualstock)."'";
3801 }
3802
3803 $result = $this->db->query($sql);
3804 if ($result) {
3805 $obj = $this->db->fetch_object($result);
3806 $this->stats_commande_fournisseur['suppliers'] = $obj->nb_suppliers;
3807 $this->stats_commande_fournisseur['nb'] = $obj->nb;
3808 $this->stats_commande_fournisseur['rows'] = $obj->nb_rows;
3809 $this->stats_commande_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3810
3811 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3812 $reshook = $hookmanager->executeHooks('loadStatsSupplierOrder', $parameters, $this, $action);
3813 if ($reshook > 0) {
3814 $this->stats_commande_fournisseur = $hookmanager->resArray['stats_commande_fournisseur'];
3815 }
3816
3817 return 1;
3818 } else {
3819 $this->error = $this->db->error().' sql='.$sql;
3820 return -1;
3821 }
3822 }
3823
3824 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3834 public function load_stats_sending($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $filterShipmentStatus = '')
3835 {
3836 // phpcs:enable
3837 global $user, $hookmanager, $action;
3838
3839 $sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,";
3840 $sql .= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty";
3841 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
3842 $sql .= ", ".$this->db->prefix()."commandedet as cd";
3843 $sql .= ", ".$this->db->prefix()."commande as c";
3844 $sql .= ", ".$this->db->prefix()."expedition as e";
3845 $sql .= ", ".$this->db->prefix()."societe as s";
3846 $sql .= " WHERE e.rowid = ed.fk_expedition";
3847 $sql .= " AND c.rowid = cd.fk_commande";
3848 $sql .= " AND e.fk_soc = s.rowid";
3849 $sql .= " AND e.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'expedition').")";
3850 $sql .= " AND ed.fk_elementdet = cd.rowid";
3851 $sql .= " AND cd.fk_product = ".((int) $this->id);
3852 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3853 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = e.fk_soc AND sc.fk_user = ".((int) $user->id);
3854 }
3855 if ($socid > 0) {
3856 $sql .= " AND e.fk_soc = ".((int) $socid);
3857 }
3858 if ($filtrestatut != '') {
3859 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3860 }
3861 if (!empty($filterShipmentStatus)) {
3862 $sql .= " AND e.fk_statut IN (".$this->db->sanitize($filterShipmentStatus).")";
3863 }
3864
3865 $result = $this->db->query($sql);
3866 if ($result) {
3867 $obj = $this->db->fetch_object($result);
3868 $this->stats_expedition['customers'] = $obj->nb_customers;
3869 $this->stats_expedition['nb'] = $obj->nb;
3870 $this->stats_expedition['rows'] = $obj->nb_rows;
3871 $this->stats_expedition['qty'] = $obj->qty ? $obj->qty : 0;
3872
3873 // if it's a virtual product, maybe it is in sending by extension
3874 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3875 $TFather = $this->getFather();
3876 if (is_array($TFather) && !empty($TFather)) {
3877 foreach ($TFather as &$fatherData) {
3878 $pFather = new Product($this->db);
3879 $pFather->id = $fatherData['id'];
3880 $qtyCoef = $fatherData['qty'];
3881
3882 if ($fatherData['incdec']) {
3883 $pFather->load_stats_sending($socid, $filtrestatut, $forVirtualStock);
3884
3885 $this->stats_expedition['customers'] += $pFather->stats_expedition['customers'];
3886 $this->stats_expedition['nb'] += $pFather->stats_expedition['nb'];
3887 $this->stats_expedition['rows'] += $pFather->stats_expedition['rows'];
3888 $this->stats_expedition['qty'] += $pFather->stats_expedition['qty'] * $qtyCoef;
3889 }
3890 }
3891 }
3892 }
3893
3894 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock, 'filterShipmentStatus' => $filterShipmentStatus);
3895 $reshook = $hookmanager->executeHooks('loadStatsSending', $parameters, $this, $action);
3896 if ($reshook > 0) {
3897 $this->stats_expedition = $hookmanager->resArray['stats_expedition'];
3898 }
3899
3900 return 1;
3901 } else {
3902 $this->error = $this->db->error();
3903 return -1;
3904 }
3905 }
3906
3907 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3917 public function load_stats_reception($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3918 {
3919 // phpcs:enable
3920 global $user, $hookmanager, $action;
3921
3922 $sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_suppliers, COUNT(DISTINCT cf.rowid) as nb,";
3923 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3924 $sql .= " FROM ".$this->db->prefix()."receptiondet_batch as fd";
3925 $sql .= ", ".$this->db->prefix()."commande_fournisseur as cf";
3926 $sql .= ", ".$this->db->prefix()."societe as s";
3927 $sql .= " WHERE cf.rowid = fd.fk_element";
3928 $sql .= " AND cf.fk_soc = s.rowid";
3929 $sql .= " AND cf.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
3930 $sql .= " AND fd.fk_product = ".((int) $this->id);
3931 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3932 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = cf.fk_soc AND sc.fk_user = ".((int) $user->id);
3933 }
3934 if ($socid > 0) {
3935 $sql .= " AND cf.fk_soc = ".((int) $socid);
3936 }
3937 if ($filtrestatut != '') {
3938 $sql .= " AND cf.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3939 }
3940 if (!empty($dateofvirtualstock)) {
3941 $sql .= " AND fd.datec <= '".$this->db->idate($dateofvirtualstock)."'";
3942 }
3943
3944 $result = $this->db->query($sql);
3945 if ($result) {
3946 $obj = $this->db->fetch_object($result);
3947 $this->stats_reception['suppliers'] = $obj->nb_suppliers;
3948 $this->stats_reception['nb'] = $obj->nb;
3949 $this->stats_reception['rows'] = $obj->nb_rows;
3950 $this->stats_reception['qty'] = $obj->qty ? $obj->qty : 0;
3951
3952 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3953 $reshook = $hookmanager->executeHooks('loadStatsReception', $parameters, $this, $action);
3954 if ($reshook > 0) {
3955 $this->stats_reception = $hookmanager->resArray['stats_reception'];
3956 }
3957
3958 return 1;
3959 } else {
3960 $this->error = $this->db->error();
3961 return -1;
3962 }
3963 }
3964
3965 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3976 public function load_stats_inproduction($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null, $warehouseid = 0)
3977 {
3978 // phpcs:enable
3979 global $user, $hookmanager, $action;
3980
3981 $serviceStockIsEnabled = isModEnabled("service") && getDolGlobalString('STOCK_SUPPORTS_SERVICES');
3982
3983 $sql = "SELECT COUNT(DISTINCT m.fk_soc) as nb_customers, COUNT(DISTINCT m.rowid) as nb,";
3984 $sql .= " COUNT(mp.rowid) as nb_rows, SUM(mp.qty) as qty, role";
3985 $sql .= " FROM ".$this->db->prefix()."mrp_production as mp";
3986 $sql .= ", ".$this->db->prefix()."mrp_mo as m";
3987 $sql .= " LEFT JOIN ".$this->db->prefix()."societe as s ON s.rowid = m.fk_soc";
3988 $sql .= " WHERE m.rowid = mp.fk_mo";
3989 $sql .= " AND m.entity IN (".getEntity(($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE')) ? 'stock' : 'mrp').")";
3990 $sql .= " AND mp.fk_product = ".((int) $this->id);
3991 $sql .= " AND (mp.disable_stock_change IN (0) OR mp.disable_stock_change IS NULL)";
3992 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3993 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = m.fk_soc AND sc.fk_user = ".((int) $user->id);
3994 }
3995 if ($socid > 0) {
3996 $sql .= " AND m.fk_soc = ".((int) $socid);
3997 }
3998 if ($filtrestatut != '') {
3999 $sql .= " AND m.status IN (".$this->db->sanitize($filtrestatut).")";
4000 }
4001 if (!empty($dateofvirtualstock)) {
4002 $sql .= " AND m.date_valid <= '".$this->db->idate($dateofvirtualstock)."'"; // better date to code ? end of production ?
4003 }
4004 if (!$serviceStockIsEnabled) {
4005 $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))";
4006 }
4007 if (!empty($warehouseid)) {
4008 $sql .= " AND m.fk_warehouse = ".((int) $warehouseid);
4009 }
4010 $sql .= " GROUP BY role";
4011
4012 if ($warehouseid) {
4013 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
4014 } else {
4015 $this->stats_mrptoconsume['customers'] = 0;
4016 $this->stats_mrptoconsume['nb'] = 0;
4017 $this->stats_mrptoconsume['rows'] = 0;
4018 $this->stats_mrptoconsume['qty'] = 0.0;
4019 $this->stats_mrptoproduce['customers'] = 0;
4020 $this->stats_mrptoproduce['nb'] = 0;
4021 $this->stats_mrptoproduce['rows'] = 0;
4022 $this->stats_mrptoproduce['qty'] = 0.0;
4023 }
4024
4025 $result = $this->db->query($sql);
4026 if ($result) {
4027 while ($obj = $this->db->fetch_object($result)) {
4028 if ($obj->role == 'toconsume' && empty($warehouseid)) {
4029 $this->stats_mrptoconsume['customers'] += (int) $obj->nb_customers;
4030 $this->stats_mrptoconsume['nb'] += (int) $obj->nb;
4031 $this->stats_mrptoconsume['rows'] += (int) $obj->nb_rows;
4032 $this->stats_mrptoconsume['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4033 }
4034 if ($obj->role == 'consumed' && empty($warehouseid)) {
4035 //$this->stats_mrptoconsume['customers'] += $obj->nb_customers;
4036 //$this->stats_mrptoconsume['nb'] += $obj->nb;
4037 //$this->stats_mrptoconsume['rows'] += $obj->nb_rows;
4038 $this->stats_mrptoconsume['qty'] -= ($obj->qty ? (float) $obj->qty : 0.0);
4039 }
4040 if ($obj->role == 'toproduce') {
4041 if ($warehouseid) {
4042 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4043 } else {
4044 $this->stats_mrptoproduce['customers'] += (int) $obj->nb_customers;
4045 $this->stats_mrptoproduce['nb'] += (int) $obj->nb;
4046 $this->stats_mrptoproduce['rows'] += (int) $obj->nb_rows;
4047 $this->stats_mrptoproduce['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4048 }
4049 }
4050 if ($obj->role == 'produced') {
4051 //$this->stats_mrptoproduce['customers'] += $obj->nb_customers;
4052 //$this->stats_mrptoproduce['nb'] += $obj->nb;
4053 //$this->stats_mrptoproduce['rows'] += $obj->nb_rows;
4054 if ($warehouseid) {
4055 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
4056 } else {
4057 $this->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
4058 }
4059 }
4060 }
4061
4062 // Clean data
4063 if ($warehouseid) {
4064 if ($this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] < 0) {
4065 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
4066 }
4067 } else {
4068 if ($this->stats_mrptoconsume['qty'] < 0) {
4069 $this->stats_mrptoconsume['qty'] = 0;
4070 }
4071 if ($this->stats_mrptoproduce['qty'] < 0) {
4072 $this->stats_mrptoproduce['qty'] = 0;
4073 }
4074 }
4075
4076 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
4077 $reshook = $hookmanager->executeHooks('loadStatsInProduction', $parameters, $this, $action);
4078 if ($reshook > 0) {
4079 $this->stats_mrptoproduce = $hookmanager->resArray['stats_mrptoproduce'];
4080 }
4081
4082 return 1;
4083 } else {
4084 $this->error = $this->db->error();
4085 return -1;
4086 }
4087 }
4088
4089 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4096 public function load_stats_contrat($socid = 0)
4097 {
4098 // phpcs:enable
4099 global $user, $hookmanager, $action;
4100
4101 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
4102 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
4103 $sql .= " FROM ".$this->db->prefix()."contratdet as cd";
4104 $sql .= ", ".$this->db->prefix()."contrat as c";
4105 $sql .= ", ".$this->db->prefix()."societe as s";
4106 $sql .= " WHERE c.rowid = cd.fk_contrat";
4107 $sql .= " AND c.fk_soc = s.rowid";
4108 $sql .= " AND c.entity IN (".getEntity('contract').")";
4109 $sql .= " AND cd.fk_product = ".((int) $this->id);
4110 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4111 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
4112 }
4113 //$sql.= " AND c.statut != 0";
4114 if ($socid > 0) {
4115 $sql .= " AND c.fk_soc = ".((int) $socid);
4116 }
4117
4118 $result = $this->db->query($sql);
4119 if ($result) {
4120 $obj = $this->db->fetch_object($result);
4121 $this->stats_contrat['customers'] = $obj->nb_customers;
4122 $this->stats_contrat['nb'] = $obj->nb;
4123 $this->stats_contrat['rows'] = $obj->nb_rows;
4124 $this->stats_contrat['qty'] = $obj->qty ? $obj->qty : 0;
4125
4126 // if it's a virtual product, maybe it is in contract by extension
4127 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4128 $TFather = $this->getFather();
4129 if (is_array($TFather) && !empty($TFather)) {
4130 foreach ($TFather as &$fatherData) {
4131 $pFather = new Product($this->db);
4132 $pFather->id = $fatherData['id'];
4133 $qtyCoef = $fatherData['qty'];
4134
4135 if ($fatherData['incdec']) {
4136 $pFather->load_stats_contrat($socid);
4137
4138 $this->stats_contrat['customers'] += $pFather->stats_contrat['customers'];
4139 $this->stats_contrat['nb'] += $pFather->stats_contrat['nb'];
4140 $this->stats_contrat['rows'] += $pFather->stats_contrat['rows'];
4141 $this->stats_contrat['qty'] += $pFather->stats_contrat['qty'] * $qtyCoef;
4142 }
4143 }
4144 }
4145 }
4146
4147 $parameters = array('socid' => $socid);
4148 $reshook = $hookmanager->executeHooks('loadStatsContract', $parameters, $this, $action);
4149 if ($reshook > 0) {
4150 $this->stats_contrat = $hookmanager->resArray['stats_contrat'];
4151 }
4152
4153 return 1;
4154 } else {
4155 $this->error = $this->db->error().' sql='.$sql;
4156 return -1;
4157 }
4158 }
4159
4160 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4167 public function load_stats_facture($socid = 0)
4168 {
4169 // phpcs:enable
4170 global $user, $hookmanager, $action;
4171
4172 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4173 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(".$this->db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1').") as qty";
4174 $sql .= " FROM ".$this->db->prefix()."facturedet as fd";
4175 $sql .= ", ".$this->db->prefix()."facture as f";
4176 $sql .= ", ".$this->db->prefix()."societe as s";
4177 $sql .= " WHERE f.rowid = fd.fk_facture";
4178 $sql .= " AND f.fk_soc = s.rowid";
4179 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4180 $sql .= " AND fd.fk_product = ".((int) $this->id);
4181 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4182 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4183 }
4184 //$sql.= " AND f.fk_statut != 0";
4185 if ($socid > 0) {
4186 $sql .= " AND f.fk_soc = ".((int) $socid);
4187 }
4188
4189 $result = $this->db->query($sql);
4190 if ($result) {
4191 $obj = $this->db->fetch_object($result);
4192 $this->stats_facture['customers'] = $obj->nb_customers;
4193 $this->stats_facture['nb'] = $obj->nb;
4194 $this->stats_facture['rows'] = $obj->nb_rows;
4195 $this->stats_facture['qty'] = $obj->qty ? $obj->qty : 0;
4196
4197 // if it's a virtual product, maybe it is in invoice by extension
4198 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4199 $TFather = $this->getFather();
4200 if (is_array($TFather) && !empty($TFather)) {
4201 foreach ($TFather as &$fatherData) {
4202 $pFather = new Product($this->db);
4203 $pFather->id = $fatherData['id'];
4204 $qtyCoef = $fatherData['qty'];
4205
4206 if ($fatherData['incdec']) {
4207 $pFather->load_stats_facture($socid);
4208
4209 $this->stats_facture['customers'] += $pFather->stats_facture['customers'];
4210 $this->stats_facture['nb'] += $pFather->stats_facture['nb'];
4211 $this->stats_facture['rows'] += $pFather->stats_facture['rows'];
4212 $this->stats_facture['qty'] += $pFather->stats_facture['qty'] * $qtyCoef;
4213 }
4214 }
4215 }
4216 }
4217
4218 $parameters = array('socid' => $socid);
4219 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoice', $parameters, $this, $action);
4220 if ($reshook > 0) {
4221 $this->stats_facture = $hookmanager->resArray['stats_facture'];
4222 }
4223
4224 return 1;
4225 } else {
4226 $this->error = $this->db->error();
4227 return -1;
4228 }
4229 }
4230
4231
4232 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4239 public function load_stats_facturerec($socid = 0)
4240 {
4241 // phpcs:enable
4242 global $user, $hookmanager, $action;
4243
4244 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4245 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4246 $sql .= " FROM ".MAIN_DB_PREFIX."facturedet_rec as fd";
4247 $sql .= ", ".MAIN_DB_PREFIX."facture_rec as f";
4248 $sql .= ", ".MAIN_DB_PREFIX."societe as s";
4249 $sql .= " WHERE f.rowid = fd.fk_facture";
4250 $sql .= " AND f.fk_soc = s.rowid";
4251 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4252 $sql .= " AND fd.fk_product = ".((int) $this->id);
4253 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4254 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4255 }
4256 //$sql.= " AND f.fk_statut != 0";
4257 if ($socid > 0) {
4258 $sql .= " AND f.fk_soc = ".((int) $socid);
4259 }
4260
4261 $result = $this->db->query($sql);
4262 if ($result) {
4263 $obj = $this->db->fetch_object($result);
4264 $this->stats_facturerec['customers'] = (int) $obj->nb_customers;
4265 $this->stats_facturerec['nb'] = (int) $obj->nb;
4266 $this->stats_facturerec['rows'] = (int) $obj->nb_rows;
4267 $this->stats_facturerec['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4268
4269 // if it's a virtual product, maybe it is in invoice by extension
4270 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4271 $TFather = $this->getFather();
4272 if (is_array($TFather) && !empty($TFather)) {
4273 foreach ($TFather as &$fatherData) {
4274 $pFather = new Product($this->db);
4275 $pFather->id = $fatherData['id'];
4276 $qtyCoef = $fatherData['qty'];
4277
4278 if ($fatherData['incdec']) {
4279 $pFather->load_stats_facture($socid);
4280
4281 $this->stats_facturerec['customers'] += $pFather->stats_facturerec['customers'];
4282 $this->stats_facturerec['nb'] += $pFather->stats_facturerec['nb'];
4283 $this->stats_facturerec['rows'] += $pFather->stats_facturerec['rows'];
4284 $this->stats_facturerec['qty'] += $pFather->stats_facturerec['qty'] * $qtyCoef;
4285 }
4286 }
4287 }
4288 }
4289
4290 $parameters = array('socid' => $socid);
4291 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoiceRec', $parameters, $this, $action);
4292 if ($reshook > 0) {
4293 $this->stats_facturerec = $hookmanager->resArray['stats_facturerec'];
4294 }
4295
4296 return 1;
4297 } else {
4298 $this->error = $this->db->error();
4299 return -1;
4300 }
4301 }
4302
4303 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4310 public function load_stats_facture_fournisseur($socid = 0)
4311 {
4312 // phpcs:enable
4313 global $user, $hookmanager, $action;
4314
4315 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
4316 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4317 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as fd";
4318 $sql .= ", ".$this->db->prefix()."facture_fourn as f";
4319 $sql .= ", ".$this->db->prefix()."societe as s";
4320 $sql .= " WHERE f.rowid = fd.fk_facture_fourn";
4321 $sql .= " AND f.fk_soc = s.rowid";
4322 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4323 $sql .= " AND fd.fk_product = ".((int) $this->id);
4324 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4325 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4326 }
4327 //$sql.= " AND f.fk_statut != 0";
4328 if ($socid > 0) {
4329 $sql .= " AND f.fk_soc = ".((int) $socid);
4330 }
4331
4332 $result = $this->db->query($sql);
4333 if ($result) {
4334 $obj = $this->db->fetch_object($result);
4335 $this->stats_facture_fournisseur['suppliers'] = (int) $obj->nb_suppliers;
4336 $this->stats_facture_fournisseur['nb'] = (int) $obj->nb;
4337 $this->stats_facture_fournisseur['rows'] = (int) $obj->nb_rows;
4338 $this->stats_facture_fournisseur['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4339
4340 $parameters = array('socid' => $socid);
4341 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoice', $parameters, $this, $action);
4342 if ($reshook > 0) {
4343 $this->stats_facture_fournisseur = $hookmanager->resArray['stats_facture_fournisseur'];
4344 }
4345
4346 return 1;
4347 } else {
4348 $this->error = $this->db->error();
4349 return -1;
4350 }
4351 }
4352
4353 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4360 public function load_stats_facturefournrec($socid = 0)
4361 {
4362 // phpcs:enable
4363 global $user, $hookmanager, $action;
4364
4365 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
4366 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4367 $sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn_det_rec as fd";
4368 $sql .= ", ".MAIN_DB_PREFIX."facture_fourn_rec as f";
4369 $sql .= ", ".MAIN_DB_PREFIX."societe as s";
4370 if (!$user->hasRight('societe', 'client', 'voir')) {
4371 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
4372 }
4373 $sql .= " WHERE f.rowid = fd.fk_facture";
4374 $sql .= " AND f.fk_soc = s.rowid";
4375 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4376 $sql .= " AND fd.fk_product = ".((int) $this->id);
4377 if (!$user->hasRight('societe', 'client', 'voir')) {
4378 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4379 }
4380 //$sql.= " AND f.fk_statut != 0";
4381 if ($socid > 0) {
4382 $sql .= " AND f.fk_soc = ".((int) $socid);
4383 }
4384
4385 $result = $this->db->query($sql);
4386 if ($result) {
4387 $obj = $this->db->fetch_object($result);
4388 $this->stats_facturefournrec['suppliers'] = (int) $obj->nb_suppliers;
4389 $this->stats_facturefournrec['nb'] = (int) $obj->nb;
4390 $this->stats_facturefournrec['rows'] = (int) $obj->nb_rows;
4391 $this->stats_facturefournrec['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4392
4393 $parameters = array('socid' => $socid);
4394 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoiceRec', $parameters, $this, $action);
4395 if ($reshook > 0) {
4396 $this->stats_facturefournrec = $hookmanager->resArray['stats_facturefournrec'];
4397 }
4398
4399 return 1;
4400 } else {
4401 $this->error = $this->db->error();
4402 return -1;
4403 }
4404 }
4405
4406 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4415 private function _get_stats($sql, $mode, $year = 0)
4416 {
4417 // phpcs:enable
4418 $tab = array();
4419
4420 $resql = $this->db->query($sql);
4421 if ($resql) {
4422 $num = $this->db->num_rows($resql);
4423 $i = 0;
4424 while ($i < $num) {
4425 $arr = $this->db->fetch_array($resql);
4426 if (is_array($arr)) {
4427 $keyfortab = (string) $arr[1];
4428 if ($year == -1) {
4429 $keyfortab = substr($keyfortab, -2);
4430 }
4431
4432 if ($mode == 'byunit') {
4433 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[0]; // 1st field
4434 } elseif ($mode == 'bynumber') {
4435 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4436 } elseif ($mode == 'byamount') {
4437 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4438 } else {
4439 // Bad value for $mode
4440 return -1;
4441 }
4442 }
4443 $i++;
4444 }
4445 } else {
4446 $this->error = $this->db->error().' sql='.$sql;
4447 return -1;
4448 }
4449
4450 if (empty($year)) {
4451 $year = dol_print_date(time(), '%Y');
4452 $month = dol_print_date(time(), '%m');
4453 } elseif ($year == -1) {
4454 $year = '';
4455 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4456 } else {
4457 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4458 }
4459
4460 $result = array();
4461
4462 for ($j = 0; $j < 12; $j++) {
4463 // $ids is 'D', 'N', 'O', 'S', ... (First letter of month in user language)
4464 $idx = ucfirst(dol_trunc(dol_print_date(dol_mktime(12, 0, 0, $month, 1, 1970), "%b"), 1, 'right', 'UTF-8', 1));
4465
4466 //print $idx.'-'.$year.'-'.$month.'<br>';
4467 $result[$j] = array($idx, isset($tab[$year.$month]) ? $tab[$year.$month] : 0);
4468 // $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0);
4469
4470 $month = "0".($month - 1);
4471 if (dol_strlen($month) == 3) {
4472 $month = substr($month, 1);
4473 }
4474 if ($month == 0) {
4475 $month = 12;
4476 if ($year !== '') { // $year is '' when we want stats for all years
4477 $year -= 1;
4478 }
4479 }
4480 }
4481
4482 return array_reverse($result);
4483 }
4484
4485
4486 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4497 public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4498 {
4499 // phpcs:enable
4500 global $user;
4501
4502 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4503 if ($mode == 'bynumber') {
4504 $sql .= ", count(DISTINCT f.rowid)";
4505 }
4506 $sql .= ", sum(d.total_ht) as total_ht";
4507 $sql .= " FROM ".$this->db->prefix()."facturedet as d, ".$this->db->prefix()."facture as f, ".$this->db->prefix()."societe as s";
4508 if ($filteronproducttype >= 0) {
4509 $sql .= ", ".$this->db->prefix()."product as p";
4510 }
4511 if (!$user->hasRight('societe', 'client', 'voir')) {
4512 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4513 }
4514 $sql .= " WHERE f.rowid = d.fk_facture";
4515 if ($this->id > 0) {
4516 $sql .= " AND d.fk_product = ".((int) $this->id);
4517 } else {
4518 $sql .= " AND d.fk_product > 0";
4519 }
4520 if ($filteronproducttype >= 0) {
4521 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4522 }
4523 $sql .= " AND f.fk_soc = s.rowid";
4524 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4525 if (!$user->hasRight('societe', 'client', 'voir')) {
4526 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4527 }
4528 if ($socid > 0) {
4529 $sql .= " AND f.fk_soc = $socid";
4530 }
4531 $sql .= $morefilter;
4532 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4533 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4534
4535 return $this->_get_stats($sql, $mode, $year);
4536 }
4537
4538
4539 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4550 public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4551 {
4552 // phpcs:enable
4553 global $user;
4554
4555 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4556 if ($mode == 'bynumber') {
4557 $sql .= ", count(DISTINCT f.rowid)";
4558 }
4559 $sql .= ", sum(d.total_ht) as total_ht";
4560 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as d, ".$this->db->prefix()."facture_fourn as f, ".$this->db->prefix()."societe as s";
4561 if ($filteronproducttype >= 0) {
4562 $sql .= ", ".$this->db->prefix()."product as p";
4563 }
4564 if (!$user->hasRight('societe', 'client', 'voir')) {
4565 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4566 }
4567 $sql .= " WHERE f.rowid = d.fk_facture_fourn";
4568 if ($this->id > 0) {
4569 $sql .= " AND d.fk_product = ".((int) $this->id);
4570 } else {
4571 $sql .= " AND d.fk_product > 0";
4572 }
4573 if ($filteronproducttype >= 0) {
4574 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4575 }
4576 $sql .= " AND f.fk_soc = s.rowid";
4577 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4578 if (!$user->hasRight('societe', 'client', 'voir')) {
4579 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4580 }
4581 if ($socid > 0) {
4582 $sql .= " AND f.fk_soc = $socid";
4583 }
4584 $sql .= $morefilter;
4585 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4586 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4587
4588 return $this->_get_stats($sql, $mode, $year);
4589 }
4590
4591 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4602 public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4603 {
4604 // phpcs:enable
4605 global $user;
4606
4607 $sql = "SELECT sum(d.qty) as qty, date_format(p.datep, '%Y%m')";
4608 if ($mode == 'bynumber') {
4609 $sql .= ", count(DISTINCT p.rowid)";
4610 }
4611 $sql .= ", sum(d.total_ht) as total_ht";
4612 $sql .= " FROM ".$this->db->prefix()."propaldet as d, ".$this->db->prefix()."propal as p, ".$this->db->prefix()."societe as s";
4613 if ($filteronproducttype >= 0) {
4614 $sql .= ", ".$this->db->prefix()."product as prod";
4615 }
4616 if (!$user->hasRight('societe', 'client', 'voir')) {
4617 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4618 }
4619 $sql .= " WHERE p.rowid = d.fk_propal";
4620 if ($this->id > 0) {
4621 $sql .= " AND d.fk_product = ".((int) $this->id);
4622 } else {
4623 $sql .= " AND d.fk_product > 0";
4624 }
4625 if ($filteronproducttype >= 0) {
4626 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4627 }
4628 $sql .= " AND p.fk_soc = s.rowid";
4629 $sql .= " AND p.entity IN (".getEntity('propal').")";
4630 if (!$user->hasRight('societe', 'client', 'voir')) {
4631 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4632 }
4633 if ($socid > 0) {
4634 $sql .= " AND p.fk_soc = ".((int) $socid);
4635 }
4636 $sql .= $morefilter;
4637 $sql .= " GROUP BY date_format(p.datep,'%Y%m')";
4638 $sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC";
4639
4640 return $this->_get_stats($sql, $mode, $year);
4641 }
4642
4643 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4654 public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4655 {
4656 // phpcs:enable
4657 global $user;
4658
4659 $sql = "SELECT sum(d.qty) as qty, date_format(p.date_valid, '%Y%m')";
4660 if ($mode == 'bynumber') {
4661 $sql .= ", count(DISTINCT p.rowid)";
4662 }
4663 $sql .= ", sum(d.total_ht) as total_ht";
4664 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as d, ".$this->db->prefix()."supplier_proposal as p, ".$this->db->prefix()."societe as s";
4665 if ($filteronproducttype >= 0) {
4666 $sql .= ", ".$this->db->prefix()."product as prod";
4667 }
4668 if (!$user->hasRight('societe', 'client', 'voir')) {
4669 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4670 }
4671 $sql .= " WHERE p.rowid = d.fk_supplier_proposal";
4672 if ($this->id > 0) {
4673 $sql .= " AND d.fk_product = ".((int) $this->id);
4674 } else {
4675 $sql .= " AND d.fk_product > 0";
4676 }
4677 if ($filteronproducttype >= 0) {
4678 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4679 }
4680 $sql .= " AND p.fk_soc = s.rowid";
4681 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
4682 if (!$user->hasRight('societe', 'client', 'voir')) {
4683 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4684 }
4685 if ($socid > 0) {
4686 $sql .= " AND p.fk_soc = ".((int) $socid);
4687 }
4688 $sql .= $morefilter;
4689 $sql .= " GROUP BY date_format(p.date_valid,'%Y%m')";
4690 $sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
4691
4692 return $this->_get_stats($sql, $mode, $year);
4693 }
4694
4695 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4706 public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4707 {
4708 // phpcs:enable
4709 global $user;
4710
4711 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4712 if ($mode == 'bynumber') {
4713 $sql .= ", count(DISTINCT c.rowid)";
4714 }
4715 $sql .= ", sum(d.total_ht) as total_ht";
4716 $sql .= " FROM ".$this->db->prefix()."commandedet as d, ".$this->db->prefix()."commande as c, ".$this->db->prefix()."societe as s";
4717 if ($filteronproducttype >= 0) {
4718 $sql .= ", ".$this->db->prefix()."product as p";
4719 }
4720 if (!$user->hasRight('societe', 'client', 'voir')) {
4721 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4722 }
4723 $sql .= " WHERE c.rowid = d.fk_commande";
4724 if ($this->id > 0) {
4725 $sql .= " AND d.fk_product = ".((int) $this->id);
4726 } else {
4727 $sql .= " AND d.fk_product > 0";
4728 }
4729 if ($filteronproducttype >= 0) {
4730 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4731 }
4732 $sql .= " AND c.fk_soc = s.rowid";
4733 $sql .= " AND c.entity IN (".getEntity('commande').")";
4734 if (!$user->hasRight('societe', 'client', 'voir')) {
4735 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4736 }
4737 if ($socid > 0) {
4738 $sql .= " AND c.fk_soc = ".((int) $socid);
4739 }
4740 $sql .= $morefilter;
4741 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4742 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4743
4744 return $this->_get_stats($sql, $mode, $year);
4745 }
4746
4747 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4758 public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4759 {
4760 // phpcs:enable
4761 global $user;
4762
4763 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4764 if ($mode == 'bynumber') {
4765 $sql .= ", count(DISTINCT c.rowid)";
4766 }
4767 $sql .= ", sum(d.total_ht) as total_ht";
4768 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as d, ".$this->db->prefix()."commande_fournisseur as c, ".$this->db->prefix()."societe as s";
4769 if ($filteronproducttype >= 0) {
4770 $sql .= ", ".$this->db->prefix()."product as p";
4771 }
4772 if (!$user->hasRight('societe', 'client', 'voir')) {
4773 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4774 }
4775 $sql .= " WHERE c.rowid = d.fk_commande";
4776 if ($this->id > 0) {
4777 $sql .= " AND d.fk_product = ".((int) $this->id);
4778 } else {
4779 $sql .= " AND d.fk_product > 0";
4780 }
4781 if ($filteronproducttype >= 0) {
4782 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4783 }
4784 $sql .= " AND c.fk_soc = s.rowid";
4785 $sql .= " AND c.entity IN (".getEntity('supplier_order').")";
4786 if (!$user->hasRight('societe', 'client', 'voir')) {
4787 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4788 }
4789 if ($socid > 0) {
4790 $sql .= " AND c.fk_soc = ".((int) $socid);
4791 }
4792 $sql .= $morefilter;
4793 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4794 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4795
4796 return $this->_get_stats($sql, $mode, $year);
4797 }
4798
4799 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4810 public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4811 {
4812 // phpcs:enable
4813 global $user;
4814
4815 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_contrat, '%Y%m')";
4816 if ($mode == 'bynumber') {
4817 $sql .= ", count(DISTINCT c.rowid)";
4818 }
4819 $sql .= ", sum(d.total_ht) as total_ht";
4820 $sql .= " FROM ".$this->db->prefix()."contratdet as d, ".$this->db->prefix()."contrat as c, ".$this->db->prefix()."societe as s";
4821 if ($filteronproducttype >= 0) {
4822 $sql .= ", ".$this->db->prefix()."product as p";
4823 }
4824 if (!$user->hasRight('societe', 'client', 'voir')) {
4825 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4826 }
4827 $sql .= " WHERE c.entity IN (".getEntity('contract').")";
4828 $sql .= " AND c.rowid = d.fk_contrat";
4829
4830 if ($this->id > 0) {
4831 $sql .= " AND d.fk_product = ".((int) $this->id);
4832 } else {
4833 $sql .= " AND d.fk_product > 0";
4834 }
4835 if ($filteronproducttype >= 0) {
4836 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4837 }
4838 $sql .= " AND c.fk_soc = s.rowid";
4839
4840 if (!$user->hasRight('societe', 'client', 'voir')) {
4841 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4842 }
4843 if ($socid > 0) {
4844 $sql .= " AND c.fk_soc = ".((int) $socid);
4845 }
4846 $sql .= $morefilter;
4847 $sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')";
4848 $sql .= " ORDER BY date_format(c.date_contrat,'%Y%m') DESC";
4849
4850 return $this->_get_stats($sql, $mode, $year);
4851 }
4852
4853 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4864 public function get_nb_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4865 {
4866 // phpcs:enable
4867 global $user;
4868
4869 $sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')";
4870 if ($mode == 'bynumber') {
4871 $sql .= ", count(DISTINCT d.rowid)";
4872 }
4873 $sql .= " FROM ".$this->db->prefix()."mrp_mo as d LEFT JOIN ".$this->db->prefix()."societe as s ON d.fk_soc = s.rowid";
4874 if ($filteronproducttype >= 0) {
4875 $sql .= ", ".$this->db->prefix()."product as p";
4876 }
4877 if (!$user->hasRight('societe', 'client', 'voir')) {
4878 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4879 }
4880
4881 $sql .= " WHERE d.entity IN (".getEntity('mo').")";
4882 $sql .= " AND d.status > 0";
4883
4884 if ($this->id > 0) {
4885 $sql .= " AND d.fk_product = ".((int) $this->id);
4886 } else {
4887 $sql .= " AND d.fk_product > 0";
4888 }
4889 if ($filteronproducttype >= 0) {
4890 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4891 }
4892
4893 if (!$user->hasRight('societe', 'client', 'voir')) {
4894 $sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4895 }
4896 if ($socid > 0) {
4897 $sql .= " AND d.fk_soc = ".((int) $socid);
4898 }
4899 $sql .= $morefilter;
4900 $sql .= " GROUP BY date_format(d.date_valid,'%Y%m')";
4901 $sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC";
4902
4903 return $this->_get_stats($sql, $mode, $year);
4904 }
4905
4906 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4917 public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
4918 {
4919 global $user;
4920
4921 // phpcs:enable
4922 // Clean parameters
4923 if (!is_numeric($id_pere)) {
4924 $id_pere = 0;
4925 }
4926 if (!is_numeric($id_fils)) {
4927 $id_fils = 0;
4928 }
4929 if (!is_numeric($incdec)) {
4930 $incdec = 0;
4931 }
4932
4933 $result = $this->del_sousproduit($id_pere, $id_fils);
4934 if ($result < 0) {
4935 return $result;
4936 }
4937
4938 // Check not already father of id_pere (to avoid father -> child -> father links)
4939 $sql = "SELECT fk_product_pere from ".$this->db->prefix()."product_association";
4940 $sql .= " WHERE fk_product_pere = ".((int) $id_fils)." AND fk_product_fils = ".((int) $id_pere);
4941 if (!$this->db->query($sql)) {
4942 dol_print_error($this->db);
4943 return -1;
4944 } else {
4945 //Selection of the highest row
4946 $sql = "SELECT MAX(rang) as max_rank FROM ".$this->db->prefix()."product_association";
4947 $sql .= " WHERE fk_product_pere = ".((int) $id_pere);
4948 $resql = $this->db->query($sql);
4949 if ($resql) {
4950 $obj = $this->db->fetch_object($resql);
4951 $rank = $obj->max_rank + 1;
4952 //Addition of a product with the highest rank +1
4953 $sql = "INSERT INTO ".$this->db->prefix()."product_association(fk_product_pere,fk_product_fils,qty,incdec,rang)";
4954 $sql .= " VALUES (".((int) $id_pere).", ".((int) $id_fils).", ".price2num($qty, 'MS').", ".((int) $incdec).", ".((int) $rank).")";
4955 if (! $this->db->query($sql)) {
4956 dol_print_error($this->db);
4957 return -1;
4958 } else {
4959 if (!$notrigger) {
4960 // Call trigger
4961 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_ADD', $user);
4962 if ($result < 0) {
4963 $this->error = $this->db->lasterror();
4964 dol_syslog(get_class($this).'::addSubproduct error='.$this->error, LOG_ERR);
4965 return -1;
4966 }
4967 }
4968 // End call triggers
4969
4970 return 1;
4971 }
4972 } else {
4973 dol_print_error($this->db);
4974 return -1;
4975 }
4976 }
4977 }
4978
4979 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4990 public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
4991 {
4992 global $user;
4993
4994 // phpcs:enable
4995 // Clean parameters
4996 if (!is_numeric($id_pere)) {
4997 $id_pere = 0;
4998 }
4999 if (!is_numeric($id_fils)) {
5000 $id_fils = 0;
5001 }
5002 if (!is_numeric($incdec)) {
5003 $incdec = 1;
5004 }
5005 if (!is_numeric($qty)) {
5006 $qty = 1;
5007 }
5008
5009 $sql = 'UPDATE '.$this->db->prefix().'product_association SET ';
5010 $sql .= 'qty = '.price2num($qty, 'MS');
5011 $sql .= ',incdec = '.((int) $incdec);
5012 $sql .= ' WHERE fk_product_pere = '.((int) $id_pere).' AND fk_product_fils = '.((int) $id_fils);
5013
5014 if (!$this->db->query($sql)) {
5015 dol_print_error($this->db);
5016 return -1;
5017 } else {
5018 if (!$notrigger) {
5019 // Call trigger
5020 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_UPDATE', $user);
5021 if ($result < 0) {
5022 $this->error = $this->db->lasterror();
5023 dol_syslog(get_class($this).'::updateSubproduct error='.$this->error, LOG_ERR);
5024 return -1;
5025 }
5026 // End call triggers
5027 }
5028
5029 return 1;
5030 }
5031 }
5032
5033 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5042 public function del_sousproduit($fk_parent, $fk_child, $notrigger = 0)
5043 {
5044 global $user;
5045
5046 // phpcs:enable
5047 if (!is_numeric($fk_parent)) {
5048 $fk_parent = 0;
5049 }
5050 if (!is_numeric($fk_child)) {
5051 $fk_child = 0;
5052 }
5053
5054 $sql = "DELETE FROM ".$this->db->prefix()."product_association";
5055 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5056 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5057
5058 dol_syslog(get_class($this).'::del_sousproduit', LOG_DEBUG);
5059 if (!$this->db->query($sql)) {
5060 dol_print_error($this->db);
5061 return -1;
5062 }
5063
5064 // Updated ranks so that none are missing
5065 $sqlrank = "SELECT rowid, rang FROM ".$this->db->prefix()."product_association";
5066 $sqlrank .= " WHERE fk_product_pere = ".((int) $fk_parent);
5067 $sqlrank .= " ORDER BY rang";
5068 $resqlrank = $this->db->query($sqlrank);
5069 if ($resqlrank) {
5070 $cpt = 0;
5071 while ($objrank = $this->db->fetch_object($resqlrank)) {
5072 $cpt++;
5073 $sql = "UPDATE ".$this->db->prefix()."product_association";
5074 $sql .= " SET rang = ".((int) $cpt);
5075 $sql .= " WHERE rowid = ".((int) $objrank->rowid);
5076 if (! $this->db->query($sql)) {
5077 dol_print_error($this->db);
5078 return -1;
5079 }
5080 }
5081 }
5082
5083 if (!$notrigger) {
5084 // Call trigger
5085 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_DELETE', $user);
5086 if ($result < 0) {
5087 $this->error = $this->db->lasterror();
5088 dol_syslog(get_class($this).'::delSubproduct error='.$this->error, LOG_ERR);
5089 return -1;
5090 }
5091 // End call triggers
5092 }
5093
5094 return 1;
5095 }
5096
5097 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5105 public function is_sousproduit($fk_parent, $fk_child)
5106 {
5107 // phpcs:enable
5108 $sql = "SELECT fk_product_pere, qty, incdec";
5109 $sql .= " FROM ".$this->db->prefix()."product_association";
5110 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5111 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5112
5113 $result = $this->db->query($sql);
5114 if ($result) {
5115 $num = $this->db->num_rows($result);
5116
5117 if ($num > 0) {
5118 $obj = $this->db->fetch_object($result);
5119
5120 $this->is_sousproduit_qty = $obj->qty;
5121 $this->is_sousproduit_incdec = $obj->incdec;
5122
5123 return 1;
5124 } else {
5125 return 0;
5126 }
5127 } else {
5128 dol_print_error($this->db);
5129 return -1;
5130 }
5131 }
5132
5133
5134 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5145 public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
5146 {
5147 // phpcs:enable
5148 global $conf;
5149
5150 $now = dol_now();
5151
5152 dol_syslog(get_class($this)."::add_fournisseur id_fourn = ".$id_fourn." ref_fourn=".$ref_fourn." quantity=".$quantity, LOG_DEBUG);
5153
5154 // Clean parameters
5155 $quantity = price2num($quantity, 'MS');
5156
5157 if ($ref_fourn) {
5158 // Check if ref is not already used
5159 $sql = "SELECT rowid, fk_product";
5160 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5161 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5162 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5163 $sql .= " AND fk_product <> ".((int) $this->id);
5164 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5165
5166 $resql = $this->db->query($sql);
5167 if ($resql) {
5168 $obj = $this->db->fetch_object($resql);
5169 if ($obj) {
5170 // If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
5171 $this->product_id_already_linked = $obj->fk_product;
5172 return -3;
5173 }
5174 $this->db->free($resql);
5175 }
5176 }
5177
5178 $sql = "SELECT rowid";
5179 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5180 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5181 if ($ref_fourn) {
5182 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5183 } else {
5184 $sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
5185 }
5186 $sql .= " AND quantity = ".((float) $quantity);
5187 $sql .= " AND fk_product = ".((int) $this->id);
5188 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5189
5190 $resql = $this->db->query($sql);
5191 if ($resql) {
5192 $obj = $this->db->fetch_object($resql);
5193
5194 // The reference supplier does not exist, we create it for this product.
5195 if (empty($obj)) {
5196 $sql = "INSERT INTO ".$this->db->prefix()."product_fournisseur_price(";
5197 $sql .= "datec";
5198 $sql .= ", entity";
5199 $sql .= ", fk_product";
5200 $sql .= ", fk_soc";
5201 $sql .= ", ref_fourn";
5202 $sql .= ", quantity";
5203 $sql .= ", fk_user";
5204 $sql .= ", tva_tx";
5205 $sql .= ") VALUES (";
5206 $sql .= "'".$this->db->idate($now)."'";
5207 $sql .= ", ".((int) $conf->entity);
5208 $sql .= ", ".((int) $this->id);
5209 $sql .= ", ".((int) $id_fourn);
5210 $sql .= ", '".$this->db->escape($ref_fourn)."'";
5211 $sql .= ", ".((float) $quantity);
5212 $sql .= ", ".((int) $user->id);
5213 $sql .= ", 0";
5214 $sql .= ")";
5215
5216 if ($this->db->query($sql)) {
5217 $this->product_fourn_price_id = $this->db->last_insert_id($this->db->prefix()."product_fournisseur_price");
5218 return 1;
5219 } else {
5220 $this->error = $this->db->lasterror();
5221 return -1;
5222 }
5223 } else {
5224 // If the supplier price already exists for this product and quantity
5225 $this->product_fourn_price_id = $obj->rowid;
5226 return 0;
5227 }
5228 } else {
5229 $this->error = $this->db->lasterror();
5230 return -2;
5231 }
5232 }
5233
5234
5235 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5241 public function list_suppliers()
5242 {
5243 // phpcs:enable
5244 global $conf;
5245
5246 $list = array();
5247
5248 $sql = "SELECT DISTINCT p.fk_soc";
5249 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as p";
5250 $sql .= " WHERE p.fk_product = ".((int) $this->id);
5251 $sql .= " AND p.entity = ".((int) $conf->entity);
5252
5253 $result = $this->db->query($sql);
5254 if ($result) {
5255 $num = $this->db->num_rows($result);
5256 $i = 0;
5257 while ($i < $num) {
5258 $obj = $this->db->fetch_object($result);
5259 $list[$i] = $obj->fk_soc;
5260 $i++;
5261 }
5262 }
5263
5264 return $list;
5265 }
5266
5267 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5275 public function clone_price($fromId, $toId)
5276 {
5277 global $user;
5278
5279 $now = dol_now();
5280
5281 $this->db->begin();
5282
5283 // prices
5284 $sql = "INSERT INTO ".$this->db->prefix()."product_price (";
5285 $sql .= " entity";
5286 $sql .= ", fk_product";
5287 $sql .= ", date_price";
5288 $sql .= ", price_level";
5289 $sql .= ", price";
5290 $sql .= ", price_ttc";
5291 $sql .= ", price_min";
5292 $sql .= ", price_min_ttc";
5293 $sql .= ", price_base_type";
5294 $sql .= ", price_label";
5295 $sql .= ", default_vat_code";
5296 $sql .= ", tva_tx";
5297 $sql .= ", recuperableonly";
5298 $sql .= ", localtax1_tx";
5299 $sql .= ", localtax1_type";
5300 $sql .= ", localtax2_tx";
5301 $sql .= ", localtax2_type";
5302 $sql .= ", fk_user_author";
5303 $sql .= ", tosell";
5304 $sql .= ", price_by_qty";
5305 $sql .= ", fk_price_expression";
5306 $sql .= ", fk_multicurrency";
5307 $sql .= ", multicurrency_code";
5308 $sql .= ", multicurrency_tx";
5309 $sql .= ", multicurrency_price";
5310 $sql .= ", multicurrency_price_ttc";
5311 $sql .= ")";
5312 $sql .= " SELECT";
5313 $sql .= " entity";
5314 $sql .= ", ".((int) $toId);
5315 $sql .= ", '".$this->db->idate($now)."'";
5316 $sql .= ", price_level";
5317 $sql .= ", price";
5318 $sql .= ", price_ttc";
5319 $sql .= ", price_min";
5320 $sql .= ", price_min_ttc";
5321 $sql .= ", price_base_type";
5322 $sql .= ", price_label";
5323 $sql .= ", default_vat_code";
5324 $sql .= ", tva_tx";
5325 $sql .= ", recuperableonly";
5326 $sql .= ", localtax1_tx";
5327 $sql .= ", localtax1_type";
5328 $sql .= ", localtax2_tx";
5329 $sql .= ", localtax2_type";
5330 $sql .= ", ".((int) $user->id);
5331 $sql .= ", tosell";
5332 $sql .= ", price_by_qty";
5333 $sql .= ", fk_price_expression";
5334 $sql .= ", fk_multicurrency";
5335 $sql .= ", multicurrency_code";
5336 $sql .= ", multicurrency_tx";
5337 $sql .= ", multicurrency_price";
5338 $sql .= ", multicurrency_price_ttc";
5339 $sql .= " FROM ".$this->db->prefix()."product_price ps";
5340 $sql .= " WHERE fk_product = ".((int) $fromId);
5341 $sql .= " AND date_price IN (SELECT MAX(pd.date_price) FROM ".$this->db->prefix()."product_price pd WHERE pd.fk_product = ".((int) $fromId)." AND pd.price_level = ps.price_level)";
5342 $sql .= " ORDER BY date_price DESC";
5343
5344 dol_syslog(__METHOD__, LOG_DEBUG);
5345 $resql = $this->db->query($sql);
5346 if (!$resql) {
5347 $this->db->rollback();
5348 return -1;
5349 }
5350
5351 $this->db->commit();
5352 return 1;
5353 }
5354
5355 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5363 public function clone_associations($fromId, $toId)
5364 {
5365 // phpcs:enable
5366 $this->db->begin();
5367
5368 $sql = 'INSERT INTO '.$this->db->prefix().'product_association (fk_product_pere, fk_product_fils, qty, incdec)';
5369 $sql .= " SELECT ".((int) $toId).", fk_product_fils, qty, incdec FROM ".$this->db->prefix()."product_association";
5370 $sql .= " WHERE fk_product_pere = ".((int) $fromId);
5371
5372 dol_syslog(get_class($this).'::clone_association', LOG_DEBUG);
5373 if (!$this->db->query($sql)) {
5374 $this->db->rollback();
5375 return -1;
5376 }
5377
5378 $this->db->commit();
5379 return 1;
5380 }
5381
5382 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5390 public function clone_fournisseurs($fromId, $toId)
5391 {
5392 // phpcs:enable
5393 $this->db->begin();
5394
5395 $now = dol_now();
5396
5397 // les fournisseurs
5398 /*$sql = "INSERT ".$this->db->prefix()."product_fournisseur ("
5399 . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
5400 . " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, ref_fourn, fk_user_author"
5401 . " FROM ".$this->db->prefix()."product_fournisseur"
5402 . " WHERE fk_product = ".((int) $fromId);
5403
5404 if ( ! $this->db->query($sql ) )
5405 {
5406 $this->db->rollback();
5407 return -1;
5408 }*/
5409
5410 // les prix de fournisseurs.
5411 $sql = "INSERT ".$this->db->prefix()."product_fournisseur_price (";
5412 $sql .= " datec, fk_product, fk_soc, price, quantity, fk_user, tva_tx)";
5413 $sql .= " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, price, quantity, fk_user, tva_tx";
5414 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5415 $sql .= " WHERE fk_product = ".((int) $fromId);
5416
5417 dol_syslog(get_class($this).'::clone_fournisseurs', LOG_DEBUG);
5418 $resql = $this->db->query($sql);
5419 if (!$resql) {
5420 $this->db->rollback();
5421 return -1;
5422 } else {
5423 $this->db->commit();
5424 return 1;
5425 }
5426 }
5427
5428 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5441 public function fetch_prod_arbo($prod, $compl_path = '', $multiply = 1, $level = 1, $id_parent = 0, $ignore_stock_load = 0)
5442 {
5443 // phpcs:enable
5444 $tmpproduct = null;
5445
5446 if ($multiply < 1) {
5447 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);
5448 $multiply = 1;
5449 }
5450
5451 //var_dump($prod);
5452 foreach ($prod as $id_product => $desc_pere) { // $id_product is 0 (first call starting with root top) or an id of a sub_product
5453 if (is_array($desc_pere)) { // If desc_pere is an array, this means it's a child
5454 $id = (!empty($desc_pere[0]) ? $desc_pere[0] : '');
5455 $nb = (!empty($desc_pere[1]) ? $desc_pere[1] : '');
5456 $type = (!empty($desc_pere[2]) ? $desc_pere[2] : '');
5457 $label = (!empty($desc_pere[3]) ? $desc_pere[3] : '');
5458 $incdec = (!empty($desc_pere[4]) ? $desc_pere[4] : 0);
5459
5460 //print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n";
5461 if (is_null($tmpproduct)) {
5462 $tmpproduct = new Product($this->db); // So we initialize tmpproduct only once for all loop.
5463 }
5464 $tmpproduct->fetch($id); // Load product to get ->ref
5465
5466 if (empty($ignore_stock_load) && ($tmpproduct->isProduct() || getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) {
5467 $tmpproduct->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
5468 }
5469
5470 $this->res[] = array(
5471 'id' => $id, // Id product
5472 'id_parent' => $id_parent,
5473 'ref' => $tmpproduct->ref, // Ref product
5474 'nb' => $nb, // Nb of units that compose parent product
5475 'nb_total' => $nb * $multiply, // Nb of units for all nb of product
5476 'stock' => $tmpproduct->stock_reel, // Stock
5477 'stock_alert' => $tmpproduct->seuil_stock_alerte, // Stock alert
5478 'label' => $label,
5479 'fullpath' => $compl_path.$label, // Label
5480 'type' => $type, // Nb of units that compose parent product
5481 'desiredstock' => $tmpproduct->desiredstock,
5482 'level' => $level,
5483 'incdec' => $incdec,
5484 'entity' => $tmpproduct->entity
5485 );
5486
5487 // Recursive call if there child has children of its own
5488 if (isset($desc_pere['childs']) && is_array($desc_pere['childs'])) {
5489 if (!is_int($desc_pere[1] * $multiply)) {
5490 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);
5491 }
5492
5493 //print 'YYY We go down for '.$desc_pere[3]." -> \n";
5494 $this->fetch_prod_arbo($desc_pere['childs'], $compl_path.$desc_pere[3]." -> ", (int) ceil($desc_pere[1] * $multiply), $level + 1, $id, $ignore_stock_load);
5495 }
5496 }
5497 }
5498 }
5499
5500 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5509 public function get_arbo_each_prod($multiply = 1, $ignore_stock_load = 0)
5510 {
5511 // phpcs:enable
5512 $this->res = array();
5513 if (isset($this->sousprods) && is_array($this->sousprods)) {
5514 foreach ($this->sousprods as $prod_name => $desc_product) {
5515 if (is_array($desc_product)) {
5516 $this->fetch_prod_arbo($desc_product, "", $multiply, 1, $this->id, $ignore_stock_load); // This set $this->res
5517 }
5518 }
5519 }
5520 //var_dump($res);
5521 return $this->res;
5522 }
5523
5531 public function hasFatherOrChild($mode = 0)
5532 {
5533 $nb = 0;
5534
5535 $sql = "SELECT COUNT(pa.rowid) as nb";
5536 $sql .= " FROM ".$this->db->prefix()."product_association as pa";
5537 if ($mode == 0) {
5538 $sql .= " WHERE pa.fk_product_fils = ".((int) $this->id)." OR pa.fk_product_pere = ".((int) $this->id);
5539 } elseif ($mode == -1) {
5540 $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)
5541 } elseif ($mode == 1) {
5542 $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)
5543 }
5544
5545 $resql = $this->db->query($sql);
5546 if ($resql) {
5547 $obj = $this->db->fetch_object($resql);
5548 if ($obj) {
5549 $nb = $obj->nb;
5550 }
5551 } else {
5552 return -1;
5553 }
5554
5555 return $nb;
5556 }
5557
5563 public function hasVariants()
5564 {
5565 $nb = 0;
5566 $sql = "SELECT count(rowid) as nb FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_parent = ".((int) $this->id);
5567 $sql .= " AND entity IN (".getEntity('product').")";
5568
5569 $resql = $this->db->query($sql);
5570 if ($resql) {
5571 $obj = $this->db->fetch_object($resql);
5572 if ($obj) {
5573 $nb = $obj->nb;
5574 }
5575 }
5576
5577 return $nb;
5578 }
5579
5580
5586 public function isVariant()
5587 {
5588 if (isModEnabled('variants')) {
5589 $sql = "SELECT rowid FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_child = ".((int) $this->id)." AND entity IN (".getEntity('product').")";
5590
5591 $query = $this->db->query($sql);
5592
5593 if ($query) {
5594 if (!$this->db->num_rows($query)) {
5595 return false;
5596 }
5597 return true;
5598 } else {
5599 dol_print_error($this->db);
5600 return -1;
5601 }
5602 } else {
5603 return false;
5604 }
5605 }
5606
5613 public function getFather()
5614 {
5615 $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";
5616 $sql .= ", p.tosell as status, p.tobuy as status_buy";
5617 $sql .= " FROM ".$this->db->prefix()."product_association as pa,";
5618 $sql .= " ".$this->db->prefix()."product as p";
5619 $sql .= " WHERE p.rowid = pa.fk_product_pere";
5620 $sql .= " AND pa.fk_product_fils = ".((int) $this->id);
5621
5622 $res = $this->db->query($sql);
5623 if ($res) {
5624 $prods = array();
5625 while ($record = $this->db->fetch_array($res)) {
5626 // $record['id'] = $record['rowid'] = id of father
5627 $prods[$record['id']] = array();
5628 $prods[$record['id']]['id'] = $record['rowid'];
5629 $prods[$record['id']]['ref'] = $record['ref'];
5630 $prods[$record['id']]['label'] = $record['label'];
5631 $prods[$record['id']]['qty'] = $record['qty'];
5632 $prods[$record['id']]['incdec'] = $record['incdec'];
5633 $prods[$record['id']]['fk_product_type'] = $record['fk_product_type'];
5634 $prods[$record['id']]['entity'] = $record['entity'];
5635 $prods[$record['id']]['status'] = $record['status'];
5636 $prods[$record['id']]['status_buy'] = $record['status_buy'];
5637 }
5638 return $prods;
5639 } else {
5640 dol_print_error($this->db);
5641 return -1;
5642 }
5643 }
5644
5645
5655 public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array())
5656 {
5657 if (empty($id)) {
5658 return array();
5659 }
5660
5661 $sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,";
5662 $sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec,";
5663 $sql .= " pa.rowid as fk_association, pa.rang";
5664 $sql .= " FROM ".$this->db->prefix()."product as p,";
5665 $sql .= " ".$this->db->prefix()."product_association as pa";
5666 $sql .= " WHERE p.rowid = pa.fk_product_fils";
5667 $sql .= " AND pa.fk_product_pere = ".((int) $id);
5668 $sql .= " AND pa.fk_product_fils <> ".((int) $id); // This should not happens, it is to avoid infinite loop if it happens
5669 $sql .= " ORDER BY pa.rang";
5670
5671 dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level. ' parents='.(is_array($parents) ? implode(',', $parents) : $parents), LOG_DEBUG);
5672
5673 // Protection against infinite loop
5674 if ($level > 30) {
5675 return array();
5676 }
5677
5678 $res = $this->db->query($sql);
5679 if ($res) {
5680 $prods = array();
5681 if ($this->db->num_rows($res) > 0) {
5682 $parents[] = $id;
5683 }
5684
5685 while ($rec = $this->db->fetch_array($res)) {
5686 if (in_array($rec['id'], $parents)) {
5687 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);
5688 continue; // We discard this child if it is already found at a higher level in tree in the same branch.
5689 }
5690
5691 $prods[$rec['rowid']] = array(
5692 0 => $rec['rowid'],
5693 1 => $rec['qty'],
5694 2 => $rec['fk_product_type'],
5695 3 => $this->db->escape($rec['label']),
5696 4 => $rec['incdec'],
5697 5 => $rec['ref'],
5698 6 => $rec['fk_association'],
5699 7 => $rec['rang']
5700 );
5701 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
5702 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
5703 if (empty($firstlevelonly)) {
5704 $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, $parents);
5705 foreach ($listofchilds as $keyChild => $valueChild) {
5706 $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
5707 }
5708 }
5709 }
5710
5711 return $prods;
5712 } else {
5713 dol_print_error($this->db);
5714 return -1;
5715 }
5716 }
5717
5718 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5725 public function get_sousproduits_arbo()
5726 {
5727 // phpcs:enable
5728 $parent = array();
5729
5730 foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) { // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
5731 $parent[$this->label][$keyChild] = $valueChild;
5732 }
5733 foreach ($parent as $key => $value) { // key=label, value is array of children
5734 $this->sousprods[$key] = $value; // @phan-suppress-current-line PhanTypeMismatchProperty
5735 }
5736 }
5737
5745 public function getTooltipContentArray($params)
5746 {
5747 global $conf, $langs, $user;
5748
5749 $langs->loadLangs(array('products', 'other'));
5750
5751 $datas = array();
5752 $nofetch = !empty($params['nofetch']);
5753
5754 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5755 return ['optimize' => $langs->trans("ShowProduct")];
5756 }
5757
5758 // Does user has permission to read product/service
5759 $permissiontoreadproduct = 0;
5760 if ($this->type == self::TYPE_PRODUCT && $user->hasRight('product', 'read')) {
5761 $permissiontoreadproduct = 1;
5762 }
5763 if ($this->type == self::TYPE_SERVICE && $user->hasRight('service', 'read')) {
5764 $permissiontoreadproduct = 1;
5765 }
5766
5767 if (!empty($this->entity) && $permissiontoreadproduct) {
5768 $tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80, 0, 0, 0, 0, '1');
5769 if ($this->nbphoto > 0) {
5770 $datas['photo'] = '<div class="photointooltip floatright">'."\n" . $tmpphoto . '</div>';
5771 }
5772 }
5773
5774 if ($this->isProduct()) {
5775 $datas['picto'] = img_picto('', 'product').' <u class="paddingrightonly">'.$langs->trans("Product").'</u>';
5776 } elseif ($this->isService()) {
5777 $datas['picto'] = img_picto('', 'service').' <u class="paddingrightonly">'.$langs->trans("Service").'</u>';
5778 }
5779 if (isset($this->status) && isset($this->status_buy)) {
5780 $datas['status'] = ' '.$this->getLibStatut(5, 0) . ' '.$this->getLibStatut(5, 1);
5781 }
5782
5783 if (!empty($this->ref)) {
5784 $datas['ref'] = '<br><b>'.$langs->trans('ProductRef').':</b> '.$this->ref;
5785 }
5786 if (!empty($this->label)) {
5787 $datas['label'] = '<br><b>'.$langs->trans('ProductLabel').':</b> '.$this->label;
5788 }
5789
5790 if ($permissiontoreadproduct) {
5791 if (!empty($this->description)) {
5792 $datas['description'] = '<br><b>'.$langs->trans('ProductDescription').':</b> '.dolGetFirstLineOfText($this->description, 5);
5793 }
5794 if ($this->isStockManaged()) {
5795 if (isModEnabled('productbatch')) {
5796 $langs->load("productbatch");
5797 $datas['batchstatus'] = "<br><b>".$langs->trans("ManageLotSerial").'</b>: '.$this->getLibStatut(0, 2);
5798 if ($this->status_batch) {
5799 $datas['batchdlc'] = "<br><b>".$langs->trans("BatchSellOrEatByMandatoryList", $langs->transnoentitiesnoconv("SellByDate"), $langs->transnoentitiesnoconv("EatByDate")).'</b>: '.$this->getSellOrEatByMandatoryLabel();
5800 }
5801 }
5802 }
5803 if (isModEnabled('barcode')) {
5804 $datas['barcode'] = '<br><b>'.$langs->trans('BarCode').':</b> '.$this->barcode;
5805 }
5806
5807 if ($this->isProduct()) {
5808 if ($this->weight) {
5809 $datas['weight'] = "<br><b>".$langs->trans("Weight").'</b>: '.$this->weight.' '.measuringUnitString(0, "weight", $this->weight_units);
5810 }
5811 $labelsize = "";
5812 if ($this->length) {
5813 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Length").'</b>: '.$this->length.' '.measuringUnitString(0, 'size', $this->length_units);
5814 }
5815 if ($this->width) {
5816 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Width").'</b>: '.$this->width.' '.measuringUnitString(0, 'size', $this->width_units);
5817 }
5818 if ($this->height) {
5819 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Height").'</b>: '.$this->height.' '.measuringUnitString(0, 'size', $this->height_units);
5820 }
5821 if ($labelsize) {
5822 $datas['size'] = "<br>".$labelsize;
5823 }
5824
5825 $labelsurfacevolume = "";
5826 if ($this->surface) {
5827 $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Surface").'</b>: '.$this->surface.' '.measuringUnitString(0, 'surface', $this->surface_units);
5828 }
5829 if ($this->volume) {
5830 $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Volume").'</b>: '.$this->volume.' '.measuringUnitString(0, 'volume', $this->volume_units);
5831 }
5832 if ($labelsurfacevolume) {
5833 $datas['surface'] = "<br>" . $labelsurfacevolume;
5834 }
5835 }
5836 if ($this->isService() && !empty($this->duration_value)) {
5837 // Duration
5838 $datas['duration'] = '<br><b>'.$langs->trans("Duration").':</b> '.$this->duration_value;
5839 if ($this->duration_value > 1) {
5840 $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"));
5841 } elseif ($this->duration_value > 0) {
5842 $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"));
5843 }
5844 $datas['duration'] .= (!empty($this->duration_unit) && isset($dur[$this->duration_unit]) ? "&nbsp;".$langs->trans($dur[$this->duration_unit]) : '');
5845 }
5846 if (empty($user->socid)) {
5847 if ($this->isStockManaged() && !empty($this->pmp) && $this->pmp) {
5848 $datas['pmp'] = "<br><b>".$langs->trans("PMPValue").'</b>: '.price($this->pmp, 0, '', 1, -1, -1, $conf->currency);
5849 }
5850
5851 if (isModEnabled('accounting')) {
5852 if ($this->status && isset($this->accountancy_code_sell)) {
5853 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5854 $selllabel = '<br>';
5855 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellCode').':</b> '.length_accountg($this->accountancy_code_sell);
5856 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellIntraCode').':</b> '.length_accountg($this->accountancy_code_sell_intra);
5857 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellExportCode').':</b> '.length_accountg($this->accountancy_code_sell_export);
5858 $datas['accountancysell'] = $selllabel;
5859 }
5860 if ($this->status_buy && isset($this->accountancy_code_buy)) {
5861 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5862 $buylabel = '';
5863 if (empty($this->status)) {
5864 $buylabel .= '<br>';
5865 }
5866 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyCode').':</b> '.length_accountg($this->accountancy_code_buy);
5867 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyIntraCode').':</b> '.length_accountg($this->accountancy_code_buy_intra);
5868 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyExportCode').':</b> '.length_accountg($this->accountancy_code_buy_export);
5869 $datas['accountancybuy'] = $buylabel;
5870 }
5871 }
5872 }
5873 // show categories for this record only in ajax to not overload lists
5874 if (isModEnabled('category') && !$nofetch) {
5875 require_once DOL_DOCUMENT_ROOT . '/categories/class/categorie.class.php';
5876 $form = new Form($this->db);
5877 $datas['categories'] = '<br>' . $form->showCategories($this->id, Categorie::TYPE_PRODUCT, 1);
5878 }
5879 }
5880
5881 return $datas;
5882 }
5883
5897 public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0, $morecss = '', $add_label = 0, $sep = ' - ')
5898 {
5899 global $langs, $hookmanager;
5900
5901 include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
5902
5903 $result = '';
5904
5905 $newref = $this->ref;
5906 if ($maxlength) {
5907 $newref = dol_trunc($newref, $maxlength, 'middle');
5908 }
5909 $params = [
5910 'id' => $this->id,
5911 'objecttype' => ($this->type == 1 ? 'service' : 'product'),
5912 'option' => $option,
5913 'nofetch' => 1,
5914 ];
5915 $classfortooltip = 'classfortooltip';
5916 $dataparams = '';
5917 if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
5918 $classfortooltip = 'classforajaxtooltip';
5919 $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
5920 $label = '';
5921 } else {
5922 $label = implode($this->getTooltipContentArray($params));
5923 }
5924
5925 $linkclose = '';
5926 if (empty($notooltip)) {
5927 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5928 $label = $langs->trans("ShowProduct");
5929 $linkclose .= ' alt="'.dol_escape_htmltag($label, 1, 1).'"';
5930 }
5931 $linkclose .= ($label ? ' title="'.dol_escape_htmltag($label, 1, 1).'"' : ' title="tocomplete"');
5932 $linkclose .= $dataparams.' class="nowraponall '.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
5933 } else {
5934 $linkclose = ' class="nowraponall'.($morecss ? ' '.$morecss : '').'"';
5935 }
5936
5937 if ($option == 'supplier' || $option == 'category') {
5938 $url = DOL_URL_ROOT.'/product/price_suppliers.php?id='.$this->id;
5939 } elseif ($option == 'stock') {
5940 $url = DOL_URL_ROOT.'/product/stock/product.php?id='.$this->id;
5941 } elseif ($option == 'composition') {
5942 $url = DOL_URL_ROOT.'/product/composition/card.php?id='.$this->id;
5943 } else {
5944 $url = DOL_URL_ROOT.'/product/card.php?id='.$this->id;
5945 }
5946
5947 if ($option !== 'nolink') {
5948 // Add param to save lastsearch_values or not
5949 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
5950 if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
5951 $add_save_lastsearch_values = 1;
5952 }
5953 if ($add_save_lastsearch_values) {
5954 $url .= '&save_lastsearch_values=1';
5955 }
5956 }
5957
5958 $linkstart = '<a href="'.$url.'"';
5959 $linkstart .= $linkclose.'>';
5960 $linkend = '</a>';
5961
5962 $result .= $linkstart;
5963 if ($withpicto) {
5964 if ($this->isProduct()) {
5965 $result .= (img_object(($notooltip ? '' : $label), 'product', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5966 }
5967 if ($this->isService()) {
5968 $result .= (img_object(($notooltip ? '' : $label), 'service', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5969 }
5970 }
5971 $result .= '<span class="aaa">'.dol_escape_htmltag($newref).'</span>';
5972 $result .= $linkend;
5973 if ($withpicto != 2) {
5974 $result .= (($add_label && $this->label) ? $sep.dol_trunc($this->label, ($add_label > 1 ? $add_label : 0)) : '');
5975 }
5976
5977 global $action;
5978 $hookmanager->initHooks(array('productdao'));
5979 $parameters = array('id' => $this->id, 'getnomurl' => &$result, 'label' => &$label);
5980 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
5981 if ($reshook > 0) {
5982 $result = $hookmanager->resPrint;
5983 } else {
5984 $result .= $hookmanager->resPrint;
5985 }
5986
5987 return $result;
5988 }
5989
5990
6001 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
6002 {
6003 global $langs;
6004
6005 $langs->load("products");
6006 $outputlangs->load("products");
6007
6008 // Positionne le modele sur le nom du modele a utiliser
6009 if (!dol_strlen($modele)) {
6010 $modele = getDolGlobalString('PRODUCT_ADDON_PDF', 'strato');
6011 }
6012
6013 $modelpath = "core/modules/product/doc/";
6014
6015 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
6016 }
6017
6025 public function getLibStatut($mode = 0, $type = 0)
6026 {
6027 switch ($type) {
6028 case 0:
6029 return $this->LibStatut($this->status, $mode, $type);
6030 case 1:
6031 return $this->LibStatut($this->status_buy, $mode, $type);
6032 case 2:
6033 return $this->LibStatut($this->status_batch, $mode, $type);
6034 default:
6035 //Simulate previous behavior but should return an error string
6036 return $this->LibStatut($this->status_buy, $mode, $type);
6037 }
6038 }
6039
6040 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6049 public function LibStatut($status, $mode = 0, $type = 0)
6050 {
6051 // phpcs:enable
6052 global $langs;
6053
6054 $labelStatus = $labelStatusShort = '';
6055
6056 $langs->load('products');
6057 if (isModEnabled('productbatch')) {
6058 $langs->load("productbatch");
6059 }
6060
6061 if ($type == 2) {
6062 switch ($mode) {
6063 case 0:
6064 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatch') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial')));
6065 return dolGetStatus($label);
6066 case 1:
6067 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort')));
6068 return dolGetStatus($label);
6069 case 2:
6070 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 1, 2);
6071 case 3:
6072 return dolGetStatus($langs->transnoentitiesnoconv('ProductStatusNotOnBatch'), '', '', empty($status) ? 'status5' : 'status4', 3, 'dot');
6073 case 4:
6074 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 0, 2);
6075 case 5:
6076 return $this->LibStatut($status, 1, 2).' '.$this->LibStatut($status, 3, 2);
6077 default:
6078 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'));
6079 }
6080 }
6081
6082 $statuttrans = empty($status) ? 'status5' : 'status4';
6083
6084 if ($status == 0) {
6085 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6086 if ($type == 0) {
6087 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnSellShort');
6088 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnSell');
6089 } elseif ($type == 1) {
6090 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBuyShort');
6091 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBuy');
6092 } elseif ($type == 2) {
6093 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBatch');
6094 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort');
6095 }
6096 } elseif ($status == 1) {
6097 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6098 if ($type == 0) {
6099 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSellShort');
6100 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSell');
6101 } elseif ($type == 1) {
6102 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBuyShort');
6103 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBuy');
6104 } elseif ($type == 2) {
6105 $labelStatus = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial'));
6106 $labelStatusShort = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort'));
6107 }
6108 } elseif ($type == 2 && $status == 2) {
6109 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSerial');
6110 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSerialShort');
6111 }
6112
6113 if ($mode > 6) {
6114 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'), '', '', 'status0', 0);
6115 } else {
6116 return dolGetStatus($labelStatus, $labelStatusShort, '', $statuttrans, $mode);
6117 }
6118 }
6119
6120
6126 public function getLibFinished()
6127 {
6128 global $langs;
6129
6130 $langs->load('products');
6131 $label = '';
6132
6133 if (isset($this->finished) && $this->finished >= 0) {
6134 $sql = "SELECT label, code FROM ".$this->db->prefix()."c_product_nature where code = ".((int) $this->finished)." AND active=1";
6135 $resql = $this->db->query($sql);
6136 if (!$resql) {
6137 $this->error = $this->db->error().' sql='.$sql;
6138 dol_syslog(__METHOD__.' Error '.$this->error, LOG_ERR);
6139 return -1;
6140 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
6141 $label = $langs->trans($res['label']);
6142 }
6143 $this->db->free($resql);
6144 }
6145
6146 return $label;
6147 }
6148
6149
6150 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6167 public function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null)
6168 {
6169 // phpcs:enable
6170 if ($id_entrepot) {
6171 $this->db->begin();
6172
6173 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
6174
6175 // Ensure $nbpiece is a number and positive
6176 $nbpiece = (float) $nbpiece;
6177 if ($nbpiece < 0) {
6178 if (!$movement) {
6179 $movement = 1;
6180 }
6181 $nbpiece = abs($nbpiece);
6182 }
6183 $op = array();
6184 $op[0] = $nbpiece;
6185 $op[1] = -$nbpiece;
6186
6187 $movementstock = new MouvementStock($this->db);
6188 $movementstock->setOrigin($origin_element, (int) $origin_id); // Set ->origin_type and ->origin_id
6189 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', '', '', '', false, 0, $disablestockchangeforsubproduct);
6190
6191 if ($result >= 0) {
6192 if ($extrafields) {
6193 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
6194 $movementstock->array_options = $array_options;
6195 $movementstock->insertExtraFields();
6196 }
6197 $this->db->commit();
6198 return 1;
6199 } else {
6200 $this->error = $movementstock->error;
6201 $this->errors = $movementstock->errors;
6202
6203 $this->db->rollback();
6204 return -1;
6205 }
6206 }
6207
6208 return -1;
6209 }
6210
6211 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6232 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)
6233 {
6234 // phpcs:enable
6235 if ($id_entrepot) {
6236 $this->db->begin();
6237
6238 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
6239
6240 // Ensure $nbpiece is a number and positive
6241 $nbpiece = (float) $nbpiece;
6242 if ($nbpiece < 0) {
6243 if (!$movement) {
6244 $movement = 1;
6245 }
6246 $nbpiece = abs($nbpiece);
6247 }
6248
6249 $op = array();
6250 $op[0] = $nbpiece;
6251 $op[1] = -$nbpiece;
6252
6253 $movementstock = new MouvementStock($this->db);
6254 $movementstock->setOrigin($origin_element, (int) $origin_id); // Set ->origin_type and ->fk_origin
6255 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', $dlc, $dluo, $lot, false, 0, $disablestockchangeforsubproduct, 0, $force_update_batch);
6256
6257 if ($result >= 0) {
6258 if ($extrafields) {
6259 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
6260 $movementstock->array_options = $array_options;
6261 $movementstock->insertExtraFields();
6262 }
6263 $this->db->commit();
6264 return 1;
6265 } else {
6266 $this->error = $movementstock->error;
6267 $this->errors = $movementstock->errors;
6268
6269 $this->db->rollback();
6270 return -1;
6271 }
6272 }
6273 return -1;
6274 }
6275
6276 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6289 public function load_stock($option = '', $includedraftpoforvirtual = null, $dateofvirtualstock = null)
6290 {
6291 // phpcs:enable
6292 $this->stock_reel = 0;
6293 $this->stock_warehouse = array();
6294 $this->stock_theorique = 0;
6295
6296 // Set filter on warehouse status
6297 $warehouseStatus = array();
6298 if (preg_match('/warehouseclosed/', $option)) {
6300 }
6301 if (preg_match('/warehouseopen/', $option)) {
6303 }
6304 if (preg_match('/warehouseinternal/', $option)) {
6305 if (getDolGlobalString('ENTREPOT_EXTRA_STATUS')) {
6307 } else {
6309 }
6310 }
6311
6312 $sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot";
6313 $sql .= " FROM ".$this->db->prefix()."product_stock as ps";
6314 $sql .= ", ".$this->db->prefix()."entrepot as w";
6315 $sql .= " WHERE w.entity IN (".getEntity('stock').")";
6316 $sql .= " AND w.rowid = ps.fk_entrepot";
6317 $sql .= " AND ps.fk_product = ".((int) $this->id);
6318 if (count($warehouseStatus)) {
6319 $sql .= " AND w.statut IN (".$this->db->sanitize(implode(',', $warehouseStatus)).")";
6320 }
6321
6322 $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;
6323
6324 dol_syslog(get_class($this)."::load_stock", LOG_DEBUG);
6325 $result = $this->db->query($sql);
6326 if ($result) {
6327 $num = $this->db->num_rows($result);
6328 $i = 0;
6329 if ($num > 0) {
6330 while ($i < $num) {
6331 $row = $this->db->fetch_object($result);
6332 $this->stock_warehouse[$row->fk_entrepot] = new stdClass();
6333 $this->stock_warehouse[$row->fk_entrepot]->real = $row->reel;
6334 $this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid;
6335 if ((!preg_match('/nobatch/', $option)) && $this->hasbatch()) {
6336 $this->stock_warehouse[$row->fk_entrepot]->detail_batch = Productbatch::findAll($this->db, $row->rowid, 1, $this->id);
6337 }
6338 $this->stock_reel += $row->reel;
6339 $i++;
6340 }
6341 $this->stock_reel = (float) price2num($this->stock_reel, 'MS');
6342 }
6343 $this->db->free($result);
6344
6345 if (!preg_match('/novirtual/', $option)) {
6346 $this->load_virtual_stock($includedraftpoforvirtual, $dateofvirtualstock); // This load stock_theorique and also load all arrays stats_xxx...
6347 }
6348
6349 return 1;
6350 } else {
6351 $this->error = $this->db->lasterror();
6352 return -1;
6353 }
6354 }
6355
6356
6357 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6367 public function load_virtual_stock($includedraftpoforvirtual = null, $dateofvirtualstock = null)
6368 {
6369 // phpcs:enable
6370 global $hookmanager, $action;
6371
6372 $stock_commande_client = 0;
6373 $stock_commande_fournisseur = 0;
6374 $stock_sending_client = 0;
6375 $stock_reception_fournisseur = 0;
6376 $stock_inproduction = 0;
6377
6378 //dol_syslog("load_virtual_stock");
6379
6380 if (isModEnabled('order')) {
6381 $result = $this->load_stats_commande(0, '1,2', 1);
6382 if ($result < 0) {
6383 dol_print_error($this->db, $this->error);
6384 }
6385 $stock_commande_client = $this->stats_commande['qty'];
6386 }
6387 if (isModEnabled("shipping")) {
6388 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
6389 $filterShipmentStatus = '';
6390 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
6391 $filterShipmentStatus = Expedition::STATUS_VALIDATED.','.Expedition::STATUS_CLOSED;
6392 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6393 $filterShipmentStatus = Expedition::STATUS_CLOSED;
6394 }
6395 $result = $this->load_stats_sending(0, '1,2', 1, $filterShipmentStatus);
6396 if ($result < 0) {
6397 dol_print_error($this->db, $this->error);
6398 }
6399 $stock_sending_client = $this->stats_expedition['qty'];
6400 }
6401 // Include supplier order lines
6402 if (isModEnabled("supplier_order")) {
6403 $filterStatus = getDolGlobalString('SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK', '3,4');
6404 if (isset($includedraftpoforvirtual)) {
6405 $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
6406 }
6407 $result = $this->load_stats_commande_fournisseur(0, $filterStatus, 1, $dateofvirtualstock);
6408 if ($result < 0) {
6409 dol_print_error($this->db, $this->error);
6410 }
6411 $stock_commande_fournisseur = $this->stats_commande_fournisseur['qty'];
6412 }
6413 // Include reception lines
6414 if (isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) {
6415 $filterStatus = '4';
6416 if (isset($includedraftpoforvirtual)) {
6417 $filterStatus = '0,'.$filterStatus;
6418 }
6419 $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock);
6420 if ($result < 0) {
6421 dol_print_error($this->db, $this->error);
6422 }
6423 $stock_reception_fournisseur = $this->stats_reception['qty'];
6424 }
6425 // Include manufacturing
6426 if (isModEnabled('mrp')) {
6427 $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock);
6428 if ($result < 0) {
6429 dol_print_error($this->db, $this->error);
6430 }
6431 $stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty'];
6432 }
6433
6434 $this->stock_theorique = $this->stock_reel + $stock_inproduction;
6435
6436 // $weBillOrderOrShipmentReception is set to 'order' or 'shipmentreception'. it will be used to know how to make virtual stock
6437 // calculation when we have a stock increase or decrease on billing. Do we have to count orders to bill or shipment/reception to bill ?
6438 $weBillOrderOrShipmentReception = getDolGlobalString('STOCK_DO_WE_BILL_ORDER_OR_SHIPMENTECEPTION_FOR_VIRTUALSTOCK', 'order');
6439
6440 // Stock decrease mode
6441 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') || getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6442 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6443 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_VALIDATE_ORDER')) {
6444 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
6445 $tmpnewprod = dol_clone($this, 1);
6446 $result = $tmpnewprod->load_stats_commande(0, '0', 1); // Get qty in draft orders
6447 $this->stock_theorique += $tmpnewprod->stats_commande['qty'];
6448 }
6449 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'order') {
6450 $this->stock_theorique -= $stock_commande_client;
6451 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6452 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6453 }
6454
6455 // Stock Increase mode
6456 if (getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION') || getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION_CLOSE')) {
6457 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6458 } 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
6459 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6460 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER')) { // Warning: stock change "on approval", not on validation !
6461 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
6462 $tmpnewprod = dol_clone($this, 1);
6463 $result = $tmpnewprod->load_stats_commande_fournisseur(0, '0', 1); // Get qty in draft orders
6464 $this->stock_theorique += $this->stats_commande_fournisseur['qty'];
6465 }
6466 $this->stock_theorique -= $stock_reception_fournisseur;
6467 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'order') {
6468 $this->stock_theorique += $stock_commande_fournisseur;
6469 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6470 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6471 }
6472
6473 $parameters = array('id' => $this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual);
6474 // Note that $action and $object may have been modified by some hooks
6475 $reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action);
6476 if ($reshook > 0) {
6477 $this->stock_theorique = $hookmanager->resArray['stock_theorique'];
6478 } elseif ($reshook == 0 && isset($hookmanager->resArray['stock_stats_hook'])) {
6479 $this->stock_theorique += $hookmanager->resArray['stock_stats_hook'];
6480 }
6481
6482 //Virtual Stock by Warehouse
6483 if (!empty($this->stock_warehouse) && getDolGlobalString('STOCK_ALLOW_VIRTUAL_STOCK_PER_WAREHOUSE')) {
6484 foreach ($this->stock_warehouse as $warehouseid => $stockwarehouse) {
6485 if (isModEnabled('mrp')) {
6486 $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock, $warehouseid);
6487 if ($result < 0) {
6488 dol_print_error($this->db, $this->error);
6489 }
6490 }
6491
6492 if ($this->fk_default_warehouse == $warehouseid) {
6493 $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']);
6494 } else {
6495 $this->stock_warehouse[$warehouseid]->virtual = $this->stock_warehouse[$warehouseid]->real + $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'];
6496 }
6497 }
6498 }
6499
6500 return 1;
6501 }
6502
6510 public function loadStockForVirtualProduct($option = '', $qtyWish = 1)
6511 {
6512 $this->stock_warehouse = array();
6513 $error = 0;
6514
6515 $this->get_sousproduits_arbo();
6516 $prods_arbo = $this->get_arbo_each_prod($qtyWish, 1);
6517 if (count($prods_arbo) > 0) {
6518 $productCachedList = array();
6519 $stockByComponentList = array();
6520
6521 foreach ($prods_arbo as $componentArr) {
6522 $componentId = $componentArr['id'];
6523 // only component whose manage stock
6524 if ($componentArr['incdec'] == 1) {
6525 if (!isset($productCachedList[$componentId])) {
6526 $componentStatic = new self($this->db);
6527 $componentStatic->fetch($componentId);
6528 // check if it's a sub-kit
6529 $childrenNb = $componentStatic->hasFatherOrChild(1);
6530 if ($childrenNb == 0) {
6531 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6532 if (!isset($stockByComponentList[$componentId])) {
6533 $stockByComponentList[$componentId] = array(
6534 'qty_need' => 0
6535 );
6536 }
6537 $stockByComponentList[$componentId]['qty_need'] += $componentArr['nb_total'];
6538 }
6539 $productCachedList[$componentId] = $componentStatic;
6540 }
6541 }
6542 }
6543
6544 if (!empty($stockByComponentList)) {
6545 foreach ($stockByComponentList as $componentId => $stockByComponentArr) {
6546 if (!isset($productCachedList[$componentId])) {
6547 $componentStatic = new self($this->db);
6548 $componentStatic->fetch($componentId);
6549 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6550 $productCachedList[$componentId] = $componentStatic;
6551 }
6552 $component = $productCachedList[$componentId];
6553
6554
6555 if ($component->stock_reel < $stockByComponentArr['qty_need']) {
6556 // not enough stock for this component to assemble this virtual product
6557 $error++;
6558 $this->error = 'Not enough component [id='.$componentId.'] in stock, real='.$component->stock_reel.' and need='.$stockByComponentArr['qty_need'];
6559 $this->errors[] = $this->error;
6560 dol_syslog(__METHOD__.' : '.$this->error, LOG_ERR);
6561 } else {
6562 if (!empty($component->stock_warehouse)) {
6563 foreach ($component->stock_warehouse as $warehouseId => $warehouseObj) {
6564 $kitWarehouseAvailable = new stdClass();
6565 $kitWarehouseAvailable->id = $warehouseObj->id;
6566 $kitWarehouseAvailable->real = $qtyWish;
6567 $this->stock_warehouse[$warehouseId] = $kitWarehouseAvailable;
6568 }
6569 }
6570 }
6571
6572 if ($error) {
6573 break;
6574 }
6575 }
6576 } elseif (getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE') && empty($productCachedList)) {
6577 // if all sub product are not stock managed when use parent stock
6578 $this->load_stock('warehouseopen');
6579 }
6580 }
6581
6582 if ($error) {
6583 return -1;
6584 } else {
6585 return 1;
6586 }
6587 }
6588
6596 public function loadBatchInfo($batch)
6597 {
6598 $result = array();
6599
6600 $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";
6601 $sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = ".((int) $this->id)." AND pb.batch = '".$this->db->escape($batch)."'";
6602 $sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby";
6603 dol_syslog(get_class($this)."::loadBatchInfo load first entry found for lot/serial = ".$batch, LOG_DEBUG);
6604 $resql = $this->db->query($sql);
6605 if ($resql) {
6606 $num = $this->db->num_rows($resql);
6607 $i = 0;
6608 while ($i < $num) {
6609 $obj = $this->db->fetch_object($resql);
6610 $result[] = array('batch' => $batch, 'eatby' => $this->db->jdate($obj->eatby), 'sellby' => $this->db->jdate($obj->sellby), 'qty' => $obj->qty);
6611 $i++;
6612 }
6613 return $result;
6614 } else {
6615 dol_print_error($this->db);
6616 $this->db->rollback();
6617 return array();
6618 }
6619 }
6620
6621 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6630 public function add_photo($sdir, $file)
6631 {
6632 // phpcs:enable
6633 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6634
6635 $result = 0;
6636
6637 $dir = $sdir;
6638 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6639 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos";
6640 } else {
6641 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product').dol_sanitizeFileName($this->ref);
6642 }
6643
6644 dol_mkdir($dir);
6645
6646 $dir_osencoded = $dir;
6647
6648 if (is_dir($dir_osencoded)) {
6649 $originImage = $dir.'/'.$file['name'];
6650
6651 // Cree fichier en taille origine
6652 $result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
6653
6654 if (file_exists(dol_osencode($originImage))) {
6655 // Create thumbs
6656 $this->addThumbs($originImage);
6657 }
6658 }
6659
6660 if (is_numeric($result) && $result > 0) {
6661 return 1;
6662 } else {
6663 return -1;
6664 }
6665 }
6666
6667 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6674 public function is_photo_available($sdir)
6675 {
6676 // phpcs:enable
6677 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6678 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6679
6680 $dir = $sdir;
6681 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6682 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos/";
6683 } else {
6684 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product');
6685 }
6686
6687 $dir_osencoded = dol_osencode($dir);
6688 if (file_exists($dir_osencoded)) {
6689 $handle = opendir($dir_osencoded);
6690 if (is_resource($handle)) {
6691 while (($file = readdir($handle)) !== false) {
6692 if (!utf8_check($file)) {
6693 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // To be sure data is stored in UTF8 in memory
6694 }
6695 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6696 return true;
6697 }
6698 }
6699 }
6700 }
6701
6702 return false;
6703 }
6704
6705 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6713 public function liste_photos($dir, $nbmax = 0)
6714 {
6715 // phpcs:enable
6716 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6717 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6718
6719 $nbphoto = 0;
6720 $tabobj = array();
6721
6722 $dir_osencoded = dol_osencode($dir);
6723 $handle = @opendir($dir_osencoded);
6724 if (is_resource($handle)) {
6725 while (($file = readdir($handle)) !== false) {
6726 if (!utf8_check($file)) {
6727 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // readdir returns ISO
6728 }
6729 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6730 $nbphoto++;
6731
6732 // We forge name of thumb.
6733 $photo = $file;
6734 $photo_vignette = '';
6735 $regs = array();
6736 if (preg_match('/('.$this->regeximgext.')$/i', $photo, $regs)) {
6737 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $photo).'_small'.$regs[0];
6738 }
6739
6740 $dirthumb = $dir.'thumbs/';
6741
6742 // Object
6743 $obj = array();
6744 $obj['photo'] = $photo;
6745 if ($photo_vignette && dol_is_file($dirthumb.$photo_vignette)) {
6746 $obj['photo_vignette'] = 'thumbs/'.$photo_vignette;
6747 } else {
6748 $obj['photo_vignette'] = "";
6749 }
6750
6751 $tabobj[$nbphoto - 1] = $obj;
6752
6753 // Do we have to continue with next photo ?
6754 if ($nbmax && $nbphoto >= $nbmax) {
6755 break;
6756 }
6757 }
6758 }
6759
6760 closedir($handle);
6761 }
6762
6763 return $tabobj;
6764 }
6765
6766 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6773 public function delete_photo($file)
6774 {
6775 // phpcs:enable
6776 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6777 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6778
6779 $dir = dirname($file).'/'; // Chemin du dossier contenant l'image d'origine
6780 $dirthumb = $dir.'/thumbs/'; // Chemin du dossier contenant la vignette
6781 $filename = preg_replace('/'.preg_quote($dir, '/').'/i', '', $file); // Nom du fichier
6782
6783 // On efface l'image d'origine
6784 dol_delete_file($file, 0, 0, 0, $this); // For triggers
6785
6786 // Si elle existe, on efface la vignette
6787 if (preg_match('/('.$this->regeximgext.')$/i', $filename, $regs)) {
6788 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_small'.$regs[0];
6789 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6790 dol_delete_file($dirthumb.$photo_vignette);
6791 }
6792
6793 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_mini'.$regs[0];
6794 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6795 dol_delete_file($dirthumb.$photo_vignette);
6796 }
6797 }
6798 }
6799
6800 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6807 public function get_image_size($file)
6808 {
6809 // phpcs:enable
6810 $file_osencoded = dol_osencode($file);
6811 $infoImg = getimagesize($file_osencoded); // Get information on image
6812 $this->imgWidth = $infoImg[0]; // Largeur de l'image
6813 $this->imgHeight = $infoImg[1]; // Hauteur de l'image
6814 }
6815
6821 public function loadStateBoard()
6822 {
6823 global $hookmanager;
6824
6825 $this->nb = array();
6826
6827 $sql = "SELECT count(p.rowid) as nb, fk_product_type";
6828 $sql .= " FROM ".$this->db->prefix()."product as p";
6829 $sql .= ' WHERE p.entity IN ('.getEntity($this->element, 1).')';
6830 // Add where from hooks
6831 if (is_object($hookmanager)) {
6832 $parameters = array();
6833 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
6834 $sql .= $hookmanager->resPrint;
6835 }
6836 $sql .= ' GROUP BY fk_product_type';
6837
6838 $resql = $this->db->query($sql);
6839 if ($resql) {
6840 while ($obj = $this->db->fetch_object($resql)) {
6841 if ($obj->fk_product_type == 1) {
6842 $this->nb["services"] = $obj->nb;
6843 } else {
6844 $this->nb["products"] = $obj->nb;
6845 }
6846 }
6847 $this->db->free($resql);
6848 return 1;
6849 } else {
6850 dol_print_error($this->db);
6851 $this->error = $this->db->error();
6852 return -1;
6853 }
6854 }
6855
6861 public function isProduct()
6862 {
6863 return $this->type == Product::TYPE_PRODUCT;
6864 }
6865
6871 public function isService()
6872 {
6873 return $this->type == Product::TYPE_SERVICE;
6874 }
6875
6881 public function isStockManaged()
6882 {
6883 return (($this->isProduct() || ($this->isService() && getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) && ($this->stockable_product > 0));
6884 }
6885
6891 public function isMandatoryPeriod()
6892 {
6893 return $this->mandatory_period == 1;
6894 }
6895
6901 public function hasbatch()
6902 {
6903 return $this->status_batch > 0;
6904 }
6905
6906
6907 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6916 public function get_barcode($object, $type = '')
6917 {
6918 // phpcs:enable
6919 global $conf;
6920
6921 $result = '';
6922 if (getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
6923 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
6924 foreach ($dirsociete as $dirroot) {
6925 $res = dol_include_once($dirroot . getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM').'.php');
6926 if ($res) {
6927 break;
6928 }
6929 }
6930 $var = getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM');
6931 $mod = new $var();
6932 '@phan-var-force ModeleNumRefBarCode $mod';
6933
6934 $result = $mod->getNextValue($object, $type);
6935
6936 dol_syslog(get_class($this)."::get_barcode barcode=".$result." module=".$var);
6937 }
6938 return $result;
6939 }
6940
6948 public function initAsSpecimen()
6949 {
6950 $now = dol_now();
6951
6952 // Initialize parameters
6953 $this->specimen = 1;
6954 $this->id = 0;
6955 $this->ref = 'PRODUCT_SPEC';
6956 $this->label = 'PRODUCT SPECIMEN';
6957 $this->description = 'This is description of this product specimen that was created the '.dol_print_date($now, 'dayhourlog').'.';
6958 $this->specimen = 1;
6959 $this->country_id = 1;
6960 $this->status = 1;
6961 $this->status_buy = 1;
6962 $this->tobatch = 0;
6963 $this->sell_or_eat_by_mandatory = 0;
6964 $this->note_private = 'This is a comment (private)';
6965 $this->note_public = 'This is a comment (public)';
6966 $this->date_creation = $now;
6967 $this->date_modification = $now;
6968
6969 $this->weight = 4;
6970 $this->weight_units = 3;
6971
6972 $this->length = 5;
6973 $this->length_units = 1;
6974 $this->width = 6;
6975 $this->width_units = 0;
6976 $this->height = null;
6977 $this->height_units = null;
6978
6979 $this->surface = 30;
6980 $this->surface_units = 0;
6981 $this->volume = 300;
6982 $this->volume_units = 0;
6983
6984 $this->barcode = -1; // Create barcode automatically
6985
6986 return 1;
6987 }
6988
6998 public function getLabelOfUnit($type = 'long', $outputlangs = null, $noentities = 0)
6999 {
7000 global $langs;
7001
7002 if (empty($this->fk_unit)) {
7003 return '';
7004 }
7005 if (empty($outputlangs)) {
7006 $outputlangs = $langs;
7007 }
7008
7009 $outputlangs->load('products');
7010 $label = '';
7011
7012 $sql = "SELECT code, label, short_label FROM ".$this->db->prefix()."c_units where rowid = ".((int) $this->fk_unit);
7013
7014 $resql = $this->db->query($sql);
7015 if (!$resql) {
7016 $this->error = $this->db->error();
7017 dol_syslog(get_class($this)."::getLabelOfUnit Error ".$this->error, LOG_ERR);
7018 return -1;
7019 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
7020 if ($type == 'short') {
7021 if ($noentities) {
7022 $label = $outputlangs->transnoentitiesnoconv($res['short_label']);
7023 } else {
7024 $label = $outputlangs->trans($res['short_label']);
7025 }
7026 } elseif ($type == 'code') {
7027 $label = $res['code'];
7028 } else {
7029 if ($outputlangs->trans('unit'.$res['code']) == 'unit'.$res['code']) {
7030 // No translation available
7031 $label = $res['label'];
7032 } else {
7033 // Return the translated value
7034 if ($noentities) {
7035 $label = $outputlangs->transnoentitiesnoconv('unit'.$res['code']);
7036 } else {
7037 $label = $outputlangs->trans('unit'.$res['code']);
7038 }
7039 }
7040 }
7041 }
7042 $this->db->free($resql);
7043
7044 return $label;
7045 }
7046
7047 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7053 public function min_recommended_price()
7054 {
7055 // phpcs:enable
7056 $maxpricesupplier = 0;
7057
7058 if (getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE')) {
7059 include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
7060 $product_fourn = new ProductFournisseur($this->db);
7061 $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', '');
7062
7063 if (is_array($product_fourn_list) && count($product_fourn_list) > 0) {
7064 foreach ($product_fourn_list as $productfourn) {
7065 if ($productfourn->fourn_unitprice > $maxpricesupplier) {
7066 $maxpricesupplier = $productfourn->fourn_unitprice;
7067 }
7068 }
7069
7070 $maxpricesupplier *= getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE');
7071 }
7072 }
7073
7074 return $maxpricesupplier;
7075 }
7076
7077
7088 public function setCategories($categories)
7089 {
7090 require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
7091 return parent::setCategoriesCommon($categories, Categorie::TYPE_PRODUCT);
7092 }
7093
7102 public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
7103 {
7104 $tables = array(
7105 'product_customer_price',
7106 'product_customer_price_log'
7107 );
7108
7109 return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
7110 }
7111
7123 public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
7124 {
7125 $sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM ".$this->db->prefix()."product_pricerules";
7126 $query = $this->db->query($sql);
7127
7128 $rules = array();
7129
7130 while ($result = $this->db->fetch_object($query)) {
7131 $rules[$result->level] = $result;
7132 }
7133
7134 //Because prices can be based on other level's prices, we temporarily store them
7135 $prices = array(
7136 1 => $baseprice
7137 );
7138
7139 $nbofproducts = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
7140 for ($i = 1; $i <= $nbofproducts; $i++) {
7141 $price = $baseprice;
7142 $price_min = $baseprice;
7143
7144 //We have to make sure it does exist and it is > 0
7145 //First price level only allows changing min_price
7146 if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) {
7147 $price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent / 100));
7148 }
7149
7150 $prices[$i] = $price;
7151
7152 //We have to make sure it does exist and it is > 0
7153 if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) {
7154 $price_min = $price * (1 - ($rules[$i]->var_min_percent / 100));
7155 }
7156
7157 //Little check to make sure the price is modified before triggering generation
7158 $check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i]));
7159 $check_type = ($baseprice == $this->multiprices_base_type[$i]);
7160
7161 if ($check_amount && $check_type) {
7162 continue;
7163 }
7164
7165 if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, 1) < 0) {
7166 return -1;
7167 }
7168 }
7169
7170 return 1;
7171 }
7172
7178 public function getRights()
7179 {
7180 global $user;
7181
7182 if ($this->isProduct()) {
7183 return $user->rights->produit;
7184 } else {
7185 return $user->rights->service;
7186 }
7187 }
7188
7195 public function info($id)
7196 {
7197 $sql = "SELECT p.rowid, p.ref, p.datec as date_creation, p.tms as date_modification,";
7198 $sql .= " p.fk_user_author, p.fk_user_modif";
7199 $sql .= " FROM ".$this->db->prefix().$this->table_element." as p";
7200 $sql .= " WHERE p.rowid = ".((int) $id);
7201
7202 $result = $this->db->query($sql);
7203 if ($result) {
7204 if ($this->db->num_rows($result)) {
7205 $obj = $this->db->fetch_object($result);
7206
7207 $this->id = $obj->rowid;
7208 $this->ref = $obj->ref;
7209
7210 $this->user_creation_id = $obj->fk_user_author;
7211 $this->user_modification_id = $obj->fk_user_modif;
7212
7213 $this->date_creation = $this->db->jdate($obj->date_creation);
7214 $this->date_modification = $this->db->jdate($obj->date_modification);
7215 }
7216
7217 $this->db->free($result);
7218 } else {
7219 dol_print_error($this->db);
7220 }
7221 }
7222
7223
7229 public function getProductDurationHours()
7230 {
7231 if (empty($this->duration_value)) {
7232 $this->errors[] = 'ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
7233 return -1;
7234 }
7235 if ($this->duration_unit == 's') {
7236 $prodDurationHours = 1. / 3600;
7237 } elseif ($this->duration_unit == 'i' || $this->duration_unit == 'mn' || $this->duration_unit == 'min') {
7238 $prodDurationHours = 1. / 60;
7239 } elseif ($this->duration_unit == 'h') {
7240 $prodDurationHours = 1.;
7241 } elseif ($this->duration_unit == 'd') {
7242 $prodDurationHours = 24.;
7243 } elseif ($this->duration_unit == 'w') {
7244 $prodDurationHours = 24. * 7;
7245 } elseif ($this->duration_unit == 'm') {
7246 $prodDurationHours = 24. * 30;
7247 } elseif ($this->duration_unit == 'y') {
7248 $prodDurationHours = 24. * 365;
7249 } else {
7250 // Unknown unit: the duration cannot be converted. Returning 0 silently made callers
7251 // compute a zero duration, and time.php divides by this value (#40805).
7252 $this->errors[] = 'ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
7253 return -1;
7254 }
7255 $prodDurationHours *= $this->duration_value;
7256
7257 return $prodDurationHours;
7258 }
7259
7260
7268 public function getKanbanView($option = '', $arraydata = null)
7269 {
7270 global $langs, $conf;
7271
7272 $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
7273
7274 $return = '<div class="box-flex-item box-flex-grow-zero">';
7275 $return .= '<div class="info-box info-box-sm">';
7276 $return .= '<div class="info-box-img">';
7277 $label = '';
7278 if ($this->is_photo_available($conf->product->multidir_output[$this->entity])) {
7279 $label .= $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 120, 160, 0, 0, 0, '', 'photoref photokanban');
7280 $return .= $label;
7281 } else {
7282 if ($this->isProduct()) {
7283 $label .= img_picto('', 'product');
7284 } elseif ($this->isService()) {
7285 $label .= img_picto('', 'service');
7286 }
7287 $return .= $label;
7288 }
7289 $return .= '</div>';
7290 $return .= '<div class="info-box-content">';
7291 $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">'.(method_exists($this, 'getNomUrl') ? $this->getNomUrl() : $this->ref).'</span>';
7292 if ($selected >= 0) {
7293 $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
7294 }
7295 if (property_exists($this, 'label')) {
7296 $return .= '<br><span class="info-box-label opacitymedium inline-block tdoverflowmax150 valignmiddle" title="'.dol_escape_htmltag($this->label).'">'.dol_escape_htmltag($this->label).'</span>';
7297 }
7298 if (property_exists($this, 'price') && property_exists($this, 'price_ttc')) {
7299 if ($this->price_base_type == 'TTC') {
7300 $return .= '<br><span class="info-box-status amount">'.price($this->price_ttc).' '.$langs->trans("TTC").'</span>';
7301 } else {
7302 if ($this->status) {
7303 $return .= '<br><span class="info-box-status amount">'.price($this->price).' '.$langs->trans("HT").'</span>';
7304 }
7305 }
7306 }
7307 $br = 1;
7308 if (property_exists($this, 'stock_reel') && $this->isProduct()) {
7309 $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>';
7310 $br = 0;
7311 }
7312 if (method_exists($this, 'getLibStatut')) {
7313 if ($br) {
7314 $return .= '<br><div class="info-box-status inline-block valignmiddle">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7315 } else {
7316 $return .= '<div class="info-box-status inline-block valignmiddle marginleftonly paddingleft">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7317 }
7318 }
7319 $return .= '</div>';
7320 $return .= '</div>';
7321 $return .= '</div>';
7322 return $return;
7323 }
7324
7331 public function getProductsToPreviewInEmail($limit)
7332 {
7333
7334 if (!is_numeric($limit)) {
7335 return -1;
7336 }
7337
7338 $sql = "SELECT p.rowid, p.ref, p.label, p.description, p.entity, ef.filename
7339 FROM ".MAIN_DB_PREFIX."product AS p
7340 JOIN ".MAIN_DB_PREFIX."ecm_files AS ef ON p.rowid = ef.src_object_id
7341 WHERE ef.entity IN (".getEntity('product').")
7342 AND (ef.filename LIKE '%.png' OR ef.filename LIKE '%.jpeg' OR ef.filename LIKE '%.svg')
7343 GROUP BY p.rowid, p.ref, p.label, p.description, p.entity, ef.filename
7344 ORDER BY p.datec ASC
7345 LIMIT " . ((int) $limit);
7346
7347 $resql = $this->db->query($sql);
7348 $products = array();
7349
7350 if ($resql) {
7351 while ($obj = $this->db->fetch_object($resql)) {
7352 $products[] = array(
7353 'rowid' => $obj->rowid,
7354 'ref' => $obj->ref,
7355 'label' => $obj->label,
7356 'description' => $obj->description,
7357 'entity' => $obj->entity,
7358 'filename' => $obj->filename
7359 );
7360 }
7361 } else {
7362 dol_print_error($this->db);
7363 }
7364 if (empty($products)) {
7365 return -1;
7366 }
7367 return $products;
7368 }
7369}
7370
7376{
7377 public $picto = 'service';
7378}
if( $user->socid > 0) if(! $user->hasRight('accounting', 'chartofaccount')) $object
Definition card.php:67
length_accountg($account)
Return General accounting account with defined length (used for product and miscellaneous)
clean_account($account)
Return accounting account without zero on the right.
$object ref
Definition info.php:90
Parent class of all other business classes (invoices, contracts, proposals, orders,...
fetch_optionals($rowid=null, $optionsArray=null)
Function to get extra fields of an object into $this->array_options This method is in most cases call...
deleteEcmFiles($mode=0)
Delete related files of object in database.
commonGenerateDocument($modelspath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams=null)
Common function for all objects extending CommonObject for generating documents.
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.
call_trigger($triggerName, $user)
Call trigger based on this instance.
addThumbs($file, $quality=50)
Build thumb.
Class to manage Dolibarr database access.
Class to manage ECM files.
const STATUS_OPEN_INTERNAL
Warehouse open and only operations for stock transfers/corrections allowed (not for customer shipping...
const STATUS_OPEN_ALL
Warehouse open and any operations are allowed (customer shipping, supplier dispatch,...
const STATUS_CLOSED
Warehouse closed, inactive.
const STATUS_CLOSED
Closed status -> parcel was received by customer / end of process prev status : validated or shipment...
const STATUS_VALIDATED
Validated status -> parcel is ready to be sent prev status : draft next status : closed or shipment_i...
const STATUS_DRAFT
Draft status.
Class to manage generation of HTML components Only common components must be here.
Class to manage stock movements.
Class to parse product price expressions.
Class ProductCombination Used to represent the relation between a product and one of its variants.
File of class to manage predefined price products or services by customer.
Class to manage predefined suppliers products.
Class to manage products or services.
static replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
Function used to replace a thirdparty id with another one.
get_nb_achat($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units or supplier invoices in which product is included.
getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp=0)
Return price of sell of a product for a seller/buyer/product.
__construct($db)
Constructor.
is_sousproduit($fk_parent, $fk_child)
Check if it is a sub-product into a kit.
const DISABLED_STOCK
Stockable product.
const SELL_OR_EAT_BY_MANDATORY_ID_NONE
Const sell or eat by mandatory id.
isStockManaged()
Return if the object is managed in stock.
setPriceExpression($expression_id)
Sets the supplier price expression.
getArrayForPriceCompare($level=0)
used to check if price have really change to avoid log pollution
get_arbo_each_prod($multiply=1, $ignore_stock_load=0)
Build the tree of subproducts and return it.
check_barcode($valuetotest, $typefortest)
Check barcode.
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.
delMultiLangs($langtodelete, $user)
Delete a language for this product.
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.
hasFatherOrChild($mode=0)
Count all parent and children products for current product (first level only)
load_stats_facturerec($socid=0)
Load array of statistics for recurring invoice for product/service.
get_nb_propalsupplier($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units in proposals in which product is included.
load_stats_facturefournrec($socid=0)
Load array of statistics for recurring supplier invoice for product/service.
get_nb_contract($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units in orders in which product is included.
load_stats_facture_fournisseur($socid=0)
Load array of statistics for vendor invoice for product/service.
get_nb_ordersupplier($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units in orders in which product is included.
getMultiLangs()
Load array this->multilangs.
get_nb_mos($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units in orders in which product is included.
clone_associations($fromId, $toId)
Clone links between products.
create($user, $notrigger=0)
Insert product into database.
load_stats_contrat($socid=0)
Charge tableau des stats contrat pour le produit/service.
isService()
Return if the object is a service.
getRights()
Returns the rights used for this class.
loadBatchInfo($batch)
Load existing information about a serial.
load_stock($option='', $includedraftpoforvirtual=null, $dateofvirtualstock=null)
Load information about stock of a product into ->stock_reel, ->stock_warehouse[] (including stock_war...
getProductDurationHours()
Return the duration of a service in hours (for a service based on duration fields)
get_buyprice($prodfournprice, $qty, $product_id=0, $fourn_ref='', $fk_soc=0)
Read price used by a provider.
clone_fournisseurs($fromId, $toId)
Recopie les fournisseurs et prix fournisseurs d'un produit/service sur un autre.
const TYPE_PRODUCT
Regular product.
add_photo($sdir, $file)
Move an uploaded file described into $file array into target directory $sdir.
log_price_delete($user, $rowid)
Delete a price line.
info($id)
Load information for tab info.
correct_stock($user, $id_entrepot, $nbpiece, $movement, $label='', $price=0, $inventorycode='', $origin_element='', $origin_id=null, $disablestockchangeforsubproduct=0, $extrafields=null)
Adjust stock in a warehouse for product.
generateDocument($modele, $outputlangs, $hidedetails=0, $hidedesc=0, $hideref=0)
Create a document onto disk according to template module.
static getSellOrEatByMandatoryList()
Get the array of labels of Sell by or Eat by all mandatory flags for each status.
getChildsArbo($id, $firstlevelonly=0, $level=1, $parents=array())
Return children of product $id.
load_virtual_stock($includedraftpoforvirtual=null, $dateofvirtualstock=null)
Load value ->stock_theorique of a product.
load_stats_propale($socid=0)
Charge tableau des stats propale pour le produit/service.
get_barcode($object, $type='')
Get a barcode from the module to generate barcode values.
setAccountancyCode($type, $value)
Sets an accountancy code for a product.
getProductsToPreviewInEmail($limit)
Retrieve and display products.
load_stats_facture($socid=0)
Charge tableau des stats facture pour le produit/service.
setCategories($categories)
Sets object to supplied categories.
load_stats_reception($socid=0, $filtrestatut='', $forVirtualStock=0, $dateofvirtualstock=null)
Charge tableau des stats réception fournisseur pour le produit/service.
get_nb_propal($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units in proposals in which product is included.
update($id, $user, $notrigger=0, $action='update', $updatetype=false)
Update a record into database.
setMultiLangs($user)
Update or add a translation for a product.
correct_stock_batch($user, $id_entrepot, $nbpiece, $movement, $label='', $price=0, $dlc='', $dluo='', $lot='', $inventorycode='', $origin_element='', $origin_id=null, $disablestockchangeforsubproduct=0, $extrafields=null, $force_update_batch=false)
Adjust stock in a warehouse for product with batch number.
load_stats_bom($socid=0)
Charge tableau des stats OF pour le produit/service.
hasbatch()
Return if the object has a batch management.
del_sousproduit($fk_parent, $fk_child, $notrigger=0)
Remove a link between a subproduct and a parent product/service.
fetch($id=0, $ref='', $ref_ext='', $barcode='', $ignore_expression=0, $ignore_price_load=0, $ignore_lang_load=0)
Load a product in memory from database.
update_sousproduit($id_pere, $id_fils, $qty, $incdec=1, $notrigger=0)
Modify composed product.
load_stats_commande($socid=0, $filtrestatut='', $forVirtualStock=0)
Charge tableau des stats commande client pour le produit/service.
delete_photo($file)
Delete a photo and its thumbs.
fetch_prod_arbo($prod, $compl_path='', $multiply=1, $level=1, $id_parent=0, $ignore_stock_load=0)
Function recursive, used only by get_arbo_each_prod(), to build tree of subproducts into ->res Define...
getKanbanView($option='', $arraydata=null)
Return clickable link of object (with eventually picto)
getLibStatut($mode=0, $type=0)
Return label of status of object.
load_stats_sending($socid=0, $filtrestatut='', $forVirtualStock=0, $filterShipmentStatus='')
Charge tableau des stats expedition client pour le produit/service.
clone_price($fromId, $toId)
Recopie les prix d'un produit/service sur un autre.
load_stats_inproduction($socid=0, $filtrestatut='', $forVirtualStock=0, $dateofvirtualstock=null, $warehouseid=0)
Charge tableau des stats production pour le produit/service.
check()
Check that ref and label are ok.
initAsSpecimen()
Initialise an instance with random values.
liste_photos($dir, $nbmax=0)
Return an array with all photos of product found on disk.
loadStateBoard()
Load indicators this->nb for the dashboard.
getFather()
Return all parent products for current product (first level only)
getNomUrl($withpicto=0, $option='', $maxlength=0, $save_lastsearch_value=-1, $notooltip=0, $morecss='', $add_label=0, $sep=' - ')
Return clickable link of object (with eventually picto)
getSellOrEatByMandatoryLabel()
Get the label for sell by or eat by mandatory flag of the current product.
verify()
Check properties of product are ok (like name, barcode, ...).
get_sousproduits_arbo()
get_nb_order($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units in orders in which product is included.
loadStockForVirtualProduct($option='', $qtyWish=1)
Load stock for components of virtual product (first level only)
min_recommended_price()
Return minimum product recommended price.
_log_price($user, $level=0)
Insert a track that we changed a customer price.
getLabelOfUnit($type='long', $outputlangs=null, $noentities=0)
Reads the units dictionary to return the translation code of a unit (if type='code'),...
_get_stats($sql, $mode, $year=0)
Return an array formatted for showing graphs.
load_stats_commande_fournisseur($socid=0, $filtrestatut='', $forVirtualStock=0, $dateofvirtualstock=null)
Charge tableau des stats commande fournisseur pour le produit/service.
isMandatoryPeriod()
Return if the object has a constraint on mandatory_period.
isProduct()
Return if the object is a product.
generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
Generates prices for a product based on product multiprice generation rules.
LibStatut($status, $mode=0, $type=0)
Return label of a given status.
const TYPE_SERVICE
Service.
is_photo_available($sdir)
Return if at least one photo is available.
get_image_size($file)
Load size of image file.
get_nb_vente($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units or customers invoices in which product is included.
getTooltipContentArray($params)
getTooltipContentArray
Class to manage products or services.
Manage record for batch number management.
static findAll($dbs, $fk_product_stock, $with_qty=0, $fk_product=0)
Return all batch detail records for a given product and warehouse.
Class with list of lots and properties.
Class to manage Dolibarr users.
hasRight($module, $permlevel1, $permlevel2='')
Return if a user has a permission.
print $langs trans("Ref").' m titre as m m statut as status
Or an array listing all the potential status of the object: array: int of the status => translated la...
Definition index.php:171
getCountry($searchkey, $withcode='', $dbtouse=null, $outputlangs=null, $entconv=1, $searchlabel='')
Return country label, code or id from an id, code or label.
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.
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_mktime($hour, $minute, $second, $month, $day, $year, $gm='auto', $check=1)
Return a timestamp date built from detailed information (by default a local PHP server timestamp) Rep...
img_picto($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $alt='', $morecss='', $marginleftonlyshort=2, $allowothertags=array())
Show picto whatever it's its name (generic function)
dolGetFirstLineOfText($text, $nboflines=1, $charset='UTF-8')
Return first line of text.
dol_osencode($str)
Return a string encoded into OS filesystem encoding.
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
dol_string_nospecial($str, $newstr='_', $badcharstoreplace='', $badcharstoremove='', $keepspaces=0)
Clean a string from all punctuation characters to use it as a ref or login.
img_object($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $allowothertags=array())
Show a picto called object_picto (generic function)
dol_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.
dol_now($mode='auto')
Return date for now.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dol_print_date($time, $format='', $tzoutput='auto', $outputlangs=null, $encodetooutput=false)
Output date in a string format according to outputlangs (or langs if not defined).
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)
dolGetStatus($statusLabel='', $statusLabelShort='', $html='', $statusType='status0', $displayMode=0, $url='', $params=array())
Output the badge of a status.
dol_clone($object, $native=2)
Create a clone of instance of object (new instance with same value for each properties) With native =...
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
dol_trunc($string, $size=40, $trunc='right', $stringencoding='UTF-8', $nodot=0, $display=0)
Truncate a string to a particular length adding '…' if string larger than length.
dol_sanitizeFileName($str, $newstr='_', $unaccent=1, $includequotes=0)
Clean a string to use it as a file name.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
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, when selling a product with vat $vatrate,...
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
getEntity($element, $shared=1, $currentobject=null)
Get list of entity id to use.
dol_mkdir($dir, $dataroot='', $newmask='')
Creation of a directory (this can create recursive subdir)
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
Returns text escaped for inclusion in HTML alt or title or value tags, or into values of HTML input f...
image_format_supported($file, $acceptsvg=0)
Return if a filename is file name of a supported image format.
global $conf
The following vars must be defined: $type2label $form $conf, $lang, The following vars may also be de...
Definition member.php:79
measuring_units_squared($unitscale)
Transform a given unit scale into the square of that unit, if known.
measuringUnitString($unitid, $measuring_style='', $unitscale=null, $use_short_label=0, $outputlangs=null)
Return translation label of a unit key.
measuring_units_cubed($unit)
Transform a given unit scale into the cube of that unit, if known.
if(preg_match('/(crypted|dolcrypt):/i', $dolibarr_main_db_pass)||!empty($dolibarr_main_db_encrypted_pass)) $conf db type
Definition repair.php:158