dolibarr 21.0.0-alpha
product.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2001-2007 Rodolphe Quiedeville <rodolphe@quiedeville.org>
3 * Copyright (C) 2004-2014 Laurent Destailleur <eldy@users.sourceforge.net>
4 * Copyright (C) 2005-2015 Regis Houssin <regis.houssin@inodbox.com>
5 * Copyright (C) 2006 Andre Cianfarani <acianfa@free.fr>
6 * Copyright (C) 2007-2011 Jean Heimburger <jean@tiaris.info>
7 * Copyright (C) 2010-2018 Juanjo Menent <jmenent@2byte.es>
8 * Copyright (C) 2012 Cedric Salvador <csalvador@gpcsolutions.fr>
9 * Copyright (C) 2013-2014 Cedric GROSS <c.gross@kreiz-it.fr>
10 * Copyright (C) 2013-2016 Marcos García <marcosgdf@gmail.com>
11 * Copyright (C) 2011-2021 Open-DSI <support@open-dsi.fr>
12 * Copyright (C) 2014 Henry Florian <florian.henry@open-concept.pro>
13 * Copyright (C) 2014-2016 Philippe Grand <philippe.grand@atoo-net.com>
14 * Copyright (C) 2014 Ion agorria <ion@agorria.com>
15 * Copyright (C) 2016-2024 Ferran Marcet <fmarcet@2byte.es>
16 * Copyright (C) 2017 Gustavo Novaro
17 * Copyright (C) 2019-2024 Frédéric France <frederic.france@free.fr>
18 * Copyright (C) 2023 Benjamin Falière <benjamin.faliere@altairis.fr>
19 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
20 *
21 * This program is free software; you can redistribute it and/or modify
22 * it under the terms of the GNU General Public License as published by
23 * the Free Software Foundation; either version 3 of the License, or
24 * (at your option) any later version.
25 *
26 * This program is distributed in the hope that it will be useful,
27 * but WITHOUT ANY WARRANTY; without even the implied warranty of
28 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 * GNU General Public License for more details.
30 *
31 * You should have received a copy of the GNU General Public License
32 * along with this program. If not, see <https://www.gnu.org/licenses/>.
33 */
34
40require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
41require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
42require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
43require_once DOL_DOCUMENT_ROOT.'/product/stock/class/productlot.class.php';
44require_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php';
45
49class Product extends CommonObject
50{
55 const SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY = 1;
56 const SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY = 2;
57 const SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT = 3;
58
62 public $element = 'product';
63
67 public $table_element = 'product';
68
72 public $fk_element = 'fk_product';
73
77 public $oldcopy;
78
82 protected $childtables = array(
83 'supplier_proposaldet' => array('name' => 'SupplierProposal', 'parent' => 'supplier_proposal', 'parentkey' => 'fk_supplier_proposal'),
84 'propaldet' => array('name' => 'Proposal', 'parent' => 'propal', 'parentkey' => 'fk_propal'),
85 'commandedet' => array('name' => 'Order', 'parent' => 'commande', 'parentkey' => 'fk_commande'),
86 'facturedet' => array('name' => 'Invoice', 'parent' => 'facture', 'parentkey' => 'fk_facture'),
87 'contratdet' => array('name' => 'Contract', 'parent' => 'contrat', 'parentkey' => 'fk_contrat'),
88 'facture_fourn_det' => array('name' => 'SupplierInvoice', 'parent' => 'facture_fourn', 'parentkey' => 'fk_facture_fourn'),
89 'commande_fournisseurdet' => array('name' => 'SupplierOrder', 'parent' => 'commande_fournisseur', 'parentkey' => 'fk_commande'),
90 'mrp_production' => array('name' => 'Mo', 'parent' => 'mrp_mo', 'parentkey' => 'fk_mo')
91 );
92
98 public $picto = 'product';
99
103 protected $table_ref_field = 'ref';
104
105 public $regeximgext = '\.gif|\.jpg|\.jpeg|\.png|\.bmp|\.webp|\.xpm|\.xbm'; // See also into images.lib.php
106
111 public $libelle;
112
118 public $label;
119
125 public $description;
126
132 public $other;
133
139 public $type = self::TYPE_PRODUCT;
140
146 public $price;
147
148 public $price_formated; // used by takepos/ajax/ajax.php
149
155 public $price_ttc;
156
157 public $price_ttc_formated; // used by takepos/ajax/ajax.php
158
164 public $price_min;
165
171 public $price_min_ttc;
172
177 public $price_base_type;
178 public $price_label;
179
181 public $multiprices = array();
182 public $multiprices_ttc = array();
183 public $multiprices_base_type = array();
184 public $multiprices_default_vat_code = array();
185 public $multiprices_min = array();
186 public $multiprices_min_ttc = array();
187 public $multiprices_tva_tx = array();
188 public $multiprices_recuperableonly = array();
189
192 public $prices_by_qty = array();
193 public $prices_by_qty_id = array();
194 public $prices_by_qty_list = array();
195
199 public $level;
200
202 public $multilangs = array();
203
206
208 public $tva_tx;
209
213 public $tva_npr = 0;
214
217
220 public $localtax2_tx;
221 public $localtax1_type;
222 public $localtax2_type;
223
224 // Properties set by get_buyprice() for return
225
226 public $desc_supplier;
227 public $vatrate_supplier;
228 public $default_vat_code_supplier;
229 public $fourn_multicurrency_price;
230 public $fourn_multicurrency_unitprice;
231 public $fourn_multicurrency_tx;
232 public $fourn_multicurrency_id;
233 public $fourn_multicurrency_code;
234 public $packaging;
235
236
243 public $lifetime;
244
251 public $qc_frequency;
252
258 public $stock_reel = 0;
259
265 public $stock_theorique;
266
272 public $cost_price;
273
275 public $pmp;
276
282 public $seuil_stock_alerte = 0;
283
287 public $desiredstock = 0;
288
300 public $duration;
301
305 public $fk_default_workstation;
306
312 public $status = 0;
313
320 public $tosell;
321
327 public $status_buy = 0;
328
335 public $tobuy;
336
342 public $finished;
343
349 public $fk_default_bom;
350
356 public $product_fourn_price_id;
357
363 public $buyprice;
364
370 public $tobatch;
371
372
378 public $status_batch = 0;
379
385 public $sell_or_eat_by_mandatory = 0;
386
392 public $batch_mask = '';
393
399 public $customcode;
400
406 public $url;
407
409 public $weight;
410 public $weight_units; // scale -3, 0, 3, 6
411 public $length;
412 public $length_units; // scale -3, 0, 3, 6
413 public $width;
414 public $width_units; // scale -3, 0, 3, 6
415 public $height;
416 public $height_units; // scale -3, 0, 3, 6
417 public $surface;
418 public $surface_units; // scale -3, 0, 3, 6
419 public $volume;
420 public $volume_units; // scale -3, 0, 3, 6
421
422 public $net_measure;
423 public $net_measure_units; // scale -3, 0, 3, 6
424
425 public $accountancy_code_sell;
426 public $accountancy_code_sell_intra;
427 public $accountancy_code_sell_export;
428 public $accountancy_code_buy;
429 public $accountancy_code_buy_intra;
430 public $accountancy_code_buy_export;
431
435 public $barcode;
436
440 public $barcode_type;
441
445 public $barcode_type_code;
446
447 public $stats_propale = array();
448 public $stats_commande = array();
449 public $stats_contrat = array();
450 public $stats_facture = array();
451 public $stats_proposal_supplier = array();
452 public $stats_commande_fournisseur = array();
453 public $stats_expedition = array();
454 public $stats_reception = array();
455 public $stats_mo = array();
456 public $stats_bom = array();
457 public $stats_mrptoconsume = array();
458 public $stats_mrptoproduce = array();
459 public $stats_facturerec = array();
460 public $stats_facture_fournisseur = array();
461
463 public $imgWidth;
464 public $imgHeight;
465
470 public $product_fourn_id;
471
476 public $product_id_already_linked;
477
482 public $nbphoto = 0;
483
485 public $stock_warehouse = array();
486
490 public $fk_default_warehouse;
491
495 public $fk_price_expression;
496
501 public $fourn_qty;
502
507 public $fourn_pu;
508
513 public $fourn_price_base_type;
514
518 public $fourn_socid;
519
525
529 public $ref_supplier;
530
536 public $fk_unit;
537
543 public $price_autogen = 0;
544
550 public $supplierprices;
551
557 public $sousprods;
558
562 public $res;
563
564
570 public $is_object_used;
571
581 public $is_sousproduit_qty;
582
593 public $is_sousproduit_incdec;
594
595 public $mandatory_period;
596
597
626 public $fields = array(
627 'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'index' => 1, 'position' => 1, 'comment' => 'Id'),
628 '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'),
629 'entity' => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'visible' => 0, 'default' => '1', 'notnull' => 1, 'index' => 1, 'position' => 5),
630 'label' => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'showoncombobox' => 2, 'position' => 15, 'csslist' => 'tdoverflowmax250'),
631 'barcode' => array('type' => 'varchar(255)', 'label' => 'Barcode', 'enabled' => 'isModEnabled("barcode")', 'position' => 20, 'visible' => -1, 'showoncombobox' => 3, 'cssview' => 'tdwordbreak', 'csslist' => 'tdoverflowmax125'),
632 'fk_barcode_type' => array('type' => 'integer', 'label' => 'BarcodeType', 'enabled' => 1, 'position' => 21, 'notnull' => 0, 'visible' => -1,),
633 'note_public' => array('type' => 'html', 'label' => 'NotePublic', 'enabled' => 1, 'visible' => 0, 'position' => 61),
634 'note' => array('type' => 'html', 'label' => 'NotePrivate', 'enabled' => 1, 'visible' => 0, 'position' => 62),
635 'datec' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 500),
636 'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 501),
637 //'date_valid' =>array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-2, 'position'=>502),
638 'fk_user_author' => array('type' => 'integer', 'label' => 'UserAuthor', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 510, 'foreignkey' => 'llx_user.rowid'),
639 'fk_user_modif' => array('type' => 'integer', 'label' => 'UserModif', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'position' => 511),
640 //'fk_user_valid' =>array('type'=>'integer', 'label'=>'UserValidation', 'enabled'=>1, 'visible'=>-1, 'position'=>512),
641 'localtax1_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax1tx', 'enabled' => 1, 'position' => 150, 'notnull' => 0, 'visible' => -1,),
642 'localtax1_type' => array('type' => 'varchar(10)', 'label' => 'Localtax1type', 'enabled' => 1, 'position' => 155, 'notnull' => 1, 'visible' => -1,),
643 'localtax2_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax2tx', 'enabled' => 1, 'position' => 160, 'notnull' => 0, 'visible' => -1,),
644 'localtax2_type' => array('type' => 'varchar(10)', 'label' => 'Localtax2type', 'enabled' => 1, 'position' => 165, 'notnull' => 1, 'visible' => -1,),
645 'last_main_doc' => array('type' => 'varchar(255)', 'label' => 'LastMainDoc', 'enabled' => 1, 'visible' => -1, 'position' => 170),
646 'import_key' => array('type' => 'varchar(14)', 'label' => 'ImportId', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'index' => 0, 'position' => 1000),
647 //'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')),
648 //'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')),
649 'mandatory_period' => array('type' => 'integer', 'label' => 'mandatoryperiod', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'default' => '0', 'index' => 1, 'position' => 1000),
650 );
651
655 const TYPE_PRODUCT = 0;
659 const TYPE_SERVICE = 1;
660
666 public function __construct($db)
667 {
668 $this->db = $db;
669
670 $this->ismultientitymanaged = 1;
671 $this->isextrafieldmanaged = 1;
672
673 $this->canvas = '';
674 }
675
681 public function check()
682 {
683 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
684 $this->ref = trim($this->ref);
685 } else {
686 $this->ref = dol_sanitizeFileName(stripslashes($this->ref));
687 }
688
689 $err = 0;
690 if (dol_strlen(trim($this->ref)) == 0) {
691 $err++;
692 }
693
694 if (dol_strlen(trim($this->label)) == 0) {
695 $err++;
696 }
697
698 if ($err > 0) {
699 return 0;
700 } else {
701 return 1;
702 }
703 }
704
712 public function create($user, $notrigger = 0)
713 {
714 global $conf, $langs;
715
716 $error = 0;
717
718 // Clean parameters
719 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
720 $this->ref = trim($this->ref);
721 } else {
722 $this->ref = dol_sanitizeFileName(dol_string_nospecial(trim($this->ref)));
723 }
724 $this->label = trim($this->label);
725 $this->price_ttc = (float) price2num($this->price_ttc);
726 $this->price = (float) price2num($this->price);
727 $this->price_min_ttc = (float) price2num($this->price_min_ttc);
728 $this->price_min = (float) price2num($this->price_min);
729 $this->price_label = trim($this->price_label);
730 if (empty($this->tva_tx)) {
731 $this->tva_tx = 0;
732 }
733 if (empty($this->tva_npr)) {
734 $this->tva_npr = 0;
735 }
736 //Local taxes
737 if (empty($this->localtax1_tx)) {
738 $this->localtax1_tx = 0;
739 }
740 if (empty($this->localtax2_tx)) {
741 $this->localtax2_tx = 0;
742 }
743 if (empty($this->localtax1_type)) {
744 $this->localtax1_type = '0';
745 }
746 if (empty($this->localtax2_type)) {
747 $this->localtax2_type = '0';
748 }
749 if (empty($this->price)) {
750 $this->price = 0;
751 }
752 if (empty($this->price_min)) {
753 $this->price_min = 0;
754 }
755 // Price by quantity
756 if (empty($this->price_by_qty)) {
757 $this->price_by_qty = 0;
758 }
759
760 if (empty($this->status)) {
761 $this->status = 0;
762 }
763 if (empty($this->status_buy)) {
764 $this->status_buy = 0;
765 }
766
767 $price_ht = 0;
768 $price_ttc = 0;
769 $price_min_ht = 0;
770 $price_min_ttc = 0;
771
772 //
773 if ($this->price_base_type == 'TTC' && $this->price_ttc > 0) {
774 $price_ttc = price2num($this->price_ttc, 'MU');
775 $price_ht = price2num($this->price_ttc / (1 + ($this->tva_tx / 100)), 'MU');
776 }
777
778 //
779 if ($this->price_base_type != 'TTC' && $this->price > 0) {
780 $price_ht = price2num($this->price, 'MU');
781 $price_ttc = price2num($this->price * (1 + ($this->tva_tx / 100)), 'MU');
782 }
783
784 //
785 if (($this->price_min_ttc > 0) && ($this->price_base_type == 'TTC')) {
786 $price_min_ttc = price2num($this->price_min_ttc, 'MU');
787 $price_min_ht = price2num($this->price_min_ttc / (1 + ($this->tva_tx / 100)), 'MU');
788 }
789
790 //
791 if (($this->price_min > 0) && ($this->price_base_type != 'TTC')) {
792 $price_min_ht = price2num($this->price_min, 'MU');
793 $price_min_ttc = price2num($this->price_min * (1 + ($this->tva_tx / 100)), 'MU');
794 }
795
796 $this->accountancy_code_buy = trim($this->accountancy_code_buy);
797 $this->accountancy_code_buy_intra = trim($this->accountancy_code_buy_intra);
798 $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
799 $this->accountancy_code_sell = trim($this->accountancy_code_sell);
800 $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
801 $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
802
803 // Barcode value
804 $this->barcode = trim($this->barcode);
805 $this->mandatory_period = empty($this->mandatory_period) ? 0 : $this->mandatory_period;
806 // Check parameters
807 if (empty($this->label)) {
808 $this->error = 'ErrorMandatoryParametersNotProvided';
809 return -1;
810 }
811
812 if (empty($this->ref) || $this->ref == 'auto') {
813 // Load object modCodeProduct
814 $module = getDolGlobalString('PRODUCT_CODEPRODUCT_ADDON', 'mod_codeproduct_leopard');
815 if ($module != 'mod_codeproduct_leopard') { // Do not load module file for leopard
816 if (substr($module, 0, 16) == 'mod_codeproduct_' && substr($module, -3) == 'php') {
817 $module = substr($module, 0, dol_strlen($module) - 4);
818 }
819 dol_include_once('/core/modules/product/'.$module.'.php');
820 $modCodeProduct = new $module();
821 '@phan-var-force ModeleProductCode $modCodeProduct';
822 if (!empty($modCodeProduct->code_auto)) {
823 $this->ref = $modCodeProduct->getNextValue($this, $this->type);
824 }
825 unset($modCodeProduct);
826 }
827
828 if (empty($this->ref)) {
829 $this->error = 'ProductModuleNotSetupForAutoRef';
830 return -2;
831 }
832 }
833
834 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);
835
836 $now = dol_now();
837
838 if (empty($this->date_creation)) {
839 $this->date_creation = $now;
840 }
841
842 $this->db->begin();
843
844 // For automatic creation during create action (not used by Dolibarr GUI, can be used by scripts)
845 if ($this->barcode == '-1' || $this->barcode == 'auto') {
846 $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
847 }
848
849 // Check more parameters
850 // If error, this->errors[] is filled
851 $result = $this->verify();
852
853 if ($result >= 0) {
854 $sql = "SELECT count(*) as nb";
855 $sql .= " FROM ".$this->db->prefix()."product";
856 $sql .= " WHERE entity IN (".getEntity('product').")";
857 $sql .= " AND ref = '".$this->db->escape($this->ref)."'";
858
859 $result = $this->db->query($sql);
860 if ($result) {
861 $obj = $this->db->fetch_object($result);
862 if ($obj->nb == 0) {
863 // Insert new product, no previous one found
864 $sql = "INSERT INTO ".$this->db->prefix()."product (";
865 $sql .= "datec";
866 $sql .= ", entity";
867 $sql .= ", ref";
868 $sql .= ", ref_ext";
869 $sql .= ", price_min";
870 $sql .= ", price_min_ttc";
871 $sql .= ", label";
872 $sql .= ", fk_user_author";
873 $sql .= ", fk_product_type";
874 $sql .= ", price";
875 $sql .= ", price_ttc";
876 $sql .= ", price_base_type";
877 $sql .= ", price_label";
878 $sql .= ", tobuy";
879 $sql .= ", tosell";
880 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
881 $sql .= ", accountancy_code_buy";
882 $sql .= ", accountancy_code_buy_intra";
883 $sql .= ", accountancy_code_buy_export";
884 $sql .= ", accountancy_code_sell";
885 $sql .= ", accountancy_code_sell_intra";
886 $sql .= ", accountancy_code_sell_export";
887 }
888 $sql .= ", canvas";
889 $sql .= ", finished";
890 $sql .= ", tobatch";
891 $sql .= ", sell_or_eat_by_mandatory";
892 $sql .= ", batch_mask";
893 $sql .= ", fk_unit";
894 $sql .= ", mandatory_period";
895 $sql .= ") VALUES (";
896 $sql .= "'".$this->db->idate($this->date_creation)."'";
897 $sql .= ", ".(!empty($this->entity) ? (int) $this->entity : (int) $conf->entity);
898 $sql .= ", '".$this->db->escape($this->ref)."'";
899 $sql .= ", ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
900 $sql .= ", ".price2num($price_min_ht);
901 $sql .= ", ".price2num($price_min_ttc);
902 $sql .= ", ".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null");
903 $sql .= ", ".((int) $user->id);
904 $sql .= ", ".((int) $this->type);
905 $sql .= ", ".price2num($price_ht, 'MT');
906 $sql .= ", ".price2num($price_ttc, 'MT');
907 $sql .= ", '".$this->db->escape($this->price_base_type)."'";
908 $sql .= ", ".(!empty($this->price_label) ? "'".$this->db->escape($this->price_label)."'" : "null");
909 $sql .= ", ".((int) $this->status);
910 $sql .= ", ".((int) $this->status_buy);
911 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
912 $sql .= ", '".$this->db->escape($this->accountancy_code_buy)."'";
913 $sql .= ", '".$this->db->escape($this->accountancy_code_buy_intra)."'";
914 $sql .= ", '".$this->db->escape($this->accountancy_code_buy_export)."'";
915 $sql .= ", '".$this->db->escape($this->accountancy_code_sell)."'";
916 $sql .= ", '".$this->db->escape($this->accountancy_code_sell_intra)."'";
917 $sql .= ", '".$this->db->escape($this->accountancy_code_sell_export)."'";
918 }
919 $sql .= ", '".$this->db->escape($this->canvas)."'";
920 $sql .= ", ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? 'NULL' : (int) $this->finished);
921 $sql .= ", ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : ((int) $this->status_batch));
922 $sql .= ", ".((empty($this->sell_or_eat_by_mandatory) || $this->sell_or_eat_by_mandatory < 0) ? 0 : ((int) $this->sell_or_eat_by_mandatory));
923 $sql .= ", '".$this->db->escape($this->batch_mask)."'";
924 $sql .= ", ".($this->fk_unit > 0 ? ((int) $this->fk_unit) : 'NULL');
925 $sql .= ", '".$this->db->escape($this->mandatory_period)."'";
926 $sql .= ")";
927
928 dol_syslog(get_class($this)."::Create", LOG_DEBUG);
929
930 $result = $this->db->query($sql);
931 if ($result) {
932 $id = $this->db->last_insert_id($this->db->prefix()."product");
933
934 if ($id > 0) {
935 $this->id = $id;
936 $this->price = $price_ht;
937 $this->price_ttc = $price_ttc;
938 $this->price_min = $price_min_ht;
939 $this->price_min_ttc = $price_min_ttc;
940
941 $result = $this->_log_price($user);
942 if ($result > 0) {
943 if ($this->update($id, $user, 1, 'add') <= 0) {
944 $error++;
945 }
946 } else {
947 $error++;
948 $this->error = $this->db->lasterror();
949 }
950
951 // update accountancy for this entity
952 if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
953 $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " .((int) $this->id) . " AND entity = " . ((int) $conf->entity));
954
955 $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
956 $sql .= " fk_product";
957 $sql .= ", entity";
958 $sql .= ", accountancy_code_buy";
959 $sql .= ", accountancy_code_buy_intra";
960 $sql .= ", accountancy_code_buy_export";
961 $sql .= ", accountancy_code_sell";
962 $sql .= ", accountancy_code_sell_intra";
963 $sql .= ", accountancy_code_sell_export";
964 $sql .= ") VALUES (";
965 $sql .= $this->id;
966 $sql .= ", " . $conf->entity;
967 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
968 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
969 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
970 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
971 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
972 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
973 $sql .= ")";
974 $result = $this->db->query($sql);
975 if (!$result) {
976 $error++;
977 $this->error = 'ErrorFailedToInsertAccountancyForEntity';
978 }
979 }
980 } else {
981 $error++;
982 $this->error = 'ErrorFailedToGetInsertedId';
983 }
984 } else {
985 $error++;
986 $this->error = $this->db->lasterror();
987 }
988 } else {
989 // Product already exists with this ref
990 $langs->load("products");
991 $error++;
992 $this->error = "ErrorProductAlreadyExists";
993 dol_syslog(get_class($this)."::Create fails, ref ".$this->ref." already exists");
994 }
995 } else {
996 $error++;
997 $this->error = $this->db->lasterror();
998 }
999
1000 if (!$error && !$notrigger) {
1001 // Call trigger
1002 $result = $this->call_trigger('PRODUCT_CREATE', $user);
1003 if ($result < 0) {
1004 $error++;
1005 }
1006 // End call triggers
1007 }
1008
1009 if (!$error) {
1010 $this->db->commit();
1011 return $this->id;
1012 } else {
1013 $this->db->rollback();
1014 return -$error;
1015 }
1016 } else {
1017 $this->db->rollback();
1018 dol_syslog(get_class($this)."::Create fails verify ".implode(',', $this->errors), LOG_WARNING);
1019 return -3;
1020 }
1021 }
1022
1023
1030 public function verify()
1031 {
1032 global $langs;
1033
1034 $this->errors = array();
1035
1036 $result = 0;
1037 $this->ref = trim($this->ref);
1038
1039 if (!$this->ref) {
1040 $this->errors[] = 'ErrorBadRef';
1041 $result = -2;
1042 }
1043
1044 $arrayofnonnegativevalue = array('weight' => 'Weight', 'width' => 'Width', 'height' => 'Height', 'length' => 'Length', 'surface' => 'Surface', 'volume' => 'Volume');
1045 foreach ($arrayofnonnegativevalue as $key => $value) {
1046 if (property_exists($this, $key) && !empty($this->$key) && ($this->$key < 0)) {
1047 $langs->loadLangs(array("main", "other"));
1048 $this->error = $langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv($value));
1049 $this->errors[] = $this->error;
1050 $result = -4;
1051 }
1052 }
1053
1054 $rescode = $this->check_barcode($this->barcode, $this->barcode_type_code);
1055 if ($rescode) {
1056 if ($rescode == -1) {
1057 $this->errors[] = 'ErrorBadBarCodeSyntax';
1058 } elseif ($rescode == -2) {
1059 $this->errors[] = 'ErrorBarCodeRequired';
1060 } elseif ($rescode == -3) {
1061 // Note: Common usage is to have barcode unique. For variants, we should have a different barcode.
1062 $this->errors[] = 'ErrorBarCodeAlreadyUsed';
1063 }
1064
1065 $result = -3;
1066 }
1067
1068 return $result;
1069 }
1070
1071 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1082 public function check_barcode($valuetotest, $typefortest)
1083 {
1084 // phpcs:enable
1085 global $conf;
1086 if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
1087 $module = strtolower(getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM'));
1088
1089 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
1090 foreach ($dirsociete as $dirroot) {
1091 $res = dol_include_once($dirroot.$module.'.php');
1092 if ($res) {
1093 break;
1094 }
1095 }
1096
1097 $mod = new $module();
1098 '@phan-var-force ModeleNumRefBarCode $mod';
1099
1100 dol_syslog(get_class($this)."::check_barcode value=".$valuetotest." type=".$typefortest." module=".$module);
1101 $result = $mod->verif($this->db, $valuetotest, $this, 0, $typefortest);
1102 return $result;
1103 } else {
1104 return 0;
1105 }
1106 }
1107
1119 public function update($id, $user, $notrigger = 0, $action = 'update', $updatetype = false)
1120 {
1121 global $langs, $conf, $hookmanager;
1122
1123 $error = 0;
1124
1125 // Check parameters
1126 if (!$this->label) {
1127 $this->label = 'MISSING LABEL';
1128 }
1129
1130 // Clean parameters
1131 if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
1132 $this->ref = trim($this->ref);
1133 } else {
1134 $this->ref = dol_string_nospecial(trim($this->ref));
1135 }
1136 $this->label = trim($this->label);
1137 $this->description = trim($this->description);
1138 $this->note_private = (isset($this->note_private) ? trim($this->note_private) : null);
1139 $this->note_public = (isset($this->note_public) ? trim($this->note_public) : null);
1140 $this->net_measure = price2num($this->net_measure);
1141 $this->net_measure_units = (empty($this->net_measure_units) ? '' : trim($this->net_measure_units));
1142 $this->weight = price2num($this->weight);
1143 $this->weight_units = (empty($this->weight_units) ? '' : trim($this->weight_units));
1144 $this->length = price2num($this->length);
1145 $this->length_units = (empty($this->length_units) ? '' : trim($this->length_units));
1146 $this->width = price2num($this->width);
1147 $this->width_units = (empty($this->width_units) ? '' : trim($this->width_units));
1148 $this->height = price2num($this->height);
1149 $this->height_units = (empty($this->height_units) ? '' : trim($this->height_units));
1150 $this->surface = price2num($this->surface);
1151 $this->surface_units = (empty($this->surface_units) ? '' : trim($this->surface_units));
1152 $this->volume = price2num($this->volume);
1153 $this->volume_units = (empty($this->volume_units) ? '' : trim($this->volume_units));
1154
1155 // set unit not defined
1156 if (is_numeric($this->length_units)) {
1157 $this->width_units = $this->length_units; // Not used yet
1158 }
1159 if (is_numeric($this->length_units)) {
1160 $this->height_units = $this->length_units; // Not used yet
1161 }
1162
1163 // Automated compute surface and volume if not filled
1164 if (empty($this->surface) && !empty($this->length) && !empty($this->width) && $this->length_units == $this->width_units) {
1165 $this->surface = (float) $this->length * (float) $this->width;
1166 $this->surface_units = measuring_units_squared($this->length_units);
1167 }
1168 if (empty($this->volume) && !empty($this->surface) && !empty($this->height) && $this->length_units == $this->height_units) {
1169 $this->volume = $this->surface * (float) $this->height;
1170 $this->volume_units = measuring_units_cubed($this->height_units);
1171 }
1172
1173 if (empty($this->tva_tx)) {
1174 $this->tva_tx = 0;
1175 }
1176 if (empty($this->tva_npr)) {
1177 $this->tva_npr = 0;
1178 }
1179 if (empty($this->localtax1_tx)) {
1180 $this->localtax1_tx = 0;
1181 }
1182 if (empty($this->localtax2_tx)) {
1183 $this->localtax2_tx = 0;
1184 }
1185 if (empty($this->localtax1_type)) {
1186 $this->localtax1_type = '0';
1187 }
1188 if (empty($this->localtax2_type)) {
1189 $this->localtax2_type = '0';
1190 }
1191 if (empty($this->status)) {
1192 $this->status = 0;
1193 }
1194 if (empty($this->status_buy)) {
1195 $this->status_buy = 0;
1196 }
1197
1198 if (empty($this->country_id)) {
1199 $this->country_id = 0;
1200 }
1201
1202 if (empty($this->state_id)) {
1203 $this->state_id = 0;
1204 }
1205
1206 // Barcode value
1207 $this->barcode = (empty($this->barcode) ? '' : trim($this->barcode));
1208
1209 $this->accountancy_code_buy = trim($this->accountancy_code_buy);
1210 $this->accountancy_code_buy_intra = (!empty($this->accountancy_code_buy_intra) ? trim($this->accountancy_code_buy_intra) : '');
1211 $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
1212 $this->accountancy_code_sell = trim($this->accountancy_code_sell);
1213 $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
1214 $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
1215
1216
1217 $this->db->begin();
1218
1219 $result = 0;
1220 // Check name is required and codes are ok or unique. If error, this->errors[] is filled
1221 if ($action != 'add') {
1222 $result = $this->verify(); // We don't check when update called during a create because verify was already done
1223 } else {
1224 // we can continue
1225 $result = 0;
1226 }
1227
1228 if ($result >= 0) {
1229 // $this->oldcopy should have been set by the caller of update (here properties were already modified)
1230 if (is_null($this->oldcopy) || (is_object($this->oldcopy) && $this->oldcopy->isEmpty())) {
1231 $this->oldcopy = dol_clone($this, 1);
1232 }
1233 // Test if batch management is activated on existing product
1234 // If yes, we create missing entries into product_batch
1235 if ($this->hasbatch() && !$this->oldcopy->hasbatch()) {
1236 //$valueforundefinedlot = 'Undefined'; // In previous version, 39 and lower
1237 $valueforundefinedlot = '000000';
1238 if (getDolGlobalString('STOCK_DEFAULT_BATCH')) {
1239 $valueforundefinedlot = getDolGlobalString('STOCK_DEFAULT_BATCH');
1240 }
1241
1242 dol_syslog("Flag batch of product id=".$this->id." is set to ON, so we will create missing records into product_batch");
1243
1244 $this->load_stock();
1245 foreach ($this->stock_warehouse as $idW => $ObjW) { // For each warehouse where we have stocks defined for this product (for each lines in product_stock)
1246 $qty_batch = 0;
1247 foreach ($ObjW->detail_batch as $detail) { // Each lines of detail in product_batch of the current $ObjW = product_stock
1248 if ($detail->batch == $valueforundefinedlot || $detail->batch == 'Undefined') {
1249 // We discard this line, we will create it later
1250 $sqlclean = "DELETE FROM ".$this->db->prefix()."product_batch WHERE batch in('Undefined', '".$this->db->escape($valueforundefinedlot)."') AND fk_product_stock = ".((int) $ObjW->id);
1251 $result = $this->db->query($sqlclean);
1252 if (!$result) {
1253 dol_print_error($this->db);
1254 exit;
1255 }
1256 continue;
1257 }
1258
1259 $qty_batch += $detail->qty;
1260 }
1261 // Quantities in batch details are not same as stock quantity,
1262 // so we add a default batch record to complete and get same qty in parent and child table
1263 if ($ObjW->real != $qty_batch) {
1264 $ObjBatch = new Productbatch($this->db);
1265 $ObjBatch->batch = $valueforundefinedlot;
1266 $ObjBatch->qty = ($ObjW->real - $qty_batch);
1267 $ObjBatch->fk_product_stock = $ObjW->id;
1268
1269 if ($ObjBatch->create($user, 1) < 0) {
1270 $error++;
1271 $this->errors = $ObjBatch->errors;
1272 } else {
1273 // we also add lot record if not exist
1274 $ObjLot = new Productlot($this->db);
1275 // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1276 if ($ObjLot->fetch(0, $this->id, $valueforundefinedlot) == 0) {
1277 $ObjLot->fk_product = $this->id;
1278 $ObjLot->entity = $this->entity;
1279 $ObjLot->fk_user_creat = $user->id;
1280 $ObjLot->batch = $valueforundefinedlot;
1281 if ($ObjLot->create($user, true) < 0) {
1282 $error++;
1283 $this->errors = $ObjLot->errors;
1284 }
1285 }
1286 }
1287 }
1288 }
1289 }
1290
1291 // For automatic creation
1292 if ($this->barcode == -1) {
1293 $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1294 }
1295
1296 $sql = "UPDATE ".$this->db->prefix()."product";
1297 $sql .= " SET label = '".$this->db->escape($this->label)."'";
1298
1299 if ($updatetype && ($this->isProduct() || $this->isService())) {
1300 $sql .= ", fk_product_type = ".((int) $this->type);
1301 }
1302
1303 $sql .= ", ref = '".$this->db->escape($this->ref)."'";
1304 $sql .= ", ref_ext = ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
1305 $sql .= ", default_vat_code = ".($this->default_vat_code ? "'".$this->db->escape($this->default_vat_code)."'" : "null");
1306 $sql .= ", tva_tx = ".((float) $this->tva_tx);
1307 $sql .= ", recuperableonly = ".((int) $this->tva_npr);
1308 $sql .= ", localtax1_tx = ".((float) $this->localtax1_tx);
1309 $sql .= ", localtax2_tx = ".((float) $this->localtax2_tx);
1310 $sql .= ", localtax1_type = ".($this->localtax1_type != '' ? "'".$this->db->escape($this->localtax1_type)."'" : "'0'");
1311 $sql .= ", localtax2_type = ".($this->localtax2_type != '' ? "'".$this->db->escape($this->localtax2_type)."'" : "'0'");
1312
1313 $sql .= ", barcode = ".(empty($this->barcode) ? "null" : "'".$this->db->escape($this->barcode)."'");
1314 $sql .= ", fk_barcode_type = ".(empty($this->barcode_type) ? "null" : $this->db->escape($this->barcode_type));
1315
1316 $sql .= ", tosell = ".(int) $this->status;
1317 $sql .= ", tobuy = ".(int) $this->status_buy;
1318 $sql .= ", tobatch = ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : (int) $this->status_batch);
1319 $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);
1320 $sql .= ", batch_mask = '".$this->db->escape($this->batch_mask)."'";
1321
1322 $sql .= ", finished = ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? "null" : (int) $this->finished);
1323 $sql .= ", fk_default_bom = ".((!isset($this->fk_default_bom) || $this->fk_default_bom < 0 || $this->fk_default_bom == '') ? "null" : (int) $this->fk_default_bom);
1324 $sql .= ", net_measure = ".($this->net_measure != '' ? "'".$this->db->escape($this->net_measure)."'" : 'null');
1325 $sql .= ", net_measure_units = ".($this->net_measure_units != '' ? "'".$this->db->escape($this->net_measure_units)."'" : 'null');
1326 $sql .= ", weight = ".($this->weight != '' ? "'".$this->db->escape($this->weight)."'" : 'null');
1327 $sql .= ", weight_units = ".($this->weight_units != '' ? "'".$this->db->escape($this->weight_units)."'" : 'null');
1328 $sql .= ", length = ".($this->length != '' ? "'".$this->db->escape($this->length)."'" : 'null');
1329 $sql .= ", length_units = ".($this->length_units != '' ? "'".$this->db->escape($this->length_units)."'" : 'null');
1330 $sql .= ", width= ".($this->width != '' ? "'".$this->db->escape($this->width)."'" : 'null');
1331 $sql .= ", width_units = ".($this->width_units != '' ? "'".$this->db->escape($this->width_units)."'" : 'null');
1332 $sql .= ", height = ".($this->height != '' ? "'".$this->db->escape($this->height)."'" : 'null');
1333 $sql .= ", height_units = ".($this->height_units != '' ? "'".$this->db->escape($this->height_units)."'" : 'null');
1334 $sql .= ", surface = ".($this->surface != '' ? "'".$this->db->escape($this->surface)."'" : 'null');
1335 $sql .= ", surface_units = ".($this->surface_units != '' ? "'".$this->db->escape($this->surface_units)."'" : 'null');
1336 $sql .= ", volume = ".($this->volume != '' ? "'".$this->db->escape($this->volume)."'" : 'null');
1337 $sql .= ", volume_units = ".($this->volume_units != '' ? "'".$this->db->escape($this->volume_units)."'" : 'null');
1338 $sql .= ", fk_default_warehouse = ".($this->fk_default_warehouse > 0 ? ((int) $this->fk_default_warehouse) : 'null');
1339 $sql .= ", fk_default_workstation = ".($this->fk_default_workstation > 0 ? ((int) $this->fk_default_workstation) : 'null');
1340 $sql .= ", seuil_stock_alerte = ".((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (float) $this->seuil_stock_alerte : 'null');
1341 $sql .= ", description = '".$this->db->escape($this->description)."'";
1342 $sql .= ", url = ".($this->url ? "'".$this->db->escape($this->url)."'" : 'null');
1343 $sql .= ", customcode = '".$this->db->escape($this->customcode)."'";
1344 $sql .= ", fk_country = ".($this->country_id > 0 ? (int) $this->country_id : 'null');
1345 $sql .= ", fk_state = ".($this->state_id > 0 ? (int) $this->state_id : 'null');
1346 $sql .= ", lifetime = ".($this->lifetime > 0 ? (int) $this->lifetime : 'null');
1347 $sql .= ", qc_frequency = ".($this->qc_frequency > 0 ? (int) $this->qc_frequency : 'null');
1348 $sql .= ", note = ".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : 'null');
1349 $sql .= ", note_public = ".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : 'null');
1350 $sql .= ", duration = '".$this->db->escape($this->duration_value.$this->duration_unit)."'";
1351 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1352 $sql .= ", accountancy_code_buy = '" . $this->db->escape($this->accountancy_code_buy) . "'";
1353 $sql .= ", accountancy_code_buy_intra = '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1354 $sql .= ", accountancy_code_buy_export = '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1355 $sql .= ", accountancy_code_sell= '" . $this->db->escape($this->accountancy_code_sell) . "'";
1356 $sql .= ", accountancy_code_sell_intra= '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1357 $sql .= ", accountancy_code_sell_export= '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1358 }
1359 $sql .= ", desiredstock = ".((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (float) $this->desiredstock : "null");
1360 $sql .= ", cost_price = ".($this->cost_price != '' ? $this->db->escape($this->cost_price) : 'null');
1361 $sql .= ", fk_unit= ".(!$this->fk_unit ? 'NULL' : (int) $this->fk_unit);
1362 $sql .= ", price_autogen = ".(!$this->price_autogen ? 0 : 1);
1363 $sql .= ", fk_price_expression = ".($this->fk_price_expression != 0 ? (int) $this->fk_price_expression : 'NULL');
1364 $sql .= ", fk_user_modif = ".($user->id > 0 ? $user->id : 'NULL');
1365 $sql .= ", mandatory_period = ".($this->mandatory_period);
1366 // stock field is not here because it is a denormalized value from product_stock.
1367 $sql .= " WHERE rowid = ".((int) $id);
1368
1369 dol_syslog(get_class($this)."::update", LOG_DEBUG);
1370
1371 $resql = $this->db->query($sql);
1372 if ($resql) {
1373 $this->id = $id;
1374
1375 // Multilangs
1376 if (getDolGlobalInt('MAIN_MULTILANGS')) {
1377 if ($this->setMultiLangs($user) < 0) {
1378 $this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1379 $this->db->rollback();
1380 return -2;
1381 }
1382 }
1383
1384 $action = 'update';
1385
1386 // update accountancy for this entity
1387 if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1388 $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1389
1390 $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1391 $sql .= " fk_product";
1392 $sql .= ", entity";
1393 $sql .= ", accountancy_code_buy";
1394 $sql .= ", accountancy_code_buy_intra";
1395 $sql .= ", accountancy_code_buy_export";
1396 $sql .= ", accountancy_code_sell";
1397 $sql .= ", accountancy_code_sell_intra";
1398 $sql .= ", accountancy_code_sell_export";
1399 $sql .= ") VALUES (";
1400 $sql .= $this->id;
1401 $sql .= ", " . $conf->entity;
1402 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1403 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1404 $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1405 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1406 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1407 $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1408 $sql .= ")";
1409 $result = $this->db->query($sql);
1410 if (!$result) {
1411 $error++;
1412 $this->error = 'ErrorFailedToUpdateAccountancyForEntity';
1413 }
1414 }
1415
1416 if (!$this->hasbatch() && $this->oldcopy->hasbatch()) {
1417 // Selection of all product stock movements that contains batchs
1418 $sql = 'SELECT pb.qty, ps.fk_entrepot, pb.batch FROM '.MAIN_DB_PREFIX.'product_batch as pb';
1419 $sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'product_stock as ps ON (ps.rowid = pb.fk_product_stock)';
1420 $sql .= ' WHERE ps.fk_product = '.(int) $this->id;
1421
1422 $resql = $this->db->query($sql);
1423 if ($resql) {
1424 $inventorycode = dol_print_date(dol_now(), '%Y%m%d%H%M%S');
1425
1426 while ($obj = $this->db->fetch_object($resql)) {
1427 $value = $obj->qty;
1428 $fk_entrepot = $obj->fk_entrepot;
1429 $price = 0;
1430 $dlc = '';
1431 $dluo = '';
1432 $batch = $obj->batch;
1433
1434 // To know how to revert stockMouvement (add or remove)
1435 $addOremove = $value > 0 ? 1 : 0; // 1 if remove, 0 if add
1436 $label = $langs->trans('BatchStockMouvementAddInGlobal');
1437 $res = $this->correct_stock_batch($user, $fk_entrepot, abs($value), $addOremove, $label, $price, $dlc, $dluo, $batch, $inventorycode, '', null, 0, null, true);
1438
1439 if ($res > 0) {
1440 $label = $langs->trans('BatchStockMouvementAddInGlobal');
1441 $res = $this->correct_stock($user, $fk_entrepot, abs($value), (int) empty($addOremove), $label, $price, $inventorycode, '', null, 0);
1442 if ($res < 0) {
1443 $error++;
1444 }
1445 } else {
1446 $error++;
1447 }
1448 }
1449 }
1450 }
1451
1452 // Actions on extra fields
1453 if (!$error) {
1454 $result = $this->insertExtraFields();
1455 if ($result < 0) {
1456 $error++;
1457 }
1458 }
1459
1460 if (!$error && !$notrigger) {
1461 // Call trigger
1462 $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1463 if ($result < 0) {
1464 $error++;
1465 }
1466 // End call triggers
1467 }
1468
1469 if (!$error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref)) {
1470 // We remove directory
1471 if ($conf->product->dir_output) {
1472 $olddir = $conf->product->dir_output."/".dol_sanitizeFileName($this->oldcopy->ref);
1473 $newdir = $conf->product->dir_output."/".dol_sanitizeFileName($this->ref);
1474 if (file_exists($olddir)) {
1475 // include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1476 // $res = dol_move($olddir, $newdir);
1477 // do not use dol_move with directory
1478 $res = @rename($olddir, $newdir);
1479 if (!$res) {
1480 $langs->load("errors");
1481 $this->error = $langs->trans('ErrorFailToRenameDir', $olddir, $newdir);
1482 $error++;
1483 } else {
1484 // to keep old entries with the new dir
1485 require_once DOL_DOCUMENT_ROOT . '/ecm/class/ecmfiles.class.php';
1486 $ecmfiles = new EcmFiles($this->db);
1487 $ecmfiles->updateAfterRename("produit/".dol_sanitizeFileName($this->oldcopy->ref), "produit/".dol_sanitizeFileName($this->ref));
1488 }
1489 }
1490 }
1491 }
1492
1493 if (!$error) {
1494 if (isModEnabled('variants')) {
1495 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1496
1497 $comb = new ProductCombination($this->db);
1498
1499 foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) {
1500 $currcomb->updateProperties($this, $user);
1501 }
1502 }
1503
1504 $this->db->commit();
1505 return 1;
1506 } else {
1507 $this->db->rollback();
1508 return -$error;
1509 }
1510 } else {
1511 if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1512 $langs->load("errors");
1513 if (empty($conf->barcode->enabled) || empty($this->barcode)) {
1514 $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductAlreadyExists", $this->ref);
1515 } else {
1516 $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductBarCodeAlreadyExists", $this->barcode);
1517 }
1518 $this->errors[] = $this->error;
1519 $this->db->rollback();
1520 return -1;
1521 } else {
1522 $this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1523 $this->errors[] = $this->error;
1524 $this->db->rollback();
1525 return -2;
1526 }
1527 }
1528 } else {
1529 $this->db->rollback();
1530 dol_syslog(get_class($this)."::Update fails verify ".implode(',', $this->errors), LOG_WARNING);
1531 return -3;
1532 }
1533 }
1534
1542 public function delete(User $user, $notrigger = 0)
1543 {
1544 global $conf, $langs;
1545 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1546
1547 $error = 0;
1548
1549 // Check parameters
1550 if (empty($this->id)) {
1551 $this->error = "Object must be fetched before calling delete";
1552 return -1;
1553 }
1554 if (($this->isProduct() && !$user->hasRight('produit', 'supprimer')) || ($this->isService() && !$user->hasRight('service', 'supprimer'))) {
1555 $this->error = "ErrorForbidden";
1556 return 0;
1557 }
1558
1559 $objectisused = $this->isObjectUsed($this->id);
1560 if (empty($objectisused)) {
1561 $this->db->begin();
1562
1563 if (!$error && empty($notrigger)) {
1564 // Call trigger
1565 $result = $this->call_trigger('PRODUCT_DELETE', $user);
1566 if ($result < 0) {
1567 $error++;
1568 }
1569 // End call triggers
1570 }
1571
1572 // Delete from product_batch on product delete
1573 if (!$error) {
1574 $sql = "DELETE FROM ".$this->db->prefix().'product_batch';
1575 $sql .= " WHERE fk_product_stock IN (";
1576 $sql .= "SELECT rowid FROM ".$this->db->prefix().'product_stock';
1577 $sql .= " WHERE fk_product = ".((int) $this->id).")";
1578
1579 $result = $this->db->query($sql);
1580 if (!$result) {
1581 $error++;
1582 $this->errors[] = $this->db->lasterror();
1583 }
1584 }
1585
1586 // Delete all child tables
1587 if (!$error) {
1588 $elements = array('product_fournisseur_price', 'product_price', 'product_lang', 'categorie_product', 'product_stock', 'product_customer_price', 'product_lot'); // product_batch is done before
1589 foreach ($elements as $table) {
1590 if (!$error) {
1591 $sql = "DELETE FROM ".$this->db->prefix().$table;
1592 $sql .= " WHERE fk_product = ".(int) $this->id;
1593
1594 $result = $this->db->query($sql);
1595 if (!$result) {
1596 $error++;
1597 $this->errors[] = $this->db->lasterror();
1598 }
1599 }
1600 }
1601 }
1602
1603 if (!$error) {
1604 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1605 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
1606
1607 //If it is a parent product, then we remove the association with child products
1608 $prodcomb = new ProductCombination($this->db);
1609
1610 if ($prodcomb->deleteByFkProductParent($user, $this->id) < 0) {
1611 $error++;
1612 $this->errors[] = 'Error deleting combinations';
1613 }
1614
1615 //We also check if it is a child product
1616 if (!$error && ($prodcomb->fetchByFkProductChild($this->id) > 0) && ($prodcomb->delete($user) < 0)) {
1617 $error++;
1618 $this->errors[] = 'Error deleting child combination';
1619 }
1620 }
1621
1622 // Delete from product_association
1623 if (!$error) {
1624 $sql = "DELETE FROM ".$this->db->prefix()."product_association";
1625 $sql .= " WHERE fk_product_pere = ".(int) $this->id." OR fk_product_fils = ".(int) $this->id;
1626
1627 $result = $this->db->query($sql);
1628 if (!$result) {
1629 $error++;
1630 $this->errors[] = $this->db->lasterror();
1631 }
1632 }
1633
1634 // Remove extrafields
1635 if (!$error) {
1636 $result = $this->deleteExtraFields();
1637 if ($result < 0) {
1638 $error++;
1639 dol_syslog(get_class($this)."::delete error -4 ".$this->error, LOG_ERR);
1640 }
1641 }
1642
1643 // Delete product
1644 if (!$error) {
1645 $sqlz = "DELETE FROM ".$this->db->prefix()."product";
1646 $sqlz .= " WHERE rowid = ".(int) $this->id;
1647
1648 $resultz = $this->db->query($sqlz);
1649 if (!$resultz) {
1650 $error++;
1651 $this->errors[] = $this->db->lasterror();
1652 }
1653 }
1654
1655 // Delete record into ECM index and physically
1656 if (!$error) {
1657 $res = $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
1658 $res = $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
1659 if (!$res) {
1660 $error++;
1661 }
1662 }
1663
1664 if (!$error) {
1665 // We remove directory
1666 $ref = dol_sanitizeFileName($this->ref);
1667 if ($conf->product->dir_output) {
1668 $dir = $conf->product->dir_output."/".$ref;
1669 if (file_exists($dir)) {
1670 $res = @dol_delete_dir_recursive($dir);
1671 if (!$res) {
1672 $this->errors[] = 'ErrorFailToDeleteDir';
1673 $error++;
1674 }
1675 }
1676 }
1677 }
1678
1679 if (!$error) {
1680 $this->db->commit();
1681 return 1;
1682 } else {
1683 foreach ($this->errors as $errmsg) {
1684 dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
1685 $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
1686 }
1687 $this->db->rollback();
1688 return -$error;
1689 }
1690 } else {
1691 $this->error = "ErrorRecordIsUsedCantDelete";
1692 return 0;
1693 }
1694 }
1695
1701 public static function getSellOrEatByMandatoryList()
1702 {
1703 global $langs;
1704
1705 $sellByLabel = $langs->trans('SellByDate');
1706 $eatByLabel = $langs->trans('EatByDate');
1707 return array(
1708 self::SELL_OR_EAT_BY_MANDATORY_ID_NONE => $langs->trans('BatchSellOrEatByMandatoryNone'),
1709 self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY => $sellByLabel,
1710 self::SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY => $eatByLabel,
1711 self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT => $langs->trans('BatchSellOrEatByMandatoryAll', $sellByLabel, $eatByLabel),
1712 );
1713 }
1714
1721 {
1722 $sellOrEatByMandatoryLabel = '';
1723
1724 $sellOrEatByMandatoryList = self::getSellOrEatByMandatoryList();
1725 if (isset($sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory])) {
1726 $sellOrEatByMandatoryLabel = $sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory];
1727 }
1728
1729 return $sellOrEatByMandatoryLabel;
1730 }
1731
1738 public function setMultiLangs($user)
1739 {
1740 global $conf, $langs;
1741
1742 $langs_available = $langs->get_available_languages(DOL_DOCUMENT_ROOT, 0, 2);
1743 $current_lang = $langs->getDefaultLang();
1744
1745 foreach ($langs_available as $key => $value) {
1746 if ($key == $current_lang) {
1747 $sql = "SELECT rowid";
1748 $sql .= " FROM ".$this->db->prefix()."product_lang";
1749 $sql .= " WHERE fk_product = ".((int) $this->id);
1750 $sql .= " AND lang = '".$this->db->escape($key)."'";
1751
1752 $result = $this->db->query($sql);
1753
1754 if ($this->db->num_rows($result)) { // if there is already a description line for this language
1755 $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
1756 $sql2 .= " SET ";
1757 $sql2 .= " label='".$this->db->escape($this->label)."',";
1758 $sql2 .= " description='".$this->db->escape($this->description)."'";
1759 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1760 $sql2 .= ", note='".$this->db->escape($this->other)."'";
1761 }
1762 $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
1763 } else {
1764 $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
1765 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1766 $sql2 .= ", note";
1767 }
1768 $sql2 .= ")";
1769 $sql2 .= " VALUES(".((int) $this->id).",'".$this->db->escape($key)."','".$this->db->escape($this->label)."',";
1770 $sql2 .= " '".$this->db->escape($this->description)."'";
1771 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1772 $sql2 .= ", '".$this->db->escape($this->other)."'";
1773 }
1774 $sql2 .= ")";
1775 }
1776 dol_syslog(get_class($this).'::setMultiLangs key = current_lang = '.$key);
1777 if (!$this->db->query($sql2)) {
1778 $this->error = $this->db->lasterror();
1779 return -1;
1780 }
1781 } elseif (isset($this->multilangs[$key])) {
1782 if (empty($this->multilangs["$key"]["label"])) {
1783 $this->errors[] = $key . ' : ' . $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Label"));
1784 return -1;
1785 }
1786
1787 $sql = "SELECT rowid";
1788 $sql .= " FROM ".$this->db->prefix()."product_lang";
1789 $sql .= " WHERE fk_product = ".((int) $this->id);
1790 $sql .= " AND lang = '".$this->db->escape($key)."'";
1791
1792 $result = $this->db->query($sql);
1793
1794 if ($this->db->num_rows($result)) { // if there is already a description line for this language
1795 $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
1796 $sql2 .= " SET ";
1797 $sql2 .= " label = '".$this->db->escape($this->multilangs["$key"]["label"])."',";
1798 $sql2 .= " description = '".$this->db->escape($this->multilangs["$key"]["description"])."'";
1799 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1800 $sql2 .= ", note = '".$this->db->escape($this->multilangs["$key"]["other"])."'";
1801 }
1802 $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
1803 } else {
1804 $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
1805 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1806 $sql2 .= ", note";
1807 }
1808 $sql2 .= ")";
1809 $sql2 .= " VALUES(".((int) $this->id).",'".$this->db->escape($key)."','".$this->db->escape($this->multilangs["$key"]["label"])."',";
1810 $sql2 .= " '".$this->db->escape($this->multilangs["$key"]["description"])."'";
1811 if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1812 $sql2 .= ", '".$this->db->escape($this->multilangs["$key"]["other"])."'";
1813 }
1814 $sql2 .= ")";
1815 }
1816
1817 // We do not save if main fields are empty
1818 if ($this->multilangs["$key"]["label"] || $this->multilangs["$key"]["description"]) {
1819 if (!$this->db->query($sql2)) {
1820 $this->error = $this->db->lasterror();
1821 return -1;
1822 }
1823 }
1824 } else {
1825 // language is not current language and we didn't provide a multilang description for this language
1826 }
1827 }
1828
1829 // Call trigger
1830 $result = $this->call_trigger('PRODUCT_SET_MULTILANGS', $user);
1831 if ($result < 0) {
1832 $this->error = $this->db->lasterror();
1833 return -1;
1834 }
1835 // End call triggers
1836
1837 return 1;
1838 }
1839
1848 public function delMultiLangs($langtodelete, $user)
1849 {
1850 $sql = "DELETE FROM ".$this->db->prefix()."product_lang";
1851 $sql .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($langtodelete)."'";
1852
1853 dol_syslog(get_class($this).'::delMultiLangs', LOG_DEBUG);
1854 $result = $this->db->query($sql);
1855 if ($result) {
1856 // Call trigger
1857 $result = $this->call_trigger('PRODUCT_DEL_MULTILANGS', $user);
1858 if ($result < 0) {
1859 $this->error = $this->db->lasterror();
1860 dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
1861 return -1;
1862 }
1863 // End call triggers
1864 return 1;
1865 } else {
1866 $this->error = $this->db->lasterror();
1867 dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
1868 return -1;
1869 }
1870 }
1871
1880 public function setAccountancyCode($type, $value)
1881 {
1882 global $user, $langs, $conf;
1883
1884 $error = 0;
1885
1886 $this->db->begin();
1887
1888 if ($type == 'buy') {
1889 $field = 'accountancy_code_buy';
1890 } elseif ($type == 'buy_intra') {
1891 $field = 'accountancy_code_buy_intra';
1892 } elseif ($type == 'buy_export') {
1893 $field = 'accountancy_code_buy_export';
1894 } elseif ($type == 'sell') {
1895 $field = 'accountancy_code_sell';
1896 } elseif ($type == 'sell_intra') {
1897 $field = 'accountancy_code_sell_intra';
1898 } elseif ($type == 'sell_export') {
1899 $field = 'accountancy_code_sell_export';
1900 } else {
1901 return -1;
1902 }
1903
1904 $sql = "UPDATE ".$this->db->prefix().$this->table_element." SET ";
1905 $sql .= "$field = '".$this->db->escape($value)."'";
1906 $sql .= " WHERE rowid = ".((int) $this->id);
1907
1908 dol_syslog(__METHOD__, LOG_DEBUG);
1909 $resql = $this->db->query($sql);
1910
1911 if ($resql) {
1912 // Call trigger
1913 $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1914 if ($result < 0) {
1915 $error++;
1916 }
1917 // End call triggers
1918
1919 if ($error) {
1920 $this->db->rollback();
1921 return -1;
1922 }
1923
1924 $this->$field = $value;
1925
1926 $this->db->commit();
1927 return 1;
1928 } else {
1929 $this->error = $this->db->lasterror();
1930 $this->db->rollback();
1931 return -1;
1932 }
1933 }
1934
1940 public function getMultiLangs()
1941 {
1942 global $langs;
1943
1944 $current_lang = $langs->getDefaultLang();
1945
1946 $sql = "SELECT lang, label, description, note as other";
1947 $sql .= " FROM ".$this->db->prefix()."product_lang";
1948 $sql .= " WHERE fk_product = ".((int) $this->id);
1949
1950 $result = $this->db->query($sql);
1951 if ($result) {
1952 while ($obj = $this->db->fetch_object($result)) {
1953 //print 'lang='.$obj->lang.' current='.$current_lang.'<br>';
1954 if ($obj->lang == $current_lang) { // si on a les traduct. dans la langue courante on les charge en infos principales.
1955 $this->label = $obj->label;
1956 $this->description = $obj->description;
1957 $this->other = $obj->other;
1958 }
1959 $this->multilangs[(string) $obj->lang]["label"] = $obj->label;
1960 $this->multilangs[(string) $obj->lang]["description"] = $obj->description;
1961 $this->multilangs[(string) $obj->lang]["other"] = $obj->other;
1962 }
1963 return 1;
1964 } else {
1965 $this->error = "Error: ".$this->db->lasterror()." - ".$sql;
1966 return -1;
1967 }
1968 }
1969
1976 private function getArrayForPriceCompare($level = 0)
1977 {
1978 $testExit = array('multiprices','multiprices_ttc','multiprices_base_type','multiprices_min','multiprices_min_ttc','multiprices_tva_tx','multiprices_recuperableonly');
1979
1980 foreach ($testExit as $field) {
1981 if (!isset($this->$field)) {
1982 return array();
1983 }
1984 $tmparray = $this->$field;
1985 if (!isset($tmparray[$level])) {
1986 return array();
1987 }
1988 }
1989
1990 $lastPrice = array(
1991 'level' => $level ? $level : 1,
1992 'multiprices' => (float) $this->multiprices[$level],
1993 'multiprices_ttc' => (float) $this->multiprices_ttc[$level],
1994 'multiprices_base_type' => $this->multiprices_base_type[$level],
1995 'multiprices_min' => (float) $this->multiprices_min[$level],
1996 'multiprices_min_ttc' => (float) $this->multiprices_min_ttc[$level],
1997 'multiprices_tva_tx' => (float) $this->multiprices_tva_tx[$level],
1998 'multiprices_recuperableonly' => (float) $this->multiprices_recuperableonly[$level],
1999 );
2000
2001 return $lastPrice;
2002 }
2003
2004
2005 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2013 private function _log_price($user, $level = 0)
2014 {
2015 // phpcs:enable
2016 global $conf;
2017
2018 $now = dol_now();
2019
2020 // Clean parameters
2021 if (empty($this->price_by_qty)) {
2022 $this->price_by_qty = 0;
2023 }
2024
2025 // Add new price
2026 $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,";
2027 $sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, price_min,price_min_ttc,price_by_qty,entity,fk_price_expression) ";
2028 $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).",";
2029 $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');
2030 $sql .= ")";
2031
2032 dol_syslog(get_class($this)."::_log_price", LOG_DEBUG);
2033 $resql = $this->db->query($sql);
2034 if (!$resql) {
2035 $this->error = $this->db->lasterror();
2036 dol_print_error($this->db);
2037 return -1;
2038 } else {
2039 return 1;
2040 }
2041 }
2042
2043
2044 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2052 public function log_price_delete($user, $rowid)
2053 {
2054 // phpcs:enable
2055 $sql = "DELETE FROM ".$this->db->prefix()."product_price_by_qty";
2056 $sql .= " WHERE fk_product_price = ".((int) $rowid);
2057 $resql = $this->db->query($sql);
2058
2059 $sql = "DELETE FROM ".$this->db->prefix()."product_price";
2060 $sql .= " WHERE rowid=".((int) $rowid);
2061 $resql = $this->db->query($sql);
2062 if ($resql) {
2063 return 1;
2064 } else {
2065 $this->error = $this->db->lasterror();
2066 return -1;
2067 }
2068 }
2069
2070
2080 public function getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp = 0)
2081 {
2082 global $conf, $hookmanager, $action;
2083
2084 // Call hook if any
2085 if (is_object($hookmanager)) {
2086 $parameters = array('thirdparty_seller' => $thirdparty_seller, 'thirdparty_buyer' => $thirdparty_buyer, 'pqp' => $pqp);
2087 // Note that $action and $object may have been modified by some hooks
2088 $reshook = $hookmanager->executeHooks('getSellPrice', $parameters, $this, $action);
2089 if ($reshook > 0) {
2090 return $hookmanager->resArray;
2091 }
2092 }
2093
2094 // Update if prices fields are defined
2095 $tva_tx = get_default_tva($thirdparty_seller, $thirdparty_buyer, $this->id);
2096 $tva_npr = get_default_npr($thirdparty_seller, $thirdparty_buyer, $this->id);
2097 if (empty($tva_tx)) {
2098 $tva_npr = 0;
2099 }
2100
2101 $pu_ht = $this->price;
2102 $pu_ttc = $this->price_ttc;
2103 $price_min = $this->price_min;
2104 $price_base_type = $this->price_base_type;
2105
2106 // If price per segment
2107 if (getDolGlobalString('PRODUIT_MULTIPRICES') && !empty($thirdparty_buyer->price_level)) {
2108 $pu_ht = $this->multiprices[$thirdparty_buyer->price_level];
2109 $pu_ttc = $this->multiprices_ttc[$thirdparty_buyer->price_level];
2110 $price_min = $this->multiprices_min[$thirdparty_buyer->price_level];
2111 $price_base_type = $this->multiprices_base_type[$thirdparty_buyer->price_level];
2112 if (getDolGlobalString('PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL')) { // using this option is a bug. kept for backward compatibility
2113 if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
2114 $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
2115 }
2116 if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
2117 $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
2118 }
2119 if (empty($tva_tx)) {
2120 $tva_npr = 0;
2121 }
2122 }
2123 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES')) {
2124 // If price per customer
2125 require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
2126
2127 $prodcustprice = new ProductCustomerPrice($this->db);
2128
2129 $filter = array('t.fk_product' => $this->id, 't.fk_soc' => $thirdparty_buyer->id);
2130
2131 $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
2132 if ($result) {
2133 if (count($prodcustprice->lines) > 0) {
2134 $pu_ht = price($prodcustprice->lines[0]->price);
2135 $price_min = price($prodcustprice->lines[0]->price_min);
2136 $pu_ttc = price($prodcustprice->lines[0]->price_ttc);
2137 $price_base_type = $prodcustprice->lines[0]->price_base_type;
2138 $tva_tx = $prodcustprice->lines[0]->tva_tx;
2139 if ($prodcustprice->lines[0]->default_vat_code && !preg_match('/\‍(.*\‍)/', $tva_tx)) {
2140 $tva_tx .= ' ('.$prodcustprice->lines[0]->default_vat_code.')';
2141 }
2142 $tva_npr = $prodcustprice->lines[0]->recuperableonly;
2143 if (empty($tva_tx)) {
2144 $tva_npr = 0;
2145 }
2146 }
2147 }
2148 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY')) {
2149 // If price per quantity
2150 if ($this->prices_by_qty[0]) {
2151 // yes, this product has some prices per quantity
2152 // Search price into product_price_by_qty from $this->id
2153 foreach ($this->prices_by_qty_list[0] as $priceforthequantityarray) {
2154 if ($priceforthequantityarray['rowid'] != $pqp) {
2155 continue;
2156 }
2157 // We found the price
2158 if ($priceforthequantityarray['price_base_type'] == 'HT') {
2159 $pu_ht = $priceforthequantityarray['unitprice'];
2160 } else {
2161 $pu_ttc = $priceforthequantityarray['unitprice'];
2162 }
2163 break;
2164 }
2165 }
2166 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) {
2167 // If price per quantity and customer
2168 if ($this->prices_by_qty[$thirdparty_buyer->price_level]) {
2169 // yes, this product has some prices per quantity
2170 // Search price into product_price_by_qty from $this->id
2171 foreach ($this->prices_by_qty_list[$thirdparty_buyer->price_level] as $priceforthequantityarray) {
2172 if ($priceforthequantityarray['rowid'] != $pqp) {
2173 continue;
2174 }
2175 // We found the price
2176 if ($priceforthequantityarray['price_base_type'] == 'HT') {
2177 $pu_ht = $priceforthequantityarray['unitprice'];
2178 } else {
2179 $pu_ttc = $priceforthequantityarray['unitprice'];
2180 }
2181 break;
2182 }
2183 }
2184 }
2185
2186 return array('pu_ht' => $pu_ht, 'pu_ttc' => $pu_ttc, 'price_min' => $price_min, 'price_base_type' => $price_base_type, 'tva_tx' => $tva_tx, 'tva_npr' => $tva_npr);
2187 }
2188
2189 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2203 public function get_buyprice($prodfournprice, $qty, $product_id = 0, $fourn_ref = '', $fk_soc = 0)
2204 {
2205 // phpcs:enable
2206 global $action, $hookmanager;
2207
2208 // Call hook if any
2209 if (is_object($hookmanager)) {
2210 $parameters = array(
2211 'prodfournprice' => $prodfournprice,
2212 'qty' => $qty,
2213 'product_id' => $product_id,
2214 'fourn_ref' => $fourn_ref,
2215 'fk_soc' => $fk_soc,
2216 );
2217 // Note that $action and $object may have been modified by some hooks
2218 $reshook = $hookmanager->executeHooks('getBuyPrice', $parameters, $this, $action);
2219 if ($reshook > 0) {
2220 return $hookmanager->resArray;
2221 }
2222 }
2223
2224 $result = 0;
2225
2226 // 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)
2227 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2228 $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,";
2229 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2230 $sql .= " pfp.packaging";
2231 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2232 $sql .= " WHERE pfp.rowid = ".((int) $prodfournprice);
2233 if ($qty > 0) {
2234 $sql .= " AND pfp.quantity <= ".((float) $qty);
2235 }
2236 $sql .= " ORDER BY pfp.quantity DESC";
2237
2238 dol_syslog(get_class($this)."::get_buyprice first search by prodfournprice/qty", LOG_DEBUG);
2239 $resql = $this->db->query($sql);
2240 if ($resql) {
2241 $obj = $this->db->fetch_object($resql);
2242 if ($obj && $obj->quantity > 0) { // If we found a supplier prices from the id of supplier price
2243 if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2244 $prod_supplier = new ProductFournisseur($this->db);
2245 $prod_supplier->product_fourn_price_id = $obj->rowid;
2246 $prod_supplier->id = $obj->fk_product;
2247 $prod_supplier->fourn_qty = $obj->quantity;
2248 $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2249 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2250
2251 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2252 $priceparser = new PriceParser($this->db);
2253 $price_result = $priceparser->parseProductSupplier($prod_supplier);
2254 if ($price_result >= 0) {
2255 $obj->price = $price_result;
2256 }
2257 }
2258 $this->product_fourn_price_id = $obj->rowid;
2259 $this->buyprice = $obj->price; // deprecated
2260 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product of supplier
2261 $this->fourn_price_base_type = 'HT'; // Price base type
2262 $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2263 $this->ref_fourn = $obj->ref_supplier; // deprecated
2264 $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2265 $this->desc_supplier = $obj->desc_supplier; // desc supplier
2266 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2267 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2268 $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2269 $this->fourn_multicurrency_price = $obj->multicurrency_price;
2270 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2271 $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2272 $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2273 $this->fourn_multicurrency_code = $obj->multicurrency_code;
2274 if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2275 $this->packaging = $obj->packaging;
2276 }
2277 $result = $obj->fk_product;
2278 return $result;
2279 } else { // If not found
2280 // 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.
2281 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2282 $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,";
2283 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2284 $sql .= " pfp.packaging";
2285 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2286 $sql .= " WHERE 1 = 1";
2287 if ($product_id > 0) {
2288 $sql .= " AND pfp.fk_product = ".((int) $product_id);
2289 }
2290 if ($fourn_ref != 'none') {
2291 $sql .= " AND pfp.ref_fourn = '".$this->db->escape($fourn_ref)."'";
2292 }
2293 if ($fk_soc > 0) {
2294 $sql .= " AND pfp.fk_soc = ".((int) $fk_soc);
2295 }
2296 if ($qty > 0) {
2297 $sql .= " AND pfp.quantity <= ".((float) $qty);
2298 }
2299 $sql .= " ORDER BY pfp.quantity DESC";
2300 $sql .= " LIMIT 1";
2301
2302 dol_syslog(get_class($this)."::get_buyprice second search from qty/ref/product_id", LOG_DEBUG);
2303 $resql = $this->db->query($sql);
2304 if ($resql) {
2305 $obj = $this->db->fetch_object($resql);
2306 if ($obj && $obj->quantity > 0) { // If found
2307 if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2308 $prod_supplier = new ProductFournisseur($this->db);
2309 $prod_supplier->product_fourn_price_id = $obj->rowid;
2310 $prod_supplier->id = $obj->fk_product;
2311 $prod_supplier->fourn_qty = $obj->quantity;
2312 $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2313 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2314
2315 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2316 $priceparser = new PriceParser($this->db);
2317 $price_result = $priceparser->parseProductSupplier($prod_supplier);
2318 if ($result >= 0) {
2319 $obj->price = $price_result;
2320 }
2321 }
2322 $this->product_fourn_price_id = $obj->rowid;
2323 $this->buyprice = $obj->price; // deprecated
2324 $this->fourn_qty = $obj->quantity; // min quantity for price for a virtual supplier
2325 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product for a virtual supplier
2326 $this->fourn_price_base_type = 'HT'; // Price base type for a virtual supplier
2327 $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2328 $this->ref_fourn = $obj->ref_supplier; // deprecated
2329 $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2330 $this->desc_supplier = $obj->desc_supplier; // desc supplier
2331 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2332 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2333 $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2334 $this->fourn_multicurrency_price = $obj->multicurrency_price;
2335 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2336 $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2337 $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2338 $this->fourn_multicurrency_code = $obj->multicurrency_code;
2339 if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2340 $this->packaging = $obj->packaging;
2341 }
2342 $result = $obj->fk_product;
2343 return $result;
2344 } else {
2345 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é.
2346 }
2347 } else {
2348 $this->error = $this->db->lasterror();
2349 return -3;
2350 }
2351 }
2352 } else {
2353 $this->error = $this->db->lasterror();
2354 return -2;
2355 }
2356 }
2357
2358
2377 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)
2378 {
2379 global $conf, $langs;
2380
2381 $lastPriceData = $this->getArrayForPriceCompare($level); // temporary store current price before update
2382
2383 $id = $this->id;
2384
2385 dol_syslog(get_class($this)."::update_price id=".$id." newprice=".$newprice." newpricebase=".$newpricebase." newminprice=".$newminprice." level=".$level." npr=".$newnpr." newdefaultvatcode=".$newdefaultvatcode);
2386
2387 // Clean parameters
2388 if (empty($this->tva_tx)) {
2389 $this->tva_tx = 0;
2390 }
2391 if (empty($newnpr)) {
2392 $newnpr = 0;
2393 }
2394 if (empty($newminprice)) {
2395 $newminprice = 0;
2396 }
2397
2398 // Check parameters
2399 if ($newvat === null || $newvat == '') { // Maintain '' for backwards compatibility
2400 $newvat = $this->tva_tx;
2401 }
2402
2403 // If multiprices are enabled, then we check if the current product is subject to price autogeneration
2404 // Price will be modified ONLY when the first one is the one that is being modified
2405 if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) && !$ignore_autogen && $this->price_autogen && ($level == 1)) {
2406 return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpbq);
2407 }
2408
2409 if (!empty($newminprice) && ($newminprice > $newprice)) {
2410 $this->error = 'ErrorPriceCantBeLowerThanMinPrice';
2411 return -1;
2412 }
2413
2414 if ($newprice === 0 || $newprice !== '') {
2415 if ($newpricebase == 'TTC') {
2416 $price_ttc = price2num($newprice, 'MU');
2417 $price = (float) price2num($newprice) / (1 + ((float) $newvat / 100));
2418 $price = price2num($price, 'MU');
2419
2420 if ($newminprice != '' || $newminprice == 0) {
2421 $price_min_ttc = price2num($newminprice, 'MU');
2422 $price_min = (float) price2num($newminprice) / (1 + ($newvat / 100));
2423 $price_min = price2num($price_min, 'MU');
2424 } else {
2425 $price_min = 0;
2426 $price_min_ttc = 0;
2427 }
2428 } else {
2429 $price = (float) price2num($newprice, 'MU');
2430 $price_ttc = ($newnpr != 1) ? price2num($newprice) * (1 + ($newvat / 100)) : $price;
2431 $price_ttc = (float) price2num($price_ttc, 'MU');
2432
2433 if ($newminprice !== '' || $newminprice === 0) {
2434 $price_min = price2num($newminprice, 'MU');
2435 $price_min_ttc = (float) price2num($newminprice) * (1 + ($newvat / 100));
2436 $price_min_ttc = price2num($price_min_ttc, 'MU');
2437 //print 'X'.$newminprice.'-'.$price_min;
2438 } else {
2439 $price_min = 0;
2440 $price_min_ttc = 0;
2441 }
2442 }
2443 //print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc;
2444
2445 if (count($localtaxes_array) > 0) {
2446 $localtaxtype1 = $localtaxes_array['0'];
2447 $localtax1 = $localtaxes_array['1'];
2448 $localtaxtype2 = $localtaxes_array['2'];
2449 $localtax2 = $localtaxes_array['3'];
2450 } else {
2451 // if array empty, we try to use the vat code
2452 if (!empty($newdefaultvatcode)) {
2453 global $mysoc;
2454 // Get record from code
2455 $sql = "SELECT t.rowid, t.code, t.recuperableonly as tva_npr, t.localtax1, t.localtax2, t.localtax1_type, t.localtax2_type";
2456 $sql .= " FROM ".MAIN_DB_PREFIX."c_tva as t, ".MAIN_DB_PREFIX."c_country as c";
2457 $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '".$this->db->escape($mysoc->country_code)."'";
2458 $sql .= " AND t.taux = ".((float) $newdefaultvatcode)." AND t.active = 1";
2459 $sql .= " AND t.code = '".$this->db->escape($newdefaultvatcode)."'";
2460 $resql = $this->db->query($sql);
2461 if ($resql) {
2462 $obj = $this->db->fetch_object($resql);
2463 if ($obj) {
2464 $npr = $obj->tva_npr;
2465 $localtax1 = $obj->localtax1;
2466 $localtax2 = $obj->localtax2;
2467 $localtaxtype1 = $obj->localtax1_type;
2468 $localtaxtype2 = $obj->localtax2_type;
2469 }
2470 }
2471 } else {
2472 // old method. deprecated because we can't retrieve type
2473 $localtaxtype1 = '0';
2474 $localtax1 = get_localtax($newvat, 1);
2475 $localtaxtype2 = '0';
2476 $localtax2 = get_localtax($newvat, 2);
2477 }
2478 }
2479 if (empty($localtax1)) {
2480 $localtax1 = 0; // If = '' then = 0
2481 }
2482 if (empty($localtax2)) {
2483 $localtax2 = 0; // If = '' then = 0
2484 }
2485
2486 $this->db->begin();
2487
2488 // Ne pas mettre de quote sur les numeriques decimaux.
2489 // Ceci provoque des stockages avec arrondis en base au lieu des valeurs exactes.
2490 $sql = "UPDATE ".$this->db->prefix()."product SET";
2491 $sql .= " price_base_type = '".$this->db->escape($newpricebase)."',";
2492 $sql .= " price = ".(float) $price.",";
2493 $sql .= " price_ttc = ".(float) $price_ttc.",";
2494 $sql .= " price_min = ".(float) $price_min.",";
2495 $sql .= " price_min_ttc = ".(float) $price_min_ttc.",";
2496 $sql .= " localtax1_tx = ".($localtax1 >= 0 ? (float) $localtax1 : 'NULL').",";
2497 $sql .= " localtax2_tx = ".($localtax2 >= 0 ? (float) $localtax2 : 'NULL').",";
2498 $sql .= " localtax1_type = ".($localtaxtype1 != '' ? "'".$this->db->escape($localtaxtype1)."'" : "'0'").",";
2499 $sql .= " localtax2_type = ".($localtaxtype2 != '' ? "'".$this->db->escape($localtaxtype2)."'" : "'0'").",";
2500 $sql .= " default_vat_code = ".($newdefaultvatcode ? "'".$this->db->escape($newdefaultvatcode)."'" : "null").",";
2501 $sql .= " price_label = ".(!empty($price_label) ? "'".$this->db->escape($price_label)."'" : "null").",";
2502 $sql .= " tva_tx = ".(float) price2num($newvat).",";
2503 $sql .= " recuperableonly = '".$this->db->escape($newnpr)."'";
2504 $sql .= " WHERE rowid = ".((int) $id);
2505
2506 dol_syslog(get_class($this)."::update_price", LOG_DEBUG);
2507 $resql = $this->db->query($sql);
2508 if ($resql) {
2509 $this->multiprices[$level] = $price;
2510 $this->multiprices_ttc[$level] = $price_ttc;
2511 $this->multiprices_min[$level] = $price_min;
2512 $this->multiprices_min_ttc[$level] = $price_min_ttc;
2513 $this->multiprices_base_type[$level] = $newpricebase;
2514 $this->multiprices_default_vat_code[$level] = $newdefaultvatcode;
2515 $this->multiprices_tva_tx[$level] = $newvat;
2516 $this->multiprices_recuperableonly[$level] = $newnpr;
2517
2518 $this->price = $price;
2519 $this->price_label = $price_label;
2520 $this->price_ttc = $price_ttc;
2521 $this->price_min = $price_min;
2522 $this->price_min_ttc = $price_min_ttc;
2523 $this->price_base_type = $newpricebase;
2524 $this->default_vat_code = $newdefaultvatcode;
2525 $this->tva_tx = $newvat;
2526 $this->tva_npr = $newnpr;
2527
2528 //Local taxes
2529 $this->localtax1_tx = $localtax1;
2530 $this->localtax2_tx = $localtax2;
2531 $this->localtax1_type = $localtaxtype1;
2532 $this->localtax2_type = $localtaxtype2;
2533
2534 // Price by quantity
2535 $this->price_by_qty = $newpbq;
2536
2537 // check if price have really change before log
2538 $newPriceData = $this->getArrayForPriceCompare($level);
2539 if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || !getDolGlobalString('PRODUIT_MULTIPRICES')) {
2540 $this->_log_price($user, $level); // Save price for level into table product_price
2541 }
2542
2543 $this->level = $level; // Store level of price edited for trigger
2544
2545 // Call trigger
2546 if (!$notrigger) {
2547 $result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user);
2548 if ($result < 0) {
2549 $this->db->rollback();
2550 return -1;
2551 }
2552 }
2553 // End call triggers
2554
2555 $this->db->commit();
2556 } else {
2557 $this->db->rollback();
2558 $this->error = $this->db->lasterror();
2559 return -1;
2560 }
2561 }
2562
2563 return 1;
2564 }
2565
2573 public function setPriceExpression($expression_id)
2574 {
2575 global $user;
2576
2577 $this->fk_price_expression = $expression_id;
2578
2579 return $this->update($this->id, $user);
2580 }
2581
2594 public function fetch($id = 0, $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0)
2595 {
2596 include_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
2597
2598 global $langs, $conf;
2599
2600 dol_syslog(get_class($this)."::fetch id=".$id." ref=".$ref." ref_ext=".$ref_ext);
2601
2602 // Check parameters
2603 if (!$id && !$ref && !$ref_ext && !$barcode) {
2604 $this->error = 'ErrorWrongParameters';
2605 dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR);
2606 return -1;
2607 }
2608
2609 $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,";
2610 $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,";
2611 $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,";
2612 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.last_main_doc,";
2613 $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,";
2614 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
2615 $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,";
2616 } else {
2617 $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,";
2618 }
2619
2620 //For MultiCompany
2621 //PMP per entity & Stocks Sharings stock_reel includes only stocks shared with this entity
2622 $separatedEntityPMP = false; // Set to true to get the AWP from table llx_product_perentity instead of field 'pmp' into llx_product.
2623 $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.
2624 $visibleWarehousesEntities = $conf->entity;
2625 if (getDolGlobalString('MULTICOMPANY_PRODUCT_SHARING_ENABLED')) {
2626 if (getDolGlobalString('MULTICOMPANY_PMP_PER_ENTITY_ENABLED')) {
2627 $checkPMPPerEntity = $this->db->query("SELECT pmp FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = ".((int) $id)." AND entity = ".(int) $conf->entity);
2628 if ($this->db->num_rows($checkPMPPerEntity) > 0) {
2629 $separatedEntityPMP = true;
2630 }
2631 }
2632 global $mc;
2633 $separatedStock = true;
2634 if (isset($mc->sharings['stock']) && !empty($mc->sharings['stock'])) {
2635 $visibleWarehousesEntities .= "," . implode(",", $mc->sharings['stock']);
2636 }
2637 }
2638 if ($separatedEntityPMP) {
2639 $sql .= " ppe.pmp,";
2640 } else {
2641 $sql .= " p.pmp,";
2642 }
2643 $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,";
2644 $sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf,";
2645 $sql .= " p.price_label,";
2646 if ($separatedStock) {
2647 $sql .= " SUM(sp.reel) as stock";
2648 } else {
2649 $sql .= " p.stock";
2650 }
2651 $sql .= " FROM ".$this->db->prefix()."product as p";
2652 if (getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED') || $separatedEntityPMP) {
2653 $sql .= " LEFT JOIN " . $this->db->prefix() . "product_perentity as ppe ON ppe.fk_product = p.rowid AND ppe.entity = " . ((int) $conf->entity);
2654 }
2655 if ($separatedStock) {
2656 $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)."))";
2657 }
2658
2659 if ($id) {
2660 $sql .= " WHERE p.rowid = ".((int) $id);
2661 } else {
2662 $sql .= " WHERE p.entity IN (".getEntity($this->element).")";
2663 if ($ref) {
2664 $sql .= " AND p.ref = '".$this->db->escape($ref)."'";
2665 } elseif ($ref_ext) {
2666 $sql .= " AND p.ref_ext = '".$this->db->escape($ref_ext)."'";
2667 } elseif ($barcode) {
2668 $sql .= " AND p.barcode = '".$this->db->escape($barcode)."'";
2669 }
2670 }
2671 if ($separatedStock) {
2672 $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,";
2673 $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,";
2674 $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,";
2675 $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
2676 $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,";
2677 if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
2678 $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,";
2679 } else {
2680 $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,";
2681 }
2682 if ($separatedEntityPMP) {
2683 $sql .= " ppe.pmp,";
2684 } else {
2685 $sql .= " p.pmp,";
2686 }
2687 $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,";
2688 $sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf";
2689 $sql .= " ,p.price_label";
2690 if (!$separatedStock) {
2691 $sql .= ", p.stock";
2692 }
2693 }
2694
2695 $resql = $this->db->query($sql);
2696 if ($resql) {
2697 unset($this->oldcopy);
2698
2699 if ($this->db->num_rows($resql) > 0) {
2700 $obj = $this->db->fetch_object($resql);
2701
2702 $this->id = $obj->rowid;
2703 $this->ref = $obj->ref;
2704 $this->ref_ext = $obj->ref_ext;
2705 $this->label = $obj->label;
2706 $this->description = $obj->description;
2707 $this->url = $obj->url;
2708 $this->note_public = $obj->note_public;
2709 $this->note_private = $obj->note_private;
2710 $this->note = $obj->note_private; // deprecated
2711
2712 $this->type = $obj->fk_product_type;
2713 $this->price_label = $obj->price_label;
2714 $this->status = $obj->tosell;
2715 $this->status_buy = $obj->tobuy;
2716 $this->status_batch = $obj->tobatch;
2717 $this->sell_or_eat_by_mandatory = $obj->sell_or_eat_by_mandatory;
2718 $this->batch_mask = $obj->batch_mask;
2719
2720 $this->customcode = $obj->customcode;
2721 $this->country_id = $obj->fk_country;
2722 $this->country_code = getCountry($this->country_id, 2, $this->db);
2723 $this->state_id = $obj->fk_state;
2724 $this->lifetime = $obj->lifetime;
2725 $this->qc_frequency = $obj->qc_frequency;
2726 $this->price = $obj->price;
2727 $this->price_ttc = $obj->price_ttc;
2728 $this->price_min = $obj->price_min;
2729 $this->price_min_ttc = $obj->price_min_ttc;
2730 $this->price_base_type = $obj->price_base_type;
2731 $this->cost_price = $obj->cost_price;
2732 $this->default_vat_code = $obj->default_vat_code;
2733 $this->tva_tx = $obj->tva_tx;
2735 $this->tva_npr = $obj->tva_npr;
2737 $this->localtax1_tx = $obj->localtax1_tx;
2738 $this->localtax2_tx = $obj->localtax2_tx;
2739 $this->localtax1_type = $obj->localtax1_type;
2740 $this->localtax2_type = $obj->localtax2_type;
2741
2742 $this->finished = $obj->finished;
2743 $this->fk_default_bom = $obj->fk_default_bom;
2744
2745 $this->duration = $obj->duration;
2746 $this->duration_value = $obj->duration ? substr($obj->duration, 0, dol_strlen($obj->duration) - 1) : null;
2747 $this->duration_unit = $obj->duration ? substr($obj->duration, -1) : null;
2748 $this->canvas = $obj->canvas;
2749 $this->net_measure = $obj->net_measure;
2750 $this->net_measure_units = $obj->net_measure_units;
2751 $this->weight = $obj->weight;
2752 $this->weight_units = $obj->weight_units;
2753 $this->length = $obj->length;
2754 $this->length_units = $obj->length_units;
2755 $this->width = $obj->width;
2756 $this->width_units = $obj->width_units;
2757 $this->height = $obj->height;
2758 $this->height_units = $obj->height_units;
2759
2760 $this->surface = $obj->surface;
2761 $this->surface_units = $obj->surface_units;
2762 $this->volume = $obj->volume;
2763 $this->volume_units = $obj->volume_units;
2764 $this->barcode = $obj->barcode;
2765 $this->barcode_type = $obj->fk_barcode_type;
2766
2767 $this->accountancy_code_buy = $obj->accountancy_code_buy;
2768 $this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra;
2769 $this->accountancy_code_buy_export = $obj->accountancy_code_buy_export;
2770 $this->accountancy_code_sell = $obj->accountancy_code_sell;
2771 $this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra;
2772 $this->accountancy_code_sell_export = $obj->accountancy_code_sell_export;
2773
2774 $this->fk_default_warehouse = $obj->fk_default_warehouse;
2775 $this->fk_default_workstation = $obj->fk_default_workstation;
2776 $this->seuil_stock_alerte = $obj->seuil_stock_alerte;
2777 $this->desiredstock = $obj->desiredstock;
2778 $this->stock_reel = $obj->stock;
2779 $this->pmp = $obj->pmp;
2780
2781 $this->date_creation = $obj->datec;
2782 $this->date_modification = $obj->tms;
2783 $this->import_key = $obj->import_key;
2784 $this->entity = $obj->entity;
2785
2786 $this->ref_ext = $obj->ref_ext;
2787 $this->fk_price_expression = $obj->fk_price_expression;
2788 $this->fk_unit = $obj->fk_unit;
2789 $this->price_autogen = $obj->price_autogen;
2790 $this->model_pdf = $obj->model_pdf;
2791 $this->last_main_doc = $obj->last_main_doc;
2792
2793 $this->mandatory_period = $obj->mandatory_period;
2794
2795 $this->db->free($resql);
2796
2797 // fetch optionals attributes and labels
2798 $this->fetch_optionals();
2799
2800 // Multilangs
2801 if (getDolGlobalInt('MAIN_MULTILANGS') && empty($ignore_lang_load)) {
2802 $this->getMultiLangs();
2803 }
2804
2805 // Load multiprices array
2806 if (getDolGlobalString('PRODUIT_MULTIPRICES') && empty($ignore_price_load)) { // prices per segment
2807 $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
2808 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
2809 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2810 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
2811 $sql .= " ,price_label";
2812 $sql .= " FROM ".$this->db->prefix()."product_price";
2813 $sql .= " WHERE entity IN (".getEntity('productprice').")";
2814 $sql .= " AND price_level=".((int) $i);
2815 $sql .= " AND fk_product = ".((int) $this->id);
2816 $sql .= " ORDER BY date_price DESC, rowid DESC"; // Get the most recent line
2817 $sql .= " LIMIT 1"; // Only the first one
2818 $resql = $this->db->query($sql);
2819 if ($resql) {
2820 $result = $this->db->fetch_array($resql);
2821
2822 $this->multiprices[$i] = $result ? $result["price"] : null;
2823 $this->multiprices_ttc[$i] = $result ? $result["price_ttc"] : null;
2824 $this->multiprices_min[$i] = $result ? $result["price_min"] : null;
2825 $this->multiprices_min_ttc[$i] = $result ? $result["price_min_ttc"] : null;
2826 $this->multiprices_base_type[$i] = $result ? $result["price_base_type"] : null;
2827 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
2828 $this->multiprices_tva_tx[$i] = $result ? $result["tva_tx"].($result ? ' ('.$result['default_vat_code'].')' : '') : null;
2829 $this->multiprices_recuperableonly[$i] = $result ? $result["recuperableonly"] : null;
2830
2831 // Price by quantity
2832 /*
2833 $this->prices_by_qty[$i]=$result["price_by_qty"];
2834 $this->prices_by_qty_id[$i]=$result["rowid"];
2835 // Récuperation de la liste des prix selon qty si flag positionné
2836 if ($this->prices_by_qty[$i] == 1)
2837 {
2838 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
2839 $sql.= " FROM ".$this->db->prefix()."product_price_by_qty";
2840 $sql.= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
2841 $sql.= " ORDER BY quantity ASC";
2842
2843 $resql = $this->db->query($sql);
2844 if ($resql)
2845 {
2846 $resultat=array();
2847 $ii=0;
2848 while ($result= $this->db->fetch_array($resql)) {
2849 $resultat[$ii]=array();
2850 $resultat[$ii]["rowid"]=$result["rowid"];
2851 $resultat[$ii]["price"]= $result["price"];
2852 $resultat[$ii]["unitprice"]= $result["unitprice"];
2853 $resultat[$ii]["quantity"]= $result["quantity"];
2854 $resultat[$ii]["remise_percent"]= $result["remise_percent"];
2855 $resultat[$ii]["remise"]= $result["remise"]; // deprecated
2856 $resultat[$ii]["price_base_type"]= $result["price_base_type"];
2857 $ii++;
2858 }
2859 $this->prices_by_qty_list[$i]=$resultat;
2860 }
2861 else
2862 {
2863 dol_print_error($this->db);
2864 return -1;
2865 }
2866 }*/
2867 } else {
2868 $this->error = $this->db->lasterror;
2869 return -1;
2870 }
2871 }
2872 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES') && empty($ignore_price_load)) { // prices per customers
2873 // Nothing loaded by default. List may be very long.
2874 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY') && empty($ignore_price_load)) { // prices per quantity
2875 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2876 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid";
2877 $sql .= " FROM ".$this->db->prefix()."product_price";
2878 $sql .= " WHERE fk_product = ".((int) $this->id);
2879 $sql .= " ORDER BY date_price DESC, rowid DESC";
2880 $sql .= " LIMIT 1";
2881
2882 $resql = $this->db->query($sql);
2883 if ($resql) {
2884 $result = $this->db->fetch_array($resql);
2885
2886 if ($result) {
2887 // Price by quantity
2888 $this->prices_by_qty[0] = $result["price_by_qty"];
2889 $this->prices_by_qty_id[0] = $result["rowid"];
2890 // Récuperation de la liste des prix selon qty si flag positionné
2891 if ($this->prices_by_qty[0] == 1) {
2892 $sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise, remise, price_base_type";
2893 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
2894 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[0]);
2895 $sql .= " ORDER BY quantity ASC";
2896
2897 $resql = $this->db->query($sql);
2898 if ($resql) {
2899 $resultat = array();
2900 $ii = 0;
2901 while ($result = $this->db->fetch_array($resql)) {
2902 $resultat[$ii] = array();
2903 $resultat[$ii]["rowid"] = $result["rowid"];
2904 $resultat[$ii]["price"] = $result["price"];
2905 $resultat[$ii]["unitprice"] = $result["unitprice"];
2906 $resultat[$ii]["quantity"] = $result["quantity"];
2907 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
2908 //$resultat[$ii]["remise"]= $result["remise"]; // deprecated
2909 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
2910 $ii++;
2911 }
2912 $this->prices_by_qty_list[0] = $resultat;
2913 } else {
2914 $this->error = $this->db->lasterror;
2915 return -1;
2916 }
2917 }
2918 }
2919 } else {
2920 $this->error = $this->db->lasterror;
2921 return -1;
2922 }
2923 } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') && empty($ignore_price_load)) { // prices per customer and quantity
2924 $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
2925 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
2926 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2927 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
2928 $sql .= " FROM ".$this->db->prefix()."product_price";
2929 $sql .= " WHERE entity IN (".getEntity('productprice').")";
2930 $sql .= " AND price_level=".((int) $i);
2931 $sql .= " AND fk_product = ".((int) $this->id);
2932 $sql .= " ORDER BY date_price DESC, rowid DESC";
2933 $sql .= " LIMIT 1";
2934 $resql = $this->db->query($sql);
2935 if (!$resql) {
2936 $this->error = $this->db->lasterror;
2937 return -1;
2938 } elseif ($result = $this->db->fetch_array($resql)) {
2939 $this->multiprices[$i] = (!empty($result["price"]) ? $result["price"] : 0);
2940 $this->multiprices_ttc[$i] = (!empty($result["price_ttc"]) ? $result["price_ttc"] : 0);
2941 $this->multiprices_min[$i] = (!empty($result["price_min"]) ? $result["price_min"] : 0);
2942 $this->multiprices_min_ttc[$i] = (!empty($result["price_min_ttc"]) ? $result["price_min_ttc"] : 0);
2943 $this->multiprices_base_type[$i] = (!empty($result["price_base_type"]) ? $result["price_base_type"] : '');
2944 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
2945 $this->multiprices_tva_tx[$i] = (!empty($result["tva_tx"]) ? $result["tva_tx"] : 0); // TODO Add ' ('.$result['default_vat_code'].')'
2946 $this->multiprices_recuperableonly[$i] = (!empty($result["recuperableonly"]) ? $result["recuperableonly"] : 0);
2947
2948 // Price by quantity
2949 $this->prices_by_qty[$i] = (!empty($result["price_by_qty"]) ? $result["price_by_qty"] : 0);
2950 $this->prices_by_qty_id[$i] = (!empty($result["rowid"]) ? $result["rowid"] : 0);
2951 // Récuperation de la liste des prix selon qty si flag positionné
2952 if ($this->prices_by_qty[$i] == 1) {
2953 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
2954 $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
2955 $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
2956 $sql .= " ORDER BY quantity ASC";
2957
2958 $resql = $this->db->query($sql);
2959 if ($resql) {
2960 $resultat = array();
2961 $ii = 0;
2962 while ($result = $this->db->fetch_array($resql)) {
2963 $resultat[$ii] = array();
2964 $resultat[$ii]["rowid"] = $result["rowid"];
2965 $resultat[$ii]["price"] = $result["price"];
2966 $resultat[$ii]["unitprice"] = $result["unitprice"];
2967 $resultat[$ii]["quantity"] = $result["quantity"];
2968 $resultat[$ii]["remise_percent"] = $result["remise_percent"];
2969 $resultat[$ii]["remise"] = $result["remise"]; // deprecated
2970 $resultat[$ii]["price_base_type"] = $result["price_base_type"];
2971 $ii++;
2972 }
2973 $this->prices_by_qty_list[$i] = $resultat;
2974 } else {
2975 $this->error = $this->db->lasterror;
2976 return -1;
2977 }
2978 }
2979 }
2980 }
2981 }
2982
2983 if (isModEnabled('dynamicprices') && !empty($this->fk_price_expression) && empty($ignore_expression)) {
2984 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2985 $priceparser = new PriceParser($this->db);
2986 $price_result = $priceparser->parseProduct($this);
2987 if ($price_result >= 0) {
2988 $this->price = $price_result;
2989 // Calculate the VAT
2990 $this->price_ttc = (float) price2num($this->price) * (1 + ($this->tva_tx / 100));
2991 $this->price_ttc = (float) price2num($this->price_ttc, 'MU');
2992 }
2993 }
2994
2995 // We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product.
2996 // Instead we just init the stock_warehouse array
2997 $this->stock_warehouse = array();
2998
2999 return 1;
3000 } else {
3001 return 0;
3002 }
3003 } else {
3004 $this->error = $this->db->lasterror();
3005 return -1;
3006 }
3007 }
3008
3009 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3016 public function load_stats_mo($socid = 0)
3017 {
3018 // phpcs:enable
3019 global $user, $hookmanager, $action;
3020
3021 $error = 0;
3022
3023 foreach (array('toconsume', 'consumed', 'toproduce', 'produced') as $role) {
3024 $this->stats_mo['customers_'.$role] = 0;
3025 $this->stats_mo['nb_'.$role] = 0;
3026 $this->stats_mo['qty_'.$role] = 0;
3027
3028 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3029 $sql .= " SUM(mp.qty) as qty";
3030 $sql .= " FROM ".$this->db->prefix()."mrp_mo as c";
3031 $sql .= " INNER JOIN ".$this->db->prefix()."mrp_production as mp ON mp.fk_mo=c.rowid";
3032 if (!$user->hasRight('societe', 'client', 'voir')) {
3033 $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc=c.fk_soc AND sc.fk_user = ".((int) $user->id);
3034 }
3035 $sql .= " WHERE ";
3036 $sql .= " c.entity IN (".getEntity('mo').")";
3037
3038 $sql .= " AND mp.fk_product = ".((int) $this->id);
3039 $sql .= " AND mp.role ='".$this->db->escape($role)."'";
3040 if ($socid > 0) {
3041 $sql .= " AND c.fk_soc = ".((int) $socid);
3042 }
3043
3044 $result = $this->db->query($sql);
3045 if ($result) {
3046 $obj = $this->db->fetch_object($result);
3047 $this->stats_mo['customers_'.$role] = $obj->nb_customers ? $obj->nb_customers : 0;
3048 $this->stats_mo['nb_'.$role] = $obj->nb ? $obj->nb : 0;
3049 $this->stats_mo['qty_'.$role] = $obj->qty ? price2num($obj->qty, 'MS') : 0; // qty may be a float due to the SUM()
3050 } else {
3051 $this->error = $this->db->error();
3052 $error++;
3053 }
3054 }
3055
3056 if (!empty($error)) {
3057 return -1;
3058 }
3059
3060 $parameters = array('socid' => $socid);
3061 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3062 if ($reshook > 0) {
3063 $this->stats_mo = $hookmanager->resArray['stats_mo'];
3064 }
3065
3066 return 1;
3067 }
3068
3069 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3076 public function load_stats_bom($socid = 0)
3077 {
3078 // phpcs:enable
3079 global $user, $hookmanager, $action;
3080
3081 $error = 0;
3082
3083 $this->stats_bom['nb_toproduce'] = 0;
3084 $this->stats_bom['nb_toconsume'] = 0;
3085 $this->stats_bom['qty_toproduce'] = 0;
3086 $this->stats_bom['qty_toconsume'] = 0;
3087
3088 $sql = "SELECT COUNT(DISTINCT b.rowid) as nb_toproduce,";
3089 $sql .= " SUM(b.qty) as qty_toproduce";
3090 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3091 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
3092 $sql .= " WHERE ";
3093 $sql .= " b.entity IN (".getEntity('bom').")";
3094 $sql .= " AND b.fk_product =".((int) $this->id);
3095 $sql .= " GROUP BY b.rowid";
3096
3097 $result = $this->db->query($sql);
3098 if ($result) {
3099 $obj = $this->db->fetch_object($result);
3100 $this->stats_bom['nb_toproduce'] = !empty($obj->nb_toproduce) ? $obj->nb_toproduce : 0;
3101 $this->stats_bom['qty_toproduce'] = !empty($obj->qty_toproduce) ? price2num($obj->qty_toproduce) : 0;
3102 } else {
3103 $this->error = $this->db->error();
3104 $error++;
3105 }
3106
3107 $sql = "SELECT COUNT(DISTINCT bl.rowid) as nb_toconsume,";
3108 $sql .= " SUM(bl.qty) as qty_toconsume";
3109 $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3110 $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
3111 $sql .= " WHERE ";
3112 $sql .= " b.entity IN (".getEntity('bom').")";
3113 $sql .= " AND bl.fk_product =".((int) $this->id);
3114
3115 $result = $this->db->query($sql);
3116 if ($result) {
3117 $obj = $this->db->fetch_object($result);
3118 $this->stats_bom['nb_toconsume'] = !empty($obj->nb_toconsume) ? $obj->nb_toconsume : 0;
3119 $this->stats_bom['qty_toconsume'] = !empty($obj->qty_toconsume) ? price2num($obj->qty_toconsume) : 0;
3120 } else {
3121 $this->error = $this->db->error();
3122 $error++;
3123 }
3124
3125 if (!empty($error)) {
3126 return -1;
3127 }
3128
3129 $parameters = array('socid' => $socid);
3130 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3131 if ($reshook > 0) {
3132 $this->stats_bom = $hookmanager->resArray['stats_bom'];
3133 }
3134
3135 return 1;
3136 }
3137
3138 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3145 public function load_stats_propale($socid = 0)
3146 {
3147 // phpcs:enable
3148 global $conf, $user, $hookmanager, $action;
3149
3150 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,";
3151 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3152 $sql .= " FROM ".$this->db->prefix()."propaldet as pd";
3153 $sql .= ", ".$this->db->prefix()."propal as p";
3154 $sql .= ", ".$this->db->prefix()."societe as s";
3155 if (!$user->hasRight('societe', 'client', 'voir')) {
3156 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3157 }
3158 $sql .= " WHERE p.rowid = pd.fk_propal";
3159 $sql .= " AND p.fk_soc = s.rowid";
3160 $sql .= " AND p.entity IN (".getEntity('propal').")";
3161 $sql .= " AND pd.fk_product = ".((int) $this->id);
3162 if (!$user->hasRight('societe', 'client', 'voir')) {
3163 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3164 }
3165 //$sql.= " AND pr.fk_statut != 0";
3166 if ($socid > 0) {
3167 $sql .= " AND p.fk_soc = ".((int) $socid);
3168 }
3169
3170 $result = $this->db->query($sql);
3171 if ($result) {
3172 $obj = $this->db->fetch_object($result);
3173 $this->stats_propale['customers'] = $obj->nb_customers;
3174 $this->stats_propale['nb'] = $obj->nb;
3175 $this->stats_propale['rows'] = $obj->nb_rows;
3176 $this->stats_propale['qty'] = $obj->qty ? $obj->qty : 0;
3177
3178 // if it's a virtual product, maybe it is in proposal by extension
3179 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3180 $TFather = $this->getFather();
3181 if (is_array($TFather) && !empty($TFather)) {
3182 foreach ($TFather as &$fatherData) {
3183 $pFather = new Product($this->db);
3184 $pFather->id = $fatherData['id'];
3185 $qtyCoef = $fatherData['qty'];
3186
3187 if ($fatherData['incdec']) {
3188 $pFather->load_stats_propale($socid);
3189
3190 $this->stats_propale['customers'] += $pFather->stats_propale['customers'];
3191 $this->stats_propale['nb'] += $pFather->stats_propale['nb'];
3192 $this->stats_propale['rows'] += $pFather->stats_propale['rows'];
3193 $this->stats_propale['qty'] += $pFather->stats_propale['qty'] * $qtyCoef;
3194 }
3195 }
3196 }
3197 }
3198
3199 $parameters = array('socid' => $socid);
3200 $reshook = $hookmanager->executeHooks('loadStatsCustomerProposal', $parameters, $this, $action);
3201 if ($reshook > 0) {
3202 $this->stats_propale = $hookmanager->resArray['stats_propale'];
3203 }
3204
3205 return 1;
3206 } else {
3207 $this->error = $this->db->error();
3208 return -1;
3209 }
3210 }
3211
3212
3213 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3220 public function load_stats_proposal_supplier($socid = 0)
3221 {
3222 // phpcs:enable
3223 global $conf, $user, $hookmanager, $action;
3224
3225 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,";
3226 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3227 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as pd";
3228 $sql .= ", ".$this->db->prefix()."supplier_proposal as p";
3229 $sql .= ", ".$this->db->prefix()."societe as s";
3230 if (!$user->hasRight('societe', 'client', 'voir')) {
3231 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3232 }
3233 $sql .= " WHERE p.rowid = pd.fk_supplier_proposal";
3234 $sql .= " AND p.fk_soc = s.rowid";
3235 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
3236 $sql .= " AND pd.fk_product = ".((int) $this->id);
3237 if (!$user->hasRight('societe', 'client', 'voir')) {
3238 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3239 }
3240 //$sql.= " AND pr.fk_statut != 0";
3241 if ($socid > 0) {
3242 $sql .= " AND p.fk_soc = ".((int) $socid);
3243 }
3244
3245 $result = $this->db->query($sql);
3246 if ($result) {
3247 $obj = $this->db->fetch_object($result);
3248 $this->stats_proposal_supplier['suppliers'] = $obj->nb_suppliers;
3249 $this->stats_proposal_supplier['nb'] = $obj->nb;
3250 $this->stats_proposal_supplier['rows'] = $obj->nb_rows;
3251 $this->stats_proposal_supplier['qty'] = $obj->qty ? $obj->qty : 0;
3252
3253 $parameters = array('socid' => $socid);
3254 $reshook = $hookmanager->executeHooks('loadStatsSupplierProposal', $parameters, $this, $action);
3255 if ($reshook > 0) {
3256 $this->stats_proposal_supplier = $hookmanager->resArray['stats_proposal_supplier'];
3257 }
3258
3259 return 1;
3260 } else {
3261 $this->error = $this->db->error();
3262 return -1;
3263 }
3264 }
3265
3266
3267 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3276 public function load_stats_commande($socid = 0, $filtrestatut = '', $forVirtualStock = 0)
3277 {
3278 // phpcs:enable
3279 global $conf, $user, $hookmanager, $action;
3280
3281 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3282 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3283 $sql .= " FROM ".$this->db->prefix()."commandedet as cd";
3284 $sql .= ", ".$this->db->prefix()."commande as c";
3285 $sql .= ", ".$this->db->prefix()."societe as s";
3286 if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3287 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3288 }
3289 $sql .= " WHERE c.rowid = cd.fk_commande";
3290 $sql .= " AND c.fk_soc = s.rowid";
3291 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'commande').")";
3292 $sql .= " AND cd.fk_product = ".((int) $this->id);
3293 if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3294 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3295 }
3296 if ($socid > 0) {
3297 $sql .= " AND c.fk_soc = ".((int) $socid);
3298 }
3299 if ($filtrestatut != '') {
3300 $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")";
3301 }
3302
3303 $result = $this->db->query($sql);
3304 if ($result) {
3305 $obj = $this->db->fetch_object($result);
3306 $this->stats_commande['customers'] = $obj->nb_customers;
3307 $this->stats_commande['nb'] = $obj->nb;
3308 $this->stats_commande['rows'] = $obj->nb_rows;
3309 $this->stats_commande['qty'] = $obj->qty ? $obj->qty : 0;
3310
3311 // if it's a virtual product, maybe it is in order by extension
3312 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3313 $TFather = $this->getFather();
3314 if (is_array($TFather) && !empty($TFather)) {
3315 foreach ($TFather as &$fatherData) {
3316 $pFather = new Product($this->db);
3317 $pFather->id = $fatherData['id'];
3318 $qtyCoef = $fatherData['qty'];
3319
3320 if ($fatherData['incdec']) {
3321 $pFather->load_stats_commande($socid, $filtrestatut);
3322
3323 $this->stats_commande['customers'] += $pFather->stats_commande['customers'];
3324 $this->stats_commande['nb'] += $pFather->stats_commande['nb'];
3325 $this->stats_commande['rows'] += $pFather->stats_commande['rows'];
3326 $this->stats_commande['qty'] += $pFather->stats_commande['qty'] * $qtyCoef;
3327 }
3328 }
3329 }
3330 }
3331
3332 // If stock decrease is on invoice validation, the theoretical stock continue to
3333 // count the orders to ship in theoretical stock when some are already removed by invoice validation.
3334 if ($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
3335 if (getDolGlobalString('DECREASE_ONLY_UNINVOICEDPRODUCTS')) {
3336 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is on, we make a compensation but only if order not yet invoice.
3337 $adeduire = 0;
3338 $sql = "SELECT SUM(".$this->db->ifsql('f.type=2', -1, 1)." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3339 $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3340 $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'))";
3341 $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid";
3342 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3343
3344 dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3345 $resql = $this->db->query($sql);
3346 if ($resql) {
3347 if ($this->db->num_rows($resql) > 0) {
3348 $obj = $this->db->fetch_object($resql);
3349 $adeduire += $obj->count;
3350 }
3351 }
3352
3353 $this->stats_commande['qty'] -= $adeduire;
3354 } else {
3355 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is off, we make a compensation with lines of invoices linked to the order
3356 include_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
3357
3358 // For every order having invoice already validated we need to decrease stock cause it's in physical stock
3359 $adeduire = 0;
3360 $sql = "SELECT sum(".$this->db->ifsql('f.type=2', -1, 1)." * fd.qty) as count FROM ".MAIN_DB_PREFIX."facturedet as fd ";
3361 $sql .= " JOIN ".MAIN_DB_PREFIX."facture as f ON fd.fk_facture = f.rowid";
3362 $sql .= " JOIN ".MAIN_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'))";
3363 $sql .= " JOIN ".MAIN_DB_PREFIX."commande as c ON el.fk_source = c.rowid";
3364 $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product = ".((int) $this->id);
3365
3366 dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3367 $resql = $this->db->query($sql);
3368 if ($resql) {
3369 if ($this->db->num_rows($resql) > 0) {
3370 $obj = $this->db->fetch_object($resql);
3371 $adeduire += $obj->count;
3372 }
3373 } else {
3374 $this->error = $this->db->error();
3375 return -1;
3376 }
3377
3378 $this->stats_commande['qty'] -= $adeduire;
3379 }
3380 }
3381
3382 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3383 $reshook = $hookmanager->executeHooks('loadStatsCustomerOrder', $parameters, $this, $action);
3384 if ($reshook > 0) {
3385 $this->stats_commande = $hookmanager->resArray['stats_commande'];
3386 }
3387 return 1;
3388 } else {
3389 $this->error = $this->db->error();
3390 return -1;
3391 }
3392 }
3393
3394 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3404 public function load_stats_commande_fournisseur($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3405 {
3406 // phpcs:enable
3407 global $conf, $user, $hookmanager, $action;
3408
3409 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,";
3410 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3411 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as cd";
3412 $sql .= ", ".$this->db->prefix()."commande_fournisseur as c";
3413 $sql .= ", ".$this->db->prefix()."societe as s";
3414 if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3415 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3416 }
3417 $sql .= " WHERE c.rowid = cd.fk_commande";
3418 $sql .= " AND c.fk_soc = s.rowid";
3419 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
3420 $sql .= " AND cd.fk_product = ".((int) $this->id);
3421 if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3422 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3423 }
3424 if ($socid > 0) {
3425 $sql .= " AND c.fk_soc = ".((int) $socid);
3426 }
3427 if ($filtrestatut != '') {
3428 $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")"; // Peut valoir 0
3429 }
3430 if (!empty($dateofvirtualstock)) {
3431 $sql .= " AND c.date_livraison <= '".$this->db->idate($dateofvirtualstock)."'";
3432 }
3433
3434 $result = $this->db->query($sql);
3435 if ($result) {
3436 $obj = $this->db->fetch_object($result);
3437 $this->stats_commande_fournisseur['suppliers'] = $obj->nb_suppliers;
3438 $this->stats_commande_fournisseur['nb'] = $obj->nb;
3439 $this->stats_commande_fournisseur['rows'] = $obj->nb_rows;
3440 $this->stats_commande_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3441
3442 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3443 $reshook = $hookmanager->executeHooks('loadStatsSupplierOrder', $parameters, $this, $action);
3444 if ($reshook > 0) {
3445 $this->stats_commande_fournisseur = $hookmanager->resArray['stats_commande_fournisseur'];
3446 }
3447
3448 return 1;
3449 } else {
3450 $this->error = $this->db->error().' sql='.$sql;
3451 return -1;
3452 }
3453 }
3454
3455 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3465 public function load_stats_sending($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $filterShipmentStatus = '')
3466 {
3467 // phpcs:enable
3468 global $conf, $user, $hookmanager, $action;
3469
3470 $sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,";
3471 $sql .= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty";
3472 $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
3473 $sql .= ", ".$this->db->prefix()."commandedet as cd";
3474 $sql .= ", ".$this->db->prefix()."commande as c";
3475 $sql .= ", ".$this->db->prefix()."expedition as e";
3476 $sql .= ", ".$this->db->prefix()."societe as s";
3477 if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3478 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3479 }
3480 $sql .= " WHERE e.rowid = ed.fk_expedition";
3481 $sql .= " AND c.rowid = cd.fk_commande";
3482 $sql .= " AND e.fk_soc = s.rowid";
3483 $sql .= " AND e.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'expedition').")";
3484 $sql .= " AND ed.fk_elementdet = cd.rowid";
3485 $sql .= " AND cd.fk_product = ".((int) $this->id);
3486 if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3487 $sql .= " AND e.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3488 }
3489 if ($socid > 0) {
3490 $sql .= " AND e.fk_soc = ".((int) $socid);
3491 }
3492 if ($filtrestatut != '') {
3493 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3494 }
3495 if (!empty($filterShipmentStatus)) {
3496 $sql .= " AND e.fk_statut IN (".$this->db->sanitize($filterShipmentStatus).")";
3497 }
3498
3499 $result = $this->db->query($sql);
3500 if ($result) {
3501 $obj = $this->db->fetch_object($result);
3502 $this->stats_expedition['customers'] = $obj->nb_customers;
3503 $this->stats_expedition['nb'] = $obj->nb;
3504 $this->stats_expedition['rows'] = $obj->nb_rows;
3505 $this->stats_expedition['qty'] = $obj->qty ? $obj->qty : 0;
3506
3507 // if it's a virtual product, maybe it is in sending by extension
3508 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3509 $TFather = $this->getFather();
3510 if (is_array($TFather) && !empty($TFather)) {
3511 foreach ($TFather as &$fatherData) {
3512 $pFather = new Product($this->db);
3513 $pFather->id = $fatherData['id'];
3514 $qtyCoef = $fatherData['qty'];
3515
3516 if ($fatherData['incdec']) {
3517 $pFather->load_stats_sending($socid, $filtrestatut, $forVirtualStock);
3518
3519 $this->stats_expedition['customers'] += $pFather->stats_expedition['customers'];
3520 $this->stats_expedition['nb'] += $pFather->stats_expedition['nb'];
3521 $this->stats_expedition['rows'] += $pFather->stats_expedition['rows'];
3522 $this->stats_expedition['qty'] += $pFather->stats_expedition['qty'] * $qtyCoef;
3523 }
3524 }
3525 }
3526 }
3527
3528 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock, 'filterShipmentStatus' => $filterShipmentStatus);
3529 $reshook = $hookmanager->executeHooks('loadStatsSending', $parameters, $this, $action);
3530 if ($reshook > 0) {
3531 $this->stats_expedition = $hookmanager->resArray['stats_expedition'];
3532 }
3533
3534 return 1;
3535 } else {
3536 $this->error = $this->db->error();
3537 return -1;
3538 }
3539 }
3540
3541 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3551 public function load_stats_reception($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3552 {
3553 // phpcs:enable
3554 global $conf, $user, $hookmanager, $action;
3555
3556 $sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_suppliers, COUNT(DISTINCT cf.rowid) as nb,";
3557 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3558 $sql .= " FROM ".$this->db->prefix()."receptiondet_batch as fd";
3559 $sql .= ", ".$this->db->prefix()."commande_fournisseur as cf";
3560 $sql .= ", ".$this->db->prefix()."societe as s";
3561 if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3562 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3563 }
3564 $sql .= " WHERE cf.rowid = fd.fk_element";
3565 $sql .= " AND cf.fk_soc = s.rowid";
3566 $sql .= " AND cf.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
3567 $sql .= " AND fd.fk_product = ".((int) $this->id);
3568 if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3569 $sql .= " AND cf.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3570 }
3571 if ($socid > 0) {
3572 $sql .= " AND cf.fk_soc = ".((int) $socid);
3573 }
3574 if ($filtrestatut != '') {
3575 $sql .= " AND cf.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3576 }
3577 if (!empty($dateofvirtualstock)) {
3578 $sql .= " AND fd.datec <= '".$this->db->idate($dateofvirtualstock)."'";
3579 }
3580
3581 $result = $this->db->query($sql);
3582 if ($result) {
3583 $obj = $this->db->fetch_object($result);
3584 $this->stats_reception['suppliers'] = $obj->nb_suppliers;
3585 $this->stats_reception['nb'] = $obj->nb;
3586 $this->stats_reception['rows'] = $obj->nb_rows;
3587 $this->stats_reception['qty'] = $obj->qty ? $obj->qty : 0;
3588
3589 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3590 $reshook = $hookmanager->executeHooks('loadStatsReception', $parameters, $this, $action);
3591 if ($reshook > 0) {
3592 $this->stats_reception = $hookmanager->resArray['stats_reception'];
3593 }
3594
3595 return 1;
3596 } else {
3597 $this->error = $this->db->error();
3598 return -1;
3599 }
3600 }
3601
3602 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3613 public function load_stats_inproduction($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null, $warehouseid = 0)
3614 {
3615 // phpcs:enable
3616 global $user, $hookmanager, $action;
3617
3618 $serviceStockIsEnabled = isModEnabled("service") && getDolGlobalString('STOCK_SUPPORTS_SERVICES');
3619
3620 $sql = "SELECT COUNT(DISTINCT m.fk_soc) as nb_customers, COUNT(DISTINCT m.rowid) as nb,";
3621 $sql .= " COUNT(mp.rowid) as nb_rows, SUM(mp.qty) as qty, role";
3622 $sql .= " FROM ".$this->db->prefix()."mrp_production as mp";
3623 $sql .= ", ".$this->db->prefix()."mrp_mo as m";
3624 $sql .= " LEFT JOIN ".$this->db->prefix()."societe as s ON s.rowid = m.fk_soc";
3625 if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3626 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3627 }
3628 $sql .= " WHERE m.rowid = mp.fk_mo";
3629 $sql .= " AND m.entity IN (".getEntity(($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE')) ? 'stock' : 'mrp').")";
3630 $sql .= " AND mp.fk_product = ".((int) $this->id);
3631 $sql .= " AND (mp.disable_stock_change IN (0) OR mp.disable_stock_change IS NULL)";
3632 if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3633 $sql .= " AND m.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3634 }
3635 if ($socid > 0) {
3636 $sql .= " AND m.fk_soc = ".((int) $socid);
3637 }
3638 if ($filtrestatut != '') {
3639 $sql .= " AND m.status IN (".$this->db->sanitize($filtrestatut).")";
3640 }
3641 if (!empty($dateofvirtualstock)) {
3642 $sql .= " AND m.date_valid <= '".$this->db->idate($dateofvirtualstock)."'"; // better date to code ? end of production ?
3643 }
3644 if (!$serviceStockIsEnabled) {
3645 $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))";
3646 }
3647 if (!empty($warehouseid)) {
3648 $sql .= " AND m.fk_warehouse = ".((int) $warehouseid);
3649 }
3650 $sql .= " GROUP BY role";
3651
3652 if ($warehouseid) {
3653 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
3654 } else {
3655 $this->stats_mrptoconsume['customers'] = 0;
3656 $this->stats_mrptoconsume['nb'] = 0;
3657 $this->stats_mrptoconsume['rows'] = 0;
3658 $this->stats_mrptoconsume['qty'] = 0;
3659 $this->stats_mrptoproduce['customers'] = 0;
3660 $this->stats_mrptoproduce['nb'] = 0;
3661 $this->stats_mrptoproduce['rows'] = 0;
3662 $this->stats_mrptoproduce['qty'] = 0;
3663 }
3664
3665 $result = $this->db->query($sql);
3666 if ($result) {
3667 while ($obj = $this->db->fetch_object($result)) {
3668 if ($obj->role == 'toconsume' && empty($warehouseid)) {
3669 $this->stats_mrptoconsume['customers'] += $obj->nb_customers;
3670 $this->stats_mrptoconsume['nb'] += $obj->nb;
3671 $this->stats_mrptoconsume['rows'] += $obj->nb_rows;
3672 $this->stats_mrptoconsume['qty'] += ($obj->qty ? $obj->qty : 0);
3673 }
3674 if ($obj->role == 'consumed' && empty($warehouseid)) {
3675 //$this->stats_mrptoconsume['customers'] += $obj->nb_customers;
3676 //$this->stats_mrptoconsume['nb'] += $obj->nb;
3677 //$this->stats_mrptoconsume['rows'] += $obj->nb_rows;
3678 $this->stats_mrptoconsume['qty'] -= ($obj->qty ? $obj->qty : 0);
3679 }
3680 if ($obj->role == 'toproduce') {
3681 if ($warehouseid) {
3682 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] += ($obj->qty ? $obj->qty : 0);
3683 } else {
3684 $this->stats_mrptoproduce['customers'] += $obj->nb_customers;
3685 $this->stats_mrptoproduce['nb'] += $obj->nb;
3686 $this->stats_mrptoproduce['rows'] += $obj->nb_rows;
3687 $this->stats_mrptoproduce['qty'] += ($obj->qty ? $obj->qty : 0);
3688 }
3689 }
3690 if ($obj->role == 'produced') {
3691 //$this->stats_mrptoproduce['customers'] += $obj->nb_customers;
3692 //$this->stats_mrptoproduce['nb'] += $obj->nb;
3693 //$this->stats_mrptoproduce['rows'] += $obj->nb_rows;
3694 if ($warehouseid) {
3695 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
3696 } else {
3697 $this->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
3698 }
3699 }
3700 }
3701
3702 // Clean data
3703 if ($warehouseid) {
3704 if ($this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] < 0) {
3705 $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
3706 }
3707 } else {
3708 if ($this->stats_mrptoconsume['qty'] < 0) {
3709 $this->stats_mrptoconsume['qty'] = 0;
3710 }
3711 if ($this->stats_mrptoproduce['qty'] < 0) {
3712 $this->stats_mrptoproduce['qty'] = 0;
3713 }
3714 }
3715
3716 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3717 $reshook = $hookmanager->executeHooks('loadStatsInProduction', $parameters, $this, $action);
3718 if ($reshook > 0) {
3719 $this->stats_mrptoproduce = $hookmanager->resArray['stats_mrptoproduce'];
3720 }
3721
3722 return 1;
3723 } else {
3724 $this->error = $this->db->error();
3725 return -1;
3726 }
3727 }
3728
3729 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3736 public function load_stats_contrat($socid = 0)
3737 {
3738 // phpcs:enable
3739 global $conf, $user, $hookmanager, $action;
3740
3741 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3742 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3743 $sql .= " FROM ".$this->db->prefix()."contratdet as cd";
3744 $sql .= ", ".$this->db->prefix()."contrat as c";
3745 $sql .= ", ".$this->db->prefix()."societe as s";
3746 if (!$user->hasRight('societe', 'client', 'voir')) {
3747 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3748 }
3749 $sql .= " WHERE c.rowid = cd.fk_contrat";
3750 $sql .= " AND c.fk_soc = s.rowid";
3751 $sql .= " AND c.entity IN (".getEntity('contract').")";
3752 $sql .= " AND cd.fk_product = ".((int) $this->id);
3753 if (!$user->hasRight('societe', 'client', 'voir')) {
3754 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3755 }
3756 //$sql.= " AND c.statut != 0";
3757 if ($socid > 0) {
3758 $sql .= " AND c.fk_soc = ".((int) $socid);
3759 }
3760
3761 $result = $this->db->query($sql);
3762 if ($result) {
3763 $obj = $this->db->fetch_object($result);
3764 $this->stats_contrat['customers'] = $obj->nb_customers;
3765 $this->stats_contrat['nb'] = $obj->nb;
3766 $this->stats_contrat['rows'] = $obj->nb_rows;
3767 $this->stats_contrat['qty'] = $obj->qty ? $obj->qty : 0;
3768
3769 // if it's a virtual product, maybe it is in contract by extension
3770 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3771 $TFather = $this->getFather();
3772 if (is_array($TFather) && !empty($TFather)) {
3773 foreach ($TFather as &$fatherData) {
3774 $pFather = new Product($this->db);
3775 $pFather->id = $fatherData['id'];
3776 $qtyCoef = $fatherData['qty'];
3777
3778 if ($fatherData['incdec']) {
3779 $pFather->load_stats_contrat($socid);
3780
3781 $this->stats_contrat['customers'] += $pFather->stats_contrat['customers'];
3782 $this->stats_contrat['nb'] += $pFather->stats_contrat['nb'];
3783 $this->stats_contrat['rows'] += $pFather->stats_contrat['rows'];
3784 $this->stats_contrat['qty'] += $pFather->stats_contrat['qty'] * $qtyCoef;
3785 }
3786 }
3787 }
3788 }
3789
3790 $parameters = array('socid' => $socid);
3791 $reshook = $hookmanager->executeHooks('loadStatsContract', $parameters, $this, $action);
3792 if ($reshook > 0) {
3793 $this->stats_contrat = $hookmanager->resArray['stats_contrat'];
3794 }
3795
3796 return 1;
3797 } else {
3798 $this->error = $this->db->error().' sql='.$sql;
3799 return -1;
3800 }
3801 }
3802
3803 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3810 public function load_stats_facture($socid = 0)
3811 {
3812 // phpcs:enable
3813 global $conf, $user, $hookmanager, $action;
3814
3815 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
3816 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(".$this->db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1').") as qty";
3817 $sql .= " FROM ".$this->db->prefix()."facturedet as fd";
3818 $sql .= ", ".$this->db->prefix()."facture as f";
3819 $sql .= ", ".$this->db->prefix()."societe as s";
3820 if (!$user->hasRight('societe', 'client', 'voir')) {
3821 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3822 }
3823 $sql .= " WHERE f.rowid = fd.fk_facture";
3824 $sql .= " AND f.fk_soc = s.rowid";
3825 $sql .= " AND f.entity IN (".getEntity('invoice').")";
3826 $sql .= " AND fd.fk_product = ".((int) $this->id);
3827 if (!$user->hasRight('societe', 'client', 'voir')) {
3828 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3829 }
3830 //$sql.= " AND f.fk_statut != 0";
3831 if ($socid > 0) {
3832 $sql .= " AND f.fk_soc = ".((int) $socid);
3833 }
3834
3835 $result = $this->db->query($sql);
3836 if ($result) {
3837 $obj = $this->db->fetch_object($result);
3838 $this->stats_facture['customers'] = $obj->nb_customers;
3839 $this->stats_facture['nb'] = $obj->nb;
3840 $this->stats_facture['rows'] = $obj->nb_rows;
3841 $this->stats_facture['qty'] = $obj->qty ? $obj->qty : 0;
3842
3843 // if it's a virtual product, maybe it is in invoice by extension
3844 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3845 $TFather = $this->getFather();
3846 if (is_array($TFather) && !empty($TFather)) {
3847 foreach ($TFather as &$fatherData) {
3848 $pFather = new Product($this->db);
3849 $pFather->id = $fatherData['id'];
3850 $qtyCoef = $fatherData['qty'];
3851
3852 if ($fatherData['incdec']) {
3853 $pFather->load_stats_facture($socid);
3854
3855 $this->stats_facture['customers'] += $pFather->stats_facture['customers'];
3856 $this->stats_facture['nb'] += $pFather->stats_facture['nb'];
3857 $this->stats_facture['rows'] += $pFather->stats_facture['rows'];
3858 $this->stats_facture['qty'] += $pFather->stats_facture['qty'] * $qtyCoef;
3859 }
3860 }
3861 }
3862 }
3863
3864 $parameters = array('socid' => $socid);
3865 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoice', $parameters, $this, $action);
3866 if ($reshook > 0) {
3867 $this->stats_facture = $hookmanager->resArray['stats_facture'];
3868 }
3869
3870 return 1;
3871 } else {
3872 $this->error = $this->db->error();
3873 return -1;
3874 }
3875 }
3876
3877
3878 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3885 public function load_stats_facturerec($socid = 0)
3886 {
3887 // phpcs:enable
3888 global $conf, $user, $hookmanager, $action;
3889
3890 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
3891 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3892 $sql .= " FROM ".MAIN_DB_PREFIX."facturedet_rec as fd";
3893 $sql .= ", ".MAIN_DB_PREFIX."facture_rec as f";
3894 $sql .= ", ".MAIN_DB_PREFIX."societe as s";
3895 if (!$user->hasRight('societe', 'client', 'voir')) {
3896 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
3897 }
3898 $sql .= " WHERE f.rowid = fd.fk_facture";
3899 $sql .= " AND f.fk_soc = s.rowid";
3900 $sql .= " AND f.entity IN (".getEntity('invoice').")";
3901 $sql .= " AND fd.fk_product = ".((int) $this->id);
3902 if (!$user->hasRight('societe', 'client', 'voir')) {
3903 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3904 }
3905 //$sql.= " AND f.fk_statut != 0";
3906 if ($socid > 0) {
3907 $sql .= " AND f.fk_soc = ".((int) $socid);
3908 }
3909
3910 $result = $this->db->query($sql);
3911 if ($result) {
3912 $obj = $this->db->fetch_object($result);
3913 $this->stats_facturerec['customers'] = $obj->nb_customers;
3914 $this->stats_facturerec['nb'] = $obj->nb;
3915 $this->stats_facturerec['rows'] = $obj->nb_rows;
3916 $this->stats_facturerec['qty'] = $obj->qty ? $obj->qty : 0;
3917
3918 // if it's a virtual product, maybe it is in invoice by extension
3919 if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3920 $TFather = $this->getFather();
3921 if (is_array($TFather) && !empty($TFather)) {
3922 foreach ($TFather as &$fatherData) {
3923 $pFather = new Product($this->db);
3924 $pFather->id = $fatherData['id'];
3925 $qtyCoef = $fatherData['qty'];
3926
3927 if ($fatherData['incdec']) {
3928 $pFather->load_stats_facture($socid);
3929
3930 $this->stats_facturerec['customers'] += $pFather->stats_facturerec['customers'];
3931 $this->stats_facturerec['nb'] += $pFather->stats_facturerec['nb'];
3932 $this->stats_facturerec['rows'] += $pFather->stats_facturerec['rows'];
3933 $this->stats_facturerec['qty'] += $pFather->stats_facturerec['qty'] * $qtyCoef;
3934 }
3935 }
3936 }
3937 }
3938
3939 $parameters = array('socid' => $socid);
3940 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoiceRec', $parameters, $this, $action);
3941 if ($reshook > 0) {
3942 $this->stats_facturerec = $hookmanager->resArray['stats_facturerec'];
3943 }
3944
3945 return 1;
3946 } else {
3947 $this->error = $this->db->error();
3948 return -1;
3949 }
3950 }
3951
3952 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3959 public function load_stats_facture_fournisseur($socid = 0)
3960 {
3961 // phpcs:enable
3962 global $conf, $user, $hookmanager, $action;
3963
3964 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
3965 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3966 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as fd";
3967 $sql .= ", ".$this->db->prefix()."facture_fourn as f";
3968 $sql .= ", ".$this->db->prefix()."societe as s";
3969 if (!$user->hasRight('societe', 'client', 'voir')) {
3970 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3971 }
3972 $sql .= " WHERE f.rowid = fd.fk_facture_fourn";
3973 $sql .= " AND f.fk_soc = s.rowid";
3974 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
3975 $sql .= " AND fd.fk_product = ".((int) $this->id);
3976 if (!$user->hasRight('societe', 'client', 'voir')) {
3977 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3978 }
3979 //$sql.= " AND f.fk_statut != 0";
3980 if ($socid > 0) {
3981 $sql .= " AND f.fk_soc = ".((int) $socid);
3982 }
3983
3984 $result = $this->db->query($sql);
3985 if ($result) {
3986 $obj = $this->db->fetch_object($result);
3987 $this->stats_facture_fournisseur['suppliers'] = $obj->nb_suppliers;
3988 $this->stats_facture_fournisseur['nb'] = $obj->nb;
3989 $this->stats_facture_fournisseur['rows'] = $obj->nb_rows;
3990 $this->stats_facture_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3991
3992 $parameters = array('socid' => $socid);
3993 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoice', $parameters, $this, $action);
3994 if ($reshook > 0) {
3995 $this->stats_facture_fournisseur = $hookmanager->resArray['stats_facture_fournisseur'];
3996 }
3997
3998 return 1;
3999 } else {
4000 $this->error = $this->db->error();
4001 return -1;
4002 }
4003 }
4004
4005 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4014 private function _get_stats($sql, $mode, $year = 0)
4015 {
4016 // phpcs:enable
4017 $tab = array();
4018
4019 $resql = $this->db->query($sql);
4020 if ($resql) {
4021 $num = $this->db->num_rows($resql);
4022 $i = 0;
4023 while ($i < $num) {
4024 $arr = $this->db->fetch_array($resql);
4025 if (is_array($arr)) {
4026 $keyfortab = (string) $arr[1];
4027 if ($year == -1) {
4028 $keyfortab = substr($keyfortab, -2);
4029 }
4030
4031 if ($mode == 'byunit') {
4032 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[0]; // 1st field
4033 } elseif ($mode == 'bynumber') {
4034 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4035 } elseif ($mode == 'byamount') {
4036 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4037 } else {
4038 // Bad value for $mode
4039 return -1;
4040 }
4041 }
4042 $i++;
4043 }
4044 } else {
4045 $this->error = $this->db->error().' sql='.$sql;
4046 return -1;
4047 }
4048
4049 if (empty($year)) {
4050 $year = dol_print_date(time(), '%Y');
4051 $month = dol_print_date(time(), '%m');
4052 } elseif ($year == -1) {
4053 $year = '';
4054 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4055 } else {
4056 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4057 }
4058
4059 $result = array();
4060
4061 for ($j = 0; $j < 12; $j++) {
4062 // $ids is 'D', 'N', 'O', 'S', ... (First letter of month in user language)
4063 $idx = ucfirst(dol_trunc(dol_print_date(dol_mktime(12, 0, 0, $month, 1, 1970), "%b"), 1, 'right', 'UTF-8', 1));
4064
4065 //print $idx.'-'.$year.'-'.$month.'<br>';
4066 $result[$j] = array($idx, isset($tab[$year.$month]) ? $tab[$year.$month] : 0);
4067 // $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0);
4068
4069 $month = "0".($month - 1);
4070 if (dol_strlen($month) == 3) {
4071 $month = substr($month, 1);
4072 }
4073 if ($month == 0) {
4074 $month = 12;
4075 $year = $year - 1;
4076 }
4077 }
4078
4079 return array_reverse($result);
4080 }
4081
4082
4083 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4094 public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4095 {
4096 // phpcs:enable
4097 global $conf;
4098 global $user;
4099
4100 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4101 if ($mode == 'bynumber') {
4102 $sql .= ", count(DISTINCT f.rowid)";
4103 }
4104 $sql .= ", sum(d.total_ht) as total_ht";
4105 $sql .= " FROM ".$this->db->prefix()."facturedet as d, ".$this->db->prefix()."facture as f, ".$this->db->prefix()."societe as s";
4106 if ($filteronproducttype >= 0) {
4107 $sql .= ", ".$this->db->prefix()."product as p";
4108 }
4109 if (!$user->hasRight('societe', 'client', 'voir')) {
4110 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4111 }
4112 $sql .= " WHERE f.rowid = d.fk_facture";
4113 if ($this->id > 0) {
4114 $sql .= " AND d.fk_product = ".((int) $this->id);
4115 } else {
4116 $sql .= " AND d.fk_product > 0";
4117 }
4118 if ($filteronproducttype >= 0) {
4119 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4120 }
4121 $sql .= " AND f.fk_soc = s.rowid";
4122 $sql .= " AND f.entity IN (".getEntity('invoice').")";
4123 if (!$user->hasRight('societe', 'client', 'voir')) {
4124 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4125 }
4126 if ($socid > 0) {
4127 $sql .= " AND f.fk_soc = $socid";
4128 }
4129 $sql .= $morefilter;
4130 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4131 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4132
4133 return $this->_get_stats($sql, $mode, $year);
4134 }
4135
4136
4137 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4148 public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4149 {
4150 // phpcs:enable
4151 global $conf;
4152 global $user;
4153
4154 $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4155 if ($mode == 'bynumber') {
4156 $sql .= ", count(DISTINCT f.rowid)";
4157 }
4158 $sql .= ", sum(d.total_ht) as total_ht";
4159 $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as d, ".$this->db->prefix()."facture_fourn as f, ".$this->db->prefix()."societe as s";
4160 if ($filteronproducttype >= 0) {
4161 $sql .= ", ".$this->db->prefix()."product as p";
4162 }
4163 if (!$user->hasRight('societe', 'client', 'voir')) {
4164 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4165 }
4166 $sql .= " WHERE f.rowid = d.fk_facture_fourn";
4167 if ($this->id > 0) {
4168 $sql .= " AND d.fk_product = ".((int) $this->id);
4169 } else {
4170 $sql .= " AND d.fk_product > 0";
4171 }
4172 if ($filteronproducttype >= 0) {
4173 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4174 }
4175 $sql .= " AND f.fk_soc = s.rowid";
4176 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4177 if (!$user->hasRight('societe', 'client', 'voir')) {
4178 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4179 }
4180 if ($socid > 0) {
4181 $sql .= " AND f.fk_soc = $socid";
4182 }
4183 $sql .= $morefilter;
4184 $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4185 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4186
4187 return $this->_get_stats($sql, $mode, $year);
4188 }
4189
4190 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4201 public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4202 {
4203 // phpcs:enable
4204 global $conf, $user;
4205
4206 $sql = "SELECT sum(d.qty) as qty, date_format(p.datep, '%Y%m')";
4207 if ($mode == 'bynumber') {
4208 $sql .= ", count(DISTINCT p.rowid)";
4209 }
4210 $sql .= ", sum(d.total_ht) as total_ht";
4211 $sql .= " FROM ".$this->db->prefix()."propaldet as d, ".$this->db->prefix()."propal as p, ".$this->db->prefix()."societe as s";
4212 if ($filteronproducttype >= 0) {
4213 $sql .= ", ".$this->db->prefix()."product as prod";
4214 }
4215 if (!$user->hasRight('societe', 'client', 'voir')) {
4216 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4217 }
4218 $sql .= " WHERE p.rowid = d.fk_propal";
4219 if ($this->id > 0) {
4220 $sql .= " AND d.fk_product = ".((int) $this->id);
4221 } else {
4222 $sql .= " AND d.fk_product > 0";
4223 }
4224 if ($filteronproducttype >= 0) {
4225 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4226 }
4227 $sql .= " AND p.fk_soc = s.rowid";
4228 $sql .= " AND p.entity IN (".getEntity('propal').")";
4229 if (!$user->hasRight('societe', 'client', 'voir')) {
4230 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4231 }
4232 if ($socid > 0) {
4233 $sql .= " AND p.fk_soc = ".((int) $socid);
4234 }
4235 $sql .= $morefilter;
4236 $sql .= " GROUP BY date_format(p.datep,'%Y%m')";
4237 $sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC";
4238
4239 return $this->_get_stats($sql, $mode, $year);
4240 }
4241
4242 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4253 public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4254 {
4255 // phpcs:enable
4256 global $conf;
4257 global $user;
4258
4259 $sql = "SELECT sum(d.qty) as qty, date_format(p.date_valid, '%Y%m')";
4260 if ($mode == 'bynumber') {
4261 $sql .= ", count(DISTINCT p.rowid)";
4262 }
4263 $sql .= ", sum(d.total_ht) as total_ht";
4264 $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as d, ".$this->db->prefix()."supplier_proposal as p, ".$this->db->prefix()."societe as s";
4265 if ($filteronproducttype >= 0) {
4266 $sql .= ", ".$this->db->prefix()."product as prod";
4267 }
4268 if (!$user->hasRight('societe', 'client', 'voir')) {
4269 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4270 }
4271 $sql .= " WHERE p.rowid = d.fk_supplier_proposal";
4272 if ($this->id > 0) {
4273 $sql .= " AND d.fk_product = ".((int) $this->id);
4274 } else {
4275 $sql .= " AND d.fk_product > 0";
4276 }
4277 if ($filteronproducttype >= 0) {
4278 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4279 }
4280 $sql .= " AND p.fk_soc = s.rowid";
4281 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
4282 if (!$user->hasRight('societe', 'client', 'voir')) {
4283 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4284 }
4285 if ($socid > 0) {
4286 $sql .= " AND p.fk_soc = ".((int) $socid);
4287 }
4288 $sql .= $morefilter;
4289 $sql .= " GROUP BY date_format(p.date_valid,'%Y%m')";
4290 $sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
4291
4292 return $this->_get_stats($sql, $mode, $year);
4293 }
4294
4295 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4306 public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4307 {
4308 // phpcs:enable
4309 global $conf, $user;
4310
4311 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4312 if ($mode == 'bynumber') {
4313 $sql .= ", count(DISTINCT c.rowid)";
4314 }
4315 $sql .= ", sum(d.total_ht) as total_ht";
4316 $sql .= " FROM ".$this->db->prefix()."commandedet as d, ".$this->db->prefix()."commande as c, ".$this->db->prefix()."societe as s";
4317 if ($filteronproducttype >= 0) {
4318 $sql .= ", ".$this->db->prefix()."product as p";
4319 }
4320 if (!$user->hasRight('societe', 'client', 'voir')) {
4321 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4322 }
4323 $sql .= " WHERE c.rowid = d.fk_commande";
4324 if ($this->id > 0) {
4325 $sql .= " AND d.fk_product = ".((int) $this->id);
4326 } else {
4327 $sql .= " AND d.fk_product > 0";
4328 }
4329 if ($filteronproducttype >= 0) {
4330 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4331 }
4332 $sql .= " AND c.fk_soc = s.rowid";
4333 $sql .= " AND c.entity IN (".getEntity('commande').")";
4334 if (!$user->hasRight('societe', 'client', 'voir')) {
4335 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4336 }
4337 if ($socid > 0) {
4338 $sql .= " AND c.fk_soc = ".((int) $socid);
4339 }
4340 $sql .= $morefilter;
4341 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4342 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4343
4344 return $this->_get_stats($sql, $mode, $year);
4345 }
4346
4347 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4358 public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4359 {
4360 // phpcs:enable
4361 global $conf, $user;
4362
4363 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4364 if ($mode == 'bynumber') {
4365 $sql .= ", count(DISTINCT c.rowid)";
4366 }
4367 $sql .= ", sum(d.total_ht) as total_ht";
4368 $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as d, ".$this->db->prefix()."commande_fournisseur as c, ".$this->db->prefix()."societe as s";
4369 if ($filteronproducttype >= 0) {
4370 $sql .= ", ".$this->db->prefix()."product as p";
4371 }
4372 if (!$user->hasRight('societe', 'client', 'voir')) {
4373 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4374 }
4375 $sql .= " WHERE c.rowid = d.fk_commande";
4376 if ($this->id > 0) {
4377 $sql .= " AND d.fk_product = ".((int) $this->id);
4378 } else {
4379 $sql .= " AND d.fk_product > 0";
4380 }
4381 if ($filteronproducttype >= 0) {
4382 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4383 }
4384 $sql .= " AND c.fk_soc = s.rowid";
4385 $sql .= " AND c.entity IN (".getEntity('supplier_order').")";
4386 if (!$user->hasRight('societe', 'client', 'voir')) {
4387 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4388 }
4389 if ($socid > 0) {
4390 $sql .= " AND c.fk_soc = ".((int) $socid);
4391 }
4392 $sql .= $morefilter;
4393 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4394 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4395
4396 return $this->_get_stats($sql, $mode, $year);
4397 }
4398
4399 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4410 public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4411 {
4412 // phpcs:enable
4413 global $conf, $user;
4414
4415 $sql = "SELECT sum(d.qty) as qty, date_format(c.date_contrat, '%Y%m')";
4416 if ($mode == 'bynumber') {
4417 $sql .= ", count(DISTINCT c.rowid)";
4418 }
4419 $sql .= ", sum(d.total_ht) as total_ht";
4420 $sql .= " FROM ".$this->db->prefix()."contratdet as d, ".$this->db->prefix()."contrat as c, ".$this->db->prefix()."societe as s";
4421 if ($filteronproducttype >= 0) {
4422 $sql .= ", ".$this->db->prefix()."product as p";
4423 }
4424 if (!$user->hasRight('societe', 'client', 'voir')) {
4425 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4426 }
4427 $sql .= " WHERE c.entity IN (".getEntity('contract').")";
4428 $sql .= " AND c.rowid = d.fk_contrat";
4429
4430 if ($this->id > 0) {
4431 $sql .= " AND d.fk_product = ".((int) $this->id);
4432 } else {
4433 $sql .= " AND d.fk_product > 0";
4434 }
4435 if ($filteronproducttype >= 0) {
4436 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4437 }
4438 $sql .= " AND c.fk_soc = s.rowid";
4439
4440 if (!$user->hasRight('societe', 'client', 'voir')) {
4441 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4442 }
4443 if ($socid > 0) {
4444 $sql .= " AND c.fk_soc = ".((int) $socid);
4445 }
4446 $sql .= $morefilter;
4447 $sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')";
4448 $sql .= " ORDER BY date_format(c.date_contrat,'%Y%m') DESC";
4449
4450 return $this->_get_stats($sql, $mode, $year);
4451 }
4452
4453 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4464 public function get_nb_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4465 {
4466 // phpcs:enable
4467 global $conf, $user;
4468
4469 $sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')";
4470 if ($mode == 'bynumber') {
4471 $sql .= ", count(DISTINCT d.rowid)";
4472 }
4473 $sql .= " FROM ".$this->db->prefix()."mrp_mo as d LEFT JOIN ".$this->db->prefix()."societe as s ON d.fk_soc = s.rowid";
4474 if ($filteronproducttype >= 0) {
4475 $sql .= ", ".$this->db->prefix()."product as p";
4476 }
4477 if (!$user->hasRight('societe', 'client', 'voir')) {
4478 $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4479 }
4480
4481 $sql .= " WHERE d.entity IN (".getEntity('mo').")";
4482 $sql .= " AND d.status > 0";
4483
4484 if ($this->id > 0) {
4485 $sql .= " AND d.fk_product = ".((int) $this->id);
4486 } else {
4487 $sql .= " AND d.fk_product > 0";
4488 }
4489 if ($filteronproducttype >= 0) {
4490 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4491 }
4492
4493 if (!$user->hasRight('societe', 'client', 'voir')) {
4494 $sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4495 }
4496 if ($socid > 0) {
4497 $sql .= " AND d.fk_soc = ".((int) $socid);
4498 }
4499 $sql .= $morefilter;
4500 $sql .= " GROUP BY date_format(d.date_valid,'%Y%m')";
4501 $sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC";
4502
4503 return $this->_get_stats($sql, $mode, $year);
4504 }
4505
4506 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4517 public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
4518 {
4519 global $user;
4520
4521 // phpcs:enable
4522 // Clean parameters
4523 if (!is_numeric($id_pere)) {
4524 $id_pere = 0;
4525 }
4526 if (!is_numeric($id_fils)) {
4527 $id_fils = 0;
4528 }
4529 if (!is_numeric($incdec)) {
4530 $incdec = 0;
4531 }
4532
4533 $result = $this->del_sousproduit($id_pere, $id_fils);
4534 if ($result < 0) {
4535 return $result;
4536 }
4537
4538 // Check not already father of id_pere (to avoid father -> child -> father links)
4539 $sql = "SELECT fk_product_pere from ".$this->db->prefix()."product_association";
4540 $sql .= " WHERE fk_product_pere = ".((int) $id_fils)." AND fk_product_fils = ".((int) $id_pere);
4541 if (!$this->db->query($sql)) {
4542 dol_print_error($this->db);
4543 return -1;
4544 } else {
4545 //Selection of the highest row
4546 $sql = "SELECT MAX(rang) as max_rank FROM ".$this->db->prefix()."product_association";
4547 $sql .= " WHERE fk_product_pere = ".((int) $id_pere);
4548 $resql = $this->db->query($sql);
4549 if ($resql) {
4550 $obj = $this->db->fetch_object($resql);
4551 $rank = $obj->max_rank + 1;
4552 //Addition of a product with the highest rank +1
4553 $sql = "INSERT INTO ".$this->db->prefix()."product_association(fk_product_pere,fk_product_fils,qty,incdec,rang)";
4554 $sql .= " VALUES (".((int) $id_pere).", ".((int) $id_fils).", ".price2num($qty, 'MS').", ".((int) $incdec).", ".((int) $rank).")";
4555 if (! $this->db->query($sql)) {
4556 dol_print_error($this->db);
4557 return -1;
4558 } else {
4559 if (!$notrigger) {
4560 // Call trigger
4561 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_ADD', $user);
4562 if ($result < 0) {
4563 $this->error = $this->db->lasterror();
4564 dol_syslog(get_class($this).'::addSubproduct error='.$this->error, LOG_ERR);
4565 return -1;
4566 }
4567 }
4568 // End call triggers
4569
4570 return 1;
4571 }
4572 } else {
4573 dol_print_error($this->db);
4574 return -1;
4575 }
4576 }
4577 }
4578
4579 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4590 public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
4591 {
4592 global $user;
4593
4594 // phpcs:enable
4595 // Clean parameters
4596 if (!is_numeric($id_pere)) {
4597 $id_pere = 0;
4598 }
4599 if (!is_numeric($id_fils)) {
4600 $id_fils = 0;
4601 }
4602 if (!is_numeric($incdec)) {
4603 $incdec = 1;
4604 }
4605 if (!is_numeric($qty)) {
4606 $qty = 1;
4607 }
4608
4609 $sql = 'UPDATE '.$this->db->prefix().'product_association SET ';
4610 $sql .= 'qty = '.price2num($qty, 'MS');
4611 $sql .= ',incdec = '.((int) $incdec);
4612 $sql .= ' WHERE fk_product_pere = '.((int) $id_pere).' AND fk_product_fils = '.((int) $id_fils);
4613
4614 if (!$this->db->query($sql)) {
4615 dol_print_error($this->db);
4616 return -1;
4617 } else {
4618 if (!$notrigger) {
4619 // Call trigger
4620 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_UPDATE', $user);
4621 if ($result < 0) {
4622 $this->error = $this->db->lasterror();
4623 dol_syslog(get_class($this).'::updateSubproduct error='.$this->error, LOG_ERR);
4624 return -1;
4625 }
4626 // End call triggers
4627 }
4628
4629 return 1;
4630 }
4631 }
4632
4633 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4642 public function del_sousproduit($fk_parent, $fk_child, $notrigger = 0)
4643 {
4644 global $user;
4645
4646 // phpcs:enable
4647 if (!is_numeric($fk_parent)) {
4648 $fk_parent = 0;
4649 }
4650 if (!is_numeric($fk_child)) {
4651 $fk_child = 0;
4652 }
4653
4654 $sql = "DELETE FROM ".$this->db->prefix()."product_association";
4655 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
4656 $sql .= " AND fk_product_fils = ".((int) $fk_child);
4657
4658 dol_syslog(get_class($this).'::del_sousproduit', LOG_DEBUG);
4659 if (!$this->db->query($sql)) {
4660 dol_print_error($this->db);
4661 return -1;
4662 }
4663
4664 // Updated ranks so that none are missing
4665 $sqlrank = "SELECT rowid, rang FROM ".$this->db->prefix()."product_association";
4666 $sqlrank .= " WHERE fk_product_pere = ".((int) $fk_parent);
4667 $sqlrank .= " ORDER BY rang";
4668 $resqlrank = $this->db->query($sqlrank);
4669 if ($resqlrank) {
4670 $cpt = 0;
4671 while ($objrank = $this->db->fetch_object($resqlrank)) {
4672 $cpt++;
4673 $sql = "UPDATE ".$this->db->prefix()."product_association";
4674 $sql .= " SET rang = ".((int) $cpt);
4675 $sql .= " WHERE rowid = ".((int) $objrank->rowid);
4676 if (! $this->db->query($sql)) {
4677 dol_print_error($this->db);
4678 return -1;
4679 }
4680 }
4681 }
4682
4683 if (!$notrigger) {
4684 // Call trigger
4685 $result = $this->call_trigger('PRODUCT_SUBPRODUCT_DELETE', $user);
4686 if ($result < 0) {
4687 $this->error = $this->db->lasterror();
4688 dol_syslog(get_class($this).'::delSubproduct error='.$this->error, LOG_ERR);
4689 return -1;
4690 }
4691 // End call triggers
4692 }
4693
4694 return 1;
4695 }
4696
4697 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4705 public function is_sousproduit($fk_parent, $fk_child)
4706 {
4707 // phpcs:enable
4708 $sql = "SELECT fk_product_pere, qty, incdec";
4709 $sql .= " FROM ".$this->db->prefix()."product_association";
4710 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
4711 $sql .= " AND fk_product_fils = ".((int) $fk_child);
4712
4713 $result = $this->db->query($sql);
4714 if ($result) {
4715 $num = $this->db->num_rows($result);
4716
4717 if ($num > 0) {
4718 $obj = $this->db->fetch_object($result);
4719
4720 $this->is_sousproduit_qty = $obj->qty;
4721 $this->is_sousproduit_incdec = $obj->incdec;
4722
4723 return 1;
4724 } else {
4725 return 0;
4726 }
4727 } else {
4728 dol_print_error($this->db);
4729 return -1;
4730 }
4731 }
4732
4733
4734 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4745 public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
4746 {
4747 // phpcs:enable
4748 global $conf;
4749
4750 $now = dol_now();
4751
4752 dol_syslog(get_class($this)."::add_fournisseur id_fourn = ".$id_fourn." ref_fourn=".$ref_fourn." quantity=".$quantity, LOG_DEBUG);
4753
4754 // Clean parameters
4755 $quantity = price2num($quantity, 'MS');
4756
4757 if ($ref_fourn) {
4758 // Check if ref is not already used
4759 $sql = "SELECT rowid, fk_product";
4760 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
4761 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
4762 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
4763 $sql .= " AND fk_product <> ".((int) $this->id);
4764 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
4765
4766 $resql = $this->db->query($sql);
4767 if ($resql) {
4768 $obj = $this->db->fetch_object($resql);
4769 if ($obj) {
4770 // If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
4771 $this->product_id_already_linked = $obj->fk_product;
4772 return -3;
4773 }
4774 $this->db->free($resql);
4775 }
4776 }
4777
4778 $sql = "SELECT rowid";
4779 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
4780 $sql .= " WHERE fk_soc = ".((int) $id_fourn);
4781 if ($ref_fourn) {
4782 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
4783 } else {
4784 $sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
4785 }
4786 $sql .= " AND quantity = ".((float) $quantity);
4787 $sql .= " AND fk_product = ".((int) $this->id);
4788 $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
4789
4790 $resql = $this->db->query($sql);
4791 if ($resql) {
4792 $obj = $this->db->fetch_object($resql);
4793
4794 // The reference supplier does not exist, we create it for this product.
4795 if (empty($obj)) {
4796 $sql = "INSERT INTO ".$this->db->prefix()."product_fournisseur_price(";
4797 $sql .= "datec";
4798 $sql .= ", entity";
4799 $sql .= ", fk_product";
4800 $sql .= ", fk_soc";
4801 $sql .= ", ref_fourn";
4802 $sql .= ", quantity";
4803 $sql .= ", fk_user";
4804 $sql .= ", tva_tx";
4805 $sql .= ") VALUES (";
4806 $sql .= "'".$this->db->idate($now)."'";
4807 $sql .= ", ".((int) $conf->entity);
4808 $sql .= ", ".((int) $this->id);
4809 $sql .= ", ".((int) $id_fourn);
4810 $sql .= ", '".$this->db->escape($ref_fourn)."'";
4811 $sql .= ", ".((float) $quantity);
4812 $sql .= ", ".((int) $user->id);
4813 $sql .= ", 0";
4814 $sql .= ")";
4815
4816 if ($this->db->query($sql)) {
4817 $this->product_fourn_price_id = $this->db->last_insert_id($this->db->prefix()."product_fournisseur_price");
4818 return 1;
4819 } else {
4820 $this->error = $this->db->lasterror();
4821 return -1;
4822 }
4823 } else {
4824 // If the supplier price already exists for this product and quantity
4825 $this->product_fourn_price_id = $obj->rowid;
4826 return 0;
4827 }
4828 } else {
4829 $this->error = $this->db->lasterror();
4830 return -2;
4831 }
4832 }
4833
4834
4835 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4841 public function list_suppliers()
4842 {
4843 // phpcs:enable
4844 global $conf;
4845
4846 $list = array();
4847
4848 $sql = "SELECT DISTINCT p.fk_soc";
4849 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as p";
4850 $sql .= " WHERE p.fk_product = ".((int) $this->id);
4851 $sql .= " AND p.entity = ".((int) $conf->entity);
4852
4853 $result = $this->db->query($sql);
4854 if ($result) {
4855 $num = $this->db->num_rows($result);
4856 $i = 0;
4857 while ($i < $num) {
4858 $obj = $this->db->fetch_object($result);
4859 $list[$i] = $obj->fk_soc;
4860 $i++;
4861 }
4862 }
4863
4864 return $list;
4865 }
4866
4867 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4875 public function clone_price($fromId, $toId)
4876 {
4877 global $conf, $user;
4878
4879 $now = dol_now();
4880
4881 $this->db->begin();
4882
4883 // prices
4884 $sql = "INSERT INTO ".$this->db->prefix()."product_price (";
4885 $sql .= " entity";
4886 $sql .= ", fk_product";
4887 $sql .= ", date_price";
4888 $sql .= ", price_level";
4889 $sql .= ", price";
4890 $sql .= ", price_ttc";
4891 $sql .= ", price_min";
4892 $sql .= ", price_min_ttc";
4893 $sql .= ", price_base_type";
4894 $sql .= ", price_label";
4895 $sql .= ", default_vat_code";
4896 $sql .= ", tva_tx";
4897 $sql .= ", recuperableonly";
4898 $sql .= ", localtax1_tx";
4899 $sql .= ", localtax1_type";
4900 $sql .= ", localtax2_tx";
4901 $sql .= ", localtax2_type";
4902 $sql .= ", fk_user_author";
4903 $sql .= ", tosell";
4904 $sql .= ", price_by_qty";
4905 $sql .= ", fk_price_expression";
4906 $sql .= ", fk_multicurrency";
4907 $sql .= ", multicurrency_code";
4908 $sql .= ", multicurrency_tx";
4909 $sql .= ", multicurrency_price";
4910 $sql .= ", multicurrency_price_ttc";
4911 $sql .= ")";
4912 $sql .= " SELECT";
4913 $sql .= " entity";
4914 $sql .= ", ".$toId;
4915 $sql .= ", '".$this->db->idate($now)."'";
4916 $sql .= ", price_level";
4917 $sql .= ", price";
4918 $sql .= ", price_ttc";
4919 $sql .= ", price_min";
4920 $sql .= ", price_min_ttc";
4921 $sql .= ", price_base_type";
4922 $sql .= ", price_label";
4923 $sql .= ", default_vat_code";
4924 $sql .= ", tva_tx";
4925 $sql .= ", recuperableonly";
4926 $sql .= ", localtax1_tx";
4927 $sql .= ", localtax1_type";
4928 $sql .= ", localtax2_tx";
4929 $sql .= ", localtax2_type";
4930 $sql .= ", ".$user->id;
4931 $sql .= ", tosell";
4932 $sql .= ", price_by_qty";
4933 $sql .= ", fk_price_expression";
4934 $sql .= ", fk_multicurrency";
4935 $sql .= ", multicurrency_code";
4936 $sql .= ", multicurrency_tx";
4937 $sql .= ", multicurrency_price";
4938 $sql .= ", multicurrency_price_ttc";
4939 $sql .= " FROM ".$this->db->prefix()."product_price ps";
4940 $sql .= " WHERE fk_product = ".((int) $fromId);
4941 $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)";
4942 $sql .= " ORDER BY date_price DESC";
4943
4944 dol_syslog(__METHOD__, LOG_DEBUG);
4945 $resql = $this->db->query($sql);
4946 if (!$resql) {
4947 $this->db->rollback();
4948 return -1;
4949 }
4950
4951 $this->db->commit();
4952 return 1;
4953 }
4954
4955 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4963 public function clone_associations($fromId, $toId)
4964 {
4965 // phpcs:enable
4966 $this->db->begin();
4967
4968 $sql = 'INSERT INTO '.$this->db->prefix().'product_association (fk_product_pere, fk_product_fils, qty, incdec)';
4969 $sql .= " SELECT ".$toId.", fk_product_fils, qty, incdec FROM ".$this->db->prefix()."product_association";
4970 $sql .= " WHERE fk_product_pere = ".((int) $fromId);
4971
4972 dol_syslog(get_class($this).'::clone_association', LOG_DEBUG);
4973 if (!$this->db->query($sql)) {
4974 $this->db->rollback();
4975 return -1;
4976 }
4977
4978 $this->db->commit();
4979 return 1;
4980 }
4981
4982 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4990 public function clone_fournisseurs($fromId, $toId)
4991 {
4992 // phpcs:enable
4993 $this->db->begin();
4994
4995 $now = dol_now();
4996
4997 // les fournisseurs
4998 /*$sql = "INSERT ".$this->db->prefix()."product_fournisseur ("
4999 . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
5000 . " SELECT '".$this->db->idate($now)."', ".$toId.", fk_soc, ref_fourn, fk_user_author"
5001 . " FROM ".$this->db->prefix()."product_fournisseur"
5002 . " WHERE fk_product = ".((int) $fromId);
5003
5004 if ( ! $this->db->query($sql ) )
5005 {
5006 $this->db->rollback();
5007 return -1;
5008 }*/
5009
5010 // les prix de fournisseurs.
5011 $sql = "INSERT ".$this->db->prefix()."product_fournisseur_price (";
5012 $sql .= " datec, fk_product, fk_soc, price, quantity, fk_user, tva_tx)";
5013 $sql .= " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, price, quantity, fk_user, tva_tx";
5014 $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
5015 $sql .= " WHERE fk_product = ".((int) $fromId);
5016
5017 dol_syslog(get_class($this).'::clone_fournisseurs', LOG_DEBUG);
5018 $resql = $this->db->query($sql);
5019 if (!$resql) {
5020 $this->db->rollback();
5021 return -1;
5022 } else {
5023 $this->db->commit();
5024 return 1;
5025 }
5026 }
5027
5028 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5041 public function fetch_prod_arbo($prod, $compl_path = '', $multiply = 1, $level = 1, $id_parent = 0, $ignore_stock_load = 0)
5042 {
5043 // phpcs:enable
5044 global $conf, $langs;
5045
5046 $tmpproduct = null;
5047 //var_dump($prod);
5048 foreach ($prod as $id_product => $desc_pere) { // $id_product is 0 (first call starting with root top) or an id of a sub_product
5049 if (is_array($desc_pere)) { // If desc_pere is an array, this means it's a child
5050 $id = (!empty($desc_pere[0]) ? $desc_pere[0] : '');
5051 $nb = (!empty($desc_pere[1]) ? $desc_pere[1] : '');
5052 $type = (!empty($desc_pere[2]) ? $desc_pere[2] : '');
5053 $label = (!empty($desc_pere[3]) ? $desc_pere[3] : '');
5054 $incdec = (!empty($desc_pere[4]) ? $desc_pere[4] : 0);
5055
5056 if ($multiply < 1) {
5057 $multiply = 1;
5058 }
5059
5060 //print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n";
5061 if (is_null($tmpproduct)) {
5062 $tmpproduct = new Product($this->db); // So we initialize tmpproduct only once for all loop.
5063 }
5064 $tmpproduct->fetch($id); // Load product to get ->ref
5065
5066 if (empty($ignore_stock_load) && ($tmpproduct->isProduct() || getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) {
5067 $tmpproduct->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
5068 }
5069
5070 $this->res[] = array(
5071 'id' => $id, // Id product
5072 'id_parent' => $id_parent,
5073 'ref' => $tmpproduct->ref, // Ref product
5074 'nb' => $nb, // Nb of units that compose parent product
5075 'nb_total' => $nb * $multiply, // Nb of units for all nb of product
5076 'stock' => $tmpproduct->stock_reel, // Stock
5077 'stock_alert' => $tmpproduct->seuil_stock_alerte, // Stock alert
5078 'label' => $label,
5079 'fullpath' => $compl_path.$label, // Label
5080 'type' => $type, // Nb of units that compose parent product
5081 'desiredstock' => $tmpproduct->desiredstock,
5082 'level' => $level,
5083 'incdec' => $incdec,
5084 'entity' => $tmpproduct->entity
5085 );
5086
5087 // Recursive call if there child has children of its own
5088 if (isset($desc_pere['childs']) && is_array($desc_pere['childs'])) {
5089 //print 'YYY We go down for '.$desc_pere[3]." -> \n";
5090 $this->fetch_prod_arbo($desc_pere['childs'], $compl_path.$desc_pere[3]." -> ", $desc_pere[1] * $multiply, $level + 1, $id, $ignore_stock_load);
5091 }
5092 }
5093 }
5094 }
5095
5096 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5105 public function get_arbo_each_prod($multiply = 1, $ignore_stock_load = 0)
5106 {
5107 // phpcs:enable
5108 $this->res = array();
5109 if (isset($this->sousprods) && is_array($this->sousprods)) {
5110 foreach ($this->sousprods as $prod_name => $desc_product) {
5111 if (is_array($desc_product)) {
5112 $this->fetch_prod_arbo($desc_product, "", $multiply, 1, $this->id, $ignore_stock_load); // This set $this->res
5113 }
5114 }
5115 }
5116 //var_dump($res);
5117 return $this->res;
5118 }
5119
5127 public function hasFatherOrChild($mode = 0)
5128 {
5129 $nb = 0;
5130
5131 $sql = "SELECT COUNT(pa.rowid) as nb";
5132 $sql .= " FROM ".$this->db->prefix()."product_association as pa";
5133 if ($mode == 0) {
5134 $sql .= " WHERE pa.fk_product_fils = ".((int) $this->id)." OR pa.fk_product_pere = ".((int) $this->id);
5135 } elseif ($mode == -1) {
5136 $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)
5137 } elseif ($mode == 1) {
5138 $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)
5139 }
5140
5141 $resql = $this->db->query($sql);
5142 if ($resql) {
5143 $obj = $this->db->fetch_object($resql);
5144 if ($obj) {
5145 $nb = $obj->nb;
5146 }
5147 } else {
5148 return -1;
5149 }
5150
5151 return $nb;
5152 }
5153
5159 public function hasVariants()
5160 {
5161 $nb = 0;
5162 $sql = "SELECT count(rowid) as nb FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_parent = ".((int) $this->id);
5163 $sql .= " AND entity IN (".getEntity('product').")";
5164
5165 $resql = $this->db->query($sql);
5166 if ($resql) {
5167 $obj = $this->db->fetch_object($resql);
5168 if ($obj) {
5169 $nb = $obj->nb;
5170 }
5171 }
5172
5173 return $nb;
5174 }
5175
5176
5182 public function isVariant()
5183 {
5184 global $conf;
5185 if (isModEnabled('variants')) {
5186 $sql = "SELECT rowid FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_child = ".((int) $this->id)." AND entity IN (".getEntity('product').")";
5187
5188 $query = $this->db->query($sql);
5189
5190 if ($query) {
5191 if (!$this->db->num_rows($query)) {
5192 return false;
5193 }
5194 return true;
5195 } else {
5196 dol_print_error($this->db);
5197 return -1;
5198 }
5199 } else {
5200 return false;
5201 }
5202 }
5203
5210 public function getFather()
5211 {
5212 $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";
5213 $sql .= ", p.tosell as status, p.tobuy as status_buy";
5214 $sql .= " FROM ".$this->db->prefix()."product_association as pa,";
5215 $sql .= " ".$this->db->prefix()."product as p";
5216 $sql .= " WHERE p.rowid = pa.fk_product_pere";
5217 $sql .= " AND pa.fk_product_fils = ".((int) $this->id);
5218
5219 $res = $this->db->query($sql);
5220 if ($res) {
5221 $prods = array();
5222 while ($record = $this->db->fetch_array($res)) {
5223 // $record['id'] = $record['rowid'] = id of father
5224 $prods[$record['id']]['id'] = $record['rowid'];
5225 $prods[$record['id']]['ref'] = $record['ref'];
5226 $prods[$record['id']]['label'] = $record['label'];
5227 $prods[$record['id']]['qty'] = $record['qty'];
5228 $prods[$record['id']]['incdec'] = $record['incdec'];
5229 $prods[$record['id']]['fk_product_type'] = $record['fk_product_type'];
5230 $prods[$record['id']]['entity'] = $record['entity'];
5231 $prods[$record['id']]['status'] = $record['status'];
5232 $prods[$record['id']]['status_buy'] = $record['status_buy'];
5233 }
5234 return $prods;
5235 } else {
5236 dol_print_error($this->db);
5237 return -1;
5238 }
5239 }
5240
5241
5251 public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array())
5252 {
5253 global $alreadyfound;
5254
5255 if (empty($id)) {
5256 return array();
5257 }
5258
5259 $sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,";
5260 $sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec,";
5261 $sql .= " pa.rowid as fk_association, pa.rang";
5262 $sql .= " FROM ".$this->db->prefix()."product as p,";
5263 $sql .= " ".$this->db->prefix()."product_association as pa";
5264 $sql .= " WHERE p.rowid = pa.fk_product_fils";
5265 $sql .= " AND pa.fk_product_pere = ".((int) $id);
5266 $sql .= " AND pa.fk_product_fils <> ".((int) $id); // This should not happens, it is to avoid infinite loop if it happens
5267 $sql .= " ORDER BY pa.rang";
5268
5269 dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level. ' parents='.(is_array($parents) ? implode(',', $parents) : $parents), LOG_DEBUG);
5270
5271 if ($level == 1) {
5272 $alreadyfound = array($id => 1); // We init array of found object to start of tree, so if we found it later (should not happened), we stop immediately
5273 }
5274 // Protection against infinite loop
5275 if ($level > 30) {
5276 return array();
5277 }
5278
5279 $res = $this->db->query($sql);
5280 if ($res) {
5281 $prods = array();
5282 while ($rec = $this->db->fetch_array($res)) {
5283 if (!empty($alreadyfound[$rec['rowid']])) {
5284 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);
5285 if (in_array($rec['id'], $parents)) {
5286 continue; // We discard this child if it is already found at a higher level in tree in the same branch.
5287 }
5288 }
5289 $alreadyfound[$rec['rowid']] = 1;
5290 $prods[$rec['rowid']] = array(
5291 0 => $rec['rowid'],
5292 1 => $rec['qty'],
5293 2 => $rec['fk_product_type'],
5294 3 => $this->db->escape($rec['label']),
5295 4 => $rec['incdec'],
5296 5 => $rec['ref'],
5297 6 => $rec['fk_association'],
5298 7 => $rec['rang']
5299 );
5300 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
5301 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
5302 if (empty($firstlevelonly)) {
5303 $parents[] = $rec['rowid'];
5304 $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, $parents);
5305 foreach ($listofchilds as $keyChild => $valueChild) {
5306 $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
5307 }
5308 }
5309 }
5310
5311 return $prods;
5312 } else {
5313 dol_print_error($this->db);
5314 return -1;
5315 }
5316 }
5317
5318 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5325 public function get_sousproduits_arbo()
5326 {
5327 // phpcs:enable
5328 $parent = array();
5329
5330 foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) { // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
5331 $parent[$this->label][$keyChild] = $valueChild;
5332 }
5333 foreach ($parent as $key => $value) { // key=label, value is array of children
5334 $this->sousprods[$key] = $value;
5335 }
5336 }
5337
5344 public function getTooltipContentArray($params)
5345 {
5346 global $conf, $langs, $user;
5347
5348 $langs->loadLangs(array('products', 'other'));
5349
5350 $datas = array();
5351 $nofetch = !empty($params['nofetch']);
5352
5353 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5354 return ['optimize' => $langs->trans("ShowProduct")];
5355 }
5356
5357 if (!empty($this->entity)) {
5358 $tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80, 0, 0, 0, 0, 1);
5359 if ($this->nbphoto > 0) {
5360 $datas['photo'] = '<div class="photointooltip floatright">'."\n" . $tmpphoto . '</div>';
5361 }
5362 }
5363
5364 if ($this->isProduct()) {
5365 $datas['picto'] = img_picto('', 'product').' <u class="paddingrightonly">'.$langs->trans("Product").'</u>';
5366 } elseif ($this->isService()) {
5367 $datas['picto'] = img_picto('', 'service').' <u class="paddingrightonly">'.$langs->trans("Service").'</u>';
5368 }
5369 if (isset($this->status) && isset($this->status_buy)) {
5370 $datas['status'] = ' '.$this->getLibStatut(5, 0) . ' '.$this->getLibStatut(5, 1);
5371 }
5372
5373 if (!empty($this->ref)) {
5374 $datas['ref'] = '<br><b>'.$langs->trans('ProductRef').':</b> '.$this->ref;
5375 }
5376 if (!empty($this->label)) {
5377 $datas['label'] = '<br><b>'.$langs->trans('ProductLabel').':</b> '.$this->label;
5378 }
5379 if (!empty($this->description)) {
5380 $datas['description'] = '<br><b>'.$langs->trans('ProductDescription').':</b> '.dolGetFirstLineOfText($this->description, 5);
5381 }
5382 if ($this->isStockManaged()) {
5383 if (isModEnabled('productbatch')) {
5384 $langs->load("productbatch");
5385 $datas['batchstatus'] = "<br><b>".$langs->trans("ManageLotSerial").'</b>: '.$this->getLibStatut(0, 2);
5386 }
5387 }
5388 if (isModEnabled('barcode')) {
5389 $datas['barcode'] = '<br><b>'.$langs->trans('BarCode').':</b> '.$this->barcode;
5390 }
5391
5392 if ($this->isProduct()) {
5393 if ($this->weight) {
5394 $datas['weight'] = "<br><b>".$langs->trans("Weight").'</b>: '.$this->weight.' '.measuringUnitString(0, "weight", $this->weight_units);
5395 }
5396 $labelsize = "";
5397 if ($this->length) {
5398 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Length").'</b>: '.$this->length.' '.measuringUnitString(0, 'size', $this->length_units);
5399 }
5400 if ($this->width) {
5401 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Width").'</b>: '.$this->width.' '.measuringUnitString(0, 'size', $this->width_units);
5402 }
5403 if ($this->height) {
5404 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Height").'</b>: '.$this->height.' '.measuringUnitString(0, 'size', $this->height_units);
5405 }
5406 if ($labelsize) {
5407 $datas['size'] = "<br>".$labelsize;
5408 }
5409
5410 $labelsurfacevolume = "";
5411 if ($this->surface) {
5412 $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Surface").'</b>: '.$this->surface.' '.measuringUnitString(0, 'surface', $this->surface_units);
5413 }
5414 if ($this->volume) {
5415 $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Volume").'</b>: '.$this->volume.' '.measuringUnitString(0, 'volume', $this->volume_units);
5416 }
5417 if ($labelsurfacevolume) {
5418 $datas['surface'] = "<br>" . $labelsurfacevolume;
5419 }
5420 }
5421 if ($this->isService() && !empty($this->duration_value)) {
5422 // Duration
5423 $datas['duration'] = '<br><b>'.$langs->trans("Duration").':</b> '.$this->duration_value;
5424 if ($this->duration_value > 1) {
5425 $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"));
5426 } elseif ($this->duration_value > 0) {
5427 $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"));
5428 }
5429 $datas['duration'] .= (!empty($this->duration_unit) && isset($dur[$this->duration_unit]) ? "&nbsp;".$langs->trans($dur[$this->duration_unit]) : '');
5430 }
5431 if (empty($user->socid)) {
5432 if (!empty($this->pmp) && $this->pmp) {
5433 $datas['pmp'] = "<br><b>".$langs->trans("PMPValue").'</b>: '.price($this->pmp, 0, '', 1, -1, -1, $conf->currency);
5434 }
5435
5436 if (isModEnabled('accounting')) {
5437 if ($this->status && isset($this->accountancy_code_sell)) {
5438 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5439 $selllabel = '<br>';
5440 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellCode').':</b> '.length_accountg($this->accountancy_code_sell);
5441 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellIntraCode').':</b> '.length_accountg($this->accountancy_code_sell_intra);
5442 $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellExportCode').':</b> '.length_accountg($this->accountancy_code_sell_export);
5443 $datas['accountancysell'] = $selllabel;
5444 }
5445 if ($this->status_buy && isset($this->accountancy_code_buy)) {
5446 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5447 $buylabel = '';
5448 if (empty($this->status)) {
5449 $buylabel .= '<br>';
5450 }
5451 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyCode').':</b> '.length_accountg($this->accountancy_code_buy);
5452 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyIntraCode').':</b> '.length_accountg($this->accountancy_code_buy_intra);
5453 $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyExportCode').':</b> '.length_accountg($this->accountancy_code_buy_export);
5454 $datas['accountancybuy'] = $buylabel;
5455 }
5456 }
5457 }
5458 // show categories for this record only in ajax to not overload lists
5459 if (isModEnabled('category') && !$nofetch) {
5460 require_once DOL_DOCUMENT_ROOT . '/categories/class/categorie.class.php';
5461 $form = new Form($this->db);
5462 $datas['categories'] = '<br>' . $form->showCategories($this->id, Categorie::TYPE_PRODUCT, 1);
5463 }
5464
5465 return $datas;
5466 }
5467
5481 public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0, $morecss = '', $add_label = 0, $sep = ' - ')
5482 {
5483 global $conf, $langs, $hookmanager, $user;
5484 include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
5485
5486 $result = '';
5487
5488 $newref = $this->ref;
5489 if ($maxlength) {
5490 $newref = dol_trunc($newref, $maxlength, 'middle');
5491 }
5492 $params = [
5493 'id' => $this->id,
5494 'objecttype' => (isset($this->type) ? ($this->type == 1 ? 'service' : 'product') : $this->element),
5495 'option' => $option,
5496 'nofetch' => 1,
5497 ];
5498 $classfortooltip = 'classfortooltip';
5499 $dataparams = '';
5500 if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
5501 $classfortooltip = 'classforajaxtooltip';
5502 $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
5503 $label = '';
5504 } else {
5505 $label = implode($this->getTooltipContentArray($params));
5506 }
5507
5508 $linkclose = '';
5509 if (empty($notooltip)) {
5510 if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5511 $label = $langs->trans("ShowProduct");
5512 $linkclose .= ' alt="'.dol_escape_htmltag($label, 1, 1).'"';
5513 }
5514 $linkclose .= ($label ? ' title="'.dol_escape_htmltag($label, 1, 1).'"' : ' title="tocomplete"');
5515 $linkclose .= $dataparams.' class="nowraponall '.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
5516 } else {
5517 $linkclose = ' class="nowraponall'.($morecss ? ' '.$morecss : '').'"';
5518 }
5519
5520 if ($option == 'supplier' || $option == 'category') {
5521 $url = DOL_URL_ROOT.'/product/price_suppliers.php?id='.$this->id;
5522 } elseif ($option == 'stock') {
5523 $url = DOL_URL_ROOT.'/product/stock/product.php?id='.$this->id;
5524 } elseif ($option == 'composition') {
5525 $url = DOL_URL_ROOT.'/product/composition/card.php?id='.$this->id;
5526 } else {
5527 $url = DOL_URL_ROOT.'/product/card.php?id='.$this->id;
5528 }
5529
5530 if ($option !== 'nolink') {
5531 // Add param to save lastsearch_values or not
5532 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
5533 if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
5534 $add_save_lastsearch_values = 1;
5535 }
5536 if ($add_save_lastsearch_values) {
5537 $url .= '&save_lastsearch_values=1';
5538 }
5539 }
5540
5541 $linkstart = '<a href="'.$url.'"';
5542 $linkstart .= $linkclose.'>';
5543 $linkend = '</a>';
5544
5545 $result .= $linkstart;
5546 if ($withpicto) {
5547 if ($this->isProduct()) {
5548 $result .= (img_object(($notooltip ? '' : $label), 'product', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5549 }
5550 if ($this->isService()) {
5551 $result .= (img_object(($notooltip ? '' : $label), 'service', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5552 }
5553 }
5554 $result .= '<span class="aaa">'.dol_escape_htmltag($newref).'</span>';
5555 $result .= $linkend;
5556 if ($withpicto != 2) {
5557 $result .= (($add_label && $this->label) ? $sep.dol_trunc($this->label, ($add_label > 1 ? $add_label : 0)) : '');
5558 }
5559
5560 global $action;
5561 $hookmanager->initHooks(array('productdao'));
5562 $parameters = array('id' => $this->id, 'getnomurl' => &$result, 'label' => &$label);
5563 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
5564 if ($reshook > 0) {
5565 $result = $hookmanager->resPrint;
5566 } else {
5567 $result .= $hookmanager->resPrint;
5568 }
5569
5570 return $result;
5571 }
5572
5573
5584 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
5585 {
5586 global $conf, $user, $langs;
5587
5588 $langs->load("products");
5589 $outputlangs->load("products");
5590
5591 // Positionne le modele sur le nom du modele a utiliser
5592 if (!dol_strlen($modele)) {
5593 $modele = getDolGlobalString('PRODUCT_ADDON_PDF', 'strato');
5594 }
5595
5596 $modelpath = "core/modules/product/doc/";
5597
5598 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
5599 }
5600
5608 public function getLibStatut($mode = 0, $type = 0)
5609 {
5610 switch ($type) {
5611 case 0:
5612 return $this->LibStatut($this->status, $mode, $type);
5613 case 1:
5614 return $this->LibStatut($this->status_buy, $mode, $type);
5615 case 2:
5616 return $this->LibStatut($this->status_batch, $mode, $type);
5617 default:
5618 //Simulate previous behavior but should return an error string
5619 return $this->LibStatut($this->status_buy, $mode, $type);
5620 }
5621 }
5622
5623 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5632 public function LibStatut($status, $mode = 0, $type = 0)
5633 {
5634 // phpcs:enable
5635 global $conf, $langs;
5636
5637 $labelStatus = $labelStatusShort = '';
5638
5639 $langs->load('products');
5640 if (isModEnabled('productbatch')) {
5641 $langs->load("productbatch");
5642 }
5643
5644 if ($type == 2) {
5645 switch ($mode) {
5646 case 0:
5647 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatch') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial')));
5648 return dolGetStatus($label);
5649 case 1:
5650 $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort')));
5651 return dolGetStatus($label);
5652 case 2:
5653 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 1, 2);
5654 case 3:
5655 return dolGetStatus($langs->transnoentitiesnoconv('ProductStatusNotOnBatch'), '', '', empty($status) ? 'status5' : 'status4', 3, 'dot');
5656 case 4:
5657 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 0, 2);
5658 case 5:
5659 return $this->LibStatut($status, 1, 2).' '.$this->LibStatut($status, 3, 2);
5660 default:
5661 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'));
5662 }
5663 }
5664
5665 $statuttrans = empty($status) ? 'status5' : 'status4';
5666
5667 if ($status == 0) {
5668 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
5669 if ($type == 0) {
5670 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnSellShort');
5671 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnSell');
5672 } elseif ($type == 1) {
5673 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBuyShort');
5674 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBuy');
5675 } elseif ($type == 2) {
5676 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBatch');
5677 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort');
5678 }
5679 } elseif ($status == 1) {
5680 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
5681 if ($type == 0) {
5682 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSellShort');
5683 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSell');
5684 } elseif ($type == 1) {
5685 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBuyShort');
5686 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBuy');
5687 } elseif ($type == 2) {
5688 $labelStatus = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial'));
5689 $labelStatusShort = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort'));
5690 }
5691 } elseif ($type == 2 && $status == 2) {
5692 $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSerial');
5693 $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSerialShort');
5694 }
5695
5696 if ($mode > 6) {
5697 return dolGetStatus($langs->transnoentitiesnoconv('Unknown'), '', '', 'status0', 0);
5698 } else {
5699 return dolGetStatus($labelStatus, $labelStatusShort, '', $statuttrans, $mode);
5700 }
5701 }
5702
5703
5709 public function getLibFinished()
5710 {
5711 global $langs;
5712 $langs->load('products');
5713 $label = '';
5714
5715 if (isset($this->finished) && $this->finished >= 0) {
5716 $sql = "SELECT label, code FROM ".$this->db->prefix()."c_product_nature where code = ".((int) $this->finished)." AND active=1";
5717 $resql = $this->db->query($sql);
5718 if (!$resql) {
5719 $this->error = $this->db->error().' sql='.$sql;
5720 dol_syslog(__METHOD__.' Error '.$this->error, LOG_ERR);
5721 return -1;
5722 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
5723 $label = $langs->trans($res['label']);
5724 }
5725 $this->db->free($resql);
5726 }
5727
5728 return $label;
5729 }
5730
5731
5732 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5749 public function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null)
5750 {
5751 // phpcs:enable
5752 if ($id_entrepot) {
5753 $this->db->begin();
5754
5755 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
5756
5757 if ($nbpiece < 0) {
5758 if (!$movement) {
5759 $movement = 1;
5760 }
5761 $nbpiece = abs($nbpiece);
5762 }
5763 $op = array();
5764 $op[0] = "+".trim((string) $nbpiece);
5765 $op[1] = "-".trim((string) $nbpiece);
5766
5767 $movementstock = new MouvementStock($this->db);
5768 $movementstock->setOrigin($origin_element, $origin_id); // Set ->origin_type and ->origin_id
5769 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', '', '', '', false, 0, $disablestockchangeforsubproduct);
5770
5771 if ($result >= 0) {
5772 if ($extrafields) {
5773 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
5774 $movementstock->array_options = $array_options;
5775 $movementstock->insertExtraFields();
5776 }
5777 $this->db->commit();
5778 return 1;
5779 } else {
5780 $this->error = $movementstock->error;
5781 $this->errors = $movementstock->errors;
5782
5783 $this->db->rollback();
5784 return -1;
5785 }
5786 }
5787
5788 return -1;
5789 }
5790
5791 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5812 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)
5813 {
5814 // phpcs:enable
5815 if ($id_entrepot) {
5816 $this->db->begin();
5817
5818 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
5819
5820 if ($nbpiece < 0) {
5821 if (!$movement) {
5822 $movement = 1;
5823 }
5824 $nbpiece = abs($nbpiece);
5825 }
5826
5827 $op = array();
5828 $op[0] = "+".trim((string) $nbpiece);
5829 $op[1] = "-".trim((string) $nbpiece);
5830
5831 $movementstock = new MouvementStock($this->db);
5832 $movementstock->setOrigin($origin_element, $origin_id); // Set ->origin_type and ->fk_origin
5833 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', $dlc, $dluo, $lot, false, 0, $disablestockchangeforsubproduct, 0, $force_update_batch);
5834
5835 if ($result >= 0) {
5836 if ($extrafields) {
5837 $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
5838 $movementstock->array_options = $array_options;
5839 $movementstock->insertExtraFields();
5840 }
5841 $this->db->commit();
5842 return 1;
5843 } else {
5844 $this->error = $movementstock->error;
5845 $this->errors = $movementstock->errors;
5846
5847 $this->db->rollback();
5848 return -1;
5849 }
5850 }
5851 return -1;
5852 }
5853
5854 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5867 public function load_stock($option = '', $includedraftpoforvirtual = null, $dateofvirtualstock = null)
5868 {
5869 // phpcs:enable
5870 global $conf;
5871
5872 $this->stock_reel = 0;
5873 $this->stock_warehouse = array();
5874 $this->stock_theorique = 0;
5875
5876 // Set filter on warehouse status
5877 $warehouseStatus = array();
5878 if (preg_match('/warehouseclosed/', $option)) {
5880 }
5881 if (preg_match('/warehouseopen/', $option)) {
5883 }
5884 if (preg_match('/warehouseinternal/', $option)) {
5885 if (getDolGlobalString('ENTREPOT_EXTRA_STATUS')) {
5887 } else {
5889 }
5890 }
5891
5892 $sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot";
5893 $sql .= " FROM ".$this->db->prefix()."product_stock as ps";
5894 $sql .= ", ".$this->db->prefix()."entrepot as w";
5895 $sql .= " WHERE w.entity IN (".getEntity('stock').")";
5896 $sql .= " AND w.rowid = ps.fk_entrepot";
5897 $sql .= " AND ps.fk_product = ".((int) $this->id);
5898 if (count($warehouseStatus)) {
5899 $sql .= " AND w.statut IN (".$this->db->sanitize(implode(',', $warehouseStatus)).")";
5900 }
5901
5902 $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;
5903
5904 dol_syslog(get_class($this)."::load_stock", LOG_DEBUG);
5905 $result = $this->db->query($sql);
5906 if ($result) {
5907 $num = $this->db->num_rows($result);
5908 $i = 0;
5909 if ($num > 0) {
5910 while ($i < $num) {
5911 $row = $this->db->fetch_object($result);
5912 $this->stock_warehouse[$row->fk_entrepot] = new stdClass();
5913 $this->stock_warehouse[$row->fk_entrepot]->real = $row->reel;
5914 $this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid;
5915 if ((!preg_match('/nobatch/', $option)) && $this->hasbatch()) {
5916 $this->stock_warehouse[$row->fk_entrepot]->detail_batch = Productbatch::findAll($this->db, $row->rowid, 1, $this->id);
5917 }
5918 $this->stock_reel += $row->reel;
5919 $i++;
5920 }
5921 }
5922 $this->db->free($result);
5923
5924 if (!preg_match('/novirtual/', $option)) {
5925 $this->load_virtual_stock($includedraftpoforvirtual, $dateofvirtualstock); // This load stock_theorique and also load all arrays stats_xxx...
5926 }
5927
5928 return 1;
5929 } else {
5930 $this->error = $this->db->lasterror();
5931 return -1;
5932 }
5933 }
5934
5935
5936 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5946 public function load_virtual_stock($includedraftpoforvirtual = null, $dateofvirtualstock = null)
5947 {
5948 // phpcs:enable
5949 global $conf, $hookmanager, $action;
5950
5951 $stock_commande_client = 0;
5952 $stock_commande_fournisseur = 0;
5953 $stock_sending_client = 0;
5954 $stock_reception_fournisseur = 0;
5955 $stock_inproduction = 0;
5956
5957 //dol_syslog("load_virtual_stock");
5958
5959 if (isModEnabled('order')) {
5960 $result = $this->load_stats_commande(0, '1,2', 1);
5961 if ($result < 0) {
5962 dol_print_error($this->db, $this->error);
5963 }
5964 $stock_commande_client = $this->stats_commande['qty'];
5965 }
5966 if (isModEnabled("shipping")) {
5967 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
5968 $filterShipmentStatus = '';
5969 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
5970 $filterShipmentStatus = Expedition::STATUS_VALIDATED.','.Expedition::STATUS_CLOSED;
5971 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
5972 $filterShipmentStatus = Expedition::STATUS_CLOSED;
5973 }
5974 $result = $this->load_stats_sending(0, '1,2', 1, $filterShipmentStatus);
5975 if ($result < 0) {
5976 dol_print_error($this->db, $this->error);
5977 }
5978 $stock_sending_client = $this->stats_expedition['qty'];
5979 }
5980 if (isModEnabled("supplier_order")) {
5981 $filterStatus = getDolGlobalString('SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK', '3,4');
5982 if (isset($includedraftpoforvirtual)) {
5983 $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
5984 }
5985 $result = $this->load_stats_commande_fournisseur(0, $filterStatus, 1, $dateofvirtualstock);
5986 if ($result < 0) {
5987 dol_print_error($this->db, $this->error);
5988 }
5989 $stock_commande_fournisseur = $this->stats_commande_fournisseur['qty'];
5990 }
5991 if ((isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) && empty($conf->reception->enabled)) {
5992 // Case module reception is not used
5993 $filterStatus = '4';
5994 if (isset($includedraftpoforvirtual)) {
5995 $filterStatus = '0,'.$filterStatus;
5996 }
5997 $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock);
5998 if ($result < 0) {
5999 dol_print_error($this->db, $this->error);
6000 }
6001 $stock_reception_fournisseur = $this->stats_reception['qty'];
6002 }
6003 if ((isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) && isModEnabled("reception")) {
6004 // Case module reception is used
6005 $filterStatus = '4';
6006 if (isset($includedraftpoforvirtual)) {
6007 $filterStatus = '0,'.$filterStatus;
6008 }
6009 $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock); // Use same tables than when module reception is not used.
6010 if ($result < 0) {
6011 dol_print_error($this->db, $this->error);
6012 }
6013 $stock_reception_fournisseur = $this->stats_reception['qty'];
6014 }
6015 if (isModEnabled('mrp')) {
6016 $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock);
6017 if ($result < 0) {
6018 dol_print_error($this->db, $this->error);
6019 }
6020 $stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty'];
6021 }
6022
6023 $this->stock_theorique = $this->stock_reel + $stock_inproduction;
6024
6025 // Stock decrease mode
6026 if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') || getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
6027 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
6028 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_VALIDATE_ORDER')) {
6029 $this->stock_theorique += 0;
6030 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
6031 $this->stock_theorique -= $stock_commande_client;
6032 }
6033 // Stock Increase mode
6034 if (getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION') || getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION_CLOSE')) {
6035 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6036 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_DISPATCH_ORDER')) {
6037 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6038 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER')) {
6039 $this->stock_theorique -= $stock_reception_fournisseur;
6040 } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL')) {
6041 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6042 }
6043
6044 $parameters = array('id' => $this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual);
6045 // Note that $action and $object may have been modified by some hooks
6046 $reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action);
6047 if ($reshook > 0) {
6048 $this->stock_theorique = $hookmanager->resArray['stock_theorique'];
6049 } elseif ($reshook == 0 && isset($hookmanager->resArray['stock_stats_hook'])) {
6050 $this->stock_theorique += $hookmanager->resArray['stock_stats_hook'];
6051 }
6052
6053 //Virtual Stock by Warehouse
6054 if (!empty($this->stock_warehouse) && getDolGlobalString('STOCK_ALLOW_VIRTUAL_STOCK_PER_WAREHOUSE')) {
6055 foreach ($this->stock_warehouse as $warehouseid => $stockwarehouse) {
6056 if (isModEnabled('mrp')) {
6057 $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock, $warehouseid);
6058 if ($result < 0) {
6059 dol_print_error($this->db, $this->error);
6060 }
6061 }
6062
6063 if ($this->fk_default_warehouse == $warehouseid) {
6064 $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']);
6065 } else {
6066 $this->stock_warehouse[$warehouseid]->virtual = $this->stock_warehouse[$warehouseid]->real + $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'];
6067 }
6068 }
6069 }
6070
6071 return 1;
6072 }
6073
6074
6082 public function loadBatchInfo($batch)
6083 {
6084 $result = array();
6085
6086 $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";
6087 $sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = ".((int) $this->id)." AND pb.batch = '".$this->db->escape($batch)."'";
6088 $sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby";
6089 dol_syslog(get_class($this)."::loadBatchInfo load first entry found for lot/serial = ".$batch, LOG_DEBUG);
6090 $resql = $this->db->query($sql);
6091 if ($resql) {
6092 $num = $this->db->num_rows($resql);
6093 $i = 0;
6094 while ($i < $num) {
6095 $obj = $this->db->fetch_object($resql);
6096 $result[] = array('batch' => $batch, 'eatby' => $this->db->jdate($obj->eatby), 'sellby' => $this->db->jdate($obj->sellby), 'qty' => $obj->qty);
6097 $i++;
6098 }
6099 return $result;
6100 } else {
6101 dol_print_error($this->db);
6102 $this->db->rollback();
6103 return array();
6104 }
6105 }
6106
6107 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6115 public function add_photo($sdir, $file)
6116 {
6117 // phpcs:enable
6118 global $conf;
6119
6120 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6121
6122 $result = 0;
6123
6124 $dir = $sdir;
6125 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6126 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos";
6127 } else {
6128 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product').dol_sanitizeFileName($this->ref);
6129 }
6130
6131 dol_mkdir($dir);
6132
6133 $dir_osencoded = $dir;
6134
6135 if (is_dir($dir_osencoded)) {
6136 $originImage = $dir.'/'.$file['name'];
6137
6138 // Cree fichier en taille origine
6139 $result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
6140
6141 if (file_exists(dol_osencode($originImage))) {
6142 // Create thumbs
6143 $this->addThumbs($originImage);
6144 }
6145 }
6146
6147 if (is_numeric($result) && $result > 0) {
6148 return 1;
6149 } else {
6150 return -1;
6151 }
6152 }
6153
6154 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6161 public function is_photo_available($sdir)
6162 {
6163 // phpcs:enable
6164 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6165 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6166
6167 global $conf;
6168
6169 $dir = $sdir;
6170 if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6171 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos/";
6172 } else {
6173 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product');
6174 }
6175
6176 $nbphoto = 0;
6177
6178 $dir_osencoded = dol_osencode($dir);
6179 if (file_exists($dir_osencoded)) {
6180 $handle = opendir($dir_osencoded);
6181 if (is_resource($handle)) {
6182 while (($file = readdir($handle)) !== false) {
6183 if (!utf8_check($file)) {
6184 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // To be sure data is stored in UTF8 in memory
6185 }
6186 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6187 return true;
6188 }
6189 }
6190 }
6191 }
6192 return false;
6193 }
6194
6195 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6203 public function liste_photos($dir, $nbmax = 0)
6204 {
6205 // phpcs:enable
6206 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6207 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6208
6209 $nbphoto = 0;
6210 $tabobj = array();
6211
6212 $dir_osencoded = dol_osencode($dir);
6213 $handle = @opendir($dir_osencoded);
6214 if (is_resource($handle)) {
6215 while (($file = readdir($handle)) !== false) {
6216 if (!utf8_check($file)) {
6217 $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // readdir returns ISO
6218 }
6219 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6220 $nbphoto++;
6221
6222 // We forge name of thumb.
6223 $photo = $file;
6224 $photo_vignette = '';
6225 $regs = array();
6226 if (preg_match('/('.$this->regeximgext.')$/i', $photo, $regs)) {
6227 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $photo).'_small'.$regs[0];
6228 }
6229
6230 $dirthumb = $dir.'thumbs/';
6231
6232 // Object
6233 $obj = array();
6234 $obj['photo'] = $photo;
6235 if ($photo_vignette && dol_is_file($dirthumb.$photo_vignette)) {
6236 $obj['photo_vignette'] = 'thumbs/'.$photo_vignette;
6237 } else {
6238 $obj['photo_vignette'] = "";
6239 }
6240
6241 $tabobj[$nbphoto - 1] = $obj;
6242
6243 // Do we have to continue with next photo ?
6244 if ($nbmax && $nbphoto >= $nbmax) {
6245 break;
6246 }
6247 }
6248 }
6249
6250 closedir($handle);
6251 }
6252
6253 return $tabobj;
6254 }
6255
6256 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6263 public function delete_photo($file)
6264 {
6265 // phpcs:enable
6266 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6267 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6268
6269 $dir = dirname($file).'/'; // Chemin du dossier contenant l'image d'origine
6270 $dirthumb = $dir.'/thumbs/'; // Chemin du dossier contenant la vignette
6271 $filename = preg_replace('/'.preg_quote($dir, '/').'/i', '', $file); // Nom du fichier
6272
6273 // On efface l'image d'origine
6274 dol_delete_file($file, 0, 0, 0, $this); // For triggers
6275
6276 // Si elle existe, on efface la vignette
6277 if (preg_match('/('.$this->regeximgext.')$/i', $filename, $regs)) {
6278 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_small'.$regs[0];
6279 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6280 dol_delete_file($dirthumb.$photo_vignette);
6281 }
6282
6283 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_mini'.$regs[0];
6284 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6285 dol_delete_file($dirthumb.$photo_vignette);
6286 }
6287 }
6288 }
6289
6290 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6297 public function get_image_size($file)
6298 {
6299 // phpcs:enable
6300 $file_osencoded = dol_osencode($file);
6301 $infoImg = getimagesize($file_osencoded); // Get information on image
6302 $this->imgWidth = $infoImg[0]; // Largeur de l'image
6303 $this->imgHeight = $infoImg[1]; // Hauteur de l'image
6304 }
6305
6311 public function loadStateBoard()
6312 {
6313 global $hookmanager;
6314
6315 $this->nb = array();
6316
6317 $sql = "SELECT count(p.rowid) as nb, fk_product_type";
6318 $sql .= " FROM ".$this->db->prefix()."product as p";
6319 $sql .= ' WHERE p.entity IN ('.getEntity($this->element, 1).')';
6320 // Add where from hooks
6321 if (is_object($hookmanager)) {
6322 $parameters = array();
6323 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
6324 $sql .= $hookmanager->resPrint;
6325 }
6326 $sql .= ' GROUP BY fk_product_type';
6327
6328 $resql = $this->db->query($sql);
6329 if ($resql) {
6330 while ($obj = $this->db->fetch_object($resql)) {
6331 if ($obj->fk_product_type == 1) {
6332 $this->nb["services"] = $obj->nb;
6333 } else {
6334 $this->nb["products"] = $obj->nb;
6335 }
6336 }
6337 $this->db->free($resql);
6338 return 1;
6339 } else {
6340 dol_print_error($this->db);
6341 $this->error = $this->db->error();
6342 return -1;
6343 }
6344 }
6345
6351 public function isProduct()
6352 {
6353 return $this->type == Product::TYPE_PRODUCT;
6354 }
6355
6361 public function isService()
6362 {
6363 return $this->type == Product::TYPE_SERVICE;
6364 }
6365
6371 public function isStockManaged()
6372 {
6373 return ($this->isProduct() || getDolGlobalString('STOCK_SUPPORTS_SERVICES'));
6374 }
6375
6381 public function isMandatoryPeriod()
6382 {
6383 return $this->mandatory_period == 1;
6384 }
6385
6391 public function hasbatch()
6392 {
6393 return $this->status_batch > 0;
6394 }
6395
6396
6397 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6406 public function get_barcode($object, $type = '')
6407 {
6408 // phpcs:enable
6409 global $conf;
6410
6411 $result = '';
6412 if (getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
6413 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
6414 foreach ($dirsociete as $dirroot) {
6415 $res = dol_include_once($dirroot . getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM').'.php');
6416 if ($res) {
6417 break;
6418 }
6419 }
6420 $var = getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM');
6421 $mod = new $var();
6422 '@phan-var-force ModeleNumRefBarCode $module';
6423
6424 $result = $mod->getNextValue($object, $type);
6425
6426 dol_syslog(get_class($this)."::get_barcode barcode=".$result." module=".$var);
6427 }
6428 return $result;
6429 }
6430
6438 public function initAsSpecimen()
6439 {
6440 $now = dol_now();
6441
6442 // Initialize parameters
6443 $this->specimen = 1;
6444 $this->id = 0;
6445 $this->ref = 'PRODUCT_SPEC';
6446 $this->label = 'PRODUCT SPECIMEN';
6447 $this->description = 'This is description of this product specimen that was created the '.dol_print_date($now, 'dayhourlog').'.';
6448 $this->specimen = 1;
6449 $this->country_id = 1;
6450 $this->status = 1;
6451 $this->status_buy = 1;
6452 $this->tobatch = 0;
6453 $this->sell_or_eat_by_mandatory = 0;
6454 $this->note_private = 'This is a comment (private)';
6455 $this->note_public = 'This is a comment (public)';
6456 $this->date_creation = $now;
6457 $this->date_modification = $now;
6458
6459 $this->weight = 4;
6460 $this->weight_units = 3;
6461
6462 $this->length = 5;
6463 $this->length_units = 1;
6464 $this->width = 6;
6465 $this->width_units = 0;
6466 $this->height = null;
6467 $this->height_units = null;
6468
6469 $this->surface = 30;
6470 $this->surface_units = 0;
6471 $this->volume = 300;
6472 $this->volume_units = 0;
6473
6474 $this->barcode = -1; // Create barcode automatically
6475
6476 return 1;
6477 }
6478
6485 public function getLabelOfUnit($type = 'long')
6486 {
6487 global $langs;
6488
6489 if (!$this->fk_unit) {
6490 return '';
6491 }
6492
6493 $langs->load('products');
6494 $label = '';
6495 $label_type = 'label';
6496 if ($type == 'short') {
6497 $label_type = 'short_label';
6498 }
6499
6500 $sql = "SELECT ".$label_type.", code from ".$this->db->prefix()."c_units where rowid = ".((int) $this->fk_unit);
6501
6502 $resql = $this->db->query($sql);
6503 if (!$resql) {
6504 $this->error = $this->db->error();
6505 dol_syslog(get_class($this)."::getLabelOfUnit Error ".$this->error, LOG_ERR);
6506 return -1;
6507 } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
6508 $label = ($label_type == 'short_label' ? $res[$label_type] : 'unit'.$res['code']);
6509 }
6510 $this->db->free($resql);
6511
6512 return $label;
6513 }
6514
6515 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6521 public function min_recommended_price()
6522 {
6523 // phpcs:enable
6524 global $conf;
6525
6526 $maxpricesupplier = 0;
6527
6528 if (getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE')) {
6529 include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
6530 $product_fourn = new ProductFournisseur($this->db);
6531 $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', '');
6532
6533 if (is_array($product_fourn_list) && count($product_fourn_list) > 0) {
6534 foreach ($product_fourn_list as $productfourn) {
6535 if ($productfourn->fourn_unitprice > $maxpricesupplier) {
6536 $maxpricesupplier = $productfourn->fourn_unitprice;
6537 }
6538 }
6539
6540 $maxpricesupplier *= getDolGlobalString('PRODUCT_MINIMUM_RECOMMENDED_PRICE');
6541 }
6542 }
6543
6544 return $maxpricesupplier;
6545 }
6546
6547
6558 public function setCategories($categories)
6559 {
6560 require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
6561 return parent::setCategoriesCommon($categories, Categorie::TYPE_PRODUCT);
6562 }
6563
6572 public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
6573 {
6574 $tables = array(
6575 'product_customer_price',
6576 'product_customer_price_log'
6577 );
6578
6579 return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
6580 }
6581
6593 public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
6594 {
6595 global $conf;
6596
6597 $sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM ".$this->db->prefix()."product_pricerules";
6598 $query = $this->db->query($sql);
6599
6600 $rules = array();
6601
6602 while ($result = $this->db->fetch_object($query)) {
6603 $rules[$result->level] = $result;
6604 }
6605
6606 //Because prices can be based on other level's prices, we temporarily store them
6607 $prices = array(
6608 1 => $baseprice
6609 );
6610
6611 $nbofproducts = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
6612 for ($i = 1; $i <= $nbofproducts; $i++) {
6613 $price = $baseprice;
6614 $price_min = $baseprice;
6615
6616 //We have to make sure it does exist and it is > 0
6617 //First price level only allows changing min_price
6618 if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) {
6619 $price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent / 100));
6620 }
6621
6622 $prices[$i] = $price;
6623
6624 //We have to make sure it does exist and it is > 0
6625 if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) {
6626 $price_min = $price * (1 - ($rules[$i]->var_min_percent / 100));
6627 }
6628
6629 //Little check to make sure the price is modified before triggering generation
6630 $check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i]));
6631 $check_type = ($baseprice == $this->multiprices_base_type[$i]);
6632
6633 if ($check_amount && $check_type) {
6634 continue;
6635 }
6636
6637 if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, 1) < 0) {
6638 return -1;
6639 }
6640 }
6641
6642 return 1;
6643 }
6644
6650 public function getRights()
6651 {
6652 global $user;
6653
6654 if ($this->isProduct()) {
6655 return $user->rights->produit;
6656 } else {
6657 return $user->rights->service;
6658 }
6659 }
6660
6667 public function info($id)
6668 {
6669 $sql = "SELECT p.rowid, p.ref, p.datec as date_creation, p.tms as date_modification,";
6670 $sql .= " p.fk_user_author, p.fk_user_modif";
6671 $sql .= " FROM ".$this->db->prefix().$this->table_element." as p";
6672 $sql .= " WHERE p.rowid = ".((int) $id);
6673
6674 $result = $this->db->query($sql);
6675 if ($result) {
6676 if ($this->db->num_rows($result)) {
6677 $obj = $this->db->fetch_object($result);
6678
6679 $this->id = $obj->rowid;
6680 $this->ref = $obj->ref;
6681
6682 $this->user_creation_id = $obj->fk_user_author;
6683 $this->user_modification_id = $obj->fk_user_modif;
6684
6685 $this->date_creation = $this->db->jdate($obj->date_creation);
6686 $this->date_modification = $this->db->jdate($obj->date_modification);
6687 }
6688
6689 $this->db->free($result);
6690 } else {
6691 dol_print_error($this->db);
6692 }
6693 }
6694
6695
6701 public function getProductDurationHours()
6702 {
6703 global $langs;
6704
6705 if (empty($this->duration_value)) {
6706 $this->errors[] = 'ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
6707 return -1;
6708 }
6709
6710 if ($this->duration_unit == 'i') {
6711 $prodDurationHours = 1. / 60;
6712 }
6713 if ($this->duration_unit == 'h') {
6714 $prodDurationHours = 1.;
6715 }
6716 if ($this->duration_unit == 'd') {
6717 $prodDurationHours = 24.;
6718 }
6719 if ($this->duration_unit == 'w') {
6720 $prodDurationHours = 24. * 7;
6721 }
6722 if ($this->duration_unit == 'm') {
6723 $prodDurationHours = 24. * 30;
6724 }
6725 if ($this->duration_unit == 'y') {
6726 $prodDurationHours = 24. * 365;
6727 }
6728 $prodDurationHours *= $this->duration_value;
6729
6730 return $prodDurationHours;
6731 }
6732
6733
6741 public function getKanbanView($option = '', $arraydata = null)
6742 {
6743 global $langs,$conf;
6744
6745 $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
6746
6747 $return = '<div class="box-flex-item box-flex-grow-zero">';
6748 $return .= '<div class="info-box info-box-sm">';
6749 $return .= '<div class="info-box-img">';
6750 $label = '';
6751 if ($this->is_photo_available($conf->product->multidir_output[$this->entity])) {
6752 $label .= $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 120, 160, 0, 0, 0, '', 'photoref photokanban');
6753 $return .= $label;
6754 } else {
6755 if ($this->isProduct()) {
6756 $label .= img_picto('', 'product');
6757 } elseif ($this->isService()) {
6758 $label .= img_picto('', 'service');
6759 }
6760 $return .= $label;
6761 }
6762 $return .= '</div>';
6763 $return .= '<div class="info-box-content">';
6764 $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">'.(method_exists($this, 'getNomUrl') ? $this->getNomUrl() : $this->ref).'</span>';
6765 if ($selected >= 0) {
6766 $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
6767 }
6768 if (property_exists($this, 'label')) {
6769 $return .= '<br><span class="info-box-label opacitymedium inline-block tdoverflowmax150 valignmiddle" title="'.dol_escape_htmltag($this->label).'">'.dol_escape_htmltag($this->label).'</span>';
6770 }
6771 if (property_exists($this, 'price') && property_exists($this, 'price_ttc')) {
6772 if ($this->price_base_type == 'TTC') {
6773 $return .= '<br><span class="info-box-status amount">'.price($this->price_ttc).' '.$langs->trans("TTC").'</span>';
6774 } else {
6775 if ($this->status) {
6776 $return .= '<br><span class="info-box-status amount">'.price($this->price).' '.$langs->trans("HT").'</span>';
6777 }
6778 }
6779 }
6780 $br = 1;
6781 if (property_exists($this, 'stock_reel') && $this->isProduct()) {
6782 $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>';
6783 $br = 0;
6784 }
6785 if (method_exists($this, 'getLibStatut')) {
6786 if ($br) {
6787 $return .= '<br><div class="info-box-status inline-block valignmiddle">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
6788 } else {
6789 $return .= '<div class="info-box-status inline-block valignmiddle marginleftonly paddingleft">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</div>';
6790 }
6791 }
6792 $return .= '</div>';
6793 $return .= '</div>';
6794 $return .= '</div>';
6795 return $return;
6796 }
6797}
6798
6804{
6805 public $picto = 'service';
6806}
if( $user->socid > 0) if(! $user->hasRight('accounting', 'chartofaccount')) $object
Definition card.php:58
length_accountg($account)
Return General accounting account with defined length (used for product and miscellaneous)
print $langs trans("AuditedSecurityEvents").'</strong >< span class="opacitymedium"></span >< br > status
Or an array listing all the potential status of the object: array: int of the status => translated la...
Definition security.php:626
$object ref
Definition info.php:79
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.
addThumbs($file)
Build thumb.
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.
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.
$price_by_qty
Price by quantity arrays.
is_sousproduit($fk_parent, $fk_child)
Check if it is a sub-product into a kit.
const SELL_OR_EAT_BY_MANDATORY_ID_NONE
Const sell or eat by mandatory id.
isStockManaged()
Return if the object is managed in stock.
$duration
Service expiration label (value + unit)
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.
getLabelOfUnit($type='long')
Returns the text label from units dictionary.
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)
Charge tableau des stats facture recurrentes pour le produit/service.
get_nb_propalsupplier($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units in proposals in which product is included.
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)
Charge tableau des stats facture pour le produit/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.
$pmp
Average price value for product entry into stock (PMP)
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)
$default_vat_code
Default VAT code for product (link to code into llx_c_tva but without foreign keys)
$duration_unit
Service expiration unit.
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.
$stock_warehouse
Contains detail of stock of product into each warehouse.
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 sell or eat by mandatory list.
$multiprices
Arrays for multiprices.
$localtax1_tx
Other local taxes.
getChildsArbo($id, $firstlevelonly=0, $level=1, $parents=array())
Return children of product $id.
load_virtual_stock($includedraftpoforvirtual=null, $dateofvirtualstock=null)
Load value ->stock_theorique of a product.
load_stats_propale($socid=0)
Charge tableau des stats propale pour le produit/service.
get_barcode($object, $type='')
Get a barcode from the module to generate barcode values.
setAccountancyCode($type, $value)
Sets an accountancy code for a product.
load_stats_facture($socid=0)
Charge tableau des stats facture pour le produit/service.
$remise_percent
Default discount percent.
$imgWidth
Size of image.
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.
$tva_tx
Default VAT rate of product.
load_stats_bom($socid=0)
Charge tableau des stats OF pour le produit/service.
hasbatch()
Return if the object has a sell-by or eat-by date.
$weight
Metric of products.
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 clicable 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.
$duration_value
Service expiration.
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 sell or eat by mandatory label.
$desiredstock
Ask for replenishment when $desiredstock < $stock_reel.
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.
min_recommended_price()
Return minimum product recommended price.
_log_price($user, $level=0)
Insert a track that we changed a customer price.
_get_stats($sql, $mode, $year=0)
Return an array formatted for showing graphs.
$multilangs
Array for multilangs.
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.
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_dir_recursive($dir, $count=0, $nophperrors=0, $onlysub=0, &$countdeleted=0, $indexdatabase=1, $nolog=0)
Remove a directory $dir and its subdirectories (or only files and subdirectories)
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_move_uploaded_file($src_file, $dest_file, $allowoverwrite, $disablevirusscan=0, $uploaderrorcode=0, $nohook=0, $varfiles='addedfile', $upload_dir='')
Check validity of a file upload from an GUI page, and move it to its final destination.
dol_is_file($pathoffile)
Return if path is a file.
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_object($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0)
Show a picto called object_picto (generic function)
img_picto($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $alt='', $morecss='', $marginleftonlyshort=2)
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.
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.
dol_clone($object, $native=0)
Create a clone of instance of object (new instance with same value for each properties) With native =...
get_default_npr(Societe $thirdparty_seller, Societe $thirdparty_buyer, $idprod=0, $idprodfournprice=0)
Function that returns whether VAT must be recoverable collected VAT (e.g.: VAT NPR in France)
dol_sanitizeFileName($str, $newstr='_', $unaccent=1)
Clean a string to use it as a file name.
dol_print_error($db=null, $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
dol_trunc($string, $size=40, $trunc='right', $stringencoding='UTF-8', $nodot=0, $display=0)
Truncate a string to a particular length adding '…' if string larger than length.
getDolGlobalString($key, $default='')
Return 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.
measuringUnitString($unit, $measuring_style='', $scale='', $use_short_label=0, $outputlangs=null)
Return translation label of a unit key.
measuring_units_squared($unit)
Transform a given unit scale into the square of that unit, if known.
measuring_units_cubed($unit)
Transform a given unit scale into the cube of that unit, if known.
if(preg_match('/crypted:/i', $dolibarr_main_db_pass)||!empty($dolibarr_main_db_encrypted_pass)) $conf db type
Definition repair.php:139