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 $year -= 1;
4474 }
4475 }
4476
4477 return array_reverse($result);
4478 }
4479
4480
4481 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4492 public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4493 {
4494 // phpcs:enable
4495 global $user;
4496
4497 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4498 if ($mode == 'bynumber') {
4499 $sql .= ", count(DISTINCT f.rowid)";
4500 }
4501 $sql .= ", sum(d.total_ht) as total_ht";
4502 $sql .= " FROM ".$this->db->prefix()."facturedet as d, ".$this->db->prefix()."facture as f, ".$this->db->prefix()."societe as s";
4503 if ($filteronproducttype >= 0) {
4504 $sql .= ", ".$this->db->prefix()."product as p";
4505 }
4506 if (!$user->hasRight('societe', 'client', 'voir')) {
4507 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4508 }
4509 $sql .= " WHERE f.rowid = d.fk_facture";
4510 if ($this->id > 0) {
4511 $sql .= " AND d.fk_product = ".((int) $this->id);
4512 } else {
4513 $sql .= " AND d.fk_product > 0";
4514 }
4515 if ($filteronproducttype >= 0) {
4516 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4517 }
4518 $sql .= " AND f.fk_soc = s.rowid";
4519 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4520 if (!$user->hasRight('societe', 'client', 'voir')) {
4521 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4522 }
4523 if ($socid > 0) {
4524 $sql .= " AND f.fk_soc = $socid";
4525 }
4526 $sql .= $morefilter;
4527 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4528 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4529
4530 return $this->_get_stats($sql, $mode, $year);
4531 }
4532
4533
4534 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4545 public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4546 {
4547 // phpcs:enable
4548 global $user;
4549
4550 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4551 if ($mode == 'bynumber') {
4552 $sql .= ", count(DISTINCT f.rowid)";
4553 }
4554 $sql .= ", sum(d.total_ht) as total_ht";
4555 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as d, ".$this->db->prefix()."facture_fourn as f, ".$this->db->prefix()."societe as s";
4556 if ($filteronproducttype >= 0) {
4557 $sql .= ", ".$this->db->prefix()."product as p";
4558 }
4559 if (!$user->hasRight('societe', 'client', 'voir')) {
4560 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4561 }
4562 $sql .= " WHERE f.rowid = d.fk_facture_fourn";
4563 if ($this->id > 0) {
4564 $sql .= " AND d.fk_product = ".((int) $this->id);
4565 } else {
4566 $sql .= " AND d.fk_product > 0";
4567 }
4568 if ($filteronproducttype >= 0) {
4569 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4570 }
4571 $sql .= " AND f.fk_soc = s.rowid";
4572 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4573 if (!$user->hasRight('societe', 'client', 'voir')) {
4574 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4575 }
4576 if ($socid > 0) {
4577 $sql .= " AND f.fk_soc = $socid";
4578 }
4579 $sql .= $morefilter;
4580 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4581 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4582
4583 return $this->_get_stats($sql, $mode, $year);
4584 }
4585
4586 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4597 public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4598 {
4599 // phpcs:enable
4600 global $user;
4601
4602 $sql = "SELECT sum(d.qty) as qty, date_format(p.datep, '%Y%m')";
4603 if ($mode == 'bynumber') {
4604 $sql .= ", count(DISTINCT p.rowid)";
4605 }
4606 $sql .= ", sum(d.total_ht) as total_ht";
4607 $sql .= " FROM ".$this->db->prefix()."propaldet as d, ".$this->db->prefix()."propal as p, ".$this->db->prefix()."societe as s";
4608 if ($filteronproducttype >= 0) {
4609 $sql .= ", ".$this->db->prefix()."product as prod";
4610 }
4611 if (!$user->hasRight('societe', 'client', 'voir')) {
4612 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4613 }
4614 $sql .= " WHERE p.rowid = d.fk_propal";
4615 if ($this->id > 0) {
4616 $sql .= " AND d.fk_product = ".((int) $this->id);
4617 } else {
4618 $sql .= " AND d.fk_product > 0";
4619 }
4620 if ($filteronproducttype >= 0) {
4621 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4622 }
4623 $sql .= " AND p.fk_soc = s.rowid";
4624 $sql .= " AND p.entity IN (".getEntity('propal').")";
4625 if (!$user->hasRight('societe', 'client', 'voir')) {
4626 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4627 }
4628 if ($socid > 0) {
4629 $sql .= " AND p.fk_soc = ".((int) $socid);
4630 }
4631 $sql .= $morefilter;
4632 $sql .= " GROUP BY date_format(p.datep,'%Y%m')";
4633 $sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC";
4634
4635 return $this->_get_stats($sql, $mode, $year);
4636 }
4637
4638 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4649 public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4650 {
4651 // phpcs:enable
4652 global $user;
4653
4654 $sql = "SELECT sum(d.qty) as qty, date_format(p.date_valid, '%Y%m')";
4655 if ($mode == 'bynumber') {
4656 $sql .= ", count(DISTINCT p.rowid)";
4657 }
4658 $sql .= ", sum(d.total_ht) as total_ht";
4659 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as d, ".$this->db->prefix()."supplier_proposal as p, ".$this->db->prefix()."societe as s";
4660 if ($filteronproducttype >= 0) {
4661 $sql .= ", ".$this->db->prefix()."product as prod";
4662 }
4663 if (!$user->hasRight('societe', 'client', 'voir')) {
4664 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4665 }
4666 $sql .= " WHERE p.rowid = d.fk_supplier_proposal";
4667 if ($this->id > 0) {
4668 $sql .= " AND d.fk_product = ".((int) $this->id);
4669 } else {
4670 $sql .= " AND d.fk_product > 0";
4671 }
4672 if ($filteronproducttype >= 0) {
4673 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4674 }
4675 $sql .= " AND p.fk_soc = s.rowid";
4676 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
4677 if (!$user->hasRight('societe', 'client', 'voir')) {
4678 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4679 }
4680 if ($socid > 0) {
4681 $sql .= " AND p.fk_soc = ".((int) $socid);
4682 }
4683 $sql .= $morefilter;
4684 $sql .= " GROUP BY date_format(p.date_valid,'%Y%m')";
4685 $sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
4686
4687 return $this->_get_stats($sql, $mode, $year);
4688 }
4689
4690 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4701 public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4702 {
4703 // phpcs:enable
4704 global $user;
4705
4706 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4707 if ($mode == 'bynumber') {
4708 $sql .= ", count(DISTINCT c.rowid)";
4709 }
4710 $sql .= ", sum(d.total_ht) as total_ht";
4711 $sql .= " FROM ".$this->db->prefix()."commandedet as d, ".$this->db->prefix()."commande as c, ".$this->db->prefix()."societe as s";
4712 if ($filteronproducttype >= 0) {
4713 $sql .= ", ".$this->db->prefix()."product as p";
4714 }
4715 if (!$user->hasRight('societe', 'client', 'voir')) {
4716 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4717 }
4718 $sql .= " WHERE c.rowid = d.fk_commande";
4719 if ($this->id > 0) {
4720 $sql .= " AND d.fk_product = ".((int) $this->id);
4721 } else {
4722 $sql .= " AND d.fk_product > 0";
4723 }
4724 if ($filteronproducttype >= 0) {
4725 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4726 }
4727 $sql .= " AND c.fk_soc = s.rowid";
4728 $sql .= " AND c.entity IN (".getEntity('commande').")";
4729 if (!$user->hasRight('societe', 'client', 'voir')) {
4730 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4731 }
4732 if ($socid > 0) {
4733 $sql .= " AND c.fk_soc = ".((int) $socid);
4734 }
4735 $sql .= $morefilter;
4736 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4737 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4738
4739 return $this->_get_stats($sql, $mode, $year);
4740 }
4741
4742 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4753 public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4754 {
4755 // phpcs:enable
4756 global $user;
4757
4758 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4759 if ($mode == 'bynumber') {
4760 $sql .= ", count(DISTINCT c.rowid)";
4761 }
4762 $sql .= ", sum(d.total_ht) as total_ht";
4763 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as d, ".$this->db->prefix()."commande_fournisseur as c, ".$this->db->prefix()."societe as s";
4764 if ($filteronproducttype >= 0) {
4765 $sql .= ", ".$this->db->prefix()."product as p";
4766 }
4767 if (!$user->hasRight('societe', 'client', 'voir')) {
4768 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4769 }
4770 $sql .= " WHERE c.rowid = d.fk_commande";
4771 if ($this->id > 0) {
4772 $sql .= " AND d.fk_product = ".((int) $this->id);
4773 } else {
4774 $sql .= " AND d.fk_product > 0";
4775 }
4776 if ($filteronproducttype >= 0) {
4777 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4778 }
4779 $sql .= " AND c.fk_soc = s.rowid";
4780 $sql .= " AND c.entity IN (".getEntity('supplier_order').")";
4781 if (!$user->hasRight('societe', 'client', 'voir')) {
4782 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4783 }
4784 if ($socid > 0) {
4785 $sql .= " AND c.fk_soc = ".((int) $socid);
4786 }
4787 $sql .= $morefilter;
4788 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4789 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4790
4791 return $this->_get_stats($sql, $mode, $year);
4792 }
4793
4794 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4805 public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4806 {
4807 // phpcs:enable
4808 global $user;
4809
4810 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_contrat, '%Y%m')";
4811 if ($mode == 'bynumber') {
4812 $sql .= ", count(DISTINCT c.rowid)";
4813 }
4814 $sql .= ", sum(d.total_ht) as total_ht";
4815 $sql .= " FROM ".$this->db->prefix()."contratdet as d, ".$this->db->prefix()."contrat as c, ".$this->db->prefix()."societe as s";
4816 if ($filteronproducttype >= 0) {
4817 $sql .= ", ".$this->db->prefix()."product as p";
4818 }
4819 if (!$user->hasRight('societe', 'client', 'voir')) {
4820 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4821 }
4822 $sql .= " WHERE c.entity IN (".getEntity('contract').")";
4823 $sql .= " AND c.rowid = d.fk_contrat";
4824
4825 if ($this->id > 0) {
4826 $sql .= " AND d.fk_product = ".((int) $this->id);
4827 } else {
4828 $sql .= " AND d.fk_product > 0";
4829 }
4830 if ($filteronproducttype >= 0) {
4831 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4832 }
4833 $sql .= " AND c.fk_soc = s.rowid";
4834
4835 if (!$user->hasRight('societe', 'client', 'voir')) {
4836 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4837 }
4838 if ($socid > 0) {
4839 $sql .= " AND c.fk_soc = ".((int) $socid);
4840 }
4841 $sql .= $morefilter;
4842 $sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')";
4843 $sql .= " ORDER BY date_format(c.date_contrat,'%Y%m') DESC";
4844
4845 return $this->_get_stats($sql, $mode, $year);
4846 }
4847
4848 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4859 public function get_nb_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4860 {
4861 // phpcs:enable
4862 global $user;
4863
4864 $sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')";
4865 if ($mode == 'bynumber') {
4866 $sql .= ", count(DISTINCT d.rowid)";
4867 }
4868 $sql .= " FROM ".$this->db->prefix()."mrp_mo as d LEFT JOIN ".$this->db->prefix()."societe as s ON d.fk_soc = s.rowid";
4869 if ($filteronproducttype >= 0) {
4870 $sql .= ", ".$this->db->prefix()."product as p";
4871 }
4872 if (!$user->hasRight('societe', 'client', 'voir')) {
4873 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4874 }
4875
4876 $sql .= " WHERE d.entity IN (".getEntity('mo').")";
4877 $sql .= " AND d.status > 0";
4878
4879 if ($this->id > 0) {
4880 $sql .= " AND d.fk_product = ".((int) $this->id);
4881 } else {
4882 $sql .= " AND d.fk_product > 0";
4883 }
4884 if ($filteronproducttype >= 0) {
4885 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4886 }
4887
4888 if (!$user->hasRight('societe', 'client', 'voir')) {
4889 $sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4890 }
4891 if ($socid > 0) {
4892 $sql .= " AND d.fk_soc = ".((int) $socid);
4893 }
4894 $sql .= $morefilter;
4895 $sql .= " GROUP BY date_format(d.date_valid,'%Y%m')";
4896 $sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC";
4897
4898 return $this->_get_stats($sql, $mode, $year);
4899 }
4900
4901 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4912 public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
4913 {
4914 global $user;
4915
4916 // phpcs:enable
4917 // Clean parameters
4918 if (!is_numeric($id_pere)) {
4919 $id_pere = 0;
4920 }
4921 if (!is_numeric($id_fils)) {
4922 $id_fils = 0;
4923 }
4924 if (!is_numeric($incdec)) {
4925 $incdec = 0;
4926 }
4927
4928 $result = $this->del_sousproduit($id_pere, $id_fils);
4929 if ($result < 0) {
4930 return $result;
4931 }
4932
4933 // Check not already father of id_pere (to avoid father -> child -> father links)
4934 $sql = "SELECT fk_product_pere from ".$this->db->prefix()."product_association";
4935 $sql .= " WHERE fk_product_pere = ".((int) $id_fils)." AND fk_product_fils = ".((int) $id_pere);
4936 if (!$this->db->query($sql)) {
4937 dol_print_error($this->db);
4938 return -1;
4939 } else {
4940 //Selection of the highest row
4941 $sql = "SELECT MAX(rang) as max_rank FROM ".$this->db->prefix()."product_association";
4942 $sql .= " WHERE fk_product_pere = ".((int) $id_pere);
4943 $resql = $this->db->query($sql);
4944 if ($resql) {
4945 $obj = $this->db->fetch_object($resql);
4946 $rank = $obj->max_rank + 1;
4947 //Addition of a product with the highest rank +1
4948 $sql = "INSERT INTO ".$this->db->prefix()."product_association(fk_product_pere,fk_product_fils,qty,incdec,rang)";
4949 $sql .= " VALUES (".((int) $id_pere).", ".((int) $id_fils).", ".price2num($qty, 'MS').", ".((int) $incdec).", ".((int) $rank).")";
4950 if (! $this->db->query($sql)) {
4951 dol_print_error($this->db);
4952 return -1;
4953 } else {
4954 if (!$notrigger) {
4955 // Call trigger
4956 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_ADD', $user);
4957 if ($result < 0) {
4958 $this->error = $this->db->lasterror();
4959 dol_syslog(get_class($this).'::addSubproduct error='.$this->error, LOG_ERR);
4960 return -1;
4961 }
4962 }
4963 // End call triggers
4964
4965 return 1;
4966 }
4967 } else {
4968 dol_print_error($this->db);
4969 return -1;
4970 }
4971 }
4972 }
4973
4974 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4985 public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
4986 {
4987 global $user;
4988
4989 // phpcs:enable
4990 // Clean parameters
4991 if (!is_numeric($id_pere)) {
4992 $id_pere = 0;
4993 }
4994 if (!is_numeric($id_fils)) {
4995 $id_fils = 0;
4996 }
4997 if (!is_numeric($incdec)) {
4998 $incdec = 1;
4999 }
5000 if (!is_numeric($qty)) {
5001 $qty = 1;
5002 }
5003
5004 $sql = 'UPDATE '.$this->db->prefix().'product_association SET ';
5005 $sql .= 'qty = '.price2num($qty, 'MS');
5006 $sql .= ',incdec = '.((int) $incdec);
5007 $sql .= ' WHERE fk_product_pere = '.((int) $id_pere).' AND fk_product_fils = '.((int) $id_fils);
5008
5009 if (!$this->db->query($sql)) {
5010 dol_print_error($this->db);
5011 return -1;
5012 } else {
5013 if (!$notrigger) {
5014 // Call trigger
5015 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_UPDATE', $user);
5016 if ($result < 0) {
5017 $this->error = $this->db->lasterror();
5018 dol_syslog(get_class($this).'::updateSubproduct error='.$this->error, LOG_ERR);
5019 return -1;
5020 }
5021 // End call triggers
5022 }
5023
5024 return 1;
5025 }
5026 }
5027
5028 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5037 public function del_sousproduit($fk_parent, $fk_child, $notrigger = 0)
5038 {
5039 global $user;
5040
5041 // phpcs:enable
5042 if (!is_numeric($fk_parent)) {
5043 $fk_parent = 0;
5044 }
5045 if (!is_numeric($fk_child)) {
5046 $fk_child = 0;
5047 }
5048
5049 $sql = "DELETE FROM ".$this->db->prefix()."product_association";
5050 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5051 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5052
5053 dol_syslog(get_class($this).'::del_sousproduit', LOG_DEBUG);
5054 if (!$this->db->query($sql)) {
5055 dol_print_error($this->db);
5056 return -1;
5057 }
5058
5059 // Updated ranks so that none are missing
5060 $sqlrank = "SELECT rowid, rang FROM ".$this->db->prefix()."product_association";
5061 $sqlrank .= " WHERE fk_product_pere = ".((int) $fk_parent);
5062 $sqlrank .= " ORDER BY rang";
5063 $resqlrank = $this->db->query($sqlrank);
5064 if ($resqlrank) {
5065 $cpt = 0;
5066 while ($objrank = $this->db->fetch_object($resqlrank)) {
5067 $cpt++;
5068 $sql = "UPDATE ".$this->db->prefix()."product_association";
5069 $sql .= " SET rang = ".((int) $cpt);
5070 $sql .= " WHERE rowid = ".((int) $objrank->rowid);
5071 if (! $this->db->query($sql)) {
5072 dol_print_error($this->db);
5073 return -1;
5074 }
5075 }
5076 }
5077
5078 if (!$notrigger) {
5079 // Call trigger
5080 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_DELETE', $user);
5081 if ($result < 0) {
5082 $this->error = $this->db->lasterror();
5083 dol_syslog(get_class($this).'::delSubproduct error='.$this->error, LOG_ERR);
5084 return -1;
5085 }
5086 // End call triggers
5087 }
5088
5089 return 1;
5090 }
5091
5092 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5100 public function is_sousproduit($fk_parent, $fk_child)
5101 {
5102 // phpcs:enable
5103 $sql = "SELECT fk_product_pere, qty, incdec";
5104 $sql .= " FROM ".$this->db->prefix()."product_association";
5105 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
5106 $sql .= " AND fk_product_fils = ".((int) $fk_child);
5107
5108 $result = $this->db->query($sql);
5109 if ($result) {
5110 $num = $this->db->num_rows($result);
5111
5112 if ($num > 0) {
5113 $obj = $this->db->fetch_object($result);
5114
5115 $this->is_sousproduit_qty = $obj->qty;
5116 $this->is_sousproduit_incdec = $obj->incdec;
5117
5118 return 1;
5119 } else {
5120 return 0;
5121 }
5122 } else {
5123 dol_print_error($this->db);
5124 return -1;
5125 }
5126 }
5127
5128
5129 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5140 public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
5141 {
5142 // phpcs:enable
5143 global $conf;
5144
5145 $now = dol_now();
5146
5147 dol_syslog(get_class($this)."::add_fournisseur id_fourn = ".$id_fourn." ref_fourn=".$ref_fourn." quantity=".$quantity, LOG_DEBUG);
5148
5149 // Clean parameters
5150 $quantity = price2num($quantity, 'MS');
5151
5152 if ($ref_fourn) {
5153 // Check if ref is not already used
5154 $sql = "SELECT rowid, fk_product";
5155 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5156 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5157 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5158 $sql .= " AND fk_product <> ".((int) $this->id);
5159 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5160
5161 $resql = $this->db->query($sql);
5162 if ($resql) {
5163 $obj = $this->db->fetch_object($resql);
5164 if ($obj) {
5165 // If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
5166 $this->product_id_already_linked = $obj->fk_product;
5167 return -3;
5168 }
5169 $this->db->free($resql);
5170 }
5171 }
5172
5173 $sql = "SELECT rowid";
5174 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5175 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
5176 if ($ref_fourn) {
5177 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
5178 } else {
5179 $sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
5180 }
5181 $sql .= " AND quantity = ".((float) $quantity);
5182 $sql .= " AND fk_product = ".((int) $this->id);
5183 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
5184
5185 $resql = $this->db->query($sql);
5186 if ($resql) {
5187 $obj = $this->db->fetch_object($resql);
5188
5189 // The reference supplier does not exist, we create it for this product.
5190 if (empty($obj)) {
5191 $sql = "INSERT INTO ".$this->db->prefix()."product_fournisseur_price(";
5192 $sql .= "datec";
5193 $sql .= ", entity";
5194 $sql .= ", fk_product";
5195 $sql .= ", fk_soc";
5196 $sql .= ", ref_fourn";
5197 $sql .= ", quantity";
5198 $sql .= ", fk_user";
5199 $sql .= ", tva_tx";
5200 $sql .= ") VALUES (";
5201 $sql .= "'".$this->db->idate($now)."'";
5202 $sql .= ", ".((int) $conf->entity);
5203 $sql .= ", ".((int) $this->id);
5204 $sql .= ", ".((int) $id_fourn);
5205 $sql .= ", '".$this->db->escape($ref_fourn)."'";
5206 $sql .= ", ".((float) $quantity);
5207 $sql .= ", ".((int) $user->id);
5208 $sql .= ", 0";
5209 $sql .= ")";
5210
5211 if ($this->db->query($sql)) {
5212 $this->product_fourn_price_id = $this->db->last_insert_id($this->db->prefix()."product_fournisseur_price");
5213 return 1;
5214 } else {
5215 $this->error = $this->db->lasterror();
5216 return -1;
5217 }
5218 } else {
5219 // If the supplier price already exists for this product and quantity
5220 $this->product_fourn_price_id = $obj->rowid;
5221 return 0;
5222 }
5223 } else {
5224 $this->error = $this->db->lasterror();
5225 return -2;
5226 }
5227 }
5228
5229
5230 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5236 public function list_suppliers()
5237 {
5238 // phpcs:enable
5239 global $conf;
5240
5241 $list = array();
5242
5243 $sql = "SELECT DISTINCT p.fk_soc";
5244 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as p";
5245 $sql .= " WHERE p.fk_product = ".((int) $this->id);
5246 $sql .= " AND p.entity = ".((int) $conf->entity);
5247
5248 $result = $this->db->query($sql);
5249 if ($result) {
5250 $num = $this->db->num_rows($result);
5251 $i = 0;
5252 while ($i < $num) {
5253 $obj = $this->db->fetch_object($result);
5254 $list[$i] = $obj->fk_soc;
5255 $i++;
5256 }
5257 }
5258
5259 return $list;
5260 }
5261
5262 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5270 public function clone_price($fromId, $toId)
5271 {
5272 global $user;
5273
5274 $now = dol_now();
5275
5276 $this->db->begin();
5277
5278 // prices
5279 $sql = "INSERT INTO ".$this->db->prefix()."product_price (";
5280 $sql .= " entity";
5281 $sql .= ", fk_product";
5282 $sql .= ", date_price";
5283 $sql .= ", price_level";
5284 $sql .= ", price";
5285 $sql .= ", price_ttc";
5286 $sql .= ", price_min";
5287 $sql .= ", price_min_ttc";
5288 $sql .= ", price_base_type";
5289 $sql .= ", price_label";
5290 $sql .= ", default_vat_code";
5291 $sql .= ", tva_tx";
5292 $sql .= ", recuperableonly";
5293 $sql .= ", localtax1_tx";
5294 $sql .= ", localtax1_type";
5295 $sql .= ", localtax2_tx";
5296 $sql .= ", localtax2_type";
5297 $sql .= ", fk_user_author";
5298 $sql .= ", tosell";
5299 $sql .= ", price_by_qty";
5300 $sql .= ", fk_price_expression";
5301 $sql .= ", fk_multicurrency";
5302 $sql .= ", multicurrency_code";
5303 $sql .= ", multicurrency_tx";
5304 $sql .= ", multicurrency_price";
5305 $sql .= ", multicurrency_price_ttc";
5306 $sql .= ")";
5307 $sql .= " SELECT";
5308 $sql .= " entity";
5309 $sql .= ", ".((int) $toId);
5310 $sql .= ", '".$this->db->idate($now)."'";
5311 $sql .= ", price_level";
5312 $sql .= ", price";
5313 $sql .= ", price_ttc";
5314 $sql .= ", price_min";
5315 $sql .= ", price_min_ttc";
5316 $sql .= ", price_base_type";
5317 $sql .= ", price_label";
5318 $sql .= ", default_vat_code";
5319 $sql .= ", tva_tx";
5320 $sql .= ", recuperableonly";
5321 $sql .= ", localtax1_tx";
5322 $sql .= ", localtax1_type";
5323 $sql .= ", localtax2_tx";
5324 $sql .= ", localtax2_type";
5325 $sql .= ", ".((int) $user->id);
5326 $sql .= ", tosell";
5327 $sql .= ", price_by_qty";
5328 $sql .= ", fk_price_expression";
5329 $sql .= ", fk_multicurrency";
5330 $sql .= ", multicurrency_code";
5331 $sql .= ", multicurrency_tx";
5332 $sql .= ", multicurrency_price";
5333 $sql .= ", multicurrency_price_ttc";
5334 $sql .= " FROM ".$this->db->prefix()."product_price ps";
5335 $sql .= " WHERE fk_product = ".((int) $fromId);
5336 $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)";
5337 $sql .= " ORDER BY date_price DESC";
5338
5339 dol_syslog(__METHOD__, LOG_DEBUG);
5340 $resql = $this->db->query($sql);
5341 if (!$resql) {
5342 $this->db->rollback();
5343 return -1;
5344 }
5345
5346 $this->db->commit();
5347 return 1;
5348 }
5349
5350 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5358 public function clone_associations($fromId, $toId)
5359 {
5360 // phpcs:enable
5361 $this->db->begin();
5362
5363 $sql = 'INSERT INTO '.$this->db->prefix().'product_association (fk_product_pere, fk_product_fils, qty, incdec)';
5364 $sql .= " SELECT ".((int) $toId).", fk_product_fils, qty, incdec FROM ".$this->db->prefix()."product_association";
5365 $sql .= " WHERE fk_product_pere = ".((int) $fromId);
5366
5367 dol_syslog(get_class($this).'::clone_association', LOG_DEBUG);
5368 if (!$this->db->query($sql)) {
5369 $this->db->rollback();
5370 return -1;
5371 }
5372
5373 $this->db->commit();
5374 return 1;
5375 }
5376
5377 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5385 public function clone_fournisseurs($fromId, $toId)
5386 {
5387 // phpcs:enable
5388 $this->db->begin();
5389
5390 $now = dol_now();
5391
5392 // les fournisseurs
5393 /*$sql = "INSERT ".$this->db->prefix()."product_fournisseur ("
5394 . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
5395 . " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, ref_fourn, fk_user_author"
5396 . " FROM ".$this->db->prefix()."product_fournisseur"
5397 . " WHERE fk_product = ".((int) $fromId);
5398
5399 if ( ! $this->db->query($sql ) )
5400 {
5401 $this->db->rollback();
5402 return -1;
5403 }*/
5404
5405 // les prix de fournisseurs.
5406 $sql = "INSERT ".$this->db->prefix()."product_fournisseur_price (";
5407 $sql .= " datec, fk_product, fk_soc, price, quantity, fk_user, tva_tx)";
5408 $sql .= " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, price, quantity, fk_user, tva_tx";
5409 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5410 $sql .= " WHERE fk_product = ".((int) $fromId);
5411
5412 dol_syslog(get_class($this).'::clone_fournisseurs', LOG_DEBUG);
5413 $resql = $this->db->query($sql);
5414 if (!$resql) {
5415 $this->db->rollback();
5416 return -1;
5417 } else {
5418 $this->db->commit();
5419 return 1;
5420 }
5421 }
5422
5423 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5436 public function fetch_prod_arbo($prod, $compl_path = '', $multiply = 1, $level = 1, $id_parent = 0, $ignore_stock_load = 0)
5437 {
5438 // phpcs:enable
5439 $tmpproduct = null;
5440
5441 if ($multiply < 1) {
5442 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);
5443 $multiply = 1;
5444 }
5445
5446 //var_dump($prod);
5447 foreach ($prod as $id_product => $desc_pere) { // $id_product is 0 (first call starting with root top) or an id of a sub_product
5448 if (is_array($desc_pere)) { // If desc_pere is an array, this means it's a child
5449 $id = (!empty($desc_pere[0]) ? $desc_pere[0] : '');
5450 $nb = (!empty($desc_pere[1]) ? $desc_pere[1] : '');
5451 $type = (!empty($desc_pere[2]) ? $desc_pere[2] : '');
5452 $label = (!empty($desc_pere[3]) ? $desc_pere[3] : '');
5453 $incdec = (!empty($desc_pere[4]) ? $desc_pere[4] : 0);
5454
5455 //print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n";
5456 if (is_null($tmpproduct)) {
5457 $tmpproduct = new Product($this->db); // So we initialize tmpproduct only once for all loop.
5458 }
5459 $tmpproduct->fetch($id); // Load product to get ->ref
5460
5461 if (empty($ignore_stock_load) && ($tmpproduct->isProduct() || getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) {
5462 $tmpproduct->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
5463 }
5464
5465 $this->res[] = array(
5466 'id' => $id, // Id product
5467 'id_parent' => $id_parent,
5468 'ref' => $tmpproduct->ref, // Ref product
5469 'nb' => $nb, // Nb of units that compose parent product
5470 'nb_total' => $nb * $multiply, // Nb of units for all nb of product
5471 'stock' => $tmpproduct->stock_reel, // Stock
5472 'stock_alert' => $tmpproduct->seuil_stock_alerte, // Stock alert
5473 'label' => $label,
5474 'fullpath' => $compl_path.$label, // Label
5475 'type' => $type, // Nb of units that compose parent product
5476 'desiredstock' => $tmpproduct->desiredstock,
5477 'level' => $level,
5478 'incdec' => $incdec,
5479 'entity' => $tmpproduct->entity
5480 );
5481
5482 // Recursive call if there child has children of its own
5483 if (isset($desc_pere['childs']) && is_array($desc_pere['childs'])) {
5484 if (!is_int($desc_pere[1] * $multiply)) {
5485 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);
5486 }
5487
5488 //print 'YYY We go down for '.$desc_pere[3]." -> \n";
5489 $this->fetch_prod_arbo($desc_pere['childs'], $compl_path.$desc_pere[3]." -> ", (int) ceil($desc_pere[1] * $multiply), $level + 1, $id, $ignore_stock_load);
5490 }
5491 }
5492 }
5493 }
5494
5495 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5504 public function get_arbo_each_prod($multiply = 1, $ignore_stock_load = 0)
5505 {
5506 // phpcs:enable
5507 $this->res = array();
5508 if (isset($this->sousprods) && is_array($this->sousprods)) {
5509 foreach ($this->sousprods as $prod_name => $desc_product) {
5510 if (is_array($desc_product)) {
5511 $this->fetch_prod_arbo($desc_product, "", $multiply, 1, $this->id, $ignore_stock_load); // This set $this->res
5512 }
5513 }
5514 }
5515 //var_dump($res);
5516 return $this->res;
5517 }
5518
5526 public function hasFatherOrChild($mode = 0)
5527 {
5528 $nb = 0;
5529
5530 $sql = "SELECT COUNT(pa.rowid) as nb";
5531 $sql .= " FROM ".$this->db->prefix()."product_association as pa";
5532 if ($mode == 0) {
5533 $sql .= " WHERE pa.fk_product_fils = ".((int) $this->id)." OR pa.fk_product_pere = ".((int) $this->id);
5534 } elseif ($mode == -1) {
5535 $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)
5536 } elseif ($mode == 1) {
5537 $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)
5538 }
5539
5540 $resql = $this->db->query($sql);
5541 if ($resql) {
5542 $obj = $this->db->fetch_object($resql);
5543 if ($obj) {
5544 $nb = $obj->nb;
5545 }
5546 } else {
5547 return -1;
5548 }
5549
5550 return $nb;
5551 }
5552
5558 public function hasVariants()
5559 {
5560 $nb = 0;
5561 $sql = "SELECT count(rowid) as nb FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_parent = ".((int) $this->id);
5562 $sql .= " AND entity IN (".getEntity('product').")";
5563
5564 $resql = $this->db->query($sql);
5565 if ($resql) {
5566 $obj = $this->db->fetch_object($resql);
5567 if ($obj) {
5568 $nb = $obj->nb;
5569 }
5570 }
5571
5572 return $nb;
5573 }
5574
5575
5581 public function isVariant()
5582 {
5583 if (isModEnabled('variants')) {
5584 $sql = "SELECT rowid FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_child = ".((int) $this->id)." AND entity IN (".getEntity('product').")";
5585
5586 $query = $this->db->query($sql);
5587
5588 if ($query) {
5589 if (!$this->db->num_rows($query)) {
5590 return false;
5591 }
5592 return true;
5593 } else {
5594 dol_print_error($this->db);
5595 return -1;
5596 }
5597 } else {
5598 return false;
5599 }
5600 }
5601
5608 public function getFather()
5609 {
5610 $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";
5611 $sql .= ", p.tosell as status, p.tobuy as status_buy";
5612 $sql .= " FROM ".$this->db->prefix()."product_association as pa,";
5613 $sql .= " ".$this->db->prefix()."product as p";
5614 $sql .= " WHERE p.rowid = pa.fk_product_pere";
5615 $sql .= " AND pa.fk_product_fils = ".((int) $this->id);
5616
5617 $res = $this->db->query($sql);
5618 if ($res) {
5619 $prods = array();
5620 while ($record = $this->db->fetch_array($res)) {
5621 // $record['id'] = $record['rowid'] = id of father
5622 $prods[$record['id']] = array();
5623 $prods[$record['id']]['id'] = $record['rowid'];
5624 $prods[$record['id']]['ref'] = $record['ref'];
5625 $prods[$record['id']]['label'] = $record['label'];
5626 $prods[$record['id']]['qty'] = $record['qty'];
5627 $prods[$record['id']]['incdec'] = $record['incdec'];
5628 $prods[$record['id']]['fk_product_type'] = $record['fk_product_type'];
5629 $prods[$record['id']]['entity'] = $record['entity'];
5630 $prods[$record['id']]['status'] = $record['status'];
5631 $prods[$record['id']]['status_buy'] = $record['status_buy'];
5632 }
5633 return $prods;
5634 } else {
5635 dol_print_error($this->db);
5636 return -1;
5637 }
5638 }
5639
5640
5650 public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array())
5651 {
5652 if (empty($id)) {
5653 return array();
5654 }
5655
5656 $sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,";
5657 $sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec,";
5658 $sql .= " pa.rowid as fk_association, pa.rang";
5659 $sql .= " FROM ".$this->db->prefix()."product as p,";
5660 $sql .= " ".$this->db->prefix()."product_association as pa";
5661 $sql .= " WHERE p.rowid = pa.fk_product_fils";
5662 $sql .= " AND pa.fk_product_pere = ".((int) $id);
5663 $sql .= " AND pa.fk_product_fils <> ".((int) $id); // This should not happens, it is to avoid infinite loop if it happens
5664 $sql .= " ORDER BY pa.rang";
5665
5666 dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level. ' parents='.(is_array($parents) ? implode(',', $parents) : $parents), LOG_DEBUG);
5667
5668 // Protection against infinite loop
5669 if ($level > 30) {
5670 return array();
5671 }
5672
5673 $res = $this->db->query($sql);
5674 if ($res) {
5675 $prods = array();
5676 if ($this->db->num_rows($res) > 0) {
5677 $parents[] = $id;
5678 }
5679
5680 while ($rec = $this->db->fetch_array($res)) {
5681 if (in_array($rec['id'], $parents)) {
5682 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);
5683 continue; // We discard this child if it is already found at a higher level in tree in the same branch.
5684 }
5685
5686 $prods[$rec['rowid']] = array(
5687 0 => $rec['rowid'],
5688 1 => $rec['qty'],
5689 2 => $rec['fk_product_type'],
5690 3 => $this->db->escape($rec['label']),
5691 4 => $rec['incdec'],
5692 5 => $rec['ref'],
5693 6 => $rec['fk_association'],
5694 7 => $rec['rang']
5695 );
5696 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
5697 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
5698 if (empty($firstlevelonly)) {
5699 $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, $parents);
5700 foreach ($listofchilds as $keyChild => $valueChild) {
5701 $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
5702 }
5703 }
5704 }
5705
5706 return $prods;
5707 } else {
5708 dol_print_error($this->db);
5709 return -1;
5710 }
5711 }
5712
5713 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5720 public function get_sousproduits_arbo()
5721 {
5722 // phpcs:enable
5723 $parent = array();
5724
5725 foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) { // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
5726 $parent[$this->label][$keyChild] = $valueChild;
5727 }
5728 foreach ($parent as $key => $value) { // key=label, value is array of children
5729 $this->sousprods[$key] = $value; // @phan-suppress-current-line PhanTypeMismatchProperty
5730 }
5731 }
5732
5740 public function getTooltipContentArray($params)
5741 {
5742 global $conf, $langs, $user;
5743
5744 $langs->loadLangs(array('products', 'other'));
5745
5746 $datas = array();
5747 $nofetch = !empty($params['nofetch']);
5748
5749 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5750 return ['optimize' => $langs->trans("ShowProduct")];
5751 }
5752
5753 // Does user has permission to read product/service
5754 $permissiontoreadproduct = 0;
5755 if ($this->type == self::TYPE_PRODUCT && $user->hasRight('product', 'read')) {
5756 $permissiontoreadproduct = 1;
5757 }
5758 if ($this->type == self::TYPE_SERVICE && $user->hasRight('service', 'read')) {
5759 $permissiontoreadproduct = 1;
5760 }
5761
5762 if (!empty($this->entity) && $permissiontoreadproduct) {
5763 $tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80, 0, 0, 0, 0, '1');
5764 if ($this->nbphoto > 0) {
5765 $datas['photo'] = '<div class="photointooltip floatright">'."\n" . $tmpphoto . '</div>';
5766 }
5767 }
5768
5769 if ($this->isProduct()) {
5770 $datas['picto'] = img_picto('', 'product').' <u class="paddingrightonly">'.$langs->trans("Product").'</u>';
5771 } elseif ($this->isService()) {
5772 $datas['picto'] = img_picto('', 'service').' <u class="paddingrightonly">'.$langs->trans("Service").'</u>';
5773 }
5774 if (isset($this->status) && isset($this->status_buy)) {
5775 $datas['status'] = ' '.$this->getLibStatut(5, 0) . ' '.$this->getLibStatut(5, 1);
5776 }
5777
5778 if (!empty($this->ref)) {
5779 $datas['ref'] = '<br><b>'.$langs->trans('ProductRef').':</b> '.$this->ref;
5780 }
5781 if (!empty($this->label)) {
5782 $datas['label'] = '<br><b>'.$langs->trans('ProductLabel').':</b> '.$this->label;
5783 }
5784
5785 if ($permissiontoreadproduct) {
5786 if (!empty($this->description)) {
5787 $datas['description'] = '<br><b>'.$langs->trans('ProductDescription').':</b> '.dolGetFirstLineOfText($this->description, 5);
5788 }
5789 if ($this->isStockManaged()) {
5790 if (isModEnabled('productbatch')) {
5791 $langs->load("productbatch");
5792 $datas['batchstatus'] = "<br><b>".$langs->trans("ManageLotSerial").'</b>: '.$this->getLibStatut(0, 2);
5793 if ($this->status_batch) {
5794 $datas['batchdlc'] = "<br><b>".$langs->trans("BatchSellOrEatByMandatoryList", $langs->transnoentitiesnoconv("SellByDate"), $langs->transnoentitiesnoconv("EatByDate")).'</b>: '.$this->getSellOrEatByMandatoryLabel();
5795 }
5796 }
5797 }
5798 if (isModEnabled('barcode')) {
5799 $datas['barcode'] = '<br><b>'.$langs->trans('BarCode').':</b> '.$this->barcode;
5800 }
5801
5802 if ($this->isProduct()) {
5803 if ($this->weight) {
5804 $datas['weight'] = "<br><b>".$langs->trans("Weight").'</b>: '.$this->weight.' '.measuringUnitString(0, "weight", $this->weight_units);
5805 }
5806 $labelsize = "";
5807 if ($this->length) {
5808 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Length").'</b>: '.$this->length.' '.measuringUnitString(0, 'size', $this->length_units);
5809 }
5810 if ($this->width) {
5811 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Width").'</b>: '.$this->width.' '.measuringUnitString(0, 'size', $this->width_units);
5812 }
5813 if ($this->height) {
5814 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Height").'</b>: '.$this->height.' '.measuringUnitString(0, 'size', $this->height_units);
5815 }
5816 if ($labelsize) {
5817 $datas['size'] = "<br>".$labelsize;
5818 }
5819
5820 $labelsurfacevolume = "";
5821 if ($this->surface) {
5822 $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Surface").'</b>: '.$this->surface.' '.measuringUnitString(0, 'surface', $this->surface_units);
5823 }
5824 if ($this->volume) {
5825 $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Volume").'</b>: '.$this->volume.' '.measuringUnitString(0, 'volume', $this->volume_units);
5826 }
5827 if ($labelsurfacevolume) {
5828 $datas['surface'] = "<br>" . $labelsurfacevolume;
5829 }
5830 }
5831 if ($this->isService() && !empty($this->duration_value)) {
5832 // Duration
5833 $datas['duration'] = '<br><b>'.$langs->trans("Duration").':</b> '.$this->duration_value;
5834 if ($this->duration_value > 1) {
5835 $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"));
5836 } elseif ($this->duration_value > 0) {
5837 $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"));
5838 }
5839 $datas['duration'] .= (!empty($this->duration_unit) && isset($dur[$this->duration_unit]) ? "&nbsp;".$langs->trans($dur[$this->duration_unit]) : '');
5840 }
5841 if (empty($user->socid)) {
5842 if ($this->isStockManaged() && !empty($this->pmp) && $this->pmp) {
5843 $datas['pmp'] = "<br><b>".$langs->trans("PMPValue").'</b>: '.price($this->pmp, 0, '', 1, -1, -1, $conf->currency);
5844 }
5845
5846 if (isModEnabled('accounting')) {
5847 if ($this->status && isset($this->accountancy_code_sell)) {
5848 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5849 $selllabel = '<br>';
5850 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellCode').':</b> '.length_accountg($this->accountancy_code_sell);
5851 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellIntraCode').':</b> '.length_accountg($this->accountancy_code_sell_intra);
5852 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellExportCode').':</b> '.length_accountg($this->accountancy_code_sell_export);
5853 $datas['accountancysell'] = $selllabel;
5854 }
5855 if ($this->status_buy && isset($this->accountancy_code_buy)) {
5856 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5857 $buylabel = '';
5858 if (empty($this->status)) {
5859 $buylabel .= '<br>';
5860 }
5861 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyCode').':</b> '.length_accountg($this->accountancy_code_buy);
5862 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyIntraCode').':</b> '.length_accountg($this->accountancy_code_buy_intra);
5863 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyExportCode').':</b> '.length_accountg($this->accountancy_code_buy_export);
5864 $datas['accountancybuy'] = $buylabel;
5865 }
5866 }
5867 }
5868 // show categories for this record only in ajax to not overload lists
5869 if (isModEnabled('category') && !$nofetch) {
5870 require_once DOL_DOCUMENT_ROOT . '/categories/class/categorie.class.php';
5871 $form = new Form($this->db);
5872 $datas['categories'] = '<br>' . $form->showCategories($this->id, Categorie::TYPE_PRODUCT, 1);
5873 }
5874 }
5875
5876 return $datas;
5877 }
5878
5892 public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0, $morecss = '', $add_label = 0, $sep = ' - ')
5893 {
5894 global $langs, $hookmanager;
5895
5896 include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
5897
5898 $result = '';
5899
5900 $newref = $this->ref;
5901 if ($maxlength) {
5902 $newref = dol_trunc($newref, $maxlength, 'middle');
5903 }
5904 $params = [
5905 'id' => $this->id,
5906 'objecttype' => ($this->type == 1 ? 'service' : 'product'),
5907 'option' => $option,
5908 'nofetch' => 1,
5909 ];
5910 $classfortooltip = 'classfortooltip';
5911 $dataparams = '';
5912 if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
5913 $classfortooltip = 'classforajaxtooltip';
5914 $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
5915 $label = '';
5916 } else {
5917 $label = implode($this->getTooltipContentArray($params));
5918 }
5919
5920 $linkclose = '';
5921 if (empty($notooltip)) {
5922 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5923 $label = $langs->trans("ShowProduct");
5924 $linkclose .= ' alt="'.dol_escape_htmltag($label, 1, 1).'"';
5925 }
5926 $linkclose .= ($label ? ' title="'.dol_escape_htmltag($label, 1, 1).'"' : ' title="tocomplete"');
5927 $linkclose .= $dataparams.' class="nowraponall '.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
5928 } else {
5929 $linkclose = ' class="nowraponall'.($morecss ? ' '.$morecss : '').'"';
5930 }
5931
5932 if ($option == 'supplier' || $option == 'category') {
5933 $url = DOL_URL_ROOT.'/product/price_suppliers.php?id='.$this->id;
5934 } elseif ($option == 'stock') {
5935 $url = DOL_URL_ROOT.'/product/stock/product.php?id='.$this->id;
5936 } elseif ($option == 'composition') {
5937 $url = DOL_URL_ROOT.'/product/composition/card.php?id='.$this->id;
5938 } else {
5939 $url = DOL_URL_ROOT.'/product/card.php?id='.$this->id;
5940 }
5941
5942 if ($option !== 'nolink') {
5943 // Add param to save lastsearch_values or not
5944 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
5945 if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
5946 $add_save_lastsearch_values = 1;
5947 }
5948 if ($add_save_lastsearch_values) {
5949 $url .= '&save_lastsearch_values=1';
5950 }
5951 }
5952
5953 $linkstart = '<a href="'.$url.'"';
5954 $linkstart .= $linkclose.'>';
5955 $linkend = '</a>';
5956
5957 $result .= $linkstart;
5958 if ($withpicto) {
5959 if ($this->isProduct()) {
5960 $result .= (img_object(($notooltip ? '' : $label), 'product', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5961 }
5962 if ($this->isService()) {
5963 $result .= (img_object(($notooltip ? '' : $label), 'service', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5964 }
5965 }
5966 $result .= '<span class="aaa">'.dol_escape_htmltag($newref).'</span>';
5967 $result .= $linkend;
5968 if ($withpicto != 2) {
5969 $result .= (($add_label && $this->label) ? $sep.dol_trunc($this->label, ($add_label > 1 ? $add_label : 0)) : '');
5970 }
5971
5972 global $action;
5973 $hookmanager->initHooks(array('productdao'));
5974 $parameters = array('id' => $this->id, 'getnomurl' => &$result, 'label' => &$label);
5975 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
5976 if ($reshook > 0) {
5977 $result = $hookmanager->resPrint;
5978 } else {
5979 $result .= $hookmanager->resPrint;
5980 }
5981
5982 return $result;
5983 }
5984
5985
5996 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
5997 {
5998 global $langs;
5999
6000 $langs->load("products");
6001 $outputlangs->load("products");
6002
6003 // Positionne le modele sur le nom du modele a utiliser
6004 if (!dol_strlen($modele)) {
6005 $modele = getDolGlobalString('PRODUCT_ADDON_PDF', 'strato');
6006 }
6007
6008 $modelpath = "core/modules/product/doc/";
6009
6010 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
6011 }
6012
6020 public function getLibStatut($mode = 0, $type = 0)
6021 {
6022 switch ($type) {
6023 case 0:
6024 return $this->LibStatut($this->status, $mode, $type);
6025 case 1:
6026 return $this->LibStatut($this->status_buy, $mode, $type);
6027 case 2:
6028 return $this->LibStatut($this->status_batch, $mode, $type);
6029 default:
6030 //Simulate previous behavior but should return an error string
6031 return $this->LibStatut($this->status_buy, $mode, $type);
6032 }
6033 }
6034
6035 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6044 public function LibStatut($status, $mode = 0, $type = 0)
6045 {
6046 // phpcs:enable
6047 global $langs;
6048
6049 $labelStatus = $labelStatusShort = '';
6050
6051 $langs->load('products');
6052 if (isModEnabled('productbatch')) {
6053 $langs->load("productbatch");
6054 }
6055
6056 if ($type == 2) {
6057 switch ($mode) {
6058 case 0:
6059 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatch') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial')));
6060 return dolGetStatus($label);
6061 case 1:
6062 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort')));
6063 return dolGetStatus($label);
6064 case 2:
6065 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 1, 2);
6066 case 3:
6067 return dolGetStatus($langs->transnoentitiesnoconv('ProductStatusNotOnBatch'), '', '', empty($status) ? 'status5' : 'status4', 3, 'dot');
6068 case 4:
6069 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 0, 2);
6070 case 5:
6071 return $this->LibStatut($status, 1, 2).' '.$this->LibStatut($status, 3, 2);
6072 default:
6073 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'));
6074 }
6075 }
6076
6077 $statuttrans = empty($status) ? 'status5' : 'status4';
6078
6079 if ($status == 0) {
6080 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6081 if ($type == 0) {
6082 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnSellShort');
6083 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnSell');
6084 } elseif ($type == 1) {
6085 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBuyShort');
6086 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBuy');
6087 } elseif ($type == 2) {
6088 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBatch');
6089 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort');
6090 }
6091 } elseif ($status == 1) {
6092 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
6093 if ($type == 0) {
6094 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSellShort');
6095 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSell');
6096 } elseif ($type == 1) {
6097 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBuyShort');
6098 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBuy');
6099 } elseif ($type == 2) {
6100 $labelStatus = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial'));
6101 $labelStatusShort = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort'));
6102 }
6103 } elseif ($type == 2 && $status == 2) {
6104 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSerial');
6105 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSerialShort');
6106 }
6107
6108 if ($mode > 6) {
6109 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'), '', '', 'status0', 0);
6110 } else {
6111 return dolGetStatus($labelStatus, $labelStatusShort, '', $statuttrans, $mode);
6112 }
6113 }
6114
6115
6121 public function getLibFinished()
6122 {
6123 global $langs;
6124
6125 $langs->load('products');
6126 $label = '';
6127
6128 if (isset($this->finished) && $this->finished >= 0) {
6129 $sql = "SELECT label, code FROM ".$this->db->prefix()."c_product_nature where code = ".((int) $this->finished)." AND active=1";
6130 $resql = $this->db->query($sql);
6131 if (!$resql) {
6132 $this->error = $this->db->error().' sql='.$sql;
6133 dol_syslog(__METHOD__.' Error '.$this->error, LOG_ERR);
6134 return -1;
6135 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
6136 $label = $langs->trans($res['label']);
6137 }
6138 $this->db->free($resql);
6139 }
6140
6141 return $label;
6142 }
6143
6144
6145 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6162 public function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null)
6163 {
6164 // phpcs:enable
6165 if ($id_entrepot) {
6166 $this->db->begin();
6167
6168 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
6169
6170 // Ensure $nbpiece is a number and positive
6171 $nbpiece = (float) $nbpiece;
6172 if ($nbpiece < 0) {
6173 if (!$movement) {
6174 $movement = 1;
6175 }
6176 $nbpiece = abs($nbpiece);
6177 }
6178 $op = array();
6179 $op[0] = $nbpiece;
6180 $op[1] = -$nbpiece;
6181
6182 $movementstock = new MouvementStock($this->db);
6183 $movementstock->setOrigin($origin_element, (int) $origin_id); // Set ->origin_type and ->origin_id
6184 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', '', '', '', false, 0, $disablestockchangeforsubproduct);
6185
6186 if ($result >= 0) {
6187 if ($extrafields) {
6188 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
6189 $movementstock->array_options = $array_options;
6190 $movementstock->insertExtraFields();
6191 }
6192 $this->db->commit();
6193 return 1;
6194 } else {
6195 $this->error = $movementstock->error;
6196 $this->errors = $movementstock->errors;
6197
6198 $this->db->rollback();
6199 return -1;
6200 }
6201 }
6202
6203 return -1;
6204 }
6205
6206 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6227 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)
6228 {
6229 // phpcs:enable
6230 if ($id_entrepot) {
6231 $this->db->begin();
6232
6233 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
6234
6235 // Ensure $nbpiece is a number and positive
6236 $nbpiece = (float) $nbpiece;
6237 if ($nbpiece < 0) {
6238 if (!$movement) {
6239 $movement = 1;
6240 }
6241 $nbpiece = abs($nbpiece);
6242 }
6243
6244 $op = array();
6245 $op[0] = $nbpiece;
6246 $op[1] = -$nbpiece;
6247
6248 $movementstock = new MouvementStock($this->db);
6249 $movementstock->setOrigin($origin_element, (int) $origin_id); // Set ->origin_type and ->fk_origin
6250 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', $dlc, $dluo, $lot, false, 0, $disablestockchangeforsubproduct, 0, $force_update_batch);
6251
6252 if ($result >= 0) {
6253 if ($extrafields) {
6254 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
6255 $movementstock->array_options = $array_options;
6256 $movementstock->insertExtraFields();
6257 }
6258 $this->db->commit();
6259 return 1;
6260 } else {
6261 $this->error = $movementstock->error;
6262 $this->errors = $movementstock->errors;
6263
6264 $this->db->rollback();
6265 return -1;
6266 }
6267 }
6268 return -1;
6269 }
6270
6271 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6284 public function load_stock($option = '', $includedraftpoforvirtual = null, $dateofvirtualstock = null)
6285 {
6286 // phpcs:enable
6287 $this->stock_reel = 0;
6288 $this->stock_warehouse = array();
6289 $this->stock_theorique = 0;
6290
6291 // Set filter on warehouse status
6292 $warehouseStatus = array();
6293 if (preg_match('/warehouseclosed/', $option)) {
6295 }
6296 if (preg_match('/warehouseopen/', $option)) {
6298 }
6299 if (preg_match('/warehouseinternal/', $option)) {
6300 if (getDolGlobalString('ENTREPOT_EXTRA_STATUS')) {
6302 } else {
6304 }
6305 }
6306
6307 $sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot";
6308 $sql .= " FROM ".$this->db->prefix()."product_stock as ps";
6309 $sql .= ", ".$this->db->prefix()."entrepot as w";
6310 $sql .= " WHERE w.entity IN (".getEntity('stock').")";
6311 $sql .= " AND w.rowid = ps.fk_entrepot";
6312 $sql .= " AND ps.fk_product = ".((int) $this->id);
6313 if (count($warehouseStatus)) {
6314 $sql .= " AND w.statut IN (".$this->db->sanitize(implode(',', $warehouseStatus)).")";
6315 }
6316
6317 $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;
6318
6319 dol_syslog(get_class($this)."::load_stock", LOG_DEBUG);
6320 $result = $this->db->query($sql);
6321 if ($result) {
6322 $num = $this->db->num_rows($result);
6323 $i = 0;
6324 if ($num > 0) {
6325 while ($i < $num) {
6326 $row = $this->db->fetch_object($result);
6327 $this->stock_warehouse[$row->fk_entrepot] = new stdClass();
6328 $this->stock_warehouse[$row->fk_entrepot]->real = $row->reel;
6329 $this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid;
6330 if ((!preg_match('/nobatch/', $option)) && $this->hasbatch()) {
6331 $this->stock_warehouse[$row->fk_entrepot]->detail_batch = Productbatch::findAll($this->db, $row->rowid, 1, $this->id);
6332 }
6333 $this->stock_reel += $row->reel;
6334 $i++;
6335 }
6336 $this->stock_reel = (float) price2num($this->stock_reel, 'MS');
6337 }
6338 $this->db->free($result);
6339
6340 if (!preg_match('/novirtual/', $option)) {
6341 $this->load_virtual_stock($includedraftpoforvirtual, $dateofvirtualstock); // This load stock_theorique and also load all arrays stats_xxx...
6342 }
6343
6344 return 1;
6345 } else {
6346 $this->error = $this->db->lasterror();
6347 return -1;
6348 }
6349 }
6350
6351
6352 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6362 public function load_virtual_stock($includedraftpoforvirtual = null, $dateofvirtualstock = null)
6363 {
6364 // phpcs:enable
6365 global $hookmanager, $action;
6366
6367 $stock_commande_client = 0;
6368 $stock_commande_fournisseur = 0;
6369 $stock_sending_client = 0;
6370 $stock_reception_fournisseur = 0;
6371 $stock_inproduction = 0;
6372
6373 //dol_syslog("load_virtual_stock");
6374
6375 if (isModEnabled('order')) {
6376 $result = $this->load_stats_commande(0, '1,2', 1);
6377 if ($result < 0) {
6378 dol_print_error($this->db, $this->error);
6379 }
6380 $stock_commande_client = $this->stats_commande['qty'];
6381 }
6382 if (isModEnabled("shipping")) {
6383 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
6384 $filterShipmentStatus = '';
6385 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
6386 $filterShipmentStatus = Expedition::STATUS_VALIDATED.','.Expedition::STATUS_CLOSED;
6387 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6388 $filterShipmentStatus = Expedition::STATUS_CLOSED;
6389 }
6390 $result = $this->load_stats_sending(0, '1,2', 1, $filterShipmentStatus);
6391 if ($result < 0) {
6392 dol_print_error($this->db, $this->error);
6393 }
6394 $stock_sending_client = $this->stats_expedition['qty'];
6395 }
6396 // Include supplier order lines
6397 if (isModEnabled("supplier_order")) {
6398 $filterStatus = getDolGlobalString('SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK', '3,4');
6399 if (isset($includedraftpoforvirtual)) {
6400 $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
6401 }
6402 $result = $this->load_stats_commande_fournisseur(0, $filterStatus, 1, $dateofvirtualstock);
6403 if ($result < 0) {
6404 dol_print_error($this->db, $this->error);
6405 }
6406 $stock_commande_fournisseur = $this->stats_commande_fournisseur['qty'];
6407 }
6408 // Include reception lines
6409 if (isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) {
6410 $filterStatus = '4';
6411 if (isset($includedraftpoforvirtual)) {
6412 $filterStatus = '0,'.$filterStatus;
6413 }
6414 $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock);
6415 if ($result < 0) {
6416 dol_print_error($this->db, $this->error);
6417 }
6418 $stock_reception_fournisseur = $this->stats_reception['qty'];
6419 }
6420 // Include manufacturing
6421 if (isModEnabled('mrp')) {
6422 $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock);
6423 if ($result < 0) {
6424 dol_print_error($this->db, $this->error);
6425 }
6426 $stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty'];
6427 }
6428
6429 $this->stock_theorique = $this->stock_reel + $stock_inproduction;
6430
6431 // $weBillOrderOrShipmentReception is set to 'order' or 'shipmentreception'. it will be used to know how to make virtual stock
6432 // calculation when we have a stock increase or decrease on billing. Do we have to count orders to bill or shipment/reception to bill ?
6433 $weBillOrderOrShipmentReception = getDolGlobalString('STOCK_DO_WE_BILL_ORDER_OR_SHIPMENTECEPTION_FOR_VIRTUALSTOCK', 'order');
6434
6435 // Stock decrease mode
6436 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') || getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6437 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6438 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_VALIDATE_ORDER')) {
6439 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
6440 $tmpnewprod = dol_clone($this, 1);
6441 $result = $tmpnewprod->load_stats_commande(0, '0', 1); // Get qty in draft orders
6442 $this->stock_theorique += $tmpnewprod->stats_commande['qty'];
6443 }
6444 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'order') {
6445 $this->stock_theorique -= $stock_commande_client;
6446 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6447 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6448 }
6449
6450 // Stock Increase mode
6451 if (getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION') || getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION_CLOSE')) {
6452 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6453 } 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
6454 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6455 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER')) { // Warning: stock change "on approval", not on validation !
6456 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
6457 $tmpnewprod = dol_clone($this, 1);
6458 $result = $tmpnewprod->load_stats_commande_fournisseur(0, '0', 1); // Get qty in draft orders
6459 $this->stock_theorique += $this->stats_commande_fournisseur['qty'];
6460 }
6461 $this->stock_theorique -= $stock_reception_fournisseur;
6462 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'order') {
6463 $this->stock_theorique += $stock_commande_fournisseur;
6464 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL') && $weBillOrderOrShipmentReception == 'shipmentreception') {
6465 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6466 }
6467
6468 $parameters = array('id' => $this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual);
6469 // Note that $action and $object may have been modified by some hooks
6470 $reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action);
6471 if ($reshook > 0) {
6472 $this->stock_theorique = $hookmanager->resArray['stock_theorique'];
6473 } elseif ($reshook == 0 && isset($hookmanager->resArray['stock_stats_hook'])) {
6474 $this->stock_theorique += $hookmanager->resArray['stock_stats_hook'];
6475 }
6476
6477 //Virtual Stock by Warehouse
6478 if (!empty($this->stock_warehouse) && getDolGlobalString('STOCK_ALLOW_VIRTUAL_STOCK_PER_WAREHOUSE')) {
6479 foreach ($this->stock_warehouse as $warehouseid => $stockwarehouse) {
6480 if (isModEnabled('mrp')) {
6481 $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock, $warehouseid);
6482 if ($result < 0) {
6483 dol_print_error($this->db, $this->error);
6484 }
6485 }
6486
6487 if ($this->fk_default_warehouse == $warehouseid) {
6488 $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']);
6489 } else {
6490 $this->stock_warehouse[$warehouseid]->virtual = $this->stock_warehouse[$warehouseid]->real + $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'];
6491 }
6492 }
6493 }
6494
6495 return 1;
6496 }
6497
6505 public function loadStockForVirtualProduct($option = '', $qtyWish = 1)
6506 {
6507 $this->stock_warehouse = array();
6508 $error = 0;
6509
6510 $this->get_sousproduits_arbo();
6511 $prods_arbo = $this->get_arbo_each_prod($qtyWish, 1);
6512 if (count($prods_arbo) > 0) {
6513 $productCachedList = array();
6514 $stockByComponentList = array();
6515
6516 foreach ($prods_arbo as $componentArr) {
6517 $componentId = $componentArr['id'];
6518 // only component whose manage stock
6519 if ($componentArr['incdec'] == 1) {
6520 if (!isset($productCachedList[$componentId])) {
6521 $componentStatic = new self($this->db);
6522 $componentStatic->fetch($componentId);
6523 // check if it's a sub-kit
6524 $childrenNb = $componentStatic->hasFatherOrChild(1);
6525 if ($childrenNb == 0) {
6526 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6527 if (!isset($stockByComponentList[$componentId])) {
6528 $stockByComponentList[$componentId] = array(
6529 'qty_need' => 0
6530 );
6531 }
6532 $stockByComponentList[$componentId]['qty_need'] += $componentArr['nb_total'];
6533 }
6534 $productCachedList[$componentId] = $componentStatic;
6535 }
6536 }
6537 }
6538
6539 if (!empty($stockByComponentList)) {
6540 foreach ($stockByComponentList as $componentId => $stockByComponentArr) {
6541 if (!isset($productCachedList[$componentId])) {
6542 $componentStatic = new self($this->db);
6543 $componentStatic->fetch($componentId);
6544 $componentStatic->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
6545 $productCachedList[$componentId] = $componentStatic;
6546 }
6547 $component = $productCachedList[$componentId];
6548
6549
6550 if ($component->stock_reel < $stockByComponentArr['qty_need']) {
6551 // not enough stock for this component to assemble this virtual product
6552 $error++;
6553 $this->error = 'Not enough component [id='.$componentId.'] in stock, real='.$component->stock_reel.' and need='.$stockByComponentArr['qty_need'];
6554 $this->errors[] = $this->error;
6555 dol_syslog(__METHOD__.' : '.$this->error, LOG_ERR);
6556 } else {
6557 if (!empty($component->stock_warehouse)) {
6558 foreach ($component->stock_warehouse as $warehouseId => $warehouseObj) {
6559 $kitWarehouseAvailable = new stdClass();
6560 $kitWarehouseAvailable->id = $warehouseObj->id;
6561 $kitWarehouseAvailable->real = $qtyWish;
6562 $this->stock_warehouse[$warehouseId] = $kitWarehouseAvailable;
6563 }
6564 }
6565 }
6566
6567 if ($error) {
6568 break;
6569 }
6570 }
6571 } elseif (getDolGlobalInt('PRODUIT_SOUSPRODUITS_ALSO_ENABLE_PARENT_STOCK_MOVE') && empty($productCachedList)) {
6572 // if all sub product are not stock managed when use parent stock
6573 $this->load_stock('warehouseopen');
6574 }
6575 }
6576
6577 if ($error) {
6578 return -1;
6579 } else {
6580 return 1;
6581 }
6582 }
6583
6591 public function loadBatchInfo($batch)
6592 {
6593 $result = array();
6594
6595 $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";
6596 $sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = ".((int) $this->id)." AND pb.batch = '".$this->db->escape($batch)."'";
6597 $sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby";
6598 dol_syslog(get_class($this)."::loadBatchInfo load first entry found for lot/serial = ".$batch, LOG_DEBUG);
6599 $resql = $this->db->query($sql);
6600 if ($resql) {
6601 $num = $this->db->num_rows($resql);
6602 $i = 0;
6603 while ($i < $num) {
6604 $obj = $this->db->fetch_object($resql);
6605 $result[] = array('batch' => $batch, 'eatby' => $this->db->jdate($obj->eatby), 'sellby' => $this->db->jdate($obj->sellby), 'qty' => $obj->qty);
6606 $i++;
6607 }
6608 return $result;
6609 } else {
6610 dol_print_error($this->db);
6611 $this->db->rollback();
6612 return array();
6613 }
6614 }
6615
6616 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6625 public function add_photo($sdir, $file)
6626 {
6627 // phpcs:enable
6628 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6629
6630 $result = 0;
6631
6632 $dir = $sdir;
6633 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6634 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos";
6635 } else {
6636 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product').dol_sanitizeFileName($this->ref);
6637 }
6638
6639 dol_mkdir($dir);
6640
6641 $dir_osencoded = $dir;
6642
6643 if (is_dir($dir_osencoded)) {
6644 $originImage = $dir.'/'.$file['name'];
6645
6646 // Cree fichier en taille origine
6647 $result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
6648
6649 if (file_exists(dol_osencode($originImage))) {
6650 // Create thumbs
6651 $this->addThumbs($originImage);
6652 }
6653 }
6654
6655 if (is_numeric($result) && $result > 0) {
6656 return 1;
6657 } else {
6658 return -1;
6659 }
6660 }
6661
6662 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6669 public function is_photo_available($sdir)
6670 {
6671 // phpcs:enable
6672 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6673 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6674
6675 $dir = $sdir;
6676 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6677 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos/";
6678 } else {
6679 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product');
6680 }
6681
6682 $dir_osencoded = dol_osencode($dir);
6683 if (file_exists($dir_osencoded)) {
6684 $handle = opendir($dir_osencoded);
6685 if (is_resource($handle)) {
6686 while (($file = readdir($handle)) !== false) {
6687 if (!utf8_check($file)) {
6688 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // To be sure data is stored in UTF8 in memory
6689 }
6690 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6691 return true;
6692 }
6693 }
6694 }
6695 }
6696
6697 return false;
6698 }
6699
6700 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6708 public function liste_photos($dir, $nbmax = 0)
6709 {
6710 // phpcs:enable
6711 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6712 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6713
6714 $nbphoto = 0;
6715 $tabobj = array();
6716
6717 $dir_osencoded = dol_osencode($dir);
6718 $handle = @opendir($dir_osencoded);
6719 if (is_resource($handle)) {
6720 while (($file = readdir($handle)) !== false) {
6721 if (!utf8_check($file)) {
6722 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // readdir returns ISO
6723 }
6724 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6725 $nbphoto++;
6726
6727 // We forge name of thumb.
6728 $photo = $file;
6729 $photo_vignette = '';
6730 $regs = array();
6731 if (preg_match('/('.$this->regeximgext.')$/i', $photo, $regs)) {
6732 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $photo).'_small'.$regs[0];
6733 }
6734
6735 $dirthumb = $dir.'thumbs/';
6736
6737 // Object
6738 $obj = array();
6739 $obj['photo'] = $photo;
6740 if ($photo_vignette && dol_is_file($dirthumb.$photo_vignette)) {
6741 $obj['photo_vignette'] = 'thumbs/'.$photo_vignette;
6742 } else {
6743 $obj['photo_vignette'] = "";
6744 }
6745
6746 $tabobj[$nbphoto - 1] = $obj;
6747
6748 // Do we have to continue with next photo ?
6749 if ($nbmax && $nbphoto >= $nbmax) {
6750 break;
6751 }
6752 }
6753 }
6754
6755 closedir($handle);
6756 }
6757
6758 return $tabobj;
6759 }
6760
6761 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6768 public function delete_photo($file)
6769 {
6770 // phpcs:enable
6771 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6772 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6773
6774 $dir = dirname($file).'/'; // Chemin du dossier contenant l'image d'origine
6775 $dirthumb = $dir.'/thumbs/'; // Chemin du dossier contenant la vignette
6776 $filename = preg_replace('/'.preg_quote($dir, '/').'/i', '', $file); // Nom du fichier
6777
6778 // On efface l'image d'origine
6779 dol_delete_file($file, 0, 0, 0, $this); // For triggers
6780
6781 // Si elle existe, on efface la vignette
6782 if (preg_match('/('.$this->regeximgext.')$/i', $filename, $regs)) {
6783 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_small'.$regs[0];
6784 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6785 dol_delete_file($dirthumb.$photo_vignette);
6786 }
6787
6788 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_mini'.$regs[0];
6789 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6790 dol_delete_file($dirthumb.$photo_vignette);
6791 }
6792 }
6793 }
6794
6795 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6802 public function get_image_size($file)
6803 {
6804 // phpcs:enable
6805 $file_osencoded = dol_osencode($file);
6806 $infoImg = getimagesize($file_osencoded); // Get information on image
6807 $this->imgWidth = $infoImg[0]; // Largeur de l'image
6808 $this->imgHeight = $infoImg[1]; // Hauteur de l'image
6809 }
6810
6816 public function loadStateBoard()
6817 {
6818 global $hookmanager;
6819
6820 $this->nb = array();
6821
6822 $sql = "SELECT count(p.rowid) as nb, fk_product_type";
6823 $sql .= " FROM ".$this->db->prefix()."product as p";
6824 $sql .= ' WHERE p.entity IN ('.getEntity($this->element, 1).')';
6825 // Add where from hooks
6826 if (is_object($hookmanager)) {
6827 $parameters = array();
6828 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
6829 $sql .= $hookmanager->resPrint;
6830 }
6831 $sql .= ' GROUP BY fk_product_type';
6832
6833 $resql = $this->db->query($sql);
6834 if ($resql) {
6835 while ($obj = $this->db->fetch_object($resql)) {
6836 if ($obj->fk_product_type == 1) {
6837 $this->nb["services"] = $obj->nb;
6838 } else {
6839 $this->nb["products"] = $obj->nb;
6840 }
6841 }
6842 $this->db->free($resql);
6843 return 1;
6844 } else {
6845 dol_print_error($this->db);
6846 $this->error = $this->db->error();
6847 return -1;
6848 }
6849 }
6850
6856 public function isProduct()
6857 {
6858 return $this->type == Product::TYPE_PRODUCT;
6859 }
6860
6866 public function isService()
6867 {
6868 return $this->type == Product::TYPE_SERVICE;
6869 }
6870
6876 public function isStockManaged()
6877 {
6878 return (($this->isProduct() || ($this->isService() && getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) && ($this->stockable_product > 0));
6879 }
6880
6886 public function isMandatoryPeriod()
6887 {
6888 return $this->mandatory_period == 1;
6889 }
6890
6896 public function hasbatch()
6897 {
6898 return $this->status_batch > 0;
6899 }
6900
6901
6902 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6911 public function get_barcode($object, $type = '')
6912 {
6913 // phpcs:enable
6914 global $conf;
6915
6916 $result = '';
6917 if (getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
6918 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
6919 foreach ($dirsociete as $dirroot) {
6920 $res = dol_include_once($dirroot . getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM').'.php');
6921 if ($res) {
6922 break;
6923 }
6924 }
6925 $var = getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM');
6926 $mod = new $var();
6927 '@phan-var-force ModeleNumRefBarCode $mod';
6928
6929 $result = $mod->getNextValue($object, $type);
6930
6931 dol_syslog(get_class($this)."::get_barcode barcode=".$result." module=".$var);
6932 }
6933 return $result;
6934 }
6935
6943 public function initAsSpecimen()
6944 {
6945 $now = dol_now();
6946
6947 // Initialize parameters
6948 $this->specimen = 1;
6949 $this->id = 0;
6950 $this->ref = 'PRODUCT_SPEC';
6951 $this->label = 'PRODUCT SPECIMEN';
6952 $this->description = 'This is description of this product specimen that was created the '.dol_print_date($now, 'dayhourlog').'.';
6953 $this->specimen = 1;
6954 $this->country_id = 1;
6955 $this->status = 1;
6956 $this->status_buy = 1;
6957 $this->tobatch = 0;
6958 $this->sell_or_eat_by_mandatory = 0;
6959 $this->note_private = 'This is a comment (private)';
6960 $this->note_public = 'This is a comment (public)';
6961 $this->date_creation = $now;
6962 $this->date_modification = $now;
6963
6964 $this->weight = 4;
6965 $this->weight_units = 3;
6966
6967 $this->length = 5;
6968 $this->length_units = 1;
6969 $this->width = 6;
6970 $this->width_units = 0;
6971 $this->height = null;
6972 $this->height_units = null;
6973
6974 $this->surface = 30;
6975 $this->surface_units = 0;
6976 $this->volume = 300;
6977 $this->volume_units = 0;
6978
6979 $this->barcode = -1; // Create barcode automatically
6980
6981 return 1;
6982 }
6983
6993 public function getLabelOfUnit($type = 'long', $outputlangs = null, $noentities = 0)
6994 {
6995 global $langs;
6996
6997 if (empty($this->fk_unit)) {
6998 return '';
6999 }
7000 if (empty($outputlangs)) {
7001 $outputlangs = $langs;
7002 }
7003
7004 $outputlangs->load('products');
7005 $label = '';
7006
7007 $sql = "SELECT code, label, short_label FROM ".$this->db->prefix()."c_units where rowid = ".((int) $this->fk_unit);
7008
7009 $resql = $this->db->query($sql);
7010 if (!$resql) {
7011 $this->error = $this->db->error();
7012 dol_syslog(get_class($this)."::getLabelOfUnit Error ".$this->error, LOG_ERR);
7013 return -1;
7014 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
7015 if ($type == 'short') {
7016 if ($noentities) {
7017 $label = $outputlangs->transnoentitiesnoconv($res['short_label']);
7018 } else {
7019 $label = $outputlangs->trans($res['short_label']);
7020 }
7021 } elseif ($type == 'code') {
7022 $label = $res['code'];
7023 } else {
7024 if ($outputlangs->trans('unit'.$res['code']) == 'unit'.$res['code']) {
7025 // No translation available
7026 $label = $res['label'];
7027 } else {
7028 // Return the translated value
7029 if ($noentities) {
7030 $label = $outputlangs->transnoentitiesnoconv('unit'.$res['code']);
7031 } else {
7032 $label = $outputlangs->trans('unit'.$res['code']);
7033 }
7034 }
7035 }
7036 }
7037 $this->db->free($resql);
7038
7039 return $label;
7040 }
7041
7042 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
7048 public function min_recommended_price()
7049 {
7050 // phpcs:enable
7051 $maxpricesupplier = 0;
7052
7053 if (getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE')) {
7054 include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
7055 $product_fourn = new ProductFournisseur($this->db);
7056 $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', '');
7057
7058 if (is_array($product_fourn_list) && count($product_fourn_list) > 0) {
7059 foreach ($product_fourn_list as $productfourn) {
7060 if ($productfourn->fourn_unitprice > $maxpricesupplier) {
7061 $maxpricesupplier = $productfourn->fourn_unitprice;
7062 }
7063 }
7064
7065 $maxpricesupplier *= getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE');
7066 }
7067 }
7068
7069 return $maxpricesupplier;
7070 }
7071
7072
7083 public function setCategories($categories)
7084 {
7085 require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
7086 return parent::setCategoriesCommon($categories, Categorie::TYPE_PRODUCT);
7087 }
7088
7097 public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
7098 {
7099 $tables = array(
7100 'product_customer_price',
7101 'product_customer_price_log'
7102 );
7103
7104 return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
7105 }
7106
7118 public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
7119 {
7120 $sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM ".$this->db->prefix()."product_pricerules";
7121 $query = $this->db->query($sql);
7122
7123 $rules = array();
7124
7125 while ($result = $this->db->fetch_object($query)) {
7126 $rules[$result->level] = $result;
7127 }
7128
7129 //Because prices can be based on other level's prices, we temporarily store them
7130 $prices = array(
7131 1 => $baseprice
7132 );
7133
7134 $nbofproducts = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
7135 for ($i = 1; $i <= $nbofproducts; $i++) {
7136 $price = $baseprice;
7137 $price_min = $baseprice;
7138
7139 //We have to make sure it does exist and it is > 0
7140 //First price level only allows changing min_price
7141 if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) {
7142 $price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent / 100));
7143 }
7144
7145 $prices[$i] = $price;
7146
7147 //We have to make sure it does exist and it is > 0
7148 if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) {
7149 $price_min = $price * (1 - ($rules[$i]->var_min_percent / 100));
7150 }
7151
7152 //Little check to make sure the price is modified before triggering generation
7153 $check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i]));
7154 $check_type = ($baseprice == $this->multiprices_base_type[$i]);
7155
7156 if ($check_amount && $check_type) {
7157 continue;
7158 }
7159
7160 if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, 1) < 0) {
7161 return -1;
7162 }
7163 }
7164
7165 return 1;
7166 }
7167
7173 public function getRights()
7174 {
7175 global $user;
7176
7177 if ($this->isProduct()) {
7178 return $user->rights->produit;
7179 } else {
7180 return $user->rights->service;
7181 }
7182 }
7183
7190 public function info($id)
7191 {
7192 $sql = "SELECT p.rowid, p.ref, p.datec as date_creation, p.tms as date_modification,";
7193 $sql .= " p.fk_user_author, p.fk_user_modif";
7194 $sql .= " FROM ".$this->db->prefix().$this->table_element." as p";
7195 $sql .= " WHERE p.rowid = ".((int) $id);
7196
7197 $result = $this->db->query($sql);
7198 if ($result) {
7199 if ($this->db->num_rows($result)) {
7200 $obj = $this->db->fetch_object($result);
7201
7202 $this->id = $obj->rowid;
7203 $this->ref = $obj->ref;
7204
7205 $this->user_creation_id = $obj->fk_user_author;
7206 $this->user_modification_id = $obj->fk_user_modif;
7207
7208 $this->date_creation = $this->db->jdate($obj->date_creation);
7209 $this->date_modification = $this->db->jdate($obj->date_modification);
7210 }
7211
7212 $this->db->free($result);
7213 } else {
7214 dol_print_error($this->db);
7215 }
7216 }
7217
7218
7224 public function getProductDurationHours()
7225 {
7226 if (empty($this->duration_value)) {
7227 $this->errors[] = 'ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
7228 return -1;
7229 }
7230 if ($this->duration_unit == 's') {
7231 $prodDurationHours = 1. / 3600;
7232 } elseif ($this->duration_unit == 'i' || $this->duration_unit == 'mn' || $this->duration_unit == 'min') {
7233 $prodDurationHours = 1. / 60;
7234 } elseif ($this->duration_unit == 'h') {
7235 $prodDurationHours = 1.;
7236 } elseif ($this->duration_unit == 'd') {
7237 $prodDurationHours = 24.;
7238 } elseif ($this->duration_unit == 'w') {
7239 $prodDurationHours = 24. * 7;
7240 } elseif ($this->duration_unit == 'm') {
7241 $prodDurationHours = 24. * 30;
7242 } elseif ($this->duration_unit == 'y') {
7243 $prodDurationHours = 24. * 365;
7244 } else {
7245 $prodDurationHours = 0.0;
7246 }
7247 $prodDurationHours *= $this->duration_value;
7248
7249 return $prodDurationHours;
7250 }
7251
7252
7260 public function getKanbanView($option = '', $arraydata = null)
7261 {
7262 global $langs, $conf;
7263
7264 $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
7265
7266 $return = '<div class="box-flex-item box-flex-grow-zero">';
7267 $return .= '<div class="info-box info-box-sm">';
7268 $return .= '<div class="info-box-img">';
7269 $label = '';
7270 if ($this->is_photo_available($conf->product->multidir_output[$this->entity])) {
7271 $label .= $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 120, 160, 0, 0, 0, '', 'photoref photokanban');
7272 $return .= $label;
7273 } else {
7274 if ($this->isProduct()) {
7275 $label .= img_picto('', 'product');
7276 } elseif ($this->isService()) {
7277 $label .= img_picto('', 'service');
7278 }
7279 $return .= $label;
7280 }
7281 $return .= '</div>';
7282 $return .= '<div class="info-box-content">';
7283 $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">'.(method_exists($this, 'getNomUrl') ? $this->getNomUrl() : $this->ref).'</span>';
7284 if ($selected >= 0) {
7285 $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
7286 }
7287 if (property_exists($this, 'label')) {
7288 $return .= '<br><span class="info-box-label opacitymedium inline-block tdoverflowmax150 valignmiddle" title="'.dol_escape_htmltag($this->label).'">'.dol_escape_htmltag($this->label).'</span>';
7289 }
7290 if (property_exists($this, 'price') && property_exists($this, 'price_ttc')) {
7291 if ($this->price_base_type == 'TTC') {
7292 $return .= '<br><span class="info-box-status amount">'.price($this->price_ttc).' '.$langs->trans("TTC").'</span>';
7293 } else {
7294 if ($this->status) {
7295 $return .= '<br><span class="info-box-status amount">'.price($this->price).' '.$langs->trans("HT").'</span>';
7296 }
7297 }
7298 }
7299 $br = 1;
7300 if (property_exists($this, 'stock_reel') && $this->isProduct()) {
7301 $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>';
7302 $br = 0;
7303 }
7304 if (method_exists($this, 'getLibStatut')) {
7305 if ($br) {
7306 $return .= '<br><div class="info-box-status inline-block valignmiddle">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7307 } else {
7308 $return .= '<div class="info-box-status inline-block valignmiddle marginleftonly paddingleft">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
7309 }
7310 }
7311 $return .= '</div>';
7312 $return .= '</div>';
7313 $return .= '</div>';
7314 return $return;
7315 }
7316
7323 public function getProductsToPreviewInEmail($limit)
7324 {
7325
7326 if (!is_numeric($limit)) {
7327 return -1;
7328 }
7329
7330 $sql = "SELECT p.rowid, p.ref, p.label, p.description, p.entity, ef.filename
7331 FROM ".MAIN_DB_PREFIX."product AS p
7332 JOIN ".MAIN_DB_PREFIX."ecm_files AS ef ON p.rowid = ef.src_object_id
7333 WHERE ef.entity IN (".getEntity('product').")
7334 AND (ef.filename LIKE '%.png' OR ef.filename LIKE '%.jpeg' OR ef.filename LIKE '%.svg')
7335 GROUP BY p.rowid, p.ref, p.label, p.description, p.entity, ef.filename
7336 ORDER BY p.datec ASC
7337 LIMIT " . ((int) $limit);
7338
7339 $resql = $this->db->query($sql);
7340 $products = array();
7341
7342 if ($resql) {
7343 while ($obj = $this->db->fetch_object($resql)) {
7344 $products[] = array(
7345 'rowid' => $obj->rowid,
7346 'ref' => $obj->ref,
7347 'label' => $obj->label,
7348 'description' => $obj->description,
7349 'entity' => $obj->entity,
7350 'filename' => $obj->filename
7351 );
7352 }
7353 } else {
7354 dol_print_error($this->db);
7355 }
7356 if (empty($products)) {
7357 return -1;
7358 }
7359 return $products;
7360 }
7361}
7362
7368{
7369 public $picto = 'service';
7370}
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