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 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);
2549 }
2550
2551 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2565 public function get_buyprice($prodfournprice, $qty, $product_id = 0, $fourn_ref = '', $fk_soc = 0)
2566 {
2567 // phpcs:enable
2568 global $action, $hookmanager;
2569
2570 // Call hook if any
2571 if (is_object($hookmanager)) {
2572 $parameters = array(
2573 'prodfournprice' => $prodfournprice,
2574 'qty' => $qty,
2575 'product_id' => $product_id,
2576 'fourn_ref' => $fourn_ref,
2577 'fk_soc' => $fk_soc,
2578 );
2579 // Note that $action and $object may have been modified by some hooks
2580 $reshook = $hookmanager->executeHooks('getBuyPrice', $parameters, $this, $action);
2581 if ($reshook > 0) {
2582 return $hookmanager->resArray;
2583 }
2584 }
2585
2586 $result = 0;
2587
2588 // 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)
2589 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2590 $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,";
2591 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2592 $sql .= " pfp.packaging";
2593 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2594 $sql .= " WHERE pfp.rowid = ".((int) $prodfournprice);
2595 if ($qty > 0) {
2596 $sql .= " AND pfp.quantity <= ".((float) $qty);
2597 }
2598 $sql .= " ORDER BY pfp.quantity DESC";
2599
2600 dol_syslog(get_class($this)."::get_buyprice first search by prodfournprice/qty", LOG_DEBUG);
2601 $resql = $this->db->query($sql);
2602 if ($resql) {
2603 $obj = $this->db->fetch_object($resql);
2604 if ($obj && $obj->quantity > 0) { // If we found a supplier prices from the id of supplier price
2605 if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2606 $prod_supplier = new ProductFournisseur($this->db);
2607 $prod_supplier->product_fourn_price_id = $obj->rowid;
2608 $prod_supplier->id = $obj->fk_product;
2609 $prod_supplier->fourn_qty = $obj->quantity;
2610 $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2611 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2612
2613 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2614 $priceparser = new PriceParser($this->db);
2615 $price_result = $priceparser->parseProductSupplier($prod_supplier);
2616 if ($price_result >= 0) {
2617 $obj->price = $price_result;
2618 }
2619 }
2620 $this->product_fourn_price_id = $obj->rowid;
2621 $this->buyprice = $obj->price; // deprecated
2622 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product of supplier
2623 $this->fourn_price_base_type = 'HT'; // Price base type
2624 $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2625 $this->ref_fourn = $obj->ref_supplier; // deprecated
2626 $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2627 $this->desc_supplier = $obj->desc_supplier; // desc supplier
2628 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2629 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2630 $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2631 $this->fourn_multicurrency_price = $obj->multicurrency_price;
2632 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2633 $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2634 $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2635 $this->fourn_multicurrency_code = $obj->multicurrency_code;
2636 if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2637 $this->packaging = (float) $obj->packaging;
2638 }
2639 $result = $obj->fk_product;
2640 return $result;
2641 } else { // If not found
2642 // 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.
2643 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2644 $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,";
2645 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2646 $sql .= " pfp.packaging";
2647 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2648 $sql .= " WHERE 1 = 1";
2649 if ($product_id > 0) {
2650 $sql .= " AND pfp.fk_product = ".((int) $product_id);
2651 }
2652 if ($fourn_ref != 'none') {
2653 $sql .= " AND pfp.ref_fourn = '".$this->db->escape($fourn_ref)."'";
2654 }
2655 if ($fk_soc > 0) {
2656 $sql .= " AND pfp.fk_soc = ".((int) $fk_soc);
2657 }
2658 if ($qty > 0) {
2659 $sql .= " AND pfp.quantity <= ".((float) $qty);
2660 }
2661 $sql .= " ORDER BY pfp.quantity DESC";
2662 $sql .= " LIMIT 1";
2663
2664 dol_syslog(get_class($this)."::get_buyprice second search from qty/ref/product_id", LOG_DEBUG);
2665 $resql = $this->db->query($sql);
2666 if ($resql) {
2667 $obj = $this->db->fetch_object($resql);
2668 if ($obj && $obj->quantity > 0) { // If found
2669 if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2670 $prod_supplier = new ProductFournisseur($this->db);
2671 $prod_supplier->product_fourn_price_id = $obj->rowid;
2672 $prod_supplier->id = $obj->fk_product;
2673 $prod_supplier->fourn_qty = $obj->quantity;
2674 $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2675 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2676
2677 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2678 $priceparser = new PriceParser($this->db);
2679 $price_result = $priceparser->parseProductSupplier($prod_supplier);
2680 if ($price_result >= 0) {
2681 $obj->price = $price_result;
2682 }
2683 }
2684 $this->product_fourn_price_id = $obj->rowid;
2685 $this->buyprice = $obj->price; // deprecated
2686 $this->fourn_qty = $obj->quantity; // min quantity for price for a virtual supplier
2687 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product for a virtual supplier
2688 $this->fourn_price_base_type = 'HT'; // Price base type for a virtual supplier
2689 $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2690 $this->ref_fourn = $obj->ref_supplier; // deprecated
2691 $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2692 $this->desc_supplier = $obj->desc_supplier; // desc supplier
2693 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2694 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2695 $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2696 $this->fourn_multicurrency_price = $obj->multicurrency_price;
2697 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2698 $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2699 $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2700 $this->fourn_multicurrency_code = $obj->multicurrency_code;
2701 if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2702 $this->packaging = (float) $obj->packaging;
2703 }
2704 $result = $obj->fk_product;
2705 return $result;
2706 } else {
2707 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é.
2708 }
2709 } else {
2710 $this->error = $this->db->lasterror();
2711 return -3;
2712 }
2713 }
2714 } else {
2715 $this->error = $this->db->lasterror();
2716 return -2;
2717 }
2718 }
2719
2720
2739 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)
2740 {
2741 $lastPriceData = $this->getArrayForPriceCompare($level); // temporary store current price before update
2742
2743 $id = $this->id;
2744
2745 dol_syslog(get_class($this)."::update_price id=".$id." newprice=".$newprice." newpricebase=".$newpricebase." newminprice=".$newminprice." level=".$level." npr=".$newnpr." newdefaultvatcode=".$newdefaultvatcode);
2746
2747 // Clean parameters
2748 if (empty($this->tva_tx)) {
2749 $this->tva_tx = 0.0;
2750 }
2751 if (empty($newnpr)) {
2752 $newnpr = 0.0;
2753 }
2754 if (empty($newminprice)) {
2755 $newminprice = 0.0;
2756 }
2757
2758 // Check parameters
2759 if ($newvat === null || $newvat == '') { // Maintain '' for backwards compatibility
2760 $newvat = (float) $this->tva_tx;
2761 }
2762
2763 $localtaxtype1 = '';
2764 $localtaxtype2 = '';
2765
2766 // If multiprices are enabled, then we check if the current product is subject to price autogeneration
2767 // Price will be modified ONLY when the first one is the one that is being modified
2768 if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && !$ignore_autogen && $this->price_autogen && ($level == 1)) {
2769 return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpbq);
2770 }
2771
2772 if (!empty($newminprice) && ($newminprice > $newprice)) {
2773 $this->error = 'ErrorPriceCantBeLowerThanMinPrice';
2774 return -1;
2775 }
2776
2777 if ($newprice === 0 || $newprice !== '') {
2778 if ($newpricebase == 'TTC') {
2779 $price_ttc = (float) price2num($newprice, 'MU');
2780 $price = (float) price2num($newprice) / (1 + ((float) $newvat / 100));
2781 $price = (float) price2num($price, 'MU');
2782
2783 if ((string) $newminprice != '0') {
2784 $price_min_ttc = (float) price2num($newminprice, 'MU');
2785 $price_min = (float) price2num($newminprice) / (1 + ($newvat / 100));
2786 $price_min = (float) price2num($price_min, 'MU');
2787 } else {
2788 $price_min = 0.0;
2789 $price_min_ttc = 0.0;
2790 }
2791 } else {
2792 $price = (float) price2num($newprice, 'MU');
2793 $price_ttc = ($newnpr != 1) ? (float) price2num($newprice) * (1 + ($newvat / 100)) : $price;
2794 $price_ttc = (float) price2num($price_ttc, 'MU');
2795
2796 if ((string) $newminprice != '0') {
2797 $price_min = (float) price2num($newminprice, 'MU');
2798 $price_min_ttc = (float) price2num($newminprice) * (1 + ($newvat / 100));
2799 $price_min_ttc = (float) price2num($price_min_ttc, 'MU');
2800 //print 'X'.$newminprice.'-'.$price_min;
2801 } else {
2802 $price_min = 0.0;
2803 $price_min_ttc = 0.0;
2804 }
2805 }
2806 //print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc;
2807 if (count($localtaxes_array) > 0) {
2808 $localtaxtype1 = $localtaxes_array['0'];
2809 $localtax1 = $localtaxes_array['1'];
2810 $localtaxtype2 = $localtaxes_array['2'];
2811 $localtax2 = $localtaxes_array['3'];
2812 } else {
2813 // if array empty, we try to use the vat code
2814 if (!empty($newdefaultvatcode)) {
2815 global $mysoc;
2816 // Get record from code
2817 $sql = "SELECT t.rowid, t.code, t.recuperableonly as tva_npr, t.localtax1, t.localtax2, t.localtax1_type, t.localtax2_type";
2818 $sql .= " FROM ".MAIN_DB_PREFIX."c_tva as t, ".MAIN_DB_PREFIX."c_country as c";
2819 $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '".$this->db->escape($mysoc->country_code)."'";
2820 $sql .= " AND t.taux = ".((float) $newdefaultvatcode)." AND t.active = 1";
2821 $sql .= " AND t.code = '".$this->db->escape($newdefaultvatcode)."'";
2822 $resql = $this->db->query($sql);
2823 if ($resql) {
2824 $obj = $this->db->fetch_object($resql);
2825 if ($obj) {
2826 $npr = $obj->tva_npr;
2827 $localtax1 = $obj->localtax1;
2828 $localtax2 = $obj->localtax2;
2829 $localtaxtype1 = $obj->localtax1_type;
2830 $localtaxtype2 = $obj->localtax2_type;
2831 }
2832 }
2833 } else {
2834 // old method. deprecated because we can't retrieve type
2835 $localtaxtype1 = '0';
2836 $localtax1 = get_localtax($newvat, 1);
2837 $localtaxtype2 = '0';
2838 $localtax2 = get_localtax($newvat, 2);
2839 }
2840 }
2841 if (empty($localtax1)) {
2842 $localtax1 = 0; // If = '' then = 0
2843 }
2844 if (empty($localtax2)) {
2845 $localtax2 = 0; // If = '' then = 0
2846 }
2847
2848 $this->db->begin();
2849
2850 // Ne pas mettre de quote sur les numeriques decimaux.
2851 // Ceci provoque des stockages avec arrondis en base au lieu des valeurs exactes.
2852 $sql = "UPDATE ".$this->db->prefix()."product SET";
2853 $sql .= " price_base_type = '".$this->db->escape($newpricebase)."',";
2854 $sql .= " price = ".(float) $price.",";
2855 $sql .= " price_ttc = ".(float) $price_ttc.",";
2856 $sql .= " price_min = ".(float) $price_min.",";
2857 $sql .= " price_min_ttc = ".(float) $price_min_ttc.",";
2858 $sql .= " localtax1_tx = ".($localtax1 >= 0 ? (float) $localtax1 : 'NULL').",";
2859 $sql .= " localtax2_tx = ".($localtax2 >= 0 ? (float) $localtax2 : 'NULL').",";
2860 $sql .= " localtax1_type = ".($localtaxtype1 != '' ? "'".$this->db->escape($localtaxtype1)."'" : "'0'").",";
2861 $sql .= " localtax2_type = ".($localtaxtype2 != '' ? "'".$this->db->escape($localtaxtype2)."'" : "'0'").",";
2862 $sql .= " default_vat_code = ".($newdefaultvatcode ? "'".$this->db->escape($newdefaultvatcode)."'" : "null").",";
2863 $sql .= " price_label = ".(!empty($price_label) ? "'".$this->db->escape($price_label)."'" : "null").",";
2864 $sql .= " tva_tx = ".(float) price2num($newvat).",";
2865 $sql .= " recuperableonly = '".$this->db->escape((string) $newnpr)."'";
2866 $sql .= " WHERE rowid = ".((int) $id);
2867
2868 dol_syslog(get_class($this)."::update_price", LOG_DEBUG);
2869 $resql = $this->db->query($sql);
2870 if ($resql) {
2871 $this->multiprices[$level] = $price;
2872 $this->multiprices_ttc[$level] = $price_ttc;
2873 $this->multiprices_min[$level] = $price_min;
2874 $this->multiprices_min_ttc[$level] = $price_min_ttc;
2875 $this->multiprices_base_type[$level] = $newpricebase;
2876 $this->multiprices_default_vat_code[$level] = $newdefaultvatcode;
2877 $this->multiprices_tva_tx[$level] = $newvat;
2878 $this->multiprices_recuperableonly[$level] = $newnpr;
2879
2880 $this->price = $price;
2881 $this->price_label = $price_label;
2882 $this->price_ttc = $price_ttc;
2883 $this->price_min = $price_min;
2884 $this->price_min_ttc = $price_min_ttc;
2885 $this->price_base_type = $newpricebase;
2886 $this->default_vat_code = $newdefaultvatcode;
2887 $this->tva_tx = $newvat;
2888 $this->tva_npr = $newnpr;
2889
2890 //Local taxes
2891 $this->localtax1_tx = $localtax1;
2892 $this->localtax2_tx = $localtax2;
2893 $this->localtax1_type = $localtaxtype1;
2894 $this->localtax2_type = $localtaxtype2;
2895
2896 // Price by quantity
2897 $this->price_by_qty = $newpbq;
2898
2899 // check if price have really change before log
2900 $newPriceData = $this->getArrayForPriceCompare($level);
2901 if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || !getDolGlobalString('PRODUIT_MULTIPRICES')) {
2902 $this->_log_price($user, $level); // Save price for level into table product_price
2903 }
2904
2905 $this->level = $level; // Store level of price edited for trigger
2906
2907 // Call trigger
2908 if (!$notrigger) {
2909 $result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user);
2910 if ($result < 0) {
2911 $this->db->rollback();
2912 return -1;
2913 }
2914 }
2915 // End call triggers
2916
2917 $this->db->commit();
2918 } else {
2919 $this->db->rollback();
2920 $this->error = $this->db->lasterror();
2921 return -1;
2922 }
2923 }
2924
2925 return 1;
2926 }
2927
2935 public function setPriceExpression($expression_id)
2936 {
2937 global $user;
2938
2939 $this->fk_price_expression = $expression_id;
2940
2941 return $this->update($this->id, $user);
2942 }
2943
2956 public function fetch($id = 0, $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0)
2957 {
2958 include_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
2959
2960 global $conf;
2961
2962 dol_syslog(get_class($this)."::fetch id=".$id." ref=".$ref." ref_ext=".$ref_ext);
2963
2964 // Check parameters
2965 if (!$id && !$ref && !$ref_ext && !$barcode) {
2966 $this->error = 'ErrorWrongParameters';
2967 dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR);
2968 return -1;
2969 }
2970
2971 $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,";
2972 $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,";
2973 $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,";
2974 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.last_main_doc,";
2975 $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,";
2976 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
2977 $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,";
2978 } else {
2979 $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,";
2980 }
2981
2982 // For MultiCompany
2983 // PMP per entity & Stocks Sharings stock_reel includes only stocks shared with this entity
2984 $separatedEntityPMP = false; // Set to true to get the AWP from table llx_product_perentity instead of field 'pmp' into llx_product.
2985 $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.
2986 $visibleWarehousesEntities = $conf->entity;
2987 if (getDolGlobalString('MULTICOMPANY_PRODUCT_SHARING_ENABLED')) {
2988 if (getDolGlobalString('MULTICOMPANY_PMP_PER_ENTITY_ENABLED')) {
2989 $checkPMPPerEntity = $this->db->query("SELECT pmp FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = ".((int) $id)." AND entity = ".(int) $conf->entity);
2990 if ($this->db->num_rows($checkPMPPerEntity) > 0) {
2991 $separatedEntityPMP = true;
2992 }
2993 }
2994 global $mc;
2995 $separatedStock = true;
2996 if (isset($mc->sharings['stock']) && !empty($mc->sharings['stock'])) {
2997 $visibleWarehousesEntities .= "," . implode(",", $mc->sharings['stock']);
2998 }
2999 }
3000 if ($separatedEntityPMP) {
3001 $sql .= " ppe.pmp,";
3002 } else {
3003 $sql .= " p.pmp,";
3004 }
3005 $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,";
3006 $sql .= " p.fk_price_expression, p.price_autogen, p.stockable_product, p.model_pdf,";
3007 $sql .= " p.price_label,";
3008 if ($separatedStock) {
3009 $sql .= " SUM(sp.reel) as stock";
3010 } else {
3011 $sql .= " p.stock";
3012 }
3013 $sql .= " FROM ".$this->db->prefix()."product as p";
3014 if (getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED') || $separatedEntityPMP) {
3015 $sql .= " LEFT JOIN " . $this->db->prefix() . "product_perentity as ppe ON ppe.fk_product = p.rowid AND ppe.entity = " . ((int) $conf->entity);
3016 }
3017 if ($separatedStock) {
3018 $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)."))";
3019 }
3020
3021 if ($id) {
3022 $sql .= " WHERE p.rowid = ".((int) $id);
3023 } else {
3024 $sql .= " WHERE p.entity IN (".getEntity($this->element).")";
3025 if ($ref) {
3026 $sql .= " AND p.ref = '".$this->db->escape($ref)."'";
3027 } elseif ($ref_ext) {
3028 $sql .= " AND p.ref_ext = '".$this->db->escape($ref_ext)."'";
3029 } elseif ($barcode) {
3030 $sql .= " AND p.barcode = '".$this->db->escape($barcode)."'";
3031 }
3032 }
3033 if ($separatedStock) {
3034 $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,";
3035 $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,";
3036 $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,";
3037 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
3038 $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,";
3039 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
3040 $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,";
3041 } else {
3042 $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,";
3043 }
3044 if ($separatedEntityPMP) {
3045 $sql .= " ppe.pmp,";
3046 } else {
3047 $sql .= " p.pmp,";
3048 }
3049 $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,";
3050 $sql .= " p.fk_price_expression, p.price_autogen, p.stockable_product, p.model_pdf,";
3051 $sql .= " p.price_label";
3052 if (!$separatedStock) {
3053 $sql .= ", p.stock";
3054 }
3055 }
3056
3057 $resql = $this->db->query($sql);
3058 if ($resql) {
3059 unset($this->oldcopy);
3060
3061 if ($this->db->num_rows($resql) > 0) {
3062 $obj = $this->db->fetch_object($resql);
3063
3064 $this->id = $obj->rowid;
3065 $this->ref = $obj->ref;
3066 $this->ref_ext = $obj->ref_ext;
3067 $this->label = $obj->label;
3068 $this->description = $obj->description;
3069 $this->url = $obj->url;
3070 $this->note_public = $obj->note_public;
3071 $this->note_private = $obj->note_private;
3072 $this->note = $obj->note_private; // deprecated
3073
3074 $this->type = $obj->fk_product_type;
3075 $this->price_label = $obj->price_label;
3076 $this->status = $obj->tosell;
3077 $this->status_buy = $obj->tobuy;
3078 $this->status_batch = $obj->tobatch;
3079 $this->sell_or_eat_by_mandatory = $obj->sell_or_eat_by_mandatory;
3080 $this->batch_mask = $obj->batch_mask;
3081
3082 $this->customcode = $obj->customcode;
3083 $this->country_id = $obj->fk_country;
3084 $this->country_code = getCountry($this->country_id, '2', $this->db);
3085 $this->state_id = $obj->fk_state;
3086 $this->lifetime = $obj->lifetime;
3087 $this->qc_frequency = $obj->qc_frequency;
3088 $this->price = $obj->price;
3089 $this->price_ttc = $obj->price_ttc;
3090 $this->price_min = $obj->price_min;
3091 $this->price_min_ttc = $obj->price_min_ttc;
3092 $this->price_base_type = $obj->price_base_type;
3093 $this->cost_price = isset($obj->cost_price) ? (float) $obj->cost_price : null;
3094 $this->default_vat_code = $obj->default_vat_code;
3095 $this->tva_tx = $obj->tva_tx;
3097 $this->tva_npr = $obj->tva_npr;
3099 $this->localtax1_tx = $obj->localtax1_tx;
3100 $this->localtax2_tx = $obj->localtax2_tx;
3101 $this->localtax1_type = $obj->localtax1_type;
3102 $this->localtax2_type = $obj->localtax2_type;
3103
3104 $this->finished = $obj->finished;
3105 $this->fk_default_bom = $obj->fk_default_bom;
3106
3107 $this->duration = $obj->duration;
3108 $matches = [];
3109 preg_match('/([\d.]+)(\w+)/', $obj->duration, $matches);
3110 $this->duration_value = !empty($matches[1]) ? (float) $matches[1] : 0;
3111 $this->duration_unit = !empty($matches[2]) ? (string) $matches[2] : null;
3112 $this->canvas = $obj->canvas;
3113 $this->net_measure = $obj->net_measure;
3114 $this->net_measure_units = $obj->net_measure_units;
3115 $this->weight = $obj->weight;
3116 $this->weight_units = (is_null($obj->weight_units) ? 0 : $obj->weight_units);
3117 $this->length = $obj->length;
3118 $this->length_units = (is_null($obj->length_units) ? 0 : $obj->length_units);
3119 $this->width = $obj->width;
3120 $this->width_units = (is_null($obj->width_units) ? 0 : $obj->width_units);
3121 $this->height = $obj->height;
3122 $this->height_units = (is_null($obj->height_units) ? 0 : $obj->height_units);
3123
3124 $this->surface = $obj->surface;
3125 $this->surface_units = (is_null($obj->surface_units) ? 0 : $obj->surface_units);
3126 $this->volume = $obj->volume;
3127 $this->volume_units = (is_null($obj->volume_units) ? 0 : $obj->volume_units);
3128 $this->barcode = $obj->barcode;
3129 $this->barcode_type = $obj->fk_barcode_type;
3130
3131 $this->accountancy_code_buy = $obj->accountancy_code_buy;
3132 $this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra;
3133 $this->accountancy_code_buy_export = $obj->accountancy_code_buy_export;
3134 $this->accountancy_code_sell = $obj->accountancy_code_sell;
3135 $this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra;
3136 $this->accountancy_code_sell_export = $obj->accountancy_code_sell_export;
3137
3138 $this->fk_default_warehouse = $obj->fk_default_warehouse;
3139 $this->fk_default_workstation = $obj->fk_default_workstation;
3140 $this->seuil_stock_alerte = $obj->seuil_stock_alerte;
3141 $this->desiredstock = $obj->desiredstock;
3142 $this->stock_reel = $obj->stock;
3143 $this->stockable_product = $obj->stockable_product;
3144 $this->pmp = $obj->pmp;
3145
3146 $this->date_creation = $this->db->jdate($obj->datec);
3147 $this->date_modification = $this->db->jdate($obj->tms);
3148
3149 $this->import_key = $obj->import_key;
3150 $this->entity = $obj->entity;
3151
3152 $this->ref_ext = $obj->ref_ext;
3153 $this->fk_price_expression = $obj->fk_price_expression;
3154 $this->fk_unit = $obj->fk_unit;
3155 $this->price_autogen = $obj->price_autogen;
3156 $this->model_pdf = $obj->model_pdf;
3157 $this->last_main_doc = $obj->last_main_doc;
3158
3159 $this->mandatory_period = $obj->mandatory_period;
3160
3161 if (getDolGlobalString('PRODUCT_USE_CUSTOMER_PACKAGING')) {
3162 $this->packaging = (float) $obj->packaging;
3163 }
3164
3165 $this->db->free($resql);
3166
3167 // fetch optionals attributes and labels
3168 $this->fetch_optionals();
3169
3170 // Multilangs
3171 if (getDolGlobalInt('MAIN_MULTILANGS') && empty($ignore_lang_load)) {
3172 $this->getMultiLangs();
3173 }
3174
3175 // Load multiprices array
3176 if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && empty($ignore_price_load)) { // prices per segment
3177 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
3178 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
3179 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3180 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
3181 $sql .= " ,price_label";
3182 $sql .= " FROM ".$this->db->prefix()."product_price";
3183 $sql .= " WHERE entity IN (".getEntity('productprice').")";
3184 $sql .= " AND price_level=".((int) $i);
3185 $sql .= " AND fk_product = ".((int) $this->id);
3186 $sql .= " ORDER BY date_price DESC, rowid DESC"; // Get the most recent line
3187 $sql .= " LIMIT 1"; // Only the first one
3188 $resql = $this->db->query($sql);
3189 if ($resql) {
3190 $result = $this->db->fetch_array($resql);
3191
3192 $this->multiprices[$i] = $result ? $result["price"] : null;
3193 $this->multiprices_ttc[$i] = $result ? $result["price_ttc"] : null;
3194 $this->multiprices_min[$i] = $result ? $result["price_min"] : null;
3195 $this->multiprices_min_ttc[$i] = $result ? $result["price_min_ttc"] : null;
3196 $this->multiprices_base_type[$i] = $result ? $result["price_base_type"] : null;
3197 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
3198 $this->multiprices_tva_tx[$i] = $result ? $result["tva_tx"].(!empty($result['default_vat_code']) ? ' ('.$result['default_vat_code'].')' : '') : null;
3199 $this->multiprices_recuperableonly[$i] = $result ? $result["recuperableonly"] : null;
3200
3201 // Price by quantity
3202 /*
3203 $this->prices_by_qty[$i]=$result["price_by_qty"];
3204 $this->prices_by_qty_id[$i]=$result["rowid"];
3205 // Récuperation de la liste des prix selon qty si flag positionné
3206 if ($this->prices_by_qty[$i] == 1)
3207 {
3208 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
3209 $sql.= " FROM ".$this->db->prefix()."product_price_by_qty";
3210 $sql.= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
3211 $sql.= " ORDER BY quantity ASC";
3212
3213 $resql = $this->db->query($sql);
3214 if ($resql)
3215 {
3216 $resultat=array();
3217 $ii=0;
3218 while ($result= $this->db->fetch_array($resql)) {
3219 $resultat[$ii]=array();
3220 $resultat[$ii]["rowid"]=$result["rowid"];
3221 $resultat[$ii]["price"]= $result["price"];
3222 $resultat[$ii]["unitprice"]= $result["unitprice"];
3223 $resultat[$ii]["quantity"]= $result["quantity"];
3224 $resultat[$ii]["remise_percent"]= $result["remise_percent"];
3225 $resultat[$ii]["remise"]= $result["remise"]; // deprecated
3226 $resultat[$ii]["price_base_type"]= $result["price_base_type"];
3227 $ii++;
3228 }
3229 $this->prices_by_qty_list[$i]=$resultat;
3230 }
3231 else
3232 {
3233 dol_print_error($this->db);
3234 return -1;
3235 }
3236 }*/
3237 } else {
3238 $this->error = $this->db->lasterror;
3239 return -1;
3240 }
3241 }
3242 } elseif ((getDolGlobalString('PRODUIT_CUSTOMER_PRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_AND_MULTIPRICES')) && empty($ignore_price_load)) { // prices per customers
3243 // Nothing loaded by default. List may be very long.
3244 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY') && empty($ignore_price_load)) { // prices per quantity
3245 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3246 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid";
3247 $sql .= " FROM ".$this->db->prefix()."product_price";
3248 $sql .= " WHERE fk_product = ".((int) $this->id);
3249 $sql .= " ORDER BY date_price DESC, rowid DESC";
3250 $sql .= " LIMIT 1";
3251
3252 $resql = $this->db->query($sql);
3253 if ($resql) {
3254 $result = $this->db->fetch_array($resql);
3255
3256 if ($result) {
3257 // Price by quantity
3258 $this->prices_by_qty[0] = $result["price_by_qty"];
3259 $this->prices_by_qty_id[0] = $result["rowid"];
3260 // Récuperation de la liste des prix selon qty si flag positionné
3261 if ($this->prices_by_qty[0] == 1) {
3262 $sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise, remise, price_base_type";
3263 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
3264 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[0]);
3265 $sql .= " ORDER BY quantity ASC";
3266
3267 $resql = $this->db->query($sql);
3268 if ($resql) {
3269 $resultat = array();
3270 $ii = 0;
3271 while ($result = $this->db->fetch_array($resql)) {
3272 $resultat[$ii] = array();
3273 $resultat[$ii]["rowid"] = $result["rowid"];
3274 $resultat[$ii]["price"] = $result["price"];
3275 $resultat[$ii]["unitprice"] = $result["unitprice"];
3276 $resultat[$ii]["quantity"] = $result["quantity"];
3277 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
3278 //$resultat[$ii]["remise"]= $result["remise"]; // deprecated
3279 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
3280 $ii++;
3281 }
3282 $this->prices_by_qty_list[0] = $resultat;
3283 } else {
3284 $this->error = $this->db->lasterror;
3285 return -1;
3286 }
3287 }
3288 }
3289 } else {
3290 $this->error = $this->db->lasterror;
3291 return -1;
3292 }
3293 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') && empty($ignore_price_load)) { // prices per customer and quantity
3294 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
3295 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
3296 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
3297 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
3298 $sql .= " FROM ".$this->db->prefix()."product_price";
3299 $sql .= " WHERE entity IN (".getEntity('productprice').")";
3300 $sql .= " AND price_level=".((int) $i);
3301 $sql .= " AND fk_product = ".((int) $this->id);
3302 $sql .= " ORDER BY date_price DESC, rowid DESC";
3303 $sql .= " LIMIT 1";
3304 $resql = $this->db->query($sql);
3305 if (!$resql) {
3306 $this->error = $this->db->lasterror;
3307 return -1;
3308 } else {
3309 $result = $this->db->fetch_array($resql);
3310 $this->multiprices[$i] = (!empty($result["price"]) ? $result["price"] : 0);
3311 $this->multiprices_ttc[$i] = (!empty($result["price_ttc"]) ? $result["price_ttc"] : 0);
3312 $this->multiprices_min[$i] = (!empty($result["price_min"]) ? $result["price_min"] : 0);
3313 $this->multiprices_min_ttc[$i] = (!empty($result["price_min_ttc"]) ? $result["price_min_ttc"] : 0);
3314 $this->multiprices_base_type[$i] = (!empty($result["price_base_type"]) ? $result["price_base_type"] : '');
3315 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
3316 $this->multiprices_tva_tx[$i] = (!empty($result["tva_tx"]) ? $result["tva_tx"] : 0); // TODO Add ' ('.$result['default_vat_code'].')'
3317 $this->multiprices_recuperableonly[$i] = (!empty($result["recuperableonly"]) ? $result["recuperableonly"] : 0);
3318
3319 // Price by quantity
3320 $this->prices_by_qty[$i] = (!empty($result["price_by_qty"]) ? $result["price_by_qty"] : 0);
3321 $this->prices_by_qty_id[$i] = (!empty($result["rowid"]) ? $result["rowid"] : 0);
3322 // Récuperation de la liste des prix selon qty si flag positionné
3323 if ($this->prices_by_qty[$i] == 1) {
3324 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
3325 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
3326 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
3327 $sql .= " ORDER BY quantity ASC";
3328
3329 $resql = $this->db->query($sql);
3330 if ($resql) {
3331 $resultat = array();
3332 $ii = 0;
3333 while ($result = $this->db->fetch_array($resql)) {
3334 $resultat[$ii] = array();
3335 $resultat[$ii]["rowid"] = $result["rowid"];
3336 $resultat[$ii]["price"] = $result["price"];
3337 $resultat[$ii]["unitprice"] = $result["unitprice"];
3338 $resultat[$ii]["quantity"] = $result["quantity"];
3339 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
3340 $resultat[$ii]["remise"] = $result["remise"]; // deprecated
3341 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
3342 $ii++;
3343 }
3344 $this->prices_by_qty_list[$i] = $resultat;
3345 } else {
3346 $this->error = $this->db->lasterror;
3347 return -1;
3348 }
3349 }
3350 }
3351 }
3352 }
3353
3354 if (isModEnabled('dynamicprices') && !empty($this->fk_price_expression) && empty($ignore_expression)) {
3355 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
3356 $priceparser = new PriceParser($this->db);
3357 $price_result = $priceparser->parseProduct($this);
3358 if ($price_result >= 0) {
3359 $this->price = $price_result;
3360 // Calculate the VAT
3361 $this->price_ttc = (float) price2num($this->price) * (1 + ($this->tva_tx / 100));
3362 $this->price_ttc = (float) price2num($this->price_ttc, 'MU');
3363 }
3364 }
3365
3366 // We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product.
3367 // Instead we just init the stock_warehouse array
3368 $this->stock_warehouse = array();
3369
3370 return 1;
3371 } else {
3372 return 0;
3373 }
3374 } else {
3375 $this->error = $this->db->lasterror();
3376 return -1;
3377 }
3378 }
3379
3380 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3387 public function load_stats_mo($socid = 0)
3388 {
3389 // phpcs:enable
3390 global $user, $hookmanager, $action;
3391
3392 $error = 0;
3393
3394 foreach (array('toconsume', 'consumed', 'toproduce', 'produced') as $role) {
3395 $this->stats_mo['customers_'.$role] = 0;
3396 $this->stats_mo['nb_'.$role] = 0;
3397 $this->stats_mo['qty_'.$role] = 0;
3398
3399 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3400 $sql .= " SUM(mp.qty) as qty";
3401 $sql .= " FROM ".$this->db->prefix()."mrp_mo as c";
3402 $sql .= " INNER JOIN ".$this->db->prefix()."mrp_production as mp ON mp.fk_mo=c.rowid";
3403 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3404 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3405 }
3406 $sql .= " WHERE ";
3407 $sql .= " c.entity IN (".getEntity('mo').")";
3408
3409 $sql .= " AND mp.fk_product = ".((int) $this->id);
3410 $sql .= " AND mp.role ='".$this->db->escape($role)."'";
3411 if ($socid > 0) {
3412 $sql .= " AND c.fk_soc = ".((int) $socid);
3413 }
3414
3415 $result = $this->db->query($sql);
3416 if ($result) {
3417 $obj = $this->db->fetch_object($result);
3418 $this->stats_mo['customers_'.$role] = $obj->nb_customers ? $obj->nb_customers : 0;
3419 $this->stats_mo['nb_'.$role] = $obj->nb ? $obj->nb : 0;
3420 $this->stats_mo['qty_'.$role] = $obj->qty ? price2num($obj->qty, 'MS') : 0; // qty may be a float due to the SUM()
3421 } else {
3422 $this->error = $this->db->error();
3423 $error++;
3424 }
3425 }
3426
3427 if (!empty($error)) {
3428 return -1;
3429 }
3430
3431 $parameters = array('socid' => $socid);
3432 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3433 if ($reshook > 0) {
3434 $this->stats_mo = $hookmanager->resArray['stats_mo'];
3435 }
3436
3437 return 1;
3438 }
3439
3440 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3447 public function load_stats_bom($socid = 0)
3448 {
3449 // phpcs:enable
3450 global $hookmanager, $action;
3451
3452 $error = 0;
3453
3454 $this->stats_bom['nb_toproduce'] = 0;
3455 $this->stats_bom['nb_toconsume'] = 0;
3456 $this->stats_bom['qty_toproduce'] = 0;
3457 $this->stats_bom['qty_toconsume'] = 0;
3458
3459 $sql = "SELECT COUNT(DISTINCT b.rowid) as nb_toproduce,";
3460 $sql .= " SUM(b.qty) as qty_toproduce";
3461 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3462 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom = b.rowid";
3463 $sql .= " WHERE ";
3464 $sql .= " b.entity IN (".getEntity('bom').")";
3465 $sql .= " AND b.fk_product =".((int) $this->id);
3466 $sql .= " GROUP BY b.rowid";
3467
3468 $result = $this->db->query($sql);
3469 if ($result) {
3470 $obj = $this->db->fetch_object($result);
3471 $this->stats_bom['nb_toproduce'] = !empty($obj->nb_toproduce) ? $obj->nb_toproduce : 0;
3472 $this->stats_bom['qty_toproduce'] = !empty($obj->qty_toproduce) ? price2num($obj->qty_toproduce) : 0;
3473 } else {
3474 $this->error = $this->db->error();
3475 $error++;
3476 }
3477
3478 $sql = "SELECT COUNT(DISTINCT bl.rowid) as nb_toconsume,";
3479 $sql .= " SUM(bl.qty) as qty_toconsume";
3480 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3481 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
3482 $sql .= " WHERE ";
3483 $sql .= " b.entity IN (".getEntity('bom').")";
3484 $sql .= " AND bl.fk_product =".((int) $this->id);
3485
3486 $result = $this->db->query($sql);
3487 if ($result) {
3488 $obj = $this->db->fetch_object($result);
3489 $this->stats_bom['nb_toconsume'] = !empty($obj->nb_toconsume) ? $obj->nb_toconsume : 0;
3490 $this->stats_bom['qty_toconsume'] = !empty($obj->qty_toconsume) ? price2num($obj->qty_toconsume) : 0;
3491 } else {
3492 $this->error = $this->db->error();
3493 $error++;
3494 }
3495
3496 if (!empty($error)) {
3497 return -1;
3498 }
3499
3500 $parameters = array('socid' => $socid);
3501 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3502 if ($reshook > 0) {
3503 $this->stats_bom = $hookmanager->resArray['stats_bom'];
3504 }
3505
3506 return 1;
3507 }
3508
3509 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3516 public function load_stats_propale($socid = 0)
3517 {
3518 // phpcs:enable
3519 global $user, $hookmanager, $action;
3520
3521 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,";
3522 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3523 $sql .= " FROM ".$this->db->prefix()."propaldet as pd";
3524 $sql .= ", ".$this->db->prefix()."propal as p";
3525 $sql .= ", ".$this->db->prefix()."societe as s";
3526 $sql .= " WHERE p.rowid = pd.fk_propal";
3527 $sql .= " AND p.fk_soc = s.rowid";
3528 $sql .= " AND p.entity IN (".getEntity('propal').")";
3529 $sql .= " AND pd.fk_product = ".((int) $this->id);
3530 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3531 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = p.fk_soc AND sc.fk_user = ".((int) $user->id);
3532 }
3533 //$sql.= " AND pr.fk_statut != 0";
3534 if ($socid > 0) {
3535 $sql .= " AND p.fk_soc = ".((int) $socid);
3536 }
3537
3538 $result = $this->db->query($sql);
3539 if ($result) {
3540 $obj = $this->db->fetch_object($result);
3541 $this->stats_propale['customers'] = $obj->nb_customers;
3542 $this->stats_propale['nb'] = $obj->nb;
3543 $this->stats_propale['rows'] = $obj->nb_rows;
3544 $this->stats_propale['qty'] = $obj->qty ? $obj->qty : 0;
3545
3546 // if it's a virtual product, maybe it is in proposal by extension
3547 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3548 $TFather = $this->getFather();
3549 if (is_array($TFather) && !empty($TFather)) {
3550 foreach ($TFather as &$fatherData) {
3551 $pFather = new Product($this->db);
3552 $pFather->id = $fatherData['id'];
3553 $qtyCoef = $fatherData['qty'];
3554
3555 if ($fatherData['incdec']) {
3556 $pFather->load_stats_propale($socid);
3557
3558 $this->stats_propale['customers'] += $pFather->stats_propale['customers'];
3559 $this->stats_propale['nb'] += $pFather->stats_propale['nb'];
3560 $this->stats_propale['rows'] += $pFather->stats_propale['rows'];
3561 $this->stats_propale['qty'] += $pFather->stats_propale['qty'] * $qtyCoef;
3562 }
3563 }
3564 }
3565 }
3566
3567 $parameters = array('socid' => $socid);
3568 $reshook = $hookmanager->executeHooks('loadStatsCustomerProposal', $parameters, $this, $action);
3569 if ($reshook > 0) {
3570 $this->stats_propale = $hookmanager->resArray['stats_propale'];
3571 }
3572
3573 return 1;
3574 } else {
3575 $this->error = $this->db->error();
3576 return -1;
3577 }
3578 }
3579
3580
3581 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3588 public function load_stats_proposal_supplier($socid = 0)
3589 {
3590 // phpcs:enable
3591 global $user, $hookmanager, $action;
3592
3593 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,";
3594 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3595 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as pd";
3596 $sql .= ", ".$this->db->prefix()."supplier_proposal as p";
3597 $sql .= ", ".$this->db->prefix()."societe as s";
3598 $sql .= " WHERE p.rowid = pd.fk_supplier_proposal";
3599 $sql .= " AND p.fk_soc = s.rowid";
3600 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
3601 $sql .= " AND pd.fk_product = ".((int) $this->id);
3602 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
3603 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = p.fk_soc AND sc.fk_user = ".((int) $user->id);
3604 }
3605 //$sql.= " AND pr.fk_statut != 0";
3606 if ($socid > 0) {
3607 $sql .= " AND p.fk_soc = ".((int) $socid);
3608 }
3609
3610 $result = $this->db->query($sql);
3611 if ($result) {
3612 $obj = $this->db->fetch_object($result);
3613 $this->stats_proposal_supplier['suppliers'] = $obj->nb_suppliers;
3614 $this->stats_proposal_supplier['nb'] = $obj->nb;
3615 $this->stats_proposal_supplier['rows'] = $obj->nb_rows;
3616 $this->stats_proposal_supplier['qty'] = $obj->qty ? $obj->qty : 0;
3617
3618 $parameters = array('socid' => $socid);
3619 $reshook = $hookmanager->executeHooks('loadStatsSupplierProposal', $parameters, $this, $action);
3620 if ($reshook > 0) {
3621 $this->stats_proposal_supplier = $hookmanager->resArray['stats_proposal_supplier'];
3622 }
3623
3624 return 1;
3625 } else {
3626 $this->error = $this->db->error();
3627 return -1;
3628 }
3629 }
3630
3631
3632 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3641 public function load_stats_commande($socid = 0, $filtrestatut = '', $forVirtualStock = 0)
3642 {
3643 // phpcs:enable
3644 global $user, $hookmanager, $action;
3645
3646 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3647 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3648 $sql .= " FROM ".$this->db->prefix()."commandedet as cd";
3649 $sql .= ", ".$this->db->prefix()."commande as c";
3650 $sql .= ", ".$this->db->prefix()."societe as s";
3651 $sql .= " WHERE c.rowid = cd.fk_commande";
3652 $sql .= " AND c.fk_soc = s.rowid";
3653 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'commande').")";
3654 $sql .= " AND cd.fk_product = ".((int) $this->id);
3655 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3656 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3657 }
3658 if ($socid > 0) {
3659 $sql .= " AND c.fk_soc = ".((int) $socid);
3660 }
3661 if ($filtrestatut != '') {
3662 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3663 }
3664
3665 $result = $this->db->query($sql);
3666 if ($result) {
3667 $obj = $this->db->fetch_object($result);
3668 $this->stats_commande['customers'] = $obj->nb_customers;
3669 $this->stats_commande['nb'] = $obj->nb;
3670 $this->stats_commande['rows'] = $obj->nb_rows;
3671 $this->stats_commande['qty'] = $obj->qty ? $obj->qty : 0;
3672
3673 // if it's a virtual product, maybe it is in order by extension
3674 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3675 $TFather = $this->getFather();
3676 if (is_array($TFather) && !empty($TFather)) {
3677 foreach ($TFather as &$fatherData) {
3678 $pFather = new Product($this->db);
3679 $pFather->id = $fatherData['id'];
3680 $qtyCoef = $fatherData['qty'];
3681
3682 if ($fatherData['incdec']) {
3683 $pFather->load_stats_commande($socid, $filtrestatut);
3684
3685 $this->stats_commande['customers'] += $pFather->stats_commande['customers'];
3686 $this->stats_commande['nb'] += $pFather->stats_commande['nb'];
3687 $this->stats_commande['rows'] += $pFather->stats_commande['rows'];
3688 $this->stats_commande['qty'] += $pFather->stats_commande['qty'] * $qtyCoef;
3689 }
3690 }
3691 }
3692 }
3693
3694 // If stock decrease is on invoice validation, the theoretical stock continue to
3695 // count the orders lines in theoretical stock when some are already removed by invoice validation.
3696 if ($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
3697 if (getDolGlobalString('DECREASE_ONLY_UNINVOICEDPRODUCTS')) {
3698 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is on, we make a compensation but only if order not yet invoice.
3699 $adeduire = 0;
3700 $sql = "SELECT SUM(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3701 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3702 $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'))";
3703 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid";
3704 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3705
3706 dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3707 $resql = $this->db->query($sql);
3708 if ($resql) {
3709 if ($this->db->num_rows($resql) > 0) {
3710 $obj = $this->db->fetch_object($resql);
3711 $adeduire += $obj->count;
3712 }
3713 }
3714
3715 $this->stats_commande['qty'] -= $adeduire;
3716 } else {
3717 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is off, we make a compensation with lines of invoices linked to the order
3718 include_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
3719
3720 // For every order having invoice already validated we need to decrease stock cause it's in physical stock
3721 $adeduire = 0;
3722 $sql = "SELECT sum(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count";
3723 $sql .= " FROM " . $this->db->prefix() . "facturedet as fd";
3724 $sql .= " JOIN " . $this->db->prefix() . "facture as f ON fd.fk_facture = f.rowid";
3725 $sql .= " JOIN " . $this->db->prefix() . "element_element as el ON el.fk_target = f.rowid AND el.targettype = 'facture' AND el.sourcetype = 'commande'";
3726 $sql .= " JOIN " . $this->db->prefix() . "commande as c ON el.fk_source = c.rowid";
3727 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product = ".((int) $this->id);
3728 $sql .= " UNION ALL";
3729 $sql .= " SELECT sum(".$this->db->ifsql('f.type=2', '-1', '1')." * fd.qty) as count";
3730 $sql .= " FROM " . $this->db->prefix() . "facturedet as fd";
3731 $sql .= " JOIN " . $this->db->prefix() . "facture as f ON fd.fk_facture = f.rowid";
3732 $sql .= " JOIN " . $this->db->prefix() . "element_element as el ON el.fk_source = f.rowid AND el.targettype = 'commande' AND el.sourcetype = 'facture'";
3733 $sql .= " JOIN " . $this->db->prefix() . "commande as c ON el.fk_source = c.rowid";
3734 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product =".((int) $this->id);
3735 dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3736 $resql = $this->db->query($sql);
3737 if ($resql) {
3738 if ($this->db->num_rows($resql) > 0) {
3739 $obj = $this->db->fetch_object($resql);
3740 $adeduire += $obj->count;
3741 }
3742 } else {
3743 $this->error = $this->db->error();
3744 return -1;
3745 }
3746
3747 $this->stats_commande['qty'] -= $adeduire;
3748 }
3749 }
3750
3751 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3752 $reshook = $hookmanager->executeHooks('loadStatsCustomerOrder', $parameters, $this, $action);
3753 if ($reshook > 0) {
3754 $this->stats_commande = $hookmanager->resArray['stats_commande'];
3755 }
3756 return 1;
3757 } else {
3758 $this->error = $this->db->error();
3759 return -1;
3760 }
3761 }
3762
3763 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3773 public function load_stats_commande_fournisseur($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3774 {
3775 // phpcs:enable
3776 global $user, $hookmanager, $action;
3777
3778 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,";
3779 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3780 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as cd";
3781 $sql .= ", ".$this->db->prefix()."commande_fournisseur as c";
3782 $sql .= ", ".$this->db->prefix()."societe as s";
3783 $sql .= " WHERE c.rowid = cd.fk_commande";
3784 $sql .= " AND c.fk_soc = s.rowid";
3785 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
3786 $sql .= " AND cd.fk_product = ".((int) $this->id);
3787 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3788 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
3789 }
3790 if ($socid > 0) {
3791 $sql .= " AND c.fk_soc = ".((int) $socid);
3792 }
3793 if ($filtrestatut != '') {
3794 $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")"; // Peut valoir 0
3795 }
3796 if (!empty($dateofvirtualstock)) {
3797 $sql .= " AND c.date_livraison <= '".$this->db->idate($dateofvirtualstock)."'";
3798 }
3799
3800 $result = $this->db->query($sql);
3801 if ($result) {
3802 $obj = $this->db->fetch_object($result);
3803 $this->stats_commande_fournisseur['suppliers'] = $obj->nb_suppliers;
3804 $this->stats_commande_fournisseur['nb'] = $obj->nb;
3805 $this->stats_commande_fournisseur['rows'] = $obj->nb_rows;
3806 $this->stats_commande_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3807
3808 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3809 $reshook = $hookmanager->executeHooks('loadStatsSupplierOrder', $parameters, $this, $action);
3810 if ($reshook > 0) {
3811 $this->stats_commande_fournisseur = $hookmanager->resArray['stats_commande_fournisseur'];
3812 }
3813
3814 return 1;
3815 } else {
3816 $this->error = $this->db->error().' sql='.$sql;
3817 return -1;
3818 }
3819 }
3820
3821 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3831 public function load_stats_sending($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $filterShipmentStatus = '')
3832 {
3833 // phpcs:enable
3834 global $user, $hookmanager, $action;
3835
3836 $sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,";
3837 $sql .= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty";
3838 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
3839 $sql .= ", ".$this->db->prefix()."commandedet as cd";
3840 $sql .= ", ".$this->db->prefix()."commande as c";
3841 $sql .= ", ".$this->db->prefix()."expedition as e";
3842 $sql .= ", ".$this->db->prefix()."societe as s";
3843 $sql .= " WHERE e.rowid = ed.fk_expedition";
3844 $sql .= " AND c.rowid = cd.fk_commande";
3845 $sql .= " AND e.fk_soc = s.rowid";
3846 $sql .= " AND e.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'expedition').")";
3847 $sql .= " AND ed.fk_elementdet = cd.rowid";
3848 $sql .= " AND cd.fk_product = ".((int) $this->id);
3849 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3850 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = e.fk_soc AND sc.fk_user = ".((int) $user->id);
3851 }
3852 if ($socid > 0) {
3853 $sql .= " AND e.fk_soc = ".((int) $socid);
3854 }
3855 if ($filtrestatut != '') {
3856 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3857 }
3858 if (!empty($filterShipmentStatus)) {
3859 $sql .= " AND e.fk_statut IN (".$this->db->sanitize($filterShipmentStatus).")";
3860 }
3861
3862 $result = $this->db->query($sql);
3863 if ($result) {
3864 $obj = $this->db->fetch_object($result);
3865 $this->stats_expedition['customers'] = $obj->nb_customers;
3866 $this->stats_expedition['nb'] = $obj->nb;
3867 $this->stats_expedition['rows'] = $obj->nb_rows;
3868 $this->stats_expedition['qty'] = $obj->qty ? $obj->qty : 0;
3869
3870 // if it's a virtual product, maybe it is in sending by extension
3871 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3872 $TFather = $this->getFather();
3873 if (is_array($TFather) && !empty($TFather)) {
3874 foreach ($TFather as &$fatherData) {
3875 $pFather = new Product($this->db);
3876 $pFather->id = $fatherData['id'];
3877 $qtyCoef = $fatherData['qty'];
3878
3879 if ($fatherData['incdec']) {
3880 $pFather->load_stats_sending($socid, $filtrestatut, $forVirtualStock);
3881
3882 $this->stats_expedition['customers'] += $pFather->stats_expedition['customers'];
3883 $this->stats_expedition['nb'] += $pFather->stats_expedition['nb'];
3884 $this->stats_expedition['rows'] += $pFather->stats_expedition['rows'];
3885 $this->stats_expedition['qty'] += $pFather->stats_expedition['qty'] * $qtyCoef;
3886 }
3887 }
3888 }
3889 }
3890
3891 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock, 'filterShipmentStatus' => $filterShipmentStatus);
3892 $reshook = $hookmanager->executeHooks('loadStatsSending', $parameters, $this, $action);
3893 if ($reshook > 0) {
3894 $this->stats_expedition = $hookmanager->resArray['stats_expedition'];
3895 }
3896
3897 return 1;
3898 } else {
3899 $this->error = $this->db->error();
3900 return -1;
3901 }
3902 }
3903
3904 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3914 public function load_stats_reception($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3915 {
3916 // phpcs:enable
3917 global $user, $hookmanager, $action;
3918
3919 $sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_suppliers, COUNT(DISTINCT cf.rowid) as nb,";
3920 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3921 $sql .= " FROM ".$this->db->prefix()."receptiondet_batch as fd";
3922 $sql .= ", ".$this->db->prefix()."commande_fournisseur as cf";
3923 $sql .= ", ".$this->db->prefix()."societe as s";
3924 $sql .= " WHERE cf.rowid = fd.fk_element";
3925 $sql .= " AND cf.fk_soc = s.rowid";
3926 $sql .= " AND cf.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
3927 $sql .= " AND fd.fk_product = ".((int) $this->id);
3928 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3929 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = cf.fk_soc AND sc.fk_user = ".((int) $user->id);
3930 }
3931 if ($socid > 0) {
3932 $sql .= " AND cf.fk_soc = ".((int) $socid);
3933 }
3934 if ($filtrestatut != '') {
3935 $sql .= " AND cf.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3936 }
3937 if (!empty($dateofvirtualstock)) {
3938 $sql .= " AND fd.datec <= '".$this->db->idate($dateofvirtualstock)."'";
3939 }
3940
3941 $result = $this->db->query($sql);
3942 if ($result) {
3943 $obj = $this->db->fetch_object($result);
3944 $this->stats_reception['suppliers'] = $obj->nb_suppliers;
3945 $this->stats_reception['nb'] = $obj->nb;
3946 $this->stats_reception['rows'] = $obj->nb_rows;
3947 $this->stats_reception['qty'] = $obj->qty ? $obj->qty : 0;
3948
3949 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3950 $reshook = $hookmanager->executeHooks('loadStatsReception', $parameters, $this, $action);
3951 if ($reshook > 0) {
3952 $this->stats_reception = $hookmanager->resArray['stats_reception'];
3953 }
3954
3955 return 1;
3956 } else {
3957 $this->error = $this->db->error();
3958 return -1;
3959 }
3960 }
3961
3962 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3973 public function load_stats_inproduction($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null, $warehouseid = 0)
3974 {
3975 // phpcs:enable
3976 global $user, $hookmanager, $action;
3977
3978 $serviceStockIsEnabled = isModEnabled("service") && getDolGlobalString('STOCK_SUPPORTS_SERVICES');
3979
3980 $sql = "SELECT COUNT(DISTINCT m.fk_soc) as nb_customers, COUNT(DISTINCT m.rowid) as nb,";
3981 $sql .= " COUNT(mp.rowid) as nb_rows, SUM(mp.qty) as qty, role";
3982 $sql .= " FROM ".$this->db->prefix()."mrp_production as mp";
3983 $sql .= ", ".$this->db->prefix()."mrp_mo as m";
3984 $sql .= " LEFT JOIN ".$this->db->prefix()."societe as s ON s.rowid = m.fk_soc";
3985 $sql .= " WHERE m.rowid = mp.fk_mo";
3986 $sql .= " AND m.entity IN (".getEntity(($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE')) ? 'stock' : 'mrp').")";
3987 $sql .= " AND mp.fk_product = ".((int) $this->id);
3988 $sql .= " AND (mp.disable_stock_change IN (0) OR mp.disable_stock_change IS NULL)";
3989 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) { // For external user, restriction is done on filter on fk_soc directly
3990 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = m.fk_soc AND sc.fk_user = ".((int) $user->id);
3991 }
3992 if ($socid > 0) {
3993 $sql .= " AND m.fk_soc = ".((int) $socid);
3994 }
3995 if ($filtrestatut != '') {
3996 $sql .= " AND m.status IN (".$this->db->sanitize($filtrestatut).")";
3997 }
3998 if (!empty($dateofvirtualstock)) {
3999 $sql .= " AND m.date_valid <= '".$this->db->idate($dateofvirtualstock)."'"; // better date to code ? end of production ?
4000 }
4001 if (!$serviceStockIsEnabled) {
4002 $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))";
4003 }
4004 if (!empty($warehouseid)) {
4005 $sql .= " AND m.fk_warehouse = ".((int) $warehouseid);
4006 }
4007 $sql .= " GROUP BY role";
4008
4009 if ($warehouseid) {
4010 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
4011 } else {
4012 $this->stats_mrptoconsume['customers'] = 0;
4013 $this->stats_mrptoconsume['nb'] = 0;
4014 $this->stats_mrptoconsume['rows'] = 0;
4015 $this->stats_mrptoconsume['qty'] = 0.0;
4016 $this->stats_mrptoproduce['customers'] = 0;
4017 $this->stats_mrptoproduce['nb'] = 0;
4018 $this->stats_mrptoproduce['rows'] = 0;
4019 $this->stats_mrptoproduce['qty'] = 0.0;
4020 }
4021
4022 $result = $this->db->query($sql);
4023 if ($result) {
4024 while ($obj = $this->db->fetch_object($result)) {
4025 if ($obj->role == 'toconsume' && empty($warehouseid)) {
4026 $this->stats_mrptoconsume['customers'] += (int) $obj->nb_customers;
4027 $this->stats_mrptoconsume['nb'] += (int) $obj->nb;
4028 $this->stats_mrptoconsume['rows'] += (int) $obj->nb_rows;
4029 $this->stats_mrptoconsume['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4030 }
4031 if ($obj->role == 'consumed' && empty($warehouseid)) {
4032 //$this->stats_mrptoconsume['customers'] += $obj->nb_customers;
4033 //$this->stats_mrptoconsume['nb'] += $obj->nb;
4034 //$this->stats_mrptoconsume['rows'] += $obj->nb_rows;
4035 $this->stats_mrptoconsume['qty'] -= ($obj->qty ? (float) $obj->qty : 0.0);
4036 }
4037 if ($obj->role == 'toproduce') {
4038 if ($warehouseid) {
4039 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4040 } else {
4041 $this->stats_mrptoproduce['customers'] += (int) $obj->nb_customers;
4042 $this->stats_mrptoproduce['nb'] += (int) $obj->nb;
4043 $this->stats_mrptoproduce['rows'] += (int) $obj->nb_rows;
4044 $this->stats_mrptoproduce['qty'] += ($obj->qty ? (float) $obj->qty : 0.0);
4045 }
4046 }
4047 if ($obj->role == 'produced') {
4048 //$this->stats_mrptoproduce['customers'] += $obj->nb_customers;
4049 //$this->stats_mrptoproduce['nb'] += $obj->nb;
4050 //$this->stats_mrptoproduce['rows'] += $obj->nb_rows;
4051 if ($warehouseid) {
4052 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
4053 } else {
4054 $this->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
4055 }
4056 }
4057 }
4058
4059 // Clean data
4060 if ($warehouseid) {
4061 if ($this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] < 0) {
4062 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
4063 }
4064 } else {
4065 if ($this->stats_mrptoconsume['qty'] < 0) {
4066 $this->stats_mrptoconsume['qty'] = 0;
4067 }
4068 if ($this->stats_mrptoproduce['qty'] < 0) {
4069 $this->stats_mrptoproduce['qty'] = 0;
4070 }
4071 }
4072
4073 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
4074 $reshook = $hookmanager->executeHooks('loadStatsInProduction', $parameters, $this, $action);
4075 if ($reshook > 0) {
4076 $this->stats_mrptoproduce = $hookmanager->resArray['stats_mrptoproduce'];
4077 }
4078
4079 return 1;
4080 } else {
4081 $this->error = $this->db->error();
4082 return -1;
4083 }
4084 }
4085
4086 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4093 public function load_stats_contrat($socid = 0)
4094 {
4095 // phpcs:enable
4096 global $user, $hookmanager, $action;
4097
4098 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
4099 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
4100 $sql .= " FROM ".$this->db->prefix()."contratdet as cd";
4101 $sql .= ", ".$this->db->prefix()."contrat as c";
4102 $sql .= ", ".$this->db->prefix()."societe as s";
4103 $sql .= " WHERE c.rowid = cd.fk_contrat";
4104 $sql .= " AND c.fk_soc = s.rowid";
4105 $sql .= " AND c.entity IN (".getEntity('contract').")";
4106 $sql .= " AND cd.fk_product = ".((int) $this->id);
4107 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4108 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = c.fk_soc AND sc.fk_user = ".((int) $user->id);
4109 }
4110 //$sql.= " AND c.statut != 0";
4111 if ($socid > 0) {
4112 $sql .= " AND c.fk_soc = ".((int) $socid);
4113 }
4114
4115 $result = $this->db->query($sql);
4116 if ($result) {
4117 $obj = $this->db->fetch_object($result);
4118 $this->stats_contrat['customers'] = $obj->nb_customers;
4119 $this->stats_contrat['nb'] = $obj->nb;
4120 $this->stats_contrat['rows'] = $obj->nb_rows;
4121 $this->stats_contrat['qty'] = $obj->qty ? $obj->qty : 0;
4122
4123 // if it's a virtual product, maybe it is in contract by extension
4124 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4125 $TFather = $this->getFather();
4126 if (is_array($TFather) && !empty($TFather)) {
4127 foreach ($TFather as &$fatherData) {
4128 $pFather = new Product($this->db);
4129 $pFather->id = $fatherData['id'];
4130 $qtyCoef = $fatherData['qty'];
4131
4132 if ($fatherData['incdec']) {
4133 $pFather->load_stats_contrat($socid);
4134
4135 $this->stats_contrat['customers'] += $pFather->stats_contrat['customers'];
4136 $this->stats_contrat['nb'] += $pFather->stats_contrat['nb'];
4137 $this->stats_contrat['rows'] += $pFather->stats_contrat['rows'];
4138 $this->stats_contrat['qty'] += $pFather->stats_contrat['qty'] * $qtyCoef;
4139 }
4140 }
4141 }
4142 }
4143
4144 $parameters = array('socid' => $socid);
4145 $reshook = $hookmanager->executeHooks('loadStatsContract', $parameters, $this, $action);
4146 if ($reshook > 0) {
4147 $this->stats_contrat = $hookmanager->resArray['stats_contrat'];
4148 }
4149
4150 return 1;
4151 } else {
4152 $this->error = $this->db->error().' sql='.$sql;
4153 return -1;
4154 }
4155 }
4156
4157 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4164 public function load_stats_facture($socid = 0)
4165 {
4166 // phpcs:enable
4167 global $user, $hookmanager, $action;
4168
4169 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4170 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(".$this->db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1').") as qty";
4171 $sql .= " FROM ".$this->db->prefix()."facturedet as fd";
4172 $sql .= ", ".$this->db->prefix()."facture as f";
4173 $sql .= ", ".$this->db->prefix()."societe as s";
4174 $sql .= " WHERE f.rowid = fd.fk_facture";
4175 $sql .= " AND f.fk_soc = s.rowid";
4176 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4177 $sql .= " AND fd.fk_product = ".((int) $this->id);
4178 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4179 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4180 }
4181 //$sql.= " AND f.fk_statut != 0";
4182 if ($socid > 0) {
4183 $sql .= " AND f.fk_soc = ".((int) $socid);
4184 }
4185
4186 $result = $this->db->query($sql);
4187 if ($result) {
4188 $obj = $this->db->fetch_object($result);
4189 $this->stats_facture['customers'] = $obj->nb_customers;
4190 $this->stats_facture['nb'] = $obj->nb;
4191 $this->stats_facture['rows'] = $obj->nb_rows;
4192 $this->stats_facture['qty'] = $obj->qty ? $obj->qty : 0;
4193
4194 // if it's a virtual product, maybe it is in invoice by extension
4195 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4196 $TFather = $this->getFather();
4197 if (is_array($TFather) && !empty($TFather)) {
4198 foreach ($TFather as &$fatherData) {
4199 $pFather = new Product($this->db);
4200 $pFather->id = $fatherData['id'];
4201 $qtyCoef = $fatherData['qty'];
4202
4203 if ($fatherData['incdec']) {
4204 $pFather->load_stats_facture($socid);
4205
4206 $this->stats_facture['customers'] += $pFather->stats_facture['customers'];
4207 $this->stats_facture['nb'] += $pFather->stats_facture['nb'];
4208 $this->stats_facture['rows'] += $pFather->stats_facture['rows'];
4209 $this->stats_facture['qty'] += $pFather->stats_facture['qty'] * $qtyCoef;
4210 }
4211 }
4212 }
4213 }
4214
4215 $parameters = array('socid' => $socid);
4216 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoice', $parameters, $this, $action);
4217 if ($reshook > 0) {
4218 $this->stats_facture = $hookmanager->resArray['stats_facture'];
4219 }
4220
4221 return 1;
4222 } else {
4223 $this->error = $this->db->error();
4224 return -1;
4225 }
4226 }
4227
4228
4229 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4236 public function load_stats_facturerec($socid = 0)
4237 {
4238 // phpcs:enable
4239 global $user, $hookmanager, $action;
4240
4241 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
4242 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4243 $sql .= " FROM ".MAIN_DB_PREFIX."facturedet_rec as fd";
4244 $sql .= ", ".MAIN_DB_PREFIX."facture_rec as f";
4245 $sql .= ", ".MAIN_DB_PREFIX."societe as s";
4246 $sql .= " WHERE f.rowid = fd.fk_facture";
4247 $sql .= " AND f.fk_soc = s.rowid";
4248 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4249 $sql .= " AND fd.fk_product = ".((int) $this->id);
4250 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4251 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4252 }
4253 //$sql.= " AND f.fk_statut != 0";
4254 if ($socid > 0) {
4255 $sql .= " AND f.fk_soc = ".((int) $socid);
4256 }
4257
4258 $result = $this->db->query($sql);
4259 if ($result) {
4260 $obj = $this->db->fetch_object($result);
4261 $this->stats_facturerec['customers'] = (int) $obj->nb_customers;
4262 $this->stats_facturerec['nb'] = (int) $obj->nb;
4263 $this->stats_facturerec['rows'] = (int) $obj->nb_rows;
4264 $this->stats_facturerec['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4265
4266 // if it's a virtual product, maybe it is in invoice by extension
4267 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
4268 $TFather = $this->getFather();
4269 if (is_array($TFather) && !empty($TFather)) {
4270 foreach ($TFather as &$fatherData) {
4271 $pFather = new Product($this->db);
4272 $pFather->id = $fatherData['id'];
4273 $qtyCoef = $fatherData['qty'];
4274
4275 if ($fatherData['incdec']) {
4276 $pFather->load_stats_facture($socid);
4277
4278 $this->stats_facturerec['customers'] += $pFather->stats_facturerec['customers'];
4279 $this->stats_facturerec['nb'] += $pFather->stats_facturerec['nb'];
4280 $this->stats_facturerec['rows'] += $pFather->stats_facturerec['rows'];
4281 $this->stats_facturerec['qty'] += $pFather->stats_facturerec['qty'] * $qtyCoef;
4282 }
4283 }
4284 }
4285 }
4286
4287 $parameters = array('socid' => $socid);
4288 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoiceRec', $parameters, $this, $action);
4289 if ($reshook > 0) {
4290 $this->stats_facturerec = $hookmanager->resArray['stats_facturerec'];
4291 }
4292
4293 return 1;
4294 } else {
4295 $this->error = $this->db->error();
4296 return -1;
4297 }
4298 }
4299
4300 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4307 public function load_stats_facture_fournisseur($socid = 0)
4308 {
4309 // phpcs:enable
4310 global $user, $hookmanager, $action;
4311
4312 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
4313 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4314 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as fd";
4315 $sql .= ", ".$this->db->prefix()."facture_fourn as f";
4316 $sql .= ", ".$this->db->prefix()."societe as s";
4317 $sql .= " WHERE f.rowid = fd.fk_facture_fourn";
4318 $sql .= " AND f.fk_soc = s.rowid";
4319 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4320 $sql .= " AND fd.fk_product = ".((int) $this->id);
4321 if (empty($user->fk_soc) && !$user->hasRight('societe', 'client', 'voir')) { // For external user, restriction is done on filter on fk_soc directly
4322 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc = f.fk_soc AND sc.fk_user = ".((int) $user->id);
4323 }
4324 //$sql.= " AND f.fk_statut != 0";
4325 if ($socid > 0) {
4326 $sql .= " AND f.fk_soc = ".((int) $socid);
4327 }
4328
4329 $result = $this->db->query($sql);
4330 if ($result) {
4331 $obj = $this->db->fetch_object($result);
4332 $this->stats_facture_fournisseur['suppliers'] = (int) $obj->nb_suppliers;
4333 $this->stats_facture_fournisseur['nb'] = (int) $obj->nb;
4334 $this->stats_facture_fournisseur['rows'] = (int) $obj->nb_rows;
4335 $this->stats_facture_fournisseur['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4336
4337 $parameters = array('socid' => $socid);
4338 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoice', $parameters, $this, $action);
4339 if ($reshook > 0) {
4340 $this->stats_facture_fournisseur = $hookmanager->resArray['stats_facture_fournisseur'];
4341 }
4342
4343 return 1;
4344 } else {
4345 $this->error = $this->db->error();
4346 return -1;
4347 }
4348 }
4349
4350 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4357 public function load_stats_facturefournrec($socid = 0)
4358 {
4359 // phpcs:enable
4360 global $user, $hookmanager, $action;
4361
4362 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
4363 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
4364 $sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn_det_rec as fd";
4365 $sql .= ", ".MAIN_DB_PREFIX."facture_fourn_rec as f";
4366 $sql .= ", ".MAIN_DB_PREFIX."societe as s";
4367 if (!$user->hasRight('societe', 'client', 'voir')) {
4368 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
4369 }
4370 $sql .= " WHERE f.rowid = fd.fk_facture";
4371 $sql .= " AND f.fk_soc = s.rowid";
4372 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4373 $sql .= " AND fd.fk_product = ".((int) $this->id);
4374 if (!$user->hasRight('societe', 'client', 'voir')) {
4375 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4376 }
4377 //$sql.= " AND f.fk_statut != 0";
4378 if ($socid > 0) {
4379 $sql .= " AND f.fk_soc = ".((int) $socid);
4380 }
4381
4382 $result = $this->db->query($sql);
4383 if ($result) {
4384 $obj = $this->db->fetch_object($result);
4385 $this->stats_facturefournrec['suppliers'] = (int) $obj->nb_suppliers;
4386 $this->stats_facturefournrec['nb'] = (int) $obj->nb;
4387 $this->stats_facturefournrec['rows'] = (int) $obj->nb_rows;
4388 $this->stats_facturefournrec['qty'] = $obj->qty ? (float) $obj->qty : 0.0;
4389
4390 $parameters = array('socid' => $socid);
4391 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoiceRec', $parameters, $this, $action);
4392 if ($reshook > 0) {
4393 $this->stats_facturefournrec = $hookmanager->resArray['stats_facturefournrec'];
4394 }
4395
4396 return 1;
4397 } else {
4398 $this->error = $this->db->error();
4399 return -1;
4400 }
4401 }
4402
4403 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4412 private function _get_stats($sql, $mode, $year = 0)
4413 {
4414 // phpcs:enable
4415 $tab = array();
4416
4417 $resql = $this->db->query($sql);
4418 if ($resql) {
4419 $num = $this->db->num_rows($resql);
4420 $i = 0;
4421 while ($i < $num) {
4422 $arr = $this->db->fetch_array($resql);
4423 if (is_array($arr)) {
4424 $keyfortab = (string) $arr[1];
4425 if ($year == -1) {
4426 $keyfortab = substr($keyfortab, -2);
4427 }
4428
4429 if ($mode == 'byunit') {
4430 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[0]; // 1st field
4431 } elseif ($mode == 'bynumber') {
4432 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4433 } elseif ($mode == 'byamount') {
4434 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4435 } else {
4436 // Bad value for $mode
4437 return -1;
4438 }
4439 }
4440 $i++;
4441 }
4442 } else {
4443 $this->error = $this->db->error().' sql='.$sql;
4444 return -1;
4445 }
4446
4447 if (empty($year)) {
4448 $year = dol_print_date(time(), '%Y');
4449 $month = dol_print_date(time(), '%m');
4450 } elseif ($year == -1) {
4451 $year = '';
4452 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4453 } else {
4454 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4455 }
4456
4457 $result = array();
4458
4459 for ($j = 0; $j < 12; $j++) {
4460 // $ids is 'D', 'N', 'O', 'S', ... (First letter of month in user language)
4461 $idx = ucfirst(dol_trunc(dol_print_date(dol_mktime(12, 0, 0, $month, 1, 1970), "%b"), 1, 'right', 'UTF-8', 1));
4462
4463 //print $idx.'-'.$year.'-'.$month.'<br>';
4464 $result[$j] = array($idx, isset($tab[$year.$month]) ? $tab[$year.$month] : 0);
4465 // $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0);
4466
4467 $month = "0".($month - 1);
4468 if (dol_strlen($month) == 3) {
4469 $month = substr($month, 1);
4470 }
4471 if ($month == 0) {
4472 $month = 12;
4473 if ($year !== '') { // $year is '' when we want stats for all years
4474 $year -= 1;
4475 }
4476 }
4477 }
4478
4479 return array_reverse($result);
4480 }
4481
4482
4483 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4494 public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4495 {
4496 // phpcs:enable
4497 global $user;
4498
4499 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4500 if ($mode == 'bynumber') {
4501 $sql .= ", count(DISTINCT f.rowid)";
4502 }
4503 $sql .= ", sum(d.total_ht) as total_ht";
4504 $sql .= " FROM ".$this->db->prefix()."facturedet as d, ".$this->db->prefix()."facture as f, ".$this->db->prefix()."societe as s";
4505 if ($filteronproducttype >= 0) {
4506 $sql .= ", ".$this->db->prefix()."product as p";
4507 }
4508 if (!$user->hasRight('societe', 'client', 'voir')) {
4509 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4510 }
4511 $sql .= " WHERE f.rowid = d.fk_facture";
4512 if ($this->id > 0) {
4513 $sql .= " AND d.fk_product = ".((int) $this->id);
4514 } else {
4515 $sql .= " AND d.fk_product > 0";
4516 }
4517 if ($filteronproducttype >= 0) {
4518 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4519 }
4520 $sql .= " AND f.fk_soc = s.rowid";
4521 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4522 if (!$user->hasRight('societe', 'client', 'voir')) {
4523 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4524 }
4525 if ($socid > 0) {
4526 $sql .= " AND f.fk_soc = $socid";
4527 }
4528 $sql .= $morefilter;
4529 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4530 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4531
4532 return $this->_get_stats($sql, $mode, $year);
4533 }
4534
4535
4536 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4547 public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4548 {
4549 // phpcs:enable
4550 global $user;
4551
4552 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4553 if ($mode == 'bynumber') {
4554 $sql .= ", count(DISTINCT f.rowid)";
4555 }
4556 $sql .= ", sum(d.total_ht) as total_ht";
4557 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as d, ".$this->db->prefix()."facture_fourn as f, ".$this->db->prefix()."societe as s";
4558 if ($filteronproducttype >= 0) {
4559 $sql .= ", ".$this->db->prefix()."product as p";
4560 }
4561 if (!$user->hasRight('societe', 'client', 'voir')) {
4562 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4563 }
4564 $sql .= " WHERE f.rowid = d.fk_facture_fourn";
4565 if ($this->id > 0) {
4566 $sql .= " AND d.fk_product = ".((int) $this->id);
4567 } else {
4568 $sql .= " AND d.fk_product > 0";
4569 }
4570 if ($filteronproducttype >= 0) {
4571 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4572 }
4573 $sql .= " AND f.fk_soc = s.rowid";
4574 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4575 if (!$user->hasRight('societe', 'client', 'voir')) {
4576 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4577 }
4578 if ($socid > 0) {
4579 $sql .= " AND f.fk_soc = $socid";
4580 }
4581 $sql .= $morefilter;
4582 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4583 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4584
4585 return $this->_get_stats($sql, $mode, $year);
4586 }
4587
4588 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4599 public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4600 {
4601 // phpcs:enable
4602 global $user;
4603
4604 $sql = "SELECT sum(d.qty) as qty, date_format(p.datep, '%Y%m')";
4605 if ($mode == 'bynumber') {
4606 $sql .= ", count(DISTINCT p.rowid)";
4607 }
4608 $sql .= ", sum(d.total_ht) as total_ht";
4609 $sql .= " FROM ".$this->db->prefix()."propaldet as d, ".$this->db->prefix()."propal as p, ".$this->db->prefix()."societe as s";
4610 if ($filteronproducttype >= 0) {
4611 $sql .= ", ".$this->db->prefix()."product as prod";
4612 }
4613 if (!$user->hasRight('societe', 'client', 'voir')) {
4614 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4615 }
4616 $sql .= " WHERE p.rowid = d.fk_propal";
4617 if ($this->id > 0) {
4618 $sql .= " AND d.fk_product = ".((int) $this->id);
4619 } else {
4620 $sql .= " AND d.fk_product > 0";
4621 }
4622 if ($filteronproducttype >= 0) {
4623 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4624 }
4625 $sql .= " AND p.fk_soc = s.rowid";
4626 $sql .= " AND p.entity IN (".getEntity('propal').")";
4627 if (!$user->hasRight('societe', 'client', 'voir')) {
4628 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4629 }
4630 if ($socid > 0) {
4631 $sql .= " AND p.fk_soc = ".((int) $socid);
4632 }
4633 $sql .= $morefilter;
4634 $sql .= " GROUP BY date_format(p.datep,'%Y%m')";
4635 $sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC";
4636
4637 return $this->_get_stats($sql, $mode, $year);
4638 }
4639
4640 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4651 public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4652 {
4653 // phpcs:enable
4654 global $user;
4655
4656 $sql = "SELECT sum(d.qty) as qty, date_format(p.date_valid, '%Y%m')";
4657 if ($mode == 'bynumber') {
4658 $sql .= ", count(DISTINCT p.rowid)";
4659 }
4660 $sql .= ", sum(d.total_ht) as total_ht";
4661 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as d, ".$this->db->prefix()."supplier_proposal as p, ".$this->db->prefix()."societe as s";
4662 if ($filteronproducttype >= 0) {
4663 $sql .= ", ".$this->db->prefix()."product as prod";
4664 }
4665 if (!$user->hasRight('societe', 'client', 'voir')) {
4666 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4667 }
4668 $sql .= " WHERE p.rowid = d.fk_supplier_proposal";
4669 if ($this->id > 0) {
4670 $sql .= " AND d.fk_product = ".((int) $this->id);
4671 } else {
4672 $sql .= " AND d.fk_product > 0";
4673 }
4674 if ($filteronproducttype >= 0) {
4675 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4676 }
4677 $sql .= " AND p.fk_soc = s.rowid";
4678 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
4679 if (!$user->hasRight('societe', 'client', 'voir')) {
4680 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4681 }
4682 if ($socid > 0) {
4683 $sql .= " AND p.fk_soc = ".((int) $socid);
4684 }
4685 $sql .= $morefilter;
4686 $sql .= " GROUP BY date_format(p.date_valid,'%Y%m')";
4687 $sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
4688
4689 return $this->_get_stats($sql, $mode, $year);
4690 }
4691
4692 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4703 public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4704 {
4705 // phpcs:enable
4706 global $user;
4707
4708 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4709 if ($mode == 'bynumber') {
4710 $sql .= ", count(DISTINCT c.rowid)";
4711 }
4712 $sql .= ", sum(d.total_ht) as total_ht";
4713 $sql .= " FROM ".$this->db->prefix()."commandedet as d, ".$this->db->prefix()."commande as c, ".$this->db->prefix()."societe as s";
4714 if ($filteronproducttype >= 0) {
4715 $sql .= ", ".$this->db->prefix()."product as p";
4716 }
4717 if (!$user->hasRight('societe', 'client', 'voir')) {
4718 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4719 }
4720 $sql .= " WHERE c.rowid = d.fk_commande";
4721 if ($this->id > 0) {
4722 $sql .= " AND d.fk_product = ".((int) $this->id);
4723 } else {
4724 $sql .= " AND d.fk_product > 0";
4725 }
4726 if ($filteronproducttype >= 0) {
4727 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4728 }
4729 $sql .= " AND c.fk_soc = s.rowid";
4730 $sql .= " AND c.entity IN (".getEntity('commande').")";
4731 if (!$user->hasRight('societe', 'client', 'voir')) {
4732 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4733 }
4734 if ($socid > 0) {
4735 $sql .= " AND c.fk_soc = ".((int) $socid);
4736 }
4737 $sql .= $morefilter;
4738 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4739 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4740
4741 return $this->_get_stats($sql, $mode, $year);
4742 }
4743
4744 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4755 public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4756 {
4757 // phpcs:enable
4758 global $user;
4759
4760 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4761 if ($mode == 'bynumber') {
4762 $sql .= ", count(DISTINCT c.rowid)";
4763 }
4764 $sql .= ", sum(d.total_ht) as total_ht";
4765 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as d, ".$this->db->prefix()."commande_fournisseur as c, ".$this->db->prefix()."societe as s";
4766 if ($filteronproducttype >= 0) {
4767 $sql .= ", ".$this->db->prefix()."product as p";
4768 }
4769 if (!$user->hasRight('societe', 'client', 'voir')) {
4770 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4771 }
4772 $sql .= " WHERE c.rowid = d.fk_commande";
4773 if ($this->id > 0) {
4774 $sql .= " AND d.fk_product = ".((int) $this->id);
4775 } else {
4776 $sql .= " AND d.fk_product > 0";
4777 }
4778 if ($filteronproducttype >= 0) {
4779 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4780 }
4781 $sql .= " AND c.fk_soc = s.rowid";
4782 $sql .= " AND c.entity IN (".getEntity('supplier_order').")";
4783 if (!$user->hasRight('societe', 'client', 'voir')) {
4784 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4785 }
4786 if ($socid > 0) {
4787 $sql .= " AND c.fk_soc = ".((int) $socid);
4788 }
4789 $sql .= $morefilter;
4790 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4791 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4792
4793 return $this->_get_stats($sql, $mode, $year);
4794 }
4795
4796 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4807 public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4808 {
4809 // phpcs:enable
4810 global $user;
4811
4812 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_contrat, '%Y%m')";
4813 if ($mode == 'bynumber') {
4814 $sql .= ", count(DISTINCT c.rowid)";
4815 }
4816 $sql .= ", sum(d.total_ht) as total_ht";
4817 $sql .= " FROM ".$this->db->prefix()."contratdet as d, ".$this->db->prefix()."contrat as c, ".$this->db->prefix()."societe as s";
4818 if ($filteronproducttype >= 0) {
4819 $sql .= ", ".$this->db->prefix()."product as p";
4820 }
4821 if (!$user->hasRight('societe', 'client', 'voir')) {
4822 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4823 }
4824 $sql .= " WHERE c.entity IN (".getEntity('contract').")";
4825 $sql .= " AND c.rowid = d.fk_contrat";
4826
4827 if ($this->id > 0) {
4828 $sql .= " AND d.fk_product = ".((int) $this->id);
4829 } else {
4830 $sql .= " AND d.fk_product > 0";
4831 }
4832 if ($filteronproducttype >= 0) {
4833 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4834 }
4835 $sql .= " AND c.fk_soc = s.rowid";
4836
4837 if (!$user->hasRight('societe', 'client', 'voir')) {
4838 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4839 }
4840 if ($socid > 0) {
4841 $sql .= " AND c.fk_soc = ".((int) $socid);
4842 }
4843 $sql .= $morefilter;
4844 $sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')";
4845 $sql .= " ORDER BY date_format(c.date_contrat,'%Y%m') DESC";
4846
4847 return $this->_get_stats($sql, $mode, $year);
4848 }
4849
4850 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4861 public function get_nb_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4862 {
4863 // phpcs:enable
4864 global $user;
4865
4866 $sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')";
4867 if ($mode == 'bynumber') {
4868 $sql .= ", count(DISTINCT d.rowid)";
4869 }
4870 $sql .= " FROM ".$this->db->prefix()."mrp_mo as d LEFT JOIN ".$this->db->prefix()."societe as s ON d.fk_soc = s.rowid";
4871 if ($filteronproducttype >= 0) {
4872 $sql .= ", ".$this->db->prefix()."product as p";
4873 }
4874 if (!$user->hasRight('societe', 'client', 'voir')) {
4875 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4876 }
4877
4878 $sql .= " WHERE d.entity IN (".getEntity('mo').")";
4879 $sql .= " AND d.status > 0";
4880
4881 if ($this->id > 0) {
4882 $sql .= " AND d.fk_product = ".((int) $this->id);
4883 } else {
4884 $sql .= " AND d.fk_product > 0";
4885 }
4886 if ($filteronproducttype >= 0) {
4887 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4888 }
4889
4890 if (!$user->hasRight('societe', 'client', 'voir')) {
4891 $sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4892 }
4893 if ($socid > 0) {
4894 $sql .= " AND d.fk_soc = ".((int) $socid);
4895 }
4896 $sql .= $morefilter;
4897 $sql .= " GROUP BY date_format(d.date_valid,'%Y%m')";
4898 $sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC";
4899
4900 return $this->_get_stats($sql, $mode, $year);
4901 }
4902
4903 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4914 public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
4915 {
4916 global $user;
4917
4918 // phpcs:enable
4919 // Clean parameters
4920 if (!is_numeric($id_pere)) {
4921 $id_pere = 0;
4922 }
4923 if (!is_numeric($id_fils)) {
4924 $id_fils = 0;
4925 }
4926 if (!is_numeric($incdec)) {
4927 $incdec = 0;
4928 }
4929
4930 $result = $this->del_sousproduit($id_pere, $id_fils);
4931 if ($result < 0) {
4932 return $result;
4933 }
4934
4935 // Check not already father of id_pere (to avoid father -> child -> father links)
4936 $sql = "SELECT fk_product_pere from ".$this->db->prefix()."product_association";
4937 $sql .= " WHERE fk_product_pere = ".((int) $id_fils)." AND fk_product_fils = ".((int) $id_pere);
4938 if (!$this->db->query($sql)) {
4939 dol_print_error($this->db);
4940 return -1;
4941 } else {
4942 //Selection of the highest row
4943 $sql = "SELECT MAX(rang) as max_rank FROM ".$this->db->prefix()."product_association";
4944 $sql .= " WHERE fk_product_pere = ".((int) $id_pere);
4945 $resql = $this->db->query($sql);
4946 if ($resql) {
4947 $obj = $this->db->fetch_object($resql);
4948 $rank = $obj->max_rank + 1;
4949 //Addition of a product with the highest rank +1
4950 $sql = "INSERT INTO ".$this->db->prefix()."product_association(fk_product_pere,fk_product_fils,qty,incdec,rang)";
4951 $sql .= " VALUES (".((int) $id_pere).", ".((int) $id_fils).", ".price2num($qty, 'MS').", ".((int) $incdec).", ".((int) $rank).")";
4952 if (! $this->db->query($sql)) {
4953 dol_print_error($this->db);
4954 return -1;
4955 } else {
4956 if (!$notrigger) {
4957 // Call trigger
4958 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_ADD', $user);
4959 if ($result < 0) {
4960 $this->error = $this->db->lasterror();
4961 dol_syslog(get_class($this).'::addSubproduct error='.$this->error, LOG_ERR);
4962 return -1;
4963 }
4964 }
4965 // End call triggers
4966
4967 return 1;
4968 }
4969 } else {
4970 dol_print_error($this->db);
4971 return -1;
4972 }
4973 }
4974 }
4975
4976 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4987 public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
4988 {
4989 global $user;
4990
4991 // phpcs:enable
4992 // Clean parameters
4993 if (!is_numeric($id_pere)) {
4994 $id_pere = 0;
4995 }
4996 if (!is_numeric($id_fils)) {
4997 $id_fils = 0;
4998 }
4999 if (!is_numeric($incdec)) {
5000 $incdec = 1;
5001 }
5002 if (!is_numeric($qty)) {
5003 $qty = 1;
5004 }
5005
5006 $sql = 'UPDATE '.$this->db->prefix().'product_association SET ';
5007 $sql .= 'qty = '.price2num($qty, 'MS');
5008 $sql .= ',incdec = '.((int) $incdec);
5009 $sql .= ' WHERE fk_product_pere = '.((int) $id_pere).' AND fk_product_fils = '.((int) $id_fils);
5010
5011 if (!$this->db->query($sql)) {
5012 dol_print_error($this->db);
5013 return -1;
5014 } else {
5015 if (!$notrigger) {
5016 // Call trigger
5017 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_UPDATE', $user);
5018 if ($result < 0) {
5019 $this->error = $this->db->lasterror();
5020 dol_syslog(get_class($this).'::updateSubproduct error='.$this->error, LOG_ERR);
5021 return -1;
5022 }
5023 // End call triggers
5024 }
5025
5026 return 1;
5027 }
5028 }
5029
5030 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5039 public function del_sousproduit($fk_parent, $fk_child, $notrigger = 0)
5040 {
5041 global $user;
5042
5043 // phpcs:enable
5044 if (!is_numeric($fk_parent)) {
5045 $fk_parent = 0;
5046 }
5047 if (!is_numeric($fk_child)) {
5048 $fk_child = 0;
5049 }
5050
5051 $sql = "DELETE FROM ".$this->db->prefix()."product_association";
5052 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5053 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5054
5055 dol_syslog(get_class($this).'::del_sousproduit', LOG_DEBUG);
5056 if (!$this->db->query($sql)) {
5057 dol_print_error($this->db);
5058 return -1;
5059 }
5060
5061 // Updated ranks so that none are missing
5062 $sqlrank = "SELECT rowid, rang FROM ".$this->db->prefix()."product_association";
5063 $sqlrank .= " WHERE fk_product_pere = ".((int) $fk_parent);
5064 $sqlrank .= " ORDER BY rang";
5065 $resqlrank = $this->db->query($sqlrank);
5066 if ($resqlrank) {
5067 $cpt = 0;
5068 while ($objrank = $this->db->fetch_object($resqlrank)) {
5069 $cpt++;
5070 $sql = "UPDATE ".$this->db->prefix()."product_association";
5071 $sql .= " SET rang = ".((int) $cpt);
5072 $sql .= " WHERE rowid = ".((int) $objrank->rowid);
5073 if (! $this->db->query($sql)) {
5074 dol_print_error($this->db);
5075 return -1;
5076 }
5077 }
5078 }
5079
5080 if (!$notrigger) {
5081 // Call trigger
5082 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_DELETE', $user);
5083 if ($result < 0) {
5084 $this->error = $this->db->lasterror();
5085 dol_syslog(get_class($this).'::delSubproduct error='.$this->error, LOG_ERR);
5086 return -1;
5087 }
5088 // End call triggers
5089 }
5090
5091 return 1;
5092 }
5093
5094 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5102 public function is_sousproduit($fk_parent, $fk_child)
5103 {
5104 // phpcs:enable
5105 $sql = "SELECT fk_product_pere, qty, incdec";
5106 $sql .= " FROM ".$this->db->prefix()."product_association";
5107 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5108 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5109
5110 $result = $this->db->query($sql);
5111 if ($result) {
5112 $num = $this->db->num_rows($result);
5113
5114 if ($num > 0) {
5115 $obj = $this->db->fetch_object($result);
5116
5117 $this->is_sousproduit_qty = $obj->qty;
5118 $this->is_sousproduit_incdec = $obj->incdec;
5119
5120 return 1;
5121 } else {
5122 return 0;
5123 }
5124 } else {
5125 dol_print_error($this->db);
5126 return -1;
5127 }
5128 }
5129
5130
5131 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5142 public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
5143 {
5144 // phpcs:enable
5145 global $conf;
5146
5147 $now = dol_now();
5148
5149 dol_syslog(get_class($this)."::add_fournisseur id_fourn = ".$id_fourn." ref_fourn=".$ref_fourn." quantity=".$quantity, LOG_DEBUG);
5150
5151 // Clean parameters
5152 $quantity = price2num($quantity, 'MS');
5153
5154 if ($ref_fourn) {
5155 // Check if ref is not already used
5156 $sql = "SELECT rowid, fk_product";
5157 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5158 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5159 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5160 $sql .= " AND fk_product <> ".((int) $this->id);
5161 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5162
5163 $resql = $this->db->query($sql);
5164 if ($resql) {
5165 $obj = $this->db->fetch_object($resql);
5166 if ($obj) {
5167 // If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
5168 $this->product_id_already_linked = $obj->fk_product;
5169 return -3;
5170 }
5171 $this->db->free($resql);
5172 }
5173 }
5174
5175 $sql = "SELECT rowid";
5176 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5177 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5178 if ($ref_fourn) {
5179 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5180 } else {
5181 $sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
5182 }
5183 $sql .= " AND quantity = ".((float) $quantity);
5184 $sql .= " AND fk_product = ".((int) $this->id);
5185 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5186
5187 $resql = $this->db->query($sql);
5188 if ($resql) {
5189 $obj = $this->db->fetch_object($resql);
5190
5191 // The reference supplier does not exist, we create it for this product.
5192 if (empty($obj)) {
5193 $sql = "INSERT INTO ".$this->db->prefix()."product_fournisseur_price(";
5194 $sql .= "datec";
5195 $sql .= ", entity";
5196 $sql .= ", fk_product";
5197 $sql .= ", fk_soc";
5198 $sql .= ", ref_fourn";
5199 $sql .= ", quantity";
5200 $sql .= ", fk_user";
5201 $sql .= ", tva_tx";
5202 $sql .= ") VALUES (";
5203 $sql .= "'".$this->db->idate($now)."'";
5204 $sql .= ", ".((int) $conf->entity);
5205 $sql .= ", ".((int) $this->id);
5206 $sql .= ", ".((int) $id_fourn);
5207 $sql .= ", '".$this->db->escape($ref_fourn)."'";
5208 $sql .= ", ".((float) $quantity);
5209 $sql .= ", ".((int) $user->id);
5210 $sql .= ", 0";
5211 $sql .= ")";
5212
5213 if ($this->db->query($sql)) {
5214 $this->product_fourn_price_id = $this->db->last_insert_id($this->db->prefix()."product_fournisseur_price");
5215 return 1;
5216 } else {
5217 $this->error = $this->db->lasterror();
5218 return -1;
5219 }
5220 } else {
5221 // If the supplier price already exists for this product and quantity
5222 $this->product_fourn_price_id = $obj->rowid;
5223 return 0;
5224 }
5225 } else {
5226 $this->error = $this->db->lasterror();
5227 return -2;
5228 }
5229 }
5230
5231
5232 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5238 public function list_suppliers()
5239 {
5240 // phpcs:enable
5241 global $conf;
5242
5243 $list = array();
5244
5245 $sql = "SELECT DISTINCT p.fk_soc";
5246 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as p";
5247 $sql .= " WHERE p.fk_product = ".((int) $this->id);
5248 $sql .= " AND p.entity = ".((int) $conf->entity);
5249
5250 $result = $this->db->query($sql);
5251 if ($result) {
5252 $num = $this->db->num_rows($result);
5253 $i = 0;
5254 while ($i < $num) {
5255 $obj = $this->db->fetch_object($result);
5256 $list[$i] = $obj->fk_soc;
5257 $i++;
5258 }
5259 }
5260
5261 return $list;
5262 }
5263
5264 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5272 public function clone_price($fromId, $toId)
5273 {
5274 global $user;
5275
5276 $now = dol_now();
5277
5278 $this->db->begin();
5279
5280 // prices
5281 $sql = "INSERT INTO ".$this->db->prefix()."product_price (";
5282 $sql .= " entity";
5283 $sql .= ", fk_product";
5284 $sql .= ", date_price";
5285 $sql .= ", price_level";
5286 $sql .= ", price";
5287 $sql .= ", price_ttc";
5288 $sql .= ", price_min";
5289 $sql .= ", price_min_ttc";
5290 $sql .= ", price_base_type";
5291 $sql .= ", price_label";
5292 $sql .= ", default_vat_code";
5293 $sql .= ", tva_tx";
5294 $sql .= ", recuperableonly";
5295 $sql .= ", localtax1_tx";
5296 $sql .= ", localtax1_type";
5297 $sql .= ", localtax2_tx";
5298 $sql .= ", localtax2_type";
5299 $sql .= ", fk_user_author";
5300 $sql .= ", tosell";
5301 $sql .= ", price_by_qty";
5302 $sql .= ", fk_price_expression";
5303 $sql .= ", fk_multicurrency";
5304 $sql .= ", multicurrency_code";
5305 $sql .= ", multicurrency_tx";
5306 $sql .= ", multicurrency_price";
5307 $sql .= ", multicurrency_price_ttc";
5308 $sql .= ")";
5309 $sql .= " SELECT";
5310 $sql .= " entity";
5311 $sql .= ", ".((int) $toId);
5312 $sql .= ", '".$this->db->idate($now)."'";
5313 $sql .= ", price_level";
5314 $sql .= ", price";
5315 $sql .= ", price_ttc";
5316 $sql .= ", price_min";
5317 $sql .= ", price_min_ttc";
5318 $sql .= ", price_base_type";
5319 $sql .= ", price_label";
5320 $sql .= ", default_vat_code";
5321 $sql .= ", tva_tx";
5322 $sql .= ", recuperableonly";
5323 $sql .= ", localtax1_tx";
5324 $sql .= ", localtax1_type";
5325 $sql .= ", localtax2_tx";
5326 $sql .= ", localtax2_type";
5327 $sql .= ", ".((int) $user->id);
5328 $sql .= ", tosell";
5329 $sql .= ", price_by_qty";
5330 $sql .= ", fk_price_expression";
5331 $sql .= ", fk_multicurrency";
5332 $sql .= ", multicurrency_code";
5333 $sql .= ", multicurrency_tx";
5334 $sql .= ", multicurrency_price";
5335 $sql .= ", multicurrency_price_ttc";
5336 $sql .= " FROM ".$this->db->prefix()."product_price ps";
5337 $sql .= " WHERE fk_product = ".((int) $fromId);
5338 $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)";
5339 $sql .= " ORDER BY date_price DESC";
5340
5341 dol_syslog(__METHOD__, LOG_DEBUG);
5342 $resql = $this->db->query($sql);
5343 if (!$resql) {
5344 $this->db->rollback();
5345 return -1;
5346 }
5347
5348 $this->db->commit();
5349 return 1;
5350 }
5351
5352 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5360 public function clone_associations($fromId, $toId)
5361 {
5362 // phpcs:enable
5363 $this->db->begin();
5364
5365 $sql = 'INSERT INTO '.$this->db->prefix().'product_association (fk_product_pere, fk_product_fils, qty, incdec)';
5366 $sql .= " SELECT ".((int) $toId).", fk_product_fils, qty, incdec FROM ".$this->db->prefix()."product_association";
5367 $sql .= " WHERE fk_product_pere = ".((int) $fromId);
5368
5369 dol_syslog(get_class($this).'::clone_association', LOG_DEBUG);
5370 if (!$this->db->query($sql)) {
5371 $this->db->rollback();
5372 return -1;
5373 }
5374
5375 $this->db->commit();
5376 return 1;
5377 }
5378
5379 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5387 public function clone_fournisseurs($fromId, $toId)
5388 {
5389 // phpcs:enable
5390 $this->db->begin();
5391
5392 $now = dol_now();
5393
5394 // les fournisseurs
5395 /*$sql = "INSERT ".$this->db->prefix()."product_fournisseur ("
5396 . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
5397 . " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, ref_fourn, fk_user_author"
5398 . " FROM ".$this->db->prefix()."product_fournisseur"
5399 . " WHERE fk_product = ".((int) $fromId);
5400
5401 if ( ! $this->db->query($sql ) )
5402 {
5403 $this->db->rollback();
5404 return -1;
5405 }*/
5406
5407 // les prix de fournisseurs.
5408 $sql = "INSERT ".$this->db->prefix()."product_fournisseur_price (";
5409 $sql .= " datec, fk_product, fk_soc, price, quantity, fk_user, tva_tx)";
5410 $sql .= " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, price, quantity, fk_user, tva_tx";
5411 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5412 $sql .= " WHERE fk_product = ".((int) $fromId);
5413
5414 dol_syslog(get_class($this).'::clone_fournisseurs', LOG_DEBUG);
5415 $resql = $this->db->query($sql);
5416 if (!$resql) {
5417 $this->db->rollback();
5418 return -1;
5419 } else {
5420 $this->db->commit();
5421 return 1;
5422 }
5423 }
5424
5425 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5438 public function fetch_prod_arbo($prod, $compl_path = '', $multiply = 1, $level = 1, $id_parent = 0, $ignore_stock_load = 0)
5439 {
5440 // phpcs:enable
5441 $tmpproduct = null;
5442
5443 if ($multiply < 1) {
5444 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);
5445 $multiply = 1;
5446 }
5447
5448 //var_dump($prod);
5449 foreach ($prod as $id_product => $desc_pere) { // $id_product is 0 (first call starting with root top) or an id of a sub_product
5450 if (is_array($desc_pere)) { // If desc_pere is an array, this means it's a child
5451 $id = (!empty($desc_pere[0]) ? $desc_pere[0] : '');
5452 $nb = (!empty($desc_pere[1]) ? $desc_pere[1] : '');
5453 $type = (!empty($desc_pere[2]) ? $desc_pere[2] : '');
5454 $label = (!empty($desc_pere[3]) ? $desc_pere[3] : '');
5455 $incdec = (!empty($desc_pere[4]) ? $desc_pere[4] : 0);
5456
5457 //print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n";
5458 if (is_null($tmpproduct)) {
5459 $tmpproduct = new Product($this->db); // So we initialize tmpproduct only once for all loop.
5460 }
5461 $tmpproduct->fetch($id); // Load product to get ->ref
5462
5463 if (empty($ignore_stock_load) && ($tmpproduct->isProduct() || getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) {
5464 $tmpproduct->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
5465 }
5466
5467 $this->res[] = array(
5468 'id' => $id, // Id product
5469 'id_parent' => $id_parent,
5470 'ref' => $tmpproduct->ref, // Ref product
5471 'nb' => $nb, // Nb of units that compose parent product
5472 'nb_total' => $nb * $multiply, // Nb of units for all nb of product
5473 'stock' => $tmpproduct->stock_reel, // Stock
5474 'stock_alert' => $tmpproduct->seuil_stock_alerte, // Stock alert
5475 'label' => $label,
5476 'fullpath' => $compl_path.$label, // Label
5477 'type' => $type, // Nb of units that compose parent product
5478 'desiredstock' => $tmpproduct->desiredstock,
5479 'level' => $level,
5480 'incdec' => $incdec,
5481 'entity' => $tmpproduct->entity
5482 );
5483
5484 // Recursive call if there child has children of its own
5485 if (isset($desc_pere['childs']) && is_array($desc_pere['childs'])) {
5486 if (!is_int($desc_pere[1] * $multiply)) {
5487 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);
5488 }
5489
5490 //print 'YYY We go down for '.$desc_pere[3]." -> \n";
5491 $this->fetch_prod_arbo($desc_pere['childs'], $compl_path.$desc_pere[3]." -> ", (int) ceil($desc_pere[1] * $multiply), $level + 1, $id, $ignore_stock_load);
5492 }
5493 }
5494 }
5495 }
5496
5497 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5506 public function get_arbo_each_prod($multiply = 1, $ignore_stock_load = 0)
5507 {
5508 // phpcs:enable
5509 $this->res = array();
5510 if (isset($this->sousprods) && is_array($this->sousprods)) {
5511 foreach ($this->sousprods as $prod_name => $desc_product) {
5512 if (is_array($desc_product)) {
5513 $this->fetch_prod_arbo($desc_product, "", $multiply, 1, $this->id, $ignore_stock_load); // This set $this->res
5514 }
5515 }
5516 }
5517 //var_dump($res);
5518 return $this->res;
5519 }
5520
5528 public function hasFatherOrChild($mode = 0)
5529 {
5530 $nb = 0;
5531
5532 $sql = "SELECT COUNT(pa.rowid) as nb";
5533 $sql .= " FROM ".$this->db->prefix()."product_association as pa";
5534 if ($mode == 0) {
5535 $sql .= " WHERE pa.fk_product_fils = ".((int) $this->id)." OR pa.fk_product_pere = ".((int) $this->id);
5536 } elseif ($mode == -1) {
5537 $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)
5538 } elseif ($mode == 1) {
5539 $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)
5540 }
5541
5542 $resql = $this->db->query($sql);
5543 if ($resql) {
5544 $obj = $this->db->fetch_object($resql);
5545 if ($obj) {
5546 $nb = $obj->nb;
5547 }
5548 } else {
5549 return -1;
5550 }
5551
5552 return $nb;
5553 }
5554
5560 public function hasVariants()
5561 {
5562 $nb = 0;
5563 $sql = "SELECT count(rowid) as nb FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_parent = ".((int) $this->id);
5564 $sql .= " AND entity IN (".getEntity('product').")";
5565
5566 $resql = $this->db->query($sql);
5567 if ($resql) {
5568 $obj = $this->db->fetch_object($resql);
5569 if ($obj) {
5570 $nb = $obj->nb;
5571 }
5572 }
5573
5574 return $nb;
5575 }
5576
5577
5583 public function isVariant()
5584 {
5585 if (isModEnabled('variants')) {
5586 $sql = "SELECT rowid FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_child = ".((int) $this->id)." AND entity IN (".getEntity('product').")";
5587
5588 $query = $this->db->query($sql);
5589
5590 if ($query) {
5591 if (!$this->db->num_rows($query)) {
5592 return false;
5593 }
5594 return true;
5595 } else {
5596 dol_print_error($this->db);
5597 return -1;
5598 }
5599 } else {
5600 return false;
5601 }
5602 }
5603
5610 public function getFather()
5611 {
5612 $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";
5613 $sql .= ", p.tosell as status, p.tobuy as status_buy";
5614 $sql .= " FROM ".$this->db->prefix()."product_association as pa,";
5615 $sql .= " ".$this->db->prefix()."product as p";
5616 $sql .= " WHERE p.rowid = pa.fk_product_pere";
5617 $sql .= " AND pa.fk_product_fils = ".((int) $this->id);
5618
5619 $res = $this->db->query($sql);
5620 if ($res) {
5621 $prods = array();
5622 while ($record = $this->db->fetch_array($res)) {
5623 // $record['id'] = $record['rowid'] = id of father
5624 $prods[$record['id']] = array();
5625 $prods[$record['id']]['id'] = $record['rowid'];
5626 $prods[$record['id']]['ref'] = $record['ref'];
5627 $prods[$record['id']]['label'] = $record['label'];
5628 $prods[$record['id']]['qty'] = $record['qty'];
5629 $prods[$record['id']]['incdec'] = $record['incdec'];
5630 $prods[$record['id']]['fk_product_type'] = $record['fk_product_type'];
5631 $prods[$record['id']]['entity'] = $record['entity'];
5632 $prods[$record['id']]['status'] = $record['status'];
5633 $prods[$record['id']]['status_buy'] = $record['status_buy'];
5634 }
5635 return $prods;
5636 } else {
5637 dol_print_error($this->db);
5638 return -1;
5639 }
5640 }
5641
5642
5652 public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array())
5653 {
5654 if (empty($id)) {
5655 return array();
5656 }
5657
5658 $sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,";
5659 $sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec,";
5660 $sql .= " pa.rowid as fk_association, pa.rang";
5661 $sql .= " FROM ".$this->db->prefix()."product as p,";
5662 $sql .= " ".$this->db->prefix()."product_association as pa";
5663 $sql .= " WHERE p.rowid = pa.fk_product_fils";
5664 $sql .= " AND pa.fk_product_pere = ".((int) $id);
5665 $sql .= " AND pa.fk_product_fils <> ".((int) $id); // This should not happens, it is to avoid infinite loop if it happens
5666 $sql .= " ORDER BY pa.rang";
5667
5668 dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level. ' parents='.(is_array($parents) ? implode(',', $parents) : $parents), LOG_DEBUG);
5669
5670 // Protection against infinite loop
5671 if ($level > 30) {
5672 return array();
5673 }
5674
5675 $res = $this->db->query($sql);
5676 if ($res) {
5677 $prods = array();
5678 if ($this->db->num_rows($res) > 0) {
5679 $parents[] = $id;
5680 }
5681
5682 while ($rec = $this->db->fetch_array($res)) {
5683 if (in_array($rec['id'], $parents)) {
5684 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);
5685 continue; // We discard this child if it is already found at a higher level in tree in the same branch.
5686 }
5687
5688 $prods[$rec['rowid']] = array(
5689 0 => $rec['rowid'],
5690 1 => $rec['qty'],
5691 2 => $rec['fk_product_type'],
5692 3 => $this->db->escape($rec['label']),
5693 4 => $rec['incdec'],
5694 5 => $rec['ref'],
5695 6 => $rec['fk_association'],
5696 7 => $rec['rang']
5697 );
5698 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
5699 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
5700 if (empty($firstlevelonly)) {
5701 $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, $parents);
5702 foreach ($listofchilds as $keyChild => $valueChild) {
5703 $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
5704 }
5705 }
5706 }
5707
5708 return $prods;
5709 } else {
5710 dol_print_error($this->db);
5711 return -1;
5712 }
5713 }
5714
5715 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5722 public function get_sousproduits_arbo()
5723 {
5724 // phpcs:enable
5725 $parent = array();
5726
5727 foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) { // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
5728 $parent[$this->label][$keyChild] = $valueChild;
5729 }
5730 foreach ($parent as $key => $value) { // key=label, value is array of children
5731 $this->sousprods[$key] = $value; // @phan-suppress-current-line PhanTypeMismatchProperty
5732 }
5733 }
5734
5742 public function getTooltipContentArray($params)
5743 {
5744 global $conf, $langs, $user;
5745
5746 $langs->loadLangs(array('products', 'other'));
5747
5748 $datas = array();
5749 $nofetch = !empty($params['nofetch']);
5750
5751 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5752 return ['optimize' => $langs->trans("ShowProduct")];
5753 }
5754
5755 // Does user has permission to read product/service
5756 $permissiontoreadproduct = 0;
5757 if ($this->type == self::TYPE_PRODUCT && $user->hasRight('product', 'read')) {
5758 $permissiontoreadproduct = 1;
5759 }
5760 if ($this->type == self::TYPE_SERVICE && $user->hasRight('service', 'read')) {
5761 $permissiontoreadproduct = 1;
5762 }
5763
5764 if (!empty($this->entity) && $permissiontoreadproduct) {
5765 $tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80, 0, 0, 0, 0, '1');
5766 if ($this->nbphoto > 0) {
5767 $datas['photo'] = '<div class="photointooltip floatright">'."\n" . $tmpphoto . '</div>';
5768 }
5769 }
5770
5771 if ($this->isProduct()) {
5772 $datas['picto'] = img_picto('', 'product').' <u class="paddingrightonly">'.$langs->trans("Product").'</u>';
5773 } elseif ($this->isService()) {
5774 $datas['picto'] = img_picto('', 'service').' <u class="paddingrightonly">'.$langs->trans("Service").'</u>';
5775 }
5776 if (isset($this->status) && isset($this->status_buy)) {
5777 $datas['status'] = ' '.$this->getLibStatut(5, 0) . ' '.$this->getLibStatut(5, 1);
5778 }
5779
5780 if (!empty($this->ref)) {
5781 $datas['ref'] = '<br><b>'.$langs->trans('ProductRef').':</b> '.$this->ref;
5782 }
5783 if (!empty($this->label)) {
5784 $datas['label'] = '<br><b>'.$langs->trans('ProductLabel').':</b> '.$this->label;
5785 }
5786
5787 if ($permissiontoreadproduct) {
5788 if (!empty($this->description)) {
5789 $datas['description'] = '<br><b>'.$langs->trans('ProductDescription').':</b> '.dolGetFirstLineOfText($this->description, 5);
5790 }
5791 if ($this->isStockManaged()) {
5792 if (isModEnabled('productbatch')) {
5793 $langs->load("productbatch");
5794 $datas['batchstatus'] = "<br><b>".$langs->trans("ManageLotSerial").'</b>: '.$this->getLibStatut(0, 2);
5795 if ($this->status_batch) {
5796 $datas['batchdlc'] = "<br><b>".$langs->trans("BatchSellOrEatByMandatoryList", $langs->transnoentitiesnoconv("SellByDate"), $langs->transnoentitiesnoconv("EatByDate")).'</b>: '.$this->getSellOrEatByMandatoryLabel();
5797 }
5798 }
5799 }
5800 if (isModEnabled('barcode')) {
5801 $datas['barcode'] = '<br><b>'.$langs->trans('BarCode').':</b> '.$this->barcode;
5802 }
5803
5804 if ($this->isProduct()) {
5805 if ($this->weight) {
5806 $datas['weight'] = "<br><b>".$langs->trans("Weight").'</b>: '.$this->weight.' '.measuringUnitString(0, "weight", $this->weight_units);
5807 }
5808 $labelsize = "";
5809 if ($this->length) {
5810 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Length").'</b>: '.$this->length.' '.measuringUnitString(0, 'size', $this->length_units);
5811 }
5812 if ($this->width) {
5813 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Width").'</b>: '.$this->width.' '.measuringUnitString(0, 'size', $this->width_units);
5814 }
5815 if ($this->height) {
5816 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Height").'</b>: '.$this->height.' '.measuringUnitString(0, 'size', $this->height_units);
5817 }
5818 if ($labelsize) {
5819 $datas['size'] = "<br>".$labelsize;
5820 }
5821
5822 $labelsurfacevolume = "";
5823 if ($this->surface) {
5824 $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Surface").'</b>: '.$this->surface.' '.measuringUnitString(0, 'surface', $this->surface_units);
5825 }
5826 if ($this->volume) {
5827 $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Volume").'</b>: '.$this->volume.' '.measuringUnitString(0, 'volume', $this->volume_units);
5828 }
5829 if ($labelsurfacevolume) {
5830 $datas['surface'] = "<br>" . $labelsurfacevolume;
5831 }
5832 }
5833 if ($this->isService() && !empty($this->duration_value)) {
5834 // Duration
5835 $datas['duration'] = '<br><b>'.$langs->trans("Duration").':</b> '.$this->duration_value;
5836 if ($this->duration_value > 1) {
5837 $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"));
5838 } elseif ($this->duration_value > 0) {
5839 $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"));
5840 }
5841 $datas['duration'] .= (!empty($this->duration_unit) && isset($dur[$this->duration_unit]) ? "&nbsp;".$langs->trans($dur[$this->duration_unit]) : '');
5842 }
5843 if (empty($user->socid)) {
5844 if ($this->isStockManaged() && !empty($this->pmp) && $this->pmp) {
5845 $datas['pmp'] = "<br><b>".$langs->trans("PMPValue").'</b>: '.price($this->pmp, 0, '', 1, -1, -1, $conf->currency);
5846 }
5847
5848 if (isModEnabled('accounting')) {
5849 if ($this->status && isset($this->accountancy_code_sell)) {
5850 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5851 $selllabel = '<br>';
5852 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellCode').':</b> '.length_accountg($this->accountancy_code_sell);
5853 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellIntraCode').':</b> '.length_accountg($this->accountancy_code_sell_intra);
5854 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellExportCode').':</b> '.length_accountg($this->accountancy_code_sell_export);
5855 $datas['accountancysell'] = $selllabel;
5856 }
5857 if ($this->status_buy && isset($this->accountancy_code_buy)) {
5858 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5859 $buylabel = '';
5860 if (empty($this->status)) {
5861 $buylabel .= '<br>';
5862 }
5863 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyCode').':</b> '.length_accountg($this->accountancy_code_buy);
5864 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyIntraCode').':</b> '.length_accountg($this->accountancy_code_buy_intra);
5865 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyExportCode').':</b> '.length_accountg($this->accountancy_code_buy_export);
5866 $datas['accountancybuy'] = $buylabel;
5867 }
5868 }
5869 }
5870 // show categories for this record only in ajax to not overload lists
5871 if (isModEnabled('category') && !$nofetch) {
5872 require_once DOL_DOCUMENT_ROOT . '/categories/class/categorie.class.php';
5873 $form = new Form($this->db);
5874 $datas['categories'] = '<br>' . $form->showCategories($this->id, Categorie::TYPE_PRODUCT, 1);
5875 }
5876 }
5877
5878 return $datas;
5879 }
5880
5894 public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0, $morecss = '', $add_label = 0, $sep = ' - ')
5895 {
5896 global $langs, $hookmanager;
5897
5898 include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
5899
5900 $result = '';
5901
5902 $newref = $this->ref;
5903 if ($maxlength) {
5904 $newref = dol_trunc($newref, $maxlength, 'middle');
5905 }
5906 $params = [
5907 'id' => $this->id,
5908 'objecttype' => ($this->type == 1 ? 'service' : 'product'),
5909 'option' => $option,
5910 'nofetch' => 1,
5911 ];
5912 $classfortooltip = 'classfortooltip';
5913 $dataparams = '';
5914 if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
5915 $classfortooltip = 'classforajaxtooltip';
5916 $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
5917 $label = '';
5918 } else {
5919 $label = implode($this->getTooltipContentArray($params));
5920 }
5921
5922 $linkclose = '';
5923 if (empty($notooltip)) {
5924 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5925 $label = $langs->trans("ShowProduct");
5926 $linkclose .= ' alt="'.dol_escape_htmltag($label, 1, 1).'"';
5927 }
5928 $linkclose .= ($label ? ' title="'.dol_escape_htmltag($label, 1, 1).'"' : ' title="tocomplete"');
5929 $linkclose .= $dataparams.' class="nowraponall '.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
5930 } else {
5931 $linkclose = ' class="nowraponall'.($morecss ? ' '.$morecss : '').'"';
5932 }
5933
5934 if ($option == 'supplier' || $option == 'category') {
5935 $url = DOL_URL_ROOT.'/product/price_suppliers.php?id='.$this->id;
5936 } elseif ($option == 'stock') {
5937 $url = DOL_URL_ROOT.'/product/stock/product.php?id='.$this->id;
5938 } elseif ($option == 'composition') {
5939 $url = DOL_URL_ROOT.'/product/composition/card.php?id='.$this->id;
5940 } else {
5941 $url = DOL_URL_ROOT.'/product/card.php?id='.$this->id;
5942 }
5943
5944 if ($option !== 'nolink') {
5945 // Add param to save lastsearch_values or not
5946 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
5947 if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
5948 $add_save_lastsearch_values = 1;
5949 }
5950 if ($add_save_lastsearch_values) {
5951 $url .= '&save_lastsearch_values=1';
5952 }
5953 }
5954
5955 $linkstart = '<a href="'.$url.'"';
5956 $linkstart .= $linkclose.'>';
5957 $linkend = '</a>';
5958
5959 $result .= $linkstart;
5960 if ($withpicto) {
5961 if ($this->isProduct()) {
5962 $result .= (img_object(($notooltip ? '' : $label), 'product', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5963 }
5964 if ($this->isService()) {
5965 $result .= (img_object(($notooltip ? '' : $label), 'service', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5966 }
5967 }
5968 $result .= '<span class="aaa">'.dol_escape_htmltag($newref).'</span>';
5969 $result .= $linkend;
5970 if ($withpicto != 2) {
5971 $result .= (($add_label && $this->label) ? $sep.dol_trunc($this->label, ($add_label > 1 ? $add_label : 0)) : '');
5972 }
5973
5974 global $action;
5975 $hookmanager->initHooks(array('productdao'));
5976 $parameters = array('id' => $this->id, 'getnomurl' => &$result, 'label' => &$label);
5977 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
5978 if ($reshook > 0) {
5979 $result = $hookmanager->resPrint;
5980 } else {
5981 $result .= $hookmanager->resPrint;
5982 }
5983
5984 return $result;
5985 }
5986
5987
5998 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
5999 {
6000 global $langs;
6001
6002 $langs->load("products");
6003 $outputlangs->load("products");
6004
6005 // Positionne le modele sur le nom du modele a utiliser
6006 if (!dol_strlen($modele)) {
6007 $modele = getDolGlobalString('PRODUCT_ADDON_PDF', 'strato');
6008 }
6009
6010 $modelpath = "core/modules/product/doc/";
6011
6012 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
6013 }
6014
6022 public function getLibStatut($mode = 0, $type = 0)
6023 {
6024 switch ($type) {
6025 case 0:
6026 return $this->LibStatut($this->status, $mode, $type);
6027 case 1:
6028 return $this->LibStatut($this->status_buy, $mode, $type);
6029 case 2:
6030 return $this->LibStatut($this->status_batch, $mode, $type);
6031 default:
6032 //Simulate previous behavior but should return an error string
6033 return $this->LibStatut($this->status_buy, $mode, $type);
6034 }
6035 }
6036
6037 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6046 public function LibStatut($status, $mode = 0, $type = 0)
6047 {
6048 // phpcs:enable
6049 global $langs;
6050
6051 $labelStatus = $labelStatusShort = '';
6052
6053 $langs->load('products');
6054 if (isModEnabled('productbatch')) {
6055 $langs->load("productbatch");
6056 }
6057
6058 if ($type == 2) {
6059 switch ($mode) {
6060 case 0:
6061 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatch') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial')));
6062 return dolGetStatus($label);
6063 case 1:
6064 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort')));
6065 return dolGetStatus($label);
6066 case 2:
6067 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 1, 2);
6068 case 3:
6069 return dolGetStatus($langs->transnoentitiesnoconv('ProductStatusNotOnBatch'), '', '', empty($status) ? 'status5' : 'status4', 3, 'dot');
6070 case 4:
6071 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 0, 2);
6072 case 5:
6073 return $this->LibStatut($status, 1, 2).' '.$this->LibStatut($status, 3, 2);
6074 default:
6075 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'));
6076 }
6077 }
6078
6079 $statuttrans = empty($status) ? 'status5' : 'status4';
6080
6081 if ($status == 0) {
6082 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6083 if ($type == 0) {
6084 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnSellShort');
6085 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnSell');
6086 } elseif ($type == 1) {
6087 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBuyShort');
6088 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBuy');
6089 } elseif ($type == 2) {
6090 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBatch');
6091 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort');
6092 }
6093 } elseif ($status == 1) {
6094 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6095 if ($type == 0) {
6096 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSellShort');
6097 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSell');
6098 } elseif ($type == 1) {
6099 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBuyShort');
6100 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBuy');
6101 } elseif ($type == 2) {
6102 $labelStatus = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial'));
6103 $labelStatusShort = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort'));
6104 }
6105 } elseif ($type == 2 && $status == 2) {
6106 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSerial');
6107 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSerialShort');
6108 }
6109
6110 if ($mode > 6) {
6111 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'), '', '', 'status0', 0);
6112 } else {
6113 return dolGetStatus($labelStatus, $labelStatusShort, '', $statuttrans, $mode);
6114 }
6115 }
6116
6117
6123 public function getLibFinished()
6124 {
6125 global $langs;
6126
6127 $langs->load('products');
6128 $label = '';
6129
6130 if (isset($this->finished) && $this->finished >= 0) {
6131 $sql = "SELECT label, code FROM ".$this->db->prefix()."c_product_nature where code = ".((int) $this->finished)." AND active=1";
6132 $resql = $this->db->query($sql);
6133 if (!$resql) {
6134 $this->error = $this->db->error().' sql='.$sql;
6135 dol_syslog(__METHOD__.' Error '.$this->error, LOG_ERR);
6136 return -1;
6137 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
6138 $label = $langs->trans($res['label']);
6139 }
6140 $this->db->free($resql);
6141 }
6142
6143 return $label;
6144 }
6145
6146
6147 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6164 public function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null)
6165 {
6166 // phpcs:enable
6167 if ($id_entrepot) {
6168 $this->db->begin();
6169
6170 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
6171
6172 // Ensure $nbpiece is a number and positive
6173 $nbpiece = (float) $nbpiece;
6174 if ($nbpiece < 0) {
6175 if (!$movement) {
6176 $movement = 1;
6177 }
6178 $nbpiece = abs($nbpiece);
6179 }
6180 $op = array();
6181 $op[0] = $nbpiece;
6182 $op[1] = -$nbpiece;
6183
6184 $movementstock = new MouvementStock($this->db);
6185 $movementstock->setOrigin($origin_element, (int) $origin_id); // Set ->origin_type and ->origin_id
6186 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', '', '', '', false, 0, $disablestockchangeforsubproduct);
6187
6188 if ($result >= 0) {
6189 if ($extrafields) {
6190 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
6191 $movementstock->array_options = $array_options;
6192 $movementstock->insertExtraFields();
6193 }
6194 $this->db->commit();
6195 return 1;
6196 } else {
6197 $this->error = $movementstock->error;
6198 $this->errors = $movementstock->errors;
6199
6200 $this->db->rollback();
6201 return -1;
6202 }
6203 }
6204
6205 return -1;
6206 }
6207
6208 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6229 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)
6230 {
6231 // phpcs:enable
6232 if ($id_entrepot) {
6233 $this->db->begin();
6234
6235 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
6236
6237 // Ensure $nbpiece is a number and positive
6238 $nbpiece = (float) $nbpiece;
6239 if ($nbpiece < 0) {
6240 if (!$movement) {
6241 $movement = 1;
6242 }
6243 $nbpiece = abs($nbpiece);
6244 }
6245
6246 $op = array();
6247 $op[0] = $nbpiece;
6248 $op[1] = -$nbpiece;
6249
6250 $movementstock = new MouvementStock($this->db);
6251 $movementstock->setOrigin($origin_element, (int) $origin_id); // Set ->origin_type and ->fk_origin
6252 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', $dlc, $dluo, $lot, false, 0, $disablestockchangeforsubproduct, 0, $force_update_batch);
6253
6254 if ($result >= 0) {
6255 if ($extrafields) {
6256 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
6257 $movementstock->array_options = $array_options;
6258 $movementstock->insertExtraFields();
6259 }
6260 $this->db->commit();
6261 return 1;
6262 } else {
6263 $this->error = $movementstock->error;
6264 $this->errors = $movementstock->errors;
6265
6266 $this->db->rollback();
6267 return -1;
6268 }
6269 }
6270 return -1;
6271 }
6272
6273 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6286 public function load_stock($option = '', $includedraftpoforvirtual = null, $dateofvirtualstock = null)
6287 {
6288 // phpcs:enable
6289 $this->stock_reel = 0;
6290 $this->stock_warehouse = array();
6291 $this->stock_theorique = 0;
6292
6293 // Set filter on warehouse status
6294 $warehouseStatus = array();
6295 if (preg_match('/warehouseclosed/', $option)) {
6297 }
6298 if (preg_match('/warehouseopen/', $option)) {
6300 }
6301 if (preg_match('/warehouseinternal/', $option)) {
6302 if (getDolGlobalString('ENTREPOT_EXTRA_STATUS')) {
6304 } else {
6306 }
6307 }
6308
6309 $sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot";
6310 $sql .= " FROM ".$this->db->prefix()."product_stock as ps";
6311 $sql .= ", ".$this->db->prefix()."entrepot as w";
6312 $sql .= " WHERE w.entity IN (".getEntity('stock').")";
6313 $sql .= " AND w.rowid = ps.fk_entrepot";
6314 $sql .= " AND ps.fk_product = ".((int) $this->id);
6315 if (count($warehouseStatus)) {
6316 $sql .= " AND w.statut IN (".$this->db->sanitize(implode(',', $warehouseStatus)).")";
6317 }
6318
6319 $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;
6320
6321 dol_syslog(get_class($this)."::load_stock", LOG_DEBUG);
6322 $result = $this->db->query($sql);
6323 if ($result) {
6324 $num = $this->db->num_rows($result);
6325 $i = 0;
6326 if ($num > 0) {
6327 while ($i < $num) {
6328 $row = $this->db->fetch_object($result);
6329 $this->stock_warehouse[$row->fk_entrepot] = new stdClass();
6330 $this->stock_warehouse[$row->fk_entrepot]->real = $row->reel;
6331 $this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid;
6332 if ((!preg_match('/nobatch/', $option)) && $this->hasbatch()) {
6333 $this->stock_warehouse[$row->fk_entrepot]->detail_batch = Productbatch::findAll($this->db, $row->rowid, 1, $this->id);
6334 }
6335 $this->stock_reel += $row->reel;
6336 $i++;
6337 }
6338 $this->stock_reel = (float) price2num($this->stock_reel, 'MS');
6339 }
6340 $this->db->free($result);
6341
6342 if (!preg_match('/novirtual/', $option)) {
6343 $this->load_virtual_stock($includedraftpoforvirtual, $dateofvirtualstock); // This load stock_theorique and also load all arrays stats_xxx...
6344 }
6345
6346 return 1;
6347 } else {
6348 $this->error = $this->db->lasterror();
6349 return -1;
6350 }
6351 }
6352
6353
6354 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6364 public function load_virtual_stock($includedraftpoforvirtual = null, $dateofvirtualstock = null)
6365 {
6366 // phpcs:enable
6367 global $hookmanager, $action;
6368
6369 $stock_commande_client = 0;
6370 $stock_commande_fournisseur = 0;
6371 $stock_sending_client = 0;
6372 $stock_reception_fournisseur = 0;
6373 $stock_inproduction = 0;
6374
6375 //dol_syslog("load_virtual_stock");
6376
6377 if (isModEnabled('order')) {
6378 $result = $this->load_stats_commande(0, '1,2', 1);
6379 if ($result < 0) {
6380 dol_print_error($this->db, $this->error);
6381 }
6382 $stock_commande_client = $this->stats_commande['qty'];
6383 }
6384 if (isModEnabled("shipping")) {
6385 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
6386 $filterShipmentStatus = '';
6387 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
6388 $filterShipmentStatus = Expedition::STATUS_VALIDATED.','.Expedition::STATUS_CLOSED;
6389 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6390 $filterShipmentStatus = Expedition::STATUS_CLOSED;
6391 }
6392 $result = $this->load_stats_sending(0, '1,2', 1, $filterShipmentStatus);
6393 if ($result < 0) {
6394 dol_print_error($this->db, $this->error);
6395 }
6396 $stock_sending_client = $this->stats_expedition['qty'];
6397 }
6398 // Include supplier order lines
6399 if (isModEnabled("supplier_order")) {
6400 $filterStatus = getDolGlobalString('SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK', '3,4');
6401 if (isset($includedraftpoforvirtual)) {
6402 $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
6403 }
6404 $result = $this->load_stats_commande_fournisseur(0, $filterStatus, 1, $dateofvirtualstock);
6405 if ($result < 0) {
6406 dol_print_error($this->db, $this->error);
6407 }
6408 $stock_commande_fournisseur = $this->stats_commande_fournisseur['qty'];
6409 }
6410 // Include reception lines
6411 if (isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) {
6412 $filterStatus = '4';
6413 if (isset($includedraftpoforvirtual)) {
6414 $filterStatus = '0,'.$filterStatus;
6415 }
6416 $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock);
6417 if ($result < 0) {
6418 dol_print_error($this->db, $this->error);
6419 }
6420 $stock_reception_fournisseur = $this->stats_reception['qty'];
6421 }
6422 // Include manufacturing
6423 if (isModEnabled('mrp')) {
6424 $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock);
6425 if ($result < 0) {
6426 dol_print_error($this->db, $this->error);
6427 }
6428 $stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty'];
6429 }
6430
6431 $this->stock_theorique = $this->stock_reel + $stock_inproduction;
6432
6433 // $weBillOrderOrShipmentReception is set to 'order' or 'shipmentreception'. it will be used to know how to make virtual stock
6434 // calculation when we have a stock increase or decrease on billing. Do we have to count orders to bill or shipment/reception to bill ?
6435 $weBillOrderOrShipmentReception = getDolGlobalString('STOCK_DO_WE_BILL_ORDER_OR_SHIPMENTECEPTION_FOR_VIRTUALSTOCK', 'order');
6436
6437 // Stock decrease mode
6438 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') || getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6439 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6440 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_VALIDATE_ORDER')) {
6441 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
6442 $tmpnewprod = dol_clone($this, 1);
6443 $result = $tmpnewprod->load_stats_commande(0, '0', 1); // Get qty in draft orders
6444 $this->stock_theorique += $tmpnewprod->stats_commande['qty'];
6445 }
6446 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'order') {
6447 $this->stock_theorique -= $stock_commande_client;
6448 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6449 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6450 }
6451
6452 // Stock Increase mode
6453 if (getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION') || getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION_CLOSE')) {
6454 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6455 } 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
6456 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6457 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER')) { // Warning: stock change "on approval", not on validation !
6458 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
6459 $tmpnewprod = dol_clone($this, 1);
6460 $result = $tmpnewprod->load_stats_commande_fournisseur(0, '0', 1); // Get qty in draft orders
6461 $this->stock_theorique += $this->stats_commande_fournisseur['qty'];
6462 }
6463 $this->stock_theorique -= $stock_reception_fournisseur;
6464 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'order') {
6465 $this->stock_theorique += $stock_commande_fournisseur;
6466 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6467 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6468 }
6469
6470 $parameters = array('id' => $this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual);
6471 // Note that $action and $object may have been modified by some hooks
6472 $reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action);
6473 if ($reshook > 0) {
6474 $this->stock_theorique = $hookmanager->resArray['stock_theorique'];
6475 } elseif ($reshook == 0 && isset($hookmanager->resArray['stock_stats_hook'])) {
6476 $this->stock_theorique += $hookmanager->resArray['stock_stats_hook'];
6477 }
6478
6479 //Virtual Stock by Warehouse
6480 if (!empty($this->stock_warehouse) && getDolGlobalString('STOCK_ALLOW_VIRTUAL_STOCK_PER_WAREHOUSE')) {
6481 foreach ($this->stock_warehouse as $warehouseid => $stockwarehouse) {
6482 if (isModEnabled('mrp')) {
6483 $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock, $warehouseid);
6484 if ($result < 0) {
6485 dol_print_error($this->db, $this->error);
6486 }
6487 }
6488
6489 if ($this->fk_default_warehouse == $warehouseid) {
6490 $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']);
6491 } else {
6492 $this->stock_warehouse[$warehouseid]->virtual = $this->stock_warehouse[$warehouseid]->real + $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'];
6493 }
6494 }
6495 }
6496
6497 return 1;
6498 }
6499
6507 public function loadStockForVirtualProduct($option = '', $qtyWish = 1)
6508 {
6509 $this->stock_warehouse = array();
6510 $error = 0;
6511
6512 $this->get_sousproduits_arbo();
6513 $prods_arbo = $this->get_arbo_each_prod($qtyWish, 1);
6514 if (count($prods_arbo) > 0) {
6515 $productCachedList = array();
6516 $stockByComponentList = array();
6517
6518 foreach ($prods_arbo as $componentArr) {
6519 $componentId = $componentArr['id'];
6520 // only component whose manage stock
6521 if ($componentArr['incdec'] == 1) {
6522 if (!isset($productCachedList[$componentId])) {
6523 $componentStatic = new self($this->db);
6524 $componentStatic->fetch($componentId);
6525 // check if it's a sub-kit
6526 $childrenNb = $componentStatic->hasFatherOrChild(1);
6527 if ($childrenNb == 0) {
6528 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6529 if (!isset($stockByComponentList[$componentId])) {
6530 $stockByComponentList[$componentId] = array(
6531 'qty_need' => 0
6532 );
6533 }
6534 $stockByComponentList[$componentId]['qty_need'] += $componentArr['nb_total'];
6535 }
6536 $productCachedList[$componentId] = $componentStatic;
6537 }
6538 }
6539 }
6540
6541 if (!empty($stockByComponentList)) {
6542 foreach ($stockByComponentList as $componentId => $stockByComponentArr) {
6543 if (!isset($productCachedList[$componentId])) {
6544 $componentStatic = new self($this->db);
6545 $componentStatic->fetch($componentId);
6546 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6547 $productCachedList[$componentId] = $componentStatic;
6548 }
6549 $component = $productCachedList[$componentId];
6550
6551
6552 if ($component->stock_reel < $stockByComponentArr['qty_need']) {
6553 // not enough stock for this component to assemble this virtual product
6554 $error++;
6555 $this->error = 'Not enough component [id='.$componentId.'] in stock, real='.$component->stock_reel.' and need='.$stockByComponentArr['qty_need'];
6556 $this->errors[] = $this->error;
6557 dol_syslog(__METHOD__.' : '.$this->error, LOG_ERR);
6558 } else {
6559 if (!empty($component->stock_warehouse)) {
6560 foreach ($component->stock_warehouse as $warehouseId => $warehouseObj) {
6561 $kitWarehouseAvailable = new stdClass();
6562 $kitWarehouseAvailable->id = $warehouseObj->id;
6563 $kitWarehouseAvailable->real = $qtyWish;
6564 $this->stock_warehouse[$warehouseId] = $kitWarehouseAvailable;
6565 }
6566 }
6567 }
6568
6569 if ($error) {
6570 break;
6571 }
6572 }
6573 } elseif (getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE') && empty($productCachedList)) {
6574 // if all sub product are not stock managed when use parent stock
6575 $this->load_stock('warehouseopen');
6576 }
6577 }
6578
6579 if ($error) {
6580 return -1;
6581 } else {
6582 return 1;
6583 }
6584 }
6585
6593 public function loadBatchInfo($batch)
6594 {
6595 $result = array();
6596
6597 $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";
6598 $sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = ".((int) $this->id)." AND pb.batch = '".$this->db->escape($batch)."'";
6599 $sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby";
6600 dol_syslog(get_class($this)."::loadBatchInfo load first entry found for lot/serial = ".$batch, LOG_DEBUG);
6601 $resql = $this->db->query($sql);
6602 if ($resql) {
6603 $num = $this->db->num_rows($resql);
6604 $i = 0;
6605 while ($i < $num) {
6606 $obj = $this->db->fetch_object($resql);
6607 $result[] = array('batch' => $batch, 'eatby' => $this->db->jdate($obj->eatby), 'sellby' => $this->db->jdate($obj->sellby), 'qty' => $obj->qty);
6608 $i++;
6609 }
6610 return $result;
6611 } else {
6612 dol_print_error($this->db);
6613 $this->db->rollback();
6614 return array();
6615 }
6616 }
6617
6618 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6627 public function add_photo($sdir, $file)
6628 {
6629 // phpcs:enable
6630 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6631
6632 $result = 0;
6633
6634 $dir = $sdir;
6635 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6636 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos";
6637 } else {
6638 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product').dol_sanitizeFileName($this->ref);
6639 }
6640
6641 dol_mkdir($dir);
6642
6643 $dir_osencoded = $dir;
6644
6645 if (is_dir($dir_osencoded)) {
6646 $originImage = $dir.'/'.$file['name'];
6647
6648 // Cree fichier en taille origine
6649 $result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
6650
6651 if (file_exists(dol_osencode($originImage))) {
6652 // Create thumbs
6653 $this->addThumbs($originImage);
6654 }
6655 }
6656
6657 if (is_numeric($result) && $result > 0) {
6658 return 1;
6659 } else {
6660 return -1;
6661 }
6662 }
6663
6664 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6671 public function is_photo_available($sdir)
6672 {
6673 // phpcs:enable
6674 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6675 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6676
6677 $dir = $sdir;
6678 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6679 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos/";
6680 } else {
6681 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product');
6682 }
6683
6684 $dir_osencoded = dol_osencode($dir);
6685 if (file_exists($dir_osencoded)) {
6686 $handle = opendir($dir_osencoded);
6687 if (is_resource($handle)) {
6688 while (($file = readdir($handle)) !== false) {
6689 if (!utf8_check($file)) {
6690 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // To be sure data is stored in UTF8 in memory
6691 }
6692 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6693 return true;
6694 }
6695 }
6696 }
6697 }
6698
6699 return false;
6700 }
6701
6702 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6710 public function liste_photos($dir, $nbmax = 0)
6711 {
6712 // phpcs:enable
6713 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6714 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6715
6716 $nbphoto = 0;
6717 $tabobj = array();
6718
6719 $dir_osencoded = dol_osencode($dir);
6720 $handle = @opendir($dir_osencoded);
6721 if (is_resource($handle)) {
6722 while (($file = readdir($handle)) !== false) {
6723 if (!utf8_check($file)) {
6724 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // readdir returns ISO
6725 }
6726 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6727 $nbphoto++;
6728
6729 // We forge name of thumb.
6730 $photo = $file;
6731 $photo_vignette = '';
6732 $regs = array();
6733 if (preg_match('/('.$this->regeximgext.')$/i', $photo, $regs)) {
6734 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $photo).'_small'.$regs[0];
6735 }
6736
6737 $dirthumb = $dir.'thumbs/';
6738
6739 // Object
6740 $obj = array();
6741 $obj['photo'] = $photo;
6742 if ($photo_vignette && dol_is_file($dirthumb.$photo_vignette)) {
6743 $obj['photo_vignette'] = 'thumbs/'.$photo_vignette;
6744 } else {
6745 $obj['photo_vignette'] = "";
6746 }
6747
6748 $tabobj[$nbphoto - 1] = $obj;
6749
6750 // Do we have to continue with next photo ?
6751 if ($nbmax && $nbphoto >= $nbmax) {
6752 break;
6753 }
6754 }
6755 }
6756
6757 closedir($handle);
6758 }
6759
6760 return $tabobj;
6761 }
6762
6763 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6770 public function delete_photo($file)
6771 {
6772 // phpcs:enable
6773 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6774 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6775
6776 $dir = dirname($file).'/'; // Chemin du dossier contenant l'image d'origine
6777 $dirthumb = $dir.'/thumbs/'; // Chemin du dossier contenant la vignette
6778 $filename = preg_replace('/'.preg_quote($dir, '/').'/i', '', $file); // Nom du fichier
6779
6780 // On efface l'image d'origine
6781 dol_delete_file($file, 0, 0, 0, $this); // For triggers
6782
6783 // Si elle existe, on efface la vignette
6784 if (preg_match('/('.$this->regeximgext.')$/i', $filename, $regs)) {
6785 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_small'.$regs[0];
6786 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6787 dol_delete_file($dirthumb.$photo_vignette);
6788 }
6789
6790 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_mini'.$regs[0];
6791 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6792 dol_delete_file($dirthumb.$photo_vignette);
6793 }
6794 }
6795 }
6796
6797 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6804 public function get_image_size($file)
6805 {
6806 // phpcs:enable
6807 $file_osencoded = dol_osencode($file);
6808 $infoImg = getimagesize($file_osencoded); // Get information on image
6809 $this->imgWidth = $infoImg[0]; // Largeur de l'image
6810 $this->imgHeight = $infoImg[1]; // Hauteur de l'image
6811 }
6812
6818 public function loadStateBoard()
6819 {
6820 global $hookmanager;
6821
6822 $this->nb = array();
6823
6824 $sql = "SELECT count(p.rowid) as nb, fk_product_type";
6825 $sql .= " FROM ".$this->db->prefix()."product as p";
6826 $sql .= ' WHERE p.entity IN ('.getEntity($this->element, 1).')';
6827 // Add where from hooks
6828 if (is_object($hookmanager)) {
6829 $parameters = array();
6830 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
6831 $sql .= $hookmanager->resPrint;
6832 }
6833 $sql .= ' GROUP BY fk_product_type';
6834
6835 $resql = $this->db->query($sql);
6836 if ($resql) {
6837 while ($obj = $this->db->fetch_object($resql)) {
6838 if ($obj->fk_product_type == 1) {
6839 $this->nb["services"] = $obj->nb;
6840 } else {
6841 $this->nb["products"] = $obj->nb;
6842 }
6843 }
6844 $this->db->free($resql);
6845 return 1;
6846 } else {
6847 dol_print_error($this->db);
6848 $this->error = $this->db->error();
6849 return -1;
6850 }
6851 }
6852
6858 public function isProduct()
6859 {
6860 return $this->type == Product::TYPE_PRODUCT;
6861 }
6862
6868 public function isService()
6869 {
6870 return $this->type == Product::TYPE_SERVICE;
6871 }
6872
6878 public function isStockManaged()
6879 {
6880 return (($this->isProduct() || ($this->isService() && getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) && ($this->stockable_product > 0));
6881 }
6882
6888 public function isMandatoryPeriod()
6889 {
6890 return $this->mandatory_period == 1;
6891 }
6892
6898 public function hasbatch()
6899 {
6900 return $this->status_batch > 0;
6901 }
6902
6903
6904 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6913 public function get_barcode($object, $type = '')
6914 {
6915 // phpcs:enable
6916 global $conf;
6917
6918 $result = '';
6919 if (getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
6920 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
6921 foreach ($dirsociete as $dirroot) {
6922 $res = dol_include_once($dirroot . getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM').'.php');
6923 if ($res) {
6924 break;
6925 }
6926 }
6927 $var = getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM');
6928 $mod = new $var();
6929 '@phan-var-force ModeleNumRefBarCode $mod';
6930
6931 $result = $mod->getNextValue($object, $type);
6932
6933 dol_syslog(get_class($this)."::get_barcode barcode=".$result." module=".$var);
6934 }
6935 return $result;
6936 }
6937
6945 public function initAsSpecimen()
6946 {
6947 $now = dol_now();
6948
6949 // Initialize parameters
6950 $this->specimen = 1;
6951 $this->id = 0;
6952 $this->ref = 'PRODUCT_SPEC';
6953 $this->label = 'PRODUCT SPECIMEN';
6954 $this->description = 'This is description of this product specimen that was created the '.dol_print_date($now, 'dayhourlog').'.';
6955 $this->specimen = 1;
6956 $this->country_id = 1;
6957 $this->status = 1;
6958 $this->status_buy = 1;
6959 $this->tobatch = 0;
6960 $this->sell_or_eat_by_mandatory = 0;
6961 $this->note_private = 'This is a comment (private)';
6962 $this->note_public = 'This is a comment (public)';
6963 $this->date_creation = $now;
6964 $this->date_modification = $now;
6965
6966 $this->weight = 4;
6967 $this->weight_units = 3;
6968
6969 $this->length = 5;
6970 $this->length_units = 1;
6971 $this->width = 6;
6972 $this->width_units = 0;
6973 $this->height = null;
6974 $this->height_units = null;
6975
6976 $this->surface = 30;
6977 $this->surface_units = 0;
6978 $this->volume = 300;
6979 $this->volume_units = 0;
6980
6981 $this->barcode = -1; // Create barcode automatically
6982
6983 return 1;
6984 }
6985
6995 public function getLabelOfUnit($type = 'long', $outputlangs = null, $noentities = 0)
6996 {
6997 global $langs;
6998
6999 if (empty($this->fk_unit)) {
7000 return '';
7001 }
7002 if (empty($outputlangs)) {
7003 $outputlangs = $langs;
7004 }
7005
7006 $outputlangs->load('products');
7007 $label = '';
7008
7009 $sql = "SELECT code, label, short_label FROM ".$this->db->prefix()."c_units where rowid = ".((int) $this->fk_unit);
7010
7011 $resql = $this->db->query($sql);
7012 if (!$resql) {
7013 $this->error = $this->db->error();
7014 dol_syslog(get_class($this)."::getLabelOfUnit Error ".$this->error, LOG_ERR);
7015 return -1;
7016 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
7017 if ($type == 'short') {
7018 if ($noentities) {
7019 $label = $outputlangs->transnoentitiesnoconv($res['short_label']);
7020 } else {
7021 $label = $outputlangs->trans($res['short_label']);
7022 }
7023 } elseif ($type == 'code') {
7024 $label = $res['code'];
7025 } else {
7026 if ($outputlangs->trans('unit'.$res['code']) == 'unit'.$res['code']) {
7027 // No translation available
7028 $label = $res['label'];
7029 } else {
7030 // Return the translated value
7031 if ($noentities) {
7032 $label = $outputlangs->transnoentitiesnoconv('unit'.$res['code']);
7033 } else {
7034 $label = $outputlangs->trans('unit'.$res['code']);
7035 }
7036 }
7037 }
7038 }
7039 $this->db->free($resql);
7040
7041 return $label;
7042 }
7043
7044 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7050 public function min_recommended_price()
7051 {
7052 // phpcs:enable
7053 $maxpricesupplier = 0;
7054
7055 if (getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE')) {
7056 include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
7057 $product_fourn = new ProductFournisseur($this->db);
7058 $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', '');
7059
7060 if (is_array($product_fourn_list) && count($product_fourn_list) > 0) {
7061 foreach ($product_fourn_list as $productfourn) {
7062 if ($productfourn->fourn_unitprice > $maxpricesupplier) {
7063 $maxpricesupplier = $productfourn->fourn_unitprice;
7064 }
7065 }
7066
7067 $maxpricesupplier *= getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE');
7068 }
7069 }
7070
7071 return $maxpricesupplier;
7072 }
7073
7074
7085 public function setCategories($categories)
7086 {
7087 require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
7088 return parent::setCategoriesCommon($categories, Categorie::TYPE_PRODUCT);
7089 }
7090
7099 public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
7100 {
7101 $tables = array(
7102 'product_customer_price',
7103 'product_customer_price_log'
7104 );
7105
7106 return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
7107 }
7108
7120 public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
7121 {
7122 $sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM ".$this->db->prefix()."product_pricerules";
7123 $query = $this->db->query($sql);
7124
7125 $rules = array();
7126
7127 while ($result = $this->db->fetch_object($query)) {
7128 $rules[$result->level] = $result;
7129 }
7130
7131 //Because prices can be based on other level's prices, we temporarily store them
7132 $prices = array(
7133 1 => $baseprice
7134 );
7135
7136 $nbofproducts = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
7137 for ($i = 1; $i <= $nbofproducts; $i++) {
7138 $price = $baseprice;
7139 $price_min = $baseprice;
7140
7141 //We have to make sure it does exist and it is > 0
7142 //First price level only allows changing min_price
7143 if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) {
7144 $price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent / 100));
7145 }
7146
7147 $prices[$i] = $price;
7148
7149 //We have to make sure it does exist and it is > 0
7150 if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) {
7151 $price_min = $price * (1 - ($rules[$i]->var_min_percent / 100));
7152 }
7153
7154 //Little check to make sure the price is modified before triggering generation
7155 $check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i]));
7156 $check_type = ($baseprice == $this->multiprices_base_type[$i]);
7157
7158 if ($check_amount && $check_type) {
7159 continue;
7160 }
7161
7162 if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, 1) < 0) {
7163 return -1;
7164 }
7165 }
7166
7167 return 1;
7168 }
7169
7175 public function getRights()
7176 {
7177 global $user;
7178
7179 if ($this->isProduct()) {
7180 return $user->rights->produit;
7181 } else {
7182 return $user->rights->service;
7183 }
7184 }
7185
7192 public function info($id)
7193 {
7194 $sql = "SELECT p.rowid, p.ref, p.datec as date_creation, p.tms as date_modification,";
7195 $sql .= " p.fk_user_author, p.fk_user_modif";
7196 $sql .= " FROM ".$this->db->prefix().$this->table_element." as p";
7197 $sql .= " WHERE p.rowid = ".((int) $id);
7198
7199 $result = $this->db->query($sql);
7200 if ($result) {
7201 if ($this->db->num_rows($result)) {
7202 $obj = $this->db->fetch_object($result);
7203
7204 $this->id = $obj->rowid;
7205 $this->ref = $obj->ref;
7206
7207 $this->user_creation_id = $obj->fk_user_author;
7208 $this->user_modification_id = $obj->fk_user_modif;
7209
7210 $this->date_creation = $this->db->jdate($obj->date_creation);
7211 $this->date_modification = $this->db->jdate($obj->date_modification);
7212 }
7213
7214 $this->db->free($result);
7215 } else {
7216 dol_print_error($this->db);
7217 }
7218 }
7219
7220
7226 public function getProductDurationHours()
7227 {
7228 if (empty($this->duration_value)) {
7229 $this->errors[] = 'ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
7230 return -1;
7231 }
7232 if ($this->duration_unit == 's') {
7233 $prodDurationHours = 1. / 3600;
7234 } elseif ($this->duration_unit == 'i' || $this->duration_unit == 'mn' || $this->duration_unit == 'min') {
7235 $prodDurationHours = 1. / 60;
7236 } elseif ($this->duration_unit == 'h') {
7237 $prodDurationHours = 1.;
7238 } elseif ($this->duration_unit == 'd') {
7239 $prodDurationHours = 24.;
7240 } elseif ($this->duration_unit == 'w') {
7241 $prodDurationHours = 24. * 7;
7242 } elseif ($this->duration_unit == 'm') {
7243 $prodDurationHours = 24. * 30;
7244 } elseif ($this->duration_unit == 'y') {
7245 $prodDurationHours = 24. * 365;
7246 } else {
7247 $prodDurationHours = 0.0;
7248 }
7249 $prodDurationHours *= $this->duration_value;
7250
7251 return $prodDurationHours;
7252 }
7253
7254
7262 public function getKanbanView($option = '', $arraydata = null)
7263 {
7264 global $langs, $conf;
7265
7266 $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
7267
7268 $return = '<div class="box-flex-item box-flex-grow-zero">';
7269 $return .= '<div class="info-box info-box-sm">';
7270 $return .= '<div class="info-box-img">';
7271 $label = '';
7272 if ($this->is_photo_available($conf->product->multidir_output[$this->entity])) {
7273 $label .= $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 120, 160, 0, 0, 0, '', 'photoref photokanban');
7274 $return .= $label;
7275 } else {
7276 if ($this->isProduct()) {
7277 $label .= img_picto('', 'product');
7278 } elseif ($this->isService()) {
7279 $label .= img_picto('', 'service');
7280 }
7281 $return .= $label;
7282 }
7283 $return .= '</div>';
7284 $return .= '<div class="info-box-content">';
7285 $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">'.(method_exists($this, 'getNomUrl') ? $this->getNomUrl() : $this->ref).'</span>';
7286 if ($selected >= 0) {
7287 $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
7288 }
7289 if (property_exists($this, 'label')) {
7290 $return .= '<br><span class="info-box-label opacitymedium inline-block tdoverflowmax150 valignmiddle" title="'.dol_escape_htmltag($this->label).'">'.dol_escape_htmltag($this->label).'</span>';
7291 }
7292 if (property_exists($this, 'price') && property_exists($this, 'price_ttc')) {
7293 if ($this->price_base_type == 'TTC') {
7294 $return .= '<br><span class="info-box-status amount">'.price($this->price_ttc).' '.$langs->trans("TTC").'</span>';
7295 } else {
7296 if ($this->status) {
7297 $return .= '<br><span class="info-box-status amount">'.price($this->price).' '.$langs->trans("HT").'</span>';
7298 }
7299 }
7300 }
7301 $br = 1;
7302 if (property_exists($this, 'stock_reel') && $this->isProduct()) {
7303 $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>';
7304 $br = 0;
7305 }
7306 if (method_exists($this, 'getLibStatut')) {
7307 if ($br) {
7308 $return .= '<br><div class="info-box-status inline-block valignmiddle">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7309 } else {
7310 $return .= '<div class="info-box-status inline-block valignmiddle marginleftonly paddingleft">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7311 }
7312 }
7313 $return .= '</div>';
7314 $return .= '</div>';
7315 $return .= '</div>';
7316 return $return;
7317 }
7318
7325 public function getProductsToPreviewInEmail($limit)
7326 {
7327
7328 if (!is_numeric($limit)) {
7329 return -1;
7330 }
7331
7332 $sql = "SELECT p.rowid, p.ref, p.label, p.description, p.entity, ef.filename
7333 FROM ".MAIN_DB_PREFIX."product AS p
7334 JOIN ".MAIN_DB_PREFIX."ecm_files AS ef ON p.rowid = ef.src_object_id
7335 WHERE ef.entity IN (".getEntity('product').")
7336 AND (ef.filename LIKE '%.png' OR ef.filename LIKE '%.jpeg' OR ef.filename LIKE '%.svg')
7337 GROUP BY p.rowid, p.ref, p.label, p.description, p.entity, ef.filename
7338 ORDER BY p.datec ASC
7339 LIMIT " . ((int) $limit);
7340
7341 $resql = $this->db->query($sql);
7342 $products = array();
7343
7344 if ($resql) {
7345 while ($obj = $this->db->fetch_object($resql)) {
7346 $products[] = array(
7347 'rowid' => $obj->rowid,
7348 'ref' => $obj->ref,
7349 'label' => $obj->label,
7350 'description' => $obj->description,
7351 'entity' => $obj->entity,
7352 'filename' => $obj->filename
7353 );
7354 }
7355 } else {
7356 dol_print_error($this->db);
7357 }
7358 if (empty($products)) {
7359 return -1;
7360 }
7361 return $products;
7362 }
7363}
7364
7370{
7371 public $picto = 'service';
7372}
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