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 
40 require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
41 require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
42 require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
43 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/productlot.class.php';
44 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php';
45 
49 class 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 
96  public $picto = 'product';
97 
101  protected $table_ref_field = 'ref';
102 
103  public $regeximgext = '\.gif|\.jpg|\.jpeg|\.png|\.bmp|\.webp|\.xpm|\.xbm'; // See also into images.lib.php
104 
109  public $libelle;
110 
116  public $label;
117 
123  public $description;
124 
130  public $other;
131 
137  public $type = self::TYPE_PRODUCT;
138 
144  public $price;
145 
146  public $price_formated; // used by takepos/ajax/ajax.php
147 
153  public $price_ttc;
154 
155  public $price_ttc_formated; // used by takepos/ajax/ajax.php
156 
162  public $price_min;
163 
169  public $price_min_ttc;
170 
175  public $price_base_type;
176  public $price_label;
177 
179  public $multiprices = array();
180  public $multiprices_ttc = array();
181  public $multiprices_base_type = array();
182  public $multiprices_default_vat_code = array();
183  public $multiprices_min = array();
184  public $multiprices_min_ttc = array();
185  public $multiprices_tva_tx = array();
186  public $multiprices_recuperableonly = array();
187 
190  public $prices_by_qty = array();
191  public $prices_by_qty_id = array();
192  public $prices_by_qty_list = array();
193 
197  public $level;
198 
200  public $multilangs = array();
201 
204 
206  public $tva_tx;
207 
211  public $tva_npr = 0;
212 
215 
218  public $localtax2_tx;
219  public $localtax1_type;
220  public $localtax2_type;
221 
222  // Properties set by get_buyprice() for return
223 
224  public $desc_supplier;
225  public $vatrate_supplier;
226  public $default_vat_code_supplier;
227  public $fourn_multicurrency_price;
228  public $fourn_multicurrency_unitprice;
229  public $fourn_multicurrency_tx;
230  public $fourn_multicurrency_id;
231  public $fourn_multicurrency_code;
232  public $packaging;
233 
234 
241  public $lifetime;
242 
249  public $qc_frequency;
250 
256  public $stock_reel = 0;
257 
263  public $stock_theorique;
264 
270  public $cost_price;
271 
273  public $pmp;
274 
280  public $seuil_stock_alerte = 0;
281 
285  public $desiredstock = 0;
286 
298  public $duration;
299 
303  public $fk_default_workstation;
304 
310  public $status = 0;
311 
318  public $tosell;
319 
325  public $status_buy = 0;
326 
333  public $tobuy;
334 
340  public $finished;
341 
347  public $fk_default_bom;
348 
354  public $product_fourn_price_id;
355 
361  public $buyprice;
362 
368  public $tobatch;
369 
370 
376  public $status_batch = 0;
377 
383  public $sell_or_eat_by_mandatory = 0;
384 
390  public $batch_mask = '';
391 
397  public $customcode;
398 
404  public $url;
405 
407  public $weight;
408  public $weight_units; // scale -3, 0, 3, 6
409  public $length;
410  public $length_units; // scale -3, 0, 3, 6
411  public $width;
412  public $width_units; // scale -3, 0, 3, 6
413  public $height;
414  public $height_units; // scale -3, 0, 3, 6
415  public $surface;
416  public $surface_units; // scale -3, 0, 3, 6
417  public $volume;
418  public $volume_units; // scale -3, 0, 3, 6
419 
420  public $net_measure;
421  public $net_measure_units; // scale -3, 0, 3, 6
422 
423  public $accountancy_code_sell;
424  public $accountancy_code_sell_intra;
425  public $accountancy_code_sell_export;
426  public $accountancy_code_buy;
427  public $accountancy_code_buy_intra;
428  public $accountancy_code_buy_export;
429 
433  public $barcode;
434 
438  public $barcode_type;
439 
443  public $barcode_type_code;
444 
445  public $stats_propale = array();
446  public $stats_commande = array();
447  public $stats_contrat = array();
448  public $stats_facture = array();
449  public $stats_proposal_supplier = array();
450  public $stats_commande_fournisseur = array();
451  public $stats_expedition = array();
452  public $stats_reception = array();
453  public $stats_mo = array();
454  public $stats_bom = array();
455  public $stats_mrptoconsume = array();
456  public $stats_mrptoproduce = array();
457  public $stats_facturerec = array();
458  public $stats_facture_fournisseur = array();
459 
461  public $imgWidth;
462  public $imgHeight;
463 
466 
469 
470  public $nbphoto = 0;
471 
473  public $stock_warehouse = array();
474 
478  public $fk_default_warehouse;
482  public $fk_price_expression;
483 
484  /* To store supplier price found */
485  public $fourn_qty;
486  public $fourn_pu;
487  public $fourn_price_base_type;
488 
492  public $fourn_socid;
493 
498  public $ref_fourn;
499 
503  public $ref_supplier;
504 
510  public $fk_unit;
511 
517  public $price_autogen = 0;
518 
524  public $supplierprices;
525 
531  public $sousprods;
532 
536  public $res;
537 
538 
544  public $is_object_used;
545 
555  public $is_sousproduit_qty;
556 
567  public $is_sousproduit_incdec;
568 
569  public $mandatory_period;
570 
571 
600  public $fields = array(
601  'rowid' => array('type' => 'integer', 'label' => 'TechnicalID', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'index' => 1, 'position' => 1, 'comment' => 'Id'),
602  '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'),
603  'entity' => array('type' => 'integer', 'label' => 'Entity', 'enabled' => 1, 'visible' => 0, 'default' => '1', 'notnull' => 1, 'index' => 1, 'position' => 5),
604  'label' => array('type' => 'varchar(255)', 'label' => 'Label', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'showoncombobox' => 2, 'position' => 15, 'csslist' => 'tdoverflowmax250'),
605  'barcode' => array('type' => 'varchar(255)', 'label' => 'Barcode', 'enabled' => 'isModEnabled("barcode")', 'position' => 20, 'visible' => -1, 'showoncombobox' => 3, 'cssview' => 'tdwordbreak', 'csslist' => 'tdoverflowmax125'),
606  'fk_barcode_type' => array('type' => 'integer', 'label' => 'BarcodeType', 'enabled' => 1, 'position' => 21, 'notnull' => 0, 'visible' => -1,),
607  'note_public' => array('type' => 'html', 'label' => 'NotePublic', 'enabled' => 1, 'visible' => 0, 'position' => 61),
608  'note' => array('type' => 'html', 'label' => 'NotePrivate', 'enabled' => 1, 'visible' => 0, 'position' => 62),
609  'datec' => array('type' => 'datetime', 'label' => 'DateCreation', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 500),
610  'tms' => array('type' => 'timestamp', 'label' => 'DateModification', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 501),
611  //'date_valid' =>array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-2, 'position'=>502),
612  'fk_user_author' => array('type' => 'integer', 'label' => 'UserAuthor', 'enabled' => 1, 'visible' => -2, 'notnull' => 1, 'position' => 510, 'foreignkey' => 'llx_user.rowid'),
613  'fk_user_modif' => array('type' => 'integer', 'label' => 'UserModif', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'position' => 511),
614  //'fk_user_valid' =>array('type'=>'integer', 'label'=>'UserValidation', 'enabled'=>1, 'visible'=>-1, 'position'=>512),
615  'localtax1_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax1tx', 'enabled' => 1, 'position' => 150, 'notnull' => 0, 'visible' => -1,),
616  'localtax1_type' => array('type' => 'varchar(10)', 'label' => 'Localtax1type', 'enabled' => 1, 'position' => 155, 'notnull' => 1, 'visible' => -1,),
617  'localtax2_tx' => array('type' => 'double(6,3)', 'label' => 'Localtax2tx', 'enabled' => 1, 'position' => 160, 'notnull' => 0, 'visible' => -1,),
618  'localtax2_type' => array('type' => 'varchar(10)', 'label' => 'Localtax2type', 'enabled' => 1, 'position' => 165, 'notnull' => 1, 'visible' => -1,),
619  'last_main_doc' => array('type' => 'varchar(255)', 'label' => 'LastMainDoc', 'enabled' => 1, 'visible' => -1, 'position' => 170),
620  'import_key' => array('type' => 'varchar(14)', 'label' => 'ImportId', 'enabled' => 1, 'visible' => -2, 'notnull' => -1, 'index' => 0, 'position' => 1000),
621  //'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')),
622  //'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')),
623  'mandatory_period' => array('type' => 'integer', 'label' => 'mandatoryperiod', 'enabled' => 1, 'visible' => 1, 'notnull' => 1, 'default' => '0', 'index' => 1, 'position' => 1000),
624  );
625 
629  const TYPE_PRODUCT = 0;
633  const TYPE_SERVICE = 1;
634 
640  public function __construct($db)
641  {
642  $this->db = $db;
643 
644  $this->ismultientitymanaged = 1;
645  $this->isextrafieldmanaged = 1;
646 
647  $this->canvas = '';
648  }
649 
655  public function check()
656  {
657  if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
658  $this->ref = trim($this->ref);
659  } else {
660  $this->ref = dol_sanitizeFileName(stripslashes($this->ref));
661  }
662 
663  $err = 0;
664  if (dol_strlen(trim($this->ref)) == 0) {
665  $err++;
666  }
667 
668  if (dol_strlen(trim($this->label)) == 0) {
669  $err++;
670  }
671 
672  if ($err > 0) {
673  return 0;
674  } else {
675  return 1;
676  }
677  }
678 
686  public function create($user, $notrigger = 0)
687  {
688  global $conf, $langs;
689 
690  $error = 0;
691 
692  // Clean parameters
693  if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
694  $this->ref = trim($this->ref);
695  } else {
696  $this->ref = dol_sanitizeFileName(dol_string_nospecial(trim($this->ref)));
697  }
698  $this->label = trim($this->label);
699  $this->price_ttc = (float) price2num($this->price_ttc);
700  $this->price = (float) price2num($this->price);
701  $this->price_min_ttc = (float) price2num($this->price_min_ttc);
702  $this->price_min = (float) price2num($this->price_min);
703  $this->price_label = trim($this->price_label);
704  if (empty($this->tva_tx)) {
705  $this->tva_tx = 0;
706  }
707  if (empty($this->tva_npr)) {
708  $this->tva_npr = 0;
709  }
710  //Local taxes
711  if (empty($this->localtax1_tx)) {
712  $this->localtax1_tx = 0;
713  }
714  if (empty($this->localtax2_tx)) {
715  $this->localtax2_tx = 0;
716  }
717  if (empty($this->localtax1_type)) {
718  $this->localtax1_type = '0';
719  }
720  if (empty($this->localtax2_type)) {
721  $this->localtax2_type = '0';
722  }
723  if (empty($this->price)) {
724  $this->price = 0;
725  }
726  if (empty($this->price_min)) {
727  $this->price_min = 0;
728  }
729  // Price by quantity
730  if (empty($this->price_by_qty)) {
731  $this->price_by_qty = 0;
732  }
733 
734  if (empty($this->status)) {
735  $this->status = 0;
736  }
737  if (empty($this->status_buy)) {
738  $this->status_buy = 0;
739  }
740 
741  $price_ht = 0;
742  $price_ttc = 0;
743  $price_min_ht = 0;
744  $price_min_ttc = 0;
745 
746  //
747  if ($this->price_base_type == 'TTC' && $this->price_ttc > 0) {
748  $price_ttc = price2num($this->price_ttc, 'MU');
749  $price_ht = price2num($this->price_ttc / (1 + ($this->tva_tx / 100)), 'MU');
750  }
751 
752  //
753  if ($this->price_base_type != 'TTC' && $this->price > 0) {
754  $price_ht = price2num($this->price, 'MU');
755  $price_ttc = price2num($this->price * (1 + ($this->tva_tx / 100)), 'MU');
756  }
757 
758  //
759  if (($this->price_min_ttc > 0) && ($this->price_base_type == 'TTC')) {
760  $price_min_ttc = price2num($this->price_min_ttc, 'MU');
761  $price_min_ht = price2num($this->price_min_ttc / (1 + ($this->tva_tx / 100)), 'MU');
762  }
763 
764  //
765  if (($this->price_min > 0) && ($this->price_base_type != 'TTC')) {
766  $price_min_ht = price2num($this->price_min, 'MU');
767  $price_min_ttc = price2num($this->price_min * (1 + ($this->tva_tx / 100)), 'MU');
768  }
769 
770  $this->accountancy_code_buy = trim($this->accountancy_code_buy);
771  $this->accountancy_code_buy_intra = trim($this->accountancy_code_buy_intra);
772  $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
773  $this->accountancy_code_sell = trim($this->accountancy_code_sell);
774  $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
775  $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
776 
777  // Barcode value
778  $this->barcode = trim($this->barcode);
779  $this->mandatory_period = empty($this->mandatory_period) ? 0 : $this->mandatory_period;
780  // Check parameters
781  if (empty($this->label)) {
782  $this->error = 'ErrorMandatoryParametersNotProvided';
783  return -1;
784  }
785 
786  if (empty($this->ref) || $this->ref == 'auto') {
787  // Load object modCodeProduct
788  $module = getDolGlobalString('PRODUCT_CODEPRODUCT_ADDON', 'mod_codeproduct_leopard');
789  if ($module != 'mod_codeproduct_leopard') { // Do not load module file for leopard
790  if (substr($module, 0, 16) == 'mod_codeproduct_' && substr($module, -3) == 'php') {
791  $module = substr($module, 0, dol_strlen($module) - 4);
792  }
793  dol_include_once('/core/modules/product/'.$module.'.php');
794  $modCodeProduct = new $module();
795  '@phan-var-force ModeleProductCode $modCodeProduct';
796  if (!empty($modCodeProduct->code_auto)) {
797  $this->ref = $modCodeProduct->getNextValue($this, $this->type);
798  }
799  unset($modCodeProduct);
800  }
801 
802  if (empty($this->ref)) {
803  $this->error = 'ProductModuleNotSetupForAutoRef';
804  return -2;
805  }
806  }
807 
808  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);
809 
810  $now = dol_now();
811 
812  if (empty($this->date_creation)) {
813  $this->date_creation = $now;
814  }
815 
816  $this->db->begin();
817 
818  // For automatic creation during create action (not used by Dolibarr GUI, can be used by scripts)
819  if ($this->barcode == '-1' || $this->barcode == 'auto') {
820  $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
821  }
822 
823  // Check more parameters
824  // If error, this->errors[] is filled
825  $result = $this->verify();
826 
827  if ($result >= 0) {
828  $sql = "SELECT count(*) as nb";
829  $sql .= " FROM ".$this->db->prefix()."product";
830  $sql .= " WHERE entity IN (".getEntity('product').")";
831  $sql .= " AND ref = '".$this->db->escape($this->ref)."'";
832 
833  $result = $this->db->query($sql);
834  if ($result) {
835  $obj = $this->db->fetch_object($result);
836  if ($obj->nb == 0) {
837  // Insert new product, no previous one found
838  $sql = "INSERT INTO ".$this->db->prefix()."product (";
839  $sql .= "datec";
840  $sql .= ", entity";
841  $sql .= ", ref";
842  $sql .= ", ref_ext";
843  $sql .= ", price_min";
844  $sql .= ", price_min_ttc";
845  $sql .= ", label";
846  $sql .= ", fk_user_author";
847  $sql .= ", fk_product_type";
848  $sql .= ", price";
849  $sql .= ", price_ttc";
850  $sql .= ", price_base_type";
851  $sql .= ", price_label";
852  $sql .= ", tobuy";
853  $sql .= ", tosell";
854  if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
855  $sql .= ", accountancy_code_buy";
856  $sql .= ", accountancy_code_buy_intra";
857  $sql .= ", accountancy_code_buy_export";
858  $sql .= ", accountancy_code_sell";
859  $sql .= ", accountancy_code_sell_intra";
860  $sql .= ", accountancy_code_sell_export";
861  }
862  $sql .= ", canvas";
863  $sql .= ", finished";
864  $sql .= ", tobatch";
865  $sql .= ", sell_or_eat_by_mandatory";
866  $sql .= ", batch_mask";
867  $sql .= ", fk_unit";
868  $sql .= ", mandatory_period";
869  $sql .= ") VALUES (";
870  $sql .= "'".$this->db->idate($this->date_creation)."'";
871  $sql .= ", ".(!empty($this->entity) ? (int) $this->entity : (int) $conf->entity);
872  $sql .= ", '".$this->db->escape($this->ref)."'";
873  $sql .= ", ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
874  $sql .= ", ".price2num($price_min_ht);
875  $sql .= ", ".price2num($price_min_ttc);
876  $sql .= ", ".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null");
877  $sql .= ", ".((int) $user->id);
878  $sql .= ", ".((int) $this->type);
879  $sql .= ", ".price2num($price_ht, 'MT');
880  $sql .= ", ".price2num($price_ttc, 'MT');
881  $sql .= ", '".$this->db->escape($this->price_base_type)."'";
882  $sql .= ", ".(!empty($this->price_label) ? "'".$this->db->escape($this->price_label)."'" : "null");
883  $sql .= ", ".((int) $this->status);
884  $sql .= ", ".((int) $this->status_buy);
885  if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
886  $sql .= ", '".$this->db->escape($this->accountancy_code_buy)."'";
887  $sql .= ", '".$this->db->escape($this->accountancy_code_buy_intra)."'";
888  $sql .= ", '".$this->db->escape($this->accountancy_code_buy_export)."'";
889  $sql .= ", '".$this->db->escape($this->accountancy_code_sell)."'";
890  $sql .= ", '".$this->db->escape($this->accountancy_code_sell_intra)."'";
891  $sql .= ", '".$this->db->escape($this->accountancy_code_sell_export)."'";
892  }
893  $sql .= ", '".$this->db->escape($this->canvas)."'";
894  $sql .= ", ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? 'NULL' : (int) $this->finished);
895  $sql .= ", ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : ((int) $this->status_batch));
896  $sql .= ", ".((empty($this->sell_or_eat_by_mandatory) || $this->sell_or_eat_by_mandatory < 0) ? 0 : ((int) $this->sell_or_eat_by_mandatory));
897  $sql .= ", '".$this->db->escape($this->batch_mask)."'";
898  $sql .= ", ".($this->fk_unit > 0 ? ((int) $this->fk_unit) : 'NULL');
899  $sql .= ", '".$this->db->escape($this->mandatory_period)."'";
900  $sql .= ")";
901 
902  dol_syslog(get_class($this)."::Create", LOG_DEBUG);
903 
904  $result = $this->db->query($sql);
905  if ($result) {
906  $id = $this->db->last_insert_id($this->db->prefix()."product");
907 
908  if ($id > 0) {
909  $this->id = $id;
910  $this->price = $price_ht;
911  $this->price_ttc = $price_ttc;
912  $this->price_min = $price_min_ht;
913  $this->price_min_ttc = $price_min_ttc;
914 
915  $result = $this->_log_price($user);
916  if ($result > 0) {
917  if ($this->update($id, $user, true, 'add') <= 0) {
918  $error++;
919  }
920  } else {
921  $error++;
922  $this->error = $this->db->lasterror();
923  }
924 
925  // update accountancy for this entity
926  if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
927  $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " .((int) $this->id) . " AND entity = " . ((int) $conf->entity));
928 
929  $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
930  $sql .= " fk_product";
931  $sql .= ", entity";
932  $sql .= ", accountancy_code_buy";
933  $sql .= ", accountancy_code_buy_intra";
934  $sql .= ", accountancy_code_buy_export";
935  $sql .= ", accountancy_code_sell";
936  $sql .= ", accountancy_code_sell_intra";
937  $sql .= ", accountancy_code_sell_export";
938  $sql .= ") VALUES (";
939  $sql .= $this->id;
940  $sql .= ", " . $conf->entity;
941  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
942  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
943  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
944  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
945  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
946  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
947  $sql .= ")";
948  $result = $this->db->query($sql);
949  if (!$result) {
950  $error++;
951  $this->error = 'ErrorFailedToInsertAccountancyForEntity';
952  }
953  }
954  } else {
955  $error++;
956  $this->error = 'ErrorFailedToGetInsertedId';
957  }
958  } else {
959  $error++;
960  $this->error = $this->db->lasterror();
961  }
962  } else {
963  // Product already exists with this ref
964  $langs->load("products");
965  $error++;
966  $this->error = "ErrorProductAlreadyExists";
967  dol_syslog(get_class($this)."::Create fails, ref ".$this->ref." already exists");
968  }
969  } else {
970  $error++;
971  $this->error = $this->db->lasterror();
972  }
973 
974  if (!$error && !$notrigger) {
975  // Call trigger
976  $result = $this->call_trigger('PRODUCT_CREATE', $user);
977  if ($result < 0) {
978  $error++;
979  }
980  // End call triggers
981  }
982 
983  if (!$error) {
984  $this->db->commit();
985  return $this->id;
986  } else {
987  $this->db->rollback();
988  return -$error;
989  }
990  } else {
991  $this->db->rollback();
992  dol_syslog(get_class($this)."::Create fails verify ".implode(',', $this->errors), LOG_WARNING);
993  return -3;
994  }
995  }
996 
997 
1004  public function verify()
1005  {
1006  global $langs;
1007 
1008  $this->errors = array();
1009 
1010  $result = 0;
1011  $this->ref = trim($this->ref);
1012 
1013  if (!$this->ref) {
1014  $this->errors[] = 'ErrorBadRef';
1015  $result = -2;
1016  }
1017 
1018  $arrayofnonnegativevalue = array('weight' => 'Weight', 'width' => 'Width', 'height' => 'Height', 'length' => 'Length', 'surface' => 'Surface', 'volume' => 'Volume');
1019  foreach ($arrayofnonnegativevalue as $key => $value) {
1020  if (property_exists($this, $key) && !empty($this->$key) && ($this->$key < 0)) {
1021  $langs->loadLangs(array("main", "other"));
1022  $this->error = $langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv($value));
1023  $this->errors[] = $this->error;
1024  $result = -4;
1025  }
1026  }
1027 
1028  $rescode = $this->check_barcode($this->barcode, $this->barcode_type_code);
1029  if ($rescode) {
1030  if ($rescode == -1) {
1031  $this->errors[] = 'ErrorBadBarCodeSyntax';
1032  } elseif ($rescode == -2) {
1033  $this->errors[] = 'ErrorBarCodeRequired';
1034  } elseif ($rescode == -3) {
1035  // Note: Common usage is to have barcode unique. For variants, we should have a different barcode.
1036  $this->errors[] = 'ErrorBarCodeAlreadyUsed';
1037  }
1038 
1039  $result = -3;
1040  }
1041 
1042  return $result;
1043  }
1044 
1045  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1056  public function check_barcode($valuetotest, $typefortest)
1057  {
1058  // phpcs:enable
1059  global $conf;
1060  if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
1061  $module = strtolower(getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM'));
1062 
1063  $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
1064  foreach ($dirsociete as $dirroot) {
1065  $res = dol_include_once($dirroot.$module.'.php');
1066  if ($res) {
1067  break;
1068  }
1069  }
1070 
1071  $mod = new $module();
1072  '@phan-var-force ModeleNumRefBarCode $mod';
1073 
1074  dol_syslog(get_class($this)."::check_barcode value=".$valuetotest." type=".$typefortest." module=".$module);
1075  $result = $mod->verif($this->db, $valuetotest, $this, 0, $typefortest);
1076  return $result;
1077  } else {
1078  return 0;
1079  }
1080  }
1081 
1093  public function update($id, $user, $notrigger = 0, $action = 'update', $updatetype = false)
1094  {
1095  global $langs, $conf, $hookmanager;
1096 
1097  $error = 0;
1098 
1099  // Check parameters
1100  if (!$this->label) {
1101  $this->label = 'MISSING LABEL';
1102  }
1103 
1104  // Clean parameters
1105  if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
1106  $this->ref = trim($this->ref);
1107  } else {
1108  $this->ref = dol_string_nospecial(trim($this->ref));
1109  }
1110  $this->label = trim($this->label);
1111  $this->description = trim($this->description);
1112  $this->note_private = (isset($this->note_private) ? trim($this->note_private) : null);
1113  $this->note_public = (isset($this->note_public) ? trim($this->note_public) : null);
1114  $this->net_measure = price2num($this->net_measure);
1115  $this->net_measure_units = (empty($this->net_measure_units) ? '' : trim($this->net_measure_units));
1116  $this->weight = price2num($this->weight);
1117  $this->weight_units = (empty($this->weight_units) ? '' : trim($this->weight_units));
1118  $this->length = price2num($this->length);
1119  $this->length_units = (empty($this->length_units) ? '' : trim($this->length_units));
1120  $this->width = price2num($this->width);
1121  $this->width_units = (empty($this->width_units) ? '' : trim($this->width_units));
1122  $this->height = price2num($this->height);
1123  $this->height_units = (empty($this->height_units) ? '' : trim($this->height_units));
1124  $this->surface = price2num($this->surface);
1125  $this->surface_units = (empty($this->surface_units) ? '' : trim($this->surface_units));
1126  $this->volume = price2num($this->volume);
1127  $this->volume_units = (empty($this->volume_units) ? '' : trim($this->volume_units));
1128 
1129  // set unit not defined
1130  if (is_numeric($this->length_units)) {
1131  $this->width_units = $this->length_units; // Not used yet
1132  }
1133  if (is_numeric($this->length_units)) {
1134  $this->height_units = $this->length_units; // Not used yet
1135  }
1136 
1137  // Automated compute surface and volume if not filled
1138  if (empty($this->surface) && !empty($this->length) && !empty($this->width) && $this->length_units == $this->width_units) {
1139  $this->surface = (float) $this->length * (float) $this->width;
1140  $this->surface_units = measuring_units_squared($this->length_units);
1141  }
1142  if (empty($this->volume) && !empty($this->surface) && !empty($this->height) && $this->length_units == $this->height_units) {
1143  $this->volume = $this->surface * (float) $this->height;
1144  $this->volume_units = measuring_units_cubed($this->height_units);
1145  }
1146 
1147  if (empty($this->tva_tx)) {
1148  $this->tva_tx = 0;
1149  }
1150  if (empty($this->tva_npr)) {
1151  $this->tva_npr = 0;
1152  }
1153  if (empty($this->localtax1_tx)) {
1154  $this->localtax1_tx = 0;
1155  }
1156  if (empty($this->localtax2_tx)) {
1157  $this->localtax2_tx = 0;
1158  }
1159  if (empty($this->localtax1_type)) {
1160  $this->localtax1_type = '0';
1161  }
1162  if (empty($this->localtax2_type)) {
1163  $this->localtax2_type = '0';
1164  }
1165  if (empty($this->status)) {
1166  $this->status = 0;
1167  }
1168  if (empty($this->status_buy)) {
1169  $this->status_buy = 0;
1170  }
1171 
1172  if (empty($this->country_id)) {
1173  $this->country_id = 0;
1174  }
1175 
1176  if (empty($this->state_id)) {
1177  $this->state_id = 0;
1178  }
1179 
1180  // Barcode value
1181  $this->barcode = (empty($this->barcode) ? '' : trim($this->barcode));
1182 
1183  $this->accountancy_code_buy = trim($this->accountancy_code_buy);
1184  $this->accountancy_code_buy_intra = (!empty($this->accountancy_code_buy_intra) ? trim($this->accountancy_code_buy_intra) : '');
1185  $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
1186  $this->accountancy_code_sell = trim($this->accountancy_code_sell);
1187  $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
1188  $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
1189 
1190 
1191  $this->db->begin();
1192 
1193  $result = 0;
1194  // Check name is required and codes are ok or unique. If error, this->errors[] is filled
1195  if ($action != 'add') {
1196  $result = $this->verify(); // We don't check when update called during a create because verify was already done
1197  } else {
1198  // we can continue
1199  $result = 0;
1200  }
1201 
1202  if ($result >= 0) {
1203  // $this->oldcopy should have been set by the caller of update (here properties were already modified)
1204  if (is_null($this->oldcopy) || (is_object($this->oldcopy) && $this->oldcopy->isEmpty())) {
1205  $this->oldcopy = dol_clone($this, 1);
1206  }
1207  // Test if batch management is activated on existing product
1208  // If yes, we create missing entries into product_batch
1209  if ($this->hasbatch() && !$this->oldcopy->hasbatch()) {
1210  //$valueforundefinedlot = 'Undefined'; // In previous version, 39 and lower
1211  $valueforundefinedlot = '000000';
1212  if (getDolGlobalString('STOCK_DEFAULT_BATCH')) {
1213  $valueforundefinedlot = getDolGlobalString('STOCK_DEFAULT_BATCH');
1214  }
1215 
1216  dol_syslog("Flag batch of product id=".$this->id." is set to ON, so we will create missing records into product_batch");
1217 
1218  $this->load_stock();
1219  foreach ($this->stock_warehouse as $idW => $ObjW) { // For each warehouse where we have stocks defined for this product (for each lines in product_stock)
1220  $qty_batch = 0;
1221  foreach ($ObjW->detail_batch as $detail) { // Each lines of detail in product_batch of the current $ObjW = product_stock
1222  if ($detail->batch == $valueforundefinedlot || $detail->batch == 'Undefined') {
1223  // We discard this line, we will create it later
1224  $sqlclean = "DELETE FROM ".$this->db->prefix()."product_batch WHERE batch in('Undefined', '".$this->db->escape($valueforundefinedlot)."') AND fk_product_stock = ".((int) $ObjW->id);
1225  $result = $this->db->query($sqlclean);
1226  if (!$result) {
1227  dol_print_error($this->db);
1228  exit;
1229  }
1230  continue;
1231  }
1232 
1233  $qty_batch += $detail->qty;
1234  }
1235  // Quantities in batch details are not same as stock quantity,
1236  // so we add a default batch record to complete and get same qty in parent and child table
1237  if ($ObjW->real != $qty_batch) {
1238  $ObjBatch = new Productbatch($this->db);
1239  $ObjBatch->batch = $valueforundefinedlot;
1240  $ObjBatch->qty = ($ObjW->real - $qty_batch);
1241  $ObjBatch->fk_product_stock = $ObjW->id;
1242 
1243  if ($ObjBatch->create($user, 1) < 0) {
1244  $error++;
1245  $this->errors = $ObjBatch->errors;
1246  } else {
1247  // we also add lot record if not exist
1248  $ObjLot = new Productlot($this->db);
1249  // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1250  if ($ObjLot->fetch(0, $this->id, $valueforundefinedlot) == 0) {
1251  $ObjLot->fk_product = $this->id;
1252  $ObjLot->entity = $this->entity;
1253  $ObjLot->fk_user_creat = $user->id;
1254  $ObjLot->batch = $valueforundefinedlot;
1255  if ($ObjLot->create($user, true) < 0) {
1256  $error++;
1257  $this->errors = $ObjLot->errors;
1258  }
1259  }
1260  }
1261  }
1262  }
1263  }
1264 
1265  // For automatic creation
1266  if ($this->barcode == -1) {
1267  $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1268  }
1269 
1270  $sql = "UPDATE ".$this->db->prefix()."product";
1271  $sql .= " SET label = '".$this->db->escape($this->label)."'";
1272 
1273  if ($updatetype && ($this->isProduct() || $this->isService())) {
1274  $sql .= ", fk_product_type = ".((int) $this->type);
1275  }
1276 
1277  $sql .= ", ref = '".$this->db->escape($this->ref)."'";
1278  $sql .= ", ref_ext = ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
1279  $sql .= ", default_vat_code = ".($this->default_vat_code ? "'".$this->db->escape($this->default_vat_code)."'" : "null");
1280  $sql .= ", tva_tx = ".((float) $this->tva_tx);
1281  $sql .= ", recuperableonly = ".((int) $this->tva_npr);
1282  $sql .= ", localtax1_tx = ".((float) $this->localtax1_tx);
1283  $sql .= ", localtax2_tx = ".((float) $this->localtax2_tx);
1284  $sql .= ", localtax1_type = ".($this->localtax1_type != '' ? "'".$this->db->escape($this->localtax1_type)."'" : "'0'");
1285  $sql .= ", localtax2_type = ".($this->localtax2_type != '' ? "'".$this->db->escape($this->localtax2_type)."'" : "'0'");
1286 
1287  $sql .= ", barcode = ".(empty($this->barcode) ? "null" : "'".$this->db->escape($this->barcode)."'");
1288  $sql .= ", fk_barcode_type = ".(empty($this->barcode_type) ? "null" : $this->db->escape($this->barcode_type));
1289 
1290  $sql .= ", tosell = ".(int) $this->status;
1291  $sql .= ", tobuy = ".(int) $this->status_buy;
1292  $sql .= ", tobatch = ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : (int) $this->status_batch);
1293  $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);
1294  $sql .= ", batch_mask = '".$this->db->escape($this->batch_mask)."'";
1295 
1296  $sql .= ", finished = ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? "null" : (int) $this->finished);
1297  $sql .= ", fk_default_bom = ".((!isset($this->fk_default_bom) || $this->fk_default_bom < 0 || $this->fk_default_bom == '') ? "null" : (int) $this->fk_default_bom);
1298  $sql .= ", net_measure = ".($this->net_measure != '' ? "'".$this->db->escape($this->net_measure)."'" : 'null');
1299  $sql .= ", net_measure_units = ".($this->net_measure_units != '' ? "'".$this->db->escape($this->net_measure_units)."'" : 'null');
1300  $sql .= ", weight = ".($this->weight != '' ? "'".$this->db->escape($this->weight)."'" : 'null');
1301  $sql .= ", weight_units = ".($this->weight_units != '' ? "'".$this->db->escape($this->weight_units)."'" : 'null');
1302  $sql .= ", length = ".($this->length != '' ? "'".$this->db->escape($this->length)."'" : 'null');
1303  $sql .= ", length_units = ".($this->length_units != '' ? "'".$this->db->escape($this->length_units)."'" : 'null');
1304  $sql .= ", width= ".($this->width != '' ? "'".$this->db->escape($this->width)."'" : 'null');
1305  $sql .= ", width_units = ".($this->width_units != '' ? "'".$this->db->escape($this->width_units)."'" : 'null');
1306  $sql .= ", height = ".($this->height != '' ? "'".$this->db->escape($this->height)."'" : 'null');
1307  $sql .= ", height_units = ".($this->height_units != '' ? "'".$this->db->escape($this->height_units)."'" : 'null');
1308  $sql .= ", surface = ".($this->surface != '' ? "'".$this->db->escape($this->surface)."'" : 'null');
1309  $sql .= ", surface_units = ".($this->surface_units != '' ? "'".$this->db->escape($this->surface_units)."'" : 'null');
1310  $sql .= ", volume = ".($this->volume != '' ? "'".$this->db->escape($this->volume)."'" : 'null');
1311  $sql .= ", volume_units = ".($this->volume_units != '' ? "'".$this->db->escape($this->volume_units)."'" : 'null');
1312  $sql .= ", fk_default_warehouse = ".($this->fk_default_warehouse > 0 ? ((int) $this->fk_default_warehouse) : 'null');
1313  $sql .= ", fk_default_workstation = ".($this->fk_default_workstation > 0 ? ((int) $this->fk_default_workstation) : 'null');
1314  $sql .= ", seuil_stock_alerte = ".((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (float) $this->seuil_stock_alerte : 'null');
1315  $sql .= ", description = '".$this->db->escape($this->description)."'";
1316  $sql .= ", url = ".($this->url ? "'".$this->db->escape($this->url)."'" : 'null');
1317  $sql .= ", customcode = '".$this->db->escape($this->customcode)."'";
1318  $sql .= ", fk_country = ".($this->country_id > 0 ? (int) $this->country_id : 'null');
1319  $sql .= ", fk_state = ".($this->state_id > 0 ? (int) $this->state_id : 'null');
1320  $sql .= ", lifetime = ".($this->lifetime > 0 ? (int) $this->lifetime : 'null');
1321  $sql .= ", qc_frequency = ".($this->qc_frequency > 0 ? (int) $this->qc_frequency : 'null');
1322  $sql .= ", note = ".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : 'null');
1323  $sql .= ", note_public = ".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : 'null');
1324  $sql .= ", duration = '".$this->db->escape($this->duration_value.$this->duration_unit)."'";
1325  if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1326  $sql .= ", accountancy_code_buy = '" . $this->db->escape($this->accountancy_code_buy) . "'";
1327  $sql .= ", accountancy_code_buy_intra = '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1328  $sql .= ", accountancy_code_buy_export = '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1329  $sql .= ", accountancy_code_sell= '" . $this->db->escape($this->accountancy_code_sell) . "'";
1330  $sql .= ", accountancy_code_sell_intra= '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1331  $sql .= ", accountancy_code_sell_export= '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1332  }
1333  $sql .= ", desiredstock = ".((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (float) $this->desiredstock : "null");
1334  $sql .= ", cost_price = ".($this->cost_price != '' ? $this->db->escape($this->cost_price) : 'null');
1335  $sql .= ", fk_unit= ".(!$this->fk_unit ? 'NULL' : (int) $this->fk_unit);
1336  $sql .= ", price_autogen = ".(!$this->price_autogen ? 0 : 1);
1337  $sql .= ", fk_price_expression = ".($this->fk_price_expression != 0 ? (int) $this->fk_price_expression : 'NULL');
1338  $sql .= ", fk_user_modif = ".($user->id > 0 ? $user->id : 'NULL');
1339  $sql .= ", mandatory_period = ".($this->mandatory_period);
1340  // stock field is not here because it is a denormalized value from product_stock.
1341  $sql .= " WHERE rowid = ".((int) $id);
1342 
1343  dol_syslog(get_class($this)."::update", LOG_DEBUG);
1344 
1345  $resql = $this->db->query($sql);
1346  if ($resql) {
1347  $this->id = $id;
1348 
1349  // Multilangs
1350  if (getDolGlobalInt('MAIN_MULTILANGS')) {
1351  if ($this->setMultiLangs($user) < 0) {
1352  $this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1353  $this->db->rollback();
1354  return -2;
1355  }
1356  }
1357 
1358  $action = 'update';
1359 
1360  // update accountancy for this entity
1361  if (!$error && getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
1362  $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1363 
1364  $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1365  $sql .= " fk_product";
1366  $sql .= ", entity";
1367  $sql .= ", accountancy_code_buy";
1368  $sql .= ", accountancy_code_buy_intra";
1369  $sql .= ", accountancy_code_buy_export";
1370  $sql .= ", accountancy_code_sell";
1371  $sql .= ", accountancy_code_sell_intra";
1372  $sql .= ", accountancy_code_sell_export";
1373  $sql .= ") VALUES (";
1374  $sql .= $this->id;
1375  $sql .= ", " . $conf->entity;
1376  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1377  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1378  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1379  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1380  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1381  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1382  $sql .= ")";
1383  $result = $this->db->query($sql);
1384  if (!$result) {
1385  $error++;
1386  $this->error = 'ErrorFailedToUpdateAccountancyForEntity';
1387  }
1388  }
1389 
1390  if (!$this->hasbatch() && $this->oldcopy->hasbatch()) {
1391  // Selection of all product stock movements that contains batchs
1392  $sql = 'SELECT pb.qty, ps.fk_entrepot, pb.batch FROM '.MAIN_DB_PREFIX.'product_batch as pb';
1393  $sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'product_stock as ps ON (ps.rowid = pb.fk_product_stock)';
1394  $sql .= ' WHERE ps.fk_product = '.(int) $this->id;
1395 
1396  $resql = $this->db->query($sql);
1397  if ($resql) {
1398  $inventorycode = dol_print_date(dol_now(), '%Y%m%d%H%M%S');
1399 
1400  while ($obj = $this->db->fetch_object($resql)) {
1401  $value = $obj->qty;
1402  $fk_entrepot = $obj->fk_entrepot;
1403  $price = 0;
1404  $dlc = '';
1405  $dluo = '';
1406  $batch = $obj->batch;
1407 
1408  // To know how to revert stockMouvement (add or remove)
1409  $addOremove = $value > 0 ? 1 : 0; // 1 if remove, 0 if add
1410  $label = $langs->trans('BatchStockMouvementAddInGlobal');
1411  $res = $this->correct_stock_batch($user, $fk_entrepot, abs($value), $addOremove, $label, $price, $dlc, $dluo, $batch, $inventorycode, '', null, 0, null, true);
1412 
1413  if ($res > 0) {
1414  $label = $langs->trans('BatchStockMouvementAddInGlobal');
1415  $res = $this->correct_stock($user, $fk_entrepot, abs($value), (int) empty($addOremove), $label, $price, $inventorycode, '', null, 0);
1416  if ($res < 0) {
1417  $error++;
1418  }
1419  } else {
1420  $error++;
1421  }
1422  }
1423  }
1424  }
1425 
1426  // Actions on extra fields
1427  if (!$error) {
1428  $result = $this->insertExtraFields();
1429  if ($result < 0) {
1430  $error++;
1431  }
1432  }
1433 
1434  if (!$error && !$notrigger) {
1435  // Call trigger
1436  $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1437  if ($result < 0) {
1438  $error++;
1439  }
1440  // End call triggers
1441  }
1442 
1443  if (!$error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref)) {
1444  // We remove directory
1445  if ($conf->product->dir_output) {
1446  $olddir = $conf->product->dir_output."/".dol_sanitizeFileName($this->oldcopy->ref);
1447  $newdir = $conf->product->dir_output."/".dol_sanitizeFileName($this->ref);
1448  if (file_exists($olddir)) {
1449  //include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1450  //$res = dol_move($olddir, $newdir);
1451  // do not use dol_move with directory
1452  $res = @rename($olddir, $newdir);
1453  if (!$res) {
1454  $langs->load("errors");
1455  $this->error = $langs->trans('ErrorFailToRenameDir', $olddir, $newdir);
1456  $error++;
1457  }
1458  }
1459  }
1460  }
1461 
1462  if (!$error) {
1463  if (isModEnabled('variants')) {
1464  include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1465 
1466  $comb = new ProductCombination($this->db);
1467 
1468  foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) {
1469  $currcomb->updateProperties($this, $user);
1470  }
1471  }
1472 
1473  $this->db->commit();
1474  return 1;
1475  } else {
1476  $this->db->rollback();
1477  return -$error;
1478  }
1479  } else {
1480  if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1481  $langs->load("errors");
1482  if (empty($conf->barcode->enabled) || empty($this->barcode)) {
1483  $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductAlreadyExists", $this->ref);
1484  } else {
1485  $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductBarCodeAlreadyExists", $this->barcode);
1486  }
1487  $this->errors[] = $this->error;
1488  $this->db->rollback();
1489  return -1;
1490  } else {
1491  $this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1492  $this->errors[] = $this->error;
1493  $this->db->rollback();
1494  return -2;
1495  }
1496  }
1497  } else {
1498  $this->db->rollback();
1499  dol_syslog(get_class($this)."::Update fails verify ".implode(',', $this->errors), LOG_WARNING);
1500  return -3;
1501  }
1502  }
1503 
1511  public function delete(User $user, $notrigger = 0)
1512  {
1513  global $conf, $langs;
1514  include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1515 
1516  $error = 0;
1517 
1518  // Check parameters
1519  if (empty($this->id)) {
1520  $this->error = "Object must be fetched before calling delete";
1521  return -1;
1522  }
1523  if (($this->type == Product::TYPE_PRODUCT && !$user->hasRight('produit', 'supprimer')) || ($this->type == Product::TYPE_SERVICE && !$user->hasRight('service', 'supprimer'))) {
1524  $this->error = "ErrorForbidden";
1525  return 0;
1526  }
1527 
1528  $objectisused = $this->isObjectUsed($this->id);
1529  if (empty($objectisused)) {
1530  $this->db->begin();
1531 
1532  if (!$error && empty($notrigger)) {
1533  // Call trigger
1534  $result = $this->call_trigger('PRODUCT_DELETE', $user);
1535  if ($result < 0) {
1536  $error++;
1537  }
1538  // End call triggers
1539  }
1540 
1541  // Delete from product_batch on product delete
1542  if (!$error) {
1543  $sql = "DELETE FROM ".$this->db->prefix().'product_batch';
1544  $sql .= " WHERE fk_product_stock IN (";
1545  $sql .= "SELECT rowid FROM ".$this->db->prefix().'product_stock';
1546  $sql .= " WHERE fk_product = ".((int) $this->id).")";
1547 
1548  $result = $this->db->query($sql);
1549  if (!$result) {
1550  $error++;
1551  $this->errors[] = $this->db->lasterror();
1552  }
1553  }
1554 
1555  // Delete all child tables
1556  if (!$error) {
1557  $elements = array('product_fournisseur_price', 'product_price', 'product_lang', 'categorie_product', 'product_stock', 'product_customer_price', 'product_lot'); // product_batch is done before
1558  foreach ($elements as $table) {
1559  if (!$error) {
1560  $sql = "DELETE FROM ".$this->db->prefix().$table;
1561  $sql .= " WHERE fk_product = ".(int) $this->id;
1562 
1563  $result = $this->db->query($sql);
1564  if (!$result) {
1565  $error++;
1566  $this->errors[] = $this->db->lasterror();
1567  }
1568  }
1569  }
1570  }
1571 
1572  if (!$error) {
1573  include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1574  include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
1575 
1576  //If it is a parent product, then we remove the association with child products
1577  $prodcomb = new ProductCombination($this->db);
1578 
1579  if ($prodcomb->deleteByFkProductParent($user, $this->id) < 0) {
1580  $error++;
1581  $this->errors[] = 'Error deleting combinations';
1582  }
1583 
1584  //We also check if it is a child product
1585  if (!$error && ($prodcomb->fetchByFkProductChild($this->id) > 0) && ($prodcomb->delete($user) < 0)) {
1586  $error++;
1587  $this->errors[] = 'Error deleting child combination';
1588  }
1589  }
1590 
1591  // Delete from product_association
1592  if (!$error) {
1593  $sql = "DELETE FROM ".$this->db->prefix()."product_association";
1594  $sql .= " WHERE fk_product_pere = ".(int) $this->id." OR fk_product_fils = ".(int) $this->id;
1595 
1596  $result = $this->db->query($sql);
1597  if (!$result) {
1598  $error++;
1599  $this->errors[] = $this->db->lasterror();
1600  }
1601  }
1602 
1603  // Remove extrafields
1604  if (!$error) {
1605  $result = $this->deleteExtraFields();
1606  if ($result < 0) {
1607  $error++;
1608  dol_syslog(get_class($this)."::delete error -4 ".$this->error, LOG_ERR);
1609  }
1610  }
1611 
1612  // Delete product
1613  if (!$error) {
1614  $sqlz = "DELETE FROM ".$this->db->prefix()."product";
1615  $sqlz .= " WHERE rowid = ".(int) $this->id;
1616 
1617  $resultz = $this->db->query($sqlz);
1618  if (!$resultz) {
1619  $error++;
1620  $this->errors[] = $this->db->lasterror();
1621  }
1622  }
1623 
1624  // Delete record into ECM index and physically
1625  if (!$error) {
1626  $res = $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
1627  $res = $this->deleteEcmFiles(1); // Deleting files physically is done later with the dol_delete_dir_recursive
1628  if (!$res) {
1629  $error++;
1630  }
1631  }
1632 
1633  if (!$error) {
1634  // We remove directory
1635  $ref = dol_sanitizeFileName($this->ref);
1636  if ($conf->product->dir_output) {
1637  $dir = $conf->product->dir_output."/".$ref;
1638  if (file_exists($dir)) {
1639  $res = @dol_delete_dir_recursive($dir);
1640  if (!$res) {
1641  $this->errors[] = 'ErrorFailToDeleteDir';
1642  $error++;
1643  }
1644  }
1645  }
1646  }
1647 
1648  if (!$error) {
1649  $this->db->commit();
1650  return 1;
1651  } else {
1652  foreach ($this->errors as $errmsg) {
1653  dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
1654  $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
1655  }
1656  $this->db->rollback();
1657  return -$error;
1658  }
1659  } else {
1660  $this->error = "ErrorRecordIsUsedCantDelete";
1661  return 0;
1662  }
1663  }
1664 
1670  public static function getSellOrEatByMandatoryList()
1671  {
1672  global $langs;
1673 
1674  $sellByLabel = $langs->trans('SellByDate');
1675  $eatByLabel = $langs->trans('EatByDate');
1676  return array(
1677  self::SELL_OR_EAT_BY_MANDATORY_ID_NONE => $langs->trans('BatchSellOrEatByMandatoryNone'),
1678  self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_BY => $sellByLabel,
1679  self::SELL_OR_EAT_BY_MANDATORY_ID_EAT_BY => $eatByLabel,
1680  self::SELL_OR_EAT_BY_MANDATORY_ID_SELL_AND_EAT => $langs->trans('BatchSellOrEatByMandatoryAll', $sellByLabel, $eatByLabel),
1681  );
1682  }
1683 
1690  {
1691  $sellOrEatByMandatoryLabel = '';
1692 
1693  $sellOrEatByMandatoryList = self::getSellOrEatByMandatoryList();
1694  if (isset($sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory])) {
1695  $sellOrEatByMandatoryLabel = $sellOrEatByMandatoryList[$this->sell_or_eat_by_mandatory];
1696  }
1697 
1698  return $sellOrEatByMandatoryLabel;
1699  }
1700 
1707  public function setMultiLangs($user)
1708  {
1709  global $conf, $langs;
1710 
1711  $langs_available = $langs->get_available_languages(DOL_DOCUMENT_ROOT, 0, 2);
1712  $current_lang = $langs->getDefaultLang();
1713 
1714  foreach ($langs_available as $key => $value) {
1715  if ($key == $current_lang) {
1716  $sql = "SELECT rowid";
1717  $sql .= " FROM ".$this->db->prefix()."product_lang";
1718  $sql .= " WHERE fk_product = ".((int) $this->id);
1719  $sql .= " AND lang = '".$this->db->escape($key)."'";
1720 
1721  $result = $this->db->query($sql);
1722 
1723  if ($this->db->num_rows($result)) { // if there is already a description line for this language
1724  $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
1725  $sql2 .= " SET ";
1726  $sql2 .= " label='".$this->db->escape($this->label)."',";
1727  $sql2 .= " description='".$this->db->escape($this->description)."'";
1728  if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1729  $sql2 .= ", note='".$this->db->escape($this->other)."'";
1730  }
1731  $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
1732  } else {
1733  $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
1734  if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1735  $sql2 .= ", note";
1736  }
1737  $sql2 .= ")";
1738  $sql2 .= " VALUES(".((int) $this->id).",'".$this->db->escape($key)."','".$this->db->escape($this->label)."',";
1739  $sql2 .= " '".$this->db->escape($this->description)."'";
1740  if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1741  $sql2 .= ", '".$this->db->escape($this->other)."'";
1742  }
1743  $sql2 .= ")";
1744  }
1745  dol_syslog(get_class($this).'::setMultiLangs key = current_lang = '.$key);
1746  if (!$this->db->query($sql2)) {
1747  $this->error = $this->db->lasterror();
1748  return -1;
1749  }
1750  } elseif (isset($this->multilangs[$key])) {
1751  if (empty($this->multilangs["$key"]["label"])) {
1752  $this->errors[] = $key . ' : ' . $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Label"));
1753  return -1;
1754  }
1755 
1756  $sql = "SELECT rowid";
1757  $sql .= " FROM ".$this->db->prefix()."product_lang";
1758  $sql .= " WHERE fk_product = ".((int) $this->id);
1759  $sql .= " AND lang = '".$this->db->escape($key)."'";
1760 
1761  $result = $this->db->query($sql);
1762 
1763  if ($this->db->num_rows($result)) { // if there is already a description line for this language
1764  $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
1765  $sql2 .= " SET ";
1766  $sql2 .= " label = '".$this->db->escape($this->multilangs["$key"]["label"])."',";
1767  $sql2 .= " description = '".$this->db->escape($this->multilangs["$key"]["description"])."'";
1768  if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1769  $sql2 .= ", note = '".$this->db->escape($this->multilangs["$key"]["other"])."'";
1770  }
1771  $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
1772  } else {
1773  $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
1774  if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1775  $sql2 .= ", note";
1776  }
1777  $sql2 .= ")";
1778  $sql2 .= " VALUES(".((int) $this->id).",'".$this->db->escape($key)."','".$this->db->escape($this->multilangs["$key"]["label"])."',";
1779  $sql2 .= " '".$this->db->escape($this->multilangs["$key"]["description"])."'";
1780  if (getDolGlobalString('PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION')) {
1781  $sql2 .= ", '".$this->db->escape($this->multilangs["$key"]["other"])."'";
1782  }
1783  $sql2 .= ")";
1784  }
1785 
1786  // We do not save if main fields are empty
1787  if ($this->multilangs["$key"]["label"] || $this->multilangs["$key"]["description"]) {
1788  if (!$this->db->query($sql2)) {
1789  $this->error = $this->db->lasterror();
1790  return -1;
1791  }
1792  }
1793  } else {
1794  // language is not current language and we didn't provide a multilang description for this language
1795  }
1796  }
1797 
1798  // Call trigger
1799  $result = $this->call_trigger('PRODUCT_SET_MULTILANGS', $user);
1800  if ($result < 0) {
1801  $this->error = $this->db->lasterror();
1802  return -1;
1803  }
1804  // End call triggers
1805 
1806  return 1;
1807  }
1808 
1817  public function delMultiLangs($langtodelete, $user)
1818  {
1819  $sql = "DELETE FROM ".$this->db->prefix()."product_lang";
1820  $sql .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($langtodelete)."'";
1821 
1822  dol_syslog(get_class($this).'::delMultiLangs', LOG_DEBUG);
1823  $result = $this->db->query($sql);
1824  if ($result) {
1825  // Call trigger
1826  $result = $this->call_trigger('PRODUCT_DEL_MULTILANGS', $user);
1827  if ($result < 0) {
1828  $this->error = $this->db->lasterror();
1829  dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
1830  return -1;
1831  }
1832  // End call triggers
1833  return 1;
1834  } else {
1835  $this->error = $this->db->lasterror();
1836  dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
1837  return -1;
1838  }
1839  }
1840 
1849  public function setAccountancyCode($type, $value)
1850  {
1851  global $user, $langs, $conf;
1852 
1853  $error = 0;
1854 
1855  $this->db->begin();
1856 
1857  if ($type == 'buy') {
1858  $field = 'accountancy_code_buy';
1859  } elseif ($type == 'buy_intra') {
1860  $field = 'accountancy_code_buy_intra';
1861  } elseif ($type == 'buy_export') {
1862  $field = 'accountancy_code_buy_export';
1863  } elseif ($type == 'sell') {
1864  $field = 'accountancy_code_sell';
1865  } elseif ($type == 'sell_intra') {
1866  $field = 'accountancy_code_sell_intra';
1867  } elseif ($type == 'sell_export') {
1868  $field = 'accountancy_code_sell_export';
1869  } else {
1870  return -1;
1871  }
1872 
1873  $sql = "UPDATE ".$this->db->prefix().$this->table_element." SET ";
1874  $sql .= "$field = '".$this->db->escape($value)."'";
1875  $sql .= " WHERE rowid = ".((int) $this->id);
1876 
1877  dol_syslog(__METHOD__, LOG_DEBUG);
1878  $resql = $this->db->query($sql);
1879 
1880  if ($resql) {
1881  // Call trigger
1882  $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1883  if ($result < 0) {
1884  $error++;
1885  }
1886  // End call triggers
1887 
1888  if ($error) {
1889  $this->db->rollback();
1890  return -1;
1891  }
1892 
1893  $this->$field = $value;
1894 
1895  $this->db->commit();
1896  return 1;
1897  } else {
1898  $this->error = $this->db->lasterror();
1899  $this->db->rollback();
1900  return -1;
1901  }
1902  }
1903 
1909  public function getMultiLangs()
1910  {
1911  global $langs;
1912 
1913  $current_lang = $langs->getDefaultLang();
1914 
1915  $sql = "SELECT lang, label, description, note as other";
1916  $sql .= " FROM ".$this->db->prefix()."product_lang";
1917  $sql .= " WHERE fk_product = ".((int) $this->id);
1918 
1919  $result = $this->db->query($sql);
1920  if ($result) {
1921  while ($obj = $this->db->fetch_object($result)) {
1922  //print 'lang='.$obj->lang.' current='.$current_lang.'<br>';
1923  if ($obj->lang == $current_lang) { // si on a les traduct. dans la langue courante on les charge en infos principales.
1924  $this->label = $obj->label;
1925  $this->description = $obj->description;
1926  $this->other = $obj->other;
1927  }
1928  $this->multilangs[(string) $obj->lang]["label"] = $obj->label;
1929  $this->multilangs[(string) $obj->lang]["description"] = $obj->description;
1930  $this->multilangs[(string) $obj->lang]["other"] = $obj->other;
1931  }
1932  return 1;
1933  } else {
1934  $this->error = "Error: ".$this->db->lasterror()." - ".$sql;
1935  return -1;
1936  }
1937  }
1938 
1945  private function getArrayForPriceCompare($level = 0)
1946  {
1947  $testExit = array('multiprices','multiprices_ttc','multiprices_base_type','multiprices_min','multiprices_min_ttc','multiprices_tva_tx','multiprices_recuperableonly');
1948 
1949  foreach ($testExit as $field) {
1950  if (!isset($this->$field)) {
1951  return array();
1952  }
1953  $tmparray = $this->$field;
1954  if (!isset($tmparray[$level])) {
1955  return array();
1956  }
1957  }
1958 
1959  $lastPrice = array(
1960  'level' => $level ? $level : 1,
1961  'multiprices' => (float) $this->multiprices[$level],
1962  'multiprices_ttc' => (float) $this->multiprices_ttc[$level],
1963  'multiprices_base_type' => $this->multiprices_base_type[$level],
1964  'multiprices_min' => (float) $this->multiprices_min[$level],
1965  'multiprices_min_ttc' => (float) $this->multiprices_min_ttc[$level],
1966  'multiprices_tva_tx' => (float) $this->multiprices_tva_tx[$level],
1967  'multiprices_recuperableonly' => (float) $this->multiprices_recuperableonly[$level],
1968  );
1969 
1970  return $lastPrice;
1971  }
1972 
1973 
1974  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1982  private function _log_price($user, $level = 0)
1983  {
1984  // phpcs:enable
1985  global $conf;
1986 
1987  $now = dol_now();
1988 
1989  // Clean parameters
1990  if (empty($this->price_by_qty)) {
1991  $this->price_by_qty = 0;
1992  }
1993 
1994  // Add new price
1995  $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,";
1996  $sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, price_min,price_min_ttc,price_by_qty,entity,fk_price_expression) ";
1997  $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).",";
1998  $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');
1999  $sql .= ")";
2000 
2001  dol_syslog(get_class($this)."::_log_price", LOG_DEBUG);
2002  $resql = $this->db->query($sql);
2003  if (!$resql) {
2004  $this->error = $this->db->lasterror();
2005  dol_print_error($this->db);
2006  return -1;
2007  } else {
2008  return 1;
2009  }
2010  }
2011 
2012 
2013  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2021  public function log_price_delete($user, $rowid)
2022  {
2023  // phpcs:enable
2024  $sql = "DELETE FROM ".$this->db->prefix()."product_price_by_qty";
2025  $sql .= " WHERE fk_product_price = ".((int) $rowid);
2026  $resql = $this->db->query($sql);
2027 
2028  $sql = "DELETE FROM ".$this->db->prefix()."product_price";
2029  $sql .= " WHERE rowid=".((int) $rowid);
2030  $resql = $this->db->query($sql);
2031  if ($resql) {
2032  return 1;
2033  } else {
2034  $this->error = $this->db->lasterror();
2035  return -1;
2036  }
2037  }
2038 
2039 
2049  public function getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp = 0)
2050  {
2051  global $conf, $hookmanager, $action;
2052 
2053  // Call hook if any
2054  if (is_object($hookmanager)) {
2055  $parameters = array('thirdparty_seller' => $thirdparty_seller, 'thirdparty_buyer' => $thirdparty_buyer, 'pqp' => $pqp);
2056  // Note that $action and $object may have been modified by some hooks
2057  $reshook = $hookmanager->executeHooks('getSellPrice', $parameters, $this, $action);
2058  if ($reshook > 0) {
2059  return $hookmanager->resArray;
2060  }
2061  }
2062 
2063  // Update if prices fields are defined
2064  $tva_tx = get_default_tva($thirdparty_seller, $thirdparty_buyer, $this->id);
2065  $tva_npr = get_default_npr($thirdparty_seller, $thirdparty_buyer, $this->id);
2066  if (empty($tva_tx)) {
2067  $tva_npr = 0;
2068  }
2069 
2070  $pu_ht = $this->price;
2071  $pu_ttc = $this->price_ttc;
2072  $price_min = $this->price_min;
2073  $price_base_type = $this->price_base_type;
2074 
2075  // If price per segment
2076  if (getDolGlobalString('PRODUIT_MULTIPRICES') && !empty($thirdparty_buyer->price_level)) {
2077  $pu_ht = $this->multiprices[$thirdparty_buyer->price_level];
2078  $pu_ttc = $this->multiprices_ttc[$thirdparty_buyer->price_level];
2079  $price_min = $this->multiprices_min[$thirdparty_buyer->price_level];
2080  $price_base_type = $this->multiprices_base_type[$thirdparty_buyer->price_level];
2081  if (getDolGlobalString('PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL')) { // using this option is a bug. kept for backward compatibility
2082  if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
2083  $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
2084  }
2085  if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
2086  $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
2087  }
2088  if (empty($tva_tx)) {
2089  $tva_npr = 0;
2090  }
2091  }
2092  } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES')) {
2093  // If price per customer
2094  require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
2095 
2096  $prodcustprice = new ProductCustomerPrice($this->db);
2097 
2098  $filter = array('t.fk_product' => $this->id, 't.fk_soc' => $thirdparty_buyer->id);
2099 
2100  $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
2101  if ($result) {
2102  if (count($prodcustprice->lines) > 0) {
2103  $pu_ht = price($prodcustprice->lines[0]->price);
2104  $price_min = price($prodcustprice->lines[0]->price_min);
2105  $pu_ttc = price($prodcustprice->lines[0]->price_ttc);
2106  $price_base_type = $prodcustprice->lines[0]->price_base_type;
2107  $tva_tx = $prodcustprice->lines[0]->tva_tx;
2108  if ($prodcustprice->lines[0]->default_vat_code && !preg_match('/\‍(.*\‍)/', $tva_tx)) {
2109  $tva_tx .= ' ('.$prodcustprice->lines[0]->default_vat_code.')';
2110  }
2111  $tva_npr = $prodcustprice->lines[0]->recuperableonly;
2112  if (empty($tva_tx)) {
2113  $tva_npr = 0;
2114  }
2115  }
2116  }
2117  } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY')) {
2118  // If price per quantity
2119  if ($this->prices_by_qty[0]) {
2120  // yes, this product has some prices per quantity
2121  // Search price into product_price_by_qty from $this->id
2122  foreach ($this->prices_by_qty_list[0] as $priceforthequantityarray) {
2123  if ($priceforthequantityarray['rowid'] != $pqp) {
2124  continue;
2125  }
2126  // We found the price
2127  if ($priceforthequantityarray['price_base_type'] == 'HT') {
2128  $pu_ht = $priceforthequantityarray['unitprice'];
2129  } else {
2130  $pu_ttc = $priceforthequantityarray['unitprice'];
2131  }
2132  break;
2133  }
2134  }
2135  } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) {
2136  // If price per quantity and customer
2137  if ($this->prices_by_qty[$thirdparty_buyer->price_level]) {
2138  // yes, this product has some prices per quantity
2139  // Search price into product_price_by_qty from $this->id
2140  foreach ($this->prices_by_qty_list[$thirdparty_buyer->price_level] as $priceforthequantityarray) {
2141  if ($priceforthequantityarray['rowid'] != $pqp) {
2142  continue;
2143  }
2144  // We found the price
2145  if ($priceforthequantityarray['price_base_type'] == 'HT') {
2146  $pu_ht = $priceforthequantityarray['unitprice'];
2147  } else {
2148  $pu_ttc = $priceforthequantityarray['unitprice'];
2149  }
2150  break;
2151  }
2152  }
2153  }
2154 
2155  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);
2156  }
2157 
2158  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2172  public function get_buyprice($prodfournprice, $qty, $product_id = 0, $fourn_ref = '', $fk_soc = 0)
2173  {
2174  // phpcs:enable
2175  global $action, $hookmanager;
2176 
2177  // Call hook if any
2178  if (is_object($hookmanager)) {
2179  $parameters = array(
2180  'prodfournprice' => $prodfournprice,
2181  'qty' => $qty,
2182  'product_id' => $product_id,
2183  'fourn_ref' => $fourn_ref,
2184  'fk_soc' => $fk_soc,
2185  );
2186  // Note that $action and $object may have been modified by some hooks
2187  $reshook = $hookmanager->executeHooks('getBuyPrice', $parameters, $this, $action);
2188  if ($reshook > 0) {
2189  return $hookmanager->resArray;
2190  }
2191  }
2192 
2193  $result = 0;
2194 
2195  // 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)
2196  $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2197  $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,";
2198  $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2199  $sql .= " pfp.packaging";
2200  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2201  $sql .= " WHERE pfp.rowid = ".((int) $prodfournprice);
2202  if ($qty > 0) {
2203  $sql .= " AND pfp.quantity <= ".((float) $qty);
2204  }
2205  $sql .= " ORDER BY pfp.quantity DESC";
2206 
2207  dol_syslog(get_class($this)."::get_buyprice first search by prodfournprice/qty", LOG_DEBUG);
2208  $resql = $this->db->query($sql);
2209  if ($resql) {
2210  $obj = $this->db->fetch_object($resql);
2211  if ($obj && $obj->quantity > 0) { // If we found a supplier prices from the id of supplier price
2212  if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2213  $prod_supplier = new ProductFournisseur($this->db);
2214  $prod_supplier->product_fourn_price_id = $obj->rowid;
2215  $prod_supplier->id = $obj->fk_product;
2216  $prod_supplier->fourn_qty = $obj->quantity;
2217  $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2218  $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2219 
2220  include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2221  $priceparser = new PriceParser($this->db);
2222  $price_result = $priceparser->parseProductSupplier($prod_supplier);
2223  if ($price_result >= 0) {
2224  $obj->price = $price_result;
2225  }
2226  }
2227  $this->product_fourn_price_id = $obj->rowid;
2228  $this->buyprice = $obj->price; // deprecated
2229  $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product of supplier
2230  $this->fourn_price_base_type = 'HT'; // Price base type
2231  $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2232  $this->ref_fourn = $obj->ref_supplier; // deprecated
2233  $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2234  $this->desc_supplier = $obj->desc_supplier; // desc supplier
2235  $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2236  $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2237  $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2238  $this->fourn_multicurrency_price = $obj->multicurrency_price;
2239  $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2240  $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2241  $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2242  $this->fourn_multicurrency_code = $obj->multicurrency_code;
2243  if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2244  $this->packaging = $obj->packaging;
2245  }
2246  $result = $obj->fk_product;
2247  return $result;
2248  } else { // If not found
2249  // 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.
2250  $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2251  $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,";
2252  $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2253  $sql .= " pfp.packaging";
2254  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2255  $sql .= " WHERE 1 = 1";
2256  if ($product_id > 0) {
2257  $sql .= " AND pfp.fk_product = ".((int) $product_id);
2258  }
2259  if ($fourn_ref != 'none') {
2260  $sql .= " AND pfp.ref_fourn = '".$this->db->escape($fourn_ref)."'";
2261  }
2262  if ($fk_soc > 0) {
2263  $sql .= " AND pfp.fk_soc = ".((int) $fk_soc);
2264  }
2265  if ($qty > 0) {
2266  $sql .= " AND pfp.quantity <= ".((float) $qty);
2267  }
2268  $sql .= " ORDER BY pfp.quantity DESC";
2269  $sql .= " LIMIT 1";
2270 
2271  dol_syslog(get_class($this)."::get_buyprice second search from qty/ref/product_id", LOG_DEBUG);
2272  $resql = $this->db->query($sql);
2273  if ($resql) {
2274  $obj = $this->db->fetch_object($resql);
2275  if ($obj && $obj->quantity > 0) { // If found
2276  if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2277  $prod_supplier = new ProductFournisseur($this->db);
2278  $prod_supplier->product_fourn_price_id = $obj->rowid;
2279  $prod_supplier->id = $obj->fk_product;
2280  $prod_supplier->fourn_qty = $obj->quantity;
2281  $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2282  $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2283 
2284  include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2285  $priceparser = new PriceParser($this->db);
2286  $price_result = $priceparser->parseProductSupplier($prod_supplier);
2287  if ($result >= 0) {
2288  $obj->price = $price_result;
2289  }
2290  }
2291  $this->product_fourn_price_id = $obj->rowid;
2292  $this->buyprice = $obj->price; // deprecated
2293  $this->fourn_qty = $obj->quantity; // min quantity for price for a virtual supplier
2294  $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product for a virtual supplier
2295  $this->fourn_price_base_type = 'HT'; // Price base type for a virtual supplier
2296  $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2297  $this->ref_fourn = $obj->ref_supplier; // deprecated
2298  $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2299  $this->desc_supplier = $obj->desc_supplier; // desc supplier
2300  $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2301  $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2302  $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2303  $this->fourn_multicurrency_price = $obj->multicurrency_price;
2304  $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2305  $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2306  $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2307  $this->fourn_multicurrency_code = $obj->multicurrency_code;
2308  if (getDolGlobalString('PRODUCT_USE_SUPPLIER_PACKAGING')) {
2309  $this->packaging = $obj->packaging;
2310  }
2311  $result = $obj->fk_product;
2312  return $result;
2313  } else {
2314  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é.
2315  }
2316  } else {
2317  $this->error = $this->db->lasterror();
2318  return -3;
2319  }
2320  }
2321  } else {
2322  $this->error = $this->db->lasterror();
2323  return -2;
2324  }
2325  }
2326 
2327 
2346  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)
2347  {
2348  global $conf, $langs;
2349 
2350  $lastPriceData = $this->getArrayForPriceCompare($level); // temporary store current price before update
2351 
2352  $id = $this->id;
2353 
2354  dol_syslog(get_class($this)."::update_price id=".$id." newprice=".$newprice." newpricebase=".$newpricebase." newminprice=".$newminprice." level=".$level." npr=".$newnpr." newdefaultvatcode=".$newdefaultvatcode);
2355 
2356  // Clean parameters
2357  if (empty($this->tva_tx)) {
2358  $this->tva_tx = 0;
2359  }
2360  if (empty($newnpr)) {
2361  $newnpr = 0;
2362  }
2363  if (empty($newminprice)) {
2364  $newminprice = 0;
2365  }
2366 
2367  // Check parameters
2368  if ($newvat === null || $newvat == '') { // Maintain '' for backwards compatibility
2369  $newvat = $this->tva_tx;
2370  }
2371 
2372  // If multiprices are enabled, then we check if the current product is subject to price autogeneration
2373  // Price will be modified ONLY when the first one is the one that is being modified
2374  if ((getDolGlobalString('PRODUIT_MULTIPRICES') || getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES')) && !$ignore_autogen && $this->price_autogen && ($level == 1)) {
2375  return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpbq);
2376  }
2377 
2378  if (!empty($newminprice) && ($newminprice > $newprice)) {
2379  $this->error = 'ErrorPriceCantBeLowerThanMinPrice';
2380  return -1;
2381  }
2382 
2383  if ($newprice !== '' || $newprice === 0) {
2384  if ($newpricebase == 'TTC') {
2385  $price_ttc = price2num($newprice, 'MU');
2386  $price = (float) price2num($newprice) / (1 + ((float) $newvat / 100));
2387  $price = price2num($price, 'MU');
2388 
2389  if ($newminprice != '' || $newminprice == 0) {
2390  $price_min_ttc = price2num($newminprice, 'MU');
2391  $price_min = (float) price2num($newminprice) / (1 + ($newvat / 100));
2392  $price_min = price2num($price_min, 'MU');
2393  } else {
2394  $price_min = 0;
2395  $price_min_ttc = 0;
2396  }
2397  } else {
2398  $price = (float) price2num($newprice, 'MU');
2399  $price_ttc = ($newnpr != 1) ? price2num($newprice) * (1 + ($newvat / 100)) : $price;
2400  $price_ttc = (float) price2num($price_ttc, 'MU');
2401 
2402  if ($newminprice !== '' || $newminprice === 0) {
2403  $price_min = price2num($newminprice, 'MU');
2404  $price_min_ttc = (float) price2num($newminprice) * (1 + ($newvat / 100));
2405  $price_min_ttc = price2num($price_min_ttc, 'MU');
2406  //print 'X'.$newminprice.'-'.$price_min;
2407  } else {
2408  $price_min = 0;
2409  $price_min_ttc = 0;
2410  }
2411  }
2412  //print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc;
2413 
2414  if (count($localtaxes_array) > 0) {
2415  $localtaxtype1 = $localtaxes_array['0'];
2416  $localtax1 = $localtaxes_array['1'];
2417  $localtaxtype2 = $localtaxes_array['2'];
2418  $localtax2 = $localtaxes_array['3'];
2419  } else {
2420  // if array empty, we try to use the vat code
2421  if (!empty($newdefaultvatcode)) {
2422  global $mysoc;
2423  // Get record from code
2424  $sql = "SELECT t.rowid, t.code, t.recuperableonly as tva_npr, t.localtax1, t.localtax2, t.localtax1_type, t.localtax2_type";
2425  $sql .= " FROM ".MAIN_DB_PREFIX."c_tva as t, ".MAIN_DB_PREFIX."c_country as c";
2426  $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '".$this->db->escape($mysoc->country_code)."'";
2427  $sql .= " AND t.taux = ".((float) $newdefaultvatcode)." AND t.active = 1";
2428  $sql .= " AND t.code = '".$this->db->escape($newdefaultvatcode)."'";
2429  $resql = $this->db->query($sql);
2430  if ($resql) {
2431  $obj = $this->db->fetch_object($resql);
2432  if ($obj) {
2433  $npr = $obj->tva_npr;
2434  $localtax1 = $obj->localtax1;
2435  $localtax2 = $obj->localtax2;
2436  $localtaxtype1 = $obj->localtax1_type;
2437  $localtaxtype2 = $obj->localtax2_type;
2438  }
2439  }
2440  } else {
2441  // old method. deprecated because we can't retrieve type
2442  $localtaxtype1 = '0';
2443  $localtax1 = get_localtax($newvat, 1);
2444  $localtaxtype2 = '0';
2445  $localtax2 = get_localtax($newvat, 2);
2446  }
2447  }
2448  if (empty($localtax1)) {
2449  $localtax1 = 0; // If = '' then = 0
2450  }
2451  if (empty($localtax2)) {
2452  $localtax2 = 0; // If = '' then = 0
2453  }
2454 
2455  $this->db->begin();
2456 
2457  // Ne pas mettre de quote sur les numeriques decimaux.
2458  // Ceci provoque des stockages avec arrondis en base au lieu des valeurs exactes.
2459  $sql = "UPDATE ".$this->db->prefix()."product SET";
2460  $sql .= " price_base_type = '".$this->db->escape($newpricebase)."',";
2461  $sql .= " price = ".(float) $price.",";
2462  $sql .= " price_ttc = ".(float) $price_ttc.",";
2463  $sql .= " price_min = ".(float) $price_min.",";
2464  $sql .= " price_min_ttc = ".(float) $price_min_ttc.",";
2465  $sql .= " localtax1_tx = ".($localtax1 >= 0 ? (float) $localtax1 : 'NULL').",";
2466  $sql .= " localtax2_tx = ".($localtax2 >= 0 ? (float) $localtax2 : 'NULL').",";
2467  $sql .= " localtax1_type = ".($localtaxtype1 != '' ? "'".$this->db->escape($localtaxtype1)."'" : "'0'").",";
2468  $sql .= " localtax2_type = ".($localtaxtype2 != '' ? "'".$this->db->escape($localtaxtype2)."'" : "'0'").",";
2469  $sql .= " default_vat_code = ".($newdefaultvatcode ? "'".$this->db->escape($newdefaultvatcode)."'" : "null").",";
2470  $sql .= " price_label = ".(!empty($price_label) ? "'".$this->db->escape($price_label)."'" : "null").",";
2471  $sql .= " tva_tx = ".(float) price2num($newvat).",";
2472  $sql .= " recuperableonly = '".$this->db->escape($newnpr)."'";
2473  $sql .= " WHERE rowid = ".((int) $id);
2474 
2475  dol_syslog(get_class($this)."::update_price", LOG_DEBUG);
2476  $resql = $this->db->query($sql);
2477  if ($resql) {
2478  $this->multiprices[$level] = $price;
2479  $this->multiprices_ttc[$level] = $price_ttc;
2480  $this->multiprices_min[$level] = $price_min;
2481  $this->multiprices_min_ttc[$level] = $price_min_ttc;
2482  $this->multiprices_base_type[$level] = $newpricebase;
2483  $this->multiprices_default_vat_code[$level] = $newdefaultvatcode;
2484  $this->multiprices_tva_tx[$level] = $newvat;
2485  $this->multiprices_recuperableonly[$level] = $newnpr;
2486 
2487  $this->price = $price;
2488  $this->price_label = $price_label;
2489  $this->price_ttc = $price_ttc;
2490  $this->price_min = $price_min;
2491  $this->price_min_ttc = $price_min_ttc;
2492  $this->price_base_type = $newpricebase;
2493  $this->default_vat_code = $newdefaultvatcode;
2494  $this->tva_tx = $newvat;
2495  $this->tva_npr = $newnpr;
2496 
2497  //Local taxes
2498  $this->localtax1_tx = $localtax1;
2499  $this->localtax2_tx = $localtax2;
2500  $this->localtax1_type = $localtaxtype1;
2501  $this->localtax2_type = $localtaxtype2;
2502 
2503  // Price by quantity
2504  $this->price_by_qty = $newpbq;
2505 
2506  // check if price have really change before log
2507  $newPriceData = $this->getArrayForPriceCompare($level);
2508  if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || !getDolGlobalString('PRODUIT_MULTIPRICES')) {
2509  $this->_log_price($user, $level); // Save price for level into table product_price
2510  }
2511 
2512  $this->level = $level; // Store level of price edited for trigger
2513 
2514  // Call trigger
2515  if (!$notrigger) {
2516  $result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user);
2517  if ($result < 0) {
2518  $this->db->rollback();
2519  return -1;
2520  }
2521  }
2522  // End call triggers
2523 
2524  $this->db->commit();
2525  } else {
2526  $this->db->rollback();
2527  $this->error = $this->db->lasterror();
2528  return -1;
2529  }
2530  }
2531 
2532  return 1;
2533  }
2534 
2542  public function setPriceExpression($expression_id)
2543  {
2544  global $user;
2545 
2546  $this->fk_price_expression = $expression_id;
2547 
2548  return $this->update($this->id, $user);
2549  }
2550 
2563  public function fetch($id = 0, $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0)
2564  {
2565  include_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
2566 
2567  global $langs, $conf;
2568 
2569  dol_syslog(get_class($this)."::fetch id=".$id." ref=".$ref." ref_ext=".$ref_ext);
2570 
2571  // Check parameters
2572  if (!$id && !$ref && !$ref_ext && !$barcode) {
2573  $this->error = 'ErrorWrongParameters';
2574  dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR);
2575  return -1;
2576  }
2577 
2578  $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,";
2579  $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,";
2580  $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,";
2581  $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units, p.last_main_doc,";
2582  $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,";
2583  if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
2584  $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,";
2585  } else {
2586  $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,";
2587  }
2588 
2589  //For MultiCompany
2590  //PMP per entity & Stocks Sharings stock_reel includes only stocks shared with this entity
2591  $separatedEntityPMP = false; // Set to true to get the AWP from table llx_product_perentity instead of field 'pmp' into llx_product.
2592  $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.
2593  $visibleWarehousesEntities = $conf->entity;
2594  if (getDolGlobalString('MULTICOMPANY_PRODUCT_SHARING_ENABLED')) {
2595  if (getDolGlobalString('MULTICOMPANY_PMP_PER_ENTITY_ENABLED')) {
2596  $checkPMPPerEntity = $this->db->query("SELECT pmp FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = ".((int) $id)." AND entity = ".(int) $conf->entity);
2597  if ($this->db->num_rows($checkPMPPerEntity) > 0) {
2598  $separatedEntityPMP = true;
2599  }
2600  }
2601  global $mc;
2602  $separatedStock = true;
2603  if (isset($mc->sharings['stock']) && !empty($mc->sharings['stock'])) {
2604  $visibleWarehousesEntities .= "," . implode(",", $mc->sharings['stock']);
2605  }
2606  }
2607  if ($separatedEntityPMP) {
2608  $sql .= " ppe.pmp,";
2609  } else {
2610  $sql .= " p.pmp,";
2611  }
2612  $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,";
2613  $sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf,";
2614  $sql .= " p.price_label,";
2615  if ($separatedStock) {
2616  $sql .= " SUM(sp.reel) as stock";
2617  } else {
2618  $sql .= " p.stock";
2619  }
2620  $sql .= " FROM ".$this->db->prefix()."product as p";
2621  if (getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED') || $separatedEntityPMP) {
2622  $sql .= " LEFT JOIN " . $this->db->prefix() . "product_perentity as ppe ON ppe.fk_product = p.rowid AND ppe.entity = " . ((int) $conf->entity);
2623  }
2624  if ($separatedStock) {
2625  $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)."))";
2626  }
2627 
2628  if ($id) {
2629  $sql .= " WHERE p.rowid = ".((int) $id);
2630  } else {
2631  $sql .= " WHERE p.entity IN (".getEntity($this->element).")";
2632  if ($ref) {
2633  $sql .= " AND p.ref = '".$this->db->escape($ref)."'";
2634  } elseif ($ref_ext) {
2635  $sql .= " AND p.ref_ext = '".$this->db->escape($ref_ext)."'";
2636  } elseif ($barcode) {
2637  $sql .= " AND p.barcode = '".$this->db->escape($barcode)."'";
2638  }
2639  }
2640  if ($separatedStock) {
2641  $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,";
2642  $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,";
2643  $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,";
2644  $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
2645  $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,";
2646  if (!getDolGlobalString('MAIN_PRODUCT_PERENTITY_SHARED')) {
2647  $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,";
2648  } else {
2649  $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,";
2650  }
2651  if ($separatedEntityPMP) {
2652  $sql .= " ppe.pmp,";
2653  } else {
2654  $sql .= " p.pmp,";
2655  }
2656  $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,";
2657  $sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf";
2658  $sql .= " ,p.price_label";
2659  if (!$separatedStock) {
2660  $sql .= ", p.stock";
2661  }
2662  }
2663 
2664  $resql = $this->db->query($sql);
2665  if ($resql) {
2666  unset($this->oldcopy);
2667 
2668  if ($this->db->num_rows($resql) > 0) {
2669  $obj = $this->db->fetch_object($resql);
2670 
2671  $this->id = $obj->rowid;
2672  $this->ref = $obj->ref;
2673  $this->ref_ext = $obj->ref_ext;
2674  $this->label = $obj->label;
2675  $this->description = $obj->description;
2676  $this->url = $obj->url;
2677  $this->note_public = $obj->note_public;
2678  $this->note_private = $obj->note_private;
2679  $this->note = $obj->note_private; // deprecated
2680 
2681  $this->type = $obj->fk_product_type;
2682  $this->price_label = $obj->price_label;
2683  $this->status = $obj->tosell;
2684  $this->status_buy = $obj->tobuy;
2685  $this->status_batch = $obj->tobatch;
2686  $this->sell_or_eat_by_mandatory = $obj->sell_or_eat_by_mandatory;
2687  $this->batch_mask = $obj->batch_mask;
2688 
2689  $this->customcode = $obj->customcode;
2690  $this->country_id = $obj->fk_country;
2691  $this->country_code = getCountry($this->country_id, 2, $this->db);
2692  $this->state_id = $obj->fk_state;
2693  $this->lifetime = $obj->lifetime;
2694  $this->qc_frequency = $obj->qc_frequency;
2695  $this->price = $obj->price;
2696  $this->price_ttc = $obj->price_ttc;
2697  $this->price_min = $obj->price_min;
2698  $this->price_min_ttc = $obj->price_min_ttc;
2699  $this->price_base_type = $obj->price_base_type;
2700  $this->cost_price = $obj->cost_price;
2701  $this->default_vat_code = $obj->default_vat_code;
2702  $this->tva_tx = $obj->tva_tx;
2704  $this->tva_npr = $obj->tva_npr;
2706  $this->localtax1_tx = $obj->localtax1_tx;
2707  $this->localtax2_tx = $obj->localtax2_tx;
2708  $this->localtax1_type = $obj->localtax1_type;
2709  $this->localtax2_type = $obj->localtax2_type;
2710 
2711  $this->finished = $obj->finished;
2712  $this->fk_default_bom = $obj->fk_default_bom;
2713 
2714  $this->duration = $obj->duration;
2715  $this->duration_value = $obj->duration ? substr($obj->duration, 0, dol_strlen($obj->duration) - 1) : null;
2716  $this->duration_unit = $obj->duration ? substr($obj->duration, -1) : null;
2717  $this->canvas = $obj->canvas;
2718  $this->net_measure = $obj->net_measure;
2719  $this->net_measure_units = $obj->net_measure_units;
2720  $this->weight = $obj->weight;
2721  $this->weight_units = $obj->weight_units;
2722  $this->length = $obj->length;
2723  $this->length_units = $obj->length_units;
2724  $this->width = $obj->width;
2725  $this->width_units = $obj->width_units;
2726  $this->height = $obj->height;
2727  $this->height_units = $obj->height_units;
2728 
2729  $this->surface = $obj->surface;
2730  $this->surface_units = $obj->surface_units;
2731  $this->volume = $obj->volume;
2732  $this->volume_units = $obj->volume_units;
2733  $this->barcode = $obj->barcode;
2734  $this->barcode_type = $obj->fk_barcode_type;
2735 
2736  $this->accountancy_code_buy = $obj->accountancy_code_buy;
2737  $this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra;
2738  $this->accountancy_code_buy_export = $obj->accountancy_code_buy_export;
2739  $this->accountancy_code_sell = $obj->accountancy_code_sell;
2740  $this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra;
2741  $this->accountancy_code_sell_export = $obj->accountancy_code_sell_export;
2742 
2743  $this->fk_default_warehouse = $obj->fk_default_warehouse;
2744  $this->fk_default_workstation = $obj->fk_default_workstation;
2745  $this->seuil_stock_alerte = $obj->seuil_stock_alerte;
2746  $this->desiredstock = $obj->desiredstock;
2747  $this->stock_reel = $obj->stock;
2748  $this->pmp = $obj->pmp;
2749 
2750  $this->date_creation = $obj->datec;
2751  $this->date_modification = $obj->tms;
2752  $this->import_key = $obj->import_key;
2753  $this->entity = $obj->entity;
2754 
2755  $this->ref_ext = $obj->ref_ext;
2756  $this->fk_price_expression = $obj->fk_price_expression;
2757  $this->fk_unit = $obj->fk_unit;
2758  $this->price_autogen = $obj->price_autogen;
2759  $this->model_pdf = $obj->model_pdf;
2760  $this->last_main_doc = $obj->last_main_doc;
2761 
2762  $this->mandatory_period = $obj->mandatory_period;
2763 
2764  $this->db->free($resql);
2765 
2766  // fetch optionals attributes and labels
2767  $this->fetch_optionals();
2768 
2769  // Multilangs
2770  if (getDolGlobalInt('MAIN_MULTILANGS') && empty($ignore_lang_load)) {
2771  $this->getMultiLangs();
2772  }
2773 
2774  // Load multiprices array
2775  if (getDolGlobalString('PRODUIT_MULTIPRICES') && empty($ignore_price_load)) { // prices per segment
2776  $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
2777  for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
2778  $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2779  $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
2780  $sql .= " ,price_label";
2781  $sql .= " FROM ".$this->db->prefix()."product_price";
2782  $sql .= " WHERE entity IN (".getEntity('productprice').")";
2783  $sql .= " AND price_level=".((int) $i);
2784  $sql .= " AND fk_product = ".((int) $this->id);
2785  $sql .= " ORDER BY date_price DESC, rowid DESC"; // Get the most recent line
2786  $sql .= " LIMIT 1"; // Only the first one
2787  $resql = $this->db->query($sql);
2788  if ($resql) {
2789  $result = $this->db->fetch_array($resql);
2790 
2791  $this->multiprices[$i] = $result ? $result["price"] : null;
2792  $this->multiprices_ttc[$i] = $result ? $result["price_ttc"] : null;
2793  $this->multiprices_min[$i] = $result ? $result["price_min"] : null;
2794  $this->multiprices_min_ttc[$i] = $result ? $result["price_min_ttc"] : null;
2795  $this->multiprices_base_type[$i] = $result ? $result["price_base_type"] : null;
2796  // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
2797  $this->multiprices_tva_tx[$i] = $result ? $result["tva_tx"].($result ? ' ('.$result['default_vat_code'].')' : '') : null;
2798  $this->multiprices_recuperableonly[$i] = $result ? $result["recuperableonly"] : null;
2799 
2800  // Price by quantity
2801  /*
2802  $this->prices_by_qty[$i]=$result["price_by_qty"];
2803  $this->prices_by_qty_id[$i]=$result["rowid"];
2804  // Récuperation de la liste des prix selon qty si flag positionné
2805  if ($this->prices_by_qty[$i] == 1)
2806  {
2807  $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
2808  $sql.= " FROM ".$this->db->prefix()."product_price_by_qty";
2809  $sql.= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
2810  $sql.= " ORDER BY quantity ASC";
2811 
2812  $resql = $this->db->query($sql);
2813  if ($resql)
2814  {
2815  $resultat=array();
2816  $ii=0;
2817  while ($result= $this->db->fetch_array($resql)) {
2818  $resultat[$ii]=array();
2819  $resultat[$ii]["rowid"]=$result["rowid"];
2820  $resultat[$ii]["price"]= $result["price"];
2821  $resultat[$ii]["unitprice"]= $result["unitprice"];
2822  $resultat[$ii]["quantity"]= $result["quantity"];
2823  $resultat[$ii]["remise_percent"]= $result["remise_percent"];
2824  $resultat[$ii]["remise"]= $result["remise"]; // deprecated
2825  $resultat[$ii]["price_base_type"]= $result["price_base_type"];
2826  $ii++;
2827  }
2828  $this->prices_by_qty_list[$i]=$resultat;
2829  }
2830  else
2831  {
2832  dol_print_error($this->db);
2833  return -1;
2834  }
2835  }*/
2836  } else {
2837  $this->error = $this->db->lasterror;
2838  return -1;
2839  }
2840  }
2841  } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES') && empty($ignore_price_load)) { // prices per customers
2842  // Nothing loaded by default. List may be very long.
2843  } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY') && empty($ignore_price_load)) { // prices per quantity
2844  $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2845  $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid";
2846  $sql .= " FROM ".$this->db->prefix()."product_price";
2847  $sql .= " WHERE fk_product = ".((int) $this->id);
2848  $sql .= " ORDER BY date_price DESC, rowid DESC";
2849  $sql .= " LIMIT 1";
2850 
2851  $resql = $this->db->query($sql);
2852  if ($resql) {
2853  $result = $this->db->fetch_array($resql);
2854 
2855  if ($result) {
2856  // Price by quantity
2857  $this->prices_by_qty[0] = $result["price_by_qty"];
2858  $this->prices_by_qty_id[0] = $result["rowid"];
2859  // Récuperation de la liste des prix selon qty si flag positionné
2860  if ($this->prices_by_qty[0] == 1) {
2861  $sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise, remise, price_base_type";
2862  $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
2863  $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[0]);
2864  $sql .= " ORDER BY quantity ASC";
2865 
2866  $resql = $this->db->query($sql);
2867  if ($resql) {
2868  $resultat = array();
2869  $ii = 0;
2870  while ($result = $this->db->fetch_array($resql)) {
2871  $resultat[$ii] = array();
2872  $resultat[$ii]["rowid"] = $result["rowid"];
2873  $resultat[$ii]["price"] = $result["price"];
2874  $resultat[$ii]["unitprice"] = $result["unitprice"];
2875  $resultat[$ii]["quantity"] = $result["quantity"];
2876  $resultat[$ii]["remise_percent"] = $result["remise_percent"];
2877  //$resultat[$ii]["remise"]= $result["remise"]; // deprecated
2878  $resultat[$ii]["price_base_type"] = $result["price_base_type"];
2879  $ii++;
2880  }
2881  $this->prices_by_qty_list[0] = $resultat;
2882  } else {
2883  $this->error = $this->db->lasterror;
2884  return -1;
2885  }
2886  }
2887  }
2888  } else {
2889  $this->error = $this->db->lasterror;
2890  return -1;
2891  }
2892  } elseif (getDolGlobalString('PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES') && empty($ignore_price_load)) { // prices per customer and quantity
2893  $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
2894  for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
2895  $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2896  $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
2897  $sql .= " FROM ".$this->db->prefix()."product_price";
2898  $sql .= " WHERE entity IN (".getEntity('productprice').")";
2899  $sql .= " AND price_level=".((int) $i);
2900  $sql .= " AND fk_product = ".((int) $this->id);
2901  $sql .= " ORDER BY date_price DESC, rowid DESC";
2902  $sql .= " LIMIT 1";
2903  $resql = $this->db->query($sql);
2904  if (!$resql) {
2905  $this->error = $this->db->lasterror;
2906  return -1;
2907  } elseif ($result = $this->db->fetch_array($resql)) {
2908  $this->multiprices[$i] = (!empty($result["price"]) ? $result["price"] : 0);
2909  $this->multiprices_ttc[$i] = (!empty($result["price_ttc"]) ? $result["price_ttc"] : 0);
2910  $this->multiprices_min[$i] = (!empty($result["price_min"]) ? $result["price_min"] : 0);
2911  $this->multiprices_min_ttc[$i] = (!empty($result["price_min_ttc"]) ? $result["price_min_ttc"] : 0);
2912  $this->multiprices_base_type[$i] = (!empty($result["price_base_type"]) ? $result["price_base_type"] : '');
2913  // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
2914  $this->multiprices_tva_tx[$i] = (!empty($result["tva_tx"]) ? $result["tva_tx"] : 0); // TODO Add ' ('.$result['default_vat_code'].')'
2915  $this->multiprices_recuperableonly[$i] = (!empty($result["recuperableonly"]) ? $result["recuperableonly"] : 0);
2916 
2917  // Price by quantity
2918  $this->prices_by_qty[$i] = (!empty($result["price_by_qty"]) ? $result["price_by_qty"] : 0);
2919  $this->prices_by_qty_id[$i] = (!empty($result["rowid"]) ? $result["rowid"] : 0);
2920  // Récuperation de la liste des prix selon qty si flag positionné
2921  if ($this->prices_by_qty[$i] == 1) {
2922  $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
2923  $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
2924  $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
2925  $sql .= " ORDER BY quantity ASC";
2926 
2927  $resql = $this->db->query($sql);
2928  if ($resql) {
2929  $resultat = array();
2930  $ii = 0;
2931  while ($result = $this->db->fetch_array($resql)) {
2932  $resultat[$ii] = array();
2933  $resultat[$ii]["rowid"] = $result["rowid"];
2934  $resultat[$ii]["price"] = $result["price"];
2935  $resultat[$ii]["unitprice"] = $result["unitprice"];
2936  $resultat[$ii]["quantity"] = $result["quantity"];
2937  $resultat[$ii]["remise_percent"] = $result["remise_percent"];
2938  $resultat[$ii]["remise"] = $result["remise"]; // deprecated
2939  $resultat[$ii]["price_base_type"] = $result["price_base_type"];
2940  $ii++;
2941  }
2942  $this->prices_by_qty_list[$i] = $resultat;
2943  } else {
2944  $this->error = $this->db->lasterror;
2945  return -1;
2946  }
2947  }
2948  }
2949  }
2950  }
2951 
2952  if (isModEnabled('dynamicprices') && !empty($this->fk_price_expression) && empty($ignore_expression)) {
2953  include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2954  $priceparser = new PriceParser($this->db);
2955  $price_result = $priceparser->parseProduct($this);
2956  if ($price_result >= 0) {
2957  $this->price = $price_result;
2958  // Calculate the VAT
2959  $this->price_ttc = (float) price2num($this->price) * (1 + ($this->tva_tx / 100));
2960  $this->price_ttc = (float) price2num($this->price_ttc, 'MU');
2961  }
2962  }
2963 
2964  // We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product.
2965  // Instead we just init the stock_warehouse array
2966  $this->stock_warehouse = array();
2967 
2968  return 1;
2969  } else {
2970  return 0;
2971  }
2972  } else {
2973  $this->error = $this->db->lasterror();
2974  return -1;
2975  }
2976  }
2977 
2978  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2985  public function load_stats_mo($socid = 0)
2986  {
2987  // phpcs:enable
2988  global $user, $hookmanager, $action;
2989 
2990  $error = 0;
2991 
2992  foreach (array('toconsume', 'consumed', 'toproduce', 'produced') as $role) {
2993  $this->stats_mo['customers_'.$role] = 0;
2994  $this->stats_mo['nb_'.$role] = 0;
2995  $this->stats_mo['qty_'.$role] = 0;
2996 
2997  $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
2998  $sql .= " SUM(mp.qty) as qty";
2999  $sql .= " FROM ".$this->db->prefix()."mrp_mo as c";
3000  $sql .= " INNER JOIN ".$this->db->prefix()."mrp_production as mp ON mp.fk_mo=c.rowid";
3001  if (!$user->hasRight('societe', 'client', 'voir')) {
3002  $sql .= " INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc=c.fk_soc AND sc.fk_user = ".((int) $user->id);
3003  }
3004  $sql .= " WHERE ";
3005  $sql .= " c.entity IN (".getEntity('mo').")";
3006 
3007  $sql .= " AND mp.fk_product = ".((int) $this->id);
3008  $sql .= " AND mp.role ='".$this->db->escape($role)."'";
3009  if ($socid > 0) {
3010  $sql .= " AND c.fk_soc = ".((int) $socid);
3011  }
3012 
3013  $result = $this->db->query($sql);
3014  if ($result) {
3015  $obj = $this->db->fetch_object($result);
3016  $this->stats_mo['customers_'.$role] = $obj->nb_customers ? $obj->nb_customers : 0;
3017  $this->stats_mo['nb_'.$role] = $obj->nb ? $obj->nb : 0;
3018  $this->stats_mo['qty_'.$role] = $obj->qty ? price2num($obj->qty, 'MS') : 0; // qty may be a float due to the SUM()
3019  } else {
3020  $this->error = $this->db->error();
3021  $error++;
3022  }
3023  }
3024 
3025  if (!empty($error)) {
3026  return -1;
3027  }
3028 
3029  $parameters = array('socid' => $socid);
3030  $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3031  if ($reshook > 0) {
3032  $this->stats_mo = $hookmanager->resArray['stats_mo'];
3033  }
3034 
3035  return 1;
3036  }
3037 
3038  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3045  public function load_stats_bom($socid = 0)
3046  {
3047  // phpcs:enable
3048  global $user, $hookmanager, $action;
3049 
3050  $error = 0;
3051 
3052  $this->stats_bom['nb_toproduce'] = 0;
3053  $this->stats_bom['nb_toconsume'] = 0;
3054  $this->stats_bom['qty_toproduce'] = 0;
3055  $this->stats_bom['qty_toconsume'] = 0;
3056 
3057  $sql = "SELECT COUNT(DISTINCT b.rowid) as nb_toproduce,";
3058  $sql .= " SUM(b.qty) as qty_toproduce";
3059  $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3060  $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
3061  $sql .= " WHERE ";
3062  $sql .= " b.entity IN (".getEntity('bom').")";
3063  $sql .= " AND b.fk_product =".((int) $this->id);
3064  $sql .= " GROUP BY b.rowid";
3065 
3066  $result = $this->db->query($sql);
3067  if ($result) {
3068  $obj = $this->db->fetch_object($result);
3069  $this->stats_bom['nb_toproduce'] = !empty($obj->nb_toproduce) ? $obj->nb_toproduce : 0;
3070  $this->stats_bom['qty_toproduce'] = !empty($obj->qty_toproduce) ? price2num($obj->qty_toproduce) : 0;
3071  } else {
3072  $this->error = $this->db->error();
3073  $error++;
3074  }
3075 
3076  $sql = "SELECT COUNT(DISTINCT bl.rowid) as nb_toconsume,";
3077  $sql .= " SUM(bl.qty) as qty_toconsume";
3078  $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
3079  $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
3080  $sql .= " WHERE ";
3081  $sql .= " b.entity IN (".getEntity('bom').")";
3082  $sql .= " AND bl.fk_product =".((int) $this->id);
3083 
3084  $result = $this->db->query($sql);
3085  if ($result) {
3086  $obj = $this->db->fetch_object($result);
3087  $this->stats_bom['nb_toconsume'] = !empty($obj->nb_toconsume) ? $obj->nb_toconsume : 0;
3088  $this->stats_bom['qty_toconsume'] = !empty($obj->qty_toconsume) ? price2num($obj->qty_toconsume) : 0;
3089  } else {
3090  $this->error = $this->db->error();
3091  $error++;
3092  }
3093 
3094  if (!empty($error)) {
3095  return -1;
3096  }
3097 
3098  $parameters = array('socid' => $socid);
3099  $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
3100  if ($reshook > 0) {
3101  $this->stats_bom = $hookmanager->resArray['stats_bom'];
3102  }
3103 
3104  return 1;
3105  }
3106 
3107  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3114  public function load_stats_propale($socid = 0)
3115  {
3116  // phpcs:enable
3117  global $conf, $user, $hookmanager, $action;
3118 
3119  $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,";
3120  $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3121  $sql .= " FROM ".$this->db->prefix()."propaldet as pd";
3122  $sql .= ", ".$this->db->prefix()."propal as p";
3123  $sql .= ", ".$this->db->prefix()."societe as s";
3124  if (!$user->hasRight('societe', 'client', 'voir')) {
3125  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3126  }
3127  $sql .= " WHERE p.rowid = pd.fk_propal";
3128  $sql .= " AND p.fk_soc = s.rowid";
3129  $sql .= " AND p.entity IN (".getEntity('propal').")";
3130  $sql .= " AND pd.fk_product = ".((int) $this->id);
3131  if (!$user->hasRight('societe', 'client', 'voir')) {
3132  $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3133  }
3134  //$sql.= " AND pr.fk_statut != 0";
3135  if ($socid > 0) {
3136  $sql .= " AND p.fk_soc = ".((int) $socid);
3137  }
3138 
3139  $result = $this->db->query($sql);
3140  if ($result) {
3141  $obj = $this->db->fetch_object($result);
3142  $this->stats_propale['customers'] = $obj->nb_customers;
3143  $this->stats_propale['nb'] = $obj->nb;
3144  $this->stats_propale['rows'] = $obj->nb_rows;
3145  $this->stats_propale['qty'] = $obj->qty ? $obj->qty : 0;
3146 
3147  // if it's a virtual product, maybe it is in proposal by extension
3148  if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3149  $TFather = $this->getFather();
3150  if (is_array($TFather) && !empty($TFather)) {
3151  foreach ($TFather as &$fatherData) {
3152  $pFather = new Product($this->db);
3153  $pFather->id = $fatherData['id'];
3154  $qtyCoef = $fatherData['qty'];
3155 
3156  if ($fatherData['incdec']) {
3157  $pFather->load_stats_propale($socid);
3158 
3159  $this->stats_propale['customers'] += $pFather->stats_propale['customers'];
3160  $this->stats_propale['nb'] += $pFather->stats_propale['nb'];
3161  $this->stats_propale['rows'] += $pFather->stats_propale['rows'];
3162  $this->stats_propale['qty'] += $pFather->stats_propale['qty'] * $qtyCoef;
3163  }
3164  }
3165  }
3166  }
3167 
3168  $parameters = array('socid' => $socid);
3169  $reshook = $hookmanager->executeHooks('loadStatsCustomerProposal', $parameters, $this, $action);
3170  if ($reshook > 0) {
3171  $this->stats_propale = $hookmanager->resArray['stats_propale'];
3172  }
3173 
3174  return 1;
3175  } else {
3176  $this->error = $this->db->error();
3177  return -1;
3178  }
3179  }
3180 
3181 
3182  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3189  public function load_stats_proposal_supplier($socid = 0)
3190  {
3191  // phpcs:enable
3192  global $conf, $user, $hookmanager, $action;
3193 
3194  $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,";
3195  $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3196  $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as pd";
3197  $sql .= ", ".$this->db->prefix()."supplier_proposal as p";
3198  $sql .= ", ".$this->db->prefix()."societe as s";
3199  if (!$user->hasRight('societe', 'client', 'voir')) {
3200  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3201  }
3202  $sql .= " WHERE p.rowid = pd.fk_supplier_proposal";
3203  $sql .= " AND p.fk_soc = s.rowid";
3204  $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
3205  $sql .= " AND pd.fk_product = ".((int) $this->id);
3206  if (!$user->hasRight('societe', 'client', 'voir')) {
3207  $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3208  }
3209  //$sql.= " AND pr.fk_statut != 0";
3210  if ($socid > 0) {
3211  $sql .= " AND p.fk_soc = ".((int) $socid);
3212  }
3213 
3214  $result = $this->db->query($sql);
3215  if ($result) {
3216  $obj = $this->db->fetch_object($result);
3217  $this->stats_proposal_supplier['suppliers'] = $obj->nb_suppliers;
3218  $this->stats_proposal_supplier['nb'] = $obj->nb;
3219  $this->stats_proposal_supplier['rows'] = $obj->nb_rows;
3220  $this->stats_proposal_supplier['qty'] = $obj->qty ? $obj->qty : 0;
3221 
3222  $parameters = array('socid' => $socid);
3223  $reshook = $hookmanager->executeHooks('loadStatsSupplierProposal', $parameters, $this, $action);
3224  if ($reshook > 0) {
3225  $this->stats_proposal_supplier = $hookmanager->resArray['stats_proposal_supplier'];
3226  }
3227 
3228  return 1;
3229  } else {
3230  $this->error = $this->db->error();
3231  return -1;
3232  }
3233  }
3234 
3235 
3236  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3245  public function load_stats_commande($socid = 0, $filtrestatut = '', $forVirtualStock = 0)
3246  {
3247  // phpcs:enable
3248  global $conf, $user, $hookmanager, $action;
3249 
3250  $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3251  $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3252  $sql .= " FROM ".$this->db->prefix()."commandedet as cd";
3253  $sql .= ", ".$this->db->prefix()."commande as c";
3254  $sql .= ", ".$this->db->prefix()."societe as s";
3255  if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3256  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3257  }
3258  $sql .= " WHERE c.rowid = cd.fk_commande";
3259  $sql .= " AND c.fk_soc = s.rowid";
3260  $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'commande').")";
3261  $sql .= " AND cd.fk_product = ".((int) $this->id);
3262  if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3263  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3264  }
3265  if ($socid > 0) {
3266  $sql .= " AND c.fk_soc = ".((int) $socid);
3267  }
3268  if ($filtrestatut != '') {
3269  $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")";
3270  }
3271 
3272  $result = $this->db->query($sql);
3273  if ($result) {
3274  $obj = $this->db->fetch_object($result);
3275  $this->stats_commande['customers'] = $obj->nb_customers;
3276  $this->stats_commande['nb'] = $obj->nb;
3277  $this->stats_commande['rows'] = $obj->nb_rows;
3278  $this->stats_commande['qty'] = $obj->qty ? $obj->qty : 0;
3279 
3280  // if it's a virtual product, maybe it is in order by extension
3281  if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3282  $TFather = $this->getFather();
3283  if (is_array($TFather) && !empty($TFather)) {
3284  foreach ($TFather as &$fatherData) {
3285  $pFather = new Product($this->db);
3286  $pFather->id = $fatherData['id'];
3287  $qtyCoef = $fatherData['qty'];
3288 
3289  if ($fatherData['incdec']) {
3290  $pFather->load_stats_commande($socid, $filtrestatut);
3291 
3292  $this->stats_commande['customers'] += $pFather->stats_commande['customers'];
3293  $this->stats_commande['nb'] += $pFather->stats_commande['nb'];
3294  $this->stats_commande['rows'] += $pFather->stats_commande['rows'];
3295  $this->stats_commande['qty'] += $pFather->stats_commande['qty'] * $qtyCoef;
3296  }
3297  }
3298  }
3299  }
3300 
3301  // If stock decrease is on invoice validation, the theoretical stock continue to
3302  // count the orders to ship in theoretical stock when some are already removed by invoice validation.
3303  if ($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
3304  if (getDolGlobalString('DECREASE_ONLY_UNINVOICEDPRODUCTS')) {
3305  // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is on, we make a compensation but only if order not yet invoice.
3306  $adeduire = 0;
3307  $sql = "SELECT SUM(".$this->db->ifsql('f.type=2', -1, 1)." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3308  $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3309  $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'))";
3310  $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid";
3311  $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3312 
3313  dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3314  $resql = $this->db->query($sql);
3315  if ($resql) {
3316  if ($this->db->num_rows($resql) > 0) {
3317  $obj = $this->db->fetch_object($resql);
3318  $adeduire += $obj->count;
3319  }
3320  }
3321 
3322  $this->stats_commande['qty'] -= $adeduire;
3323  } else {
3324  // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is off, we make a compensation with lines of invoices linked to the order
3325  include_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
3326 
3327  // For every order having invoice already validated we need to decrease stock cause it's in physical stock
3328  $adeduire = 0;
3329  $sql = "SELECT sum(".$this->db->ifsql('f.type=2', -1, 1)." * fd.qty) as count FROM ".MAIN_DB_PREFIX."facturedet as fd ";
3330  $sql .= " JOIN ".MAIN_DB_PREFIX."facture as f ON fd.fk_facture = f.rowid";
3331  $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'))";
3332  $sql .= " JOIN ".MAIN_DB_PREFIX."commande as c ON el.fk_source = c.rowid";
3333  $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product = ".((int) $this->id);
3334 
3335  dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3336  $resql = $this->db->query($sql);
3337  if ($resql) {
3338  if ($this->db->num_rows($resql) > 0) {
3339  $obj = $this->db->fetch_object($resql);
3340  $adeduire += $obj->count;
3341  }
3342  } else {
3343  $this->error = $this->db->error();
3344  return -1;
3345  }
3346 
3347  $this->stats_commande['qty'] -= $adeduire;
3348  }
3349  }
3350 
3351  $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3352  $reshook = $hookmanager->executeHooks('loadStatsCustomerOrder', $parameters, $this, $action);
3353  if ($reshook > 0) {
3354  $this->stats_commande = $hookmanager->resArray['stats_commande'];
3355  }
3356  return 1;
3357  } else {
3358  $this->error = $this->db->error();
3359  return -1;
3360  }
3361  }
3362 
3363  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3373  public function load_stats_commande_fournisseur($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3374  {
3375  // phpcs:enable
3376  global $conf, $user, $hookmanager, $action;
3377 
3378  $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,";
3379  $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3380  $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as cd";
3381  $sql .= ", ".$this->db->prefix()."commande_fournisseur as c";
3382  $sql .= ", ".$this->db->prefix()."societe as s";
3383  if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3384  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3385  }
3386  $sql .= " WHERE c.rowid = cd.fk_commande";
3387  $sql .= " AND c.fk_soc = s.rowid";
3388  $sql .= " AND c.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
3389  $sql .= " AND cd.fk_product = ".((int) $this->id);
3390  if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3391  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3392  }
3393  if ($socid > 0) {
3394  $sql .= " AND c.fk_soc = ".((int) $socid);
3395  }
3396  if ($filtrestatut != '') {
3397  $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")"; // Peut valoir 0
3398  }
3399  if (!empty($dateofvirtualstock)) {
3400  $sql .= " AND c.date_livraison <= '".$this->db->idate($dateofvirtualstock)."'";
3401  }
3402 
3403  $result = $this->db->query($sql);
3404  if ($result) {
3405  $obj = $this->db->fetch_object($result);
3406  $this->stats_commande_fournisseur['suppliers'] = $obj->nb_suppliers;
3407  $this->stats_commande_fournisseur['nb'] = $obj->nb;
3408  $this->stats_commande_fournisseur['rows'] = $obj->nb_rows;
3409  $this->stats_commande_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3410 
3411  $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3412  $reshook = $hookmanager->executeHooks('loadStatsSupplierOrder', $parameters, $this, $action);
3413  if ($reshook > 0) {
3414  $this->stats_commande_fournisseur = $hookmanager->resArray['stats_commande_fournisseur'];
3415  }
3416 
3417  return 1;
3418  } else {
3419  $this->error = $this->db->error().' sql='.$sql;
3420  return -1;
3421  }
3422  }
3423 
3424  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3434  public function load_stats_sending($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $filterShipmentStatus = '')
3435  {
3436  // phpcs:enable
3437  global $conf, $user, $hookmanager, $action;
3438 
3439  $sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,";
3440  $sql .= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty";
3441  $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
3442  $sql .= ", ".$this->db->prefix()."commandedet as cd";
3443  $sql .= ", ".$this->db->prefix()."commande as c";
3444  $sql .= ", ".$this->db->prefix()."expedition as e";
3445  $sql .= ", ".$this->db->prefix()."societe as s";
3446  if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3447  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3448  }
3449  $sql .= " WHERE e.rowid = ed.fk_expedition";
3450  $sql .= " AND c.rowid = cd.fk_commande";
3451  $sql .= " AND e.fk_soc = s.rowid";
3452  $sql .= " AND e.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'expedition').")";
3453  $sql .= " AND ed.fk_elementdet = cd.rowid";
3454  $sql .= " AND cd.fk_product = ".((int) $this->id);
3455  if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3456  $sql .= " AND e.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3457  }
3458  if ($socid > 0) {
3459  $sql .= " AND e.fk_soc = ".((int) $socid);
3460  }
3461  if ($filtrestatut != '') {
3462  $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3463  }
3464  if (!empty($filterShipmentStatus)) {
3465  $sql .= " AND e.fk_statut IN (".$this->db->sanitize($filterShipmentStatus).")";
3466  }
3467 
3468  $result = $this->db->query($sql);
3469  if ($result) {
3470  $obj = $this->db->fetch_object($result);
3471  $this->stats_expedition['customers'] = $obj->nb_customers;
3472  $this->stats_expedition['nb'] = $obj->nb;
3473  $this->stats_expedition['rows'] = $obj->nb_rows;
3474  $this->stats_expedition['qty'] = $obj->qty ? $obj->qty : 0;
3475 
3476  // if it's a virtual product, maybe it is in sending by extension
3477  if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3478  $TFather = $this->getFather();
3479  if (is_array($TFather) && !empty($TFather)) {
3480  foreach ($TFather as &$fatherData) {
3481  $pFather = new Product($this->db);
3482  $pFather->id = $fatherData['id'];
3483  $qtyCoef = $fatherData['qty'];
3484 
3485  if ($fatherData['incdec']) {
3486  $pFather->load_stats_sending($socid, $filtrestatut, $forVirtualStock);
3487 
3488  $this->stats_expedition['customers'] += $pFather->stats_expedition['customers'];
3489  $this->stats_expedition['nb'] += $pFather->stats_expedition['nb'];
3490  $this->stats_expedition['rows'] += $pFather->stats_expedition['rows'];
3491  $this->stats_expedition['qty'] += $pFather->stats_expedition['qty'] * $qtyCoef;
3492  }
3493  }
3494  }
3495  }
3496 
3497  $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock, 'filterShipmentStatus' => $filterShipmentStatus);
3498  $reshook = $hookmanager->executeHooks('loadStatsSending', $parameters, $this, $action);
3499  if ($reshook > 0) {
3500  $this->stats_expedition = $hookmanager->resArray['stats_expedition'];
3501  }
3502 
3503  return 1;
3504  } else {
3505  $this->error = $this->db->error();
3506  return -1;
3507  }
3508  }
3509 
3510  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3520  public function load_stats_reception($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3521  {
3522  // phpcs:enable
3523  global $conf, $user, $hookmanager, $action;
3524 
3525  $sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_suppliers, COUNT(DISTINCT cf.rowid) as nb,";
3526  $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3527  $sql .= " FROM ".$this->db->prefix()."receptiondet_batch as fd";
3528  $sql .= ", ".$this->db->prefix()."commande_fournisseur as cf";
3529  $sql .= ", ".$this->db->prefix()."societe as s";
3530  if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3531  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3532  }
3533  $sql .= " WHERE cf.rowid = fd.fk_element";
3534  $sql .= " AND cf.fk_soc = s.rowid";
3535  $sql .= " AND cf.entity IN (".getEntity($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE') ? 'stock' : 'supplier_order').")";
3536  $sql .= " AND fd.fk_product = ".((int) $this->id);
3537  if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3538  $sql .= " AND cf.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3539  }
3540  if ($socid > 0) {
3541  $sql .= " AND cf.fk_soc = ".((int) $socid);
3542  }
3543  if ($filtrestatut != '') {
3544  $sql .= " AND cf.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3545  }
3546  if (!empty($dateofvirtualstock)) {
3547  $sql .= " AND fd.datec <= '".$this->db->idate($dateofvirtualstock)."'";
3548  }
3549 
3550  $result = $this->db->query($sql);
3551  if ($result) {
3552  $obj = $this->db->fetch_object($result);
3553  $this->stats_reception['suppliers'] = $obj->nb_suppliers;
3554  $this->stats_reception['nb'] = $obj->nb;
3555  $this->stats_reception['rows'] = $obj->nb_rows;
3556  $this->stats_reception['qty'] = $obj->qty ? $obj->qty : 0;
3557 
3558  $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3559  $reshook = $hookmanager->executeHooks('loadStatsReception', $parameters, $this, $action);
3560  if ($reshook > 0) {
3561  $this->stats_reception = $hookmanager->resArray['stats_reception'];
3562  }
3563 
3564  return 1;
3565  } else {
3566  $this->error = $this->db->error();
3567  return -1;
3568  }
3569  }
3570 
3571  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3582  public function load_stats_inproduction($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null, $warehouseid = 0)
3583  {
3584  // phpcs:enable
3585  global $user, $hookmanager, $action;
3586 
3587  $serviceStockIsEnabled = isModEnabled("service") && getDolGlobalString('STOCK_SUPPORTS_SERVICES');
3588 
3589  $sql = "SELECT COUNT(DISTINCT m.fk_soc) as nb_customers, COUNT(DISTINCT m.rowid) as nb,";
3590  $sql .= " COUNT(mp.rowid) as nb_rows, SUM(mp.qty) as qty, role";
3591  $sql .= " FROM ".$this->db->prefix()."mrp_production as mp";
3592  $sql .= ", ".$this->db->prefix()."mrp_mo as m";
3593  $sql .= " LEFT JOIN ".$this->db->prefix()."societe as s ON s.rowid = m.fk_soc";
3594  if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3595  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3596  }
3597  $sql .= " WHERE m.rowid = mp.fk_mo";
3598  $sql .= " AND m.entity IN (".getEntity(($forVirtualStock && getDolGlobalString('STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE')) ? 'stock' : 'mrp').")";
3599  $sql .= " AND mp.fk_product = ".((int) $this->id);
3600  $sql .= " AND (mp.disable_stock_change IN (0) OR mp.disable_stock_change IS NULL)";
3601  if (!$user->hasRight('societe', 'client', 'voir') && !$forVirtualStock) {
3602  $sql .= " AND m.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3603  }
3604  if ($socid > 0) {
3605  $sql .= " AND m.fk_soc = ".((int) $socid);
3606  }
3607  if ($filtrestatut != '') {
3608  $sql .= " AND m.status IN (".$this->db->sanitize($filtrestatut).")";
3609  }
3610  if (!empty($dateofvirtualstock)) {
3611  $sql .= " AND m.date_valid <= '".$this->db->idate($dateofvirtualstock)."'"; // better date to code ? end of production ?
3612  }
3613  if (!$serviceStockIsEnabled) {
3614  $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))";
3615  }
3616  if (!empty($warehouseid)) {
3617  $sql .= " AND m.fk_warehouse = ".((int) $warehouseid);
3618  }
3619  $sql .= " GROUP BY role";
3620 
3621  if ($warehouseid) {
3622  $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
3623  } else {
3624  $this->stats_mrptoconsume['customers'] = 0;
3625  $this->stats_mrptoconsume['nb'] = 0;
3626  $this->stats_mrptoconsume['rows'] = 0;
3627  $this->stats_mrptoconsume['qty'] = 0;
3628  $this->stats_mrptoproduce['customers'] = 0;
3629  $this->stats_mrptoproduce['nb'] = 0;
3630  $this->stats_mrptoproduce['rows'] = 0;
3631  $this->stats_mrptoproduce['qty'] = 0;
3632  }
3633 
3634  $result = $this->db->query($sql);
3635  if ($result) {
3636  while ($obj = $this->db->fetch_object($result)) {
3637  if ($obj->role == 'toconsume' && empty($warehouseid)) {
3638  $this->stats_mrptoconsume['customers'] += $obj->nb_customers;
3639  $this->stats_mrptoconsume['nb'] += $obj->nb;
3640  $this->stats_mrptoconsume['rows'] += $obj->nb_rows;
3641  $this->stats_mrptoconsume['qty'] += ($obj->qty ? $obj->qty : 0);
3642  }
3643  if ($obj->role == 'consumed' && empty($warehouseid)) {
3644  //$this->stats_mrptoconsume['customers'] += $obj->nb_customers;
3645  //$this->stats_mrptoconsume['nb'] += $obj->nb;
3646  //$this->stats_mrptoconsume['rows'] += $obj->nb_rows;
3647  $this->stats_mrptoconsume['qty'] -= ($obj->qty ? $obj->qty : 0);
3648  }
3649  if ($obj->role == 'toproduce') {
3650  if ($warehouseid) {
3651  $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] += ($obj->qty ? $obj->qty : 0);
3652  } else {
3653  $this->stats_mrptoproduce['customers'] += $obj->nb_customers;
3654  $this->stats_mrptoproduce['nb'] += $obj->nb;
3655  $this->stats_mrptoproduce['rows'] += $obj->nb_rows;
3656  $this->stats_mrptoproduce['qty'] += ($obj->qty ? $obj->qty : 0);
3657  }
3658  }
3659  if ($obj->role == 'produced') {
3660  //$this->stats_mrptoproduce['customers'] += $obj->nb_customers;
3661  //$this->stats_mrptoproduce['nb'] += $obj->nb;
3662  //$this->stats_mrptoproduce['rows'] += $obj->nb_rows;
3663  if ($warehouseid) {
3664  $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
3665  } else {
3666  $this->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
3667  }
3668  }
3669  }
3670 
3671  // Clean data
3672  if ($warehouseid) {
3673  if ($this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] < 0) {
3674  $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'] = 0;
3675  }
3676  } else {
3677  if ($this->stats_mrptoconsume['qty'] < 0) {
3678  $this->stats_mrptoconsume['qty'] = 0;
3679  }
3680  if ($this->stats_mrptoproduce['qty'] < 0) {
3681  $this->stats_mrptoproduce['qty'] = 0;
3682  }
3683  }
3684 
3685  $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3686  $reshook = $hookmanager->executeHooks('loadStatsInProduction', $parameters, $this, $action);
3687  if ($reshook > 0) {
3688  $this->stats_mrptoproduce = $hookmanager->resArray['stats_mrptoproduce'];
3689  }
3690 
3691  return 1;
3692  } else {
3693  $this->error = $this->db->error();
3694  return -1;
3695  }
3696  }
3697 
3698  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3705  public function load_stats_contrat($socid = 0)
3706  {
3707  // phpcs:enable
3708  global $conf, $user, $hookmanager, $action;
3709 
3710  $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3711  $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3712  $sql .= " FROM ".$this->db->prefix()."contratdet as cd";
3713  $sql .= ", ".$this->db->prefix()."contrat as c";
3714  $sql .= ", ".$this->db->prefix()."societe as s";
3715  if (!$user->hasRight('societe', 'client', 'voir')) {
3716  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3717  }
3718  $sql .= " WHERE c.rowid = cd.fk_contrat";
3719  $sql .= " AND c.fk_soc = s.rowid";
3720  $sql .= " AND c.entity IN (".getEntity('contract').")";
3721  $sql .= " AND cd.fk_product = ".((int) $this->id);
3722  if (!$user->hasRight('societe', 'client', 'voir')) {
3723  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3724  }
3725  //$sql.= " AND c.statut != 0";
3726  if ($socid > 0) {
3727  $sql .= " AND c.fk_soc = ".((int) $socid);
3728  }
3729 
3730  $result = $this->db->query($sql);
3731  if ($result) {
3732  $obj = $this->db->fetch_object($result);
3733  $this->stats_contrat['customers'] = $obj->nb_customers;
3734  $this->stats_contrat['nb'] = $obj->nb;
3735  $this->stats_contrat['rows'] = $obj->nb_rows;
3736  $this->stats_contrat['qty'] = $obj->qty ? $obj->qty : 0;
3737 
3738  // if it's a virtual product, maybe it is in contract by extension
3739  if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3740  $TFather = $this->getFather();
3741  if (is_array($TFather) && !empty($TFather)) {
3742  foreach ($TFather as &$fatherData) {
3743  $pFather = new Product($this->db);
3744  $pFather->id = $fatherData['id'];
3745  $qtyCoef = $fatherData['qty'];
3746 
3747  if ($fatherData['incdec']) {
3748  $pFather->load_stats_contrat($socid);
3749 
3750  $this->stats_contrat['customers'] += $pFather->stats_contrat['customers'];
3751  $this->stats_contrat['nb'] += $pFather->stats_contrat['nb'];
3752  $this->stats_contrat['rows'] += $pFather->stats_contrat['rows'];
3753  $this->stats_contrat['qty'] += $pFather->stats_contrat['qty'] * $qtyCoef;
3754  }
3755  }
3756  }
3757  }
3758 
3759  $parameters = array('socid' => $socid);
3760  $reshook = $hookmanager->executeHooks('loadStatsContract', $parameters, $this, $action);
3761  if ($reshook > 0) {
3762  $this->stats_contrat = $hookmanager->resArray['stats_contrat'];
3763  }
3764 
3765  return 1;
3766  } else {
3767  $this->error = $this->db->error().' sql='.$sql;
3768  return -1;
3769  }
3770  }
3771 
3772  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3779  public function load_stats_facture($socid = 0)
3780  {
3781  // phpcs:enable
3782  global $conf, $user, $hookmanager, $action;
3783 
3784  $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
3785  $sql .= " COUNT(fd.rowid) as nb_rows, SUM(".$this->db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1').") as qty";
3786  $sql .= " FROM ".$this->db->prefix()."facturedet as fd";
3787  $sql .= ", ".$this->db->prefix()."facture as f";
3788  $sql .= ", ".$this->db->prefix()."societe as s";
3789  if (!$user->hasRight('societe', 'client', 'voir')) {
3790  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3791  }
3792  $sql .= " WHERE f.rowid = fd.fk_facture";
3793  $sql .= " AND f.fk_soc = s.rowid";
3794  $sql .= " AND f.entity IN (".getEntity('invoice').")";
3795  $sql .= " AND fd.fk_product = ".((int) $this->id);
3796  if (!$user->hasRight('societe', 'client', 'voir')) {
3797  $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3798  }
3799  //$sql.= " AND f.fk_statut != 0";
3800  if ($socid > 0) {
3801  $sql .= " AND f.fk_soc = ".((int) $socid);
3802  }
3803 
3804  $result = $this->db->query($sql);
3805  if ($result) {
3806  $obj = $this->db->fetch_object($result);
3807  $this->stats_facture['customers'] = $obj->nb_customers;
3808  $this->stats_facture['nb'] = $obj->nb;
3809  $this->stats_facture['rows'] = $obj->nb_rows;
3810  $this->stats_facture['qty'] = $obj->qty ? $obj->qty : 0;
3811 
3812  // if it's a virtual product, maybe it is in invoice by extension
3813  if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3814  $TFather = $this->getFather();
3815  if (is_array($TFather) && !empty($TFather)) {
3816  foreach ($TFather as &$fatherData) {
3817  $pFather = new Product($this->db);
3818  $pFather->id = $fatherData['id'];
3819  $qtyCoef = $fatherData['qty'];
3820 
3821  if ($fatherData['incdec']) {
3822  $pFather->load_stats_facture($socid);
3823 
3824  $this->stats_facture['customers'] += $pFather->stats_facture['customers'];
3825  $this->stats_facture['nb'] += $pFather->stats_facture['nb'];
3826  $this->stats_facture['rows'] += $pFather->stats_facture['rows'];
3827  $this->stats_facture['qty'] += $pFather->stats_facture['qty'] * $qtyCoef;
3828  }
3829  }
3830  }
3831  }
3832 
3833  $parameters = array('socid' => $socid);
3834  $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoice', $parameters, $this, $action);
3835  if ($reshook > 0) {
3836  $this->stats_facture = $hookmanager->resArray['stats_facture'];
3837  }
3838 
3839  return 1;
3840  } else {
3841  $this->error = $this->db->error();
3842  return -1;
3843  }
3844  }
3845 
3846 
3847  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3854  public function load_stats_facturerec($socid = 0)
3855  {
3856  // phpcs:enable
3857  global $conf, $user, $hookmanager, $action;
3858 
3859  $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
3860  $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3861  $sql .= " FROM ".MAIN_DB_PREFIX."facturedet_rec as fd";
3862  $sql .= ", ".MAIN_DB_PREFIX."facture_rec as f";
3863  $sql .= ", ".MAIN_DB_PREFIX."societe as s";
3864  if (!$user->hasRight('societe', 'client', 'voir')) {
3865  $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
3866  }
3867  $sql .= " WHERE f.rowid = fd.fk_facture";
3868  $sql .= " AND f.fk_soc = s.rowid";
3869  $sql .= " AND f.entity IN (".getEntity('invoice').")";
3870  $sql .= " AND fd.fk_product = ".((int) $this->id);
3871  if (!$user->hasRight('societe', 'client', 'voir')) {
3872  $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3873  }
3874  //$sql.= " AND f.fk_statut != 0";
3875  if ($socid > 0) {
3876  $sql .= " AND f.fk_soc = ".((int) $socid);
3877  }
3878 
3879  $result = $this->db->query($sql);
3880  if ($result) {
3881  $obj = $this->db->fetch_object($result);
3882  $this->stats_facturerec['customers'] = $obj->nb_customers;
3883  $this->stats_facturerec['nb'] = $obj->nb;
3884  $this->stats_facturerec['rows'] = $obj->nb_rows;
3885  $this->stats_facturerec['qty'] = $obj->qty ? $obj->qty : 0;
3886 
3887  // if it's a virtual product, maybe it is in invoice by extension
3888  if (getDolGlobalString('PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC')) {
3889  $TFather = $this->getFather();
3890  if (is_array($TFather) && !empty($TFather)) {
3891  foreach ($TFather as &$fatherData) {
3892  $pFather = new Product($this->db);
3893  $pFather->id = $fatherData['id'];
3894  $qtyCoef = $fatherData['qty'];
3895 
3896  if ($fatherData['incdec']) {
3897  $pFather->load_stats_facture($socid);
3898 
3899  $this->stats_facturerec['customers'] += $pFather->stats_facturerec['customers'];
3900  $this->stats_facturerec['nb'] += $pFather->stats_facturerec['nb'];
3901  $this->stats_facturerec['rows'] += $pFather->stats_facturerec['rows'];
3902  $this->stats_facturerec['qty'] += $pFather->stats_facturerec['qty'] * $qtyCoef;
3903  }
3904  }
3905  }
3906  }
3907 
3908  $parameters = array('socid' => $socid);
3909  $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoiceRec', $parameters, $this, $action);
3910  if ($reshook > 0) {
3911  $this->stats_facturerec = $hookmanager->resArray['stats_facturerec'];
3912  }
3913 
3914  return 1;
3915  } else {
3916  $this->error = $this->db->error();
3917  return -1;
3918  }
3919  }
3920 
3921  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3928  public function load_stats_facture_fournisseur($socid = 0)
3929  {
3930  // phpcs:enable
3931  global $conf, $user, $hookmanager, $action;
3932 
3933  $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
3934  $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3935  $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as fd";
3936  $sql .= ", ".$this->db->prefix()."facture_fourn as f";
3937  $sql .= ", ".$this->db->prefix()."societe as s";
3938  if (!$user->hasRight('societe', 'client', 'voir')) {
3939  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3940  }
3941  $sql .= " WHERE f.rowid = fd.fk_facture_fourn";
3942  $sql .= " AND f.fk_soc = s.rowid";
3943  $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
3944  $sql .= " AND fd.fk_product = ".((int) $this->id);
3945  if (!$user->hasRight('societe', 'client', 'voir')) {
3946  $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3947  }
3948  //$sql.= " AND f.fk_statut != 0";
3949  if ($socid > 0) {
3950  $sql .= " AND f.fk_soc = ".((int) $socid);
3951  }
3952 
3953  $result = $this->db->query($sql);
3954  if ($result) {
3955  $obj = $this->db->fetch_object($result);
3956  $this->stats_facture_fournisseur['suppliers'] = $obj->nb_suppliers;
3957  $this->stats_facture_fournisseur['nb'] = $obj->nb;
3958  $this->stats_facture_fournisseur['rows'] = $obj->nb_rows;
3959  $this->stats_facture_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3960 
3961  $parameters = array('socid' => $socid);
3962  $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoice', $parameters, $this, $action);
3963  if ($reshook > 0) {
3964  $this->stats_facture_fournisseur = $hookmanager->resArray['stats_facture_fournisseur'];
3965  }
3966 
3967  return 1;
3968  } else {
3969  $this->error = $this->db->error();
3970  return -1;
3971  }
3972  }
3973 
3974  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3983  private function _get_stats($sql, $mode, $year = 0)
3984  {
3985  // phpcs:enable
3986  $tab = array();
3987 
3988  $resql = $this->db->query($sql);
3989  if ($resql) {
3990  $num = $this->db->num_rows($resql);
3991  $i = 0;
3992  while ($i < $num) {
3993  $arr = $this->db->fetch_array($resql);
3994  if (is_array($arr)) {
3995  $keyfortab = (string) $arr[1];
3996  if ($year == -1) {
3997  $keyfortab = substr($keyfortab, -2);
3998  }
3999 
4000  if ($mode == 'byunit') {
4001  $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[0]; // 1st field
4002  } elseif ($mode == 'bynumber') {
4003  $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4004  } elseif ($mode == 'byamount') {
4005  $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
4006  } else {
4007  // Bad value for $mode
4008  return -1;
4009  }
4010  }
4011  $i++;
4012  }
4013  } else {
4014  $this->error = $this->db->error().' sql='.$sql;
4015  return -1;
4016  }
4017 
4018  if (empty($year)) {
4019  $year = dol_print_date(time(), '%Y');
4020  $month = dol_print_date(time(), '%m');
4021  } elseif ($year == -1) {
4022  $year = '';
4023  $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4024  } else {
4025  $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
4026  }
4027 
4028  $result = array();
4029 
4030  for ($j = 0; $j < 12; $j++) {
4031  // $ids is 'D', 'N', 'O', 'S', ... (First letter of month in user language)
4032  $idx = ucfirst(dol_trunc(dol_print_date(dol_mktime(12, 0, 0, $month, 1, 1970), "%b"), 1, 'right', 'UTF-8', 1));
4033 
4034  //print $idx.'-'.$year.'-'.$month.'<br>';
4035  $result[$j] = array($idx, isset($tab[$year.$month]) ? $tab[$year.$month] : 0);
4036  // $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0);
4037 
4038  $month = "0".($month - 1);
4039  if (dol_strlen($month) == 3) {
4040  $month = substr($month, 1);
4041  }
4042  if ($month == 0) {
4043  $month = 12;
4044  $year = $year - 1;
4045  }
4046  }
4047 
4048  return array_reverse($result);
4049  }
4050 
4051 
4052  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4063  public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4064  {
4065  // phpcs:enable
4066  global $conf;
4067  global $user;
4068 
4069  $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4070  if ($mode == 'bynumber') {
4071  $sql .= ", count(DISTINCT f.rowid)";
4072  }
4073  $sql .= ", sum(d.total_ht) as total_ht";
4074  $sql .= " FROM ".$this->db->prefix()."facturedet as d, ".$this->db->prefix()."facture as f, ".$this->db->prefix()."societe as s";
4075  if ($filteronproducttype >= 0) {
4076  $sql .= ", ".$this->db->prefix()."product as p";
4077  }
4078  if (!$user->hasRight('societe', 'client', 'voir')) {
4079  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4080  }
4081  $sql .= " WHERE f.rowid = d.fk_facture";
4082  if ($this->id > 0) {
4083  $sql .= " AND d.fk_product = ".((int) $this->id);
4084  } else {
4085  $sql .= " AND d.fk_product > 0";
4086  }
4087  if ($filteronproducttype >= 0) {
4088  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4089  }
4090  $sql .= " AND f.fk_soc = s.rowid";
4091  $sql .= " AND f.entity IN (".getEntity('invoice').")";
4092  if (!$user->hasRight('societe', 'client', 'voir')) {
4093  $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4094  }
4095  if ($socid > 0) {
4096  $sql .= " AND f.fk_soc = $socid";
4097  }
4098  $sql .= $morefilter;
4099  $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4100  $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4101 
4102  return $this->_get_stats($sql, $mode, $year);
4103  }
4104 
4105 
4106  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4117  public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4118  {
4119  // phpcs:enable
4120  global $conf;
4121  global $user;
4122 
4123  $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
4124  if ($mode == 'bynumber') {
4125  $sql .= ", count(DISTINCT f.rowid)";
4126  }
4127  $sql .= ", sum(d.total_ht) as total_ht";
4128  $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as d, ".$this->db->prefix()."facture_fourn as f, ".$this->db->prefix()."societe as s";
4129  if ($filteronproducttype >= 0) {
4130  $sql .= ", ".$this->db->prefix()."product as p";
4131  }
4132  if (!$user->hasRight('societe', 'client', 'voir')) {
4133  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4134  }
4135  $sql .= " WHERE f.rowid = d.fk_facture_fourn";
4136  if ($this->id > 0) {
4137  $sql .= " AND d.fk_product = ".((int) $this->id);
4138  } else {
4139  $sql .= " AND d.fk_product > 0";
4140  }
4141  if ($filteronproducttype >= 0) {
4142  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4143  }
4144  $sql .= " AND f.fk_soc = s.rowid";
4145  $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
4146  if (!$user->hasRight('societe', 'client', 'voir')) {
4147  $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4148  }
4149  if ($socid > 0) {
4150  $sql .= " AND f.fk_soc = $socid";
4151  }
4152  $sql .= $morefilter;
4153  $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
4154  $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
4155 
4156  return $this->_get_stats($sql, $mode, $year);
4157  }
4158 
4159  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4170  public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4171  {
4172  // phpcs:enable
4173  global $conf, $user;
4174 
4175  $sql = "SELECT sum(d.qty) as qty, date_format(p.datep, '%Y%m')";
4176  if ($mode == 'bynumber') {
4177  $sql .= ", count(DISTINCT p.rowid)";
4178  }
4179  $sql .= ", sum(d.total_ht) as total_ht";
4180  $sql .= " FROM ".$this->db->prefix()."propaldet as d, ".$this->db->prefix()."propal as p, ".$this->db->prefix()."societe as s";
4181  if ($filteronproducttype >= 0) {
4182  $sql .= ", ".$this->db->prefix()."product as prod";
4183  }
4184  if (!$user->hasRight('societe', 'client', 'voir')) {
4185  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4186  }
4187  $sql .= " WHERE p.rowid = d.fk_propal";
4188  if ($this->id > 0) {
4189  $sql .= " AND d.fk_product = ".((int) $this->id);
4190  } else {
4191  $sql .= " AND d.fk_product > 0";
4192  }
4193  if ($filteronproducttype >= 0) {
4194  $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4195  }
4196  $sql .= " AND p.fk_soc = s.rowid";
4197  $sql .= " AND p.entity IN (".getEntity('propal').")";
4198  if (!$user->hasRight('societe', 'client', 'voir')) {
4199  $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4200  }
4201  if ($socid > 0) {
4202  $sql .= " AND p.fk_soc = ".((int) $socid);
4203  }
4204  $sql .= $morefilter;
4205  $sql .= " GROUP BY date_format(p.datep,'%Y%m')";
4206  $sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC";
4207 
4208  return $this->_get_stats($sql, $mode, $year);
4209  }
4210 
4211  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4222  public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4223  {
4224  // phpcs:enable
4225  global $conf;
4226  global $user;
4227 
4228  $sql = "SELECT sum(d.qty) as qty, date_format(p.date_valid, '%Y%m')";
4229  if ($mode == 'bynumber') {
4230  $sql .= ", count(DISTINCT p.rowid)";
4231  }
4232  $sql .= ", sum(d.total_ht) as total_ht";
4233  $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as d, ".$this->db->prefix()."supplier_proposal as p, ".$this->db->prefix()."societe as s";
4234  if ($filteronproducttype >= 0) {
4235  $sql .= ", ".$this->db->prefix()."product as prod";
4236  }
4237  if (!$user->hasRight('societe', 'client', 'voir')) {
4238  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4239  }
4240  $sql .= " WHERE p.rowid = d.fk_supplier_proposal";
4241  if ($this->id > 0) {
4242  $sql .= " AND d.fk_product = ".((int) $this->id);
4243  } else {
4244  $sql .= " AND d.fk_product > 0";
4245  }
4246  if ($filteronproducttype >= 0) {
4247  $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4248  }
4249  $sql .= " AND p.fk_soc = s.rowid";
4250  $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
4251  if (!$user->hasRight('societe', 'client', 'voir')) {
4252  $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4253  }
4254  if ($socid > 0) {
4255  $sql .= " AND p.fk_soc = ".((int) $socid);
4256  }
4257  $sql .= $morefilter;
4258  $sql .= " GROUP BY date_format(p.date_valid,'%Y%m')";
4259  $sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
4260 
4261  return $this->_get_stats($sql, $mode, $year);
4262  }
4263 
4264  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4275  public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4276  {
4277  // phpcs:enable
4278  global $conf, $user;
4279 
4280  $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4281  if ($mode == 'bynumber') {
4282  $sql .= ", count(DISTINCT c.rowid)";
4283  }
4284  $sql .= ", sum(d.total_ht) as total_ht";
4285  $sql .= " FROM ".$this->db->prefix()."commandedet as d, ".$this->db->prefix()."commande as c, ".$this->db->prefix()."societe as s";
4286  if ($filteronproducttype >= 0) {
4287  $sql .= ", ".$this->db->prefix()."product as p";
4288  }
4289  if (!$user->hasRight('societe', 'client', 'voir')) {
4290  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4291  }
4292  $sql .= " WHERE c.rowid = d.fk_commande";
4293  if ($this->id > 0) {
4294  $sql .= " AND d.fk_product = ".((int) $this->id);
4295  } else {
4296  $sql .= " AND d.fk_product > 0";
4297  }
4298  if ($filteronproducttype >= 0) {
4299  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4300  }
4301  $sql .= " AND c.fk_soc = s.rowid";
4302  $sql .= " AND c.entity IN (".getEntity('commande').")";
4303  if (!$user->hasRight('societe', 'client', 'voir')) {
4304  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4305  }
4306  if ($socid > 0) {
4307  $sql .= " AND c.fk_soc = ".((int) $socid);
4308  }
4309  $sql .= $morefilter;
4310  $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4311  $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4312 
4313  return $this->_get_stats($sql, $mode, $year);
4314  }
4315 
4316  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4327  public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4328  {
4329  // phpcs:enable
4330  global $conf, $user;
4331 
4332  $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4333  if ($mode == 'bynumber') {
4334  $sql .= ", count(DISTINCT c.rowid)";
4335  }
4336  $sql .= ", sum(d.total_ht) as total_ht";
4337  $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as d, ".$this->db->prefix()."commande_fournisseur as c, ".$this->db->prefix()."societe as s";
4338  if ($filteronproducttype >= 0) {
4339  $sql .= ", ".$this->db->prefix()."product as p";
4340  }
4341  if (!$user->hasRight('societe', 'client', 'voir')) {
4342  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4343  }
4344  $sql .= " WHERE c.rowid = d.fk_commande";
4345  if ($this->id > 0) {
4346  $sql .= " AND d.fk_product = ".((int) $this->id);
4347  } else {
4348  $sql .= " AND d.fk_product > 0";
4349  }
4350  if ($filteronproducttype >= 0) {
4351  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4352  }
4353  $sql .= " AND c.fk_soc = s.rowid";
4354  $sql .= " AND c.entity IN (".getEntity('supplier_order').")";
4355  if (!$user->hasRight('societe', 'client', 'voir')) {
4356  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4357  }
4358  if ($socid > 0) {
4359  $sql .= " AND c.fk_soc = ".((int) $socid);
4360  }
4361  $sql .= $morefilter;
4362  $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4363  $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4364 
4365  return $this->_get_stats($sql, $mode, $year);
4366  }
4367 
4368  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4379  public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4380  {
4381  // phpcs:enable
4382  global $conf, $user;
4383 
4384  $sql = "SELECT sum(d.qty) as qty, date_format(c.date_contrat, '%Y%m')";
4385  if ($mode == 'bynumber') {
4386  $sql .= ", count(DISTINCT c.rowid)";
4387  }
4388  $sql .= ", sum(d.total_ht) as total_ht";
4389  $sql .= " FROM ".$this->db->prefix()."contratdet as d, ".$this->db->prefix()."contrat as c, ".$this->db->prefix()."societe as s";
4390  if ($filteronproducttype >= 0) {
4391  $sql .= ", ".$this->db->prefix()."product as p";
4392  }
4393  if (!$user->hasRight('societe', 'client', 'voir')) {
4394  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4395  }
4396  $sql .= " WHERE c.entity IN (".getEntity('contract').")";
4397  $sql .= " AND c.rowid = d.fk_contrat";
4398 
4399  if ($this->id > 0) {
4400  $sql .= " AND d.fk_product = ".((int) $this->id);
4401  } else {
4402  $sql .= " AND d.fk_product > 0";
4403  }
4404  if ($filteronproducttype >= 0) {
4405  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4406  }
4407  $sql .= " AND c.fk_soc = s.rowid";
4408 
4409  if (!$user->hasRight('societe', 'client', 'voir')) {
4410  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4411  }
4412  if ($socid > 0) {
4413  $sql .= " AND c.fk_soc = ".((int) $socid);
4414  }
4415  $sql .= $morefilter;
4416  $sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')";
4417  $sql .= " ORDER BY date_format(c.date_contrat,'%Y%m') DESC";
4418 
4419  return $this->_get_stats($sql, $mode, $year);
4420  }
4421 
4422  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4433  public function get_nb_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4434  {
4435  // phpcs:enable
4436  global $conf, $user;
4437 
4438  $sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')";
4439  if ($mode == 'bynumber') {
4440  $sql .= ", count(DISTINCT d.rowid)";
4441  }
4442  $sql .= " FROM ".$this->db->prefix()."mrp_mo as d LEFT JOIN ".$this->db->prefix()."societe as s ON d.fk_soc = s.rowid";
4443  if ($filteronproducttype >= 0) {
4444  $sql .= ", ".$this->db->prefix()."product as p";
4445  }
4446  if (!$user->hasRight('societe', 'client', 'voir')) {
4447  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4448  }
4449 
4450  $sql .= " WHERE d.entity IN (".getEntity('mo').")";
4451  $sql .= " AND d.status > 0";
4452 
4453  if ($this->id > 0) {
4454  $sql .= " AND d.fk_product = ".((int) $this->id);
4455  } else {
4456  $sql .= " AND d.fk_product > 0";
4457  }
4458  if ($filteronproducttype >= 0) {
4459  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4460  }
4461 
4462  if (!$user->hasRight('societe', 'client', 'voir')) {
4463  $sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4464  }
4465  if ($socid > 0) {
4466  $sql .= " AND d.fk_soc = ".((int) $socid);
4467  }
4468  $sql .= $morefilter;
4469  $sql .= " GROUP BY date_format(d.date_valid,'%Y%m')";
4470  $sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC";
4471 
4472  return $this->_get_stats($sql, $mode, $year);
4473  }
4474 
4475  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4486  public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
4487  {
4488  global $user;
4489 
4490  // phpcs:enable
4491  // Clean parameters
4492  if (!is_numeric($id_pere)) {
4493  $id_pere = 0;
4494  }
4495  if (!is_numeric($id_fils)) {
4496  $id_fils = 0;
4497  }
4498  if (!is_numeric($incdec)) {
4499  $incdec = 0;
4500  }
4501 
4502  $result = $this->del_sousproduit($id_pere, $id_fils);
4503  if ($result < 0) {
4504  return $result;
4505  }
4506 
4507  // Check not already father of id_pere (to avoid father -> child -> father links)
4508  $sql = "SELECT fk_product_pere from ".$this->db->prefix()."product_association";
4509  $sql .= " WHERE fk_product_pere = ".((int) $id_fils)." AND fk_product_fils = ".((int) $id_pere);
4510  if (!$this->db->query($sql)) {
4511  dol_print_error($this->db);
4512  return -1;
4513  } else {
4514  //Selection of the highest row
4515  $sql = "SELECT MAX(rang) as max_rank FROM ".$this->db->prefix()."product_association";
4516  $sql .= " WHERE fk_product_pere = ".((int) $id_pere);
4517  $resql = $this->db->query($sql);
4518  if ($resql) {
4519  $obj = $this->db->fetch_object($resql);
4520  $rank = $obj->max_rank + 1;
4521  //Addition of a product with the highest rank +1
4522  $sql = "INSERT INTO ".$this->db->prefix()."product_association(fk_product_pere,fk_product_fils,qty,incdec,rang)";
4523  $sql .= " VALUES (".((int) $id_pere).", ".((int) $id_fils).", ".price2num($qty, 'MS').", ".((int) $incdec).", ".((int) $rank).")";
4524  if (! $this->db->query($sql)) {
4525  dol_print_error($this->db);
4526  return -1;
4527  } else {
4528  if (!$notrigger) {
4529  // Call trigger
4530  $result = $this->call_trigger('PRODUCT_SUBPRODUCT_ADD', $user);
4531  if ($result < 0) {
4532  $this->error = $this->db->lasterror();
4533  dol_syslog(get_class($this).'::addSubproduct error='.$this->error, LOG_ERR);
4534  return -1;
4535  }
4536  }
4537  // End call triggers
4538 
4539  return 1;
4540  }
4541  } else {
4542  dol_print_error($this->db);
4543  return -1;
4544  }
4545  }
4546  }
4547 
4548  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4559  public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1, $notrigger = 0)
4560  {
4561  global $user;
4562 
4563  // phpcs:enable
4564  // Clean parameters
4565  if (!is_numeric($id_pere)) {
4566  $id_pere = 0;
4567  }
4568  if (!is_numeric($id_fils)) {
4569  $id_fils = 0;
4570  }
4571  if (!is_numeric($incdec)) {
4572  $incdec = 1;
4573  }
4574  if (!is_numeric($qty)) {
4575  $qty = 1;
4576  }
4577 
4578  $sql = 'UPDATE '.$this->db->prefix().'product_association SET ';
4579  $sql .= 'qty = '.price2num($qty, 'MS');
4580  $sql .= ',incdec = '.((int) $incdec);
4581  $sql .= ' WHERE fk_product_pere = '.((int) $id_pere).' AND fk_product_fils = '.((int) $id_fils);
4582 
4583  if (!$this->db->query($sql)) {
4584  dol_print_error($this->db);
4585  return -1;
4586  } else {
4587  if (!$notrigger) {
4588  // Call trigger
4589  $result = $this->call_trigger('PRODUCT_SUBPRODUCT_UPDATE', $user);
4590  if ($result < 0) {
4591  $this->error = $this->db->lasterror();
4592  dol_syslog(get_class($this).'::updateSubproduct error='.$this->error, LOG_ERR);
4593  return -1;
4594  }
4595  // End call triggers
4596  }
4597 
4598  return 1;
4599  }
4600  }
4601 
4602  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4611  public function del_sousproduit($fk_parent, $fk_child, $notrigger = 0)
4612  {
4613  global $user;
4614 
4615  // phpcs:enable
4616  if (!is_numeric($fk_parent)) {
4617  $fk_parent = 0;
4618  }
4619  if (!is_numeric($fk_child)) {
4620  $fk_child = 0;
4621  }
4622 
4623  $sql = "DELETE FROM ".$this->db->prefix()."product_association";
4624  $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
4625  $sql .= " AND fk_product_fils = ".((int) $fk_child);
4626 
4627  dol_syslog(get_class($this).'::del_sousproduit', LOG_DEBUG);
4628  if (!$this->db->query($sql)) {
4629  dol_print_error($this->db);
4630  return -1;
4631  }
4632 
4633  // Updated ranks so that none are missing
4634  $sqlrank = "SELECT rowid, rang FROM ".$this->db->prefix()."product_association";
4635  $sqlrank .= " WHERE fk_product_pere = ".((int) $fk_parent);
4636  $sqlrank .= " ORDER BY rang";
4637  $resqlrank = $this->db->query($sqlrank);
4638  if ($resqlrank) {
4639  $cpt = 0;
4640  while ($objrank = $this->db->fetch_object($resqlrank)) {
4641  $cpt++;
4642  $sql = "UPDATE ".$this->db->prefix()."product_association";
4643  $sql .= " SET rang = ".((int) $cpt);
4644  $sql .= " WHERE rowid = ".((int) $objrank->rowid);
4645  if (! $this->db->query($sql)) {
4646  dol_print_error($this->db);
4647  return -1;
4648  }
4649  }
4650  }
4651 
4652  if (!$notrigger) {
4653  // Call trigger
4654  $result = $this->call_trigger('PRODUCT_SUBPRODUCT_DELETE', $user);
4655  if ($result < 0) {
4656  $this->error = $this->db->lasterror();
4657  dol_syslog(get_class($this).'::delSubproduct error='.$this->error, LOG_ERR);
4658  return -1;
4659  }
4660  // End call triggers
4661  }
4662 
4663  return 1;
4664  }
4665 
4666  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4674  public function is_sousproduit($fk_parent, $fk_child)
4675  {
4676  // phpcs:enable
4677  $sql = "SELECT fk_product_pere, qty, incdec";
4678  $sql .= " FROM ".$this->db->prefix()."product_association";
4679  $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
4680  $sql .= " AND fk_product_fils = ".((int) $fk_child);
4681 
4682  $result = $this->db->query($sql);
4683  if ($result) {
4684  $num = $this->db->num_rows($result);
4685 
4686  if ($num > 0) {
4687  $obj = $this->db->fetch_object($result);
4688 
4689  $this->is_sousproduit_qty = $obj->qty;
4690  $this->is_sousproduit_incdec = $obj->incdec;
4691 
4692  return 1;
4693  } else {
4694  return 0;
4695  }
4696  } else {
4697  dol_print_error($this->db);
4698  return -1;
4699  }
4700  }
4701 
4702 
4703  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4714  public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
4715  {
4716  // phpcs:enable
4717  global $conf;
4718 
4719  $now = dol_now();
4720 
4721  dol_syslog(get_class($this)."::add_fournisseur id_fourn = ".$id_fourn." ref_fourn=".$ref_fourn." quantity=".$quantity, LOG_DEBUG);
4722 
4723  // Clean parameters
4724  $quantity = price2num($quantity, 'MS');
4725 
4726  if ($ref_fourn) {
4727  // Check if ref is not already used
4728  $sql = "SELECT rowid, fk_product";
4729  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
4730  $sql .= " WHERE fk_soc = ".((int) $id_fourn);
4731  $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
4732  $sql .= " AND fk_product <> ".((int) $this->id);
4733  $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
4734 
4735  $resql = $this->db->query($sql);
4736  if ($resql) {
4737  $obj = $this->db->fetch_object($resql);
4738  if ($obj) {
4739  // If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
4740  $this->product_id_already_linked = $obj->fk_product;
4741  return -3;
4742  }
4743  $this->db->free($resql);
4744  }
4745  }
4746 
4747  $sql = "SELECT rowid";
4748  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
4749  $sql .= " WHERE fk_soc = ".((int) $id_fourn);
4750  if ($ref_fourn) {
4751  $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
4752  } else {
4753  $sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
4754  }
4755  $sql .= " AND quantity = ".((float) $quantity);
4756  $sql .= " AND fk_product = ".((int) $this->id);
4757  $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
4758 
4759  $resql = $this->db->query($sql);
4760  if ($resql) {
4761  $obj = $this->db->fetch_object($resql);
4762 
4763  // The reference supplier does not exist, we create it for this product.
4764  if (empty($obj)) {
4765  $sql = "INSERT INTO ".$this->db->prefix()."product_fournisseur_price(";
4766  $sql .= "datec";
4767  $sql .= ", entity";
4768  $sql .= ", fk_product";
4769  $sql .= ", fk_soc";
4770  $sql .= ", ref_fourn";
4771  $sql .= ", quantity";
4772  $sql .= ", fk_user";
4773  $sql .= ", tva_tx";
4774  $sql .= ") VALUES (";
4775  $sql .= "'".$this->db->idate($now)."'";
4776  $sql .= ", ".((int) $conf->entity);
4777  $sql .= ", ".((int) $this->id);
4778  $sql .= ", ".((int) $id_fourn);
4779  $sql .= ", '".$this->db->escape($ref_fourn)."'";
4780  $sql .= ", ".((float) $quantity);
4781  $sql .= ", ".((int) $user->id);
4782  $sql .= ", 0";
4783  $sql .= ")";
4784 
4785  if ($this->db->query($sql)) {
4786  $this->product_fourn_price_id = $this->db->last_insert_id($this->db->prefix()."product_fournisseur_price");
4787  return 1;
4788  } else {
4789  $this->error = $this->db->lasterror();
4790  return -1;
4791  }
4792  } else {
4793  // If the supplier price already exists for this product and quantity
4794  $this->product_fourn_price_id = $obj->rowid;
4795  return 0;
4796  }
4797  } else {
4798  $this->error = $this->db->lasterror();
4799  return -2;
4800  }
4801  }
4802 
4803 
4804  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4810  public function list_suppliers()
4811  {
4812  // phpcs:enable
4813  global $conf;
4814 
4815  $list = array();
4816 
4817  $sql = "SELECT DISTINCT p.fk_soc";
4818  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as p";
4819  $sql .= " WHERE p.fk_product = ".((int) $this->id);
4820  $sql .= " AND p.entity = ".((int) $conf->entity);
4821 
4822  $result = $this->db->query($sql);
4823  if ($result) {
4824  $num = $this->db->num_rows($result);
4825  $i = 0;
4826  while ($i < $num) {
4827  $obj = $this->db->fetch_object($result);
4828  $list[$i] = $obj->fk_soc;
4829  $i++;
4830  }
4831  }
4832 
4833  return $list;
4834  }
4835 
4836  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4844  public function clone_price($fromId, $toId)
4845  {
4846  global $conf, $user;
4847 
4848  $now = dol_now();
4849 
4850  $this->db->begin();
4851 
4852  // prices
4853  $sql = "INSERT INTO ".$this->db->prefix()."product_price (";
4854  $sql .= " entity";
4855  $sql .= ", fk_product";
4856  $sql .= ", date_price";
4857  $sql .= ", price_level";
4858  $sql .= ", price";
4859  $sql .= ", price_ttc";
4860  $sql .= ", price_min";
4861  $sql .= ", price_min_ttc";
4862  $sql .= ", price_base_type";
4863  $sql .= ", price_label";
4864  $sql .= ", default_vat_code";
4865  $sql .= ", tva_tx";
4866  $sql .= ", recuperableonly";
4867  $sql .= ", localtax1_tx";
4868  $sql .= ", localtax1_type";
4869  $sql .= ", localtax2_tx";
4870  $sql .= ", localtax2_type";
4871  $sql .= ", fk_user_author";
4872  $sql .= ", tosell";
4873  $sql .= ", price_by_qty";
4874  $sql .= ", fk_price_expression";
4875  $sql .= ", fk_multicurrency";
4876  $sql .= ", multicurrency_code";
4877  $sql .= ", multicurrency_tx";
4878  $sql .= ", multicurrency_price";
4879  $sql .= ", multicurrency_price_ttc";
4880  $sql .= ")";
4881  $sql .= " SELECT";
4882  $sql .= " entity";
4883  $sql .= ", ".$toId;
4884  $sql .= ", '".$this->db->idate($now)."'";
4885  $sql .= ", price_level";
4886  $sql .= ", price";
4887  $sql .= ", price_ttc";
4888  $sql .= ", price_min";
4889  $sql .= ", price_min_ttc";
4890  $sql .= ", price_base_type";
4891  $sql .= ", price_label";
4892  $sql .= ", default_vat_code";
4893  $sql .= ", tva_tx";
4894  $sql .= ", recuperableonly";
4895  $sql .= ", localtax1_tx";
4896  $sql .= ", localtax1_type";
4897  $sql .= ", localtax2_tx";
4898  $sql .= ", localtax2_type";
4899  $sql .= ", ".$user->id;
4900  $sql .= ", tosell";
4901  $sql .= ", price_by_qty";
4902  $sql .= ", fk_price_expression";
4903  $sql .= ", fk_multicurrency";
4904  $sql .= ", multicurrency_code";
4905  $sql .= ", multicurrency_tx";
4906  $sql .= ", multicurrency_price";
4907  $sql .= ", multicurrency_price_ttc";
4908  $sql .= " FROM ".$this->db->prefix()."product_price ps";
4909  $sql .= " WHERE fk_product = ".((int) $fromId);
4910  $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)";
4911  $sql .= " ORDER BY date_price DESC";
4912 
4913  dol_syslog(__METHOD__, LOG_DEBUG);
4914  $resql = $this->db->query($sql);
4915  if (!$resql) {
4916  $this->db->rollback();
4917  return -1;
4918  }
4919 
4920  $this->db->commit();
4921  return 1;
4922  }
4923 
4924  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4932  public function clone_associations($fromId, $toId)
4933  {
4934  // phpcs:enable
4935  $this->db->begin();
4936 
4937  $sql = 'INSERT INTO '.$this->db->prefix().'product_association (fk_product_pere, fk_product_fils, qty, incdec)';
4938  $sql .= " SELECT ".$toId.", fk_product_fils, qty, incdec FROM ".$this->db->prefix()."product_association";
4939  $sql .= " WHERE fk_product_pere = ".((int) $fromId);
4940 
4941  dol_syslog(get_class($this).'::clone_association', LOG_DEBUG);
4942  if (!$this->db->query($sql)) {
4943  $this->db->rollback();
4944  return -1;
4945  }
4946 
4947  $this->db->commit();
4948  return 1;
4949  }
4950 
4951  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4959  public function clone_fournisseurs($fromId, $toId)
4960  {
4961  // phpcs:enable
4962  $this->db->begin();
4963 
4964  $now = dol_now();
4965 
4966  // les fournisseurs
4967  /*$sql = "INSERT ".$this->db->prefix()."product_fournisseur ("
4968  . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
4969  . " SELECT '".$this->db->idate($now)."', ".$toId.", fk_soc, ref_fourn, fk_user_author"
4970  . " FROM ".$this->db->prefix()."product_fournisseur"
4971  . " WHERE fk_product = ".((int) $fromId);
4972 
4973  if ( ! $this->db->query($sql ) )
4974  {
4975  $this->db->rollback();
4976  return -1;
4977  }*/
4978 
4979  // les prix de fournisseurs.
4980  $sql = "INSERT ".$this->db->prefix()."product_fournisseur_price (";
4981  $sql .= " datec, fk_product, fk_soc, price, quantity, fk_user, tva_tx)";
4982  $sql .= " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, price, quantity, fk_user, tva_tx";
4983  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
4984  $sql .= " WHERE fk_product = ".((int) $fromId);
4985 
4986  dol_syslog(get_class($this).'::clone_fournisseurs', LOG_DEBUG);
4987  $resql = $this->db->query($sql);
4988  if (!$resql) {
4989  $this->db->rollback();
4990  return -1;
4991  } else {
4992  $this->db->commit();
4993  return 1;
4994  }
4995  }
4996 
4997  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5010  public function fetch_prod_arbo($prod, $compl_path = '', $multiply = 1, $level = 1, $id_parent = 0, $ignore_stock_load = 0)
5011  {
5012  // phpcs:enable
5013  global $conf, $langs;
5014 
5015  $tmpproduct = null;
5016  //var_dump($prod);
5017  foreach ($prod as $id_product => $desc_pere) { // $id_product is 0 (first call starting with root top) or an id of a sub_product
5018  if (is_array($desc_pere)) { // If desc_pere is an array, this means it's a child
5019  $id = (!empty($desc_pere[0]) ? $desc_pere[0] : '');
5020  $nb = (!empty($desc_pere[1]) ? $desc_pere[1] : '');
5021  $type = (!empty($desc_pere[2]) ? $desc_pere[2] : '');
5022  $label = (!empty($desc_pere[3]) ? $desc_pere[3] : '');
5023  $incdec = (!empty($desc_pere[4]) ? $desc_pere[4] : 0);
5024 
5025  if ($multiply < 1) {
5026  $multiply = 1;
5027  }
5028 
5029  //print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n";
5030  if (is_null($tmpproduct)) {
5031  $tmpproduct = new Product($this->db); // So we initialize tmpproduct only once for all loop.
5032  }
5033  $tmpproduct->fetch($id); // Load product to get ->ref
5034 
5035  if (empty($ignore_stock_load) && ($tmpproduct->isProduct() || getDolGlobalString('STOCK_SUPPORTS_SERVICES'))) {
5036  $tmpproduct->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
5037  }
5038 
5039  $this->res[] = array(
5040  'id' => $id, // Id product
5041  'id_parent' => $id_parent,
5042  'ref' => $tmpproduct->ref, // Ref product
5043  'nb' => $nb, // Nb of units that compose parent product
5044  'nb_total' => $nb * $multiply, // Nb of units for all nb of product
5045  'stock' => $tmpproduct->stock_reel, // Stock
5046  'stock_alert' => $tmpproduct->seuil_stock_alerte, // Stock alert
5047  'label' => $label,
5048  'fullpath' => $compl_path.$label, // Label
5049  'type' => $type, // Nb of units that compose parent product
5050  'desiredstock' => $tmpproduct->desiredstock,
5051  'level' => $level,
5052  'incdec' => $incdec,
5053  'entity' => $tmpproduct->entity
5054  );
5055 
5056  // Recursive call if there child has children of its own
5057  if (isset($desc_pere['childs']) && is_array($desc_pere['childs'])) {
5058  //print 'YYY We go down for '.$desc_pere[3]." -> \n";
5059  $this->fetch_prod_arbo($desc_pere['childs'], $compl_path.$desc_pere[3]." -> ", $desc_pere[1] * $multiply, $level + 1, $id, $ignore_stock_load);
5060  }
5061  }
5062  }
5063  }
5064 
5065  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5074  public function get_arbo_each_prod($multiply = 1, $ignore_stock_load = 0)
5075  {
5076  // phpcs:enable
5077  $this->res = array();
5078  if (isset($this->sousprods) && is_array($this->sousprods)) {
5079  foreach ($this->sousprods as $prod_name => $desc_product) {
5080  if (is_array($desc_product)) {
5081  $this->fetch_prod_arbo($desc_product, "", $multiply, 1, $this->id, $ignore_stock_load); // This set $this->res
5082  }
5083  }
5084  }
5085  //var_dump($res);
5086  return $this->res;
5087  }
5088 
5096  public function hasFatherOrChild($mode = 0)
5097  {
5098  $nb = 0;
5099 
5100  $sql = "SELECT COUNT(pa.rowid) as nb";
5101  $sql .= " FROM ".$this->db->prefix()."product_association as pa";
5102  if ($mode == 0) {
5103  $sql .= " WHERE pa.fk_product_fils = ".((int) $this->id)." OR pa.fk_product_pere = ".((int) $this->id);
5104  } elseif ($mode == -1) {
5105  $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)
5106  } elseif ($mode == 1) {
5107  $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)
5108  }
5109 
5110  $resql = $this->db->query($sql);
5111  if ($resql) {
5112  $obj = $this->db->fetch_object($resql);
5113  if ($obj) {
5114  $nb = $obj->nb;
5115  }
5116  } else {
5117  return -1;
5118  }
5119 
5120  return $nb;
5121  }
5122 
5128  public function hasVariants()
5129  {
5130  $nb = 0;
5131  $sql = "SELECT count(rowid) as nb FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_parent = ".((int) $this->id);
5132  $sql .= " AND entity IN (".getEntity('product').")";
5133 
5134  $resql = $this->db->query($sql);
5135  if ($resql) {
5136  $obj = $this->db->fetch_object($resql);
5137  if ($obj) {
5138  $nb = $obj->nb;
5139  }
5140  }
5141 
5142  return $nb;
5143  }
5144 
5145 
5151  public function isVariant()
5152  {
5153  global $conf;
5154  if (isModEnabled('variants')) {
5155  $sql = "SELECT rowid FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_child = ".((int) $this->id)." AND entity IN (".getEntity('product').")";
5156 
5157  $query = $this->db->query($sql);
5158 
5159  if ($query) {
5160  if (!$this->db->num_rows($query)) {
5161  return false;
5162  }
5163  return true;
5164  } else {
5165  dol_print_error($this->db);
5166  return -1;
5167  }
5168  } else {
5169  return false;
5170  }
5171  }
5172 
5179  public function getFather()
5180  {
5181  $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";
5182  $sql .= ", p.tosell as status, p.tobuy as status_buy";
5183  $sql .= " FROM ".$this->db->prefix()."product_association as pa,";
5184  $sql .= " ".$this->db->prefix()."product as p";
5185  $sql .= " WHERE p.rowid = pa.fk_product_pere";
5186  $sql .= " AND pa.fk_product_fils = ".((int) $this->id);
5187 
5188  $res = $this->db->query($sql);
5189  if ($res) {
5190  $prods = array();
5191  while ($record = $this->db->fetch_array($res)) {
5192  // $record['id'] = $record['rowid'] = id of father
5193  $prods[$record['id']]['id'] = $record['rowid'];
5194  $prods[$record['id']]['ref'] = $record['ref'];
5195  $prods[$record['id']]['label'] = $record['label'];
5196  $prods[$record['id']]['qty'] = $record['qty'];
5197  $prods[$record['id']]['incdec'] = $record['incdec'];
5198  $prods[$record['id']]['fk_product_type'] = $record['fk_product_type'];
5199  $prods[$record['id']]['entity'] = $record['entity'];
5200  $prods[$record['id']]['status'] = $record['status'];
5201  $prods[$record['id']]['status_buy'] = $record['status_buy'];
5202  }
5203  return $prods;
5204  } else {
5205  dol_print_error($this->db);
5206  return -1;
5207  }
5208  }
5209 
5210 
5220  public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array())
5221  {
5222  global $alreadyfound;
5223 
5224  if (empty($id)) {
5225  return array();
5226  }
5227 
5228  $sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,";
5229  $sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec,";
5230  $sql .= " pa.rowid as fk_association, pa.rang";
5231  $sql .= " FROM ".$this->db->prefix()."product as p,";
5232  $sql .= " ".$this->db->prefix()."product_association as pa";
5233  $sql .= " WHERE p.rowid = pa.fk_product_fils";
5234  $sql .= " AND pa.fk_product_pere = ".((int) $id);
5235  $sql .= " AND pa.fk_product_fils <> ".((int) $id); // This should not happens, it is to avoid infinite loop if it happens
5236  $sql .= " ORDER BY pa.rang";
5237 
5238  dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level. ' parents='.(is_array($parents) ? implode(',', $parents) : $parents), LOG_DEBUG);
5239 
5240  if ($level == 1) {
5241  $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
5242  }
5243  // Protection against infinite loop
5244  if ($level > 30) {
5245  return array();
5246  }
5247 
5248  $res = $this->db->query($sql);
5249  if ($res) {
5250  $prods = array();
5251  while ($rec = $this->db->fetch_array($res)) {
5252  if (!empty($alreadyfound[$rec['rowid']])) {
5253  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);
5254  if (in_array($rec['id'], $parents)) {
5255  continue; // We discard this child if it is already found at a higher level in tree in the same branch.
5256  }
5257  }
5258  $alreadyfound[$rec['rowid']] = 1;
5259  $prods[$rec['rowid']] = array(
5260  0 => $rec['rowid'],
5261  1 => $rec['qty'],
5262  2 => $rec['fk_product_type'],
5263  3 => $this->db->escape($rec['label']),
5264  4 => $rec['incdec'],
5265  5 => $rec['ref'],
5266  6 => $rec['fk_association'],
5267  7 => $rec['rang']
5268  );
5269  //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
5270  //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
5271  if (empty($firstlevelonly)) {
5272  $parents[] = $rec['rowid'];
5273  $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, $parents);
5274  foreach ($listofchilds as $keyChild => $valueChild) {
5275  $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
5276  }
5277  }
5278  }
5279 
5280  return $prods;
5281  } else {
5282  dol_print_error($this->db);
5283  return -1;
5284  }
5285  }
5286 
5287  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5294  public function get_sousproduits_arbo()
5295  {
5296  // phpcs:enable
5297  $parent = array();
5298 
5299  foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) { // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
5300  $parent[$this->label][$keyChild] = $valueChild;
5301  }
5302  foreach ($parent as $key => $value) { // key=label, value is array of children
5303  $this->sousprods[$key] = $value;
5304  }
5305  }
5306 
5313  public function getTooltipContentArray($params)
5314  {
5315  global $conf, $langs, $user;
5316 
5317  $langs->loadLangs(array('products', 'other'));
5318 
5319  $datas = array();
5320  $nofetch = !empty($params['nofetch']);
5321 
5322  if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5323  return ['optimize' => $langs->trans("ShowProduct")];
5324  }
5325 
5326  if (!empty($this->entity)) {
5327  $tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80, 0, 0, 0, 0, 1);
5328  if ($this->nbphoto > 0) {
5329  $datas['photo'] = '<div class="photointooltip floatright">'."\n" . $tmpphoto . '</div>';
5330  }
5331  }
5332 
5333  if ($this->type == Product::TYPE_PRODUCT) {
5334  $datas['picto'] = img_picto('', 'product').' <u class="paddingrightonly">'.$langs->trans("Product").'</u>';
5335  } elseif ($this->type == Product::TYPE_SERVICE) {
5336  $datas['picto'] = img_picto('', 'service').' <u class="paddingrightonly">'.$langs->trans("Service").'</u>';
5337  }
5338  if (isset($this->status) && isset($this->status_buy)) {
5339  $datas['status'] = ' '.$this->getLibStatut(5, 0) . ' '.$this->getLibStatut(5, 1);
5340  }
5341 
5342  if (!empty($this->ref)) {
5343  $datas['ref'] = '<br><b>'.$langs->trans('ProductRef').':</b> '.$this->ref;
5344  }
5345  if (!empty($this->label)) {
5346  $datas['label'] = '<br><b>'.$langs->trans('ProductLabel').':</b> '.$this->label;
5347  }
5348  if (!empty($this->description)) {
5349  $datas['description'] = '<br><b>'.$langs->trans('ProductDescription').':</b> '.dolGetFirstLineOfText($this->description, 5);
5350  }
5351  if ($this->type == Product::TYPE_PRODUCT || getDolGlobalString('STOCK_SUPPORTS_SERVICES')) {
5352  if (isModEnabled('productbatch')) {
5353  $langs->load("productbatch");
5354  $datas['batchstatus'] = "<br><b>".$langs->trans("ManageLotSerial").'</b>: '.$this->getLibStatut(0, 2);
5355  }
5356  }
5357  if (isModEnabled('barcode')) {
5358  $datas['barcode'] = '<br><b>'.$langs->trans('BarCode').':</b> '.$this->barcode;
5359  }
5360 
5361  if ($this->type == Product::TYPE_PRODUCT) {
5362  if ($this->weight) {
5363  $datas['weight'] = "<br><b>".$langs->trans("Weight").'</b>: '.$this->weight.' '.measuringUnitString(0, "weight", $this->weight_units);
5364  }
5365  $labelsize = "";
5366  if ($this->length) {
5367  $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Length").'</b>: '.$this->length.' '.measuringUnitString(0, 'size', $this->length_units);
5368  }
5369  if ($this->width) {
5370  $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Width").'</b>: '.$this->width.' '.measuringUnitString(0, 'size', $this->width_units);
5371  }
5372  if ($this->height) {
5373  $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Height").'</b>: '.$this->height.' '.measuringUnitString(0, 'size', $this->height_units);
5374  }
5375  if ($labelsize) {
5376  $datas['size'] = "<br>".$labelsize;
5377  }
5378 
5379  $labelsurfacevolume = "";
5380  if ($this->surface) {
5381  $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Surface").'</b>: '.$this->surface.' '.measuringUnitString(0, 'surface', $this->surface_units);
5382  }
5383  if ($this->volume) {
5384  $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Volume").'</b>: '.$this->volume.' '.measuringUnitString(0, 'volume', $this->volume_units);
5385  }
5386  if ($labelsurfacevolume) {
5387  $datas['surface'] = "<br>" . $labelsurfacevolume;
5388  }
5389  }
5390  if ($this->type == Product::TYPE_SERVICE && !empty($this->duration_value)) {
5391  // Duration
5392  $datas['duration'] = '<br><b>'.$langs->trans("Duration").':</b> '.$this->duration_value;
5393  if ($this->duration_value > 1) {
5394  $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"));
5395  } elseif ($this->duration_value > 0) {
5396  $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"));
5397  }
5398  $datas['duration'] .= (!empty($this->duration_unit) && isset($dur[$this->duration_unit]) ? "&nbsp;".$langs->trans($dur[$this->duration_unit]) : '');
5399  }
5400  if (empty($user->socid)) {
5401  if (!empty($this->pmp) && $this->pmp) {
5402  $datas['pmp'] = "<br><b>".$langs->trans("PMPValue").'</b>: '.price($this->pmp, 0, '', 1, -1, -1, $conf->currency);
5403  }
5404 
5405  if (isModEnabled('accounting')) {
5406  if ($this->status && isset($this->accountancy_code_sell)) {
5407  include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5408  $selllabel = '<br>';
5409  $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellCode').':</b> '.length_accountg($this->accountancy_code_sell);
5410  $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellIntraCode').':</b> '.length_accountg($this->accountancy_code_sell_intra);
5411  $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellExportCode').':</b> '.length_accountg($this->accountancy_code_sell_export);
5412  $datas['accountancysell'] = $selllabel;
5413  }
5414  if ($this->status_buy && isset($this->accountancy_code_buy)) {
5415  include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5416  $buylabel = '';
5417  if (empty($this->status)) {
5418  $buylabel .= '<br>';
5419  }
5420  $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyCode').':</b> '.length_accountg($this->accountancy_code_buy);
5421  $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyIntraCode').':</b> '.length_accountg($this->accountancy_code_buy_intra);
5422  $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyExportCode').':</b> '.length_accountg($this->accountancy_code_buy_export);
5423  $datas['accountancybuy'] = $buylabel;
5424  }
5425  }
5426  }
5427  // show categories for this record only in ajax to not overload lists
5428  if (isModEnabled('category') && !$nofetch) {
5429  require_once DOL_DOCUMENT_ROOT . '/categories/class/categorie.class.php';
5430  $form = new Form($this->db);
5431  $datas['categories'] = '<br>' . $form->showCategories($this->id, Categorie::TYPE_PRODUCT, 1);
5432  }
5433 
5434  return $datas;
5435  }
5436 
5450  public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0, $morecss = '', $add_label = 0, $sep = ' - ')
5451  {
5452  global $conf, $langs, $hookmanager, $user;
5453  include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
5454 
5455  $result = '';
5456 
5457  $newref = $this->ref;
5458  if ($maxlength) {
5459  $newref = dol_trunc($newref, $maxlength, 'middle');
5460  }
5461  $params = [
5462  'id' => $this->id,
5463  'objecttype' => (isset($this->type) ? ($this->type == 1 ? 'service' : 'product') : $this->element),
5464  'option' => $option,
5465  'nofetch' => 1,
5466  ];
5467  $classfortooltip = 'classfortooltip';
5468  $dataparams = '';
5469  if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
5470  $classfortooltip = 'classforajaxtooltip';
5471  $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
5472  $label = '';
5473  } else {
5474  $label = implode($this->getTooltipContentArray($params));
5475  }
5476 
5477  $linkclose = '';
5478  if (empty($notooltip)) {
5479  if (getDolGlobalString('MAIN_OPTIMIZEFORTEXTBROWSER')) {
5480  $label = $langs->trans("ShowProduct");
5481  $linkclose .= ' alt="'.dol_escape_htmltag($label, 1, 1).'"';
5482  }
5483  $linkclose .= ($label ? ' title="'.dol_escape_htmltag($label, 1, 1).'"' : ' title="tocomplete"');
5484  $linkclose .= $dataparams.' class="nowraponall '.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
5485  } else {
5486  $linkclose = ' class="nowraponall'.($morecss ? ' '.$morecss : '').'"';
5487  }
5488 
5489  if ($option == 'supplier' || $option == 'category') {
5490  $url = DOL_URL_ROOT.'/product/price_suppliers.php?id='.$this->id;
5491  } elseif ($option == 'stock') {
5492  $url = DOL_URL_ROOT.'/product/stock/product.php?id='.$this->id;
5493  } elseif ($option == 'composition') {
5494  $url = DOL_URL_ROOT.'/product/composition/card.php?id='.$this->id;
5495  } else {
5496  $url = DOL_URL_ROOT.'/product/card.php?id='.$this->id;
5497  }
5498 
5499  if ($option !== 'nolink') {
5500  // Add param to save lastsearch_values or not
5501  $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
5502  if ($save_lastsearch_value == -1 && isset($_SERVER["PHP_SELF"]) && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
5503  $add_save_lastsearch_values = 1;
5504  }
5505  if ($add_save_lastsearch_values) {
5506  $url .= '&save_lastsearch_values=1';
5507  }
5508  }
5509 
5510  $linkstart = '<a href="'.$url.'"';
5511  $linkstart .= $linkclose.'>';
5512  $linkend = '</a>';
5513 
5514  $result .= $linkstart;
5515  if ($withpicto) {
5516  if ($this->type == Product::TYPE_PRODUCT) {
5517  $result .= (img_object(($notooltip ? '' : $label), 'product', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5518  }
5519  if ($this->type == Product::TYPE_SERVICE) {
5520  $result .= (img_object(($notooltip ? '' : $label), 'service', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5521  }
5522  }
5523  $result .= '<span class="aaa">'.dol_escape_htmltag($newref).'</span>';
5524  $result .= $linkend;
5525  if ($withpicto != 2) {
5526  $result .= (($add_label && $this->label) ? $sep.dol_trunc($this->label, ($add_label > 1 ? $add_label : 0)) : '');
5527  }
5528 
5529  global $action;
5530  $hookmanager->initHooks(array('productdao'));
5531  $parameters = array('id' => $this->id, 'getnomurl' => &$result, 'label' => &$label);
5532  $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
5533  if ($reshook > 0) {
5534  $result = $hookmanager->resPrint;
5535  } else {
5536  $result .= $hookmanager->resPrint;
5537  }
5538 
5539  return $result;
5540  }
5541 
5542 
5553  public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
5554  {
5555  global $conf, $user, $langs;
5556 
5557  $langs->load("products");
5558  $outputlangs->load("products");
5559 
5560  // Positionne le modele sur le nom du modele a utiliser
5561  if (!dol_strlen($modele)) {
5562  $modele = getDolGlobalString('PRODUCT_ADDON_PDF', 'strato');
5563  }
5564 
5565  $modelpath = "core/modules/product/doc/";
5566 
5567  return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
5568  }
5569 
5577  public function getLibStatut($mode = 0, $type = 0)
5578  {
5579  switch ($type) {
5580  case 0:
5581  return $this->LibStatut($this->status, $mode, $type);
5582  case 1:
5583  return $this->LibStatut($this->status_buy, $mode, $type);
5584  case 2:
5585  return $this->LibStatut($this->status_batch, $mode, $type);
5586  default:
5587  //Simulate previous behavior but should return an error string
5588  return $this->LibStatut($this->status_buy, $mode, $type);
5589  }
5590  }
5591 
5592  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5601  public function LibStatut($status, $mode = 0, $type = 0)
5602  {
5603  // phpcs:enable
5604  global $conf, $langs;
5605 
5606  $labelStatus = $labelStatusShort = '';
5607 
5608  $langs->load('products');
5609  if (isModEnabled('productbatch')) {
5610  $langs->load("productbatch");
5611  }
5612 
5613  if ($type == 2) {
5614  switch ($mode) {
5615  case 0:
5616  $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatch') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial')));
5617  return dolGetStatus($label);
5618  case 1:
5619  $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort')));
5620  return dolGetStatus($label);
5621  case 2:
5622  return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 1, 2);
5623  case 3:
5624  return dolGetStatus($langs->transnoentitiesnoconv('ProductStatusNotOnBatch'), '', '', empty($status) ? 'status5' : 'status4', 3, 'dot');
5625  case 4:
5626  return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 0, 2);
5627  case 5:
5628  return $this->LibStatut($status, 1, 2).' '.$this->LibStatut($status, 3, 2);
5629  default:
5630  return dolGetStatus($langs->transnoentitiesnoconv('Unknown'));
5631  }
5632  }
5633 
5634  $statuttrans = empty($status) ? 'status5' : 'status4';
5635 
5636  if ($status == 0) {
5637  // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
5638  if ($type == 0) {
5639  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnSellShort');
5640  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnSell');
5641  } elseif ($type == 1) {
5642  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBuyShort');
5643  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBuy');
5644  } elseif ($type == 2) {
5645  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBatch');
5646  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort');
5647  }
5648  } elseif ($status == 1) {
5649  // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
5650  if ($type == 0) {
5651  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSellShort');
5652  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSell');
5653  } elseif ($type == 1) {
5654  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBuyShort');
5655  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBuy');
5656  } elseif ($type == 2) {
5657  $labelStatus = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial'));
5658  $labelStatusShort = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort'));
5659  }
5660  } elseif ($type == 2 && $status == 2) {
5661  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSerial');
5662  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSerialShort');
5663  }
5664 
5665  if ($mode > 6) {
5666  return dolGetStatus($langs->transnoentitiesnoconv('Unknown'), '', '', 'status0', 0);
5667  } else {
5668  return dolGetStatus($labelStatus, $labelStatusShort, '', $statuttrans, $mode);
5669  }
5670  }
5671 
5672 
5678  public function getLibFinished()
5679  {
5680  global $langs;
5681  $langs->load('products');
5682  $label = '';
5683 
5684  if (isset($this->finished) && $this->finished >= 0) {
5685  $sql = "SELECT label, code FROM ".$this->db->prefix()."c_product_nature where code = ".((int) $this->finished)." AND active=1";
5686  $resql = $this->db->query($sql);
5687  if (!$resql) {
5688  $this->error = $this->db->error().' sql='.$sql;
5689  dol_syslog(__METHOD__.' Error '.$this->error, LOG_ERR);
5690  return -1;
5691  } elseif ($this->db->num_rows($resql) > 0 && $res = $this->db->fetch_array($resql)) {
5692  $label = $langs->trans($res['label']);
5693  }
5694  $this->db->free($resql);
5695  }
5696 
5697  return $label;
5698  }
5699 
5700 
5701  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5718  public function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null)
5719  {
5720  // phpcs:enable
5721  if ($id_entrepot) {
5722  $this->db->begin();
5723 
5724  include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
5725 
5726  if ($nbpiece < 0) {
5727  if (!$movement) {
5728  $movement = 1;
5729  }
5730  $nbpiece = abs($nbpiece);
5731  }
5732  $op = array();
5733  $op[0] = "+".trim((string) $nbpiece);
5734  $op[1] = "-".trim((string) $nbpiece);
5735 
5736  $movementstock = new MouvementStock($this->db);
5737  $movementstock->setOrigin($origin_element, $origin_id); // Set ->origin_type and ->origin_id
5738  $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', '', '', '', false, 0, $disablestockchangeforsubproduct);
5739 
5740  if ($result >= 0) {
5741  if ($extrafields) {
5742  $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
5743  $movementstock->array_options = $array_options;
5744  $movementstock->insertExtraFields();
5745  }
5746  $this->db->commit();
5747  return 1;
5748  } else {
5749  $this->error = $movementstock->error;
5750  $this->errors = $movementstock->errors;
5751 
5752  $this->db->rollback();
5753  return -1;
5754  }
5755  }
5756 
5757  return -1;
5758  }
5759 
5760  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5781  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)
5782  {
5783  // phpcs:enable
5784  if ($id_entrepot) {
5785  $this->db->begin();
5786 
5787  include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
5788 
5789  if ($nbpiece < 0) {
5790  if (!$movement) {
5791  $movement = 1;
5792  }
5793  $nbpiece = abs($nbpiece);
5794  }
5795 
5796  $op = array();
5797  $op[0] = "+".trim((string) $nbpiece);
5798  $op[1] = "-".trim((string) $nbpiece);
5799 
5800  $movementstock = new MouvementStock($this->db);
5801  $movementstock->setOrigin($origin_element, $origin_id); // Set ->origin_type and ->fk_origin
5802  $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', $dlc, $dluo, $lot, false, 0, $disablestockchangeforsubproduct, 0, $force_update_batch);
5803 
5804  if ($result >= 0) {
5805  if ($extrafields) {
5806  $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
5807  $movementstock->array_options = $array_options;
5808  $movementstock->insertExtraFields();
5809  }
5810  $this->db->commit();
5811  return 1;
5812  } else {
5813  $this->error = $movementstock->error;
5814  $this->errors = $movementstock->errors;
5815 
5816  $this->db->rollback();
5817  return -1;
5818  }
5819  }
5820  return -1;
5821  }
5822 
5823  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5836  public function load_stock($option = '', $includedraftpoforvirtual = null, $dateofvirtualstock = null)
5837  {
5838  // phpcs:enable
5839  global $conf;
5840 
5841  $this->stock_reel = 0;
5842  $this->stock_warehouse = array();
5843  $this->stock_theorique = 0;
5844 
5845  // Set filter on warehouse status
5846  $warehouseStatus = array();
5847  if (preg_match('/warehouseclosed/', $option)) {
5849  }
5850  if (preg_match('/warehouseopen/', $option)) {
5852  }
5853  if (preg_match('/warehouseinternal/', $option)) {
5854  if (getDolGlobalString('ENTREPOT_EXTRA_STATUS')) {
5856  } else {
5858  }
5859  }
5860 
5861  $sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot";
5862  $sql .= " FROM ".$this->db->prefix()."product_stock as ps";
5863  $sql .= ", ".$this->db->prefix()."entrepot as w";
5864  $sql .= " WHERE w.entity IN (".getEntity('stock').")";
5865  $sql .= " AND w.rowid = ps.fk_entrepot";
5866  $sql .= " AND ps.fk_product = ".((int) $this->id);
5867  if (count($warehouseStatus)) {
5868  $sql .= " AND w.statut IN (".$this->db->sanitize(implode(',', $warehouseStatus)).")";
5869  }
5870 
5871  $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;
5872 
5873  dol_syslog(get_class($this)."::load_stock", LOG_DEBUG);
5874  $result = $this->db->query($sql);
5875  if ($result) {
5876  $num = $this->db->num_rows($result);
5877  $i = 0;
5878  if ($num > 0) {
5879  while ($i < $num) {
5880  $row = $this->db->fetch_object($result);
5881  $this->stock_warehouse[$row->fk_entrepot] = new stdClass();
5882  $this->stock_warehouse[$row->fk_entrepot]->real = $row->reel;
5883  $this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid;
5884  if ((!preg_match('/nobatch/', $option)) && $this->hasbatch()) {
5885  $this->stock_warehouse[$row->fk_entrepot]->detail_batch = Productbatch::findAll($this->db, $row->rowid, 1, $this->id);
5886  }
5887  $this->stock_reel += $row->reel;
5888  $i++;
5889  }
5890  }
5891  $this->db->free($result);
5892 
5893  if (!preg_match('/novirtual/', $option)) {
5894  $this->load_virtual_stock($includedraftpoforvirtual, $dateofvirtualstock); // This load stock_theorique and also load all arrays stats_xxx...
5895  }
5896 
5897  return 1;
5898  } else {
5899  $this->error = $this->db->lasterror();
5900  return -1;
5901  }
5902  }
5903 
5904 
5905  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5915  public function load_virtual_stock($includedraftpoforvirtual = null, $dateofvirtualstock = null)
5916  {
5917  // phpcs:enable
5918  global $conf, $hookmanager, $action;
5919 
5920  $stock_commande_client = 0;
5921  $stock_commande_fournisseur = 0;
5922  $stock_sending_client = 0;
5923  $stock_reception_fournisseur = 0;
5924  $stock_inproduction = 0;
5925 
5926  //dol_syslog("load_virtual_stock");
5927 
5928  if (isModEnabled('order')) {
5929  $result = $this->load_stats_commande(0, '1,2', 1);
5930  if ($result < 0) {
5931  dol_print_error($this->db, $this->error);
5932  }
5933  $stock_commande_client = $this->stats_commande['qty'];
5934  }
5935  if (isModEnabled("shipping")) {
5936  require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
5937  $filterShipmentStatus = '';
5938  if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT')) {
5939  $filterShipmentStatus = Expedition::STATUS_VALIDATED.','.Expedition::STATUS_CLOSED;
5940  } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
5941  $filterShipmentStatus = Expedition::STATUS_CLOSED;
5942  }
5943  $result = $this->load_stats_sending(0, '1,2', 1, $filterShipmentStatus);
5944  if ($result < 0) {
5945  dol_print_error($this->db, $this->error);
5946  }
5947  $stock_sending_client = $this->stats_expedition['qty'];
5948  }
5949  if (isModEnabled("supplier_order")) {
5950  $filterStatus = getDolGlobalString('SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK', '3,4');
5951  if (isset($includedraftpoforvirtual)) {
5952  $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
5953  }
5954  $result = $this->load_stats_commande_fournisseur(0, $filterStatus, 1, $dateofvirtualstock);
5955  if ($result < 0) {
5956  dol_print_error($this->db, $this->error);
5957  }
5958  $stock_commande_fournisseur = $this->stats_commande_fournisseur['qty'];
5959  }
5960  if ((isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) && empty($conf->reception->enabled)) {
5961  // Case module reception is not used
5962  $filterStatus = '4';
5963  if (isset($includedraftpoforvirtual)) {
5964  $filterStatus = '0,'.$filterStatus;
5965  }
5966  $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock);
5967  if ($result < 0) {
5968  dol_print_error($this->db, $this->error);
5969  }
5970  $stock_reception_fournisseur = $this->stats_reception['qty'];
5971  }
5972  if ((isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) && isModEnabled("reception")) {
5973  // Case module reception is used
5974  $filterStatus = '4';
5975  if (isset($includedraftpoforvirtual)) {
5976  $filterStatus = '0,'.$filterStatus;
5977  }
5978  $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock); // Use same tables than when module reception is not used.
5979  if ($result < 0) {
5980  dol_print_error($this->db, $this->error);
5981  }
5982  $stock_reception_fournisseur = $this->stats_reception['qty'];
5983  }
5984  if (isModEnabled('mrp')) {
5985  $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock);
5986  if ($result < 0) {
5987  dol_print_error($this->db, $this->error);
5988  }
5989  $stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty'];
5990  }
5991 
5992  $this->stock_theorique = $this->stock_reel + $stock_inproduction;
5993 
5994  // Stock decrease mode
5995  if (getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT') || getDolGlobalString('STOCK_CALCULATE_ON_SHIPMENT_CLOSE')) {
5996  $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
5997  } elseif (getDolGlobalString('STOCK_CALCULATE_ON_VALIDATE_ORDER')) {
5998  $this->stock_theorique += 0;
5999  } elseif (getDolGlobalString('STOCK_CALCULATE_ON_BILL')) {
6000  $this->stock_theorique -= $stock_commande_client;
6001  }
6002  // Stock Increase mode
6003  if (getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION') || getDolGlobalString('STOCK_CALCULATE_ON_RECEPTION_CLOSE')) {
6004  $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6005  } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_DISPATCH_ORDER')) {
6006  $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6007  } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER')) {
6008  $this->stock_theorique -= $stock_reception_fournisseur;
6009  } elseif (getDolGlobalString('STOCK_CALCULATE_ON_SUPPLIER_BILL')) {
6010  $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
6011  }
6012 
6013  $parameters = array('id' => $this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual);
6014  // Note that $action and $object may have been modified by some hooks
6015  $reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action);
6016  if ($reshook > 0) {
6017  $this->stock_theorique = $hookmanager->resArray['stock_theorique'];
6018  } elseif ($reshook == 0 && isset($hookmanager->resArray['stock_stats_hook'])) {
6019  $this->stock_theorique += $hookmanager->resArray['stock_stats_hook'];
6020  }
6021 
6022  //Virtual Stock by Warehouse
6023  if (!empty($this->stock_warehouse) && getDolGlobalString('STOCK_ALLOW_VIRTUAL_STOCK_PER_WAREHOUSE')) {
6024  foreach ($this->stock_warehouse as $warehouseid => $stockwarehouse) {
6025  if (isModEnabled('mrp')) {
6026  $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock, $warehouseid);
6027  if ($result < 0) {
6028  dol_print_error($this->db, $this->error);
6029  }
6030  }
6031 
6032  if ($this->fk_default_warehouse == $warehouseid) {
6033  $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']);
6034  } else {
6035  $this->stock_warehouse[$warehouseid]->virtual = $this->stock_warehouse[$warehouseid]->real + $this->stock_warehouse[$warehouseid]->stats_mrptoproduce['qty'];
6036  }
6037  }
6038  }
6039 
6040  return 1;
6041  }
6042 
6043 
6051  public function loadBatchInfo($batch)
6052  {
6053  $result = array();
6054 
6055  $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";
6056  $sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = ".((int) $this->id)." AND pb.batch = '".$this->db->escape($batch)."'";
6057  $sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby";
6058  dol_syslog(get_class($this)."::loadBatchInfo load first entry found for lot/serial = ".$batch, LOG_DEBUG);
6059  $resql = $this->db->query($sql);
6060  if ($resql) {
6061  $num = $this->db->num_rows($resql);
6062  $i = 0;
6063  while ($i < $num) {
6064  $obj = $this->db->fetch_object($resql);
6065  $result[] = array('batch' => $batch, 'eatby' => $this->db->jdate($obj->eatby), 'sellby' => $this->db->jdate($obj->sellby), 'qty' => $obj->qty);
6066  $i++;
6067  }
6068  return $result;
6069  } else {
6070  dol_print_error($this->db);
6071  $this->db->rollback();
6072  return array();
6073  }
6074  }
6075 
6076  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6084  public function add_photo($sdir, $file)
6085  {
6086  // phpcs:enable
6087  global $conf;
6088 
6089  include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6090 
6091  $result = 0;
6092 
6093  $dir = $sdir;
6094  if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6095  $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos";
6096  } else {
6097  $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product').dol_sanitizeFileName($this->ref);
6098  }
6099 
6100  dol_mkdir($dir);
6101 
6102  $dir_osencoded = $dir;
6103 
6104  if (is_dir($dir_osencoded)) {
6105  $originImage = $dir.'/'.$file['name'];
6106 
6107  // Cree fichier en taille origine
6108  $result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
6109 
6110  if (file_exists(dol_osencode($originImage))) {
6111  // Create thumbs
6112  $this->addThumbs($originImage);
6113  }
6114  }
6115 
6116  if (is_numeric($result) && $result > 0) {
6117  return 1;
6118  } else {
6119  return -1;
6120  }
6121  }
6122 
6123  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6130  public function is_photo_available($sdir)
6131  {
6132  // phpcs:enable
6133  include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6134  include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6135 
6136  global $conf;
6137 
6138  $dir = $sdir;
6139  if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
6140  $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos/";
6141  } else {
6142  $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product');
6143  }
6144 
6145  $nbphoto = 0;
6146 
6147  $dir_osencoded = dol_osencode($dir);
6148  if (file_exists($dir_osencoded)) {
6149  $handle = opendir($dir_osencoded);
6150  if (is_resource($handle)) {
6151  while (($file = readdir($handle)) !== false) {
6152  if (!utf8_check($file)) {
6153  $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // To be sure data is stored in UTF8 in memory
6154  }
6155  if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6156  return true;
6157  }
6158  }
6159  }
6160  }
6161  return false;
6162  }
6163 
6164  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6172  public function liste_photos($dir, $nbmax = 0)
6173  {
6174  // phpcs:enable
6175  include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6176  include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6177 
6178  $nbphoto = 0;
6179  $tabobj = array();
6180 
6181  $dir_osencoded = dol_osencode($dir);
6182  $handle = @opendir($dir_osencoded);
6183  if (is_resource($handle)) {
6184  while (($file = readdir($handle)) !== false) {
6185  if (!utf8_check($file)) {
6186  $file = mb_convert_encoding($file, 'UTF-8', 'ISO-8859-1'); // readdir returns ISO
6187  }
6188  if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
6189  $nbphoto++;
6190 
6191  // We forge name of thumb.
6192  $photo = $file;
6193  $photo_vignette = '';
6194  $regs = array();
6195  if (preg_match('/('.$this->regeximgext.')$/i', $photo, $regs)) {
6196  $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $photo).'_small'.$regs[0];
6197  }
6198 
6199  $dirthumb = $dir.'thumbs/';
6200 
6201  // Object
6202  $obj = array();
6203  $obj['photo'] = $photo;
6204  if ($photo_vignette && dol_is_file($dirthumb.$photo_vignette)) {
6205  $obj['photo_vignette'] = 'thumbs/'.$photo_vignette;
6206  } else {
6207  $obj['photo_vignette'] = "";
6208  }
6209 
6210  $tabobj[$nbphoto - 1] = $obj;
6211 
6212  // Do we have to continue with next photo ?
6213  if ($nbmax && $nbphoto >= $nbmax) {
6214  break;
6215  }
6216  }
6217  }
6218 
6219  closedir($handle);
6220  }
6221 
6222  return $tabobj;
6223  }
6224 
6225  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6232  public function delete_photo($file)
6233  {
6234  // phpcs:enable
6235  include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
6236  include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
6237 
6238  $dir = dirname($file).'/'; // Chemin du dossier contenant l'image d'origine
6239  $dirthumb = $dir.'/thumbs/'; // Chemin du dossier contenant la vignette
6240  $filename = preg_replace('/'.preg_quote($dir, '/').'/i', '', $file); // Nom du fichier
6241 
6242  // On efface l'image d'origine
6243  dol_delete_file($file, 0, 0, 0, $this); // For triggers
6244 
6245  // Si elle existe, on efface la vignette
6246  if (preg_match('/('.$this->regeximgext.')$/i', $filename, $regs)) {
6247  $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_small'.$regs[0];
6248  if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6249  dol_delete_file($dirthumb.$photo_vignette);
6250  }
6251 
6252  $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_mini'.$regs[0];
6253  if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
6254  dol_delete_file($dirthumb.$photo_vignette);
6255  }
6256  }
6257  }
6258 
6259  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6266  public function get_image_size($file)
6267  {
6268  // phpcs:enable
6269  $file_osencoded = dol_osencode($file);
6270  $infoImg = getimagesize($file_osencoded); // Get information on image
6271  $this->imgWidth = $infoImg[0]; // Largeur de l'image
6272  $this->imgHeight = $infoImg[1]; // Hauteur de l'image
6273  }
6274 
6280  public function loadStateBoard()
6281  {
6282  global $hookmanager;
6283 
6284  $this->nb = array();
6285 
6286  $sql = "SELECT count(p.rowid) as nb, fk_product_type";
6287  $sql .= " FROM ".$this->db->prefix()."product as p";
6288  $sql .= ' WHERE p.entity IN ('.getEntity($this->element, 1).')';
6289  // Add where from hooks
6290  if (is_object($hookmanager)) {
6291  $parameters = array();
6292  $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
6293  $sql .= $hookmanager->resPrint;
6294  }
6295  $sql .= ' GROUP BY fk_product_type';
6296 
6297  $resql = $this->db->query($sql);
6298  if ($resql) {
6299  while ($obj = $this->db->fetch_object($resql)) {
6300  if ($obj->fk_product_type == 1) {
6301  $this->nb["services"] = $obj->nb;
6302  } else {
6303  $this->nb["products"] = $obj->nb;
6304  }
6305  }
6306  $this->db->free($resql);
6307  return 1;
6308  } else {
6309  dol_print_error($this->db);
6310  $this->error = $this->db->error();
6311  return -1;
6312  }
6313  }
6314 
6320  public function isProduct()
6321  {
6322  return ($this->type == Product::TYPE_PRODUCT ? true : false);
6323  }
6324 
6330  public function isService()
6331  {
6332  return ($this->type == Product::TYPE_SERVICE ? true : false);
6333  }
6334 
6340  public function isStockManaged()
6341  {
6342  return ($this->isProduct() || getDolGlobalString('STOCK_SUPPORTS_SERVICES'));
6343  }
6344 
6350  public function isMandatoryPeriod()
6351  {
6352  return ($this->mandatory_period == 1 ? true : false);
6353  }
6354 
6360  public function hasbatch()
6361  {
6362  return ($this->status_batch > 0 ? true : false);
6363  }
6364 
6365 
6366  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6375  public function get_barcode($object, $type = '')
6376  {
6377  // phpcs:enable
6378  global $conf;
6379 
6380  $result = '';
6381  if (getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) {
6382  $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
6383  foreach ($dirsociete as $dirroot) {
6384  $res = dol_include_once($dirroot . getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM').'.php');
6385  if ($res) {
6386  break;
6387  }
6388  }
6389  $var = getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM');
6390  $mod = new $var();
6391  '@phan-var-force ModeleNumRefBarCode $module';
6392 
6393  $result = $mod->getNextValue($object, $type);
6394 
6395  dol_syslog(get_class($this)."::get_barcode barcode=".$result." module=".$var);
6396  }
6397  return $result;
6398  }
6399 
6407  public function initAsSpecimen()
6408  {
6409  $now = dol_now();
6410 
6411  // Initialize parameters
6412  $this->specimen = 1;
6413  $this->id = 0;
6414  $this->ref = 'PRODUCT_SPEC';
6415  $this->label = 'PRODUCT SPECIMEN';
6416  $this->description = 'This is description of this product specimen that was created the '.dol_print_date($now, 'dayhourlog').'.';
6417  $this->specimen = 1;
6418  $this->country_id = 1;
6419  $this->status = 1;
6420  $this->status_buy = 1;
6421  $this->tobatch = 0;
6422