dolibarr  19.0.0-dev
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-2018 Ferran Marcet <fmarcet@2byte.es>
16  * Copyright (C) 2017 Gustavo Novaro
17  * Copyright (C) 2019-2023 Frédéric France <frederic.france@netlogic.fr>
18  *
19  * This program is free software; you can redistribute it and/or modify
20  * it under the terms of the GNU General Public License as published by
21  * the Free Software Foundation; either version 3 of the License, or
22  * (at your option) any later version.
23  *
24  * This program is distributed in the hope that it will be useful,
25  * but WITHOUT ANY WARRANTY; without even the implied warranty of
26  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27  * GNU General Public License for more details.
28  *
29  * You should have received a copy of the GNU General Public License
30  * along with this program. If not, see <https://www.gnu.org/licenses/>.
31  */
32 
38 require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
39 require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
40 require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php';
41 require_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php';
42 
46 class Product extends CommonObject
47 {
51  public $element = 'product';
52 
56  public $table_element = 'product';
57 
61  public $fk_element = 'fk_product';
62 
66  protected $childtables = array(
67  'supplier_proposaldet' => array('name' => 'SupplierProposal', 'parent' => 'supplier_proposal', 'parentkey' => 'fk_supplier_proposal'),
68  'propaldet' => array('name' => 'Proposal', 'parent' => 'propal', 'parentkey' => 'fk_propal'),
69  'commandedet' => array('name' => 'Order', 'parent' => 'commande', 'parentkey' => 'fk_commande'),
70  'facturedet' => array('name' => 'Invoice', 'parent' => 'facture', 'parentkey' => 'fk_facture'),
71  'contratdet' => array('name' => 'Contract', 'parent' => 'contrat', 'parentkey' => 'fk_contrat'),
72  'facture_fourn_det' => array('name' => 'SupplierInvoice', 'parent' => 'facture_fourn', 'parentkey' => 'fk_facture_fourn'),
73  'commande_fournisseurdet' => array('name' => 'SupplierOrder', 'parent' => 'commande_fournisseur', 'parentkey' => 'fk_commande')
74  );
75 
81  public $ismultientitymanaged = 1;
82 
86  public $isextrafieldmanaged = 1;
87 
91  public $picto = 'product';
92 
96  protected $table_ref_field = 'ref';
97 
98  public $regeximgext = '\.gif|\.jpg|\.jpeg|\.png|\.bmp|\.webp|\.xpm|\.xbm'; // See also into images.lib.php
99 
104  public $libelle;
105 
111  public $label;
112 
118  public $description;
119 
125  public $other;
126 
132  public $type = self::TYPE_PRODUCT;
133 
139  public $price;
140 
141  public $price_formated; // used by takepos/ajax/ajax.php
142 
148  public $price_ttc;
149 
150  public $price_ttc_formated; // used by takepos/ajax/ajax.php
151 
157  public $price_min;
158 
164  public $price_min_ttc;
165 
170  public $price_base_type;
171 
173  public $multiprices = array();
174  public $multiprices_ttc = array();
175  public $multiprices_base_type = array();
176  public $multiprices_min = array();
177  public $multiprices_min_ttc = array();
178  public $multiprices_tva_tx = array();
179  public $multiprices_recuperableonly = array();
180 
183  public $prices_by_qty = array();
184  public $prices_by_qty_id = array();
185  public $prices_by_qty_list = array();
186 
188  public $multilangs = array();
189 
192 
194  public $tva_tx;
195 
199  public $tva_npr = 0;
204 
207 
210  public $localtax2_tx;
211  public $localtax1_type;
212  public $localtax2_type;
213 
214  // Properties set by get_buyprice() for return
215 
216  public $desc_supplier;
217  public $vatrate_supplier;
218  public $default_vat_code_supplier;
219  public $fourn_multicurrency_price;
220  public $fourn_multicurrency_unitprice;
221  public $fourn_multicurrency_tx;
222  public $fourn_multicurrency_id;
223  public $fourn_multicurrency_code;
224  public $packaging;
225 
226 
227  public $lifetime;
228 
229  public $qc_frequency;
230 
236  public $stock_reel = 0;
237 
243  public $stock_theorique;
244 
250  public $cost_price;
251 
253  public $pmp;
254 
260  public $seuil_stock_alerte = 0;
261 
265  public $desiredstock = 0;
266 
278  public $duration;
279 
284 
290  public $status = 0;
291 
298  public $tosell;
299 
305  public $status_buy = 0;
306 
313  public $tobuy;
314 
320  public $finished;
321 
327  public $fk_default_bom;
328 
334  public $status_batch = 0;
335 
341  public $batch_mask = '';
342 
348  public $customcode;
349 
355  public $url;
356 
358  public $weight;
359  public $weight_units; // scale -3, 0, 3, 6
360  public $length;
361  public $length_units; // scale -3, 0, 3, 6
362  public $width;
363  public $width_units; // scale -3, 0, 3, 6
364  public $height;
365  public $height_units; // scale -3, 0, 3, 6
366  public $surface;
367  public $surface_units; // scale -3, 0, 3, 6
368  public $volume;
369  public $volume_units; // scale -3, 0, 3, 6
370 
371  public $net_measure;
372  public $net_measure_units; // scale -3, 0, 3, 6
373 
374  public $accountancy_code_sell;
375  public $accountancy_code_sell_intra;
376  public $accountancy_code_sell_export;
377  public $accountancy_code_buy;
378  public $accountancy_code_buy_intra;
379  public $accountancy_code_buy_export;
380 
386  public $barcode;
387 
393  public $barcode_type;
394 
400  public $barcode_type_code;
401 
402  public $stats_propale = array();
403  public $stats_commande = array();
404  public $stats_contrat = array();
405  public $stats_facture = array();
406  public $stats_proposal_supplier = array();
407  public $stats_commande_fournisseur = array();
408  public $stats_expedition = array();
409  public $stats_reception = array();
410  public $stats_mo = array();
411  public $stats_bom = array();
412  public $stats_mrptoconsume = array();
413  public $stats_mrptoproduce = array();
414  public $stats_facturerec = array();
415  public $stats_facture_fournisseur = array();
416 
418  public $imgWidth;
419  public $imgHeight;
420 
424  public $date_creation;
425 
429  public $date_modification;
430 
433 
436 
437  public $nbphoto = 0;
438 
440  public $stock_warehouse = array();
441 
442  public $oldcopy;
443 
447  public $fk_default_warehouse;
451  public $fk_price_expression;
452 
453  /* To store supplier price found */
454  public $fourn_qty;
455  public $fourn_pu;
456  public $fourn_price_base_type;
457  public $fourn_socid;
458 
463  public $ref_fourn;
464 
468  public $ref_supplier;
469 
475  public $fk_unit;
476 
482  public $price_autogen = 0;
483 
489  public $supplierprices;
490 
496  public $sousprods;
497 
501  public $res;
502 
503 
509  public $is_object_used;
510 
511 
515  public $mandatory_period;
516 
545  public $fields = array(
546  'rowid' => array('type'=>'integer', 'label'=>'TechnicalID', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'index'=>1, 'position'=>1, 'comment'=>'Id'),
547  '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'),
548  'entity' =>array('type'=>'integer', 'label'=>'Entity', 'enabled'=>1, 'visible'=>0, 'default'=>1, 'notnull'=>1, 'index'=>1, 'position'=>5),
549  'label' =>array('type'=>'varchar(255)', 'label'=>'Label', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'showoncombobox'=>2, 'position'=>15, 'csslist'=>'tdoverflowmax250'),
550  'barcode' =>array('type'=>'varchar(255)', 'label'=>'Barcode', 'enabled'=>'isModEnabled("barcode")', 'position'=>20, 'visible'=>-1, 'showoncombobox'=>3),
551  'fk_barcode_type' => array('type'=>'integer', 'label'=>'BarcodeType', 'enabled'=>'1', 'position'=>21, 'notnull'=>0, 'visible'=>-1,),
552  'note_public' =>array('type'=>'html', 'label'=>'NotePublic', 'enabled'=>1, 'visible'=>0, 'position'=>61),
553  'note' =>array('type'=>'html', 'label'=>'NotePrivate', 'enabled'=>1, 'visible'=>0, 'position'=>62),
554  'datec' =>array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'position'=>500),
555  'tms' =>array('type'=>'timestamp', 'label'=>'DateModification', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'position'=>501),
556  //'date_valid' =>array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-2, 'position'=>502),
557  'fk_user_author'=>array('type'=>'integer', 'label'=>'UserAuthor', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'position'=>510, 'foreignkey'=>'llx_user.rowid'),
558  'fk_user_modif' =>array('type'=>'integer', 'label'=>'UserModif', 'enabled'=>1, 'visible'=>-2, 'notnull'=>-1, 'position'=>511),
559  //'fk_user_valid' =>array('type'=>'integer', 'label'=>'UserValidation', 'enabled'=>1, 'visible'=>-1, 'position'=>512),
560  'localtax1_tx' => array('type'=>'double(6,3)', 'label'=>'Localtax1tx', 'enabled'=>'1', 'position'=>150, 'notnull'=>0, 'visible'=>-1,),
561  'localtax1_type' => array('type'=>'varchar(10)', 'label'=>'Localtax1type', 'enabled'=>'1', 'position'=>155, 'notnull'=>1, 'visible'=>-1,),
562  'localtax2_tx' => array('type'=>'double(6,3)', 'label'=>'Localtax2tx', 'enabled'=>'1', 'position'=>160, 'notnull'=>0, 'visible'=>-1,),
563  'localtax2_type' => array('type'=>'varchar(10)', 'label'=>'Localtax2type', 'enabled'=>'1', 'position'=>165, 'notnull'=>1, 'visible'=>-1,),
564  'import_key' =>array('type'=>'varchar(14)', 'label'=>'ImportId', 'enabled'=>1, 'visible'=>-2, 'notnull'=>-1, 'index'=>0, 'position'=>1000),
565  //'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')),
566  //'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')),
567  'mandatory_period' => array('type'=>'integer', 'label'=>'mandatory_period', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'default'=>0, 'index'=>1, 'position'=>1000),
568  );
569 
573  const TYPE_PRODUCT = 0;
577  const TYPE_SERVICE = 1;
581  const TYPE_ASSEMBLYKIT = 2;
585  const TYPE_STOCKKIT = 3;
586 
587 
593  public function __construct($db)
594  {
595  $this->db = $db;
596  $this->canvas = '';
597  }
598 
604  public function check()
605  {
606  if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
607  $this->ref = trim($this->ref);
608  } else {
609  $this->ref = dol_sanitizeFileName(stripslashes($this->ref));
610  }
611 
612  $err = 0;
613  if (dol_strlen(trim($this->ref)) == 0) {
614  $err++;
615  }
616 
617  if (dol_strlen(trim($this->label)) == 0) {
618  $err++;
619  }
620 
621  if ($err > 0) {
622  return 0;
623  } else {
624  return 1;
625  }
626  }
627 
635  public function create($user, $notrigger = 0)
636  {
637  global $conf, $langs;
638 
639  $error = 0;
640 
641  // Clean parameters
642  if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
643  $this->ref = trim($this->ref);
644  } else {
645  $this->ref = dol_sanitizeFileName(dol_string_nospecial(trim($this->ref)));
646  }
647  $this->label = trim($this->label);
648  $this->price_ttc = price2num($this->price_ttc);
649  $this->price = price2num($this->price);
650  $this->price_min_ttc = price2num($this->price_min_ttc);
651  $this->price_min = price2num($this->price_min);
652  if (empty($this->tva_tx)) {
653  $this->tva_tx = 0;
654  }
655  if (empty($this->tva_npr)) {
656  $this->tva_npr = 0;
657  }
658  //Local taxes
659  if (empty($this->localtax1_tx)) {
660  $this->localtax1_tx = 0;
661  }
662  if (empty($this->localtax2_tx)) {
663  $this->localtax2_tx = 0;
664  }
665  if (empty($this->localtax1_type)) {
666  $this->localtax1_type = '0';
667  }
668  if (empty($this->localtax2_type)) {
669  $this->localtax2_type = '0';
670  }
671  if (empty($this->price)) {
672  $this->price = 0;
673  }
674  if (empty($this->price_min)) {
675  $this->price_min = 0;
676  }
677  // Price by quantity
678  if (empty($this->price_by_qty)) {
679  $this->price_by_qty = 0;
680  }
681 
682  if (empty($this->status)) {
683  $this->status = 0;
684  }
685  if (empty($this->status_buy)) {
686  $this->status_buy = 0;
687  }
688 
689  $price_ht = 0;
690  $price_ttc = 0;
691  $price_min_ht = 0;
692  $price_min_ttc = 0;
693 
694  //
695  if ($this->price_base_type == 'TTC' && $this->price_ttc > 0) {
696  $price_ttc = price2num($this->price_ttc, 'MU');
697  $price_ht = price2num($this->price_ttc / (1 + ($this->tva_tx / 100)), 'MU');
698  }
699 
700  //
701  if ($this->price_base_type != 'TTC' && $this->price > 0) {
702  $price_ht = price2num($this->price, 'MU');
703  $price_ttc = price2num($this->price * (1 + ($this->tva_tx / 100)), 'MU');
704  }
705 
706  //
707  if (($this->price_min_ttc > 0) && ($this->price_base_type == 'TTC')) {
708  $price_min_ttc = price2num($this->price_min_ttc, 'MU');
709  $price_min_ht = price2num($this->price_min_ttc / (1 + ($this->tva_tx / 100)), 'MU');
710  }
711 
712  //
713  if (($this->price_min > 0) && ($this->price_base_type != 'TTC')) {
714  $price_min_ht = price2num($this->price_min, 'MU');
715  $price_min_ttc = price2num($this->price_min * (1 + ($this->tva_tx / 100)), 'MU');
716  }
717 
718  $this->accountancy_code_buy = trim($this->accountancy_code_buy);
719  $this->accountancy_code_buy_intra = trim($this->accountancy_code_buy_intra);
720  $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
721  $this->accountancy_code_sell = trim($this->accountancy_code_sell);
722  $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
723  $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
724 
725  // Barcode value
726  $this->barcode = trim($this->barcode);
727  $this->mandatory_period = empty($this->mandatory_period) ? 0 : $this->mandatory_period;
728  // Check parameters
729  if (empty($this->label)) {
730  $this->error = 'ErrorMandatoryParametersNotProvided';
731  return -1;
732  }
733 
734  if (empty($this->ref) || $this->ref == 'auto') {
735  // Load object modCodeProduct
736  $module = getDolGlobalString('PRODUCT_CODEPRODUCT_ADDON', 'mod_codeproduct_leopard');
737  if ($module != 'mod_codeproduct_leopard') { // Do not load module file for leopard
738  if (substr($module, 0, 16) == 'mod_codeproduct_' && substr($module, -3) == 'php') {
739  $module = substr($module, 0, dol_strlen($module) - 4);
740  }
741  dol_include_once('/core/modules/product/'.$module.'.php');
742  $modCodeProduct = new $module;
743  if (!empty($modCodeProduct->code_auto)) {
744  $this->ref = $modCodeProduct->getNextValue($this, $this->type);
745  }
746  unset($modCodeProduct);
747  }
748 
749  if (empty($this->ref)) {
750  $this->error = 'ProductModuleNotSetupForAutoRef';
751  return -2;
752  }
753  }
754 
755  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);
756 
757  $now = dol_now();
758 
759  $this->db->begin();
760 
761  // For automatic creation during create action (not used by Dolibarr GUI, can be used by scripts)
762  if ($this->barcode == -1) {
763  $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
764  }
765 
766  // Check more parameters
767  // If error, this->errors[] is filled
768  $result = $this->verify();
769 
770  if ($result >= 0) {
771  $sql = "SELECT count(*) as nb";
772  $sql .= " FROM ".$this->db->prefix()."product";
773  $sql .= " WHERE entity IN (".getEntity('product').")";
774  $sql .= " AND ref = '".$this->db->escape($this->ref)."'";
775 
776  $result = $this->db->query($sql);
777  if ($result) {
778  $obj = $this->db->fetch_object($result);
779  if ($obj->nb == 0) {
780  // Produit non deja existant
781  $sql = "INSERT INTO ".$this->db->prefix()."product (";
782  $sql .= "datec";
783  $sql .= ", entity";
784  $sql .= ", ref";
785  $sql .= ", ref_ext";
786  $sql .= ", price_min";
787  $sql .= ", price_min_ttc";
788  $sql .= ", label";
789  $sql .= ", fk_user_author";
790  $sql .= ", fk_product_type";
791  $sql .= ", price";
792  $sql .= ", price_ttc";
793  $sql .= ", price_base_type";
794  $sql .= ", tobuy";
795  $sql .= ", tosell";
796  if (empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
797  $sql .= ", accountancy_code_buy";
798  $sql .= ", accountancy_code_buy_intra";
799  $sql .= ", accountancy_code_buy_export";
800  $sql .= ", accountancy_code_sell";
801  $sql .= ", accountancy_code_sell_intra";
802  $sql .= ", accountancy_code_sell_export";
803  }
804  $sql .= ", canvas";
805  $sql .= ", finished";
806  $sql .= ", tobatch";
807  $sql .= ", batch_mask";
808  $sql .= ", fk_unit";
809  $sql .= ", mandatory_period";
810  $sql .= ") VALUES (";
811  $sql .= "'".$this->db->idate($now)."'";
812  $sql .= ", ".((int) $conf->entity);
813  $sql .= ", '".$this->db->escape($this->ref)."'";
814  $sql .= ", ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
815  $sql .= ", ".price2num($price_min_ht);
816  $sql .= ", ".price2num($price_min_ttc);
817  $sql .= ", ".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null");
818  $sql .= ", ".((int) $user->id);
819  $sql .= ", ".((int) $this->type);
820  $sql .= ", ".price2num($price_ht, 'MT');
821  $sql .= ", ".price2num($price_ttc, 'MT');
822  $sql .= ", '".$this->db->escape($this->price_base_type)."'";
823  $sql .= ", ".((int) $this->status);
824  $sql .= ", ".((int) $this->status_buy);
825  if (empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
826  $sql .= ", '".$this->db->escape($this->accountancy_code_buy)."'";
827  $sql .= ", '".$this->db->escape($this->accountancy_code_buy_intra)."'";
828  $sql .= ", '".$this->db->escape($this->accountancy_code_buy_export)."'";
829  $sql .= ", '".$this->db->escape($this->accountancy_code_sell)."'";
830  $sql .= ", '".$this->db->escape($this->accountancy_code_sell_intra)."'";
831  $sql .= ", '".$this->db->escape($this->accountancy_code_sell_export)."'";
832  }
833  $sql .= ", '".$this->db->escape($this->canvas)."'";
834  $sql .= ", ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? 'NULL' : (int) $this->finished);
835  $sql .= ", ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : ((int) $this->status_batch));
836  $sql .= ", '".$this->db->escape($this->batch_mask)."'";
837  $sql .= ", ".($this->fk_unit > 0 ? ((int) $this->fk_unit) : 'NULL');
838  $sql .= ", '".$this->db->escape($this->mandatory_period)."'";
839  $sql .= ")";
840 
841  dol_syslog(get_class($this)."::Create", LOG_DEBUG);
842  $result = $this->db->query($sql);
843  if ($result) {
844  $id = $this->db->last_insert_id($this->db->prefix()."product");
845 
846  if ($id > 0) {
847  $this->id = $id;
848  $this->price = $price_ht;
849  $this->price_ttc = $price_ttc;
850  $this->price_min = $price_min_ht;
851  $this->price_min_ttc = $price_min_ttc;
852 
853  $result = $this->_log_price($user);
854  if ($result > 0) {
855  if ($this->update($id, $user, true, 'add') <= 0) {
856  $error++;
857  }
858  } else {
859  $error++;
860  $this->error = $this->db->lasterror();
861  }
862 
863  // update accountancy for this entity
864  if (!$error && !empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
865  $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " .((int) $this->id) . " AND entity = " . ((int) $conf->entity));
866 
867  $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
868  $sql .= " fk_product";
869  $sql .= ", entity";
870  $sql .= ", accountancy_code_buy";
871  $sql .= ", accountancy_code_buy_intra";
872  $sql .= ", accountancy_code_buy_export";
873  $sql .= ", accountancy_code_sell";
874  $sql .= ", accountancy_code_sell_intra";
875  $sql .= ", accountancy_code_sell_export";
876  $sql .= ") VALUES (";
877  $sql .= $this->id;
878  $sql .= ", " . $conf->entity;
879  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
880  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
881  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
882  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
883  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
884  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
885  $sql .= ")";
886  $result = $this->db->query($sql);
887  if (!$result) {
888  $error++;
889  $this->error = 'ErrorFailedToInsertAccountancyForEntity';
890  }
891  }
892  } else {
893  $error++;
894  $this->error = 'ErrorFailedToGetInsertedId';
895  }
896  } else {
897  $error++;
898  $this->error = $this->db->lasterror();
899  }
900  } else {
901  // Product already exists with this ref
902  $langs->load("products");
903  $error++;
904  $this->error = "ErrorProductAlreadyExists";
905  dol_syslog(get_class($this)."::Create fails, ref ".$this->ref." already exists");
906  }
907  } else {
908  $error++;
909  $this->error = $this->db->lasterror();
910  }
911 
912  if (!$error && !$notrigger) {
913  // Call trigger
914  $result = $this->call_trigger('PRODUCT_CREATE', $user);
915  if ($result < 0) {
916  $error++;
917  }
918  // End call triggers
919  }
920 
921  if (!$error) {
922  $this->db->commit();
923  return $this->id;
924  } else {
925  $this->db->rollback();
926  return -$error;
927  }
928  } else {
929  $this->db->rollback();
930  dol_syslog(get_class($this)."::Create fails verify ".join(',', $this->errors), LOG_WARNING);
931  return -3;
932  }
933  }
934 
935 
942  public function verify()
943  {
944  global $langs;
945 
946  $this->errors = array();
947 
948  $result = 0;
949  $this->ref = trim($this->ref);
950 
951  if (!$this->ref) {
952  $this->errors[] = 'ErrorBadRef';
953  $result = -2;
954  }
955 
956  $arrayofnonnegativevalue = array('weight'=>'Weight', 'width'=>'Width', 'height'=>'Height', 'length'=>'Length', 'surface'=>'Surface', 'volume'=>'Volume');
957  foreach ($arrayofnonnegativevalue as $key => $value) {
958  if (property_exists($this, $key) && !empty($this->$key) && ($this->$key < 0)) {
959  $langs->loadLangs(array("main", "other"));
960  $this->error = $langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv($value));
961  $this->errors[] = $this->error;
962  $result = -4;
963  }
964  }
965 
966  $rescode = $this->check_barcode($this->barcode, $this->barcode_type_code);
967  if ($rescode) {
968  if ($rescode == -1) {
969  $this->errors[] = 'ErrorBadBarCodeSyntax';
970  } elseif ($rescode == -2) {
971  $this->errors[] = 'ErrorBarCodeRequired';
972  } elseif ($rescode == -3) {
973  // Note: Common usage is to have barcode unique. For variants, we should have a different barcode.
974  $this->errors[] = 'ErrorBarCodeAlreadyUsed';
975  }
976 
977  $result = -3;
978  }
979 
980  return $result;
981  }
982 
983  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
994  public function check_barcode($valuetotest, $typefortest)
995  {
996  // phpcs:enable
997  global $conf;
998  if (isModEnabled('barcode') && !empty($conf->global->BARCODE_PRODUCT_ADDON_NUM)) {
999  $module = strtolower($conf->global->BARCODE_PRODUCT_ADDON_NUM);
1000 
1001  $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
1002  foreach ($dirsociete as $dirroot) {
1003  $res = dol_include_once($dirroot.$module.'.php');
1004  if ($res) {
1005  break;
1006  }
1007  }
1008 
1009  $mod = new $module();
1010 
1011  dol_syslog(get_class($this)."::check_barcode value=".$valuetotest." type=".$typefortest." module=".$module);
1012  $result = $mod->verif($this->db, $valuetotest, $this, 0, $typefortest);
1013  return $result;
1014  } else {
1015  return 0;
1016  }
1017  }
1018 
1030  public function update($id, $user, $notrigger = false, $action = 'update', $updatetype = false)
1031  {
1032  global $langs, $conf, $hookmanager;
1033 
1034  $error = 0;
1035 
1036  // Check parameters
1037  if (!$this->label) {
1038  $this->label = 'MISSING LABEL';
1039  }
1040 
1041  // Clean parameters
1042  if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
1043  $this->ref = trim($this->ref);
1044  } else {
1045  $this->ref = dol_string_nospecial(trim($this->ref));
1046  }
1047  $this->label = trim($this->label);
1048  $this->description = trim($this->description);
1049  $this->note_private = (isset($this->note_private) ? trim($this->note_private) : null);
1050  $this->note_public = (isset($this->note_public) ? trim($this->note_public) : null);
1051  $this->net_measure = price2num($this->net_measure);
1052  $this->net_measure_units = trim($this->net_measure_units);
1053  $this->weight = price2num($this->weight);
1054  $this->weight_units = trim($this->weight_units);
1055  $this->length = price2num($this->length);
1056  $this->length_units = trim($this->length_units);
1057  $this->width = price2num($this->width);
1058  $this->width_units = trim($this->width_units);
1059  $this->height = price2num($this->height);
1060  $this->height_units = trim($this->height_units);
1061  $this->surface = price2num($this->surface);
1062  $this->surface_units = trim($this->surface_units);
1063  $this->volume = price2num($this->volume);
1064  $this->volume_units = trim($this->volume_units);
1065 
1066  // set unit not defined
1067  if (is_numeric($this->length_units)) {
1068  $this->width_units = $this->length_units; // Not used yet
1069  }
1070  if (is_numeric($this->length_units)) {
1071  $this->height_units = $this->length_units; // Not used yet
1072  }
1073 
1074  // Automated compute surface and volume if not filled
1075  if (empty($this->surface) && !empty($this->length) && !empty($this->width) && $this->length_units == $this->width_units) {
1076  $this->surface = $this->length * $this->width;
1077  $this->surface_units = measuring_units_squared($this->length_units);
1078  }
1079  if (empty($this->volume) && !empty($this->surface) && !empty($this->height) && $this->length_units == $this->height_units) {
1080  $this->volume = $this->surface * $this->height;
1081  $this->volume_units = measuring_units_cubed($this->height_units);
1082  }
1083 
1084  if (empty($this->tva_tx)) {
1085  $this->tva_tx = 0;
1086  }
1087  if (empty($this->tva_npr)) {
1088  $this->tva_npr = 0;
1089  }
1090  if (empty($this->localtax1_tx)) {
1091  $this->localtax1_tx = 0;
1092  }
1093  if (empty($this->localtax2_tx)) {
1094  $this->localtax2_tx = 0;
1095  }
1096  if (empty($this->localtax1_type)) {
1097  $this->localtax1_type = '0';
1098  }
1099  if (empty($this->localtax2_type)) {
1100  $this->localtax2_type = '0';
1101  }
1102  if (empty($this->status)) {
1103  $this->status = 0;
1104  }
1105  if (empty($this->status_buy)) {
1106  $this->status_buy = 0;
1107  }
1108 
1109  if (empty($this->country_id)) {
1110  $this->country_id = 0;
1111  }
1112 
1113  if (empty($this->state_id)) {
1114  $this->state_id = 0;
1115  }
1116 
1117  // Barcode value
1118  $this->barcode = trim($this->barcode);
1119 
1120  $this->accountancy_code_buy = trim($this->accountancy_code_buy);
1121  $this->accountancy_code_buy_intra = trim($this->accountancy_code_buy_intra);
1122  $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
1123  $this->accountancy_code_sell = trim($this->accountancy_code_sell);
1124  $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
1125  $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
1126 
1127 
1128  $this->db->begin();
1129 
1130  $result = 0;
1131  // Check name is required and codes are ok or unique. If error, this->errors[] is filled
1132  if ($action != 'add') {
1133  $result = $this->verify(); // We don't check when update called during a create because verify was already done
1134  } else {
1135  // we can continue
1136  $result = 0;
1137  }
1138 
1139  if ($result >= 0) {
1140  // $this->oldcopy should have been set by the caller of update (here properties were already modified)
1141  if (empty($this->oldcopy)) {
1142  $this->oldcopy = dol_clone($this);
1143  }
1144 
1145  // Test if batch management is activated on existing product
1146  // If yes, we create missing entries into product_batch
1147  if ($this->hasbatch() && !$this->oldcopy->hasbatch()) {
1148  //$valueforundefinedlot = 'Undefined'; // In previous version, 39 and lower
1149  $valueforundefinedlot = '000000';
1150  if (!empty($conf->global->STOCK_DEFAULT_BATCH)) {
1151  $valueforundefinedlot = $conf->global->STOCK_DEFAULT_BATCH;
1152  }
1153 
1154  dol_syslog("Flag batch of product id=".$this->id." is set to ON, so we will create missing records into product_batch");
1155 
1156  $this->load_stock();
1157  foreach ($this->stock_warehouse as $idW => $ObjW) { // For each warehouse where we have stocks defined for this product (for each lines in product_stock)
1158  $qty_batch = 0;
1159  foreach ($ObjW->detail_batch as $detail) { // Each lines of detail in product_batch of the current $ObjW = product_stock
1160  if ($detail->batch == $valueforundefinedlot || $detail->batch == 'Undefined') {
1161  // We discard this line, we will create it later
1162  $sqlclean = "DELETE FROM ".$this->db->prefix()."product_batch WHERE batch in('Undefined', '".$this->db->escape($valueforundefinedlot)."') AND fk_product_stock = ".((int) $ObjW->id);
1163  $result = $this->db->query($sqlclean);
1164  if (!$result) {
1165  dol_print_error($this->db);
1166  exit;
1167  }
1168  continue;
1169  }
1170 
1171  $qty_batch += $detail->qty;
1172  }
1173  // Quantities in batch details are not same as stock quantity,
1174  // so we add a default batch record to complete and get same qty in parent and child table
1175  if ($ObjW->real <> $qty_batch) {
1176  $ObjBatch = new Productbatch($this->db);
1177  $ObjBatch->batch = $valueforundefinedlot;
1178  $ObjBatch->qty = ($ObjW->real - $qty_batch);
1179  $ObjBatch->fk_product_stock = $ObjW->id;
1180 
1181  if ($ObjBatch->create($user, 1) < 0) {
1182  $error++;
1183  $this->errors = $ObjBatch->errors;
1184  }
1185  }
1186  }
1187  }
1188 
1189  // For automatic creation
1190  if ($this->barcode == -1) {
1191  $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1192  }
1193 
1194  $sql = "UPDATE ".$this->db->prefix()."product";
1195  $sql .= " SET label = '".$this->db->escape($this->label)."'";
1196 
1197  if ($updatetype && ($this->isProduct() || $this->isService())) {
1198  $sql .= ", fk_product_type = ".((int) $this->type);
1199  }
1200 
1201  $sql .= ", ref = '".$this->db->escape($this->ref)."'";
1202  $sql .= ", ref_ext = ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
1203  $sql .= ", default_vat_code = ".($this->default_vat_code ? "'".$this->db->escape($this->default_vat_code)."'" : "null");
1204  $sql .= ", tva_tx = ".((float) $this->tva_tx);
1205  $sql .= ", recuperableonly = ".((int) $this->tva_npr);
1206  $sql .= ", localtax1_tx = ".((float) $this->localtax1_tx);
1207  $sql .= ", localtax2_tx = ".((float) $this->localtax2_tx);
1208  $sql .= ", localtax1_type = ".($this->localtax1_type != '' ? "'".$this->db->escape($this->localtax1_type)."'" : "'0'");
1209  $sql .= ", localtax2_type = ".($this->localtax2_type != '' ? "'".$this->db->escape($this->localtax2_type)."'" : "'0'");
1210 
1211  $sql .= ", barcode = ".(empty($this->barcode) ? "null" : "'".$this->db->escape($this->barcode)."'");
1212  $sql .= ", fk_barcode_type = ".(empty($this->barcode_type) ? "null" : $this->db->escape($this->barcode_type));
1213 
1214  $sql .= ", tosell = ".(int) $this->status;
1215  $sql .= ", tobuy = ".(int) $this->status_buy;
1216  $sql .= ", tobatch = ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : (int) $this->status_batch);
1217  $sql .= ", batch_mask = '".$this->db->escape($this->batch_mask)."'";
1218 
1219  $sql .= ", finished = ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? "null" : (int) $this->finished);
1220  $sql .= ", fk_default_bom = ".((!isset($this->fk_default_bom) || $this->fk_default_bom < 0 || $this->fk_default_bom == '') ? "null" : (int) $this->fk_default_bom);
1221  $sql .= ", net_measure = ".($this->net_measure != '' ? "'".$this->db->escape($this->net_measure)."'" : 'null');
1222  $sql .= ", net_measure_units = ".($this->net_measure_units != '' ? "'".$this->db->escape($this->net_measure_units)."'" : 'null');
1223  $sql .= ", weight = ".($this->weight != '' ? "'".$this->db->escape($this->weight)."'" : 'null');
1224  $sql .= ", weight_units = ".($this->weight_units != '' ? "'".$this->db->escape($this->weight_units)."'" : 'null');
1225  $sql .= ", length = ".($this->length != '' ? "'".$this->db->escape($this->length)."'" : 'null');
1226  $sql .= ", length_units = ".($this->length_units != '' ? "'".$this->db->escape($this->length_units)."'" : 'null');
1227  $sql .= ", width= ".($this->width != '' ? "'".$this->db->escape($this->width)."'" : 'null');
1228  $sql .= ", width_units = ".($this->width_units != '' ? "'".$this->db->escape($this->width_units)."'" : 'null');
1229  $sql .= ", height = ".($this->height != '' ? "'".$this->db->escape($this->height)."'" : 'null');
1230  $sql .= ", height_units = ".($this->height_units != '' ? "'".$this->db->escape($this->height_units)."'" : 'null');
1231  $sql .= ", surface = ".($this->surface != '' ? "'".$this->db->escape($this->surface)."'" : 'null');
1232  $sql .= ", surface_units = ".($this->surface_units != '' ? "'".$this->db->escape($this->surface_units)."'" : 'null');
1233  $sql .= ", volume = ".($this->volume != '' ? "'".$this->db->escape($this->volume)."'" : 'null');
1234  $sql .= ", volume_units = ".($this->volume_units != '' ? "'".$this->db->escape($this->volume_units)."'" : 'null');
1235  $sql .= ", fk_default_warehouse = ".($this->fk_default_warehouse > 0 ? ((int) $this->fk_default_warehouse) : 'null');
1236  $sql .= ", fk_default_workstation = ".($this->fk_default_workstation > 0 ? ((int) $this->fk_default_workstation) : 'null');
1237  $sql .= ", seuil_stock_alerte = ".((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (float) $this->seuil_stock_alerte : 'null');
1238  $sql .= ", description = '".$this->db->escape($this->description)."'";
1239  $sql .= ", url = ".($this->url ? "'".$this->db->escape($this->url)."'" : 'null');
1240  $sql .= ", customcode = '".$this->db->escape($this->customcode)."'";
1241  $sql .= ", fk_country = ".($this->country_id > 0 ? (int) $this->country_id : 'null');
1242  $sql .= ", fk_state = ".($this->state_id > 0 ? (int) $this->state_id : 'null');
1243  $sql .= ", lifetime = ".($this->lifetime > 0 ? (int) $this->lifetime : 'null');
1244  $sql .= ", qc_frequency = ".($this->qc_frequency > 0 ? (int) $this->qc_frequency : 'null');
1245  $sql .= ", note = ".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : 'null');
1246  $sql .= ", note_public = ".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : 'null');
1247  $sql .= ", duration = '".$this->db->escape($this->duration_value.$this->duration_unit)."'";
1248  if (empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
1249  $sql .= ", accountancy_code_buy = '" . $this->db->escape($this->accountancy_code_buy) . "'";
1250  $sql .= ", accountancy_code_buy_intra = '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1251  $sql .= ", accountancy_code_buy_export = '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1252  $sql .= ", accountancy_code_sell= '" . $this->db->escape($this->accountancy_code_sell) . "'";
1253  $sql .= ", accountancy_code_sell_intra= '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1254  $sql .= ", accountancy_code_sell_export= '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1255  }
1256  $sql .= ", desiredstock = ".((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (float) $this->desiredstock : "null");
1257  $sql .= ", cost_price = ".($this->cost_price != '' ? $this->db->escape($this->cost_price) : 'null');
1258  $sql .= ", fk_unit= ".(!$this->fk_unit ? 'NULL' : (int) $this->fk_unit);
1259  $sql .= ", price_autogen = ".(!$this->price_autogen ? 0 : 1);
1260  $sql .= ", fk_price_expression = ".($this->fk_price_expression != 0 ? (int) $this->fk_price_expression : 'NULL');
1261  $sql .= ", fk_user_modif = ".($user->id > 0 ? $user->id : 'NULL');
1262  $sql .= ", mandatory_period = ".($this->mandatory_period );
1263  // stock field is not here because it is a denormalized value from product_stock.
1264  $sql .= " WHERE rowid = ".((int) $id);
1265 
1266  dol_syslog(get_class($this)."::update", LOG_DEBUG);
1267 
1268  $resql = $this->db->query($sql);
1269  if ($resql) {
1270  $this->id = $id;
1271 
1272  // Multilangs
1273  if (getDolGlobalInt('MAIN_MULTILANGS')) {
1274  if ($this->setMultiLangs($user) < 0) {
1275  $this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1276  return -2;
1277  }
1278  }
1279 
1280  $action = 'update';
1281 
1282  // update accountancy for this entity
1283  if (!$error && !empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
1284  $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1285 
1286  $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1287  $sql .= " fk_product";
1288  $sql .= ", entity";
1289  $sql .= ", accountancy_code_buy";
1290  $sql .= ", accountancy_code_buy_intra";
1291  $sql .= ", accountancy_code_buy_export";
1292  $sql .= ", accountancy_code_sell";
1293  $sql .= ", accountancy_code_sell_intra";
1294  $sql .= ", accountancy_code_sell_export";
1295  $sql .= ") VALUES (";
1296  $sql .= $this->id;
1297  $sql .= ", " . $conf->entity;
1298  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1299  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1300  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1301  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1302  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1303  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1304  $sql .= ")";
1305  $result = $this->db->query($sql);
1306  if (!$result) {
1307  $error++;
1308  $this->error = 'ErrorFailedToUpdateAccountancyForEntity';
1309  }
1310  }
1311 
1312  // Actions on extra fields
1313  if (!$error) {
1314  $result = $this->insertExtraFields();
1315  if ($result < 0) {
1316  $error++;
1317  }
1318  }
1319 
1320  if (!$error && !$notrigger) {
1321  // Call trigger
1322  $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1323  if ($result < 0) {
1324  $error++;
1325  }
1326  // End call triggers
1327  }
1328 
1329  if (!$error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref)) {
1330  // We remove directory
1331  if ($conf->product->dir_output) {
1332  $olddir = $conf->product->dir_output."/".dol_sanitizeFileName($this->oldcopy->ref);
1333  $newdir = $conf->product->dir_output."/".dol_sanitizeFileName($this->ref);
1334  if (file_exists($olddir)) {
1335  //include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1336  //$res = dol_move($olddir, $newdir);
1337  // do not use dol_move with directory
1338  $res = @rename($olddir, $newdir);
1339  if (!$res) {
1340  $langs->load("errors");
1341  $this->error = $langs->trans('ErrorFailToRenameDir', $olddir, $newdir);
1342  $error++;
1343  }
1344  }
1345  }
1346  }
1347 
1348  if (!$error) {
1349  if (isModEnabled('variants')) {
1350  include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1351 
1352  $comb = new ProductCombination($this->db);
1353 
1354  foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) {
1355  $currcomb->updateProperties($this, $user);
1356  }
1357  }
1358 
1359  $this->db->commit();
1360  return 1;
1361  } else {
1362  $this->db->rollback();
1363  return -$error;
1364  }
1365  } else {
1366  if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1367  $langs->load("errors");
1368  if (empty($conf->barcode->enabled) || empty($this->barcode)) {
1369  $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductAlreadyExists", $this->ref);
1370  } else {
1371  $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductBarCodeAlreadyExists", $this->barcode);
1372  }
1373  $this->errors[] = $this->error;
1374  $this->db->rollback();
1375  return -1;
1376  } else {
1377  $this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1378  $this->errors[] = $this->error;
1379  $this->db->rollback();
1380  return -2;
1381  }
1382  }
1383  } else {
1384  $this->db->rollback();
1385  dol_syslog(get_class($this)."::Update fails verify ".join(',', $this->errors), LOG_WARNING);
1386  return -3;
1387  }
1388  }
1389 
1397  public function delete(User $user, $notrigger = 0)
1398  {
1399  global $conf, $langs;
1400  include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1401 
1402  $error = 0;
1403 
1404  // Check parameters
1405  if (empty($this->id)) {
1406  $this->error = "Object must be fetched before calling delete";
1407  return -1;
1408  }
1409  if (($this->type == Product::TYPE_PRODUCT && empty($user->rights->produit->supprimer)) || ($this->type == Product::TYPE_SERVICE && empty($user->rights->service->supprimer))) {
1410  $this->error = "ErrorForbidden";
1411  return 0;
1412  }
1413 
1414  $objectisused = $this->isObjectUsed($this->id);
1415  if (empty($objectisused)) {
1416  $this->db->begin();
1417 
1418  if (!$error && empty($notrigger)) {
1419  // Call trigger
1420  $result = $this->call_trigger('PRODUCT_DELETE', $user);
1421  if ($result < 0) {
1422  $error++;
1423  }
1424  // End call triggers
1425  }
1426 
1427  // Delete from product_batch on product delete
1428  if (!$error) {
1429  $sql = "DELETE FROM ".$this->db->prefix().'product_batch';
1430  $sql .= " WHERE fk_product_stock IN (";
1431  $sql .= "SELECT rowid FROM ".$this->db->prefix().'product_stock';
1432  $sql .= " WHERE fk_product = ".((int) $this->id).")";
1433 
1434  $result = $this->db->query($sql);
1435  if (!$result) {
1436  $error++;
1437  $this->errors[] = $this->db->lasterror();
1438  }
1439  }
1440 
1441  // Delete all child tables
1442  if (!$error) {
1443  $elements = array('product_fournisseur_price', 'product_price', 'product_lang', 'categorie_product', 'product_stock', 'product_customer_price', 'product_lot'); // product_batch is done before
1444  foreach ($elements as $table) {
1445  if (!$error) {
1446  $sql = "DELETE FROM ".$this->db->prefix().$table;
1447  $sql .= " WHERE fk_product = ".(int) $this->id;
1448 
1449  $result = $this->db->query($sql);
1450  if (!$result) {
1451  $error++;
1452  $this->errors[] = $this->db->lasterror();
1453  }
1454  }
1455  }
1456  }
1457 
1458  if (!$error) {
1459  include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1460  include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
1461 
1462  //If it is a parent product, then we remove the association with child products
1463  $prodcomb = new ProductCombination($this->db);
1464 
1465  if ($prodcomb->deleteByFkProductParent($user, $this->id) < 0) {
1466  $error++;
1467  $this->errors[] = 'Error deleting combinations';
1468  }
1469 
1470  //We also check if it is a child product
1471  if (!$error && ($prodcomb->fetchByFkProductChild($this->id) > 0) && ($prodcomb->delete($user) < 0)) {
1472  $error++;
1473  $this->errors[] = 'Error deleting child combination';
1474  }
1475  }
1476 
1477  // Delete from product_association
1478  if (!$error) {
1479  $sql = "DELETE FROM ".$this->db->prefix()."product_association";
1480  $sql .= " WHERE fk_product_pere = ".(int) $this->id." OR fk_product_fils = ".(int) $this->id;
1481 
1482  $result = $this->db->query($sql);
1483  if (!$result) {
1484  $error++;
1485  $this->errors[] = $this->db->lasterror();
1486  }
1487  }
1488 
1489  // Remove extrafields
1490  if (!$error) {
1491  $result = $this->deleteExtraFields();
1492  if ($result < 0) {
1493  $error++;
1494  dol_syslog(get_class($this)."::delete error -4 ".$this->error, LOG_ERR);
1495  }
1496  }
1497 
1498  // Delete product
1499  if (!$error) {
1500  $sqlz = "DELETE FROM ".$this->db->prefix()."product";
1501  $sqlz .= " WHERE rowid = ".(int) $this->id;
1502 
1503  $resultz = $this->db->query($sqlz);
1504  if (!$resultz) {
1505  $error++;
1506  $this->errors[] = $this->db->lasterror();
1507  }
1508  }
1509 
1510  // Delete record into ECM index and physically
1511  if (!$error) {
1512  $res = $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
1513  if (!$res) {
1514  $error++;
1515  }
1516  }
1517 
1518  if (!$error) {
1519  // We remove directory
1520  $ref = dol_sanitizeFileName($this->ref);
1521  if ($conf->product->dir_output) {
1522  $dir = $conf->product->dir_output."/".$ref;
1523  if (file_exists($dir)) {
1524  $res = @dol_delete_dir_recursive($dir);
1525  if (!$res) {
1526  $this->errors[] = 'ErrorFailToDeleteDir';
1527  $error++;
1528  }
1529  }
1530  }
1531  }
1532 
1533  if (!$error) {
1534  $this->db->commit();
1535  return 1;
1536  } else {
1537  foreach ($this->errors as $errmsg) {
1538  dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
1539  $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
1540  }
1541  $this->db->rollback();
1542  return -$error;
1543  }
1544  } else {
1545  $this->error = "ErrorRecordIsUsedCantDelete";
1546  return 0;
1547  }
1548  }
1549 
1556  public function setMultiLangs($user)
1557  {
1558  global $conf, $langs;
1559 
1560  $langs_available = $langs->get_available_languages(DOL_DOCUMENT_ROOT, 0, 2);
1561  $current_lang = $langs->getDefaultLang();
1562 
1563  foreach ($langs_available as $key => $value) {
1564  if ($key == $current_lang) {
1565  $sql = "SELECT rowid";
1566  $sql .= " FROM ".$this->db->prefix()."product_lang";
1567  $sql .= " WHERE fk_product = ".((int) $this->id);
1568  $sql .= " AND lang = '".$this->db->escape($key)."'";
1569 
1570  $result = $this->db->query($sql);
1571 
1572  if ($this->db->num_rows($result)) { // if there is already a description line for this language
1573  $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
1574  $sql2 .= " SET ";
1575  $sql2 .= " label='".$this->db->escape($this->label)."',";
1576  $sql2 .= " description='".$this->db->escape($this->description)."'";
1577  if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1578  $sql2 .= ", note='".$this->db->escape($this->other)."'";
1579  }
1580  $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
1581  } else {
1582  $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
1583  if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1584  $sql2 .= ", note";
1585  }
1586  $sql2 .= ")";
1587  $sql2 .= " VALUES(".$this->id.",'".$this->db->escape($key)."','".$this->db->escape($this->label)."',";
1588  $sql2 .= " '".$this->db->escape($this->description)."'";
1589  if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1590  $sql2 .= ", '".$this->db->escape($this->other)."'";
1591  }
1592  $sql2 .= ")";
1593  }
1594  dol_syslog(get_class($this).'::setMultiLangs key = current_lang = '.$key);
1595  if (!$this->db->query($sql2)) {
1596  $this->error = $this->db->lasterror();
1597  return -1;
1598  }
1599  } elseif (isset($this->multilangs[$key])) {
1600  if (empty($this->multilangs["$key"]["label"])) {
1601  $this->errors[] = $key . ' : ' . $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Label"));
1602  return -1;
1603  }
1604 
1605  $sql = "SELECT rowid";
1606  $sql .= " FROM ".$this->db->prefix()."product_lang";
1607  $sql .= " WHERE fk_product = ".((int) $this->id);
1608  $sql .= " AND lang = '".$this->db->escape($key)."'";
1609 
1610  $result = $this->db->query($sql);
1611 
1612  if ($this->db->num_rows($result)) { // if there is already a description line for this language
1613  $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
1614  $sql2 .= " SET ";
1615  $sql2 .= " label = '".$this->db->escape($this->multilangs["$key"]["label"])."',";
1616  $sql2 .= " description = '".$this->db->escape($this->multilangs["$key"]["description"])."'";
1617  if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1618  $sql2 .= ", note = '".$this->db->escape($this->multilangs["$key"]["other"])."'";
1619  }
1620  $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
1621  } else {
1622  $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
1623  if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1624  $sql2 .= ", note";
1625  }
1626  $sql2 .= ")";
1627  $sql2 .= " VALUES(".$this->id.",'".$this->db->escape($key)."','".$this->db->escape($this->multilangs["$key"]["label"])."',";
1628  $sql2 .= " '".$this->db->escape($this->multilangs["$key"]["description"])."'";
1629  if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1630  $sql2 .= ", '".$this->db->escape($this->multilangs["$key"]["other"])."'";
1631  }
1632  $sql2 .= ")";
1633  }
1634 
1635  // We do not save if main fields are empty
1636  if ($this->multilangs["$key"]["label"] || $this->multilangs["$key"]["description"]) {
1637  if (!$this->db->query($sql2)) {
1638  $this->error = $this->db->lasterror();
1639  return -1;
1640  }
1641  }
1642  } else {
1643  // language is not current language and we didn't provide a multilang description for this language
1644  }
1645  }
1646 
1647  // Call trigger
1648  $result = $this->call_trigger('PRODUCT_SET_MULTILANGS', $user);
1649  if ($result < 0) {
1650  $this->error = $this->db->lasterror();
1651  return -1;
1652  }
1653  // End call triggers
1654 
1655  return 1;
1656  }
1657 
1666  public function delMultiLangs($langtodelete, $user)
1667  {
1668  $sql = "DELETE FROM ".$this->db->prefix()."product_lang";
1669  $sql .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($langtodelete)."'";
1670 
1671  dol_syslog(get_class($this).'::delMultiLangs', LOG_DEBUG);
1672  $result = $this->db->query($sql);
1673  if ($result) {
1674  // Call trigger
1675  $result = $this->call_trigger('PRODUCT_DEL_MULTILANGS', $user);
1676  if ($result < 0) {
1677  $this->error = $this->db->lasterror();
1678  dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
1679  return -1;
1680  }
1681  // End call triggers
1682  return 1;
1683  } else {
1684  $this->error = $this->db->lasterror();
1685  dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
1686  return -1;
1687  }
1688  }
1689 
1698  public function setAccountancyCode($type, $value)
1699  {
1700  global $user, $langs, $conf;
1701 
1702  $error = 0;
1703 
1704  $this->db->begin();
1705 
1706  if ($type == 'buy') {
1707  $field = 'accountancy_code_buy';
1708  } elseif ($type == 'buy_intra') {
1709  $field = 'accountancy_code_buy_intra';
1710  } elseif ($type == 'buy_export') {
1711  $field = 'accountancy_code_buy_export';
1712  } elseif ($type == 'sell') {
1713  $field = 'accountancy_code_sell';
1714  } elseif ($type == 'sell_intra') {
1715  $field = 'accountancy_code_sell_intra';
1716  } elseif ($type == 'sell_export') {
1717  $field = 'accountancy_code_sell_export';
1718  } else {
1719  return -1;
1720  }
1721 
1722  $sql = "UPDATE ".$this->db->prefix().$this->table_element." SET ";
1723  $sql .= "$field = '".$this->db->escape($value)."'";
1724  $sql .= " WHERE rowid = ".((int) $this->id);
1725 
1726  dol_syslog(__METHOD__, LOG_DEBUG);
1727  $resql = $this->db->query($sql);
1728 
1729  if ($resql) {
1730  // Call trigger
1731  $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1732  if ($result < 0) {
1733  $error++;
1734  }
1735  // End call triggers
1736 
1737  if ($error) {
1738  $this->db->rollback();
1739  return -1;
1740  }
1741 
1742  $this->$field = $value;
1743 
1744  $this->db->commit();
1745  return 1;
1746  } else {
1747  $this->error = $this->db->lasterror();
1748  $this->db->rollback();
1749  return -1;
1750  }
1751  }
1752 
1758  public function getMultiLangs()
1759  {
1760  global $langs;
1761 
1762  $current_lang = $langs->getDefaultLang();
1763 
1764  $sql = "SELECT lang, label, description, note as other";
1765  $sql .= " FROM ".$this->db->prefix()."product_lang";
1766  $sql .= " WHERE fk_product = ".((int) $this->id);
1767 
1768  $result = $this->db->query($sql);
1769  if ($result) {
1770  while ($obj = $this->db->fetch_object($result)) {
1771  //print 'lang='.$obj->lang.' current='.$current_lang.'<br>';
1772  if ($obj->lang == $current_lang) { // si on a les traduct. dans la langue courante on les charge en infos principales.
1773  $this->label = $obj->label;
1774  $this->description = $obj->description;
1775  $this->other = $obj->other;
1776  }
1777  $this->multilangs["$obj->lang"]["label"] = $obj->label;
1778  $this->multilangs["$obj->lang"]["description"] = $obj->description;
1779  $this->multilangs["$obj->lang"]["other"] = $obj->other;
1780  }
1781  return 1;
1782  } else {
1783  $this->error = "Error: ".$this->db->lasterror()." - ".$sql;
1784  return -1;
1785  }
1786  }
1787 
1794  private function getArrayForPriceCompare($level = 0)
1795  {
1796 
1797  $testExit = array('multiprices','multiprices_ttc','multiprices_base_type','multiprices_min','multiprices_min_ttc','multiprices_tva_tx','multiprices_recuperableonly');
1798 
1799  foreach ($testExit as $field) {
1800  if (!isset($this->$field)) {
1801  return array();
1802  }
1803  $tmparray = $this->$field;
1804  if (!isset($tmparray[$level])) {
1805  return array();
1806  }
1807  }
1808 
1809  $lastPrice = array(
1810  'level' => $level ? $level : 1,
1811  'multiprices' => doubleval($this->multiprices[$level]),
1812  'multiprices_ttc' => doubleval($this->multiprices_ttc[$level]),
1813  'multiprices_base_type' => $this->multiprices_base_type[$level],
1814  'multiprices_min' => doubleval($this->multiprices_min[$level]),
1815  'multiprices_min_ttc' => doubleval($this->multiprices_min_ttc[$level]),
1816  'multiprices_tva_tx' => doubleval($this->multiprices_tva_tx[$level]),
1817  'multiprices_recuperableonly' => doubleval($this->multiprices_recuperableonly[$level]),
1818  );
1819 
1820  return $lastPrice;
1821  }
1822 
1823 
1824  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1832  private function _log_price($user, $level = 0)
1833  {
1834  // phpcs:enable
1835  global $conf;
1836 
1837  $now = dol_now();
1838 
1839  // Clean parameters
1840  if (empty($this->price_by_qty)) {
1841  $this->price_by_qty = 0;
1842  }
1843 
1844  // Add new price
1845  $sql = "INSERT INTO ".$this->db->prefix()."product_price(price_level,date_price, fk_product, fk_user_author, price, price_ttc, price_base_type,tosell, tva_tx, default_vat_code, recuperableonly,";
1846  $sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, price_min,price_min_ttc,price_by_qty,entity,fk_price_expression) ";
1847  $sql .= " VALUES(".($level ? ((int) $level) : 1).", '".$this->db->idate($now)."', ".((int) $this->id).", ".((int) $user->id).", ".((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).",";
1848  $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');
1849  $sql .= ")";
1850 
1851  dol_syslog(get_class($this)."::_log_price", LOG_DEBUG);
1852  $resql = $this->db->query($sql);
1853  if (!$resql) {
1854  $this->error = $this->db->lasterror();
1855  dol_print_error($this->db);
1856  return -1;
1857  } else {
1858  return 1;
1859  }
1860  }
1861 
1862 
1863  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1871  public function log_price_delete($user, $rowid)
1872  {
1873  // phpcs:enable
1874  $sql = "DELETE FROM ".$this->db->prefix()."product_price_by_qty";
1875  $sql .= " WHERE fk_product_price = ".((int) $rowid);
1876  $resql = $this->db->query($sql);
1877 
1878  $sql = "DELETE FROM ".$this->db->prefix()."product_price";
1879  $sql .= " WHERE rowid=".((int) $rowid);
1880  $resql = $this->db->query($sql);
1881  if ($resql) {
1882  return 1;
1883  } else {
1884  $this->error = $this->db->lasterror();
1885  return -1;
1886  }
1887  }
1888 
1889 
1899  public function getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp = 0)
1900  {
1901  global $conf, $db, $hookmanager, $action;
1902 
1903  // Call hook if any
1904  if (is_object($hookmanager)) {
1905  $parameters = array('thirdparty_seller'=>$thirdparty_seller, 'thirdparty_buyer' => $thirdparty_buyer, 'pqp' => $pqp);
1906  // Note that $action and $object may have been modified by some hooks
1907  $reshook = $hookmanager->executeHooks('getSellPrice', $parameters, $this, $action);
1908  if ($reshook > 0) {
1909  return $hookmanager->resArray;
1910  }
1911  }
1912 
1913  // Update if prices fields are defined
1914  $tva_tx = get_default_tva($thirdparty_seller, $thirdparty_buyer, $this->id);
1915  $tva_npr = get_default_npr($thirdparty_seller, $thirdparty_buyer, $this->id);
1916  if (empty($tva_tx)) {
1917  $tva_npr = 0;
1918  }
1919 
1920  $pu_ht = $this->price;
1921  $pu_ttc = $this->price_ttc;
1922  $price_min = $this->price_min;
1923  $price_base_type = $this->price_base_type;
1924 
1925  // If price per segment
1926  if (!empty($conf->global->PRODUIT_MULTIPRICES) && !empty($thirdparty_buyer->price_level)) {
1927  $pu_ht = $this->multiprices[$thirdparty_buyer->price_level];
1928  $pu_ttc = $this->multiprices_ttc[$thirdparty_buyer->price_level];
1929  $price_min = $this->multiprices_min[$thirdparty_buyer->price_level];
1930  $price_base_type = $this->multiprices_base_type[$thirdparty_buyer->price_level];
1931  if (!empty($conf->global->PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL)) { // using this option is a bug. kept for backward compatibility
1932  if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
1933  $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
1934  }
1935  if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
1936  $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
1937  }
1938  if (empty($tva_tx)) {
1939  $tva_npr = 0;
1940  }
1941  }
1942  } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES)) {
1943  // If price per customer
1944  require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
1945 
1946  $prodcustprice = new Productcustomerprice($this->db);
1947 
1948  $filter = array('t.fk_product' => $this->id, 't.fk_soc' => $thirdparty_buyer->id);
1949 
1950  $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
1951  if ($result) {
1952  if (count($prodcustprice->lines) > 0) {
1953  $pu_ht = price($prodcustprice->lines[0]->price);
1954  $price_min = price($prodcustprice->lines[0]->price_min);
1955  $pu_ttc = price($prodcustprice->lines[0]->price_ttc);
1956  $price_base_type = $prodcustprice->lines[0]->price_base_type;
1957  $tva_tx = $prodcustprice->lines[0]->tva_tx;
1958  if ($prodcustprice->lines[0]->default_vat_code && !preg_match('/\‍(.*\‍)/', $tva_tx)) {
1959  $tva_tx .= ' ('.$prodcustprice->lines[0]->default_vat_code.')';
1960  }
1961  $tva_npr = $prodcustprice->lines[0]->recuperableonly;
1962  if (empty($tva_tx)) {
1963  $tva_npr = 0;
1964  }
1965  }
1966  }
1967  } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY)) {
1968  // If price per quantity
1969  if ($this->prices_by_qty[0]) {
1970  // yes, this product has some prices per quantity
1971  // Search price into product_price_by_qty from $this->id
1972  foreach ($this->prices_by_qty_list[0] as $priceforthequantityarray) {
1973  if ($priceforthequantityarray['rowid'] != $pqp) {
1974  continue;
1975  }
1976  // We found the price
1977  if ($priceforthequantityarray['price_base_type'] == 'HT') {
1978  $pu_ht = $priceforthequantityarray['unitprice'];
1979  } else {
1980  $pu_ttc = $priceforthequantityarray['unitprice'];
1981  }
1982  break;
1983  }
1984  }
1985  } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES)) {
1986  // If price per quantity and customer
1987  if ($this->prices_by_qty[$thirdparty_buyer->price_level]) {
1988  // yes, this product has some prices per quantity
1989  // Search price into product_price_by_qty from $this->id
1990  foreach ($this->prices_by_qty_list[$thirdparty_buyer->price_level] as $priceforthequantityarray) {
1991  if ($priceforthequantityarray['rowid'] != $pqp) {
1992  continue;
1993  }
1994  // We found the price
1995  if ($priceforthequantityarray['price_base_type'] == 'HT') {
1996  $pu_ht = $priceforthequantityarray['unitprice'];
1997  } else {
1998  $pu_ttc = $priceforthequantityarray['unitprice'];
1999  }
2000  break;
2001  }
2002  }
2003  }
2004 
2005  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);
2006  }
2007 
2008  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2022  public function get_buyprice($prodfournprice, $qty, $product_id = 0, $fourn_ref = '', $fk_soc = 0)
2023  {
2024  // phpcs:enable
2025  global $conf;
2026  $result = 0;
2027 
2028  // 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)
2029  $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2030  $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,";
2031  $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2032  $sql .= " pfp.packaging";
2033  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2034  $sql .= " WHERE pfp.rowid = ".((int) $prodfournprice);
2035  if ($qty > 0) {
2036  $sql .= " AND pfp.quantity <= ".((float) $qty);
2037  }
2038  $sql .= " ORDER BY pfp.quantity DESC";
2039 
2040  dol_syslog(get_class($this)."::get_buyprice first search by prodfournprice/qty", LOG_DEBUG);
2041  $resql = $this->db->query($sql);
2042  if ($resql) {
2043  $obj = $this->db->fetch_object($resql);
2044  if ($obj && $obj->quantity > 0) { // If we found a supplier prices from the id of supplier price
2045  if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2046  $prod_supplier = new ProductFournisseur($this->db);
2047  $prod_supplier->product_fourn_price_id = $obj->rowid;
2048  $prod_supplier->id = $obj->fk_product;
2049  $prod_supplier->fourn_qty = $obj->quantity;
2050  $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2051  $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2052 
2053  include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2054  $priceparser = new PriceParser($this->db);
2055  $price_result = $priceparser->parseProductSupplier($prod_supplier);
2056  if ($price_result >= 0) {
2057  $obj->price = $price_result;
2058  }
2059  }
2060  $this->product_fourn_price_id = $obj->rowid;
2061  $this->buyprice = $obj->price; // deprecated
2062  $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product of supplier
2063  $this->fourn_price_base_type = 'HT'; // Price base type
2064  $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2065  $this->ref_fourn = $obj->ref_supplier; // deprecated
2066  $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2067  $this->desc_supplier = $obj->desc_supplier; // desc supplier
2068  $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2069  $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2070  $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2071  $this->fourn_multicurrency_price = $obj->multicurrency_price;
2072  $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2073  $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2074  $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2075  $this->fourn_multicurrency_code = $obj->multicurrency_code;
2076  if (!empty($conf->global->PRODUCT_USE_SUPPLIER_PACKAGING)) {
2077  $this->packaging = $obj->packaging;
2078  }
2079  $result = $obj->fk_product;
2080  return $result;
2081  } else { // If not found
2082  // 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.
2083  $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2084  $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,";
2085  $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2086  $sql .= " pfp.packaging";
2087  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2088  $sql .= " WHERE 1 = 1";
2089  if ($product_id > 0) {
2090  $sql .= " AND pfp.fk_product = ".((int) $product_id);
2091  }
2092  if ($fourn_ref != 'none') {
2093  $sql .= " AND pfp.ref_fourn = '".$this->db->escape($fourn_ref)."'";
2094  }
2095  if ($fk_soc > 0) {
2096  $sql .= " AND pfp.fk_soc = ".((int) $fk_soc);
2097  }
2098  if ($qty > 0) {
2099  $sql .= " AND pfp.quantity <= ".((float) $qty);
2100  }
2101  $sql .= " ORDER BY pfp.quantity DESC";
2102  $sql .= " LIMIT 1";
2103 
2104  dol_syslog(get_class($this)."::get_buyprice second search from qty/ref/product_id", LOG_DEBUG);
2105  $resql = $this->db->query($sql);
2106  if ($resql) {
2107  $obj = $this->db->fetch_object($resql);
2108  if ($obj && $obj->quantity > 0) { // If found
2109  if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2110  $prod_supplier = new ProductFournisseur($this->db);
2111  $prod_supplier->product_fourn_price_id = $obj->rowid;
2112  $prod_supplier->id = $obj->fk_product;
2113  $prod_supplier->fourn_qty = $obj->quantity;
2114  $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2115  $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2116 
2117  include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2118  $priceparser = new PriceParser($this->db);
2119  $price_result = $priceparser->parseProductSupplier($prod_supplier);
2120  if ($result >= 0) {
2121  $obj->price = $price_result;
2122  }
2123  }
2124  $this->product_fourn_price_id = $obj->rowid;
2125  $this->buyprice = $obj->price; // deprecated
2126  $this->fourn_qty = $obj->quantity; // min quantity for price for a virtual supplier
2127  $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product for a virtual supplier
2128  $this->fourn_price_base_type = 'HT'; // Price base type for a virtual supplier
2129  $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2130  $this->ref_fourn = $obj->ref_supplier; // deprecated
2131  $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2132  $this->desc_supplier = $obj->desc_supplier; // desc supplier
2133  $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2134  $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2135  $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2136  $this->fourn_multicurrency_price = $obj->multicurrency_price;
2137  $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2138  $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2139  $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2140  $this->fourn_multicurrency_code = $obj->multicurrency_code;
2141  if (!empty($conf->global->PRODUCT_USE_SUPPLIER_PACKAGING)) {
2142  $this->packaging = $obj->packaging;
2143  }
2144  $result = $obj->fk_product;
2145  return $result;
2146  } else {
2147  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é.
2148  }
2149  } else {
2150  $this->error = $this->db->lasterror();
2151  return -3;
2152  }
2153  }
2154  } else {
2155  $this->error = $this->db->lasterror();
2156  return -2;
2157  }
2158  }
2159 
2160 
2177  public function updatePrice($newprice, $newpricebase, $user, $newvat = '', $newminprice = 0, $level = 0, $newnpr = 0, $newpbq = 0, $ignore_autogen = 0, $localtaxes_array = array(), $newdefaultvatcode = '')
2178  {
2179  global $conf, $langs;
2180 
2181  $lastPriceData = $this->getArrayForPriceCompare($level); // temporary store current price before update
2182 
2183  $id = $this->id;
2184 
2185  dol_syslog(get_class($this)."::update_price id=".$id." newprice=".$newprice." newpricebase=".$newpricebase." newminprice=".$newminprice." level=".$level." npr=".$newnpr." newdefaultvatcode=".$newdefaultvatcode);
2186 
2187  // Clean parameters
2188  if (empty($this->tva_tx)) {
2189  $this->tva_tx = 0;
2190  }
2191  if (empty($newnpr)) {
2192  $newnpr = 0;
2193  }
2194  if (empty($newminprice)) {
2195  $newminprice = 0;
2196  }
2197  if (empty($newminprice)) {
2198  $newminprice = 0;
2199  }
2200 
2201  // Check parameters
2202  if ($newvat == '') {
2203  $newvat = $this->tva_tx;
2204  }
2205 
2206  // If multiprices are enabled, then we check if the current product is subject to price autogeneration
2207  // Price will be modified ONLY when the first one is the one that is being modified
2208  if ((!empty($conf->global->PRODUIT_MULTIPRICES) || !empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES)) && !$ignore_autogen && $this->price_autogen && ($level == 1)) {
2209  return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpbq);
2210  }
2211 
2212  if (!empty($newminprice) && ($newminprice > $newprice)) {
2213  $this->error = 'ErrorPriceCantBeLowerThanMinPrice';
2214  return -1;
2215  }
2216 
2217  if ($newprice !== '' || $newprice === 0) {
2218  if ($newpricebase == 'TTC') {
2219  $price_ttc = price2num($newprice, 'MU');
2220  $price = price2num($newprice) / (1 + ($newvat / 100));
2221  $price = price2num($price, 'MU');
2222 
2223  if ($newminprice != '' || $newminprice == 0) {
2224  $price_min_ttc = price2num($newminprice, 'MU');
2225  $price_min = price2num($newminprice) / (1 + ($newvat / 100));
2226  $price_min = price2num($price_min, 'MU');
2227  } else {
2228  $price_min = 0;
2229  $price_min_ttc = 0;
2230  }
2231  } else {
2232  $price = price2num($newprice, 'MU');
2233  $price_ttc = ($newnpr != 1) ? (float) price2num($newprice) * (1 + ($newvat / 100)) : $price;
2234  $price_ttc = price2num($price_ttc, 'MU');
2235 
2236  if ($newminprice !== '' || $newminprice === 0) {
2237  $price_min = price2num($newminprice, 'MU');
2238  $price_min_ttc = price2num($newminprice) * (1 + ($newvat / 100));
2239  $price_min_ttc = price2num($price_min_ttc, 'MU');
2240  //print 'X'.$newminprice.'-'.$price_min;
2241  } else {
2242  $price_min = 0;
2243  $price_min_ttc = 0;
2244  }
2245  }
2246  //print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc;
2247 
2248  if (count($localtaxes_array) > 0) {
2249  $localtaxtype1 = $localtaxes_array['0'];
2250  $localtax1 = $localtaxes_array['1'];
2251  $localtaxtype2 = $localtaxes_array['2'];
2252  $localtax2 = $localtaxes_array['3'];
2253  } else {
2254  // if array empty, we try to use the vat code
2255  if (!empty($newdefaultvatcode)) {
2256  global $mysoc;
2257  // Get record from code
2258  $sql = "SELECT t.rowid, t.code, t.recuperableonly, t.localtax1, t.localtax2, t.localtax1_type, t.localtax2_type";
2259  $sql .= " FROM ".MAIN_DB_PREFIX."c_tva as t, ".MAIN_DB_PREFIX."c_country as c";
2260  $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '".$this->db->escape($mysoc->country_code)."'";
2261  $sql .= " AND t.taux = ".((float) $newdefaultvatcode)." AND t.active = 1";
2262  $sql .= " AND t.code = '".$this->db->escape($newdefaultvatcode)."'";
2263  $resql = $this->db->query($sql);
2264  if ($resql) {
2265  $obj = $this->db->fetch_object($resql);
2266  if ($obj) {
2267  $npr = $obj->recuperableonly;
2268  $localtax1 = $obj->localtax1;
2269  $localtax2 = $obj->localtax2;
2270  $localtaxtype1 = $obj->localtax1_type;
2271  $localtaxtype2 = $obj->localtax2_type;
2272  }
2273  }
2274  } else {
2275  // old method. deprecated because we can't retrieve type
2276  $localtaxtype1 = '0';
2277  $localtax1 = get_localtax($newvat, 1);
2278  $localtaxtype2 = '0';
2279  $localtax2 = get_localtax($newvat, 2);
2280  }
2281  }
2282  if (empty($localtax1)) {
2283  $localtax1 = 0; // If = '' then = 0
2284  }
2285  if (empty($localtax2)) {
2286  $localtax2 = 0; // If = '' then = 0
2287  }
2288 
2289  $this->db->begin();
2290 
2291  // Ne pas mettre de quote sur les numeriques decimaux.
2292  // Ceci provoque des stockages avec arrondis en base au lieu des valeurs exactes.
2293  $sql = "UPDATE ".$this->db->prefix()."product SET";
2294  $sql .= " price_base_type='".$this->db->escape($newpricebase)."',";
2295  $sql .= " price=".$price.",";
2296  $sql .= " price_ttc=".$price_ttc.",";
2297  $sql .= " price_min=".$price_min.",";
2298  $sql .= " price_min_ttc=".$price_min_ttc.",";
2299  $sql .= " localtax1_tx=".($localtax1 >= 0 ? $localtax1 : 'NULL').",";
2300  $sql .= " localtax2_tx=".($localtax2 >= 0 ? $localtax2 : 'NULL').",";
2301  $sql .= " localtax1_type=".($localtaxtype1 != '' ? "'".$this->db->escape($localtaxtype1)."'" : "'0'").",";
2302  $sql .= " localtax2_type=".($localtaxtype2 != '' ? "'".$this->db->escape($localtaxtype2)."'" : "'0'").",";
2303  $sql .= " default_vat_code=".($newdefaultvatcode ? "'".$this->db->escape($newdefaultvatcode)."'" : "null").",";
2304  $sql .= " tva_tx='".price2num($newvat)."',";
2305  $sql .= " recuperableonly='".$this->db->escape($newnpr)."'";
2306  $sql .= " WHERE rowid = ".((int) $id);
2307 
2308  dol_syslog(get_class($this)."::update_price", LOG_DEBUG);
2309  $resql = $this->db->query($sql);
2310  if ($resql) {
2311  $this->multiprices[$level] = $price;
2312  $this->multiprices_ttc[$level] = $price_ttc;
2313  $this->multiprices_min[$level] = $price_min;
2314  $this->multiprices_min_ttc[$level] = $price_min_ttc;
2315  $this->multiprices_base_type[$level] = $newpricebase;
2316  $this->multiprices_default_vat_code[$level] = $newdefaultvatcode;
2317  $this->multiprices_tva_tx[$level] = $newvat;
2318  $this->multiprices_recuperableonly[$level] = $newnpr;
2319 
2320  $this->price = $price;
2321  $this->price_ttc = $price_ttc;
2322  $this->price_min = $price_min;
2323  $this->price_min_ttc = $price_min_ttc;
2324  $this->price_base_type = $newpricebase;
2325  $this->default_vat_code = $newdefaultvatcode;
2326  $this->tva_tx = $newvat;
2327  $this->tva_npr = $newnpr;
2328  //Local taxes
2329  $this->localtax1_tx = $localtax1;
2330  $this->localtax2_tx = $localtax2;
2331  $this->localtax1_type = $localtaxtype1;
2332  $this->localtax2_type = $localtaxtype2;
2333 
2334  // Price by quantity
2335  $this->price_by_qty = $newpbq;
2336 
2337  // check if price have really change before log
2338  $newPriceData = $this->getArrayForPriceCompare($level);
2339  if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || empty($conf->global->PRODUIT_MULTIPRICES)) {
2340  $this->_log_price($user, $level); // Save price for level into table product_price
2341  }
2342 
2343  $this->level = $level; // Store level of price edited for trigger
2344 
2345  // Call trigger
2346  $result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user);
2347  if ($result < 0) {
2348  $this->db->rollback();
2349  return -1;
2350  }
2351  // End call triggers
2352 
2353  $this->db->commit();
2354  } else {
2355  $this->db->rollback();
2356  $this->error = $this->db->lasterror();
2357  return -1;
2358  }
2359  }
2360 
2361  return 1;
2362  }
2363 
2371  public function setPriceExpression($expression_id)
2372  {
2373  global $user;
2374 
2375  $this->fk_price_expression = $expression_id;
2376 
2377  return $this->update($this->id, $user);
2378  }
2379 
2392  public function fetch($id = '', $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0)
2393  {
2394  include_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
2395 
2396  global $langs, $conf;
2397 
2398  dol_syslog(get_class($this)."::fetch id=".$id." ref=".$ref." ref_ext=".$ref_ext);
2399 
2400  // Check parameters
2401  if (!$id && !$ref && !$ref_ext && !$barcode) {
2402  $this->error = 'ErrorWrongParameters';
2403  dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR);
2404  return -1;
2405  }
2406 
2407  $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,";
2408  $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,";
2409  $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,";
2410  $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
2411  $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,";
2412  if (empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
2413  $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,";
2414  } else {
2415  $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,";
2416  }
2417 
2418  //For MultiCompany
2419  //PMP per entity & Stocks Sharings stock_reel includes only stocks shared with this entity
2420  $separatedEntityPMP = false; // Set to true to get the AWP from table llx_product_perentity instead of field 'pmp' into llx_product.
2421  $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.
2422  $visibleWarehousesEntities = $conf->entity;
2423  if (!empty($conf->global->MULTICOMPANY_PRODUCT_SHARING_ENABLED)) {
2424  if (!empty($conf->global->MULTICOMPANY_PMP_PER_ENTITY_ENABLED)) {
2425  $checkPMPPerEntity = $this->db->query("SELECT pmp FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = ".((int) $id)." AND entity = ".(int) $conf->entity);
2426  if ($this->db->num_rows($checkPMPPerEntity)>0) {
2427  $separatedEntityPMP = true;
2428  }
2429  }
2430  global $mc;
2431  $separatedStock = true;
2432  if (isset($mc->sharings['stock']) && !empty($mc->sharings['stock'])) {
2433  $visibleWarehousesEntities .= "," . implode(",", $mc->sharings['stock']);
2434  }
2435  }
2436  if ($separatedEntityPMP) {
2437  $sql .= " ppe.pmp,";
2438  } else {
2439  $sql .= " p.pmp,";
2440  }
2441  $sql .= " p.datec, p.tms, p.import_key, p.entity, p.desiredstock, p.tobatch, p.batch_mask, p.fk_unit,";
2442  $sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf,";
2443  if ($separatedStock) {
2444  $sql .= " SUM(sp.reel) as stock";
2445  } else {
2446  $sql .= " p.stock";
2447  }
2448  $sql .= " FROM ".$this->db->prefix()."product as p";
2449  if (!empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED) || $separatedEntityPMP) {
2450  $sql .= " LEFT JOIN " . $this->db->prefix() . "product_perentity as ppe ON ppe.fk_product = p.rowid AND ppe.entity = " . ((int) $conf->entity);
2451  }
2452  if ($separatedStock) {
2453  $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)."))";
2454  }
2455 
2456  if ($id) {
2457  $sql .= " WHERE p.rowid = ".((int) $id);
2458  } else {
2459  $sql .= " WHERE p.entity IN (".getEntity($this->element).")";
2460  if ($ref) {
2461  $sql .= " AND p.ref = '".$this->db->escape($ref)."'";
2462  } elseif ($ref_ext) {
2463  $sql .= " AND p.ref_ext = '".$this->db->escape($ref_ext)."'";
2464  } elseif ($barcode) {
2465  $sql .= " AND p.barcode = '".$this->db->escape($barcode)."'";
2466  }
2467  }
2468  if ($separatedStock) {
2469  $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,";
2470  $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,";
2471  $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,";
2472  $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
2473  $sql .= " p.surface, p.surface_units, p.volume, p.volume_units, p.barcode, p.fk_barcode_type, p.finished,";
2474  if (empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
2475  $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,";
2476  } else {
2477  $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,";
2478  }
2479  if ($separatedEntityPMP) {
2480  $sql .= " ppe.pmp,";
2481  } else {
2482  $sql .= " p.pmp,";
2483  }
2484  $sql .= " p.datec, p.tms, p.import_key, p.entity, p.desiredstock, p.tobatch, p.batch_mask, p.fk_unit,";
2485  $sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf";
2486  if (!$separatedStock) {
2487  $sql .= ", p.stock";
2488  }
2489  }
2490 
2491  $resql = $this->db->query($sql);
2492  if ($resql) {
2493  unset($this->oldcopy);
2494 
2495  if ($this->db->num_rows($resql) > 0) {
2496  $obj = $this->db->fetch_object($resql);
2497 
2498  $this->id = $obj->rowid;
2499  $this->ref = $obj->ref;
2500  $this->ref_ext = $obj->ref_ext;
2501  $this->label = $obj->label;
2502  $this->description = $obj->description;
2503  $this->url = $obj->url;
2504  $this->note_public = $obj->note_public;
2505  $this->note_private = $obj->note_private;
2506  $this->note = $obj->note_private; // deprecated
2507 
2508  $this->type = $obj->fk_product_type;
2509  $this->status = $obj->tosell;
2510  $this->status_buy = $obj->tobuy;
2511  $this->status_batch = $obj->tobatch;
2512  $this->batch_mask = $obj->batch_mask;
2513 
2514  $this->customcode = $obj->customcode;
2515  $this->country_id = $obj->fk_country;
2516  $this->country_code = getCountry($this->country_id, 2, $this->db);
2517  $this->state_id = $obj->fk_state;
2518  $this->lifetime = $obj->lifetime;
2519  $this->qc_frequency = $obj->qc_frequency;
2520  $this->price = $obj->price;
2521  $this->price_ttc = $obj->price_ttc;
2522  $this->price_min = $obj->price_min;
2523  $this->price_min_ttc = $obj->price_min_ttc;
2524  $this->price_base_type = $obj->price_base_type;
2525  $this->cost_price = $obj->cost_price;
2526  $this->default_vat_code = $obj->default_vat_code;
2527  $this->tva_tx = $obj->tva_tx;
2529  $this->tva_npr = $obj->tva_npr;
2530  $this->recuperableonly = $obj->tva_npr; // For backward compatibility
2532  $this->localtax1_tx = $obj->localtax1_tx;
2533  $this->localtax2_tx = $obj->localtax2_tx;
2534  $this->localtax1_type = $obj->localtax1_type;
2535  $this->localtax2_type = $obj->localtax2_type;
2536 
2537  $this->finished = $obj->finished;
2538  $this->fk_default_bom = $obj->fk_default_bom;
2539 
2540  $this->duration = $obj->duration;
2541  $this->duration_value = $obj->duration ? substr($obj->duration, 0, dol_strlen($obj->duration) - 1) : null;
2542  $this->duration_unit = $obj->duration ? substr($obj->duration, -1) : null;
2543  $this->canvas = $obj->canvas;
2544  $this->net_measure = $obj->net_measure;
2545  $this->net_measure_units = $obj->net_measure_units;
2546  $this->weight = $obj->weight;
2547  $this->weight_units = $obj->weight_units;
2548  $this->length = $obj->length;
2549  $this->length_units = $obj->length_units;
2550  $this->width = $obj->width;
2551  $this->width_units = $obj->width_units;
2552  $this->height = $obj->height;
2553  $this->height_units = $obj->height_units;
2554 
2555  $this->surface = $obj->surface;
2556  $this->surface_units = $obj->surface_units;
2557  $this->volume = $obj->volume;
2558  $this->volume_units = $obj->volume_units;
2559  $this->barcode = $obj->barcode;
2560  $this->barcode_type = $obj->fk_barcode_type;
2561 
2562  $this->accountancy_code_buy = $obj->accountancy_code_buy;
2563  $this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra;
2564  $this->accountancy_code_buy_export = $obj->accountancy_code_buy_export;
2565  $this->accountancy_code_sell = $obj->accountancy_code_sell;
2566  $this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra;
2567  $this->accountancy_code_sell_export = $obj->accountancy_code_sell_export;
2568 
2569  $this->fk_default_warehouse = $obj->fk_default_warehouse;
2570  $this->fk_default_workstation = $obj->fk_default_workstation;
2571  $this->seuil_stock_alerte = $obj->seuil_stock_alerte;
2572  $this->desiredstock = $obj->desiredstock;
2573  $this->stock_reel = $obj->stock;
2574  $this->pmp = $obj->pmp;
2575 
2576  $this->date_creation = $obj->datec;
2577  $this->date_modification = $obj->tms;
2578  $this->import_key = $obj->import_key;
2579  $this->entity = $obj->entity;
2580 
2581  $this->ref_ext = $obj->ref_ext;
2582  $this->fk_price_expression = $obj->fk_price_expression;
2583  $this->fk_unit = $obj->fk_unit;
2584  $this->price_autogen = $obj->price_autogen;
2585  $this->model_pdf = $obj->model_pdf;
2586 
2587  $this->mandatory_period = $obj->mandatory_period;
2588 
2589  $this->db->free($resql);
2590 
2591  // fetch optionals attributes and labels
2592  $this->fetch_optionals();
2593 
2594  // Multilangs
2595  if (getDolGlobalInt('MAIN_MULTILANGS') && empty($ignore_lang_load)) {
2596  $this->getMultiLangs();
2597  }
2598 
2599  // Load multiprices array
2600  if (!empty($conf->global->PRODUIT_MULTIPRICES) && empty($ignore_price_load)) { // prices per segment
2601  for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) {
2602  $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2603  $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
2604  $sql .= " FROM ".$this->db->prefix()."product_price";
2605  $sql .= " WHERE entity IN (".getEntity('productprice').")";
2606  $sql .= " AND price_level=".((int) $i);
2607  $sql .= " AND fk_product = ".((int) $this->id);
2608  $sql .= " ORDER BY date_price DESC, rowid DESC"; // Get the most recent line
2609  $sql .= " LIMIT 1"; // Only the first one
2610  $resql = $this->db->query($sql);
2611  if ($resql) {
2612  $result = $this->db->fetch_array($resql);
2613 
2614  $this->multiprices[$i] = $result ? $result["price"] : null;
2615  $this->multiprices_ttc[$i] = $result ? $result["price_ttc"] : null;
2616  $this->multiprices_min[$i] = $result ? $result["price_min"] : null;
2617  $this->multiprices_min_ttc[$i] = $result ? $result["price_min_ttc"] : null;
2618  $this->multiprices_base_type[$i] = $result ? $result["price_base_type"] : null;
2619  // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
2620  $this->multiprices_tva_tx[$i] = $result ? $result["tva_tx"].($result ? ' ('.$result['default_vat_code'].')' : '') : null;
2621  $this->multiprices_recuperableonly[$i] = $result ? $result["recuperableonly"] : null;
2622 
2623  // Price by quantity
2624  /*
2625  $this->prices_by_qty[$i]=$result["price_by_qty"];
2626  $this->prices_by_qty_id[$i]=$result["rowid"];
2627  // Récuperation de la liste des prix selon qty si flag positionné
2628  if ($this->prices_by_qty[$i] == 1)
2629  {
2630  $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
2631  $sql.= " FROM ".$this->db->prefix()."product_price_by_qty";
2632  $sql.= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
2633  $sql.= " ORDER BY quantity ASC";
2634  $resultat=array();
2635  $resql = $this->db->query($sql);
2636  if ($resql)
2637  {
2638  $ii=0;
2639  while ($result= $this->db->fetch_array($resql)) {
2640  $resultat[$ii]=array();
2641  $resultat[$ii]["rowid"]=$result["rowid"];
2642  $resultat[$ii]["price"]= $result["price"];
2643  $resultat[$ii]["unitprice"]= $result["unitprice"];
2644  $resultat[$ii]["quantity"]= $result["quantity"];
2645  $resultat[$ii]["remise_percent"]= $result["remise_percent"];
2646  $resultat[$ii]["remise"]= $result["remise"]; // deprecated
2647  $resultat[$ii]["price_base_type"]= $result["price_base_type"];
2648  $ii++;
2649  }
2650  $this->prices_by_qty_list[$i]=$resultat;
2651  }
2652  else
2653  {
2654  dol_print_error($this->db);
2655  return -1;
2656  }
2657  }*/
2658  } else {
2659  $this->error = $this->db->lasterror;
2660  return -1;
2661  }
2662  }
2663  } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES) && empty($ignore_price_load)) { // prices per customers
2664  // Nothing loaded by default. List may be very long.
2665  } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY) && empty($ignore_price_load)) { // prices per quantity
2666  $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2667  $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid";
2668  $sql .= " FROM ".$this->db->prefix()."product_price";
2669  $sql .= " WHERE fk_product = ".((int) $this->id);
2670  $sql .= " ORDER BY date_price DESC, rowid DESC";
2671  $sql .= " LIMIT 1";
2672  $resql = $this->db->query($sql);
2673  if ($resql) {
2674  $result = $this->db->fetch_array($resql);
2675 
2676  // Price by quantity
2677  $this->prices_by_qty[0] = $result["price_by_qty"];
2678  $this->prices_by_qty_id[0] = $result["rowid"];
2679  // Récuperation de la liste des prix selon qty si flag positionné
2680  if ($this->prices_by_qty[0] == 1) {
2681  $sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise, remise, price_base_type";
2682  $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
2683  $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[0]);
2684  $sql .= " ORDER BY quantity ASC";
2685  $resultat = array();
2686  $resql = $this->db->query($sql);
2687  if ($resql) {
2688  $ii = 0;
2689  while ($result = $this->db->fetch_array($resql)) {
2690  $resultat[$ii] = array();
2691  $resultat[$ii]["rowid"] = $result["rowid"];
2692  $resultat[$ii]["price"] = $result["price"];
2693  $resultat[$ii]["unitprice"] = $result["unitprice"];
2694  $resultat[$ii]["quantity"] = $result["quantity"];
2695  $resultat[$ii]["remise_percent"] = $result["remise_percent"];
2696  //$resultat[$ii]["remise"]= $result["remise"]; // deprecated
2697  $resultat[$ii]["price_base_type"] = $result["price_base_type"];
2698  $ii++;
2699  }
2700  $this->prices_by_qty_list[0] = $resultat;
2701  } else {
2702  $this->error = $this->db->lasterror;
2703  return -1;
2704  }
2705  }
2706  } else {
2707  $this->error = $this->db->lasterror;
2708  return -1;
2709  }
2710  } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES) && empty($ignore_price_load)) { // prices per customer and quantity
2711  for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) {
2712  $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2713  $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
2714  $sql .= " FROM ".$this->db->prefix()."product_price";
2715  $sql .= " WHERE entity IN (".getEntity('productprice').")";
2716  $sql .= " AND price_level=".((int) $i);
2717  $sql .= " AND fk_product = ".((int) $this->id);
2718  $sql .= " ORDER BY date_price DESC, rowid DESC";
2719  $sql .= " LIMIT 1";
2720  $resql = $this->db->query($sql);
2721  if ($resql) {
2722  $result = $this->db->fetch_array($resql);
2723 
2724  $this->multiprices[$i] = $result["price"];
2725  $this->multiprices_ttc[$i] = $result["price_ttc"];
2726  $this->multiprices_min[$i] = $result["price_min"];
2727  $this->multiprices_min_ttc[$i] = $result["price_min_ttc"];
2728  $this->multiprices_base_type[$i] = $result["price_base_type"];
2729  // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
2730  $this->multiprices_tva_tx[$i] = $result["tva_tx"]; // TODO Add ' ('.$result['default_vat_code'].')'
2731  $this->multiprices_recuperableonly[$i] = $result["recuperableonly"];
2732 
2733  // Price by quantity
2734  $this->prices_by_qty[$i] = $result["price_by_qty"];
2735  $this->prices_by_qty_id[$i] = $result["rowid"];
2736  // Récuperation de la liste des prix selon qty si flag positionné
2737  if ($this->prices_by_qty[$i] == 1) {
2738  $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
2739  $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
2740  $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
2741  $sql .= " ORDER BY quantity ASC";
2742  $resultat = array();
2743  $resql = $this->db->query($sql);
2744  if ($resql) {
2745  $ii = 0;
2746  while ($result = $this->db->fetch_array($resql)) {
2747  $resultat[$ii] = array();
2748  $resultat[$ii]["rowid"] = $result["rowid"];
2749  $resultat[$ii]["price"] = $result["price"];
2750  $resultat[$ii]["unitprice"] = $result["unitprice"];
2751  $resultat[$ii]["quantity"] = $result["quantity"];
2752  $resultat[$ii]["remise_percent"] = $result["remise_percent"];
2753  $resultat[$ii]["remise"] = $result["remise"]; // deprecated
2754  $resultat[$ii]["price_base_type"] = $result["price_base_type"];
2755  $ii++;
2756  }
2757  $this->prices_by_qty_list[$i] = $resultat;
2758  } else {
2759  $this->error = $this->db->lasterror;
2760  return -1;
2761  }
2762  }
2763  } else {
2764  $this->error = $this->db->lasterror;
2765  return -1;
2766  }
2767  }
2768  }
2769 
2770  if (isModEnabled('dynamicprices') && !empty($this->fk_price_expression) && empty($ignore_expression)) {
2771  include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2772  $priceparser = new PriceParser($this->db);
2773  $price_result = $priceparser->parseProduct($this);
2774  if ($price_result >= 0) {
2775  $this->price = $price_result;
2776  // Calculate the VAT
2777  $this->price_ttc = price2num($this->price) * (1 + ($this->tva_tx / 100));
2778  $this->price_ttc = price2num($this->price_ttc, 'MU');
2779  }
2780  }
2781 
2782  // We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product.
2783  // Instead we just init the stock_warehouse array
2784  $this->stock_warehouse = array();
2785 
2786  return 1;
2787  } else {
2788  return 0;
2789  }
2790  } else {
2791  $this->error = $this->db->lasterror();
2792  return -1;
2793  }
2794  }
2795 
2796  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2803  public function load_stats_mo($socid = 0)
2804  {
2805  // phpcs:enable
2806  global $user, $hookmanager, $action;
2807 
2808  $error = 0;
2809 
2810  foreach (array('toconsume', 'consumed', 'toproduce', 'produced') as $role) {
2811  $this->stats_mo['customers_'.$role] = 0;
2812  $this->stats_mo['nb_'.$role] = 0;
2813  $this->stats_mo['qty_'.$role] = 0;
2814 
2815  $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
2816  $sql .= " SUM(mp.qty) as qty";
2817  $sql .= " FROM ".$this->db->prefix()."mrp_mo as c";
2818  $sql .= " INNER JOIN ".$this->db->prefix()."mrp_production as mp ON mp.fk_mo=c.rowid";
2819  if (empty($user->rights->societe->client->voir) && !$socid) {
2820  $sql .= "INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc=c.fk_soc AND sc.fk_user = ".((int) $user->id);
2821  }
2822  $sql .= " WHERE ";
2823  $sql .= " c.entity IN (".getEntity('mo').")";
2824 
2825  $sql .= " AND mp.fk_product = ".((int) $this->id);
2826  $sql .= " AND mp.role ='".$this->db->escape($role)."'";
2827  if ($socid > 0) {
2828  $sql .= " AND c.fk_soc = ".((int) $socid);
2829  }
2830 
2831  $result = $this->db->query($sql);
2832  if ($result) {
2833  $obj = $this->db->fetch_object($result);
2834  $this->stats_mo['customers_'.$role] = $obj->nb_customers ? $obj->nb_customers : 0;
2835  $this->stats_mo['nb_'.$role] = $obj->nb ? $obj->nb : 0;
2836  $this->stats_mo['qty_'.$role] = $obj->qty ? price2num($obj->qty, 'MS') : 0; // qty may be a float due to the SUM()
2837  } else {
2838  $this->error = $this->db->error();
2839  $error++;
2840  }
2841  }
2842 
2843  if (!empty($error)) {
2844  return -1;
2845  }
2846 
2847  $parameters = array('socid' => $socid);
2848  $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
2849  if ($reshook > 0) {
2850  $this->stats_mo = $hookmanager->resArray['stats_mo'];
2851  }
2852 
2853  return 1;
2854  }
2855 
2856  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2863  public function load_stats_bom($socid = 0)
2864  {
2865  // phpcs:enable
2866  global $user, $hookmanager, $action;
2867 
2868  $error = 0;
2869 
2870  $this->stats_bom['nb_toproduce'] = 0;
2871  $this->stats_bom['nb_toconsume'] = 0;
2872  $this->stats_bom['qty_toproduce'] = 0;
2873  $this->stats_bom['qty_toconsume'] = 0;
2874 
2875  $sql = "SELECT COUNT(DISTINCT b.rowid) as nb_toproduce,";
2876  $sql .= " SUM(b.qty) as qty_toproduce";
2877  $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
2878  $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
2879  $sql .= " WHERE ";
2880  $sql .= " b.entity IN (".getEntity('bom').")";
2881  $sql .= " AND b.fk_product =".((int) $this->id);
2882  $sql .= " GROUP BY b.rowid";
2883 
2884  $result = $this->db->query($sql);
2885  if ($result) {
2886  $obj = $this->db->fetch_object($result);
2887  $this->stats_bom['nb_toproduce'] = !empty($obj->nb_toproduce) ? $obj->nb_toproduce : 0;
2888  $this->stats_bom['qty_toproduce'] = !empty($obj->qty_toproduce) ? price2num($obj->qty_toproduce) : 0;
2889  } else {
2890  $this->error = $this->db->error();
2891  $error++;
2892  }
2893 
2894  $sql = "SELECT COUNT(DISTINCT bl.rowid) as nb_toconsume,";
2895  $sql .= " SUM(bl.qty) as qty_toconsume";
2896  $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
2897  $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
2898  $sql .= " WHERE ";
2899  $sql .= " b.entity IN (".getEntity('bom').")";
2900  $sql .= " AND bl.fk_product =".((int) $this->id);
2901 
2902  $result = $this->db->query($sql);
2903  if ($result) {
2904  $obj = $this->db->fetch_object($result);
2905  $this->stats_bom['nb_toconsume'] = !empty($obj->nb_toconsume) ? $obj->nb_toconsume : 0;
2906  $this->stats_bom['qty_toconsume'] = !empty($obj->qty_toconsume) ? price2num($obj->qty_toconsume) : 0;
2907  } else {
2908  $this->error = $this->db->error();
2909  $error++;
2910  }
2911 
2912  if (!empty($error)) {
2913  return -1;
2914  }
2915 
2916  $parameters = array('socid' => $socid);
2917  $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
2918  if ($reshook > 0) {
2919  $this->stats_bom = $hookmanager->resArray['stats_bom'];
2920  }
2921 
2922  return 1;
2923  }
2924 
2925  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2932  public function load_stats_propale($socid = 0)
2933  {
2934  // phpcs:enable
2935  global $conf, $user, $hookmanager, $action;
2936 
2937  $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,";
2938  $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
2939  $sql .= " FROM ".$this->db->prefix()."propaldet as pd";
2940  $sql .= ", ".$this->db->prefix()."propal as p";
2941  $sql .= ", ".$this->db->prefix()."societe as s";
2942  if (empty($user->rights->societe->client->voir) && !$socid) {
2943  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
2944  }
2945  $sql .= " WHERE p.rowid = pd.fk_propal";
2946  $sql .= " AND p.fk_soc = s.rowid";
2947  $sql .= " AND p.entity IN (".getEntity('propal').")";
2948  $sql .= " AND pd.fk_product = ".((int) $this->id);
2949  if (empty($user->rights->societe->client->voir) && !$socid) {
2950  $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
2951  }
2952  //$sql.= " AND pr.fk_statut != 0";
2953  if ($socid > 0) {
2954  $sql .= " AND p.fk_soc = ".((int) $socid);
2955  }
2956 
2957  $result = $this->db->query($sql);
2958  if ($result) {
2959  $obj = $this->db->fetch_object($result);
2960  $this->stats_propale['customers'] = $obj->nb_customers;
2961  $this->stats_propale['nb'] = $obj->nb;
2962  $this->stats_propale['rows'] = $obj->nb_rows;
2963  $this->stats_propale['qty'] = $obj->qty ? $obj->qty : 0;
2964 
2965  // if it's a virtual product, maybe it is in proposal by extension
2966  if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
2967  $TFather = $this->getFather();
2968  if (is_array($TFather) && !empty($TFather)) {
2969  foreach ($TFather as &$fatherData) {
2970  $pFather = new Product($this->db);
2971  $pFather->id = $fatherData['id'];
2972  $qtyCoef = $fatherData['qty'];
2973 
2974  if ($fatherData['incdec']) {
2975  $pFather->load_stats_propale($socid);
2976 
2977  $this->stats_propale['customers'] += $pFather->stats_propale['customers'];
2978  $this->stats_propale['nb'] += $pFather->stats_propale['nb'];
2979  $this->stats_propale['rows'] += $pFather->stats_propale['rows'];
2980  $this->stats_propale['qty'] += $pFather->stats_propale['qty'] * $qtyCoef;
2981  }
2982  }
2983  }
2984  }
2985 
2986  $parameters = array('socid' => $socid);
2987  $reshook = $hookmanager->executeHooks('loadStatsCustomerProposal', $parameters, $this, $action);
2988  if ($reshook > 0) {
2989  $this->stats_propale = $hookmanager->resArray['stats_propale'];
2990  }
2991 
2992  return 1;
2993  } else {
2994  $this->error = $this->db->error();
2995  return -1;
2996  }
2997  }
2998 
2999 
3000  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3007  public function load_stats_proposal_supplier($socid = 0)
3008  {
3009  // phpcs:enable
3010  global $conf, $user, $hookmanager, $action;
3011 
3012  $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,";
3013  $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
3014  $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as pd";
3015  $sql .= ", ".$this->db->prefix()."supplier_proposal as p";
3016  $sql .= ", ".$this->db->prefix()."societe as s";
3017  if (empty($user->rights->societe->client->voir) && !$socid) {
3018  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3019  }
3020  $sql .= " WHERE p.rowid = pd.fk_supplier_proposal";
3021  $sql .= " AND p.fk_soc = s.rowid";
3022  $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
3023  $sql .= " AND pd.fk_product = ".((int) $this->id);
3024  if (empty($user->rights->societe->client->voir) && !$socid) {
3025  $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3026  }
3027  //$sql.= " AND pr.fk_statut != 0";
3028  if ($socid > 0) {
3029  $sql .= " AND p.fk_soc = ".((int) $socid);
3030  }
3031 
3032  $result = $this->db->query($sql);
3033  if ($result) {
3034  $obj = $this->db->fetch_object($result);
3035  $this->stats_proposal_supplier['suppliers'] = $obj->nb_suppliers;
3036  $this->stats_proposal_supplier['nb'] = $obj->nb;
3037  $this->stats_proposal_supplier['rows'] = $obj->nb_rows;
3038  $this->stats_proposal_supplier['qty'] = $obj->qty ? $obj->qty : 0;
3039 
3040  $parameters = array('socid' => $socid);
3041  $reshook = $hookmanager->executeHooks('loadStatsSupplierProposal', $parameters, $this, $action);
3042  if ($reshook > 0) {
3043  $this->stats_proposal_supplier = $hookmanager->resArray['stats_proposal_supplier'];
3044  }
3045 
3046  return 1;
3047  } else {
3048  $this->error = $this->db->error();
3049  return -1;
3050  }
3051  }
3052 
3053 
3054  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3063  public function load_stats_commande($socid = 0, $filtrestatut = '', $forVirtualStock = 0)
3064  {
3065  // phpcs:enable
3066  global $conf, $user, $hookmanager, $action;
3067 
3068  $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3069  $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3070  $sql .= " FROM ".$this->db->prefix()."commandedet as cd";
3071  $sql .= ", ".$this->db->prefix()."commande as c";
3072  $sql .= ", ".$this->db->prefix()."societe as s";
3073  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3074  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3075  }
3076  $sql .= " WHERE c.rowid = cd.fk_commande";
3077  $sql .= " AND c.fk_soc = s.rowid";
3078  $sql .= " AND c.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'commande').")";
3079  $sql .= " AND cd.fk_product = ".((int) $this->id);
3080  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3081  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3082  }
3083  if ($socid > 0) {
3084  $sql .= " AND c.fk_soc = ".((int) $socid);
3085  }
3086  if ($filtrestatut <> '') {
3087  $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")";
3088  }
3089 
3090  $result = $this->db->query($sql);
3091  if ($result) {
3092  $obj = $this->db->fetch_object($result);
3093  $this->stats_commande['customers'] = $obj->nb_customers;
3094  $this->stats_commande['nb'] = $obj->nb;
3095  $this->stats_commande['rows'] = $obj->nb_rows;
3096  $this->stats_commande['qty'] = $obj->qty ? $obj->qty : 0;
3097 
3098  // if it's a virtual product, maybe it is in order by extension
3099  if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
3100  $TFather = $this->getFather();
3101  if (is_array($TFather) && !empty($TFather)) {
3102  foreach ($TFather as &$fatherData) {
3103  $pFather = new Product($this->db);
3104  $pFather->id = $fatherData['id'];
3105  $qtyCoef = $fatherData['qty'];
3106 
3107  if ($fatherData['incdec']) {
3108  $pFather->load_stats_commande($socid, $filtrestatut);
3109 
3110  $this->stats_commande['customers'] += $pFather->stats_commande['customers'];
3111  $this->stats_commande['nb'] += $pFather->stats_commande['nb'];
3112  $this->stats_commande['rows'] += $pFather->stats_commande['rows'];
3113  $this->stats_commande['qty'] += $pFather->stats_commande['qty'] * $qtyCoef;
3114  }
3115  }
3116  }
3117  }
3118 
3119  // If stock decrease is on invoice validation, the theorical stock continue to
3120  // count the orders to ship in theorical stock when some are already removed by invoice validation.
3121  if ($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_ON_BILL)) {
3122  if (!empty($conf->global->DECREASE_ONLY_UNINVOICEDPRODUCTS)) {
3123  // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is on, we make a compensation but only if order not yet invoice.
3124  $adeduire = 0;
3125  $sql = "SELECT SUM(".$this->db->ifsql('f.type=2', -1, 1)." * fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3126  $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid";
3127  $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'))";
3128  $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid";
3129  $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3130 
3131  dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3132  $resql = $this->db->query($sql);
3133  if ($resql) {
3134  if ($this->db->num_rows($resql) > 0) {
3135  $obj = $this->db->fetch_object($resql);
3136  $adeduire += $obj->count;
3137  }
3138  }
3139 
3140  $this->stats_commande['qty'] -= $adeduire;
3141  } else {
3142  // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is off, we make a compensation with lines of invoices linked to the order
3143  include_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
3144 
3145  // For every order having invoice already validated we need to decrease stock cause it's in physical stock
3146  $adeduire = 0;
3147  $sql = "SELECT sum(".$this->db->ifsql('f.type=2', -1, 1)." * fd.qty) as count FROM ".MAIN_DB_PREFIX."facturedet as fd ";
3148  $sql .= " JOIN ".MAIN_DB_PREFIX."facture as f ON fd.fk_facture = f.rowid";
3149  $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'))";
3150  $sql .= " JOIN ".MAIN_DB_PREFIX."commande as c ON el.fk_source = c.rowid";
3151  $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND f.fk_statut > ".Facture::STATUS_DRAFT." AND fd.fk_product = ".((int) $this->id);
3152 
3153  dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3154  $resql = $this->db->query($sql);
3155  if ($resql) {
3156  if ($this->db->num_rows($resql) > 0) {
3157  $obj = $this->db->fetch_object($resql);
3158  $adeduire += $obj->count;
3159  }
3160  } else {
3161  $this->error = $this->db->error();
3162  return -1;
3163  }
3164 
3165  $this->stats_commande['qty'] -= $adeduire;
3166  }
3167  }
3168 
3169  $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3170  $reshook = $hookmanager->executeHooks('loadStatsCustomerOrder', $parameters, $this, $action);
3171  if ($reshook > 0) {
3172  $this->stats_commande = $hookmanager->resArray['stats_commande'];
3173  }
3174  return 1;
3175  } else {
3176  $this->error = $this->db->error();
3177  return -1;
3178  }
3179  }
3180 
3181  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3191  public function load_stats_commande_fournisseur($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3192  {
3193  // phpcs:enable
3194  global $conf, $user, $hookmanager, $action;
3195 
3196  $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,";
3197  $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3198  $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as cd";
3199  $sql .= ", ".$this->db->prefix()."commande_fournisseur as c";
3200  $sql .= ", ".$this->db->prefix()."societe as s";
3201  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3202  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3203  }
3204  $sql .= " WHERE c.rowid = cd.fk_commande";
3205  $sql .= " AND c.fk_soc = s.rowid";
3206  $sql .= " AND c.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'supplier_order').")";
3207  $sql .= " AND cd.fk_product = ".((int) $this->id);
3208  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3209  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3210  }
3211  if ($socid > 0) {
3212  $sql .= " AND c.fk_soc = ".((int) $socid);
3213  }
3214  if ($filtrestatut != '') {
3215  $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")"; // Peut valoir 0
3216  }
3217  if (!empty($dateofvirtualstock)) {
3218  $sql .= " AND c.date_livraison <= '".$this->db->idate($dateofvirtualstock)."'";
3219  }
3220 
3221  $result = $this->db->query($sql);
3222  if ($result) {
3223  $obj = $this->db->fetch_object($result);
3224  $this->stats_commande_fournisseur['suppliers'] = $obj->nb_suppliers;
3225  $this->stats_commande_fournisseur['nb'] = $obj->nb;
3226  $this->stats_commande_fournisseur['rows'] = $obj->nb_rows;
3227  $this->stats_commande_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3228 
3229  $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3230  $reshook = $hookmanager->executeHooks('loadStatsSupplierOrder', $parameters, $this, $action);
3231  if ($reshook > 0) {
3232  $this->stats_commande_fournisseur = $hookmanager->resArray['stats_commande_fournisseur'];
3233  }
3234 
3235  return 1;
3236  } else {
3237  $this->error = $this->db->error().' sql='.$sql;
3238  return -1;
3239  }
3240  }
3241 
3242  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3252  public function load_stats_sending($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $filterShipmentStatus = '')
3253  {
3254  // phpcs:enable
3255  global $conf, $user, $hookmanager, $action;
3256 
3257  $sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,";
3258  $sql .= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty";
3259  $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
3260  $sql .= ", ".$this->db->prefix()."commandedet as cd";
3261  $sql .= ", ".$this->db->prefix()."commande as c";
3262  $sql .= ", ".$this->db->prefix()."expedition as e";
3263  $sql .= ", ".$this->db->prefix()."societe as s";
3264  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3265  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3266  }
3267  $sql .= " WHERE e.rowid = ed.fk_expedition";
3268  $sql .= " AND c.rowid = cd.fk_commande";
3269  $sql .= " AND e.fk_soc = s.rowid";
3270  $sql .= " AND e.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'expedition').")";
3271  $sql .= " AND ed.fk_origin_line = cd.rowid";
3272  $sql .= " AND cd.fk_product = ".((int) $this->id);
3273  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3274  $sql .= " AND e.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3275  }
3276  if ($socid > 0) {
3277  $sql .= " AND e.fk_soc = ".((int) $socid);
3278  }
3279  if ($filtrestatut <> '') {
3280  $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3281  }
3282  if (!empty($filterShipmentStatus)) {
3283  $sql .= " AND e.fk_statut IN (".$this->db->sanitize($filterShipmentStatus).")";
3284  }
3285 
3286  $result = $this->db->query($sql);
3287  if ($result) {
3288  $obj = $this->db->fetch_object($result);
3289  $this->stats_expedition['customers'] = $obj->nb_customers;
3290  $this->stats_expedition['nb'] = $obj->nb;
3291  $this->stats_expedition['rows'] = $obj->nb_rows;
3292  $this->stats_expedition['qty'] = $obj->qty ? $obj->qty : 0;
3293 
3294  // if it's a virtual product, maybe it is in sending by extension
3295  if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
3296  $TFather = $this->getFather();
3297  if (is_array($TFather) && !empty($TFather)) {
3298  foreach ($TFather as &$fatherData) {
3299  $pFather = new Product($this->db);
3300  $pFather->id = $fatherData['id'];
3301  $qtyCoef = $fatherData['qty'];
3302 
3303  if ($fatherData['incdec']) {
3304  $pFather->load_stats_sending($socid, $filtrestatut, $forVirtualStock);
3305 
3306  $this->stats_expedition['customers'] += $pFather->stats_expedition['customers'];
3307  $this->stats_expedition['nb'] += $pFather->stats_expedition['nb'];
3308  $this->stats_expedition['rows'] += $pFather->stats_expedition['rows'];
3309  $this->stats_expedition['qty'] += $pFather->stats_expedition['qty'] * $qtyCoef;
3310  }
3311  }
3312  }
3313  }
3314 
3315  $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock, 'filterShipmentStatus' => $filterShipmentStatus);
3316  $reshook = $hookmanager->executeHooks('loadStatsSending', $parameters, $this, $action);
3317  if ($reshook > 0) {
3318  $this->stats_expedition = $hookmanager->resArray['stats_expedition'];
3319  }
3320 
3321  return 1;
3322  } else {
3323  $this->error = $this->db->error();
3324  return -1;
3325  }
3326  }
3327 
3328  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3338  public function load_stats_reception($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3339  {
3340  // phpcs:enable
3341  global $conf, $user, $hookmanager, $action;
3342 
3343  $sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_suppliers, COUNT(DISTINCT cf.rowid) as nb,";
3344  $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3345  $sql .= " FROM ".$this->db->prefix()."commande_fournisseur_dispatch as fd";
3346  $sql .= ", ".$this->db->prefix()."commande_fournisseur as cf";
3347  $sql .= ", ".$this->db->prefix()."societe as s";
3348  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3349  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3350  }
3351  $sql .= " WHERE cf.rowid = fd.fk_commande";
3352  $sql .= " AND cf.fk_soc = s.rowid";
3353  $sql .= " AND cf.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'supplier_order').")";
3354  $sql .= " AND fd.fk_product = ".((int) $this->id);
3355  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3356  $sql .= " AND cf.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3357  }
3358  if ($socid > 0) {
3359  $sql .= " AND cf.fk_soc = ".((int) $socid);
3360  }
3361  if ($filtrestatut <> '') {
3362  $sql .= " AND cf.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3363  }
3364  if (!empty($dateofvirtualstock)) {
3365  $sql .= " AND fd.datec <= '".$this->db->idate($dateofvirtualstock)."'";
3366  }
3367 
3368  $result = $this->db->query($sql);
3369  if ($result) {
3370  $obj = $this->db->fetch_object($result);
3371  $this->stats_reception['suppliers'] = $obj->nb_suppliers;
3372  $this->stats_reception['nb'] = $obj->nb;
3373  $this->stats_reception['rows'] = $obj->nb_rows;
3374  $this->stats_reception['qty'] = $obj->qty ? $obj->qty : 0;
3375 
3376  $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3377  $reshook = $hookmanager->executeHooks('loadStatsReception', $parameters, $this, $action);
3378  if ($reshook > 0) {
3379  $this->stats_reception = $hookmanager->resArray['stats_reception'];
3380  }
3381 
3382  return 1;
3383  } else {
3384  $this->error = $this->db->error();
3385  return -1;
3386  }
3387  }
3388 
3389  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3399  public function load_stats_inproduction($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3400  {
3401  // phpcs:enable
3402  global $conf, $user, $hookmanager, $action;
3403 
3404  $sql = "SELECT COUNT(DISTINCT m.fk_soc) as nb_customers, COUNT(DISTINCT m.rowid) as nb,";
3405  $sql .= " COUNT(mp.rowid) as nb_rows, SUM(mp.qty) as qty, role";
3406  $sql .= " FROM ".$this->db->prefix()."mrp_production as mp";
3407  $sql .= ", ".$this->db->prefix()."mrp_mo as m";
3408  $sql .= " LEFT JOIN ".$this->db->prefix()."societe as s ON s.rowid = m.fk_soc";
3409  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3410  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3411  }
3412  $sql .= " WHERE m.rowid = mp.fk_mo";
3413  $sql .= " AND m.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'mrp').")";
3414  $sql .= " AND mp.fk_product = ".((int) $this->id);
3415  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3416  $sql .= " AND m.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3417  }
3418  if ($socid > 0) {
3419  $sql .= " AND m.fk_soc = ".((int) $socid);
3420  }
3421  if ($filtrestatut <> '') {
3422  $sql .= " AND m.status IN (".$this->db->sanitize($filtrestatut).")";
3423  }
3424  if (!empty($dateofvirtualstock)) {
3425  $sql .= " AND m.date_valid <= '".$this->db->idate($dateofvirtualstock)."'"; // better date to code ? end of production ?
3426  }
3427  $sql .= " GROUP BY role";
3428 
3429  $this->stats_mrptoconsume['customers'] = 0;
3430  $this->stats_mrptoconsume['nb'] = 0;
3431  $this->stats_mrptoconsume['rows'] = 0;
3432  $this->stats_mrptoconsume['qty'] = 0;
3433  $this->stats_mrptoproduce['customers'] = 0;
3434  $this->stats_mrptoproduce['nb'] = 0;
3435  $this->stats_mrptoproduce['rows'] = 0;
3436  $this->stats_mrptoproduce['qty'] = 0;
3437 
3438  $result = $this->db->query($sql);
3439  if ($result) {
3440  while ($obj = $this->db->fetch_object($result)) {
3441  if ($obj->role == 'toconsume') {
3442  $this->stats_mrptoconsume['customers'] += $obj->nb_customers;
3443  $this->stats_mrptoconsume['nb'] += $obj->nb;
3444  $this->stats_mrptoconsume['rows'] += $obj->nb_rows;
3445  $this->stats_mrptoconsume['qty'] += ($obj->qty ? $obj->qty : 0);
3446  }
3447  if ($obj->role == 'consumed') {
3448  //$this->stats_mrptoconsume['customers'] += $obj->nb_customers;
3449  //$this->stats_mrptoconsume['nb'] += $obj->nb;
3450  //$this->stats_mrptoconsume['rows'] += $obj->nb_rows;
3451  $this->stats_mrptoconsume['qty'] -= ($obj->qty ? $obj->qty : 0);
3452  }
3453  if ($obj->role == 'toproduce') {
3454  $this->stats_mrptoproduce['customers'] += $obj->nb_customers;
3455  $this->stats_mrptoproduce['nb'] += $obj->nb;
3456  $this->stats_mrptoproduce['rows'] += $obj->nb_rows;
3457  $this->stats_mrptoproduce['qty'] += ($obj->qty ? $obj->qty : 0);
3458  }
3459  if ($obj->role == 'produced') {
3460  //$this->stats_mrptoproduce['customers'] += $obj->nb_customers;
3461  //$this->stats_mrptoproduce['nb'] += $obj->nb;
3462  //$this->stats_mrptoproduce['rows'] += $obj->nb_rows;
3463  $this->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
3464  }
3465  }
3466 
3467  // Clean data
3468  if ($this->stats_mrptoconsume['qty'] < 0) {
3469  $this->stats_mrptoconsume['qty'] = 0;
3470  }
3471  if ($this->stats_mrptoproduce['qty'] < 0) {
3472  $this->stats_mrptoproduce['qty'] = 0;
3473  }
3474 
3475  $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3476  $reshook = $hookmanager->executeHooks('loadStatsInProduction', $parameters, $this, $action);
3477  if ($reshook > 0) {
3478  $this->stats_mrptoproduce = $hookmanager->resArray['stats_mrptoproduce'];
3479  }
3480 
3481  return 1;
3482  } else {
3483  $this->error = $this->db->error();
3484  return -1;
3485  }
3486  }
3487 
3488  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3495  public function load_stats_contrat($socid = 0)
3496  {
3497  // phpcs:enable
3498  global $conf, $user, $hookmanager, $action;
3499 
3500  $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3501  $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3502  $sql .= " FROM ".$this->db->prefix()."contratdet as cd";
3503  $sql .= ", ".$this->db->prefix()."contrat as c";
3504  $sql .= ", ".$this->db->prefix()."societe as s";
3505  if (empty($user->rights->societe->client->voir) && !$socid) {
3506  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3507  }
3508  $sql .= " WHERE c.rowid = cd.fk_contrat";
3509  $sql .= " AND c.fk_soc = s.rowid";
3510  $sql .= " AND c.entity IN (".getEntity('contract').")";
3511  $sql .= " AND cd.fk_product = ".((int) $this->id);
3512  if (empty($user->rights->societe->client->voir) && !$socid) {
3513  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3514  }
3515  //$sql.= " AND c.statut != 0";
3516  if ($socid > 0) {
3517  $sql .= " AND c.fk_soc = ".((int) $socid);
3518  }
3519 
3520  $result = $this->db->query($sql);
3521  if ($result) {
3522  $obj = $this->db->fetch_object($result);
3523  $this->stats_contrat['customers'] = $obj->nb_customers;
3524  $this->stats_contrat['nb'] = $obj->nb;
3525  $this->stats_contrat['rows'] = $obj->nb_rows;
3526  $this->stats_contrat['qty'] = $obj->qty ? $obj->qty : 0;
3527 
3528  // if it's a virtual product, maybe it is in contract by extension
3529  if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
3530  $TFather = $this->getFather();
3531  if (is_array($TFather) && !empty($TFather)) {
3532  foreach ($TFather as &$fatherData) {
3533  $pFather = new Product($this->db);
3534  $pFather->id = $fatherData['id'];
3535  $qtyCoef = $fatherData['qty'];
3536 
3537  if ($fatherData['incdec']) {
3538  $pFather->load_stats_contrat($socid);
3539 
3540  $this->stats_contrat['customers'] += $pFather->stats_contrat['customers'];
3541  $this->stats_contrat['nb'] += $pFather->stats_contrat['nb'];
3542  $this->stats_contrat['rows'] += $pFather->stats_contrat['rows'];
3543  $this->stats_contrat['qty'] += $pFather->stats_contrat['qty'] * $qtyCoef;
3544  }
3545  }
3546  }
3547  }
3548 
3549  $parameters = array('socid' => $socid);
3550  $reshook = $hookmanager->executeHooks('loadStatsContract', $parameters, $this, $action);
3551  if ($reshook > 0) {
3552  $this->stats_contrat = $hookmanager->resArray['stats_contrat'];
3553  }
3554 
3555  return 1;
3556  } else {
3557  $this->error = $this->db->error().' sql='.$sql;
3558  return -1;
3559  }
3560  }
3561 
3562  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3569  public function load_stats_facture($socid = 0)
3570  {
3571  // phpcs:enable
3572  global $db, $conf, $user, $hookmanager, $action;
3573 
3574  $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
3575  $sql .= " COUNT(fd.rowid) as nb_rows, SUM(".$this->db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1').") as qty";
3576  $sql .= " FROM ".$this->db->prefix()."facturedet as fd";
3577  $sql .= ", ".$this->db->prefix()."facture as f";
3578  $sql .= ", ".$this->db->prefix()."societe as s";
3579  if (empty($user->rights->societe->client->voir) && !$socid) {
3580  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3581  }
3582  $sql .= " WHERE f.rowid = fd.fk_facture";
3583  $sql .= " AND f.fk_soc = s.rowid";
3584  $sql .= " AND f.entity IN (".getEntity('invoice').")";
3585  $sql .= " AND fd.fk_product = ".((int) $this->id);
3586  if (empty($user->rights->societe->client->voir) && !$socid) {
3587  $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3588  }
3589  //$sql.= " AND f.fk_statut != 0";
3590  if ($socid > 0) {
3591  $sql .= " AND f.fk_soc = ".((int) $socid);
3592  }
3593 
3594  $result = $this->db->query($sql);
3595  if ($result) {
3596  $obj = $this->db->fetch_object($result);
3597  $this->stats_facture['customers'] = $obj->nb_customers;
3598  $this->stats_facture['nb'] = $obj->nb;
3599  $this->stats_facture['rows'] = $obj->nb_rows;
3600  $this->stats_facture['qty'] = $obj->qty ? $obj->qty : 0;
3601 
3602  // if it's a virtual product, maybe it is in invoice by extension
3603  if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
3604  $TFather = $this->getFather();
3605  if (is_array($TFather) && !empty($TFather)) {
3606  foreach ($TFather as &$fatherData) {
3607  $pFather = new Product($this->db);
3608  $pFather->id = $fatherData['id'];
3609  $qtyCoef = $fatherData['qty'];
3610 
3611  if ($fatherData['incdec']) {
3612  $pFather->load_stats_facture($socid);
3613 
3614  $this->stats_facture['customers'] += $pFather->stats_facture['customers'];
3615  $this->stats_facture['nb'] += $pFather->stats_facture['nb'];
3616  $this->stats_facture['rows'] += $pFather->stats_facture['rows'];
3617  $this->stats_facture['qty'] += $pFather->stats_facture['qty'] * $qtyCoef;
3618  }
3619  }
3620  }
3621  }
3622 
3623  $parameters = array('socid' => $socid);
3624  $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoice', $parameters, $this, $action);
3625  if ($reshook > 0) {
3626  $this->stats_facture = $hookmanager->resArray['stats_facture'];
3627  }
3628 
3629  return 1;
3630  } else {
3631  $this->error = $this->db->error();
3632  return -1;
3633  }
3634  }
3635 
3636 
3637  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3644  public function load_stats_facturerec($socid = 0)
3645  {
3646  // phpcs:enable
3647  global $db, $conf, $user, $hookmanager;
3648 
3649  $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
3650  $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3651  $sql .= " FROM ".MAIN_DB_PREFIX."facturedet_rec as fd";
3652  $sql .= ", ".MAIN_DB_PREFIX."facture_rec as f";
3653  $sql .= ", ".MAIN_DB_PREFIX."societe as s";
3654  if (empty($user->rights->societe->client->voir) && !$socid) {
3655  $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
3656  }
3657  $sql .= " WHERE f.rowid = fd.fk_facture";
3658  $sql .= " AND f.fk_soc = s.rowid";
3659  $sql .= " AND f.entity IN (".getEntity('invoice').")";
3660  $sql .= " AND fd.fk_product = ".((int) $this->id);
3661  if (empty($user->rights->societe->client->voir) && !$socid) {
3662  $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3663  }
3664  //$sql.= " AND f.fk_statut != 0";
3665  if ($socid > 0) {
3666  $sql .= " AND f.fk_soc = ".((int) $socid);
3667  }
3668 
3669  $result = $this->db->query($sql);
3670  if ($result) {
3671  $obj = $this->db->fetch_object($result);
3672  $this->stats_facturerec['customers'] = $obj->nb_customers;
3673  $this->stats_facturerec['nb'] = $obj->nb;
3674  $this->stats_facturerec['rows'] = $obj->nb_rows;
3675  $this->stats_facturerec['qty'] = $obj->qty ? $obj->qty : 0;
3676 
3677  // if it's a virtual product, maybe it is in invoice by extension
3678  if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
3679  $TFather = $this->getFather();
3680  if (is_array($TFather) && !empty($TFather)) {
3681  foreach ($TFather as &$fatherData) {
3682  $pFather = new Product($this->db);
3683  $pFather->id = $fatherData['id'];
3684  $qtyCoef = $fatherData['qty'];
3685 
3686  if ($fatherData['incdec']) {
3687  $pFather->load_stats_facture($socid);
3688 
3689  $this->stats_facturerec['customers'] += $pFather->stats_facturerec['customers'];
3690  $this->stats_facturerec['nb'] += $pFather->stats_facturerec['nb'];
3691  $this->stats_facturerec['rows'] += $pFather->stats_facturerec['rows'];
3692  $this->stats_facturerec['qty'] += $pFather->stats_facturerec['qty'] * $qtyCoef;
3693  }
3694  }
3695  }
3696  }
3697 
3698  $parameters = array('socid' => $socid);
3699  $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoiceRec', $parameters, $this, $action);
3700  if ($reshook > 0) {
3701  $this->stats_facturerec = $hookmanager->resArray['stats_facturerec'];
3702  }
3703 
3704  return 1;
3705  } else {
3706  $this->error = $this->db->error();
3707  return -1;
3708  }
3709  }
3710 
3711  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3718  public function load_stats_facture_fournisseur($socid = 0)
3719  {
3720  // phpcs:enable
3721  global $conf, $user, $hookmanager, $action;
3722 
3723  $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
3724  $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3725  $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as fd";
3726  $sql .= ", ".$this->db->prefix()."facture_fourn as f";
3727  $sql .= ", ".$this->db->prefix()."societe as s";
3728  if (empty($user->rights->societe->client->voir) && !$socid) {
3729  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3730  }
3731  $sql .= " WHERE f.rowid = fd.fk_facture_fourn";
3732  $sql .= " AND f.fk_soc = s.rowid";
3733  $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
3734  $sql .= " AND fd.fk_product = ".((int) $this->id);
3735  if (empty($user->rights->societe->client->voir) && !$socid) {
3736  $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3737  }
3738  //$sql.= " AND f.fk_statut != 0";
3739  if ($socid > 0) {
3740  $sql .= " AND f.fk_soc = ".((int) $socid);
3741  }
3742 
3743  $result = $this->db->query($sql);
3744  if ($result) {
3745  $obj = $this->db->fetch_object($result);
3746  $this->stats_facture_fournisseur['suppliers'] = $obj->nb_suppliers;
3747  $this->stats_facture_fournisseur['nb'] = $obj->nb;
3748  $this->stats_facture_fournisseur['rows'] = $obj->nb_rows;
3749  $this->stats_facture_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3750 
3751  $parameters = array('socid' => $socid);
3752  $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoice', $parameters, $this, $action);
3753  if ($reshook > 0) {
3754  $this->stats_facture_fournisseur = $hookmanager->resArray['stats_facture_fournisseur'];
3755  }
3756 
3757  return 1;
3758  } else {
3759  $this->error = $this->db->error();
3760  return -1;
3761  }
3762  }
3763 
3764  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3773  private function _get_stats($sql, $mode, $year = 0)
3774  {
3775  // phpcs:enable
3776  $tab = array();
3777 
3778  $resql = $this->db->query($sql);
3779  if ($resql) {
3780  $num = $this->db->num_rows($resql);
3781  $i = 0;
3782  while ($i < $num) {
3783  $arr = $this->db->fetch_array($resql);
3784  $keyfortab = (string) $arr[1];
3785  if ($year == -1) {
3786  $keyfortab = substr($keyfortab, -2);
3787  }
3788 
3789  if ($mode == 'byunit') {
3790  $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[0]; // 1st field
3791  } elseif ($mode == 'bynumber') {
3792  $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
3793  } elseif ($mode == 'byamount') {
3794  $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
3795  } else {
3796  // Bad value for $mode
3797  return -1;
3798  }
3799  $i++;
3800  }
3801  } else {
3802  $this->error = $this->db->error().' sql='.$sql;
3803  return -1;
3804  }
3805 
3806  if (empty($year)) {
3807  $year = strftime('%Y', time());
3808  $month = strftime('%m', time());
3809  } elseif ($year == -1) {
3810  $year = '';
3811  $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
3812  } else {
3813  $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
3814  }
3815 
3816  $result = array();
3817 
3818  for ($j = 0; $j < 12; $j++) {
3819  // $ids is 'D', 'N', 'O', 'S', ... (First letter of month in user language)
3820  $idx = ucfirst(dol_trunc(dol_print_date(dol_mktime(12, 0, 0, $month, 1, 1970), "%b"), 1, 'right', 'UTF-8', 1));
3821 
3822  //print $idx.'-'.$year.'-'.$month.'<br>';
3823  $result[$j] = array($idx, isset($tab[$year.$month]) ? $tab[$year.$month] : 0);
3824  // $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0);
3825 
3826  $month = "0".($month - 1);
3827  if (dol_strlen($month) == 3) {
3828  $month = substr($month, 1);
3829  }
3830  if ($month == 0) {
3831  $month = 12;
3832  $year = $year - 1;
3833  }
3834  }
3835 
3836  return array_reverse($result);
3837  }
3838 
3839 
3840  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3851  public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
3852  {
3853  // phpcs:enable
3854  global $conf;
3855  global $user;
3856 
3857  $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
3858  if ($mode == 'bynumber') {
3859  $sql .= ", count(DISTINCT f.rowid)";
3860  }
3861  $sql .= ", sum(d.total_ht) as total_ht";
3862  $sql .= " FROM ".$this->db->prefix()."facturedet as d, ".$this->db->prefix()."facture as f, ".$this->db->prefix()."societe as s";
3863  if ($filteronproducttype >= 0) {
3864  $sql .= ", ".$this->db->prefix()."product as p";
3865  }
3866  if (empty($user->rights->societe->client->voir) && !$socid) {
3867  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3868  }
3869  $sql .= " WHERE f.rowid = d.fk_facture";
3870  if ($this->id > 0) {
3871  $sql .= " AND d.fk_product = ".((int) $this->id);
3872  } else {
3873  $sql .= " AND d.fk_product > 0";
3874  }
3875  if ($filteronproducttype >= 0) {
3876  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
3877  }
3878  $sql .= " AND f.fk_soc = s.rowid";
3879  $sql .= " AND f.entity IN (".getEntity('invoice').")";
3880  if (empty($user->rights->societe->client->voir) && !$socid) {
3881  $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3882  }
3883  if ($socid > 0) {
3884  $sql .= " AND f.fk_soc = $socid";
3885  }
3886  $sql .= $morefilter;
3887  $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
3888  $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
3889 
3890  return $this->_get_stats($sql, $mode, $year);
3891  }
3892 
3893 
3894  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3905  public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
3906  {
3907  // phpcs:enable
3908  global $conf;
3909  global $user;
3910 
3911  $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
3912  if ($mode == 'bynumber') {
3913  $sql .= ", count(DISTINCT f.rowid)";
3914  }
3915  $sql .= ", sum(d.total_ht) as total_ht";
3916  $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as d, ".$this->db->prefix()."facture_fourn as f, ".$this->db->prefix()."societe as s";
3917  if ($filteronproducttype >= 0) {
3918  $sql .= ", ".$this->db->prefix()."product as p";
3919  }
3920  if (empty($user->rights->societe->client->voir) && !$socid) {
3921  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3922  }
3923  $sql .= " WHERE f.rowid = d.fk_facture_fourn";
3924  if ($this->id > 0) {
3925  $sql .= " AND d.fk_product = ".((int) $this->id);
3926  } else {
3927  $sql .= " AND d.fk_product > 0";
3928  }
3929  if ($filteronproducttype >= 0) {
3930  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
3931  }
3932  $sql .= " AND f.fk_soc = s.rowid";
3933  $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
3934  if (empty($user->rights->societe->client->voir) && !$socid) {
3935  $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3936  }
3937  if ($socid > 0) {
3938  $sql .= " AND f.fk_soc = $socid";
3939  }
3940  $sql .= $morefilter;
3941  $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
3942  $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
3943 
3944  return $this->_get_stats($sql, $mode, $year);
3945  }
3946 
3947  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3958  public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
3959  {
3960  // phpcs:enable
3961  global $conf, $user;
3962 
3963  $sql = "SELECT sum(d.qty) as qty, date_format(p.datep, '%Y%m')";
3964  if ($mode == 'bynumber') {
3965  $sql .= ", count(DISTINCT p.rowid)";
3966  }
3967  $sql .= ", sum(d.total_ht) as total_ht";
3968  $sql .= " FROM ".$this->db->prefix()."propaldet as d, ".$this->db->prefix()."propal as p, ".$this->db->prefix()."societe as s";
3969  if ($filteronproducttype >= 0) {
3970  $sql .= ", ".$this->db->prefix()."product as prod";
3971  }
3972  if (empty($user->rights->societe->client->voir) && !$socid) {
3973  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3974  }
3975  $sql .= " WHERE p.rowid = d.fk_propal";
3976  if ($this->id > 0) {
3977  $sql .= " AND d.fk_product = ".((int) $this->id);
3978  } else {
3979  $sql .= " AND d.fk_product > 0";
3980  }
3981  if ($filteronproducttype >= 0) {
3982  $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
3983  }
3984  $sql .= " AND p.fk_soc = s.rowid";
3985  $sql .= " AND p.entity IN (".getEntity('propal').")";
3986  if (empty($user->rights->societe->client->voir) && !$socid) {
3987  $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3988  }
3989  if ($socid > 0) {
3990  $sql .= " AND p.fk_soc = ".((int) $socid);
3991  }
3992  $sql .= $morefilter;
3993  $sql .= " GROUP BY date_format(p.datep,'%Y%m')";
3994  $sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC";
3995 
3996  return $this->_get_stats($sql, $mode, $year);
3997  }
3998 
3999  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4010  public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4011  {
4012  // phpcs:enable
4013  global $conf;
4014  global $user;
4015 
4016  $sql = "SELECT sum(d.qty) as qty, date_format(p.date_valid, '%Y%m')";
4017  if ($mode == 'bynumber') {
4018  $sql .= ", count(DISTINCT p.rowid)";
4019  }
4020  $sql .= ", sum(d.total_ht) as total_ht";
4021  $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as d, ".$this->db->prefix()."supplier_proposal as p, ".$this->db->prefix()."societe as s";
4022  if ($filteronproducttype >= 0) {
4023  $sql .= ", ".$this->db->prefix()."product as prod";
4024  }
4025  if (empty($user->rights->societe->client->voir) && !$socid) {
4026  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4027  }
4028  $sql .= " WHERE p.rowid = d.fk_supplier_proposal";
4029  if ($this->id > 0) {
4030  $sql .= " AND d.fk_product = ".((int) $this->id);
4031  } else {
4032  $sql .= " AND d.fk_product > 0";
4033  }
4034  if ($filteronproducttype >= 0) {
4035  $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4036  }
4037  $sql .= " AND p.fk_soc = s.rowid";
4038  $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
4039  if (empty($user->rights->societe->client->voir) && !$socid) {
4040  $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4041  }
4042  if ($socid > 0) {
4043  $sql .= " AND p.fk_soc = ".((int) $socid);
4044  }
4045  $sql .= $morefilter;
4046  $sql .= " GROUP BY date_format(p.date_valid,'%Y%m')";
4047  $sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
4048 
4049  return $this->_get_stats($sql, $mode, $year);
4050  }
4051 
4052  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4063  public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4064  {
4065  // phpcs:enable
4066  global $conf, $user;
4067 
4068  $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4069  if ($mode == 'bynumber') {
4070  $sql .= ", count(DISTINCT c.rowid)";
4071  }
4072  $sql .= ", sum(d.total_ht) as total_ht";
4073  $sql .= " FROM ".$this->db->prefix()."commandedet as d, ".$this->db->prefix()."commande as c, ".$this->db->prefix()."societe as s";
4074  if ($filteronproducttype >= 0) {
4075  $sql .= ", ".$this->db->prefix()."product as p";
4076  }
4077  if (empty($user->rights->societe->client->voir) && !$socid) {
4078  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4079  }
4080  $sql .= " WHERE c.rowid = d.fk_commande";
4081  if ($this->id > 0) {
4082  $sql .= " AND d.fk_product = ".((int) $this->id);
4083  } else {
4084  $sql .= " AND d.fk_product > 0";
4085  }
4086  if ($filteronproducttype >= 0) {
4087  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4088  }
4089  $sql .= " AND c.fk_soc = s.rowid";
4090  $sql .= " AND c.entity IN (".getEntity('commande').")";
4091  if (empty($user->rights->societe->client->voir) && !$socid) {
4092  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4093  }
4094  if ($socid > 0) {
4095  $sql .= " AND c.fk_soc = ".((int) $socid);
4096  }
4097  $sql .= $morefilter;
4098  $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4099  $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4100 
4101  return $this->_get_stats($sql, $mode, $year);
4102  }
4103 
4104  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4115  public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4116  {
4117  // phpcs:enable
4118  global $conf, $user;
4119 
4120  $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4121  if ($mode == 'bynumber') {
4122  $sql .= ", count(DISTINCT c.rowid)";
4123  }
4124  $sql .= ", sum(d.total_ht) as total_ht";
4125  $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as d, ".$this->db->prefix()."commande_fournisseur as c, ".$this->db->prefix()."societe as s";
4126  if ($filteronproducttype >= 0) {
4127  $sql .= ", ".$this->db->prefix()."product as p";
4128  }
4129  if (empty($user->rights->societe->client->voir) && !$socid) {
4130  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4131  }
4132  $sql .= " WHERE c.rowid = d.fk_commande";
4133  if ($this->id > 0) {
4134  $sql .= " AND d.fk_product = ".((int) $this->id);
4135  } else {
4136  $sql .= " AND d.fk_product > 0";
4137  }
4138  if ($filteronproducttype >= 0) {
4139  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4140  }
4141  $sql .= " AND c.fk_soc = s.rowid";
4142  $sql .= " AND c.entity IN (".getEntity('supplier_order').")";
4143  if (empty($user->rights->societe->client->voir) && !$socid) {
4144  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4145  }
4146  if ($socid > 0) {
4147  $sql .= " AND c.fk_soc = ".((int) $socid);
4148  }
4149  $sql .= $morefilter;
4150  $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4151  $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4152 
4153  return $this->_get_stats($sql, $mode, $year);
4154  }
4155 
4156  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4167  public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4168  {
4169  // phpcs:enable
4170  global $conf, $user;
4171 
4172  $sql = "SELECT sum(d.qty) as qty, date_format(c.date_contrat, '%Y%m')";
4173  if ($mode == 'bynumber') {
4174  $sql .= ", count(DISTINCT c.rowid)";
4175  }
4176  $sql .= ", sum(d.total_ht) as total_ht";
4177  $sql .= " FROM ".$this->db->prefix()."contratdet as d, ".$this->db->prefix()."contrat as c, ".$this->db->prefix()."societe as s";
4178  if ($filteronproducttype >= 0) {
4179  $sql .= ", ".$this->db->prefix()."product as p";
4180  }
4181  if (empty($user->rights->societe->client->voir) && !$socid) {
4182  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4183  }
4184 
4185  $sql .= " WHERE c.entity IN (".getEntity('contract').")";
4186  $sql .= " AND c.rowid = d.fk_contrat";
4187 
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 p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4195  }
4196  $sql .= " AND c.fk_soc = s.rowid";
4197 
4198  if (empty($user->rights->societe->client->voir) && !$socid) {
4199  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4200  }
4201  if ($socid > 0) {
4202  $sql .= " AND c.fk_soc = ".((int) $socid);
4203  }
4204  $sql .= $morefilter;
4205  $sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')";
4206  $sql .= " ORDER BY date_format(c.date_contrat,'%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_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4223  {
4224  // phpcs:enable
4225  global $conf, $user;
4226 
4227  $sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')";
4228  if ($mode == 'bynumber') {
4229  $sql .= ", count(DISTINCT d.rowid)";
4230  }
4231  $sql .= " FROM ".$this->db->prefix()."mrp_mo as d LEFT JOIN ".$this->db->prefix()."societe as s ON d.fk_soc = s.rowid";
4232  if ($filteronproducttype >= 0) {
4233  $sql .= ", ".$this->db->prefix()."product as p";
4234  }
4235  if (empty($user->rights->societe->client->voir) && !$socid) {
4236  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4237  }
4238 
4239  $sql .= " WHERE d.entity IN (".getEntity('mo').")";
4240  $sql .= " AND d.status > 0";
4241 
4242  if ($this->id > 0) {
4243  $sql .= " AND d.fk_product = ".((int) $this->id);
4244  } else {
4245  $sql .= " AND d.fk_product > 0";
4246  }
4247  if ($filteronproducttype >= 0) {
4248  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4249  }
4250 
4251  if (empty($user->rights->societe->client->voir) && !$socid) {
4252  $sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4253  }
4254  if ($socid > 0) {
4255  $sql .= " AND d.fk_soc = ".((int) $socid);
4256  }
4257  $sql .= $morefilter;
4258  $sql .= " GROUP BY date_format(d.date_valid,'%Y%m')";
4259  $sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC";
4260 
4261  return $this->_get_stats($sql, $mode, $year);
4262  }
4263 
4264  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4274  public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1)
4275  {
4276  // phpcs:enable
4277  // Clean parameters
4278  if (!is_numeric($id_pere)) {
4279  $id_pere = 0;
4280  }
4281  if (!is_numeric($id_fils)) {
4282  $id_fils = 0;
4283  }
4284  if (!is_numeric($incdec)) {
4285  $incdec = 0;
4286  }
4287 
4288  $result = $this->del_sousproduit($id_pere, $id_fils);
4289  if ($result < 0) {
4290  return $result;
4291  }
4292 
4293  // Check not already father of id_pere (to avoid father -> child -> father links)
4294  $sql = "SELECT fk_product_pere from ".$this->db->prefix()."product_association";
4295  $sql .= " WHERE fk_product_pere = ".((int) $id_fils)." AND fk_product_fils = ".((int) $id_pere);
4296  if (!$this->db->query($sql)) {
4297  dol_print_error($this->db);
4298  return -1;
4299  } else {
4300  //Selection of the highest row
4301  $sql = "SELECT MAX(rang) as max_rank FROM ".$this->db->prefix()."product_association";
4302  $sql .= " WHERE fk_product_pere = ".((int) $id_pere);
4303  $resql = $this->db->query($sql);
4304  if ($resql) {
4305  $obj = $this->db->fetch_object($resql);
4306  $rank = $obj->max_rank + 1;
4307  //Addition of a product with the highest rank +1
4308  $sql = "INSERT INTO ".$this->db->prefix()."product_association(fk_product_pere,fk_product_fils,qty,incdec,rang)";
4309  $sql .= " VALUES (".((int) $id_pere).", ".((int) $id_fils).", ".price2num($qty, 'MS').", ".price2num($incdec, 'MS').", ".((int) $rank).")";
4310  if (! $this->db->query($sql)) {
4311  dol_print_error($this->db);
4312  return -1;
4313  } else {
4314  return 1;
4315  }
4316  } else {
4317  dol_print_error($this->db);
4318  return -1;
4319  }
4320  }
4321  }
4322 
4323  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4333  public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1)
4334  {
4335  // phpcs:enable
4336  // Clean parameters
4337  if (!is_numeric($id_pere)) {
4338  $id_pere = 0;
4339  }
4340  if (!is_numeric($id_fils)) {
4341  $id_fils = 0;
4342  }
4343  if (!is_numeric($incdec)) {
4344  $incdec = 1;
4345  }
4346  if (!is_numeric($qty)) {
4347  $qty = 1;
4348  }
4349 
4350  $sql = 'UPDATE '.$this->db->prefix().'product_association SET ';
4351  $sql .= 'qty = '.price2num($qty, 'MS');
4352  $sql .= ',incdec = '.price2num($incdec, 'MS');
4353  $sql .= ' WHERE fk_product_pere = '.((int) $id_pere).' AND fk_product_fils = '.((int) $id_fils);
4354 
4355  if (!$this->db->query($sql)) {
4356  dol_print_error($this->db);
4357  return -1;
4358  } else {
4359  return 1;
4360  }
4361  }
4362 
4363  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4371  public function del_sousproduit($fk_parent, $fk_child)
4372  {
4373  // phpcs:enable
4374  if (!is_numeric($fk_parent)) {
4375  $fk_parent = 0;
4376  }
4377  if (!is_numeric($fk_child)) {
4378  $fk_child = 0;
4379  }
4380 
4381  $sql = "DELETE FROM ".$this->db->prefix()."product_association";
4382  $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
4383  $sql .= " AND fk_product_fils = ".((int) $fk_child);
4384 
4385  dol_syslog(get_class($this).'::del_sousproduit', LOG_DEBUG);
4386  if (!$this->db->query($sql)) {
4387  dol_print_error($this->db);
4388  return -1;
4389  }
4390 
4391  // Updated ranks so that none are missing
4392  $sqlrank = "SELECT rowid, rang FROM ".$this->db->prefix()."product_association";
4393  $sqlrank.= " WHERE fk_product_pere = ".((int) $fk_parent);
4394  $sqlrank.= " ORDER BY rang";
4395  $resqlrank = $this->db->query($sqlrank);
4396  if ($resqlrank) {
4397  $cpt = 0;
4398  while ($objrank = $this->db->fetch_object($resqlrank)) {
4399  $cpt++;
4400  $sql = "UPDATE ".$this->db->prefix()."product_association";
4401  $sql.= " SET rang = ".((int) $cpt);
4402  $sql.= " WHERE rowid = ".((int) $objrank->rowid);
4403  if (! $this->db->query($sql)) {
4404  dol_print_error($this->db);
4405  return -1;
4406  }
4407  }
4408  }
4409  return 1;
4410  }
4411 
4412  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4420  public function is_sousproduit($fk_parent, $fk_child)
4421  {
4422  // phpcs:enable
4423  $sql = "SELECT fk_product_pere, qty, incdec";
4424  $sql .= " FROM ".$this->db->prefix()."product_association";
4425  $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
4426  $sql .= " AND fk_product_fils = ".((int) $fk_child);
4427 
4428  $result = $this->db->query($sql);
4429  if ($result) {
4430  $num = $this->db->num_rows($result);
4431 
4432  if ($num > 0) {
4433  $obj = $this->db->fetch_object($result);
4434 
4435  $this->is_sousproduit_qty = $obj->qty;
4436  $this->is_sousproduit_incdec = $obj->incdec;
4437 
4438  return true;
4439  } else {
4440  return false;
4441  }
4442  } else {
4443  dol_print_error($this->db);
4444  return -1;
4445  }
4446  }
4447 
4448 
4449  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4460  public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
4461  {
4462  // phpcs:enable
4463  global $conf;
4464 
4465  $now = dol_now();
4466 
4467  dol_syslog(get_class($this)."::add_fournisseur id_fourn = ".$id_fourn." ref_fourn=".$ref_fourn." quantity=".$quantity, LOG_DEBUG);
4468 
4469  // Clean parameters
4470  $quantity = price2num($quantity, 'MS');
4471 
4472  if ($ref_fourn) {
4473  $sql = "SELECT rowid, fk_product";
4474  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
4475  $sql .= " WHERE fk_soc = ".((int) $id_fourn);
4476  $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
4477  $sql .= " AND fk_product <> ".((int) $this->id);
4478  $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
4479 
4480  $resql = $this->db->query($sql);
4481  if ($resql) {
4482  $obj = $this->db->fetch_object($resql);
4483  if ($obj) {
4484  // If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
4485  $this->product_id_already_linked = $obj->fk_product;
4486  return -3;
4487  }
4488  $this->db->free($resql);
4489  }
4490  }
4491 
4492  $sql = "SELECT rowid";
4493  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
4494  $sql .= " WHERE fk_soc = ".((int) $id_fourn);
4495  if ($ref_fourn) {
4496  $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
4497  } else {
4498  $sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
4499  }
4500  $sql .= " AND quantity = ".((float) $quantity);
4501  $sql .= " AND fk_product = ".((int) $this->id);
4502  $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
4503 
4504  $resql = $this->db->query($sql);
4505  if ($resql) {
4506  $obj = $this->db->fetch_object($resql);
4507 
4508  // The reference supplier does not exist, we create it for this product.
4509  if (empty($obj)) {
4510  $sql = "INSERT INTO ".$this->db->prefix()."product_fournisseur_price(";
4511  $sql .= "datec";
4512  $sql .= ", entity";
4513  $sql .= ", fk_product";
4514  $sql .= ", fk_soc";
4515  $sql .= ", ref_fourn";
4516  $sql .= ", quantity";
4517  $sql .= ", fk_user";
4518  $sql .= ", tva_tx";
4519  $sql .= ") VALUES (";
4520  $sql .= "'".$this->db->idate($now)."'";
4521  $sql .= ", ".$conf->entity;
4522  $sql .= ", ".$this->id;
4523  $sql .= ", ".$id_fourn;
4524  $sql .= ", '".$this->db->escape($ref_fourn)."'";
4525  $sql .= ", ".$quantity;
4526  $sql .= ", ".$user->id;
4527  $sql .= ", 0";
4528  $sql .= ")";
4529 
4530  if ($this->db->query($sql)) {
4531  $this->product_fourn_price_id = $this->db->last_insert_id($this->db->prefix()."product_fournisseur_price");
4532  return 1;
4533  } else {
4534  $this->error = $this->db->lasterror();
4535  return -1;
4536  }
4537  } else {
4538  // If the supplier price already exists for this product and quantity
4539  $this->product_fourn_price_id = $obj->rowid;
4540  return 0;
4541  }
4542  } else {
4543  $this->error = $this->db->lasterror();
4544  return -2;
4545  }
4546  }
4547 
4548 
4549  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4555  public function list_suppliers()
4556  {
4557  // phpcs:enable
4558  global $conf;
4559 
4560  $list = array();
4561 
4562  $sql = "SELECT DISTINCT p.fk_soc";
4563  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as p";
4564  $sql .= " WHERE p.fk_product = ".((int) $this->id);
4565  $sql .= " AND p.entity = ".((int) $conf->entity);
4566 
4567  $result = $this->db->query($sql);
4568  if ($result) {
4569  $num = $this->db->num_rows($result);
4570  $i = 0;
4571  while ($i < $num) {
4572  $obj = $this->db->fetch_object($result);
4573  $list[$i] = $obj->fk_soc;
4574  $i++;
4575  }
4576  }
4577 
4578  return $list;
4579  }
4580 
4581  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4589  public function clone_price($fromId, $toId)
4590  {
4591  global $conf, $user;
4592 
4593  $now = dol_now();
4594 
4595  $this->db->begin();
4596 
4597  // prices
4598  $sql = "INSERT INTO ".$this->db->prefix()."product_price (";
4599  $sql .= " entity";
4600  $sql .= ", fk_product";
4601  $sql .= ", date_price";
4602  $sql .= ", price_level";
4603  $sql .= ", price";
4604  $sql .= ", price_ttc";
4605  $sql .= ", price_min";
4606  $sql .= ", price_min_ttc";
4607  $sql .= ", price_base_type";
4608  $sql .= ", default_vat_code";
4609  $sql .= ", tva_tx";
4610  $sql .= ", recuperableonly";
4611  $sql .= ", localtax1_tx";
4612  $sql .= ", localtax1_type";
4613  $sql .= ", localtax2_tx";
4614  $sql .= ", localtax2_type";
4615  $sql .= ", fk_user_author";
4616  $sql .= ", tosell";
4617  $sql .= ", price_by_qty";
4618  $sql .= ", fk_price_expression";
4619  $sql .= ", fk_multicurrency";
4620  $sql .= ", multicurrency_code";
4621  $sql .= ", multicurrency_tx";
4622  $sql .= ", multicurrency_price";
4623  $sql .= ", multicurrency_price_ttc";
4624  $sql .= ")";
4625  $sql .= " SELECT";
4626  $sql .= " entity";
4627  $sql .= ", ".$toId;
4628  $sql .= ", '".$this->db->idate($now)."'";
4629  $sql .= ", price_level";
4630  $sql .= ", price";
4631  $sql .= ", price_ttc";
4632  $sql .= ", price_min";
4633  $sql .= ", price_min_ttc";
4634  $sql .= ", price_base_type";
4635  $sql .= ", default_vat_code";
4636  $sql .= ", tva_tx";
4637  $sql .= ", recuperableonly";
4638  $sql .= ", localtax1_tx";
4639  $sql .= ", localtax1_type";
4640  $sql .= ", localtax2_tx";
4641  $sql .= ", localtax2_type";
4642  $sql .= ", ".$user->id;
4643  $sql .= ", tosell";
4644  $sql .= ", price_by_qty";
4645  $sql .= ", fk_price_expression";
4646  $sql .= ", fk_multicurrency";
4647  $sql .= ", multicurrency_code";
4648  $sql .= ", multicurrency_tx";
4649  $sql .= ", multicurrency_price";
4650  $sql .= ", multicurrency_price_ttc";
4651  $sql .= " FROM ".$this->db->prefix()."product_price";
4652  $sql .= " WHERE fk_product = ".((int) $fromId);
4653  $sql .= " ORDER BY date_price DESC";
4654  if ($conf->global->PRODUIT_MULTIPRICES_LIMIT > 0) {
4655  $sql .= " LIMIT ".$conf->global->PRODUIT_MULTIPRICES_LIMIT;
4656  }
4657 
4658  dol_syslog(__METHOD__, LOG_DEBUG);
4659  $resql = $this->db->query($sql);
4660  if (!$resql) {
4661  $this->db->rollback();
4662  return -1;
4663  }
4664 
4665  $this->db->commit();
4666  return 1;
4667  }
4668 
4669  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4677  public function clone_associations($fromId, $toId)
4678  {
4679  // phpcs:enable
4680  $this->db->begin();
4681 
4682  $sql = 'INSERT INTO '.$this->db->prefix().'product_association (fk_product_pere, fk_product_fils, qty)';
4683  $sql .= " SELECT ".$toId.", fk_product_fils, qty FROM ".$this->db->prefix()."product_association";
4684  $sql .= " WHERE fk_product_pere = ".((int) $fromId);
4685 
4686  dol_syslog(get_class($this).'::clone_association', LOG_DEBUG);
4687  if (!$this->db->query($sql)) {
4688  $this->db->rollback();
4689  return -1;
4690  }
4691 
4692  $this->db->commit();
4693  return 1;
4694  }
4695 
4696  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4704  public function clone_fournisseurs($fromId, $toId)
4705  {
4706  // phpcs:enable
4707  $this->db->begin();
4708 
4709  $now = dol_now();
4710 
4711  // les fournisseurs
4712  /*$sql = "INSERT ".$this->db->prefix()."product_fournisseur ("
4713  . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
4714  . " SELECT '".$this->db->idate($now)."', ".$toId.", fk_soc, ref_fourn, fk_user_author"
4715  . " FROM ".$this->db->prefix()."product_fournisseur"
4716  . " WHERE fk_product = ".((int) $fromId);
4717 
4718  if ( ! $this->db->query($sql ) )
4719  {
4720  $this->db->rollback();
4721  return -1;
4722  }*/
4723 
4724  // les prix de fournisseurs.
4725  $sql = "INSERT ".$this->db->prefix()."product_fournisseur_price (";
4726  $sql .= " datec, fk_product, fk_soc, price, quantity, fk_user)";
4727  $sql .= " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, price, quantity, fk_user";
4728  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
4729  $sql .= " WHERE fk_product = ".((int) $fromId);
4730 
4731  dol_syslog(get_class($this).'::clone_fournisseurs', LOG_DEBUG);
4732  $resql = $this->db->query($sql);
4733  if (!$resql) {
4734  $this->db->rollback();
4735  return -1;
4736  } else {
4737  $this->db->commit();
4738  return 1;
4739  }
4740  }
4741 
4742  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4755  public function fetch_prod_arbo($prod, $compl_path = '', $multiply = 1, $level = 1, $id_parent = 0, $ignore_stock_load = 0)
4756  {
4757  // phpcs:enable
4758  global $conf, $langs;
4759 
4760  $tmpproduct = null;
4761  //var_dump($prod);
4762  foreach ($prod as $id_product => $desc_pere) { // $id_product is 0 (first call starting with root top) or an id of a sub_product
4763  if (is_array($desc_pere)) { // If desc_pere is an array, this means it's a child
4764  $id = (!empty($desc_pere[0]) ? $desc_pere[0] : '');
4765  $nb = (!empty($desc_pere[1]) ? $desc_pere[1] : '');
4766  $type = (!empty($desc_pere[2]) ? $desc_pere[2] : '');
4767  $label = (!empty($desc_pere[3]) ? $desc_pere[3] : '');
4768  $incdec = (!empty($desc_pere[4]) ? $desc_pere[4] : 0);
4769 
4770  if ($multiply < 1) {
4771  $multiply = 1;
4772  }
4773 
4774  //print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n";
4775  if (is_null($tmpproduct)) {
4776  $tmpproduct = new Product($this->db); // So we initialize tmpproduct only once for all loop.
4777  }
4778  $tmpproduct->fetch($id); // Load product to get ->ref
4779 
4780  if (empty($ignore_stock_load) && ($tmpproduct->isProduct() || !empty($conf->global->STOCK_SUPPORTS_SERVICES))) {
4781  $tmpproduct->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
4782  }
4783 
4784  $this->res[] = array(
4785  'id'=>$id, // Id product
4786  'id_parent'=>$id_parent,
4787  'ref'=>$tmpproduct->ref, // Ref product
4788  'nb'=>$nb, // Nb of units that compose parent product
4789  'nb_total'=>$nb * $multiply, // Nb of units for all nb of product
4790  'stock'=>$tmpproduct->stock_reel, // Stock
4791  'stock_alert'=>$tmpproduct->seuil_stock_alerte, // Stock alert
4792  'label'=>$label,
4793  'fullpath'=>$compl_path.$label, // Label
4794  'type'=>$type, // Nb of units that compose parent product
4795  'desiredstock'=>$tmpproduct->desiredstock,
4796  'level'=>$level,
4797  'incdec'=>$incdec,
4798  'entity'=>$tmpproduct->entity
4799  );
4800 
4801  // Recursive call if there is childs to child
4802  if (isset($desc_pere['childs']) && is_array($desc_pere['childs'])) {
4803  //print 'YYY We go down for '.$desc_pere[3]." -> \n";
4804  $this->fetch_prod_arbo($desc_pere['childs'], $compl_path.$desc_pere[3]." -> ", $desc_pere[1] * $multiply, $level + 1, $id, $ignore_stock_load);
4805  }
4806  }
4807  }
4808  }
4809 
4810  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4819  public function get_arbo_each_prod($multiply = 1, $ignore_stock_load = 0)
4820  {
4821  // phpcs:enable
4822  $this->res = array();
4823  if (isset($this->sousprods) && is_array($this->sousprods)) {
4824  foreach ($this->sousprods as $prod_name => $desc_product) {
4825  if (is_array($desc_product)) {
4826  $this->fetch_prod_arbo($desc_product, "", $multiply, 1, $this->id, $ignore_stock_load); // This set $this->res
4827  }
4828  }
4829  }
4830  //var_dump($res);
4831  return $this->res;
4832  }
4833 
4841  public function hasFatherOrChild($mode = 0)
4842  {
4843  $nb = 0;
4844 
4845  $sql = "SELECT COUNT(pa.rowid) as nb";
4846  $sql .= " FROM ".$this->db->prefix()."product_association as pa";
4847  if ($mode == 0) {
4848  $sql .= " WHERE pa.fk_product_fils = ".((int) $this->id)." OR pa.fk_product_pere = ".((int) $this->id);
4849  } elseif ($mode == -1) {
4850  $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)
4851  } elseif ($mode == 1) {
4852  $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)
4853  }
4854 
4855  $resql = $this->db->query($sql);
4856  if ($resql) {
4857  $obj = $this->db->fetch_object($resql);
4858  if ($obj) {
4859  $nb = $obj->nb;
4860  }
4861  } else {
4862  return -1;
4863  }
4864 
4865  return $nb;
4866  }
4867 
4873  public function hasVariants()
4874  {
4875  $nb = 0;
4876  $sql = "SELECT count(rowid) as nb FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_parent = ".((int) $this->id);
4877  $sql .= " AND entity IN (".getEntity('product').")";
4878 
4879  $resql = $this->db->query($sql);
4880  if ($resql) {
4881  $obj = $this->db->fetch_object($resql);
4882  if ($obj) {
4883  $nb = $obj->nb;
4884  }
4885  }
4886 
4887  return $nb;
4888  }
4889 
4890 
4896  public function isVariant()
4897  {
4898  global $conf;
4899  if (isModEnabled('variants')) {
4900  $sql = "SELECT rowid FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_child = ".((int) $this->id)." AND entity IN (".getEntity('product').")";
4901 
4902  $query = $this->db->query($sql);
4903 
4904  if ($query) {
4905  if (!$this->db->num_rows($query)) {
4906  return false;
4907  }
4908  return true;
4909  } else {
4910  dol_print_error($this->db);
4911  return -1;
4912  }
4913  } else {
4914  return false;
4915  }
4916  }
4917 
4924  public function getFather()
4925  {
4926  $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";
4927  $sql .= ", p.tosell as status, p.tobuy as status_buy";
4928  $sql .= " FROM ".$this->db->prefix()."product_association as pa,";
4929  $sql .= " ".$this->db->prefix()."product as p";
4930  $sql .= " WHERE p.rowid = pa.fk_product_pere";
4931  $sql .= " AND pa.fk_product_fils = ".((int) $this->id);
4932 
4933  $res = $this->db->query($sql);
4934  if ($res) {
4935  $prods = array();
4936  while ($record = $this->db->fetch_array($res)) {
4937  // $record['id'] = $record['rowid'] = id of father
4938  $prods[$record['id']]['id'] = $record['rowid'];
4939  $prods[$record['id']]['ref'] = $record['ref'];
4940  $prods[$record['id']]['label'] = $record['label'];
4941  $prods[$record['id']]['qty'] = $record['qty'];
4942  $prods[$record['id']]['incdec'] = $record['incdec'];
4943  $prods[$record['id']]['fk_product_type'] = $record['fk_product_type'];
4944  $prods[$record['id']]['entity'] = $record['entity'];
4945  $prods[$record['id']]['status'] = $record['status'];
4946  $prods[$record['id']]['status_buy'] = $record['status_buy'];
4947  }
4948  return $prods;
4949  } else {
4950  dol_print_error($this->db);
4951  return -1;
4952  }
4953  }
4954 
4955 
4965  public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array())
4966  {
4967  global $alreadyfound;
4968 
4969  if (empty($id)) {
4970  return array();
4971  }
4972 
4973  $sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,";
4974  $sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec,";
4975  $sql .= " pa.rowid as fk_association, pa.rang";
4976  $sql .= " FROM ".$this->db->prefix()."product as p,";
4977  $sql .= " ".$this->db->prefix()."product_association as pa";
4978  $sql .= " WHERE p.rowid = pa.fk_product_fils";
4979  $sql .= " AND pa.fk_product_pere = ".((int) $id);
4980  $sql .= " AND pa.fk_product_fils <> ".((int) $id); // This should not happens, it is to avoid infinite loop if it happens
4981  $sql.= " ORDER BY pa.rang";
4982 
4983  dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level. ' parents='.(is_array($parents)?implode(',', $parents):$parents), LOG_DEBUG);
4984 
4985  if ($level == 1) {
4986  $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 immediatly
4987  }
4988  // Protection against infinite loop
4989  if ($level > 30) {
4990  return array();
4991  }
4992 
4993  $res = $this->db->query($sql);
4994  if ($res) {
4995  $prods = array();
4996  while ($rec = $this->db->fetch_array($res)) {
4997  if (!empty($alreadyfound[$rec['rowid']])) {
4998  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);
4999  if (in_array($rec['id'], $parents)) {
5000  continue; // We discard this child if it is already found at a higher level in tree in the same branch.
5001  }
5002  }
5003  $alreadyfound[$rec['rowid']] = 1;
5004  $prods[$rec['rowid']] = array(
5005  0=>$rec['rowid'],
5006  1=>$rec['qty'],
5007  2=>$rec['fk_product_type'],
5008  3=>$this->db->escape($rec['label']),
5009  4=>$rec['incdec'],
5010  5=>$rec['ref'],
5011  6=>$rec['fk_association'],
5012  7=>$rec['rang']
5013  );
5014  //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
5015  //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
5016  if (empty($firstlevelonly)) {
5017  $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, array_push($parents, $rec['rowid']));
5018  foreach ($listofchilds as $keyChild => $valueChild) {
5019  $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
5020  }
5021  }
5022  }
5023 
5024  return $prods;
5025  } else {
5026  dol_print_error($this->db);
5027  return -1;
5028  }
5029  }
5030 
5031  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5038  public function get_sousproduits_arbo()
5039  {
5040  // phpcs:enable
5041  $parent = array();
5042 
5043  foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) { // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
5044  $parent[$this->label][$keyChild] = $valueChild;
5045  }
5046  foreach ($parent as $key => $value) { // key=label, value is array of childs
5047  $this->sousprods[$key] = $value;
5048  }
5049  }
5050 
5057  public function getTooltipContentArray($params)
5058  {
5059  global $conf, $langs;
5060 
5061  $langs->load('products', 'other');
5062 
5063  $datas = array();
5064  $nofetch = !empty($params['nofetch']);
5065 
5066  if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER)) {
5067  return ['optimize' => $langs->trans("ShowProduct")];
5068  }
5069 
5070  if (!empty($this->entity)) {
5071  $tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80, 0, 0, 0, 0, 1);
5072  if ($this->nbphoto > 0) {
5073  $datas['photo'] = '<div class="photointooltip floatright">'."\n" . $tmpphoto . '</div>';
5074  }
5075  }
5076 
5077  if ($this->type == Product::TYPE_PRODUCT) {
5078  $datas['picto'] = img_picto('', 'product').' <u class="paddingrightonly">'.$langs->trans("Product").'</u>';
5079  } elseif ($this->type == Product::TYPE_SERVICE) {
5080  $datas['picto']= img_picto('', 'service').' <u class="paddingrightonly">'.$langs->trans("Service").'</u>';
5081  }
5082  if (isset($this->status) && isset($this->status_buy)) {
5083  $datas['status']= ' '.$this->getLibStatut(5, 0) . ' '.$this->getLibStatut(5, 1);
5084  }
5085 
5086  if (!empty($this->ref)) {
5087  $datas['ref']= '<br><b>'.$langs->trans('ProductRef').':</b> '.$this->ref;
5088  }
5089  if (!empty($this->label)) {
5090  $datas['label']= '<br><b>'.$langs->trans('ProductLabel').':</b> '.$this->label;
5091  }
5092  if ($this->type == Product::TYPE_PRODUCT || !empty($conf->global->STOCK_SUPPORTS_SERVICES)) {
5093  if (isModEnabled('productbatch')) {
5094  $langs->load("productbatch");
5095  $datas['batchstatus']= "<br><b>".$langs->trans("ManageLotSerial").'</b>: '.$this->getLibStatut(0, 2);
5096  }
5097  }
5098  if (isModEnabled('barcode')) {
5099  $datas['barcode']= '<br><b>'.$langs->trans('BarCode').':</b> '.$this->barcode;
5100  }
5101 
5102  if ($this->type == Product::TYPE_PRODUCT) {
5103  if ($this->weight) {
5104  $datas['weight']= "<br><b>".$langs->trans("Weight").'</b>: '.$this->weight.' '.measuringUnitString(0, "weight", $this->weight_units);
5105  }
5106  $labelsize = "";
5107  if ($this->length) {
5108  $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Length").'</b>: '.$this->length.' '.measuringUnitString(0, 'size', $this->length_units);
5109  }
5110  if ($this->width) {
5111  $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Width").'</b>: '.$this->width.' '.measuringUnitString(0, 'size', $this->width_units);
5112  }
5113  if ($this->height) {
5114  $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Height").'</b>: '.$this->height.' '.measuringUnitString(0, 'size', $this->height_units);
5115  }
5116  if ($labelsize) {
5117  $datas['size']= "<br>".$labelsize;
5118  }
5119 
5120  $labelsurfacevolume = "";
5121  if ($this->surface) {
5122  $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Surface").'</b>: '.$this->surface.' '.measuringUnitString(0, 'surface', $this->surface_units);
5123  }
5124  if ($this->volume) {
5125  $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Volume").'</b>: '.$this->volume.' '.measuringUnitString(0, 'volume', $this->volume_units);
5126  }
5127  if ($labelsurfacevolume) {
5128  $datas['surface']= "<br>" . $labelsurfacevolume;
5129  }
5130  }
5131  if (!empty($this->pmp) && $this->pmp) {
5132  $datas['pmp'] = "<br><b>".$langs->trans("PMPValue").'</b>: '.price($this->pmp, 0, '', 1, -1, -1, $conf->currency);
5133  }
5134 
5135  if (isModEnabled('accounting')) {
5136  if ($this->status && isset($this->accountancy_code_sell)) {
5137  include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5138  $selllabel = '<br>';
5139  $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellCode').':</b> '.length_accountg($this->accountancy_code_sell);
5140  $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellIntraCode').':</b> '.length_accountg($this->accountancy_code_sell_intra);
5141  $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellExportCode').':</b> '.length_accountg($this->accountancy_code_sell_export);
5142  $datas['accountancysell'] = $selllabel;
5143  }
5144  if ($this->status_buy && isset($this->accountancy_code_buy)) {
5145  include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5146  $buylabel = '';
5147  if (empty($this->status)) {
5148  $buylabel .= '<br>';
5149  }
5150  $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyCode').':</b> '.length_accountg($this->accountancy_code_buy);
5151  $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyIntraCode').':</b> '.length_accountg($this->accountancy_code_buy_intra);
5152  $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyExportCode').':</b> '.length_accountg($this->accountancy_code_buy_export);
5153  $datas['accountancybuy'] = $buylabel;
5154  }
5155  }
5156  // show categories for this record only in ajax to not overload lists
5157  if (isModEnabled('categorie') && !$nofetch) {
5158  require_once DOL_DOCUMENT_ROOT . '/categories/class/categorie.class.php';
5159  $form = new Form($this->db);
5160  $datas['categories'] = '<br>' . $form->showCategories($this->id, Categorie::TYPE_PRODUCT, 1);
5161  }
5162 
5163  return $datas;
5164  }
5165 
5179  public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0, $morecss = '', $add_label = 0, $sep = ' - ')
5180  {
5181  global $conf, $langs, $hookmanager;
5182  include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
5183 
5184  $result = '';
5185 
5186  $newref = $this->ref;
5187  if ($maxlength) {
5188  $newref = dol_trunc($newref, $maxlength, 'middle');
5189  }
5190  $params = [
5191  'id' => $this->id,
5192  'objecttype' => $this->element,
5193  'option' => $option,
5194  'nofetch' => 1,
5195  ];
5196  $classfortooltip = 'classfortooltip';
5197  $dataparams = '';
5198  if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
5199  $classfortooltip = 'classforajaxtooltip';
5200  $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
5201  $label = '';
5202  } else {
5203  $label = implode($this->getTooltipContentArray($params));
5204  }
5205 
5206  $linkclose = '';
5207  if (empty($notooltip)) {
5208  if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER)) {
5209  $label = $langs->trans("ShowProduct");
5210  $linkclose .= ' alt="'.dol_escape_htmltag($label, 1, 1).'"';
5211  }
5212  $linkclose .= ($label ? ' title="'.dol_escape_htmltag($label, 1, 1).'"' : ' title="tocomplete"');
5213  $linkclose .= $dataparams.' class="nowraponall '.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
5214  } else {
5215  $linkclose = ' class="nowraponall'.($morecss ? ' '.$morecss : '').'"';
5216  }
5217 
5218  if ($option == 'supplier' || $option == 'category') {
5219  $url = DOL_URL_ROOT.'/product/fournisseurs.php?id='.$this->id;
5220  } elseif ($option == 'stock') {
5221  $url = DOL_URL_ROOT.'/product/stock/product.php?id='.$this->id;
5222  } elseif ($option == 'composition') {
5223  $url = DOL_URL_ROOT.'/product/composition/card.php?id='.$this->id;
5224  } else {
5225  $url = DOL_URL_ROOT.'/product/card.php?id='.$this->id;
5226  }
5227 
5228  if ($option !== 'nolink') {
5229  // Add param to save lastsearch_values or not
5230  $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
5231  if ($save_lastsearch_value == -1 && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
5232  $add_save_lastsearch_values = 1;
5233  }
5234  if ($add_save_lastsearch_values) {
5235  $url .= '&save_lastsearch_values=1';
5236  }
5237  }
5238 
5239  $linkstart = '<a href="'.$url.'"';
5240  $linkstart .= $linkclose.'>';
5241  $linkend = '</a>';
5242 
5243  $result .= $linkstart;
5244  if ($withpicto) {
5245  if ($this->type == Product::TYPE_PRODUCT) {
5246  $result .= (img_object(($notooltip ? '' : $label), 'product', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5247  }
5248  if ($this->type == Product::TYPE_SERVICE) {
5249  $result .= (img_object(($notooltip ? '' : $label), 'service', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5250  }
5251  }
5252  $result .= dol_escape_htmltag($newref);
5253  $result .= $linkend;
5254  if ($withpicto != 2) {
5255  $result .= (($add_label && $this->label) ? $sep.dol_trunc($this->label, ($add_label > 1 ? $add_label : 0)) : '');
5256  }
5257 
5258  global $action;
5259  $hookmanager->initHooks(array('productdao'));
5260  $parameters = array('id'=>$this->id, 'getnomurl' => &$result, 'label' => &$label);
5261  $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
5262  if ($reshook > 0) {
5263  $result = $hookmanager->resPrint;
5264  } else {
5265  $result .= $hookmanager->resPrint;
5266  }
5267 
5268  return $result;
5269  }
5270 
5271 
5282  public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
5283  {
5284  global $conf, $user, $langs;
5285 
5286  $langs->load("products");
5287  $outputlangs->load("products");
5288 
5289  // Positionne le modele sur le nom du modele a utiliser
5290  if (!dol_strlen($modele)) {
5291  $modele = getDolGlobalString('PRODUCT_ADDON_PDF', 'strato');
5292  }
5293 
5294  $modelpath = "core/modules/product/doc/";
5295 
5296  return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
5297  }
5298 
5306  public function getLibStatut($mode = 0, $type = 0)
5307  {
5308  switch ($type) {
5309  case 0:
5310  return $this->LibStatut($this->status, $mode, $type);
5311  case 1:
5312  return $this->LibStatut($this->status_buy, $mode, $type);
5313  case 2:
5314  return $this->LibStatut($this->status_batch, $mode, $type);
5315  default:
5316  //Simulate previous behavior but should return an error string
5317  return $this->LibStatut($this->status_buy, $mode, $type);
5318  }
5319  }
5320 
5321  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5330  public function LibStatut($status, $mode = 0, $type = 0)
5331  {
5332  // phpcs:enable
5333  global $conf, $langs;
5334 
5335  $labelStatus = $labelStatusShort = '';
5336 
5337  $langs->load('products');
5338  if (isModEnabled('productbatch')) {
5339  $langs->load("productbatch");
5340  }
5341 
5342  if ($type == 2) {
5343  switch ($mode) {
5344  case 0:
5345  $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatch') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial')));
5346  return dolGetStatus($label);
5347  case 1:
5348  $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort')));
5349  return dolGetStatus($label);
5350  case 2:
5351  return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 1, 2);
5352  case 3:
5353  return dolGetStatus($langs->transnoentitiesnoconv('ProductStatusNotOnBatch'), '', '', empty($status) ? 'status5' : 'status4', 3, 'dot');
5354  case 4:
5355  return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 0, 2);
5356  case 5:
5357  return $this->LibStatut($status, 1, 2).' '.$this->LibStatut($status, 3, 2);
5358  default:
5359  return dolGetStatus($langs->transnoentitiesnoconv('Unknown'));
5360  }
5361  }
5362 
5363  $statuttrans = empty($status) ? 'status5' : 'status4';
5364 
5365  if ($status == 0) {
5366  // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
5367  if ($type == 0) {
5368  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnSellShort');
5369  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnSell');
5370  } elseif ($type == 1) {
5371  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBuyShort');
5372  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBuy');
5373  } elseif ($type == 2) {
5374  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBatch');
5375  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort');
5376  }
5377  } elseif ($status == 1) {
5378  // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
5379  if ($type == 0) {
5380  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSellShort');
5381  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSell');
5382  } elseif ($type == 1) {
5383  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBuyShort');
5384  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBuy');
5385  } elseif ($type == 2) {
5386  $labelStatus = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial'));
5387  $labelStatusShort = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort'));
5388  }
5389  } elseif ( $type == 2 && $status == 2 ) {
5390  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSerial');
5391  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSerialShort');
5392  }
5393 
5394  if ($mode > 6) {
5395  return dolGetStatus($langs->transnoentitiesnoconv('Unknown'), '', '', 'status0', 0);
5396  } else {
5397  return dolGetStatus($labelStatus, $labelStatusShort, '', $statuttrans, $mode);
5398  }
5399  }
5400 
5401 
5407  public function getLibFinished()
5408  {
5409  global $langs;
5410  $langs->load('products');
5411 
5412  if (isset($this->finished) && $this->finished >= 0) {
5413  $sql = "SELECT label, code FROM ".$this->db->prefix()."c_product_nature where code = ".((int) $this->finished)." AND active=1";
5414  $resql = $this->db->query($sql);
5415  if ($resql && $this->db->num_rows($resql) > 0) {
5416  $res = $this->db->fetch_array($resql);
5417  $label = $langs->trans($res['label']);
5418  $this->db->free($resql);
5419  return $label;
5420  } else {
5421  $this->error = $this->db->error().' sql='.$sql;
5422  dol_syslog(__METHOD__.' Error '.$this->error, LOG_ERR);
5423  return -1;
5424  }
5425  }
5426 
5427  return '';
5428  }
5429 
5430 
5431  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5448  public function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null)
5449  {
5450  // phpcs:enable
5451  if ($id_entrepot) {
5452  $this->db->begin();
5453 
5454  include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
5455 
5456  if ($nbpiece < 0) {
5457  if (!$movement) {
5458  $movement = 1;
5459  }
5460  $nbpiece = abs($nbpiece);
5461  }
5462 
5463  $op[0] = "+".trim($nbpiece);
5464  $op[1] = "-".trim($nbpiece);
5465 
5466  $movementstock = new MouvementStock($this->db);
5467  $movementstock->setOrigin($origin_element, $origin_id); // Set ->origin_type and ->origin_id
5468  $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', '', '', '', false, 0, $disablestockchangeforsubproduct);
5469 
5470  if ($result >= 0) {
5471  if ($extrafields) {
5472  $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
5473  $movementstock->array_options = $array_options;
5474  $movementstock->insertExtraFields();
5475  }
5476  $this->db->commit();
5477  return 1;
5478  } else {
5479  $this->error = $movementstock->error;
5480  $this->errors = $movementstock->errors;
5481 
5482  $this->db->rollback();
5483  return -1;
5484  }
5485  }
5486 
5487  return -1;
5488  }
5489 
5490  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5510  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)
5511  {
5512  // phpcs:enable
5513  if ($id_entrepot) {
5514  $this->db->begin();
5515 
5516  include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
5517 
5518  if ($nbpiece < 0) {
5519  if (!$movement) {
5520  $movement = 1;
5521  }
5522  $nbpiece = abs($nbpiece);
5523  }
5524 
5525  $op[0] = "+".trim($nbpiece);
5526  $op[1] = "-".trim($nbpiece);
5527 
5528  $movementstock = new MouvementStock($this->db);
5529  $movementstock->setOrigin($origin_element, $origin_id); // Set ->origin_type and ->fk_origin
5530  $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', $dlc, $dluo, $lot, false, 0, $disablestockchangeforsubproduct);
5531 
5532  if ($result >= 0) {
5533  if ($extrafields) {
5534  $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
5535  $movementstock->array_options = $array_options;
5536  $movementstock->insertExtraFields();
5537  }
5538  $this->db->commit();
5539  return 1;
5540  } else {
5541  $this->error = $movementstock->error;
5542  $this->errors = $movementstock->errors;
5543 
5544  $this->db->rollback();
5545  return -1;
5546  }
5547  }
5548  return -1;
5549  }
5550 
5551  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5564  public function load_stock($option = '', $includedraftpoforvirtual = null, $dateofvirtualstock = null)
5565  {
5566  // phpcs:enable
5567  global $conf;
5568 
5569  $this->stock_reel = 0;
5570  $this->stock_warehouse = array();
5571  $this->stock_theorique = 0;
5572 
5573  // Set filter on warehouse status
5574  $warehouseStatus = array();
5575  if (preg_match('/warehouseclosed/', $option)) {
5577  }
5578  if (preg_match('/warehouseopen/', $option)) {
5580  }
5581  if (preg_match('/warehouseinternal/', $option)) {
5582  if (!empty($conf->global->ENTREPOT_EXTRA_STATUS)) {
5584  } else {
5586  }
5587  }
5588 
5589  $sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot";
5590  $sql .= " FROM ".$this->db->prefix()."product_stock as ps";
5591  $sql .= ", ".$this->db->prefix()."entrepot as w";
5592  $sql .= " WHERE w.entity IN (".getEntity('stock').")";
5593  $sql .= " AND w.rowid = ps.fk_entrepot";
5594  $sql .= " AND ps.fk_product = ".((int) $this->id);
5595  if (count($warehouseStatus)) {
5596  $sql .= " AND w.statut IN (".$this->db->sanitize(implode(',', $warehouseStatus)).")";
5597  }
5598 
5599  $sql .= " ORDER BY ps.reel ".(!empty($conf->global->DO_NOT_TRY_TO_DEFRAGMENT_STOCKS_WAREHOUSE)?'DESC':'ASC'); // Note : qty ASC is important for expedition card, to avoid stock fragmentation;
5600 
5601  dol_syslog(get_class($this)."::load_stock", LOG_DEBUG);
5602  $result = $this->db->query($sql);
5603  if ($result) {
5604  $num = $this->db->num_rows($result);
5605  $i = 0;
5606  if ($num > 0) {
5607  while ($i < $num) {
5608  $row = $this->db->fetch_object($result);
5609  $this->stock_warehouse[$row->fk_entrepot] = new stdClass();
5610  $this->stock_warehouse[$row->fk_entrepot]->real = $row->reel;
5611  $this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid;
5612  if ((!preg_match('/nobatch/', $option)) && $this->hasbatch()) {
5613  $this->stock_warehouse[$row->fk_entrepot]->detail_batch = Productbatch::findAll($this->db, $row->rowid, 1, $this->id);
5614  }
5615  $this->stock_reel += $row->reel;
5616  $i++;
5617  }
5618  }
5619  $this->db->free($result);
5620 
5621  if (!preg_match('/novirtual/', $option)) {
5622  $this->load_virtual_stock($includedraftpoforvirtual, $dateofvirtualstock); // This load stock_theorique and also load all arrays stats_xxx...
5623  }
5624 
5625  return 1;
5626  } else {
5627  $this->error = $this->db->lasterror();
5628  return -1;
5629  }
5630  }
5631 
5632 
5633  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5643  public function load_virtual_stock($includedraftpoforvirtual = null, $dateofvirtualstock = null)
5644  {
5645  // phpcs:enable
5646  global $conf, $hookmanager, $action;
5647 
5648  $stock_commande_client = 0;
5649  $stock_commande_fournisseur = 0;
5650  $stock_sending_client = 0;
5651  $stock_reception_fournisseur = 0;
5652  $stock_inproduction = 0;
5653 
5654  //dol_syslog("load_virtual_stock");
5655 
5656  if (isModEnabled('commande')) {
5657  $result = $this->load_stats_commande(0, '1,2', 1);
5658  if ($result < 0) {
5659  dol_print_error($this->db, $this->error);
5660  }
5661  $stock_commande_client = $this->stats_commande['qty'];
5662  }
5663  if (isModEnabled("expedition")) {
5664  require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
5665  $filterShipmentStatus = '';
5666  if (!empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT)) {
5667  $filterShipmentStatus = Expedition::STATUS_VALIDATED.','.Expedition::STATUS_CLOSED;
5668  } elseif (!empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE)) {
5669  $filterShipmentStatus = Expedition::STATUS_CLOSED;
5670  }
5671  $result = $this->load_stats_sending(0, '1,2', 1, $filterShipmentStatus);
5672  if ($result < 0) {
5673  dol_print_error($this->db, $this->error);
5674  }
5675  $stock_sending_client = $this->stats_expedition['qty'];
5676  }
5677  if (isModEnabled("supplier_order")) {
5678  $filterStatus = empty($conf->global->SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK) ? '3,4' : $conf->global->SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK;
5679  if (isset($includedraftpoforvirtual)) {
5680  $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
5681  }
5682  $result = $this->load_stats_commande_fournisseur(0, $filterStatus, 1, $dateofvirtualstock);
5683  if ($result < 0) {
5684  dol_print_error($this->db, $this->error);
5685  }
5686  $stock_commande_fournisseur = $this->stats_commande_fournisseur['qty'];
5687  }
5688  if ((isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) && empty($conf->reception->enabled)) {
5689  // Case module reception is not used
5690  $filterStatus = '4';
5691  if (isset($includedraftpoforvirtual)) {
5692  $filterStatus = '0,'.$filterStatus;
5693  }
5694  $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock);
5695  if ($result < 0) {
5696  dol_print_error($this->db, $this->error);
5697  }
5698  $stock_reception_fournisseur = $this->stats_reception['qty'];
5699  }
5700  if ((isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) && isModEnabled("reception")) {
5701  // Case module reception is used
5702  $filterStatus = '4';
5703  if (isset($includedraftpoforvirtual)) {
5704  $filterStatus = '0,'.$filterStatus;
5705  }
5706  $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock); // Use same tables than when module reception is not used.
5707  if ($result < 0) {
5708  dol_print_error($this->db, $this->error);
5709  }
5710  $stock_reception_fournisseur = $this->stats_reception['qty'];
5711  }
5712  if (isModEnabled('mrp')) {
5713  $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock);
5714  if ($result < 0) {
5715  dol_print_error($this->db, $this->error);
5716  }
5717  $stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty'];
5718  }
5719 
5720  $this->stock_theorique = $this->stock_reel + $stock_inproduction;
5721 
5722  // Stock decrease mode
5723  if (!empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT) || !empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE)) {
5724  $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
5725  } elseif (!empty($conf->global->STOCK_CALCULATE_ON_VALIDATE_ORDER)) {
5726  $this->stock_theorique += 0;
5727  } elseif (!empty($conf->global->STOCK_CALCULATE_ON_BILL)) {
5728  $this->stock_theorique -= $stock_commande_client;
5729  }
5730  // Stock Increase mode
5731  if (!empty($conf->global->STOCK_CALCULATE_ON_RECEPTION) || !empty($conf->global->STOCK_CALCULATE_ON_RECEPTION_CLOSE)) {
5732  $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
5733  } elseif (!empty($conf->global->STOCK_CALCULATE_ON_SUPPLIER_DISPATCH_ORDER)) {
5734  $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
5735  } elseif (!empty($conf->global->STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER)) {
5736  $this->stock_theorique -= $stock_reception_fournisseur;
5737  } elseif (!empty($conf->global->STOCK_CALCULATE_ON_SUPPLIER_BILL)) {
5738  $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
5739  }
5740 
5741  $hookmanager->initHooks(array('productdao'));
5742  $parameters = array('id'=>$this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual);
5743  // Note that $action and $object may have been modified by some hooks
5744  $reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action);
5745  if ($reshook > 0) {
5746  $this->stock_theorique = $hookmanager->resArray['stock_theorique'];
5747  }
5748 
5749  return 1;
5750  }
5751 
5752 
5760  public function loadBatchInfo($batch)
5761  {
5762  $result = array();
5763 
5764  $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";
5765  $sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = ".((int) $this->id)." AND pb.batch = '".$this->db->escape($batch)."'";
5766  $sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby";
5767  dol_syslog(get_class($this)."::loadBatchInfo load first entry found for lot/serial = ".$batch, LOG_DEBUG);
5768  $resql = $this->db->query($sql);
5769  if ($resql) {
5770  $num = $this->db->num_rows($resql);
5771  $i = 0;
5772  while ($i < $num) {
5773  $obj = $this->db->fetch_object($resql);
5774  $result[] = array('batch'=>$batch, 'eatby'=>$this->db->jdate($obj->eatby), 'sellby'=>$this->db->jdate($obj->sellby), 'qty'=>$obj->qty);
5775  $i++;
5776  }
5777  return $result;
5778  } else {
5779  dol_print_error($this->db);
5780  $this->db->rollback();
5781  return array();
5782  }
5783  }
5784 
5785  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5793  public function add_photo($sdir, $file)
5794  {
5795  // phpcs:enable
5796  global $conf;
5797 
5798  include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
5799 
5800  $result = 0;
5801 
5802  $dir = $sdir;
5803  if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
5804  $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos";
5805  } else {
5806  $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product').dol_sanitizeFileName($this->ref);
5807  }
5808 
5809  dol_mkdir($dir);
5810 
5811  $dir_osencoded = $dir;
5812 
5813  if (is_dir($dir_osencoded)) {
5814  $originImage = $dir.'/'.$file['name'];
5815 
5816  // Cree fichier en taille origine
5817  $result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
5818 
5819  if (file_exists(dol_osencode($originImage))) {
5820  // Create thumbs
5821  $this->addThumbs($originImage);
5822  }
5823  }
5824 
5825  if (is_numeric($result) && $result > 0) {
5826  return 1;
5827  } else {
5828  return -1;
5829  }
5830  }
5831 
5832  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5839  public function is_photo_available($sdir)
5840  {
5841  // phpcs:enable
5842  include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
5843  include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
5844 
5845  global $conf;
5846 
5847  $dir = $sdir;
5848  if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
5849  $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos/";
5850  } else {
5851  $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product');
5852  }
5853 
5854  $nbphoto = 0;
5855 
5856  $dir_osencoded = dol_osencode($dir);
5857  if (file_exists($dir_osencoded)) {
5858  $handle = opendir($dir_osencoded);
5859  if (is_resource($handle)) {
5860  while (($file = readdir($handle)) !== false) {
5861  if (!utf8_check($file)) {
5862  $file = utf8_encode($file); // To be sure data is stored in UTF8 in memory
5863  }
5864  if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
5865  return true;
5866  }
5867  }
5868  }
5869  }
5870  return false;
5871  }
5872 
5873  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5881  public function liste_photos($dir, $nbmax = 0)
5882  {
5883  // phpcs:enable
5884  include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
5885  include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
5886 
5887  $nbphoto = 0;
5888  $tabobj = array();
5889 
5890  $dir_osencoded = dol_osencode($dir);
5891  $handle = @opendir($dir_osencoded);
5892  if (is_resource($handle)) {
5893  while (($file = readdir($handle)) !== false) {
5894  if (!utf8_check($file)) {
5895  $file = utf8_encode($file); // readdir returns ISO
5896  }
5897  if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
5898  $nbphoto++;
5899 
5900  // We forge name of thumb.
5901  $photo = $file;
5902  $photo_vignette = '';
5903  $regs = array();
5904  if (preg_match('/('.$this->regeximgext.')$/i', $photo, $regs)) {
5905  $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $photo).'_small'.$regs[0];
5906  }
5907 
5908  $dirthumb = $dir.'thumbs/';
5909 
5910  // Objet
5911  $obj = array();
5912  $obj['photo'] = $photo;
5913  if ($photo_vignette && dol_is_file($dirthumb.$photo_vignette)) {
5914  $obj['photo_vignette'] = 'thumbs/'.$photo_vignette;
5915  } else {
5916  $obj['photo_vignette'] = "";
5917  }
5918 
5919  $tabobj[$nbphoto - 1] = $obj;
5920 
5921  // Do we have to continue with next photo ?
5922  if ($nbmax && $nbphoto >= $nbmax) {
5923  break;
5924  }
5925  }
5926  }
5927 
5928  closedir($handle);
5929  }
5930 
5931  return $tabobj;
5932  }
5933 
5934  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5941  public function delete_photo($file)
5942  {
5943  // phpcs:enable
5944  include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
5945  include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
5946 
5947  $dir = dirname($file).'/'; // Chemin du dossier contenant l'image d'origine
5948  $dirthumb = $dir.'/thumbs/'; // Chemin du dossier contenant la vignette
5949  $filename = preg_replace('/'.preg_quote($dir, '/').'/i', '', $file); // Nom du fichier
5950 
5951  // On efface l'image d'origine
5952  dol_delete_file($file, 0, 0, 0, $this); // For triggers
5953 
5954  // Si elle existe, on efface la vignette
5955  if (preg_match('/('.$this->regeximgext.')$/i', $filename, $regs)) {
5956  $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_small'.$regs[0];
5957  if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
5958  dol_delete_file($dirthumb.$photo_vignette);
5959  }
5960 
5961  $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_mini'.$regs[0];
5962  if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
5963  dol_delete_file($dirthumb.$photo_vignette);
5964  }
5965  }
5966  }
5967 
5968  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5975  public function get_image_size($file)
5976  {
5977  // phpcs:enable
5978  $file_osencoded = dol_osencode($file);
5979  $infoImg = getimagesize($file_osencoded); // Get information on image
5980  $this->imgWidth = $infoImg[0]; // Largeur de l'image
5981  $this->imgHeight = $infoImg[1]; // Hauteur de l'image
5982  }
5983 
5984  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5990  public function load_state_board()
5991  {
5992  // phpcs:enable
5993  global $hookmanager;
5994 
5995  $this->nb = array();
5996 
5997  $sql = "SELECT count(p.rowid) as nb, fk_product_type";
5998  $sql .= " FROM ".$this->db->prefix()."product as p";
5999  $sql .= ' WHERE p.entity IN ('.getEntity($this->element, 1).')';
6000  // Add where from hooks
6001  if (is_object($hookmanager)) {
6002  $parameters = array();
6003  $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
6004  $sql .= $hookmanager->resPrint;
6005  }
6006  $sql .= ' GROUP BY fk_product_type';
6007 
6008  $resql = $this->db->query($sql);
6009  if ($resql) {
6010  while ($obj = $this->db->fetch_object($resql)) {
6011  if ($obj->fk_product_type == 1) {
6012  $this->nb["services"] = $obj->nb;
6013  } else {
6014  $this->nb["products"] = $obj->nb;
6015  }
6016  }
6017  $this->db->free($resql);
6018  return 1;
6019  } else {
6020  dol_print_error($this->db);
6021  $this->error = $this->db->error();
6022  return -1;
6023  }
6024  }
6025 
6031  public function isProduct()
6032  {
6033  return ($this->type == Product::TYPE_PRODUCT ? true : false);
6034  }
6035 
6041  public function isService()
6042  {
6043  return ($this->type == Product::TYPE_SERVICE ? true : false);
6044  }
6045 
6046 
6052  public function isMandatoryPeriod()
6053  {
6054  return ($this->mandatory_period == 1 ? true : false);
6055  }
6056  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6065  public function get_barcode($object, $type = '')
6066  {
6067  // phpcs:enable
6068  global $conf;
6069 
6070  $result = '';
6071  if (!empty($conf->global->BARCODE_PRODUCT_ADDON_NUM)) {
6072  $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
6073  foreach ($dirsociete as $dirroot) {
6074  $res = dol_include_once($dirroot.$conf->global->BARCODE_PRODUCT_ADDON_NUM.'.php');
6075  if ($res) {
6076  break;
6077  }
6078  }
6079  $var = $conf->global->BARCODE_PRODUCT_ADDON_NUM;
6080  $mod = new $var;
6081 
6082  $result = $mod->getNextValue($object, $type);
6083 
6084  dol_syslog(get_class($this)."::get_barcode barcode=".$result." module=".$var);
6085  }
6086  return $result;
6087  }
6088 
6096  public function initAsSpecimen()
6097  {
6098  global $user, $langs, $conf, $mysoc;
6099 
6100  $now = dol_now();
6101 
6102  // Initialize parameters
6103  $this->specimen = 1;
6104  $this->id = 0;
6105  $this->ref = 'PRODUCT_SPEC';
6106  $this->label = 'PRODUCT SPECIMEN';
6107  $this->description = 'This is description of this product specimen that was created the '.dol_print_date($now, 'dayhourlog').'.';
6108  $this->specimen = 1;
6109  $this->country_id = 1;
6110  $this->status = 1;
6111  $this->status_buy = 1;
6112  $this->tobatch = 0;
6113  $this->note_private = 'This is a comment (private)';
6114  $this->note_public = 'This is a comment (public)';
6115  $this->date_creation = $now;
6116  $this->date_modification = $now;
6117 
6118  $this->weight = 4;
6119  $this->weight_units = 3;
6120 
6121  $this->length = 5;
6122  $this->length_units = 1;
6123  $this->width = 6;
6124  $this->width_units = 0;
6125  $this->height = null;
6126  $this->height_units = null;
6127 
6128  $this->surface = 30;
6129  $this->surface_units = 0;
6130  $this->volume = 300;
6131  $this->volume_units = 0;
6132 
6133  $this->barcode = -1; // Create barcode automatically
6134  }
6135 
6142  public function getLabelOfUnit($type = 'long')
6143  {
6144  global $langs;
6145 
6146  if (!$this->fk_unit) {
6147  return '';
6148  }
6149 
6150  $langs->load('products');
6151 
6152  $label_type = 'label';
6153  if ($type == 'short') {
6154  $label_type = 'short_label';
6155  }
6156 
6157  $sql = "SELECT ".$label_type.", code from ".$this->db->prefix()."c_units where rowid = ".((int) $this->fk_unit);
6158 
6159  $resql = $this->db->query($sql);
6160  if ($resql && $this->db->num_rows($resql) > 0) {
6161  $res = $this->db->fetch_array($resql);
6162  $label = ($label_type == 'short_label' ? $res[$label_type] : 'unit'.$res['code']);
6163  $this->db->free($resql);
6164  return $label;
6165  } else {
6166  $this->error = $this->db->error();
6167  dol_syslog(get_class($this)."::getLabelOfUnit Error ".$this->error, LOG_ERR);
6168  return -1;
6169  }
6170  }
6171 
6177  public function hasbatch()
6178  {
6179  return ($this->status_batch > 0 ? true : false);
6180  }
6181 
6182 
6183  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6189  public function min_recommended_price()
6190  {
6191  // phpcs:enable
6192  global $conf;
6193 
6194  $maxpricesupplier = 0;
6195 
6196  if (!empty($conf->global->PRODUCT_MINIMUM_RECOMMENDED_PRICE)) {
6197  include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
6198  $product_fourn = new ProductFournisseur($this->db);
6199  $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', '');
6200 
6201  if (is_array($product_fourn_list) && count($product_fourn_list) > 0) {
6202  foreach ($product_fourn_list as $productfourn) {
6203  if ($productfourn->fourn_unitprice > $maxpricesupplier) {
6204  $maxpricesupplier = $productfourn->fourn_unitprice;
6205  }
6206  }
6207 
6208  $maxpricesupplier *= $conf->global->PRODUCT_MINIMUM_RECOMMENDED_PRICE;
6209  }
6210  }
6211 
6212  return $maxpricesupplier;
6213  }
6214 
6215 
6226  public function setCategories($categories)
6227  {
6228  require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
6229  return parent::setCategoriesCommon($categories, Categorie::TYPE_PRODUCT);
6230  }
6231 
6240  public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
6241  {
6242  $tables = array(
6243  'product_customer_price',
6244  'product_customer_price_log'
6245  );
6246 
6247  return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
6248  }
6249 
6261  public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
6262  {
6263  global $conf, $db;
6264 
6265  $sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM ".$this->db->prefix()."product_pricerules";
6266  $query = $this->db->query($sql);
6267 
6268  $rules = array();
6269 
6270  while ($result = $this->db->fetch_object($query)) {
6271  $rules[$result->level] = $result;
6272  }
6273 
6274  //Because prices can be based on other level's prices, we temporarily store them
6275  $prices = array(
6276  1 => $baseprice
6277  );
6278 
6279  for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) {
6280  $price = $baseprice;
6281  $price_min = $baseprice;
6282 
6283  //We have to make sure it does exist and it is > 0
6284  //First price level only allows changing min_price
6285  if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) {
6286  $price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent / 100));
6287  }
6288 
6289  $prices[$i] = $price;
6290 
6291  //We have to make sure it does exist and it is > 0
6292  if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) {
6293  $price_min = $price * (1 - ($rules[$i]->var_min_percent / 100));
6294  }
6295 
6296  //Little check to make sure the price is modified before triggering generation
6297  $check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i]));
6298  $check_type = ($baseprice == $this->multiprices_base_type[$i]);
6299 
6300  if ($check_amount && $check_type) {
6301  continue;
6302  }
6303 
6304  if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, true) < 0) {
6305  return -1;
6306  }
6307  }
6308 
6309  return 1;
6310  }
6311 
6317  public function getRights()
6318  {
6319  global $user;
6320 
6321  if ($this->isProduct()) {
6322  return $user->rights->produit;
6323  } else {
6324  return $user->rights->service;
6325  }
6326  }
6327 
6334  public function info($id)
6335  {
6336  $sql = "SELECT p.rowid, p.ref, p.datec as date_creation, p.tms as date_modification,";
6337  $sql .= " p.fk_user_author, p.fk_user_modif";
6338  $sql .= " FROM ".$this->db->prefix().$this->table_element." as p";
6339  $sql .= " WHERE p.rowid = ".((int) $id);
6340 
6341  $result = $this->db->query($sql);
6342  if ($result) {
6343  if ($this->db->num_rows($result)) {
6344  $obj = $this->db->fetch_object($result);
6345 
6346  $this->id = $obj->rowid;
6347 
6348  if ($obj->fk_user_author) {
6349  $cuser = new User($this->db);
6350  $cuser->fetch($obj->fk_user_author);
6351  $this->user_creation = $cuser;
6352  }
6353 
6354  if ($obj->fk_user_modif) {
6355  $muser = new User($this->db);
6356  $muser->fetch($obj->fk_user_modif);
6357  $this->user_modification = $muser;
6358  }
6359 
6360  $this->ref = $obj->ref;
6361  $this->date_creation = $this->db->jdate($obj->date_creation);
6362  $this->date_modification = $this->db->jdate($obj->date_modification);
6363  }
6364 
6365  $this->db->free($result);
6366  } else {
6367  dol_print_error($this->db);
6368  }
6369  }
6370 
6371 
6376  public function getProductDurationHours()
6377  {
6378  global $langs;
6379 
6380  if (empty($this->duration_value)) {
6381  $this->errors[]='ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
6382  return -1;
6383  }
6384 
6385  if ($this->duration_unit == 'i') {
6386  $prodDurationHours = 1. / 60;
6387  }
6388  if ($this->duration_unit == 'h') {
6389  $prodDurationHours = 1.;
6390  }
6391  if ($this->duration_unit == 'd') {
6392  $prodDurationHours = 24.;
6393  }
6394  if ($this->duration_unit == 'w') {
6395  $prodDurationHours = 24. * 7;
6396  }
6397  if ($this->duration_unit == 'm') {
6398  $prodDurationHours = 24. * 30;
6399  }
6400  if ($this->duration_unit == 'y') {
6401  $prodDurationHours = 24. * 365;
6402  }
6403  $prodDurationHours *= $this->duration_value;
6404 
6405  return $prodDurationHours;
6406  }
6407 
6408 
6416  public function getKanbanView($option = '', $arraydata = null)
6417  {
6418  global $langs,$conf;
6419 
6420  $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
6421 
6422  $return = '<div class="box-flex-item box-flex-grow-zero">';
6423  $return .= '<div class="info-box info-box-sm">';
6424  $return .= '<div class="info-box-img">';
6425  $label = '';
6426  if ($this->is_photo_available($conf->product->multidir_output[$this->entity])) {
6427  $label .= $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 120, 160, 0, 0, 0, '', 'photoref photokanban');
6428  $return .= $label;
6429  } else {
6430  if ($this->type == Product::TYPE_PRODUCT) {
6431  $label .= img_picto('', 'product');
6432  } elseif ($this->type == Product::TYPE_SERVICE) {
6433  $label .= img_picto('', 'service');
6434  }
6435  $return .= $label;
6436  }
6437  $return .= '</div>';
6438  $return .= '<div class="info-box-content">';
6439  $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">'.(method_exists($this, 'getNomUrl') ? $this->getNomUrl() : $this->ref).'</span>';
6440  $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
6441  if (property_exists($this, 'label')) {
6442  $return .= '<br><span class="info-box-label opacitymedium">'.$this->label.'</span>';
6443  }
6444  if (property_exists($this, 'price') && property_exists($this, 'price_ttc')) {
6445  if ($this->price_base_type == 'TTC') {
6446  $return .= '<br><span class="info-box-status amount">'.price($this->price_ttc).' '.$langs->trans("TTC").'</span>';
6447  } else {
6448  if ($this->status) {
6449  $return .= '<br><span class="info-box-status amount">'.price($this->price).' '.$langs->trans("HT").'</span>';
6450  }
6451  }
6452  }
6453  if (property_exists($this, 'stock_reel')) {
6454  $return .= '<br><span class="info-box-status opacitymedium">'.$langs->trans('PhysicalStock').' : <span class="bold">'.$this->stock_reel.'</span></span>';
6455  }
6456  if (method_exists($this, 'getLibStatut')) {
6457  $return .='<br><span class="info-box-status margintoponly">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</span>';
6458  }
6459  $return .= '</div>';
6460  $return .= '</div>';
6461  $return .= '</div>';
6462  return $return;
6463  }
6464 }
6465 
6471 {
6472  public $picto = 'service';
6473 }
length_accountg($account)
Return General accounting account with defined length (used for product and miscellaneous)
$object ref
Definition: info.php:78
Parent class of all other business classes (invoices, contracts, proposals, orders,...
fetch_optionals($rowid=null, $optionsArray=null)
Function to get extra fields of an object into $this->array_options This method is in most cases call...
deleteEcmFiles($mode=0)
Delete related files of object in database.
commonGenerateDocument($modelspath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref, $moreparams=null)
Common function for all objects extending CommonObject for generating documents.
isObjectUsed($id=0, $entity=0)
Function to check if an object is used by others (by children).
deleteExtraFields()
Delete all extra fields values for the current object.
addThumbs($file)
Build thumb.
static commonReplaceThirdparty(DoliDB $dbs, $origin_id, $dest_id, array $tables, $ignoreerrors=0)
Function used to replace a thirdparty id with another one.
show_photos($modulepart, $sdir, $size=0, $nbmax=0, $nbbyrow=5, $showfilename=0, $showaction=0, $maxHeight=120, $maxWidth=160, $nolink=0, $overwritetitle=0, $usesharelink=0, $cache='', $addphotorefcss='photoref')
Show photos of an object (nbmax maximum), into several columns.
insertExtraFields($trigger='', $userused=null)
Add/Update all extra fields values for the current object.
call_trigger($triggerName, $user)
Call trigger based on this instance.
Class to manage Dolibarr database access.
const STATUS_OPEN_INTERNAL
Warehouse open and only operations for stock transfers/corrections allowed (not for customer shipping...
const STATUS_OPEN_ALL
Warehouse open and any operations are allowed (customer shipping, supplier dispatch,...
const STATUS_CLOSED
Warehouse closed, inactive.
const STATUS_CLOSED
Closed status.
const STATUS_VALIDATED
Validated status.
const STATUS_DRAFT
Draft status.
Class to manage generation of HTML components Only common components must be here.
Class to manage stock movements.
Class to parse product price expressions.
Class ProductCombination Used to represent a product combination.
Class to manage predefined suppliers products.
Class to manage products or services.
static replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
Function used to replace a thirdparty id with another one.
get_nb_achat($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units or supplier invoices in which product is included.
getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp=0)
Return price of sell of a product for a seller/buyer/product.
__construct($db)
Constructor.
$price_by_qty
Price by quantity arrays.
is_sousproduit($fk_parent, $fk_child)
Check if it is a sub-product into a kit.
$duration
Service expiration label (value + unit)
setPriceExpression($expression_id)
Sets the supplier price expression.
getArrayForPriceCompare($level=0)
used to check if price have really change to avoid log pollution
get_arbo_each_prod($multiply=1, $ignore_stock_load=0)
Build the tree of subproducts and return it.
check_barcode($valuetotest, $typefortest)
Check barcode.
correct_stock_batch($user, $id_entrepot, $nbpiece, $movement, $label='', $price=0, $dlc='', $dluo='', $lot='', $inventorycode='', $origin_element='', $origin_id=null, $disablestockchangeforsubproduct=0, $extrafields=null)
Adjust stock in a warehouse for product with batch number.
list_suppliers()
Return list of suppliers providing the product or service.
load_stats_mo($socid=0)
Charge tableau des stats OF pour le produit/service.