dolibarr  18.0.0-beta
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 
197  public $tva_npr = 0;
198 
201 
204  public $localtax2_tx;
205  public $localtax1_type;
206  public $localtax2_type;
207 
208  // Properties set by get_buyprice() for return
209 
210  public $desc_supplier;
211  public $vatrate_supplier;
212  public $default_vat_code_supplier;
213  public $fourn_multicurrency_price;
214  public $fourn_multicurrency_unitprice;
215  public $fourn_multicurrency_tx;
216  public $fourn_multicurrency_id;
217  public $fourn_multicurrency_code;
218  public $packaging;
219 
220 
221  public $lifetime;
222 
223  public $qc_frequency;
224 
230  public $stock_reel = 0;
231 
237  public $stock_theorique;
238 
244  public $cost_price;
245 
247  public $pmp;
248 
254  public $seuil_stock_alerte = 0;
255 
259  public $desiredstock = 0;
260 
261  /*
262  * Service expiration
263  */
264  public $duration_value;
265 
266  /*
267  * Service Workstation
268  */
269  public $fk_default_workstation;
270 
275 
281  public $status = 0;
282 
289  public $tosell;
290 
296  public $status_buy = 0;
297 
304  public $tobuy;
305 
311  public $finished;
312 
318  public $fk_default_bom;
319 
325  public $status_batch = 0;
326 
332  public $batch_mask = '';
333 
339  public $customcode;
340 
346  public $url;
347 
349  public $weight;
350  public $weight_units; // scale -3, 0, 3, 6
351  public $length;
352  public $length_units; // scale -3, 0, 3, 6
353  public $width;
354  public $width_units; // scale -3, 0, 3, 6
355  public $height;
356  public $height_units; // scale -3, 0, 3, 6
357  public $surface;
358  public $surface_units; // scale -3, 0, 3, 6
359  public $volume;
360  public $volume_units; // scale -3, 0, 3, 6
361 
362  public $net_measure;
363  public $net_measure_units; // scale -3, 0, 3, 6
364 
365  public $accountancy_code_sell;
366  public $accountancy_code_sell_intra;
367  public $accountancy_code_sell_export;
368  public $accountancy_code_buy;
369  public $accountancy_code_buy_intra;
370  public $accountancy_code_buy_export;
371 
377  public $barcode;
378 
384  public $barcode_type;
385 
391  public $barcode_type_code;
392 
393  public $stats_propale = array();
394  public $stats_commande = array();
395  public $stats_contrat = array();
396  public $stats_facture = array();
397  public $stats_commande_fournisseur = array();
398  public $stats_expedition = array();
399  public $stats_reception = array();
400  public $stats_mo = array();
401  public $stats_bom = array();
402  public $stats_mrptoconsume = array();
403  public $stats_mrptoproduce = array();
404  public $stats_facturerec = array();
405  public $stats_facture_fournisseur = array();
406 
408  public $imgWidth;
409  public $imgHeight;
410 
414  public $date_creation;
415 
419  public $date_modification;
420 
423 
426 
427  public $nbphoto = 0;
428 
430  public $stock_warehouse = array();
431 
432  public $oldcopy;
433 
437  public $fk_default_warehouse;
441  public $fk_price_expression;
442 
443  /* To store supplier price found */
444  public $fourn_qty;
445  public $fourn_pu;
446  public $fourn_price_base_type;
447  public $fourn_socid;
448 
453  public $ref_fourn;
454 
458  public $ref_supplier;
459 
465  public $fk_unit;
466 
472  public $price_autogen = 0;
473 
479  public $supplierprices;
480 
486  public $is_object_used;
487 
488 
494  public $mandatory_period;
495 
524  public $fields = array(
525  'rowid' => array('type'=>'integer', 'label'=>'TechnicalID', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'index'=>1, 'position'=>1, 'comment'=>'Id'),
526  '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'),
527  'entity' =>array('type'=>'integer', 'label'=>'Entity', 'enabled'=>1, 'visible'=>0, 'default'=>1, 'notnull'=>1, 'index'=>1, 'position'=>5),
528  'label' =>array('type'=>'varchar(255)', 'label'=>'Label', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'showoncombobox'=>2, 'position'=>15, 'csslist'=>'tdoverflowmax250'),
529  'barcode' =>array('type'=>'varchar(255)', 'label'=>'Barcode', 'enabled'=>'isModEnabled("barcode")', 'position'=>20, 'visible'=>-1, 'showoncombobox'=>3),
530  'fk_barcode_type' => array('type'=>'integer', 'label'=>'BarcodeType', 'enabled'=>'1', 'position'=>21, 'notnull'=>0, 'visible'=>-1,),
531  'note_public' =>array('type'=>'html', 'label'=>'NotePublic', 'enabled'=>1, 'visible'=>0, 'position'=>61),
532  'note' =>array('type'=>'html', 'label'=>'NotePrivate', 'enabled'=>1, 'visible'=>0, 'position'=>62),
533  'datec' =>array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'position'=>500),
534  'tms' =>array('type'=>'timestamp', 'label'=>'DateModification', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'position'=>501),
535  //'date_valid' =>array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-2, 'position'=>502),
536  'fk_user_author'=>array('type'=>'integer', 'label'=>'UserAuthor', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'position'=>510, 'foreignkey'=>'llx_user.rowid'),
537  'fk_user_modif' =>array('type'=>'integer', 'label'=>'UserModif', 'enabled'=>1, 'visible'=>-2, 'notnull'=>-1, 'position'=>511),
538  //'fk_user_valid' =>array('type'=>'integer', 'label'=>'UserValidation', 'enabled'=>1, 'visible'=>-1, 'position'=>512),
539  'localtax1_tx' => array('type'=>'double(6,3)', 'label'=>'Localtax1tx', 'enabled'=>'1', 'position'=>150, 'notnull'=>0, 'visible'=>-1,),
540  'localtax1_type' => array('type'=>'varchar(10)', 'label'=>'Localtax1type', 'enabled'=>'1', 'position'=>155, 'notnull'=>1, 'visible'=>-1,),
541  'localtax2_tx' => array('type'=>'double(6,3)', 'label'=>'Localtax2tx', 'enabled'=>'1', 'position'=>160, 'notnull'=>0, 'visible'=>-1,),
542  'localtax2_type' => array('type'=>'varchar(10)', 'label'=>'Localtax2type', 'enabled'=>'1', 'position'=>165, 'notnull'=>1, 'visible'=>-1,),
543  'import_key' =>array('type'=>'varchar(14)', 'label'=>'ImportId', 'enabled'=>1, 'visible'=>-2, 'notnull'=>-1, 'index'=>0, 'position'=>1000),
544  //'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')),
545  //'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')),
546  'mandatory_period' => array('type'=>'integer', 'label'=>'mandatory_period', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'default'=>0, 'index'=>1, 'position'=>1000),
547  );
548 
552  const TYPE_PRODUCT = 0;
556  const TYPE_SERVICE = 1;
560  const TYPE_ASSEMBLYKIT = 2;
564  const TYPE_STOCKKIT = 3;
565 
566 
572  public function __construct($db)
573  {
574  $this->db = $db;
575  $this->canvas = '';
576  }
577 
583  public function check()
584  {
585  if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
586  $this->ref = trim($this->ref);
587  } else {
588  $this->ref = dol_sanitizeFileName(stripslashes($this->ref));
589  }
590 
591  $err = 0;
592  if (dol_strlen(trim($this->ref)) == 0) {
593  $err++;
594  }
595 
596  if (dol_strlen(trim($this->label)) == 0) {
597  $err++;
598  }
599 
600  if ($err > 0) {
601  return 0;
602  } else {
603  return 1;
604  }
605  }
606 
614  public function create($user, $notrigger = 0)
615  {
616  global $conf, $langs;
617 
618  $error = 0;
619 
620  // Clean parameters
621  if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
622  $this->ref = trim($this->ref);
623  } else {
624  $this->ref = dol_sanitizeFileName(dol_string_nospecial(trim($this->ref)));
625  }
626  $this->label = trim($this->label);
627  $this->price_ttc = price2num($this->price_ttc);
628  $this->price = price2num($this->price);
629  $this->price_min_ttc = price2num($this->price_min_ttc);
630  $this->price_min = price2num($this->price_min);
631  if (empty($this->tva_tx)) {
632  $this->tva_tx = 0;
633  }
634  if (empty($this->tva_npr)) {
635  $this->tva_npr = 0;
636  }
637  //Local taxes
638  if (empty($this->localtax1_tx)) {
639  $this->localtax1_tx = 0;
640  }
641  if (empty($this->localtax2_tx)) {
642  $this->localtax2_tx = 0;
643  }
644  if (empty($this->localtax1_type)) {
645  $this->localtax1_type = '0';
646  }
647  if (empty($this->localtax2_type)) {
648  $this->localtax2_type = '0';
649  }
650  if (empty($this->price)) {
651  $this->price = 0;
652  }
653  if (empty($this->price_min)) {
654  $this->price_min = 0;
655  }
656  // Price by quantity
657  if (empty($this->price_by_qty)) {
658  $this->price_by_qty = 0;
659  }
660 
661  if (empty($this->status)) {
662  $this->status = 0;
663  }
664  if (empty($this->status_buy)) {
665  $this->status_buy = 0;
666  }
667 
668  $price_ht = 0;
669  $price_ttc = 0;
670  $price_min_ht = 0;
671  $price_min_ttc = 0;
672 
673  //
674  if ($this->price_base_type == 'TTC' && $this->price_ttc > 0) {
675  $price_ttc = price2num($this->price_ttc, 'MU');
676  $price_ht = price2num($this->price_ttc / (1 + ($this->tva_tx / 100)), 'MU');
677  }
678 
679  //
680  if ($this->price_base_type != 'TTC' && $this->price > 0) {
681  $price_ht = price2num($this->price, 'MU');
682  $price_ttc = price2num($this->price * (1 + ($this->tva_tx / 100)), 'MU');
683  }
684 
685  //
686  if (($this->price_min_ttc > 0) && ($this->price_base_type == 'TTC')) {
687  $price_min_ttc = price2num($this->price_min_ttc, 'MU');
688  $price_min_ht = price2num($this->price_min_ttc / (1 + ($this->tva_tx / 100)), 'MU');
689  }
690 
691  //
692  if (($this->price_min > 0) && ($this->price_base_type != 'TTC')) {
693  $price_min_ht = price2num($this->price_min, 'MU');
694  $price_min_ttc = price2num($this->price_min * (1 + ($this->tva_tx / 100)), 'MU');
695  }
696 
697  $this->accountancy_code_buy = trim($this->accountancy_code_buy);
698  $this->accountancy_code_buy_intra = trim($this->accountancy_code_buy_intra);
699  $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
700  $this->accountancy_code_sell = trim($this->accountancy_code_sell);
701  $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
702  $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
703 
704  // Barcode value
705  $this->barcode = trim($this->barcode);
706  $this->mandatory_period = empty($this->mandatory_period) ? 0 : $this->mandatory_period;
707  // Check parameters
708  if (empty($this->label)) {
709  $this->error = 'ErrorMandatoryParametersNotProvided';
710  return -1;
711  }
712 
713  if (empty($this->ref) || $this->ref == 'auto') {
714  // Load object modCodeProduct
715  $module = getDolGlobalString('PRODUCT_CODEPRODUCT_ADDON', 'mod_codeproduct_leopard');
716  if ($module != 'mod_codeproduct_leopard') { // Do not load module file for leopard
717  if (substr($module, 0, 16) == 'mod_codeproduct_' && substr($module, -3) == 'php') {
718  $module = substr($module, 0, dol_strlen($module) - 4);
719  }
720  dol_include_once('/core/modules/product/'.$module.'.php');
721  $modCodeProduct = new $module;
722  if (!empty($modCodeProduct->code_auto)) {
723  $this->ref = $modCodeProduct->getNextValue($this, $this->type);
724  }
725  unset($modCodeProduct);
726  }
727 
728  if (empty($this->ref)) {
729  $this->error = 'ProductModuleNotSetupForAutoRef';
730  return -2;
731  }
732  }
733 
734  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);
735 
736  $now = dol_now();
737 
738  $this->db->begin();
739 
740  // For automatic creation during create action (not used by Dolibarr GUI, can be used by scripts)
741  if ($this->barcode == -1) {
742  $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
743  }
744 
745  // Check more parameters
746  // If error, this->errors[] is filled
747  $result = $this->verify();
748 
749  if ($result >= 0) {
750  $sql = "SELECT count(*) as nb";
751  $sql .= " FROM ".$this->db->prefix()."product";
752  $sql .= " WHERE entity IN (".getEntity('product').")";
753  $sql .= " AND ref = '".$this->db->escape($this->ref)."'";
754 
755  $result = $this->db->query($sql);
756  if ($result) {
757  $obj = $this->db->fetch_object($result);
758  if ($obj->nb == 0) {
759  // Produit non deja existant
760  $sql = "INSERT INTO ".$this->db->prefix()."product (";
761  $sql .= "datec";
762  $sql .= ", entity";
763  $sql .= ", ref";
764  $sql .= ", ref_ext";
765  $sql .= ", price_min";
766  $sql .= ", price_min_ttc";
767  $sql .= ", label";
768  $sql .= ", fk_user_author";
769  $sql .= ", fk_product_type";
770  $sql .= ", price";
771  $sql .= ", price_ttc";
772  $sql .= ", price_base_type";
773  $sql .= ", tobuy";
774  $sql .= ", tosell";
775  if (empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
776  $sql .= ", accountancy_code_buy";
777  $sql .= ", accountancy_code_buy_intra";
778  $sql .= ", accountancy_code_buy_export";
779  $sql .= ", accountancy_code_sell";
780  $sql .= ", accountancy_code_sell_intra";
781  $sql .= ", accountancy_code_sell_export";
782  }
783  $sql .= ", canvas";
784  $sql .= ", finished";
785  $sql .= ", tobatch";
786  $sql .= ", batch_mask";
787  $sql .= ", fk_unit";
788  $sql .= ", mandatory_period";
789  $sql .= ") VALUES (";
790  $sql .= "'".$this->db->idate($now)."'";
791  $sql .= ", ".((int) $conf->entity);
792  $sql .= ", '".$this->db->escape($this->ref)."'";
793  $sql .= ", ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
794  $sql .= ", ".price2num($price_min_ht);
795  $sql .= ", ".price2num($price_min_ttc);
796  $sql .= ", ".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null");
797  $sql .= ", ".((int) $user->id);
798  $sql .= ", ".((int) $this->type);
799  $sql .= ", ".price2num($price_ht, 'MT');
800  $sql .= ", ".price2num($price_ttc, 'MT');
801  $sql .= ", '".$this->db->escape($this->price_base_type)."'";
802  $sql .= ", ".((int) $this->status);
803  $sql .= ", ".((int) $this->status_buy);
804  if (empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
805  $sql .= ", '".$this->db->escape($this->accountancy_code_buy)."'";
806  $sql .= ", '".$this->db->escape($this->accountancy_code_buy_intra)."'";
807  $sql .= ", '".$this->db->escape($this->accountancy_code_buy_export)."'";
808  $sql .= ", '".$this->db->escape($this->accountancy_code_sell)."'";
809  $sql .= ", '".$this->db->escape($this->accountancy_code_sell_intra)."'";
810  $sql .= ", '".$this->db->escape($this->accountancy_code_sell_export)."'";
811  }
812  $sql .= ", '".$this->db->escape($this->canvas)."'";
813  $sql .= ", ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? 'NULL' : (int) $this->finished);
814  $sql .= ", ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : ((int) $this->status_batch));
815  $sql .= ", '".$this->db->escape($this->batch_mask)."'";
816  $sql .= ", ".($this->fk_unit > 0 ? ((int) $this->fk_unit) : 'NULL');
817  $sql .= ", '".$this->db->escape($this->mandatory_period)."'";
818  $sql .= ")";
819 
820  dol_syslog(get_class($this)."::Create", LOG_DEBUG);
821  $result = $this->db->query($sql);
822  if ($result) {
823  $id = $this->db->last_insert_id($this->db->prefix()."product");
824 
825  if ($id > 0) {
826  $this->id = $id;
827  $this->price = $price_ht;
828  $this->price_ttc = $price_ttc;
829  $this->price_min = $price_min_ht;
830  $this->price_min_ttc = $price_min_ttc;
831 
832  $result = $this->_log_price($user);
833  if ($result > 0) {
834  if ($this->update($id, $user, true, 'add') <= 0) {
835  $error++;
836  }
837  } else {
838  $error++;
839  $this->error = $this->db->lasterror();
840  }
841 
842  // update accountancy for this entity
843  if (!$error && !empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
844  $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " .((int) $this->id) . " AND entity = " . ((int) $conf->entity));
845 
846  $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
847  $sql .= " fk_product";
848  $sql .= ", entity";
849  $sql .= ", accountancy_code_buy";
850  $sql .= ", accountancy_code_buy_intra";
851  $sql .= ", accountancy_code_buy_export";
852  $sql .= ", accountancy_code_sell";
853  $sql .= ", accountancy_code_sell_intra";
854  $sql .= ", accountancy_code_sell_export";
855  $sql .= ") VALUES (";
856  $sql .= $this->id;
857  $sql .= ", " . $conf->entity;
858  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
859  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
860  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
861  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
862  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
863  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
864  $sql .= ")";
865  $result = $this->db->query($sql);
866  if (!$result) {
867  $error++;
868  $this->error = 'ErrorFailedToInsertAccountancyForEntity';
869  }
870  }
871  } else {
872  $error++;
873  $this->error = 'ErrorFailedToGetInsertedId';
874  }
875  } else {
876  $error++;
877  $this->error = $this->db->lasterror();
878  }
879  } else {
880  // Product already exists with this ref
881  $langs->load("products");
882  $error++;
883  $this->error = "ErrorProductAlreadyExists";
884  dol_syslog(get_class($this)."::Create fails, ref ".$this->ref." already exists");
885  }
886  } else {
887  $error++;
888  $this->error = $this->db->lasterror();
889  }
890 
891  if (!$error && !$notrigger) {
892  // Call trigger
893  $result = $this->call_trigger('PRODUCT_CREATE', $user);
894  if ($result < 0) {
895  $error++;
896  }
897  // End call triggers
898  }
899 
900  if (!$error) {
901  $this->db->commit();
902  return $this->id;
903  } else {
904  $this->db->rollback();
905  return -$error;
906  }
907  } else {
908  $this->db->rollback();
909  dol_syslog(get_class($this)."::Create fails verify ".join(',', $this->errors), LOG_WARNING);
910  return -3;
911  }
912  }
913 
914 
921  public function verify()
922  {
923  global $langs;
924 
925  $this->errors = array();
926 
927  $result = 0;
928  $this->ref = trim($this->ref);
929 
930  if (!$this->ref) {
931  $this->errors[] = 'ErrorBadRef';
932  $result = -2;
933  }
934 
935  $arrayofnonnegativevalue = array('weight'=>'Weight', 'width'=>'Width', 'height'=>'Height', 'length'=>'Length', 'surface'=>'Surface', 'volume'=>'Volume');
936  foreach ($arrayofnonnegativevalue as $key => $value) {
937  if (property_exists($this, $key) && !empty($this->$key) && ($this->$key < 0)) {
938  $langs->loadLangs(array("main", "other"));
939  $this->error = $langs->trans("FieldCannotBeNegative", $langs->transnoentitiesnoconv($value));
940  $this->errors[] = $this->error;
941  $result = -4;
942  }
943  }
944 
945  $rescode = $this->check_barcode($this->barcode, $this->barcode_type_code);
946  if ($rescode) {
947  if ($rescode == -1) {
948  $this->errors[] = 'ErrorBadBarCodeSyntax';
949  } elseif ($rescode == -2) {
950  $this->errors[] = 'ErrorBarCodeRequired';
951  } elseif ($rescode == -3) {
952  // Note: Common usage is to have barcode unique. For variants, we should have a different barcode.
953  $this->errors[] = 'ErrorBarCodeAlreadyUsed';
954  }
955 
956  $result = -3;
957  }
958 
959  return $result;
960  }
961 
962  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
973  public function check_barcode($valuetotest, $typefortest)
974  {
975  // phpcs:enable
976  global $conf;
977  if (isModEnabled('barcode') && !empty($conf->global->BARCODE_PRODUCT_ADDON_NUM)) {
978  $module = strtolower($conf->global->BARCODE_PRODUCT_ADDON_NUM);
979 
980  $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
981  foreach ($dirsociete as $dirroot) {
982  $res = dol_include_once($dirroot.$module.'.php');
983  if ($res) {
984  break;
985  }
986  }
987 
988  $mod = new $module();
989 
990  dol_syslog(get_class($this)."::check_barcode value=".$valuetotest." type=".$typefortest." module=".$module);
991  $result = $mod->verif($this->db, $valuetotest, $this, 0, $typefortest);
992  return $result;
993  } else {
994  return 0;
995  }
996  }
997 
1009  public function update($id, $user, $notrigger = false, $action = 'update', $updatetype = false)
1010  {
1011  global $langs, $conf, $hookmanager;
1012 
1013  $error = 0;
1014 
1015  // Check parameters
1016  if (!$this->label) {
1017  $this->label = 'MISSING LABEL';
1018  }
1019 
1020  // Clean parameters
1021  if (getDolGlobalInt('MAIN_SECURITY_ALLOW_UNSECURED_REF_LABELS')) {
1022  $this->ref = trim($this->ref);
1023  } else {
1024  $this->ref = dol_string_nospecial(trim($this->ref));
1025  }
1026  $this->label = trim($this->label);
1027  $this->description = trim($this->description);
1028  $this->note_private = (isset($this->note_private) ? trim($this->note_private) : null);
1029  $this->note_public = (isset($this->note_public) ? trim($this->note_public) : null);
1030  $this->net_measure = price2num($this->net_measure);
1031  $this->net_measure_units = trim($this->net_measure_units);
1032  $this->weight = price2num($this->weight);
1033  $this->weight_units = trim($this->weight_units);
1034  $this->length = price2num($this->length);
1035  $this->length_units = trim($this->length_units);
1036  $this->width = price2num($this->width);
1037  $this->width_units = trim($this->width_units);
1038  $this->height = price2num($this->height);
1039  $this->height_units = trim($this->height_units);
1040  $this->surface = price2num($this->surface);
1041  $this->surface_units = trim($this->surface_units);
1042  $this->volume = price2num($this->volume);
1043  $this->volume_units = trim($this->volume_units);
1044 
1045  // set unit not defined
1046  if (is_numeric($this->length_units)) {
1047  $this->width_units = $this->length_units; // Not used yet
1048  }
1049  if (is_numeric($this->length_units)) {
1050  $this->height_units = $this->length_units; // Not used yet
1051  }
1052 
1053  // Automated compute surface and volume if not filled
1054  if (empty($this->surface) && !empty($this->length) && !empty($this->width) && $this->length_units == $this->width_units) {
1055  $this->surface = $this->length * $this->width;
1056  $this->surface_units = measuring_units_squared($this->length_units);
1057  }
1058  if (empty($this->volume) && !empty($this->surface) && !empty($this->height) && $this->length_units == $this->height_units) {
1059  $this->volume = $this->surface * $this->height;
1060  $this->volume_units = measuring_units_cubed($this->height_units);
1061  }
1062 
1063  if (empty($this->tva_tx)) {
1064  $this->tva_tx = 0;
1065  }
1066  if (empty($this->tva_npr)) {
1067  $this->tva_npr = 0;
1068  }
1069  if (empty($this->localtax1_tx)) {
1070  $this->localtax1_tx = 0;
1071  }
1072  if (empty($this->localtax2_tx)) {
1073  $this->localtax2_tx = 0;
1074  }
1075  if (empty($this->localtax1_type)) {
1076  $this->localtax1_type = '0';
1077  }
1078  if (empty($this->localtax2_type)) {
1079  $this->localtax2_type = '0';
1080  }
1081  if (empty($this->status)) {
1082  $this->status = 0;
1083  }
1084  if (empty($this->status_buy)) {
1085  $this->status_buy = 0;
1086  }
1087 
1088  if (empty($this->country_id)) {
1089  $this->country_id = 0;
1090  }
1091 
1092  if (empty($this->state_id)) {
1093  $this->state_id = 0;
1094  }
1095 
1096  // Barcode value
1097  $this->barcode = trim($this->barcode);
1098 
1099  $this->accountancy_code_buy = trim($this->accountancy_code_buy);
1100  $this->accountancy_code_buy_intra = trim($this->accountancy_code_buy_intra);
1101  $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export);
1102  $this->accountancy_code_sell = trim($this->accountancy_code_sell);
1103  $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra);
1104  $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export);
1105 
1106 
1107  $this->db->begin();
1108 
1109  $result = 0;
1110  // Check name is required and codes are ok or unique. If error, this->errors[] is filled
1111  if ($action != 'add') {
1112  $result = $this->verify(); // We don't check when update called during a create because verify was already done
1113  } else {
1114  // we can continue
1115  $result = 0;
1116  }
1117 
1118  if ($result >= 0) {
1119  // $this->oldcopy should have been set by the caller of update (here properties were already modified)
1120  if (empty($this->oldcopy)) {
1121  $this->oldcopy = dol_clone($this);
1122  }
1123 
1124  // Test if batch management is activated on existing product
1125  // If yes, we create missing entries into product_batch
1126  if ($this->hasbatch() && !$this->oldcopy->hasbatch()) {
1127  //$valueforundefinedlot = 'Undefined'; // In previous version, 39 and lower
1128  $valueforundefinedlot = '000000';
1129  if (!empty($conf->global->STOCK_DEFAULT_BATCH)) {
1130  $valueforundefinedlot = $conf->global->STOCK_DEFAULT_BATCH;
1131  }
1132 
1133  dol_syslog("Flag batch of product id=".$this->id." is set to ON, so we will create missing records into product_batch");
1134 
1135  $this->load_stock();
1136  foreach ($this->stock_warehouse as $idW => $ObjW) { // For each warehouse where we have stocks defined for this product (for each lines in product_stock)
1137  $qty_batch = 0;
1138  foreach ($ObjW->detail_batch as $detail) { // Each lines of detail in product_batch of the current $ObjW = product_stock
1139  if ($detail->batch == $valueforundefinedlot || $detail->batch == 'Undefined') {
1140  // We discard this line, we will create it later
1141  $sqlclean = "DELETE FROM ".$this->db->prefix()."product_batch WHERE batch in('Undefined', '".$this->db->escape($valueforundefinedlot)."') AND fk_product_stock = ".((int) $ObjW->id);
1142  $result = $this->db->query($sqlclean);
1143  if (!$result) {
1144  dol_print_error($this->db);
1145  exit;
1146  }
1147  continue;
1148  }
1149 
1150  $qty_batch += $detail->qty;
1151  }
1152  // Quantities in batch details are not same as stock quantity,
1153  // so we add a default batch record to complete and get same qty in parent and child table
1154  if ($ObjW->real <> $qty_batch) {
1155  $ObjBatch = new Productbatch($this->db);
1156  $ObjBatch->batch = $valueforundefinedlot;
1157  $ObjBatch->qty = ($ObjW->real - $qty_batch);
1158  $ObjBatch->fk_product_stock = $ObjW->id;
1159 
1160  if ($ObjBatch->create($user, 1) < 0) {
1161  $error++;
1162  $this->errors = $ObjBatch->errors;
1163  }
1164  }
1165  }
1166  }
1167 
1168  // For automatic creation
1169  if ($this->barcode == -1) {
1170  $this->barcode = $this->get_barcode($this, $this->barcode_type_code);
1171  }
1172 
1173  $sql = "UPDATE ".$this->db->prefix()."product";
1174  $sql .= " SET label = '".$this->db->escape($this->label)."'";
1175 
1176  if ($updatetype && ($this->isProduct() || $this->isService())) {
1177  $sql .= ", fk_product_type = ".((int) $this->type);
1178  }
1179 
1180  $sql .= ", ref = '".$this->db->escape($this->ref)."'";
1181  $sql .= ", ref_ext = ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null");
1182  $sql .= ", default_vat_code = ".($this->default_vat_code ? "'".$this->db->escape($this->default_vat_code)."'" : "null");
1183  $sql .= ", tva_tx = ".((float) $this->tva_tx);
1184  $sql .= ", recuperableonly = ".((int) $this->tva_npr);
1185  $sql .= ", localtax1_tx = ".((float) $this->localtax1_tx);
1186  $sql .= ", localtax2_tx = ".((float) $this->localtax2_tx);
1187  $sql .= ", localtax1_type = ".($this->localtax1_type != '' ? "'".$this->db->escape($this->localtax1_type)."'" : "'0'");
1188  $sql .= ", localtax2_type = ".($this->localtax2_type != '' ? "'".$this->db->escape($this->localtax2_type)."'" : "'0'");
1189 
1190  $sql .= ", barcode = ".(empty($this->barcode) ? "null" : "'".$this->db->escape($this->barcode)."'");
1191  $sql .= ", fk_barcode_type = ".(empty($this->barcode_type) ? "null" : $this->db->escape($this->barcode_type));
1192 
1193  $sql .= ", tosell = ".(int) $this->status;
1194  $sql .= ", tobuy = ".(int) $this->status_buy;
1195  $sql .= ", tobatch = ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : (int) $this->status_batch);
1196  $sql .= ", batch_mask = '".$this->db->escape($this->batch_mask)."'";
1197 
1198  $sql .= ", finished = ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? "null" : (int) $this->finished);
1199  $sql .= ", fk_default_bom = ".((!isset($this->fk_default_bom) || $this->fk_default_bom < 0 || $this->fk_default_bom == '') ? "null" : (int) $this->fk_default_bom);
1200  $sql .= ", net_measure = ".($this->net_measure != '' ? "'".$this->db->escape($this->net_measure)."'" : 'null');
1201  $sql .= ", net_measure_units = ".($this->net_measure_units != '' ? "'".$this->db->escape($this->net_measure_units)."'" : 'null');
1202  $sql .= ", weight = ".($this->weight != '' ? "'".$this->db->escape($this->weight)."'" : 'null');
1203  $sql .= ", weight_units = ".($this->weight_units != '' ? "'".$this->db->escape($this->weight_units)."'" : 'null');
1204  $sql .= ", length = ".($this->length != '' ? "'".$this->db->escape($this->length)."'" : 'null');
1205  $sql .= ", length_units = ".($this->length_units != '' ? "'".$this->db->escape($this->length_units)."'" : 'null');
1206  $sql .= ", width= ".($this->width != '' ? "'".$this->db->escape($this->width)."'" : 'null');
1207  $sql .= ", width_units = ".($this->width_units != '' ? "'".$this->db->escape($this->width_units)."'" : 'null');
1208  $sql .= ", height = ".($this->height != '' ? "'".$this->db->escape($this->height)."'" : 'null');
1209  $sql .= ", height_units = ".($this->height_units != '' ? "'".$this->db->escape($this->height_units)."'" : 'null');
1210  $sql .= ", surface = ".($this->surface != '' ? "'".$this->db->escape($this->surface)."'" : 'null');
1211  $sql .= ", surface_units = ".($this->surface_units != '' ? "'".$this->db->escape($this->surface_units)."'" : 'null');
1212  $sql .= ", volume = ".($this->volume != '' ? "'".$this->db->escape($this->volume)."'" : 'null');
1213  $sql .= ", volume_units = ".($this->volume_units != '' ? "'".$this->db->escape($this->volume_units)."'" : 'null');
1214  $sql .= ", fk_default_warehouse = ".($this->fk_default_warehouse > 0 ? $this->db->escape($this->fk_default_warehouse) : 'null');
1215  $sql .= ", fk_default_workstation = ".($this->fk_default_workstation > 0 ? $this->db->escape($this->fk_default_workstation) : 'null');
1216  $sql .= ", seuil_stock_alerte = ".((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (float) $this->seuil_stock_alerte : 'null');
1217  $sql .= ", description = '".$this->db->escape($this->description)."'";
1218  $sql .= ", url = ".($this->url ? "'".$this->db->escape($this->url)."'" : 'null');
1219  $sql .= ", customcode = '".$this->db->escape($this->customcode)."'";
1220  $sql .= ", fk_country = ".($this->country_id > 0 ? (int) $this->country_id : 'null');
1221  $sql .= ", fk_state = ".($this->state_id > 0 ? (int) $this->state_id : 'null');
1222  $sql .= ", lifetime = ".($this->lifetime > 0 ? (int) $this->lifetime : 'null');
1223  $sql .= ", qc_frequency = ".($this->qc_frequency > 0 ? (int) $this->qc_frequency : 'null');
1224  $sql .= ", note = ".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : 'null');
1225  $sql .= ", note_public = ".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : 'null');
1226  $sql .= ", duration = '".$this->db->escape($this->duration_value.$this->duration_unit)."'";
1227  if (empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
1228  $sql .= ", accountancy_code_buy = '" . $this->db->escape($this->accountancy_code_buy) . "'";
1229  $sql .= ", accountancy_code_buy_intra = '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1230  $sql .= ", accountancy_code_buy_export = '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1231  $sql .= ", accountancy_code_sell= '" . $this->db->escape($this->accountancy_code_sell) . "'";
1232  $sql .= ", accountancy_code_sell_intra= '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1233  $sql .= ", accountancy_code_sell_export= '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1234  }
1235  $sql .= ", desiredstock = ".((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (float) $this->desiredstock : "null");
1236  $sql .= ", cost_price = ".($this->cost_price != '' ? $this->db->escape($this->cost_price) : 'null');
1237  $sql .= ", fk_unit= ".(!$this->fk_unit ? 'NULL' : (int) $this->fk_unit);
1238  $sql .= ", price_autogen = ".(!$this->price_autogen ? 0 : 1);
1239  $sql .= ", fk_price_expression = ".($this->fk_price_expression != 0 ? (int) $this->fk_price_expression : 'NULL');
1240  $sql .= ", fk_user_modif = ".($user->id > 0 ? $user->id : 'NULL');
1241  $sql .= ", mandatory_period = ".($this->mandatory_period );
1242  // stock field is not here because it is a denormalized value from product_stock.
1243  $sql .= " WHERE rowid = ".((int) $id);
1244 
1245  dol_syslog(get_class($this)."::update", LOG_DEBUG);
1246 
1247  $resql = $this->db->query($sql);
1248  if ($resql) {
1249  $this->id = $id;
1250 
1251  // Multilangs
1252  if (getDolGlobalInt('MAIN_MULTILANGS')) {
1253  if ($this->setMultiLangs($user) < 0) {
1254  $this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1255  return -2;
1256  }
1257  }
1258 
1259  $action = 'update';
1260 
1261  // update accountancy for this entity
1262  if (!$error && !empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
1263  $this->db->query("DELETE FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = " . ((int) $this->id) . " AND entity = " . ((int) $conf->entity));
1264 
1265  $sql = "INSERT INTO " . $this->db->prefix() . "product_perentity (";
1266  $sql .= " fk_product";
1267  $sql .= ", entity";
1268  $sql .= ", accountancy_code_buy";
1269  $sql .= ", accountancy_code_buy_intra";
1270  $sql .= ", accountancy_code_buy_export";
1271  $sql .= ", accountancy_code_sell";
1272  $sql .= ", accountancy_code_sell_intra";
1273  $sql .= ", accountancy_code_sell_export";
1274  $sql .= ") VALUES (";
1275  $sql .= $this->id;
1276  $sql .= ", " . $conf->entity;
1277  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy) . "'";
1278  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_intra) . "'";
1279  $sql .= ", '" . $this->db->escape($this->accountancy_code_buy_export) . "'";
1280  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell) . "'";
1281  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_intra) . "'";
1282  $sql .= ", '" . $this->db->escape($this->accountancy_code_sell_export) . "'";
1283  $sql .= ")";
1284  $result = $this->db->query($sql);
1285  if (!$result) {
1286  $error++;
1287  $this->error = 'ErrorFailedToUpdateAccountancyForEntity';
1288  }
1289  }
1290 
1291  // Actions on extra fields
1292  if (!$error) {
1293  $result = $this->insertExtraFields();
1294  if ($result < 0) {
1295  $error++;
1296  }
1297  }
1298 
1299  if (!$error && !$notrigger) {
1300  // Call trigger
1301  $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1302  if ($result < 0) {
1303  $error++;
1304  }
1305  // End call triggers
1306  }
1307 
1308  if (!$error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref)) {
1309  // We remove directory
1310  if ($conf->product->dir_output) {
1311  $olddir = $conf->product->dir_output."/".dol_sanitizeFileName($this->oldcopy->ref);
1312  $newdir = $conf->product->dir_output."/".dol_sanitizeFileName($this->ref);
1313  if (file_exists($olddir)) {
1314  //include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php';
1315  //$res = dol_move($olddir, $newdir);
1316  // do not use dol_move with directory
1317  $res = @rename($olddir, $newdir);
1318  if (!$res) {
1319  $langs->load("errors");
1320  $this->error = $langs->trans('ErrorFailToRenameDir', $olddir, $newdir);
1321  $error++;
1322  }
1323  }
1324  }
1325  }
1326 
1327  if (!$error) {
1328  if (isModEnabled('variants')) {
1329  include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1330 
1331  $comb = new ProductCombination($this->db);
1332 
1333  foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) {
1334  $currcomb->updateProperties($this, $user);
1335  }
1336  }
1337 
1338  $this->db->commit();
1339  return 1;
1340  } else {
1341  $this->db->rollback();
1342  return -$error;
1343  }
1344  } else {
1345  if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
1346  $langs->load("errors");
1347  if (empty($conf->barcode->enabled) || empty($this->barcode)) {
1348  $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductAlreadyExists", $this->ref);
1349  } else {
1350  $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductBarCodeAlreadyExists", $this->barcode);
1351  }
1352  $this->errors[] = $this->error;
1353  $this->db->rollback();
1354  return -1;
1355  } else {
1356  $this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql;
1357  $this->errors[] = $this->error;
1358  $this->db->rollback();
1359  return -2;
1360  }
1361  }
1362  } else {
1363  $this->db->rollback();
1364  dol_syslog(get_class($this)."::Update fails verify ".join(',', $this->errors), LOG_WARNING);
1365  return -3;
1366  }
1367  }
1368 
1376  public function delete(User $user, $notrigger = 0)
1377  {
1378  global $conf, $langs;
1379  include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
1380 
1381  $error = 0;
1382 
1383  // Check parameters
1384  if (empty($this->id)) {
1385  $this->error = "Object must be fetched before calling delete";
1386  return -1;
1387  }
1388  if (($this->type == Product::TYPE_PRODUCT && empty($user->rights->produit->supprimer)) || ($this->type == Product::TYPE_SERVICE && empty($user->rights->service->supprimer))) {
1389  $this->error = "ErrorForbidden";
1390  return 0;
1391  }
1392 
1393  $objectisused = $this->isObjectUsed($this->id);
1394  if (empty($objectisused)) {
1395  $this->db->begin();
1396 
1397  if (!$error && empty($notrigger)) {
1398  // Call trigger
1399  $result = $this->call_trigger('PRODUCT_DELETE', $user);
1400  if ($result < 0) {
1401  $error++;
1402  }
1403  // End call triggers
1404  }
1405 
1406  // Delete from product_batch on product delete
1407  if (!$error) {
1408  $sql = "DELETE FROM ".$this->db->prefix().'product_batch';
1409  $sql .= " WHERE fk_product_stock IN (";
1410  $sql .= "SELECT rowid FROM ".$this->db->prefix().'product_stock';
1411  $sql .= " WHERE fk_product = ".((int) $this->id).")";
1412 
1413  $result = $this->db->query($sql);
1414  if (!$result) {
1415  $error++;
1416  $this->errors[] = $this->db->lasterror();
1417  }
1418  }
1419 
1420  // Delete all child tables
1421  if (!$error) {
1422  $elements = array('product_fournisseur_price', 'product_price', 'product_lang', 'categorie_product', 'product_stock', 'product_customer_price', 'product_lot'); // product_batch is done before
1423  foreach ($elements as $table) {
1424  if (!$error) {
1425  $sql = "DELETE FROM ".$this->db->prefix().$table;
1426  $sql .= " WHERE fk_product = ".(int) $this->id;
1427 
1428  $result = $this->db->query($sql);
1429  if (!$result) {
1430  $error++;
1431  $this->errors[] = $this->db->lasterror();
1432  }
1433  }
1434  }
1435  }
1436 
1437  if (!$error) {
1438  include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php';
1439  include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
1440 
1441  //If it is a parent product, then we remove the association with child products
1442  $prodcomb = new ProductCombination($this->db);
1443 
1444  if ($prodcomb->deleteByFkProductParent($user, $this->id) < 0) {
1445  $error++;
1446  $this->errors[] = 'Error deleting combinations';
1447  }
1448 
1449  //We also check if it is a child product
1450  if (!$error && ($prodcomb->fetchByFkProductChild($this->id) > 0) && ($prodcomb->delete($user) < 0)) {
1451  $error++;
1452  $this->errors[] = 'Error deleting child combination';
1453  }
1454  }
1455 
1456  // Delete from product_association
1457  if (!$error) {
1458  $sql = "DELETE FROM ".$this->db->prefix()."product_association";
1459  $sql .= " WHERE fk_product_pere = ".(int) $this->id." OR fk_product_fils = ".(int) $this->id;
1460 
1461  $result = $this->db->query($sql);
1462  if (!$result) {
1463  $error++;
1464  $this->errors[] = $this->db->lasterror();
1465  }
1466  }
1467 
1468  // Remove extrafields
1469  if (!$error) {
1470  $result = $this->deleteExtraFields();
1471  if ($result < 0) {
1472  $error++;
1473  dol_syslog(get_class($this)."::delete error -4 ".$this->error, LOG_ERR);
1474  }
1475  }
1476 
1477  // Delete product
1478  if (!$error) {
1479  $sqlz = "DELETE FROM ".$this->db->prefix()."product";
1480  $sqlz .= " WHERE rowid = ".(int) $this->id;
1481 
1482  $resultz = $this->db->query($sqlz);
1483  if (!$resultz) {
1484  $error++;
1485  $this->errors[] = $this->db->lasterror();
1486  }
1487  }
1488 
1489  // Delete record into ECM index and physically
1490  if (!$error) {
1491  $res = $this->deleteEcmFiles(0); // Deleting files physically is done later with the dol_delete_dir_recursive
1492  if (!$res) {
1493  $error++;
1494  }
1495  }
1496 
1497  if (!$error) {
1498  // We remove directory
1499  $ref = dol_sanitizeFileName($this->ref);
1500  if ($conf->product->dir_output) {
1501  $dir = $conf->product->dir_output."/".$ref;
1502  if (file_exists($dir)) {
1503  $res = @dol_delete_dir_recursive($dir);
1504  if (!$res) {
1505  $this->errors[] = 'ErrorFailToDeleteDir';
1506  $error++;
1507  }
1508  }
1509  }
1510  }
1511 
1512  if (!$error) {
1513  $this->db->commit();
1514  return 1;
1515  } else {
1516  foreach ($this->errors as $errmsg) {
1517  dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
1518  $this->error .= ($this->error ? ', '.$errmsg : $errmsg);
1519  }
1520  $this->db->rollback();
1521  return -$error;
1522  }
1523  } else {
1524  $this->error = "ErrorRecordIsUsedCantDelete";
1525  return 0;
1526  }
1527  }
1528 
1535  public function setMultiLangs($user)
1536  {
1537  global $conf, $langs;
1538 
1539  $langs_available = $langs->get_available_languages(DOL_DOCUMENT_ROOT, 0, 2);
1540  $current_lang = $langs->getDefaultLang();
1541 
1542  foreach ($langs_available as $key => $value) {
1543  if ($key == $current_lang) {
1544  $sql = "SELECT rowid";
1545  $sql .= " FROM ".$this->db->prefix()."product_lang";
1546  $sql .= " WHERE fk_product = ".((int) $this->id);
1547  $sql .= " AND lang = '".$this->db->escape($key)."'";
1548 
1549  $result = $this->db->query($sql);
1550 
1551  if ($this->db->num_rows($result)) { // if there is already a description line for this language
1552  $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
1553  $sql2 .= " SET ";
1554  $sql2 .= " label='".$this->db->escape($this->label)."',";
1555  $sql2 .= " description='".$this->db->escape($this->description)."'";
1556  if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1557  $sql2 .= ", note='".$this->db->escape($this->other)."'";
1558  }
1559  $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
1560  } else {
1561  $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
1562  if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1563  $sql2 .= ", note";
1564  }
1565  $sql2 .= ")";
1566  $sql2 .= " VALUES(".$this->id.",'".$this->db->escape($key)."','".$this->db->escape($this->label)."',";
1567  $sql2 .= " '".$this->db->escape($this->description)."'";
1568  if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1569  $sql2 .= ", '".$this->db->escape($this->other)."'";
1570  }
1571  $sql2 .= ")";
1572  }
1573  dol_syslog(get_class($this).'::setMultiLangs key = current_lang = '.$key);
1574  if (!$this->db->query($sql2)) {
1575  $this->error = $this->db->lasterror();
1576  return -1;
1577  }
1578  } elseif (isset($this->multilangs[$key])) {
1579  if (empty($this->multilangs["$key"]["label"])) {
1580  $this->errors[] = $key . ' : ' . $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Label"));
1581  return -1;
1582  }
1583 
1584  $sql = "SELECT rowid";
1585  $sql .= " FROM ".$this->db->prefix()."product_lang";
1586  $sql .= " WHERE fk_product = ".((int) $this->id);
1587  $sql .= " AND lang = '".$this->db->escape($key)."'";
1588 
1589  $result = $this->db->query($sql);
1590 
1591  if ($this->db->num_rows($result)) { // if there is already a description line for this language
1592  $sql2 = "UPDATE ".$this->db->prefix()."product_lang";
1593  $sql2 .= " SET ";
1594  $sql2 .= " label = '".$this->db->escape($this->multilangs["$key"]["label"])."',";
1595  $sql2 .= " description = '".$this->db->escape($this->multilangs["$key"]["description"])."'";
1596  if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1597  $sql2 .= ", note = '".$this->db->escape($this->multilangs["$key"]["other"])."'";
1598  }
1599  $sql2 .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($key)."'";
1600  } else {
1601  $sql2 = "INSERT INTO ".$this->db->prefix()."product_lang (fk_product, lang, label, description";
1602  if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1603  $sql2 .= ", note";
1604  }
1605  $sql2 .= ")";
1606  $sql2 .= " VALUES(".$this->id.",'".$this->db->escape($key)."','".$this->db->escape($this->multilangs["$key"]["label"])."',";
1607  $sql2 .= " '".$this->db->escape($this->multilangs["$key"]["description"])."'";
1608  if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) {
1609  $sql2 .= ", '".$this->db->escape($this->multilangs["$key"]["other"])."'";
1610  }
1611  $sql2 .= ")";
1612  }
1613 
1614  // We do not save if main fields are empty
1615  if ($this->multilangs["$key"]["label"] || $this->multilangs["$key"]["description"]) {
1616  if (!$this->db->query($sql2)) {
1617  $this->error = $this->db->lasterror();
1618  return -1;
1619  }
1620  }
1621  } else {
1622  // language is not current language and we didn't provide a multilang description for this language
1623  }
1624  }
1625 
1626  // Call trigger
1627  $result = $this->call_trigger('PRODUCT_SET_MULTILANGS', $user);
1628  if ($result < 0) {
1629  $this->error = $this->db->lasterror();
1630  return -1;
1631  }
1632  // End call triggers
1633 
1634  return 1;
1635  }
1636 
1645  public function delMultiLangs($langtodelete, $user)
1646  {
1647  $sql = "DELETE FROM ".$this->db->prefix()."product_lang";
1648  $sql .= " WHERE fk_product = ".((int) $this->id)." AND lang = '".$this->db->escape($langtodelete)."'";
1649 
1650  dol_syslog(get_class($this).'::delMultiLangs', LOG_DEBUG);
1651  $result = $this->db->query($sql);
1652  if ($result) {
1653  // Call trigger
1654  $result = $this->call_trigger('PRODUCT_DEL_MULTILANGS', $user);
1655  if ($result < 0) {
1656  $this->error = $this->db->lasterror();
1657  dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
1658  return -1;
1659  }
1660  // End call triggers
1661  return 1;
1662  } else {
1663  $this->error = $this->db->lasterror();
1664  dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR);
1665  return -1;
1666  }
1667  }
1668 
1677  public function setAccountancyCode($type, $value)
1678  {
1679  global $user, $langs, $conf;
1680 
1681  $error = 0;
1682 
1683  $this->db->begin();
1684 
1685  if ($type == 'buy') {
1686  $field = 'accountancy_code_buy';
1687  } elseif ($type == 'buy_intra') {
1688  $field = 'accountancy_code_buy_intra';
1689  } elseif ($type == 'buy_export') {
1690  $field = 'accountancy_code_buy_export';
1691  } elseif ($type == 'sell') {
1692  $field = 'accountancy_code_sell';
1693  } elseif ($type == 'sell_intra') {
1694  $field = 'accountancy_code_sell_intra';
1695  } elseif ($type == 'sell_export') {
1696  $field = 'accountancy_code_sell_export';
1697  } else {
1698  return -1;
1699  }
1700 
1701  $sql = "UPDATE ".$this->db->prefix().$this->table_element." SET ";
1702  $sql .= "$field = '".$this->db->escape($value)."'";
1703  $sql .= " WHERE rowid = ".((int) $this->id);
1704 
1705  dol_syslog(__METHOD__, LOG_DEBUG);
1706  $resql = $this->db->query($sql);
1707 
1708  if ($resql) {
1709  // Call trigger
1710  $result = $this->call_trigger('PRODUCT_MODIFY', $user);
1711  if ($result < 0) {
1712  $error++;
1713  }
1714  // End call triggers
1715 
1716  if ($error) {
1717  $this->db->rollback();
1718  return -1;
1719  }
1720 
1721  $this->$field = $value;
1722 
1723  $this->db->commit();
1724  return 1;
1725  } else {
1726  $this->error = $this->db->lasterror();
1727  $this->db->rollback();
1728  return -1;
1729  }
1730  }
1731 
1737  public function getMultiLangs()
1738  {
1739  global $langs;
1740 
1741  $current_lang = $langs->getDefaultLang();
1742 
1743  $sql = "SELECT lang, label, description, note as other";
1744  $sql .= " FROM ".$this->db->prefix()."product_lang";
1745  $sql .= " WHERE fk_product = ".((int) $this->id);
1746 
1747  $result = $this->db->query($sql);
1748  if ($result) {
1749  while ($obj = $this->db->fetch_object($result)) {
1750  //print 'lang='.$obj->lang.' current='.$current_lang.'<br>';
1751  if ($obj->lang == $current_lang) { // si on a les traduct. dans la langue courante on les charge en infos principales.
1752  $this->label = $obj->label;
1753  $this->description = $obj->description;
1754  $this->other = $obj->other;
1755  }
1756  $this->multilangs["$obj->lang"]["label"] = $obj->label;
1757  $this->multilangs["$obj->lang"]["description"] = $obj->description;
1758  $this->multilangs["$obj->lang"]["other"] = $obj->other;
1759  }
1760  return 1;
1761  } else {
1762  $this->error = "Error: ".$this->db->lasterror()." - ".$sql;
1763  return -1;
1764  }
1765  }
1766 
1773  private function getArrayForPriceCompare($level = 0)
1774  {
1775 
1776  $testExit = array('multiprices','multiprices_ttc','multiprices_base_type','multiprices_min','multiprices_min_ttc','multiprices_tva_tx','multiprices_recuperableonly');
1777 
1778  foreach ($testExit as $field) {
1779  if (!isset($this->$field)) {
1780  return array();
1781  }
1782  $tmparray = $this->$field;
1783  if (!isset($tmparray[$level])) {
1784  return array();
1785  }
1786  }
1787 
1788  $lastPrice = array(
1789  'level' => $level ? $level : 1,
1790  'multiprices' => doubleval($this->multiprices[$level]),
1791  'multiprices_ttc' => doubleval($this->multiprices_ttc[$level]),
1792  'multiprices_base_type' => $this->multiprices_base_type[$level],
1793  'multiprices_min' => doubleval($this->multiprices_min[$level]),
1794  'multiprices_min_ttc' => doubleval($this->multiprices_min_ttc[$level]),
1795  'multiprices_tva_tx' => doubleval($this->multiprices_tva_tx[$level]),
1796  'multiprices_recuperableonly' => doubleval($this->multiprices_recuperableonly[$level]),
1797  );
1798 
1799  return $lastPrice;
1800  }
1801 
1802 
1803  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1811  private function _log_price($user, $level = 0)
1812  {
1813  // phpcs:enable
1814  global $conf;
1815 
1816  $now = dol_now();
1817 
1818  // Clean parameters
1819  if (empty($this->price_by_qty)) {
1820  $this->price_by_qty = 0;
1821  }
1822 
1823  // Add new price
1824  $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,";
1825  $sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, price_min,price_min_ttc,price_by_qty,entity,fk_price_expression) ";
1826  $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).",";
1827  $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');
1828  $sql .= ")";
1829 
1830  dol_syslog(get_class($this)."::_log_price", LOG_DEBUG);
1831  $resql = $this->db->query($sql);
1832  if (!$resql) {
1833  $this->error = $this->db->lasterror();
1834  dol_print_error($this->db);
1835  return -1;
1836  } else {
1837  return 1;
1838  }
1839  }
1840 
1841 
1842  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
1850  public function log_price_delete($user, $rowid)
1851  {
1852  // phpcs:enable
1853  $sql = "DELETE FROM ".$this->db->prefix()."product_price_by_qty";
1854  $sql .= " WHERE fk_product_price = ".((int) $rowid);
1855  $resql = $this->db->query($sql);
1856 
1857  $sql = "DELETE FROM ".$this->db->prefix()."product_price";
1858  $sql .= " WHERE rowid=".((int) $rowid);
1859  $resql = $this->db->query($sql);
1860  if ($resql) {
1861  return 1;
1862  } else {
1863  $this->error = $this->db->lasterror();
1864  return -1;
1865  }
1866  }
1867 
1868 
1878  public function getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp = 0)
1879  {
1880  global $conf, $db, $hookmanager, $action;
1881 
1882  // Call hook if any
1883  if (is_object($hookmanager)) {
1884  $parameters = array('thirdparty_seller'=>$thirdparty_seller, 'thirdparty_buyer' => $thirdparty_buyer, 'pqp' => $pqp);
1885  // Note that $action and $object may have been modified by some hooks
1886  $reshook = $hookmanager->executeHooks('getSellPrice', $parameters, $this, $action);
1887  if ($reshook > 0) {
1888  return $hookmanager->resArray;
1889  }
1890  }
1891 
1892  // Update if prices fields are defined
1893  $tva_tx = get_default_tva($thirdparty_seller, $thirdparty_buyer, $this->id);
1894  $tva_npr = get_default_npr($thirdparty_seller, $thirdparty_buyer, $this->id);
1895  if (empty($tva_tx)) {
1896  $tva_npr = 0;
1897  }
1898 
1899  $pu_ht = $this->price;
1900  $pu_ttc = $this->price_ttc;
1901  $price_min = $this->price_min;
1902  $price_base_type = $this->price_base_type;
1903 
1904  // If price per segment
1905  if (!empty($conf->global->PRODUIT_MULTIPRICES) && !empty($thirdparty_buyer->price_level)) {
1906  $pu_ht = $this->multiprices[$thirdparty_buyer->price_level];
1907  $pu_ttc = $this->multiprices_ttc[$thirdparty_buyer->price_level];
1908  $price_min = $this->multiprices_min[$thirdparty_buyer->price_level];
1909  $price_base_type = $this->multiprices_base_type[$thirdparty_buyer->price_level];
1910  if (!empty($conf->global->PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL)) { // using this option is a bug. kept for backward compatibility
1911  if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) {
1912  $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level];
1913  }
1914  if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) {
1915  $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level];
1916  }
1917  if (empty($tva_tx)) {
1918  $tva_npr = 0;
1919  }
1920  }
1921  } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES)) {
1922  // If price per customer
1923  require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
1924 
1925  $prodcustprice = new Productcustomerprice($this->db);
1926 
1927  $filter = array('t.fk_product' => $this->id, 't.fk_soc' => $thirdparty_buyer->id);
1928 
1929  $result = $prodcustprice->fetchAll('', '', 0, 0, $filter);
1930  if ($result) {
1931  if (count($prodcustprice->lines) > 0) {
1932  $pu_ht = price($prodcustprice->lines[0]->price);
1933  $price_min = price($prodcustprice->lines[0]->price_min);
1934  $pu_ttc = price($prodcustprice->lines[0]->price_ttc);
1935  $price_base_type = $prodcustprice->lines[0]->price_base_type;
1936  $tva_tx = $prodcustprice->lines[0]->tva_tx;
1937  if ($prodcustprice->lines[0]->default_vat_code && !preg_match('/\(.*\)/', $tva_tx)) {
1938  $tva_tx .= ' ('.$prodcustprice->lines[0]->default_vat_code.')';
1939  }
1940  $tva_npr = $prodcustprice->lines[0]->recuperableonly;
1941  if (empty($tva_tx)) {
1942  $tva_npr = 0;
1943  }
1944  }
1945  }
1946  } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY)) {
1947  // If price per quantity
1948  if ($this->prices_by_qty[0]) {
1949  // yes, this product has some prices per quantity
1950  // Search price into product_price_by_qty from $this->id
1951  foreach ($this->prices_by_qty_list[0] as $priceforthequantityarray) {
1952  if ($priceforthequantityarray['rowid'] != $pqp) {
1953  continue;
1954  }
1955  // We found the price
1956  if ($priceforthequantityarray['price_base_type'] == 'HT') {
1957  $pu_ht = $priceforthequantityarray['unitprice'];
1958  } else {
1959  $pu_ttc = $priceforthequantityarray['unitprice'];
1960  }
1961  break;
1962  }
1963  }
1964  } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES)) {
1965  // If price per quantity and customer
1966  if ($this->prices_by_qty[$thirdparty_buyer->price_level]) {
1967  // yes, this product has some prices per quantity
1968  // Search price into product_price_by_qty from $this->id
1969  foreach ($this->prices_by_qty_list[$thirdparty_buyer->price_level] as $priceforthequantityarray) {
1970  if ($priceforthequantityarray['rowid'] != $pqp) {
1971  continue;
1972  }
1973  // We found the price
1974  if ($priceforthequantityarray['price_base_type'] == 'HT') {
1975  $pu_ht = $priceforthequantityarray['unitprice'];
1976  } else {
1977  $pu_ttc = $priceforthequantityarray['unitprice'];
1978  }
1979  break;
1980  }
1981  }
1982  }
1983 
1984  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);
1985  }
1986 
1987  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2001  public function get_buyprice($prodfournprice, $qty, $product_id = 0, $fourn_ref = '', $fk_soc = 0)
2002  {
2003  // phpcs:enable
2004  global $conf;
2005  $result = 0;
2006 
2007  // 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)
2008  $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2009  $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,";
2010  $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2011  $sql .= " pfp.packaging";
2012  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2013  $sql .= " WHERE pfp.rowid = ".((int) $prodfournprice);
2014  if ($qty > 0) {
2015  $sql .= " AND pfp.quantity <= ".((float) $qty);
2016  }
2017  $sql .= " ORDER BY pfp.quantity DESC";
2018 
2019  dol_syslog(get_class($this)."::get_buyprice first search by prodfournprice/qty", LOG_DEBUG);
2020  $resql = $this->db->query($sql);
2021  if ($resql) {
2022  $obj = $this->db->fetch_object($resql);
2023  if ($obj && $obj->quantity > 0) { // If we found a supplier prices from the id of supplier price
2024  if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2025  $prod_supplier = new ProductFournisseur($this->db);
2026  $prod_supplier->product_fourn_price_id = $obj->rowid;
2027  $prod_supplier->id = $obj->fk_product;
2028  $prod_supplier->fourn_qty = $obj->quantity;
2029  $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2030  $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2031 
2032  include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2033  $priceparser = new PriceParser($this->db);
2034  $price_result = $priceparser->parseProductSupplier($prod_supplier);
2035  if ($price_result >= 0) {
2036  $obj->price = $price_result;
2037  }
2038  }
2039  $this->product_fourn_price_id = $obj->rowid;
2040  $this->buyprice = $obj->price; // deprecated
2041  $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product of supplier
2042  $this->fourn_price_base_type = 'HT'; // Price base type
2043  $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2044  $this->ref_fourn = $obj->ref_supplier; // deprecated
2045  $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2046  $this->desc_supplier = $obj->desc_supplier; // desc supplier
2047  $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2048  $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2049  $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2050  $this->fourn_multicurrency_price = $obj->multicurrency_price;
2051  $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2052  $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2053  $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2054  $this->fourn_multicurrency_code = $obj->multicurrency_code;
2055  if (!empty($conf->global->PRODUCT_USE_SUPPLIER_PACKAGING)) {
2056  $this->packaging = $obj->packaging;
2057  }
2058  $result = $obj->fk_product;
2059  return $result;
2060  } else { // If not found
2061  // 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.
2062  $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,";
2063  $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,";
2064  $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,";
2065  $sql .= " pfp.packaging";
2066  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp";
2067  $sql .= " WHERE 1 = 1";
2068  if ($product_id > 0) {
2069  $sql .= " AND pfp.fk_product = ".((int) $product_id);
2070  }
2071  if ($fourn_ref != 'none') {
2072  $sql .= " AND pfp.ref_fourn = '".$this->db->escape($fourn_ref)."'";
2073  }
2074  if ($fk_soc > 0) {
2075  $sql .= " AND pfp.fk_soc = ".((int) $fk_soc);
2076  }
2077  if ($qty > 0) {
2078  $sql .= " AND pfp.quantity <= ".((float) $qty);
2079  }
2080  $sql .= " ORDER BY pfp.quantity DESC";
2081  $sql .= " LIMIT 1";
2082 
2083  dol_syslog(get_class($this)."::get_buyprice second search from qty/ref/product_id", LOG_DEBUG);
2084  $resql = $this->db->query($sql);
2085  if ($resql) {
2086  $obj = $this->db->fetch_object($resql);
2087  if ($obj && $obj->quantity > 0) { // If found
2088  if (isModEnabled('dynamicprices') && !empty($obj->fk_supplier_price_expression)) {
2089  $prod_supplier = new ProductFournisseur($this->db);
2090  $prod_supplier->product_fourn_price_id = $obj->rowid;
2091  $prod_supplier->id = $obj->fk_product;
2092  $prod_supplier->fourn_qty = $obj->quantity;
2093  $prod_supplier->fourn_tva_tx = $obj->tva_tx;
2094  $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression;
2095 
2096  include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2097  $priceparser = new PriceParser($this->db);
2098  $price_result = $priceparser->parseProductSupplier($prod_supplier);
2099  if ($result >= 0) {
2100  $obj->price = $price_result;
2101  }
2102  }
2103  $this->product_fourn_price_id = $obj->rowid;
2104  $this->buyprice = $obj->price; // deprecated
2105  $this->fourn_qty = $obj->quantity; // min quantity for price for a virtual supplier
2106  $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product for a virtual supplier
2107  $this->fourn_price_base_type = 'HT'; // Price base type for a virtual supplier
2108  $this->fourn_socid = $obj->fk_soc; // Company that offer this price
2109  $this->ref_fourn = $obj->ref_supplier; // deprecated
2110  $this->ref_supplier = $obj->ref_supplier; // Ref supplier
2111  $this->desc_supplier = $obj->desc_supplier; // desc supplier
2112  $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed
2113  $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier
2114  $this->default_vat_code_supplier = $obj->default_vat_code; // Vat code supplier
2115  $this->fourn_multicurrency_price = $obj->multicurrency_price;
2116  $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice;
2117  $this->fourn_multicurrency_tx = $obj->multicurrency_tx;
2118  $this->fourn_multicurrency_id = $obj->fk_multicurrency;
2119  $this->fourn_multicurrency_code = $obj->multicurrency_code;
2120  if (!empty($conf->global->PRODUCT_USE_SUPPLIER_PACKAGING)) {
2121  $this->packaging = $obj->packaging;
2122  }
2123  $result = $obj->fk_product;
2124  return $result;
2125  } else {
2126  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é.
2127  }
2128  } else {
2129  $this->error = $this->db->lasterror();
2130  return -3;
2131  }
2132  }
2133  } else {
2134  $this->error = $this->db->lasterror();
2135  return -2;
2136  }
2137  }
2138 
2139 
2156  public function updatePrice($newprice, $newpricebase, $user, $newvat = '', $newminprice = 0, $level = 0, $newnpr = 0, $newpbq = 0, $ignore_autogen = 0, $localtaxes_array = array(), $newdefaultvatcode = '')
2157  {
2158  global $conf, $langs;
2159 
2160  $lastPriceData = $this->getArrayForPriceCompare($level); // temporary store current price before update
2161 
2162  $id = $this->id;
2163 
2164  dol_syslog(get_class($this)."::update_price id=".$id." newprice=".$newprice." newpricebase=".$newpricebase." newminprice=".$newminprice." level=".$level." npr=".$newnpr." newdefaultvatcode=".$newdefaultvatcode);
2165 
2166  // Clean parameters
2167  if (empty($this->tva_tx)) {
2168  $this->tva_tx = 0;
2169  }
2170  if (empty($newnpr)) {
2171  $newnpr = 0;
2172  }
2173  if (empty($newminprice)) {
2174  $newminprice = 0;
2175  }
2176  if (empty($newminprice)) {
2177  $newminprice = 0;
2178  }
2179 
2180  // Check parameters
2181  if ($newvat == '') {
2182  $newvat = $this->tva_tx;
2183  }
2184 
2185  // If multiprices are enabled, then we check if the current product is subject to price autogeneration
2186  // Price will be modified ONLY when the first one is the one that is being modified
2187  if ((!empty($conf->global->PRODUIT_MULTIPRICES) || !empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES)) && !$ignore_autogen && $this->price_autogen && ($level == 1)) {
2188  return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpbq);
2189  }
2190 
2191  if (!empty($newminprice) && ($newminprice > $newprice)) {
2192  $this->error = 'ErrorPriceCantBeLowerThanMinPrice';
2193  return -1;
2194  }
2195 
2196  if ($newprice !== '' || $newprice === 0) {
2197  if ($newpricebase == 'TTC') {
2198  $price_ttc = price2num($newprice, 'MU');
2199  $price = price2num($newprice) / (1 + ($newvat / 100));
2200  $price = price2num($price, 'MU');
2201 
2202  if ($newminprice != '' || $newminprice == 0) {
2203  $price_min_ttc = price2num($newminprice, 'MU');
2204  $price_min = price2num($newminprice) / (1 + ($newvat / 100));
2205  $price_min = price2num($price_min, 'MU');
2206  } else {
2207  $price_min = 0;
2208  $price_min_ttc = 0;
2209  }
2210  } else {
2211  $price = price2num($newprice, 'MU');
2212  $price_ttc = ($newnpr != 1) ? (float) price2num($newprice) * (1 + ($newvat / 100)) : $price;
2213  $price_ttc = price2num($price_ttc, 'MU');
2214 
2215  if ($newminprice !== '' || $newminprice === 0) {
2216  $price_min = price2num($newminprice, 'MU');
2217  $price_min_ttc = price2num($newminprice) * (1 + ($newvat / 100));
2218  $price_min_ttc = price2num($price_min_ttc, 'MU');
2219  //print 'X'.$newminprice.'-'.$price_min;
2220  } else {
2221  $price_min = 0;
2222  $price_min_ttc = 0;
2223  }
2224  }
2225  //print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc;
2226 
2227  if (count($localtaxes_array) > 0) {
2228  $localtaxtype1 = $localtaxes_array['0'];
2229  $localtax1 = $localtaxes_array['1'];
2230  $localtaxtype2 = $localtaxes_array['2'];
2231  $localtax2 = $localtaxes_array['3'];
2232  } else {
2233  // if array empty, we try to use the vat code
2234  if (!empty($newdefaultvatcode)) {
2235  global $mysoc;
2236  // Get record from code
2237  $sql = "SELECT t.rowid, t.code, t.recuperableonly, t.localtax1, t.localtax2, t.localtax1_type, t.localtax2_type";
2238  $sql .= " FROM ".MAIN_DB_PREFIX."c_tva as t, ".MAIN_DB_PREFIX."c_country as c";
2239  $sql .= " WHERE t.fk_pays = c.rowid AND c.code = '".$this->db->escape($mysoc->country_code)."'";
2240  $sql .= " AND t.taux = ".((float) $newdefaultvatcode)." AND t.active = 1";
2241  $sql .= " AND t.code = '".$this->db->escape($newdefaultvatcode)."'";
2242  $resql = $this->db->query($sql);
2243  if ($resql) {
2244  $obj = $this->db->fetch_object($resql);
2245  if ($obj) {
2246  $npr = $obj->recuperableonly;
2247  $localtax1 = $obj->localtax1;
2248  $localtax2 = $obj->localtax2;
2249  $localtaxtype1 = $obj->localtax1_type;
2250  $localtaxtype2 = $obj->localtax2_type;
2251  }
2252  }
2253  } else {
2254  // old method. deprecated because we can't retrieve type
2255  $localtaxtype1 = '0';
2256  $localtax1 = get_localtax($newvat, 1);
2257  $localtaxtype2 = '0';
2258  $localtax2 = get_localtax($newvat, 2);
2259  }
2260  }
2261  if (empty($localtax1)) {
2262  $localtax1 = 0; // If = '' then = 0
2263  }
2264  if (empty($localtax2)) {
2265  $localtax2 = 0; // If = '' then = 0
2266  }
2267 
2268  $this->db->begin();
2269 
2270  // Ne pas mettre de quote sur les numeriques decimaux.
2271  // Ceci provoque des stockages avec arrondis en base au lieu des valeurs exactes.
2272  $sql = "UPDATE ".$this->db->prefix()."product SET";
2273  $sql .= " price_base_type='".$this->db->escape($newpricebase)."',";
2274  $sql .= " price=".$price.",";
2275  $sql .= " price_ttc=".$price_ttc.",";
2276  $sql .= " price_min=".$price_min.",";
2277  $sql .= " price_min_ttc=".$price_min_ttc.",";
2278  $sql .= " localtax1_tx=".($localtax1 >= 0 ? $localtax1 : 'NULL').",";
2279  $sql .= " localtax2_tx=".($localtax2 >= 0 ? $localtax2 : 'NULL').",";
2280  $sql .= " localtax1_type=".($localtaxtype1 != '' ? "'".$this->db->escape($localtaxtype1)."'" : "'0'").",";
2281  $sql .= " localtax2_type=".($localtaxtype2 != '' ? "'".$this->db->escape($localtaxtype2)."'" : "'0'").",";
2282  $sql .= " default_vat_code=".($newdefaultvatcode ? "'".$this->db->escape($newdefaultvatcode)."'" : "null").",";
2283  $sql .= " tva_tx='".price2num($newvat)."',";
2284  $sql .= " recuperableonly='".$this->db->escape($newnpr)."'";
2285  $sql .= " WHERE rowid = ".((int) $id);
2286 
2287  dol_syslog(get_class($this)."::update_price", LOG_DEBUG);
2288  $resql = $this->db->query($sql);
2289  if ($resql) {
2290  $this->multiprices[$level] = $price;
2291  $this->multiprices_ttc[$level] = $price_ttc;
2292  $this->multiprices_min[$level] = $price_min;
2293  $this->multiprices_min_ttc[$level] = $price_min_ttc;
2294  $this->multiprices_base_type[$level] = $newpricebase;
2295  $this->multiprices_default_vat_code[$level] = $newdefaultvatcode;
2296  $this->multiprices_tva_tx[$level] = $newvat;
2297  $this->multiprices_recuperableonly[$level] = $newnpr;
2298 
2299  $this->price = $price;
2300  $this->price_ttc = $price_ttc;
2301  $this->price_min = $price_min;
2302  $this->price_min_ttc = $price_min_ttc;
2303  $this->price_base_type = $newpricebase;
2304  $this->default_vat_code = $newdefaultvatcode;
2305  $this->tva_tx = $newvat;
2306  $this->tva_npr = $newnpr;
2307  //Local taxes
2308  $this->localtax1_tx = $localtax1;
2309  $this->localtax2_tx = $localtax2;
2310  $this->localtax1_type = $localtaxtype1;
2311  $this->localtax2_type = $localtaxtype2;
2312 
2313  // Price by quantity
2314  $this->price_by_qty = $newpbq;
2315 
2316  // check if price have really change before log
2317  $newPriceData = $this->getArrayForPriceCompare($level);
2318  if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || empty($conf->global->PRODUIT_MULTIPRICES)) {
2319  $this->_log_price($user, $level); // Save price for level into table product_price
2320  }
2321 
2322  $this->level = $level; // Store level of price edited for trigger
2323 
2324  // Call trigger
2325  $result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user);
2326  if ($result < 0) {
2327  $this->db->rollback();
2328  return -1;
2329  }
2330  // End call triggers
2331 
2332  $this->db->commit();
2333  } else {
2334  $this->db->rollback();
2335  $this->error = $this->db->lasterror();
2336  return -1;
2337  }
2338  }
2339 
2340  return 1;
2341  }
2342 
2350  public function setPriceExpression($expression_id)
2351  {
2352  global $user;
2353 
2354  $this->fk_price_expression = $expression_id;
2355 
2356  return $this->update($this->id, $user);
2357  }
2358 
2371  public function fetch($id = '', $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0)
2372  {
2373  include_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
2374 
2375  global $langs, $conf;
2376 
2377  dol_syslog(get_class($this)."::fetch id=".$id." ref=".$ref." ref_ext=".$ref_ext);
2378 
2379  // Check parameters
2380  if (!$id && !$ref && !$ref_ext && !$barcode) {
2381  $this->error = 'ErrorWrongParameters';
2382  dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR);
2383  return -1;
2384  }
2385 
2386  $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,";
2387  $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,";
2388  $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,";
2389  $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
2390  $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,";
2391  if (empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
2392  $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,";
2393  } else {
2394  $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,";
2395  }
2396 
2397  //For MultiCompany
2398  //PMP per entity & Stocks Sharings stock_reel includes only stocks shared with this entity
2399  $separatedEntityPMP = false; // Set to true to get the AWP from table llx_product_perentity instead of field 'pmp' into llx_product.
2400  $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.
2401  $visibleWarehousesEntities = $conf->entity;
2402  if (!empty($conf->global->MULTICOMPANY_PRODUCT_SHARING_ENABLED)) {
2403  if (!empty($conf->global->MULTICOMPANY_PMP_PER_ENTITY_ENABLED)) {
2404  $checkPMPPerEntity = $this->db->query("SELECT pmp FROM " . $this->db->prefix() . "product_perentity WHERE fk_product = ".((int) $id)." AND entity = ".(int) $conf->entity);
2405  if ($this->db->num_rows($checkPMPPerEntity)>0) {
2406  $separatedEntityPMP = true;
2407  }
2408  }
2409  global $mc;
2410  $separatedStock = true;
2411  if (isset($mc->sharings['stock']) && !empty($mc->sharings['stock'])) {
2412  $visibleWarehousesEntities .= "," . implode(",", $mc->sharings['stock']);
2413  }
2414  }
2415  if ($separatedEntityPMP) {
2416  $sql .= " ppe.pmp,";
2417  } else {
2418  $sql .= " p.pmp,";
2419  }
2420  $sql .= " p.datec, p.tms, p.import_key, p.entity, p.desiredstock, p.tobatch, p.batch_mask, p.fk_unit,";
2421  $sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf,";
2422  if ($separatedStock) {
2423  $sql .= " SUM(sp.reel) as stock";
2424  } else {
2425  $sql .= " p.stock";
2426  }
2427  $sql .= " FROM ".$this->db->prefix()."product as p";
2428  if (!empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED) || $separatedEntityPMP) {
2429  $sql .= " LEFT JOIN " . $this->db->prefix() . "product_perentity as ppe ON ppe.fk_product = p.rowid AND ppe.entity = " . ((int) $conf->entity);
2430  }
2431  if ($separatedStock) {
2432  $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)."))";
2433  }
2434 
2435  if ($id) {
2436  $sql .= " WHERE p.rowid = ".((int) $id);
2437  } else {
2438  $sql .= " WHERE p.entity IN (".getEntity($this->element).")";
2439  if ($ref) {
2440  $sql .= " AND p.ref = '".$this->db->escape($ref)."'";
2441  } elseif ($ref_ext) {
2442  $sql .= " AND p.ref_ext = '".$this->db->escape($ref_ext)."'";
2443  } elseif ($barcode) {
2444  $sql .= " AND p.barcode = '".$this->db->escape($barcode)."'";
2445  }
2446  }
2447  if ($separatedStock) {
2448  $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,";
2449  $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,";
2450  $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,";
2451  $sql .= " p.length, p.length_units, p.width, p.width_units, p.height, p.height_units,";
2452  $sql .= " p.surface, p.surface_units, p.volume, p.volume_units, p.barcode, p.fk_barcode_type, p.finished,";
2453  if (empty($conf->global->MAIN_PRODUCT_PERENTITY_SHARED)) {
2454  $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,";
2455  } else {
2456  $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,";
2457  }
2458  if ($separatedEntityPMP) {
2459  $sql .= " ppe.pmp,";
2460  } else {
2461  $sql .= " p.pmp,";
2462  }
2463  $sql .= " p.datec, p.tms, p.import_key, p.entity, p.desiredstock, p.tobatch, p.batch_mask, p.fk_unit,";
2464  $sql .= " p.fk_price_expression, p.price_autogen, p.model_pdf";
2465  if (!$separatedStock) {
2466  $sql .= ", p.stock";
2467  }
2468  }
2469 
2470  $resql = $this->db->query($sql);
2471  if ($resql) {
2472  unset($this->oldcopy);
2473 
2474  if ($this->db->num_rows($resql) > 0) {
2475  $obj = $this->db->fetch_object($resql);
2476 
2477  $this->id = $obj->rowid;
2478  $this->ref = $obj->ref;
2479  $this->ref_ext = $obj->ref_ext;
2480  $this->label = $obj->label;
2481  $this->description = $obj->description;
2482  $this->url = $obj->url;
2483  $this->note_public = $obj->note_public;
2484  $this->note_private = $obj->note_private;
2485  $this->note = $obj->note_private; // deprecated
2486 
2487  $this->type = $obj->fk_product_type;
2488  $this->status = $obj->tosell;
2489  $this->status_buy = $obj->tobuy;
2490  $this->status_batch = $obj->tobatch;
2491  $this->batch_mask = $obj->batch_mask;
2492 
2493  $this->customcode = $obj->customcode;
2494  $this->country_id = $obj->fk_country;
2495  $this->country_code = getCountry($this->country_id, 2, $this->db);
2496  $this->state_id = $obj->fk_state;
2497  $this->lifetime = $obj->lifetime;
2498  $this->qc_frequency = $obj->qc_frequency;
2499  $this->price = $obj->price;
2500  $this->price_ttc = $obj->price_ttc;
2501  $this->price_min = $obj->price_min;
2502  $this->price_min_ttc = $obj->price_min_ttc;
2503  $this->price_base_type = $obj->price_base_type;
2504  $this->cost_price = $obj->cost_price;
2505  $this->default_vat_code = $obj->default_vat_code;
2506  $this->tva_tx = $obj->tva_tx;
2508  $this->tva_npr = $obj->tva_npr;
2509  $this->recuperableonly = $obj->tva_npr; // For backward compatibility
2511  $this->localtax1_tx = $obj->localtax1_tx;
2512  $this->localtax2_tx = $obj->localtax2_tx;
2513  $this->localtax1_type = $obj->localtax1_type;
2514  $this->localtax2_type = $obj->localtax2_type;
2515 
2516  $this->finished = $obj->finished;
2517  $this->fk_default_bom = $obj->fk_default_bom;
2518 
2519  $this->duration = $obj->duration;
2520  $this->duration_value = substr($obj->duration, 0, dol_strlen($obj->duration) - 1);
2521  $this->duration_unit = substr($obj->duration, -1);
2522  $this->canvas = $obj->canvas;
2523  $this->net_measure = $obj->net_measure;
2524  $this->net_measure_units = $obj->net_measure_units;
2525  $this->weight = $obj->weight;
2526  $this->weight_units = $obj->weight_units;
2527  $this->length = $obj->length;
2528  $this->length_units = $obj->length_units;
2529  $this->width = $obj->width;
2530  $this->width_units = $obj->width_units;
2531  $this->height = $obj->height;
2532  $this->height_units = $obj->height_units;
2533 
2534  $this->surface = $obj->surface;
2535  $this->surface_units = $obj->surface_units;
2536  $this->volume = $obj->volume;
2537  $this->volume_units = $obj->volume_units;
2538  $this->barcode = $obj->barcode;
2539  $this->barcode_type = $obj->fk_barcode_type;
2540 
2541  $this->accountancy_code_buy = $obj->accountancy_code_buy;
2542  $this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra;
2543  $this->accountancy_code_buy_export = $obj->accountancy_code_buy_export;
2544  $this->accountancy_code_sell = $obj->accountancy_code_sell;
2545  $this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra;
2546  $this->accountancy_code_sell_export = $obj->accountancy_code_sell_export;
2547 
2548  $this->fk_default_warehouse = $obj->fk_default_warehouse;
2549  $this->fk_default_workstation = $obj->fk_default_workstation;
2550  $this->seuil_stock_alerte = $obj->seuil_stock_alerte;
2551  $this->desiredstock = $obj->desiredstock;
2552  $this->stock_reel = $obj->stock;
2553  $this->pmp = $obj->pmp;
2554 
2555  $this->date_creation = $obj->datec;
2556  $this->date_modification = $obj->tms;
2557  $this->import_key = $obj->import_key;
2558  $this->entity = $obj->entity;
2559 
2560  $this->ref_ext = $obj->ref_ext;
2561  $this->fk_price_expression = $obj->fk_price_expression;
2562  $this->fk_unit = $obj->fk_unit;
2563  $this->price_autogen = $obj->price_autogen;
2564  $this->model_pdf = $obj->model_pdf;
2565 
2566  $this->mandatory_period = $obj->mandatory_period;
2567 
2568  $this->db->free($resql);
2569 
2570  // fetch optionals attributes and labels
2571  $this->fetch_optionals();
2572 
2573  // Multilangs
2574  if (getDolGlobalInt('MAIN_MULTILANGS') && empty($ignore_lang_load)) {
2575  $this->getMultiLangs();
2576  }
2577 
2578  // Load multiprices array
2579  if (!empty($conf->global->PRODUIT_MULTIPRICES) && empty($ignore_price_load)) { // prices per segment
2580  for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) {
2581  $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2582  $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
2583  $sql .= " FROM ".$this->db->prefix()."product_price";
2584  $sql .= " WHERE entity IN (".getEntity('productprice').")";
2585  $sql .= " AND price_level=".((int) $i);
2586  $sql .= " AND fk_product = ".((int) $this->id);
2587  $sql .= " ORDER BY date_price DESC, rowid DESC"; // Get the most recent line
2588  $sql .= " LIMIT 1"; // Only the first one
2589  $resql = $this->db->query($sql);
2590  if ($resql) {
2591  $result = $this->db->fetch_array($resql);
2592 
2593  $this->multiprices[$i] = $result ? $result["price"] : null;
2594  $this->multiprices_ttc[$i] = $result ? $result["price_ttc"] : null;
2595  $this->multiprices_min[$i] = $result ? $result["price_min"] : null;
2596  $this->multiprices_min_ttc[$i] = $result ? $result["price_min_ttc"] : null;
2597  $this->multiprices_base_type[$i] = $result ? $result["price_base_type"] : null;
2598  // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
2599  $this->multiprices_tva_tx[$i] = $result ? $result["tva_tx"].($result ? ' ('.$result['default_vat_code'].')' : '') : null;
2600  $this->multiprices_recuperableonly[$i] = $result ? $result["recuperableonly"] : null;
2601 
2602  // Price by quantity
2603  /*
2604  $this->prices_by_qty[$i]=$result["price_by_qty"];
2605  $this->prices_by_qty_id[$i]=$result["rowid"];
2606  // Récuperation de la liste des prix selon qty si flag positionné
2607  if ($this->prices_by_qty[$i] == 1)
2608  {
2609  $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
2610  $sql.= " FROM ".$this->db->prefix()."product_price_by_qty";
2611  $sql.= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
2612  $sql.= " ORDER BY quantity ASC";
2613  $resultat=array();
2614  $resql = $this->db->query($sql);
2615  if ($resql)
2616  {
2617  $ii=0;
2618  while ($result= $this->db->fetch_array($resql)) {
2619  $resultat[$ii]=array();
2620  $resultat[$ii]["rowid"]=$result["rowid"];
2621  $resultat[$ii]["price"]= $result["price"];
2622  $resultat[$ii]["unitprice"]= $result["unitprice"];
2623  $resultat[$ii]["quantity"]= $result["quantity"];
2624  $resultat[$ii]["remise_percent"]= $result["remise_percent"];
2625  $resultat[$ii]["remise"]= $result["remise"]; // deprecated
2626  $resultat[$ii]["price_base_type"]= $result["price_base_type"];
2627  $ii++;
2628  }
2629  $this->prices_by_qty_list[$i]=$resultat;
2630  }
2631  else
2632  {
2633  dol_print_error($this->db);
2634  return -1;
2635  }
2636  }*/
2637  } else {
2638  $this->error = $this->db->lasterror;
2639  return -1;
2640  }
2641  }
2642  } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES) && empty($ignore_price_load)) { // prices per customers
2643  // Nothing loaded by default. List may be very long.
2644  } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY) && empty($ignore_price_load)) { // prices per quantity
2645  $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2646  $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid";
2647  $sql .= " FROM ".$this->db->prefix()."product_price";
2648  $sql .= " WHERE fk_product = ".((int) $this->id);
2649  $sql .= " ORDER BY date_price DESC, rowid DESC";
2650  $sql .= " LIMIT 1";
2651  $resql = $this->db->query($sql);
2652  if ($resql) {
2653  $result = $this->db->fetch_array($resql);
2654 
2655  // Price by quantity
2656  $this->prices_by_qty[0] = $result["price_by_qty"];
2657  $this->prices_by_qty_id[0] = $result["rowid"];
2658  // Récuperation de la liste des prix selon qty si flag positionné
2659  if ($this->prices_by_qty[0] == 1) {
2660  $sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise, remise, price_base_type";
2661  $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
2662  $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[0]);
2663  $sql .= " ORDER BY quantity ASC";
2664  $resultat = array();
2665  $resql = $this->db->query($sql);
2666  if ($resql) {
2667  $ii = 0;
2668  while ($result = $this->db->fetch_array($resql)) {
2669  $resultat[$ii] = array();
2670  $resultat[$ii]["rowid"] = $result["rowid"];
2671  $resultat[$ii]["price"] = $result["price"];
2672  $resultat[$ii]["unitprice"] = $result["unitprice"];
2673  $resultat[$ii]["quantity"] = $result["quantity"];
2674  $resultat[$ii]["remise_percent"] = $result["remise_percent"];
2675  //$resultat[$ii]["remise"]= $result["remise"]; // deprecated
2676  $resultat[$ii]["price_base_type"] = $result["price_base_type"];
2677  $ii++;
2678  }
2679  $this->prices_by_qty_list[0] = $resultat;
2680  } else {
2681  $this->error = $this->db->lasterror;
2682  return -1;
2683  }
2684  }
2685  } else {
2686  $this->error = $this->db->lasterror;
2687  return -1;
2688  }
2689  } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES) && empty($ignore_price_load)) { // prices per customer and quantity
2690  for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) {
2691  $sql = "SELECT price, price_ttc, price_min, price_min_ttc,";
2692  $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly";
2693  $sql .= " FROM ".$this->db->prefix()."product_price";
2694  $sql .= " WHERE entity IN (".getEntity('productprice').")";
2695  $sql .= " AND price_level=".((int) $i);
2696  $sql .= " AND fk_product = ".((int) $this->id);
2697  $sql .= " ORDER BY date_price DESC, rowid DESC";
2698  $sql .= " LIMIT 1";
2699  $resql = $this->db->query($sql);
2700  if ($resql) {
2701  $result = $this->db->fetch_array($resql);
2702 
2703  $this->multiprices[$i] = $result["price"];
2704  $this->multiprices_ttc[$i] = $result["price_ttc"];
2705  $this->multiprices_min[$i] = $result["price_min"];
2706  $this->multiprices_min_ttc[$i] = $result["price_min_ttc"];
2707  $this->multiprices_base_type[$i] = $result["price_base_type"];
2708  // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on
2709  $this->multiprices_tva_tx[$i] = $result["tva_tx"]; // TODO Add ' ('.$result['default_vat_code'].')'
2710  $this->multiprices_recuperableonly[$i] = $result["recuperableonly"];
2711 
2712  // Price by quantity
2713  $this->prices_by_qty[$i] = $result["price_by_qty"];
2714  $this->prices_by_qty_id[$i] = $result["rowid"];
2715  // Récuperation de la liste des prix selon qty si flag positionné
2716  if ($this->prices_by_qty[$i] == 1) {
2717  $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type";
2718  $sql .= " FROM ".$this->db->prefix()."product_price_by_qty";
2719  $sql .= " WHERE fk_product_price = ".((int) $this->prices_by_qty_id[$i]);
2720  $sql .= " ORDER BY quantity ASC";
2721  $resultat = array();
2722  $resql = $this->db->query($sql);
2723  if ($resql) {
2724  $ii = 0;
2725  while ($result = $this->db->fetch_array($resql)) {
2726  $resultat[$ii] = array();
2727  $resultat[$ii]["rowid"] = $result["rowid"];
2728  $resultat[$ii]["price"] = $result["price"];
2729  $resultat[$ii]["unitprice"] = $result["unitprice"];
2730  $resultat[$ii]["quantity"] = $result["quantity"];
2731  $resultat[$ii]["remise_percent"] = $result["remise_percent"];
2732  $resultat[$ii]["remise"] = $result["remise"]; // deprecated
2733  $resultat[$ii]["price_base_type"] = $result["price_base_type"];
2734  $ii++;
2735  }
2736  $this->prices_by_qty_list[$i] = $resultat;
2737  } else {
2738  $this->error = $this->db->lasterror;
2739  return -1;
2740  }
2741  }
2742  } else {
2743  $this->error = $this->db->lasterror;
2744  return -1;
2745  }
2746  }
2747  }
2748 
2749  if (isModEnabled('dynamicprices') && !empty($this->fk_price_expression) && empty($ignore_expression)) {
2750  include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php';
2751  $priceparser = new PriceParser($this->db);
2752  $price_result = $priceparser->parseProduct($this);
2753  if ($price_result >= 0) {
2754  $this->price = $price_result;
2755  // Calculate the VAT
2756  $this->price_ttc = price2num($this->price) * (1 + ($this->tva_tx / 100));
2757  $this->price_ttc = price2num($this->price_ttc, 'MU');
2758  }
2759  }
2760 
2761  // We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product.
2762  // Instead we just init the stock_warehouse array
2763  $this->stock_warehouse = array();
2764 
2765  return 1;
2766  } else {
2767  return 0;
2768  }
2769  } else {
2770  $this->error = $this->db->lasterror();
2771  return -1;
2772  }
2773  }
2774 
2775  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2782  public function load_stats_mo($socid = 0)
2783  {
2784  // phpcs:enable
2785  global $user, $hookmanager, $action;
2786 
2787  $error = 0;
2788 
2789  foreach (array('toconsume', 'consumed', 'toproduce', 'produced') as $role) {
2790  $this->stats_mo['customers_'.$role] = 0;
2791  $this->stats_mo['nb_'.$role] = 0;
2792  $this->stats_mo['qty_'.$role] = 0;
2793 
2794  $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
2795  $sql .= " SUM(mp.qty) as qty";
2796  $sql .= " FROM ".$this->db->prefix()."mrp_mo as c";
2797  $sql .= " INNER JOIN ".$this->db->prefix()."mrp_production as mp ON mp.fk_mo=c.rowid";
2798  if (empty($user->rights->societe->client->voir) && !$socid) {
2799  $sql .= "INNER JOIN ".$this->db->prefix()."societe_commerciaux as sc ON sc.fk_soc=c.fk_soc AND sc.fk_user = ".((int) $user->id);
2800  }
2801  $sql .= " WHERE ";
2802  $sql .= " c.entity IN (".getEntity('mo').")";
2803 
2804  $sql .= " AND mp.fk_product = ".((int) $this->id);
2805  $sql .= " AND mp.role ='".$this->db->escape($role)."'";
2806  if ($socid > 0) {
2807  $sql .= " AND c.fk_soc = ".((int) $socid);
2808  }
2809 
2810  $result = $this->db->query($sql);
2811  if ($result) {
2812  $obj = $this->db->fetch_object($result);
2813  $this->stats_mo['customers_'.$role] = $obj->nb_customers ? $obj->nb_customers : 0;
2814  $this->stats_mo['nb_'.$role] = $obj->nb ? $obj->nb : 0;
2815  $this->stats_mo['qty_'.$role] = $obj->qty ? price2num($obj->qty, 'MS') : 0; // qty may be a float due to the SUM()
2816  } else {
2817  $this->error = $this->db->error();
2818  $error++;
2819  }
2820  }
2821 
2822  if (!empty($error)) {
2823  return -1;
2824  }
2825 
2826  $parameters = array('socid' => $socid);
2827  $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
2828  if ($reshook > 0) {
2829  $this->stats_mo = $hookmanager->resArray['stats_mo'];
2830  }
2831 
2832  return 1;
2833  }
2834 
2835  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2842  public function load_stats_bom($socid = 0)
2843  {
2844  // phpcs:enable
2845  global $user, $hookmanager, $action;
2846 
2847  $error = 0;
2848 
2849  $this->stats_bom['nb_toproduce'] = 0;
2850  $this->stats_bom['nb_toconsume'] = 0;
2851  $this->stats_bom['qty_toproduce'] = 0;
2852  $this->stats_bom['qty_toconsume'] = 0;
2853 
2854  $sql = "SELECT COUNT(DISTINCT b.rowid) as nb_toproduce,";
2855  $sql .= " SUM(b.qty) as qty_toproduce";
2856  $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
2857  $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
2858  $sql .= " WHERE ";
2859  $sql .= " b.entity IN (".getEntity('bom').")";
2860  $sql .= " AND b.fk_product =".((int) $this->id);
2861  $sql .= " GROUP BY b.rowid";
2862 
2863  $result = $this->db->query($sql);
2864  if ($result) {
2865  $obj = $this->db->fetch_object($result);
2866  $this->stats_bom['nb_toproduce'] = !empty($obj->nb_toproduce) ? $obj->nb_toproduce : 0;
2867  $this->stats_bom['qty_toproduce'] = !empty($obj->qty_toproduce) ? price2num($obj->qty_toproduce) : 0;
2868  } else {
2869  $this->error = $this->db->error();
2870  $error++;
2871  }
2872 
2873  $sql = "SELECT COUNT(DISTINCT bl.rowid) as nb_toconsume,";
2874  $sql .= " SUM(bl.qty) as qty_toconsume";
2875  $sql .= " FROM ".$this->db->prefix()."bom_bom as b";
2876  $sql .= " INNER JOIN ".$this->db->prefix()."bom_bomline as bl ON bl.fk_bom=b.rowid";
2877  $sql .= " WHERE ";
2878  $sql .= " b.entity IN (".getEntity('bom').")";
2879  $sql .= " AND bl.fk_product =".((int) $this->id);
2880 
2881  $result = $this->db->query($sql);
2882  if ($result) {
2883  $obj = $this->db->fetch_object($result);
2884  $this->stats_bom['nb_toconsume'] = !empty($obj->nb_toconsume) ? $obj->nb_toconsume : 0;
2885  $this->stats_bom['qty_toconsume'] = !empty($obj->qty_toconsume) ? price2num($obj->qty_toconsume) : 0;
2886  } else {
2887  $this->error = $this->db->error();
2888  $error++;
2889  }
2890 
2891  if (!empty($error)) {
2892  return -1;
2893  }
2894 
2895  $parameters = array('socid' => $socid);
2896  $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action);
2897  if ($reshook > 0) {
2898  $this->stats_bom = $hookmanager->resArray['stats_bom'];
2899  }
2900 
2901  return 1;
2902  }
2903 
2904  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2911  public function load_stats_propale($socid = 0)
2912  {
2913  // phpcs:enable
2914  global $conf, $user, $hookmanager, $action;
2915 
2916  $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,";
2917  $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
2918  $sql .= " FROM ".$this->db->prefix()."propaldet as pd";
2919  $sql .= ", ".$this->db->prefix()."propal as p";
2920  $sql .= ", ".$this->db->prefix()."societe as s";
2921  if (empty($user->rights->societe->client->voir) && !$socid) {
2922  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
2923  }
2924  $sql .= " WHERE p.rowid = pd.fk_propal";
2925  $sql .= " AND p.fk_soc = s.rowid";
2926  $sql .= " AND p.entity IN (".getEntity('propal').")";
2927  $sql .= " AND pd.fk_product = ".((int) $this->id);
2928  if (empty($user->rights->societe->client->voir) && !$socid) {
2929  $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
2930  }
2931  //$sql.= " AND pr.fk_statut != 0";
2932  if ($socid > 0) {
2933  $sql .= " AND p.fk_soc = ".((int) $socid);
2934  }
2935 
2936  $result = $this->db->query($sql);
2937  if ($result) {
2938  $obj = $this->db->fetch_object($result);
2939  $this->stats_propale['customers'] = $obj->nb_customers;
2940  $this->stats_propale['nb'] = $obj->nb;
2941  $this->stats_propale['rows'] = $obj->nb_rows;
2942  $this->stats_propale['qty'] = $obj->qty ? $obj->qty : 0;
2943 
2944  // if it's a virtual product, maybe it is in proposal by extension
2945  if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
2946  $TFather = $this->getFather();
2947  if (is_array($TFather) && !empty($TFather)) {
2948  foreach ($TFather as &$fatherData) {
2949  $pFather = new Product($this->db);
2950  $pFather->id = $fatherData['id'];
2951  $qtyCoef = $fatherData['qty'];
2952 
2953  if ($fatherData['incdec']) {
2954  $pFather->load_stats_propale($socid);
2955 
2956  $this->stats_propale['customers'] += $pFather->stats_propale['customers'];
2957  $this->stats_propale['nb'] += $pFather->stats_propale['nb'];
2958  $this->stats_propale['rows'] += $pFather->stats_propale['rows'];
2959  $this->stats_propale['qty'] += $pFather->stats_propale['qty'] * $qtyCoef;
2960  }
2961  }
2962  }
2963  }
2964 
2965  $parameters = array('socid' => $socid);
2966  $reshook = $hookmanager->executeHooks('loadStatsCustomerProposal', $parameters, $this, $action);
2967  if ($reshook > 0) {
2968  $this->stats_propale = $hookmanager->resArray['stats_propale'];
2969  }
2970 
2971  return 1;
2972  } else {
2973  $this->error = $this->db->error();
2974  return -1;
2975  }
2976  }
2977 
2978 
2979  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
2986  public function load_stats_proposal_supplier($socid = 0)
2987  {
2988  // phpcs:enable
2989  global $conf, $user, $hookmanager, $action;
2990 
2991  $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,";
2992  $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty";
2993  $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as pd";
2994  $sql .= ", ".$this->db->prefix()."supplier_proposal as p";
2995  $sql .= ", ".$this->db->prefix()."societe as s";
2996  if (empty($user->rights->societe->client->voir) && !$socid) {
2997  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
2998  }
2999  $sql .= " WHERE p.rowid = pd.fk_supplier_proposal";
3000  $sql .= " AND p.fk_soc = s.rowid";
3001  $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
3002  $sql .= " AND pd.fk_product = ".((int) $this->id);
3003  if (empty($user->rights->societe->client->voir) && !$socid) {
3004  $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3005  }
3006  //$sql.= " AND pr.fk_statut != 0";
3007  if ($socid > 0) {
3008  $sql .= " AND p.fk_soc = ".((int) $socid);
3009  }
3010 
3011  $result = $this->db->query($sql);
3012  if ($result) {
3013  $obj = $this->db->fetch_object($result);
3014  $this->stats_proposal_supplier['suppliers'] = $obj->nb_suppliers;
3015  $this->stats_proposal_supplier['nb'] = $obj->nb;
3016  $this->stats_proposal_supplier['rows'] = $obj->nb_rows;
3017  $this->stats_proposal_supplier['qty'] = $obj->qty ? $obj->qty : 0;
3018 
3019  $parameters = array('socid' => $socid);
3020  $reshook = $hookmanager->executeHooks('loadStatsSupplierProposal', $parameters, $this, $action);
3021  if ($reshook > 0) {
3022  $this->stats_proposal_supplier = $hookmanager->resArray['stats_proposal_supplier'];
3023  }
3024 
3025  return 1;
3026  } else {
3027  $this->error = $this->db->error();
3028  return -1;
3029  }
3030  }
3031 
3032 
3033  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3042  public function load_stats_commande($socid = 0, $filtrestatut = '', $forVirtualStock = 0)
3043  {
3044  // phpcs:enable
3045  global $conf, $user, $hookmanager, $action;
3046 
3047  $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3048  $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3049  $sql .= " FROM ".$this->db->prefix()."commandedet as cd";
3050  $sql .= ", ".$this->db->prefix()."commande as c";
3051  $sql .= ", ".$this->db->prefix()."societe as s";
3052  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3053  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3054  }
3055  $sql .= " WHERE c.rowid = cd.fk_commande";
3056  $sql .= " AND c.fk_soc = s.rowid";
3057  $sql .= " AND c.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'commande').")";
3058  $sql .= " AND cd.fk_product = ".((int) $this->id);
3059  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3060  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3061  }
3062  if ($socid > 0) {
3063  $sql .= " AND c.fk_soc = ".((int) $socid);
3064  }
3065  if ($filtrestatut <> '') {
3066  $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")";
3067  }
3068 
3069  $result = $this->db->query($sql);
3070  if ($result) {
3071  $obj = $this->db->fetch_object($result);
3072  $this->stats_commande['customers'] = $obj->nb_customers;
3073  $this->stats_commande['nb'] = $obj->nb;
3074  $this->stats_commande['rows'] = $obj->nb_rows;
3075  $this->stats_commande['qty'] = $obj->qty ? $obj->qty : 0;
3076 
3077  // if it's a virtual product, maybe it is in order by extension
3078  if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
3079  $TFather = $this->getFather();
3080  if (is_array($TFather) && !empty($TFather)) {
3081  foreach ($TFather as &$fatherData) {
3082  $pFather = new Product($this->db);
3083  $pFather->id = $fatherData['id'];
3084  $qtyCoef = $fatherData['qty'];
3085 
3086  if ($fatherData['incdec']) {
3087  $pFather->load_stats_commande($socid, $filtrestatut);
3088 
3089  $this->stats_commande['customers'] += $pFather->stats_commande['customers'];
3090  $this->stats_commande['nb'] += $pFather->stats_commande['nb'];
3091  $this->stats_commande['rows'] += $pFather->stats_commande['rows'];
3092  $this->stats_commande['qty'] += $pFather->stats_commande['qty'] * $qtyCoef;
3093  }
3094  }
3095  }
3096  }
3097 
3098  // If stock decrease is on invoice validation, the theorical stock continue to
3099  // count the orders to ship in theorical stock when some are already removed by invoice validation.
3100  if ($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_ON_BILL)) {
3101  if (!empty($conf->global->DECREASE_ONLY_UNINVOICEDPRODUCTS)) {
3102  // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is on, we make a compensation but only if order not yet invoice.
3103  $adeduire = 0;
3104  $sql = "SELECT sum(fd.qty) as count FROM ".$this->db->prefix()."facturedet as fd ";
3105  $sql .= " JOIN ".$this->db->prefix()."facture as f ON fd.fk_facture = f.rowid ";
3106  $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'))";
3107  $sql .= " JOIN ".$this->db->prefix()."commande as c ON el.fk_source = c.rowid ";
3108  $sql .= " WHERE c.fk_statut IN (".$this->db->sanitize($filtrestatut).") AND c.facture = 0 AND fd.fk_product = ".((int) $this->id);
3109  dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3110  $resql = $this->db->query($sql);
3111  if ($resql) {
3112  if ($this->db->num_rows($resql) > 0) {
3113  $obj = $this->db->fetch_object($resql);
3114  $adeduire += $obj->count;
3115  }
3116  }
3117 
3118  $this->stats_commande['qty'] -= $adeduire;
3119  } else {
3120  // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is off, we make a compensation with lines of invoices linked to the order
3121  include_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
3122 
3123  // For every order having invoice already validated we need to decrease stock cause it's in physical stock
3124  $adeduire = 0;
3125  $sql = 'SELECT sum(fd.qty) as count FROM '.MAIN_DB_PREFIX.'facturedet as fd ';
3126  $sql .= ' JOIN '.MAIN_DB_PREFIX.'facture as f ON fd.fk_facture = f.rowid ';
3127  $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'))";
3128  $sql .= ' JOIN '.MAIN_DB_PREFIX.'commande as c ON el.fk_source = c.rowid ';
3129  $sql .= ' WHERE c.fk_statut IN ('.$this->db->sanitize($filtrestatut).') AND f.fk_statut > '.Facture::STATUS_DRAFT.' AND fd.fk_product = '.((int) $this->id);
3130  dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE);
3131  $resql = $this->db->query($sql);
3132  if ($resql) {
3133  if ($this->db->num_rows($resql) > 0) {
3134  $obj = $this->db->fetch_object($resql);
3135  $adeduire += $obj->count;
3136  }
3137  }
3138 
3139  $this->stats_commande['qty'] -= $adeduire;
3140  }
3141  }
3142 
3143  $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3144  $reshook = $hookmanager->executeHooks('loadStatsCustomerOrder', $parameters, $this, $action);
3145  if ($reshook > 0) {
3146  $this->stats_commande = $hookmanager->resArray['stats_commande'];
3147  }
3148  return 1;
3149  } else {
3150  $this->error = $this->db->error();
3151  return -1;
3152  }
3153  }
3154 
3155  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3165  public function load_stats_commande_fournisseur($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3166  {
3167  // phpcs:enable
3168  global $conf, $user, $hookmanager, $action;
3169 
3170  $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,";
3171  $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3172  $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as cd";
3173  $sql .= ", ".$this->db->prefix()."commande_fournisseur as c";
3174  $sql .= ", ".$this->db->prefix()."societe as s";
3175  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3176  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3177  }
3178  $sql .= " WHERE c.rowid = cd.fk_commande";
3179  $sql .= " AND c.fk_soc = s.rowid";
3180  $sql .= " AND c.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'supplier_order').")";
3181  $sql .= " AND cd.fk_product = ".((int) $this->id);
3182  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3183  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3184  }
3185  if ($socid > 0) {
3186  $sql .= " AND c.fk_soc = ".((int) $socid);
3187  }
3188  if ($filtrestatut != '') {
3189  $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")"; // Peut valoir 0
3190  }
3191  if (!empty($dateofvirtualstock)) {
3192  $sql .= " AND c.date_livraison <= '".$this->db->idate($dateofvirtualstock)."'";
3193  }
3194 
3195  $result = $this->db->query($sql);
3196  if ($result) {
3197  $obj = $this->db->fetch_object($result);
3198  $this->stats_commande_fournisseur['suppliers'] = $obj->nb_suppliers;
3199  $this->stats_commande_fournisseur['nb'] = $obj->nb;
3200  $this->stats_commande_fournisseur['rows'] = $obj->nb_rows;
3201  $this->stats_commande_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3202 
3203  $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3204  $reshook = $hookmanager->executeHooks('loadStatsSupplierOrder', $parameters, $this, $action);
3205  if ($reshook > 0) {
3206  $this->stats_commande_fournisseur = $hookmanager->resArray['stats_commande_fournisseur'];
3207  }
3208 
3209  return 1;
3210  } else {
3211  $this->error = $this->db->error().' sql='.$sql;
3212  return -1;
3213  }
3214  }
3215 
3216  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3226  public function load_stats_sending($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $filterShipmentStatus = '')
3227  {
3228  // phpcs:enable
3229  global $conf, $user, $hookmanager, $action;
3230 
3231  $sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,";
3232  $sql .= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty";
3233  $sql .= " FROM ".$this->db->prefix()."expeditiondet as ed";
3234  $sql .= ", ".$this->db->prefix()."commandedet as cd";
3235  $sql .= ", ".$this->db->prefix()."commande as c";
3236  $sql .= ", ".$this->db->prefix()."expedition as e";
3237  $sql .= ", ".$this->db->prefix()."societe as s";
3238  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3239  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3240  }
3241  $sql .= " WHERE e.rowid = ed.fk_expedition";
3242  $sql .= " AND c.rowid = cd.fk_commande";
3243  $sql .= " AND e.fk_soc = s.rowid";
3244  $sql .= " AND e.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'expedition').")";
3245  $sql .= " AND ed.fk_origin_line = cd.rowid";
3246  $sql .= " AND cd.fk_product = ".((int) $this->id);
3247  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3248  $sql .= " AND e.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3249  }
3250  if ($socid > 0) {
3251  $sql .= " AND e.fk_soc = ".((int) $socid);
3252  }
3253  if ($filtrestatut <> '') {
3254  $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3255  }
3256  if (!empty($filterShipmentStatus)) {
3257  $sql .= " AND e.fk_statut IN (".$this->db->sanitize($filterShipmentStatus).")";
3258  }
3259 
3260  $result = $this->db->query($sql);
3261  if ($result) {
3262  $obj = $this->db->fetch_object($result);
3263  $this->stats_expedition['customers'] = $obj->nb_customers;
3264  $this->stats_expedition['nb'] = $obj->nb;
3265  $this->stats_expedition['rows'] = $obj->nb_rows;
3266  $this->stats_expedition['qty'] = $obj->qty ? $obj->qty : 0;
3267 
3268  // if it's a virtual product, maybe it is in sending by extension
3269  if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
3270  $TFather = $this->getFather();
3271  if (is_array($TFather) && !empty($TFather)) {
3272  foreach ($TFather as &$fatherData) {
3273  $pFather = new Product($this->db);
3274  $pFather->id = $fatherData['id'];
3275  $qtyCoef = $fatherData['qty'];
3276 
3277  if ($fatherData['incdec']) {
3278  $pFather->load_stats_sending($socid, $filtrestatut, $forVirtualStock);
3279 
3280  $this->stats_expedition['customers'] += $pFather->stats_expedition['customers'];
3281  $this->stats_expedition['nb'] += $pFather->stats_expedition['nb'];
3282  $this->stats_expedition['rows'] += $pFather->stats_expedition['rows'];
3283  $this->stats_expedition['qty'] += $pFather->stats_expedition['qty'] * $qtyCoef;
3284  }
3285  }
3286  }
3287  }
3288 
3289  $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock, 'filterShipmentStatus' => $filterShipmentStatus);
3290  $reshook = $hookmanager->executeHooks('loadStatsSending', $parameters, $this, $action);
3291  if ($reshook > 0) {
3292  $this->stats_expedition = $hookmanager->resArray['stats_expedition'];
3293  }
3294 
3295  return 1;
3296  } else {
3297  $this->error = $this->db->error();
3298  return -1;
3299  }
3300  }
3301 
3302  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3312  public function load_stats_reception($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3313  {
3314  // phpcs:enable
3315  global $conf, $user, $hookmanager, $action;
3316 
3317  $sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_suppliers, COUNT(DISTINCT cf.rowid) as nb,";
3318  $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3319  $sql .= " FROM ".$this->db->prefix()."commande_fournisseur_dispatch as fd";
3320  $sql .= ", ".$this->db->prefix()."commande_fournisseur as cf";
3321  $sql .= ", ".$this->db->prefix()."societe as s";
3322  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3323  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3324  }
3325  $sql .= " WHERE cf.rowid = fd.fk_commande";
3326  $sql .= " AND cf.fk_soc = s.rowid";
3327  $sql .= " AND cf.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'supplier_order').")";
3328  $sql .= " AND fd.fk_product = ".((int) $this->id);
3329  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3330  $sql .= " AND cf.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3331  }
3332  if ($socid > 0) {
3333  $sql .= " AND cf.fk_soc = ".((int) $socid);
3334  }
3335  if ($filtrestatut <> '') {
3336  $sql .= " AND cf.fk_statut IN (".$this->db->sanitize($filtrestatut).")";
3337  }
3338  if (!empty($dateofvirtualstock)) {
3339  $sql .= " AND fd.datec <= '".$this->db->idate($dateofvirtualstock)."'";
3340  }
3341 
3342  $result = $this->db->query($sql);
3343  if ($result) {
3344  $obj = $this->db->fetch_object($result);
3345  $this->stats_reception['suppliers'] = $obj->nb_suppliers;
3346  $this->stats_reception['nb'] = $obj->nb;
3347  $this->stats_reception['rows'] = $obj->nb_rows;
3348  $this->stats_reception['qty'] = $obj->qty ? $obj->qty : 0;
3349 
3350  $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3351  $reshook = $hookmanager->executeHooks('loadStatsReception', $parameters, $this, $action);
3352  if ($reshook > 0) {
3353  $this->stats_reception = $hookmanager->resArray['stats_reception'];
3354  }
3355 
3356  return 1;
3357  } else {
3358  $this->error = $this->db->error();
3359  return -1;
3360  }
3361  }
3362 
3363  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3373  public function load_stats_inproduction($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $dateofvirtualstock = null)
3374  {
3375  // phpcs:enable
3376  global $conf, $user, $hookmanager, $action;
3377 
3378  $sql = "SELECT COUNT(DISTINCT m.fk_soc) as nb_customers, COUNT(DISTINCT m.rowid) as nb,";
3379  $sql .= " COUNT(mp.rowid) as nb_rows, SUM(mp.qty) as qty, role";
3380  $sql .= " FROM ".$this->db->prefix()."mrp_production as mp";
3381  $sql .= ", ".$this->db->prefix()."mrp_mo as m";
3382  $sql .= " LEFT JOIN ".$this->db->prefix()."societe as s ON s.rowid = m.fk_soc";
3383  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3384  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3385  }
3386  $sql .= " WHERE m.rowid = mp.fk_mo";
3387  $sql .= " AND m.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'mrp').")";
3388  $sql .= " AND mp.fk_product = ".((int) $this->id);
3389  if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) {
3390  $sql .= " AND m.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3391  }
3392  if ($socid > 0) {
3393  $sql .= " AND m.fk_soc = ".((int) $socid);
3394  }
3395  if ($filtrestatut <> '') {
3396  $sql .= " AND m.status IN (".$this->db->sanitize($filtrestatut).")";
3397  }
3398  if (!empty($dateofvirtualstock)) {
3399  $sql .= " AND m.date_valid <= '".$this->db->idate($dateofvirtualstock)."'"; // better date to code ? end of production ?
3400  }
3401  $sql .= " GROUP BY role";
3402 
3403  $this->stats_mrptoconsume['customers'] = 0;
3404  $this->stats_mrptoconsume['nb'] = 0;
3405  $this->stats_mrptoconsume['rows'] = 0;
3406  $this->stats_mrptoconsume['qty'] = 0;
3407  $this->stats_mrptoproduce['customers'] = 0;
3408  $this->stats_mrptoproduce['nb'] = 0;
3409  $this->stats_mrptoproduce['rows'] = 0;
3410  $this->stats_mrptoproduce['qty'] = 0;
3411 
3412  $result = $this->db->query($sql);
3413  if ($result) {
3414  while ($obj = $this->db->fetch_object($result)) {
3415  if ($obj->role == 'toconsume') {
3416  $this->stats_mrptoconsume['customers'] += $obj->nb_customers;
3417  $this->stats_mrptoconsume['nb'] += $obj->nb;
3418  $this->stats_mrptoconsume['rows'] += $obj->nb_rows;
3419  $this->stats_mrptoconsume['qty'] += ($obj->qty ? $obj->qty : 0);
3420  }
3421  if ($obj->role == 'consumed') {
3422  //$this->stats_mrptoconsume['customers'] += $obj->nb_customers;
3423  //$this->stats_mrptoconsume['nb'] += $obj->nb;
3424  //$this->stats_mrptoconsume['rows'] += $obj->nb_rows;
3425  $this->stats_mrptoconsume['qty'] -= ($obj->qty ? $obj->qty : 0);
3426  }
3427  if ($obj->role == 'toproduce') {
3428  $this->stats_mrptoproduce['customers'] += $obj->nb_customers;
3429  $this->stats_mrptoproduce['nb'] += $obj->nb;
3430  $this->stats_mrptoproduce['rows'] += $obj->nb_rows;
3431  $this->stats_mrptoproduce['qty'] += ($obj->qty ? $obj->qty : 0);
3432  }
3433  if ($obj->role == 'produced') {
3434  //$this->stats_mrptoproduce['customers'] += $obj->nb_customers;
3435  //$this->stats_mrptoproduce['nb'] += $obj->nb;
3436  //$this->stats_mrptoproduce['rows'] += $obj->nb_rows;
3437  $this->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0);
3438  }
3439  }
3440 
3441  // Clean data
3442  if ($this->stats_mrptoconsume['qty'] < 0) {
3443  $this->stats_mrptoconsume['qty'] = 0;
3444  }
3445  if ($this->stats_mrptoproduce['qty'] < 0) {
3446  $this->stats_mrptoproduce['qty'] = 0;
3447  }
3448 
3449  $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock);
3450  $reshook = $hookmanager->executeHooks('loadStatsInProduction', $parameters, $this, $action);
3451  if ($reshook > 0) {
3452  $this->stats_mrptoproduce = $hookmanager->resArray['stats_mrptoproduce'];
3453  }
3454 
3455  return 1;
3456  } else {
3457  $this->error = $this->db->error();
3458  return -1;
3459  }
3460  }
3461 
3462  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3469  public function load_stats_contrat($socid = 0)
3470  {
3471  // phpcs:enable
3472  global $conf, $user, $hookmanager, $action;
3473 
3474  $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,";
3475  $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty";
3476  $sql .= " FROM ".$this->db->prefix()."contratdet as cd";
3477  $sql .= ", ".$this->db->prefix()."contrat as c";
3478  $sql .= ", ".$this->db->prefix()."societe as s";
3479  if (empty($user->rights->societe->client->voir) && !$socid) {
3480  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3481  }
3482  $sql .= " WHERE c.rowid = cd.fk_contrat";
3483  $sql .= " AND c.fk_soc = s.rowid";
3484  $sql .= " AND c.entity IN (".getEntity('contract').")";
3485  $sql .= " AND cd.fk_product = ".((int) $this->id);
3486  if (empty($user->rights->societe->client->voir) && !$socid) {
3487  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3488  }
3489  //$sql.= " AND c.statut != 0";
3490  if ($socid > 0) {
3491  $sql .= " AND c.fk_soc = ".((int) $socid);
3492  }
3493 
3494  $result = $this->db->query($sql);
3495  if ($result) {
3496  $obj = $this->db->fetch_object($result);
3497  $this->stats_contrat['customers'] = $obj->nb_customers;
3498  $this->stats_contrat['nb'] = $obj->nb;
3499  $this->stats_contrat['rows'] = $obj->nb_rows;
3500  $this->stats_contrat['qty'] = $obj->qty ? $obj->qty : 0;
3501 
3502  // if it's a virtual product, maybe it is in contract by extension
3503  if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
3504  $TFather = $this->getFather();
3505  if (is_array($TFather) && !empty($TFather)) {
3506  foreach ($TFather as &$fatherData) {
3507  $pFather = new Product($this->db);
3508  $pFather->id = $fatherData['id'];
3509  $qtyCoef = $fatherData['qty'];
3510 
3511  if ($fatherData['incdec']) {
3512  $pFather->load_stats_contrat($socid);
3513 
3514  $this->stats_contrat['customers'] += $pFather->stats_contrat['customers'];
3515  $this->stats_contrat['nb'] += $pFather->stats_contrat['nb'];
3516  $this->stats_contrat['rows'] += $pFather->stats_contrat['rows'];
3517  $this->stats_contrat['qty'] += $pFather->stats_contrat['qty'] * $qtyCoef;
3518  }
3519  }
3520  }
3521  }
3522 
3523  $parameters = array('socid' => $socid);
3524  $reshook = $hookmanager->executeHooks('loadStatsContract', $parameters, $this, $action);
3525  if ($reshook > 0) {
3526  $this->stats_contrat = $hookmanager->resArray['stats_contrat'];
3527  }
3528 
3529  return 1;
3530  } else {
3531  $this->error = $this->db->error().' sql='.$sql;
3532  return -1;
3533  }
3534  }
3535 
3536  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3543  public function load_stats_facture($socid = 0)
3544  {
3545  // phpcs:enable
3546  global $db, $conf, $user, $hookmanager, $action;
3547 
3548  $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
3549  $sql .= " COUNT(fd.rowid) as nb_rows, SUM(".$this->db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1').") as qty";
3550  $sql .= " FROM ".$this->db->prefix()."facturedet as fd";
3551  $sql .= ", ".$this->db->prefix()."facture as f";
3552  $sql .= ", ".$this->db->prefix()."societe as s";
3553  if (empty($user->rights->societe->client->voir) && !$socid) {
3554  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3555  }
3556  $sql .= " WHERE f.rowid = fd.fk_facture";
3557  $sql .= " AND f.fk_soc = s.rowid";
3558  $sql .= " AND f.entity IN (".getEntity('invoice').")";
3559  $sql .= " AND fd.fk_product = ".((int) $this->id);
3560  if (empty($user->rights->societe->client->voir) && !$socid) {
3561  $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3562  }
3563  //$sql.= " AND f.fk_statut != 0";
3564  if ($socid > 0) {
3565  $sql .= " AND f.fk_soc = ".((int) $socid);
3566  }
3567 
3568  $result = $this->db->query($sql);
3569  if ($result) {
3570  $obj = $this->db->fetch_object($result);
3571  $this->stats_facture['customers'] = $obj->nb_customers;
3572  $this->stats_facture['nb'] = $obj->nb;
3573  $this->stats_facture['rows'] = $obj->nb_rows;
3574  $this->stats_facture['qty'] = $obj->qty ? $obj->qty : 0;
3575 
3576  // if it's a virtual product, maybe it is in invoice by extension
3577  if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
3578  $TFather = $this->getFather();
3579  if (is_array($TFather) && !empty($TFather)) {
3580  foreach ($TFather as &$fatherData) {
3581  $pFather = new Product($this->db);
3582  $pFather->id = $fatherData['id'];
3583  $qtyCoef = $fatherData['qty'];
3584 
3585  if ($fatherData['incdec']) {
3586  $pFather->load_stats_facture($socid);
3587 
3588  $this->stats_facture['customers'] += $pFather->stats_facture['customers'];
3589  $this->stats_facture['nb'] += $pFather->stats_facture['nb'];
3590  $this->stats_facture['rows'] += $pFather->stats_facture['rows'];
3591  $this->stats_facture['qty'] += $pFather->stats_facture['qty'] * $qtyCoef;
3592  }
3593  }
3594  }
3595  }
3596 
3597  $parameters = array('socid' => $socid);
3598  $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoice', $parameters, $this, $action);
3599  if ($reshook > 0) {
3600  $this->stats_facture = $hookmanager->resArray['stats_facture'];
3601  }
3602 
3603  return 1;
3604  } else {
3605  $this->error = $this->db->error();
3606  return -1;
3607  }
3608  }
3609 
3610 
3611  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3618  public function load_stats_facturerec($socid = 0)
3619  {
3620  // phpcs:enable
3621  global $db, $conf, $user, $hookmanager;
3622 
3623  $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
3624  $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3625  $sql .= " FROM ".MAIN_DB_PREFIX."facturedet_rec as fd";
3626  $sql .= ", ".MAIN_DB_PREFIX."facture_rec as f";
3627  $sql .= ", ".MAIN_DB_PREFIX."societe as s";
3628  if (empty($user->rights->societe->client->voir) && !$socid) {
3629  $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc";
3630  }
3631  $sql .= " WHERE f.rowid = fd.fk_facture";
3632  $sql .= " AND f.fk_soc = s.rowid";
3633  $sql .= " AND f.entity IN (".getEntity('invoice').")";
3634  $sql .= " AND fd.fk_product = ".((int) $this->id);
3635  if (empty($user->rights->societe->client->voir) && !$socid) {
3636  $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3637  }
3638  //$sql.= " AND f.fk_statut != 0";
3639  if ($socid > 0) {
3640  $sql .= " AND f.fk_soc = ".((int) $socid);
3641  }
3642 
3643  $result = $this->db->query($sql);
3644  if ($result) {
3645  $obj = $this->db->fetch_object($result);
3646  $this->stats_facturerec['customers'] = $obj->nb_customers;
3647  $this->stats_facturerec['nb'] = $obj->nb;
3648  $this->stats_facturerec['rows'] = $obj->nb_rows;
3649  $this->stats_facturerec['qty'] = $obj->qty ? $obj->qty : 0;
3650 
3651  // if it's a virtual product, maybe it is in invoice by extension
3652  if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) {
3653  $TFather = $this->getFather();
3654  if (is_array($TFather) && !empty($TFather)) {
3655  foreach ($TFather as &$fatherData) {
3656  $pFather = new Product($this->db);
3657  $pFather->id = $fatherData['id'];
3658  $qtyCoef = $fatherData['qty'];
3659 
3660  if ($fatherData['incdec']) {
3661  $pFather->load_stats_facture($socid);
3662 
3663  $this->stats_facturerec['customers'] += $pFather->stats_facturerec['customers'];
3664  $this->stats_facturerec['nb'] += $pFather->stats_facturerec['nb'];
3665  $this->stats_facturerec['rows'] += $pFather->stats_facturerec['rows'];
3666  $this->stats_facturerec['qty'] += $pFather->stats_facturerec['qty'] * $qtyCoef;
3667  }
3668  }
3669  }
3670  }
3671 
3672  $parameters = array('socid' => $socid);
3673  $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoiceRec', $parameters, $this, $action);
3674  if ($reshook > 0) {
3675  $this->stats_facturerec = $hookmanager->resArray['stats_facturerec'];
3676  }
3677 
3678  return 1;
3679  } else {
3680  $this->error = $this->db->error();
3681  return -1;
3682  }
3683  }
3684 
3685  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3692  public function load_stats_facture_fournisseur($socid = 0)
3693  {
3694  // phpcs:enable
3695  global $conf, $user, $hookmanager, $action;
3696 
3697  $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,";
3698  $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
3699  $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as fd";
3700  $sql .= ", ".$this->db->prefix()."facture_fourn as f";
3701  $sql .= ", ".$this->db->prefix()."societe as s";
3702  if (empty($user->rights->societe->client->voir) && !$socid) {
3703  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3704  }
3705  $sql .= " WHERE f.rowid = fd.fk_facture_fourn";
3706  $sql .= " AND f.fk_soc = s.rowid";
3707  $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
3708  $sql .= " AND fd.fk_product = ".((int) $this->id);
3709  if (empty($user->rights->societe->client->voir) && !$socid) {
3710  $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3711  }
3712  //$sql.= " AND f.fk_statut != 0";
3713  if ($socid > 0) {
3714  $sql .= " AND f.fk_soc = ".((int) $socid);
3715  }
3716 
3717  $result = $this->db->query($sql);
3718  if ($result) {
3719  $obj = $this->db->fetch_object($result);
3720  $this->stats_facture_fournisseur['suppliers'] = $obj->nb_suppliers;
3721  $this->stats_facture_fournisseur['nb'] = $obj->nb;
3722  $this->stats_facture_fournisseur['rows'] = $obj->nb_rows;
3723  $this->stats_facture_fournisseur['qty'] = $obj->qty ? $obj->qty : 0;
3724 
3725  $parameters = array('socid' => $socid);
3726  $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoice', $parameters, $this, $action);
3727  if ($reshook > 0) {
3728  $this->stats_facture_fournisseur = $hookmanager->resArray['stats_facture_fournisseur'];
3729  }
3730 
3731  return 1;
3732  } else {
3733  $this->error = $this->db->error();
3734  return -1;
3735  }
3736  }
3737 
3738  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3747  private function _get_stats($sql, $mode, $year = 0)
3748  {
3749  // phpcs:enable
3750  $tab = array();
3751 
3752  $resql = $this->db->query($sql);
3753  if ($resql) {
3754  $num = $this->db->num_rows($resql);
3755  $i = 0;
3756  while ($i < $num) {
3757  $arr = $this->db->fetch_array($resql);
3758  $keyfortab = (string) $arr[1];
3759  if ($year == -1) {
3760  $keyfortab = substr($keyfortab, -2);
3761  }
3762 
3763  if ($mode == 'byunit') {
3764  $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[0]; // 1st field
3765  } elseif ($mode == 'bynumber') {
3766  $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
3767  } elseif ($mode == 'byamount') {
3768  $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field
3769  } else {
3770  // Bad value for $mode
3771  return -1;
3772  }
3773  $i++;
3774  }
3775  } else {
3776  $this->error = $this->db->error().' sql='.$sql;
3777  return -1;
3778  }
3779 
3780  if (empty($year)) {
3781  $year = strftime('%Y', time());
3782  $month = strftime('%m', time());
3783  } elseif ($year == -1) {
3784  $year = '';
3785  $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
3786  } else {
3787  $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year.
3788  }
3789 
3790  $result = array();
3791 
3792  for ($j = 0; $j < 12; $j++) {
3793  // $ids is 'D', 'N', 'O', 'S', ... (First letter of month in user language)
3794  $idx = ucfirst(dol_trunc(dol_print_date(dol_mktime(12, 0, 0, $month, 1, 1970), "%b"), 1, 'right', 'UTF-8', 1));
3795 
3796  //print $idx.'-'.$year.'-'.$month.'<br>';
3797  $result[$j] = array($idx, isset($tab[$year.$month]) ? $tab[$year.$month] : 0);
3798  // $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0);
3799 
3800  $month = "0".($month - 1);
3801  if (dol_strlen($month) == 3) {
3802  $month = substr($month, 1);
3803  }
3804  if ($month == 0) {
3805  $month = 12;
3806  $year = $year - 1;
3807  }
3808  }
3809 
3810  return array_reverse($result);
3811  }
3812 
3813 
3814  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3825  public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
3826  {
3827  // phpcs:enable
3828  global $conf;
3829  global $user;
3830 
3831  $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
3832  if ($mode == 'bynumber') {
3833  $sql .= ", count(DISTINCT f.rowid)";
3834  }
3835  $sql .= ", sum(d.total_ht) as total_ht";
3836  $sql .= " FROM ".$this->db->prefix()."facturedet as d, ".$this->db->prefix()."facture as f, ".$this->db->prefix()."societe as s";
3837  if ($filteronproducttype >= 0) {
3838  $sql .= ", ".$this->db->prefix()."product as p";
3839  }
3840  if (empty($user->rights->societe->client->voir) && !$socid) {
3841  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3842  }
3843  $sql .= " WHERE f.rowid = d.fk_facture";
3844  if ($this->id > 0) {
3845  $sql .= " AND d.fk_product = ".((int) $this->id);
3846  } else {
3847  $sql .= " AND d.fk_product > 0";
3848  }
3849  if ($filteronproducttype >= 0) {
3850  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
3851  }
3852  $sql .= " AND f.fk_soc = s.rowid";
3853  $sql .= " AND f.entity IN (".getEntity('invoice').")";
3854  if (empty($user->rights->societe->client->voir) && !$socid) {
3855  $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3856  }
3857  if ($socid > 0) {
3858  $sql .= " AND f.fk_soc = $socid";
3859  }
3860  $sql .= $morefilter;
3861  $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
3862  $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
3863 
3864  return $this->_get_stats($sql, $mode, $year);
3865  }
3866 
3867 
3868  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3879  public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
3880  {
3881  // phpcs:enable
3882  global $conf;
3883  global $user;
3884 
3885  $sql = "SELECT sum(d.qty) as qty, date_format(f.datef, '%Y%m')";
3886  if ($mode == 'bynumber') {
3887  $sql .= ", count(DISTINCT f.rowid)";
3888  }
3889  $sql .= ", sum(d.total_ht) as total_ht";
3890  $sql .= " FROM ".$this->db->prefix()."facture_fourn_det as d, ".$this->db->prefix()."facture_fourn as f, ".$this->db->prefix()."societe as s";
3891  if ($filteronproducttype >= 0) {
3892  $sql .= ", ".$this->db->prefix()."product as p";
3893  }
3894  if (empty($user->rights->societe->client->voir) && !$socid) {
3895  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3896  }
3897  $sql .= " WHERE f.rowid = d.fk_facture_fourn";
3898  if ($this->id > 0) {
3899  $sql .= " AND d.fk_product = ".((int) $this->id);
3900  } else {
3901  $sql .= " AND d.fk_product > 0";
3902  }
3903  if ($filteronproducttype >= 0) {
3904  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
3905  }
3906  $sql .= " AND f.fk_soc = s.rowid";
3907  $sql .= " AND f.entity IN (".getEntity('facture_fourn').")";
3908  if (empty($user->rights->societe->client->voir) && !$socid) {
3909  $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3910  }
3911  if ($socid > 0) {
3912  $sql .= " AND f.fk_soc = $socid";
3913  }
3914  $sql .= $morefilter;
3915  $sql .= " GROUP BY date_format(f.datef,'%Y%m')";
3916  $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC";
3917 
3918  return $this->_get_stats($sql, $mode, $year);
3919  }
3920 
3921  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3932  public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
3933  {
3934  // phpcs:enable
3935  global $conf, $user;
3936 
3937  $sql = "SELECT sum(d.qty) as qty, date_format(p.datep, '%Y%m')";
3938  if ($mode == 'bynumber') {
3939  $sql .= ", count(DISTINCT p.rowid)";
3940  }
3941  $sql .= ", sum(d.total_ht) as total_ht";
3942  $sql .= " FROM ".$this->db->prefix()."propaldet as d, ".$this->db->prefix()."propal as p, ".$this->db->prefix()."societe as s";
3943  if ($filteronproducttype >= 0) {
3944  $sql .= ", ".$this->db->prefix()."product as prod";
3945  }
3946  if (empty($user->rights->societe->client->voir) && !$socid) {
3947  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
3948  }
3949  $sql .= " WHERE p.rowid = d.fk_propal";
3950  if ($this->id > 0) {
3951  $sql .= " AND d.fk_product = ".((int) $this->id);
3952  } else {
3953  $sql .= " AND d.fk_product > 0";
3954  }
3955  if ($filteronproducttype >= 0) {
3956  $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
3957  }
3958  $sql .= " AND p.fk_soc = s.rowid";
3959  $sql .= " AND p.entity IN (".getEntity('propal').")";
3960  if (empty($user->rights->societe->client->voir) && !$socid) {
3961  $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
3962  }
3963  if ($socid > 0) {
3964  $sql .= " AND p.fk_soc = ".((int) $socid);
3965  }
3966  $sql .= $morefilter;
3967  $sql .= " GROUP BY date_format(p.datep,'%Y%m')";
3968  $sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC";
3969 
3970  return $this->_get_stats($sql, $mode, $year);
3971  }
3972 
3973  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
3984  public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
3985  {
3986  // phpcs:enable
3987  global $conf;
3988  global $user;
3989 
3990  $sql = "SELECT sum(d.qty) as qty, date_format(p.date_valid, '%Y%m')";
3991  if ($mode == 'bynumber') {
3992  $sql .= ", count(DISTINCT p.rowid)";
3993  }
3994  $sql .= ", sum(d.total_ht) as total_ht";
3995  $sql .= " FROM ".$this->db->prefix()."supplier_proposaldet as d, ".$this->db->prefix()."supplier_proposal as p, ".$this->db->prefix()."societe as s";
3996  if ($filteronproducttype >= 0) {
3997  $sql .= ", ".$this->db->prefix()."product as prod";
3998  }
3999  if (empty($user->rights->societe->client->voir) && !$socid) {
4000  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4001  }
4002  $sql .= " WHERE p.rowid = d.fk_supplier_proposal";
4003  if ($this->id > 0) {
4004  $sql .= " AND d.fk_product = ".((int) $this->id);
4005  } else {
4006  $sql .= " AND d.fk_product > 0";
4007  }
4008  if ($filteronproducttype >= 0) {
4009  $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype);
4010  }
4011  $sql .= " AND p.fk_soc = s.rowid";
4012  $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")";
4013  if (empty($user->rights->societe->client->voir) && !$socid) {
4014  $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4015  }
4016  if ($socid > 0) {
4017  $sql .= " AND p.fk_soc = ".((int) $socid);
4018  }
4019  $sql .= $morefilter;
4020  $sql .= " GROUP BY date_format(p.date_valid,'%Y%m')";
4021  $sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC";
4022 
4023  return $this->_get_stats($sql, $mode, $year);
4024  }
4025 
4026  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4037  public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4038  {
4039  // phpcs:enable
4040  global $conf, $user;
4041 
4042  $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4043  if ($mode == 'bynumber') {
4044  $sql .= ", count(DISTINCT c.rowid)";
4045  }
4046  $sql .= ", sum(d.total_ht) as total_ht";
4047  $sql .= " FROM ".$this->db->prefix()."commandedet as d, ".$this->db->prefix()."commande as c, ".$this->db->prefix()."societe as s";
4048  if ($filteronproducttype >= 0) {
4049  $sql .= ", ".$this->db->prefix()."product as p";
4050  }
4051  if (empty($user->rights->societe->client->voir) && !$socid) {
4052  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4053  }
4054  $sql .= " WHERE c.rowid = d.fk_commande";
4055  if ($this->id > 0) {
4056  $sql .= " AND d.fk_product = ".((int) $this->id);
4057  } else {
4058  $sql .= " AND d.fk_product > 0";
4059  }
4060  if ($filteronproducttype >= 0) {
4061  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4062  }
4063  $sql .= " AND c.fk_soc = s.rowid";
4064  $sql .= " AND c.entity IN (".getEntity('commande').")";
4065  if (empty($user->rights->societe->client->voir) && !$socid) {
4066  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4067  }
4068  if ($socid > 0) {
4069  $sql .= " AND c.fk_soc = ".((int) $socid);
4070  }
4071  $sql .= $morefilter;
4072  $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4073  $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4074 
4075  return $this->_get_stats($sql, $mode, $year);
4076  }
4077 
4078  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4089  public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4090  {
4091  // phpcs:enable
4092  global $conf, $user;
4093 
4094  $sql = "SELECT sum(d.qty) as qty, date_format(c.date_commande, '%Y%m')";
4095  if ($mode == 'bynumber') {
4096  $sql .= ", count(DISTINCT c.rowid)";
4097  }
4098  $sql .= ", sum(d.total_ht) as total_ht";
4099  $sql .= " FROM ".$this->db->prefix()."commande_fournisseurdet as d, ".$this->db->prefix()."commande_fournisseur as c, ".$this->db->prefix()."societe as s";
4100  if ($filteronproducttype >= 0) {
4101  $sql .= ", ".$this->db->prefix()."product as p";
4102  }
4103  if (empty($user->rights->societe->client->voir) && !$socid) {
4104  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4105  }
4106  $sql .= " WHERE c.rowid = d.fk_commande";
4107  if ($this->id > 0) {
4108  $sql .= " AND d.fk_product = ".((int) $this->id);
4109  } else {
4110  $sql .= " AND d.fk_product > 0";
4111  }
4112  if ($filteronproducttype >= 0) {
4113  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4114  }
4115  $sql .= " AND c.fk_soc = s.rowid";
4116  $sql .= " AND c.entity IN (".getEntity('supplier_order').")";
4117  if (empty($user->rights->societe->client->voir) && !$socid) {
4118  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4119  }
4120  if ($socid > 0) {
4121  $sql .= " AND c.fk_soc = ".((int) $socid);
4122  }
4123  $sql .= $morefilter;
4124  $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')";
4125  $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC";
4126 
4127  return $this->_get_stats($sql, $mode, $year);
4128  }
4129 
4130  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4141  public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4142  {
4143  // phpcs:enable
4144  global $conf, $user;
4145 
4146  $sql = "SELECT sum(d.qty) as qty, date_format(c.date_contrat, '%Y%m')";
4147  if ($mode == 'bynumber') {
4148  $sql .= ", count(DISTINCT c.rowid)";
4149  }
4150  $sql .= ", sum(d.total_ht) as total_ht";
4151  $sql .= " FROM ".$this->db->prefix()."contratdet as d, ".$this->db->prefix()."contrat as c, ".$this->db->prefix()."societe as s";
4152  if ($filteronproducttype >= 0) {
4153  $sql .= ", ".$this->db->prefix()."product as p";
4154  }
4155  if (empty($user->rights->societe->client->voir) && !$socid) {
4156  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4157  }
4158 
4159  $sql .= " WHERE c.entity IN (".getEntity('contract').")";
4160  $sql .= " AND c.rowid = d.fk_contrat";
4161 
4162  if ($this->id > 0) {
4163  $sql .= " AND d.fk_product = ".((int) $this->id);
4164  } else {
4165  $sql .= " AND d.fk_product > 0";
4166  }
4167  if ($filteronproducttype >= 0) {
4168  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4169  }
4170  $sql .= " AND c.fk_soc = s.rowid";
4171 
4172  if (empty($user->rights->societe->client->voir) && !$socid) {
4173  $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4174  }
4175  if ($socid > 0) {
4176  $sql .= " AND c.fk_soc = ".((int) $socid);
4177  }
4178  $sql .= $morefilter;
4179  $sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')";
4180  $sql .= " ORDER BY date_format(c.date_contrat,'%Y%m') DESC";
4181 
4182  return $this->_get_stats($sql, $mode, $year);
4183  }
4184 
4185  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4196  public function get_nb_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '')
4197  {
4198  // phpcs:enable
4199  global $conf, $user;
4200 
4201  $sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')";
4202  if ($mode == 'bynumber') {
4203  $sql .= ", count(DISTINCT d.rowid)";
4204  }
4205  $sql .= " FROM ".$this->db->prefix()."mrp_mo as d LEFT JOIN ".$this->db->prefix()."societe as s ON d.fk_soc = s.rowid";
4206  if ($filteronproducttype >= 0) {
4207  $sql .= ", ".$this->db->prefix()."product as p";
4208  }
4209  if (empty($user->rights->societe->client->voir) && !$socid) {
4210  $sql .= ", ".$this->db->prefix()."societe_commerciaux as sc";
4211  }
4212 
4213  $sql .= " WHERE d.entity IN (".getEntity('mo').")";
4214  $sql .= " AND d.status > 0";
4215 
4216  if ($this->id > 0) {
4217  $sql .= " AND d.fk_product = ".((int) $this->id);
4218  } else {
4219  $sql .= " AND d.fk_product > 0";
4220  }
4221  if ($filteronproducttype >= 0) {
4222  $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype);
4223  }
4224 
4225  if (empty($user->rights->societe->client->voir) && !$socid) {
4226  $sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = ".((int) $user->id);
4227  }
4228  if ($socid > 0) {
4229  $sql .= " AND d.fk_soc = ".((int) $socid);
4230  }
4231  $sql .= $morefilter;
4232  $sql .= " GROUP BY date_format(d.date_valid,'%Y%m')";
4233  $sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC";
4234 
4235  return $this->_get_stats($sql, $mode, $year);
4236  }
4237 
4238  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4248  public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1)
4249  {
4250  // phpcs:enable
4251  // Clean parameters
4252  if (!is_numeric($id_pere)) {
4253  $id_pere = 0;
4254  }
4255  if (!is_numeric($id_fils)) {
4256  $id_fils = 0;
4257  }
4258  if (!is_numeric($incdec)) {
4259  $incdec = 0;
4260  }
4261 
4262  $result = $this->del_sousproduit($id_pere, $id_fils);
4263  if ($result < 0) {
4264  return $result;
4265  }
4266 
4267  // Check not already father of id_pere (to avoid father -> child -> father links)
4268  $sql = "SELECT fk_product_pere from ".$this->db->prefix()."product_association";
4269  $sql .= " WHERE fk_product_pere = ".((int) $id_fils)." AND fk_product_fils = ".((int) $id_pere);
4270  if (!$this->db->query($sql)) {
4271  dol_print_error($this->db);
4272  return -1;
4273  } else {
4274  //Selection of the highest row
4275  $sql = "SELECT MAX(rang) as max_rank FROM ".$this->db->prefix()."product_association";
4276  $sql .= " WHERE fk_product_pere = ".((int) $id_pere);
4277  $resql = $this->db->query($sql);
4278  if ($resql) {
4279  $obj = $this->db->fetch_object($resql);
4280  $rank = $obj->max_rank + 1;
4281  //Addition of a product with the highest rank +1
4282  $sql = "INSERT INTO ".$this->db->prefix()."product_association(fk_product_pere,fk_product_fils,qty,incdec,rang)";
4283  $sql .= " VALUES (".((int) $id_pere).", ".((int) $id_fils).", ".price2num($qty, 'MS').", ".price2num($incdec, 'MS').", ".((int) $rank).")";
4284  if (! $this->db->query($sql)) {
4285  dol_print_error($this->db);
4286  return -1;
4287  } else {
4288  return 1;
4289  }
4290  } else {
4291  dol_print_error($this->db);
4292  return -1;
4293  }
4294  }
4295  }
4296 
4297  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4307  public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1)
4308  {
4309  // phpcs:enable
4310  // Clean parameters
4311  if (!is_numeric($id_pere)) {
4312  $id_pere = 0;
4313  }
4314  if (!is_numeric($id_fils)) {
4315  $id_fils = 0;
4316  }
4317  if (!is_numeric($incdec)) {
4318  $incdec = 1;
4319  }
4320  if (!is_numeric($qty)) {
4321  $qty = 1;
4322  }
4323 
4324  $sql = 'UPDATE '.$this->db->prefix().'product_association SET ';
4325  $sql .= 'qty = '.price2num($qty, 'MS');
4326  $sql .= ',incdec = '.price2num($incdec, 'MS');
4327  $sql .= ' WHERE fk_product_pere = '.((int) $id_pere).' AND fk_product_fils = '.((int) $id_fils);
4328 
4329  if (!$this->db->query($sql)) {
4330  dol_print_error($this->db);
4331  return -1;
4332  } else {
4333  return 1;
4334  }
4335  }
4336 
4337  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4345  public function del_sousproduit($fk_parent, $fk_child)
4346  {
4347  // phpcs:enable
4348  if (!is_numeric($fk_parent)) {
4349  $fk_parent = 0;
4350  }
4351  if (!is_numeric($fk_child)) {
4352  $fk_child = 0;
4353  }
4354 
4355  $sql = "DELETE FROM ".$this->db->prefix()."product_association";
4356  $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
4357  $sql .= " AND fk_product_fils = ".((int) $fk_child);
4358 
4359  dol_syslog(get_class($this).'::del_sousproduit', LOG_DEBUG);
4360  if (!$this->db->query($sql)) {
4361  dol_print_error($this->db);
4362  return -1;
4363  }
4364 
4365  // Updated ranks so that none are missing
4366  $sqlrank = "SELECT rowid, rang FROM ".$this->db->prefix()."product_association";
4367  $sqlrank.= " WHERE fk_product_pere = ".((int) $fk_parent);
4368  $sqlrank.= " ORDER BY rang";
4369  $resqlrank = $this->db->query($sqlrank);
4370  if ($resqlrank) {
4371  $cpt = 0;
4372  while ($objrank = $this->db->fetch_object($resqlrank)) {
4373  $cpt++;
4374  $sql = "UPDATE ".$this->db->prefix()."product_association";
4375  $sql.= " SET rang = ".((int) $cpt);
4376  $sql.= " WHERE rowid = ".((int) $objrank->rowid);
4377  if (! $this->db->query($sql)) {
4378  dol_print_error($this->db);
4379  return -1;
4380  }
4381  }
4382  }
4383  return 1;
4384  }
4385 
4386  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4394  public function is_sousproduit($fk_parent, $fk_child)
4395  {
4396  // phpcs:enable
4397  $sql = "SELECT fk_product_pere, qty, incdec";
4398  $sql .= " FROM ".$this->db->prefix()."product_association";
4399  $sql .= " WHERE fk_product_pere = ".((int) $fk_parent);
4400  $sql .= " AND fk_product_fils = ".((int) $fk_child);
4401 
4402  $result = $this->db->query($sql);
4403  if ($result) {
4404  $num = $this->db->num_rows($result);
4405 
4406  if ($num > 0) {
4407  $obj = $this->db->fetch_object($result);
4408 
4409  $this->is_sousproduit_qty = $obj->qty;
4410  $this->is_sousproduit_incdec = $obj->incdec;
4411 
4412  return true;
4413  } else {
4414  return false;
4415  }
4416  } else {
4417  dol_print_error($this->db);
4418  return -1;
4419  }
4420  }
4421 
4422 
4423  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4434  public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
4435  {
4436  // phpcs:enable
4437  global $conf;
4438 
4439  $now = dol_now();
4440 
4441  dol_syslog(get_class($this)."::add_fournisseur id_fourn = ".$id_fourn." ref_fourn=".$ref_fourn." quantity=".$quantity, LOG_DEBUG);
4442 
4443  // Clean parameters
4444  $quantity = price2num($quantity, 'MS');
4445 
4446  if ($ref_fourn) {
4447  $sql = "SELECT rowid, fk_product";
4448  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
4449  $sql .= " WHERE fk_soc = ".((int) $id_fourn);
4450  $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
4451  $sql .= " AND fk_product <> ".((int) $this->id);
4452  $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
4453 
4454  $resql = $this->db->query($sql);
4455  if ($resql) {
4456  $obj = $this->db->fetch_object($resql);
4457  if ($obj) {
4458  // If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies)
4459  $this->product_id_already_linked = $obj->fk_product;
4460  return -3;
4461  }
4462  $this->db->free($resql);
4463  }
4464  }
4465 
4466  $sql = "SELECT rowid";
4467  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
4468  $sql .= " WHERE fk_soc = ".((int) $id_fourn);
4469  if ($ref_fourn) {
4470  $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'";
4471  } else {
4472  $sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)";
4473  }
4474  $sql .= " AND quantity = ".((float) $quantity);
4475  $sql .= " AND fk_product = ".((int) $this->id);
4476  $sql .= " AND entity IN (".getEntity('productsupplierprice').")";
4477 
4478  $resql = $this->db->query($sql);
4479  if ($resql) {
4480  $obj = $this->db->fetch_object($resql);
4481 
4482  // The reference supplier does not exist, we create it for this product.
4483  if (empty($obj)) {
4484  $sql = "INSERT INTO ".$this->db->prefix()."product_fournisseur_price(";
4485  $sql .= "datec";
4486  $sql .= ", entity";
4487  $sql .= ", fk_product";
4488  $sql .= ", fk_soc";
4489  $sql .= ", ref_fourn";
4490  $sql .= ", quantity";
4491  $sql .= ", fk_user";
4492  $sql .= ", tva_tx";
4493  $sql .= ") VALUES (";
4494  $sql .= "'".$this->db->idate($now)."'";
4495  $sql .= ", ".$conf->entity;
4496  $sql .= ", ".$this->id;
4497  $sql .= ", ".$id_fourn;
4498  $sql .= ", '".$this->db->escape($ref_fourn)."'";
4499  $sql .= ", ".$quantity;
4500  $sql .= ", ".$user->id;
4501  $sql .= ", 0";
4502  $sql .= ")";
4503 
4504  if ($this->db->query($sql)) {
4505  $this->product_fourn_price_id = $this->db->last_insert_id($this->db->prefix()."product_fournisseur_price");
4506  return 1;
4507  } else {
4508  $this->error = $this->db->lasterror();
4509  return -1;
4510  }
4511  } else {
4512  // If the supplier price already exists for this product and quantity
4513  $this->product_fourn_price_id = $obj->rowid;
4514  return 0;
4515  }
4516  } else {
4517  $this->error = $this->db->lasterror();
4518  return -2;
4519  }
4520  }
4521 
4522 
4523  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4529  public function list_suppliers()
4530  {
4531  // phpcs:enable
4532  global $conf;
4533 
4534  $list = array();
4535 
4536  $sql = "SELECT DISTINCT p.fk_soc";
4537  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as p";
4538  $sql .= " WHERE p.fk_product = ".((int) $this->id);
4539  $sql .= " AND p.entity = ".((int) $conf->entity);
4540 
4541  $result = $this->db->query($sql);
4542  if ($result) {
4543  $num = $this->db->num_rows($result);
4544  $i = 0;
4545  while ($i < $num) {
4546  $obj = $this->db->fetch_object($result);
4547  $list[$i] = $obj->fk_soc;
4548  $i++;
4549  }
4550  }
4551 
4552  return $list;
4553  }
4554 
4555  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4563  public function clone_price($fromId, $toId)
4564  {
4565  global $conf, $user;
4566 
4567  $now = dol_now();
4568 
4569  $this->db->begin();
4570 
4571  // prices
4572  $sql = "INSERT INTO ".$this->db->prefix()."product_price (";
4573  $sql .= " entity";
4574  $sql .= ", fk_product";
4575  $sql .= ", date_price";
4576  $sql .= ", price_level";
4577  $sql .= ", price";
4578  $sql .= ", price_ttc";
4579  $sql .= ", price_min";
4580  $sql .= ", price_min_ttc";
4581  $sql .= ", price_base_type";
4582  $sql .= ", default_vat_code";
4583  $sql .= ", tva_tx";
4584  $sql .= ", recuperableonly";
4585  $sql .= ", localtax1_tx";
4586  $sql .= ", localtax1_type";
4587  $sql .= ", localtax2_tx";
4588  $sql .= ", localtax2_type";
4589  $sql .= ", fk_user_author";
4590  $sql .= ", tosell";
4591  $sql .= ", price_by_qty";
4592  $sql .= ", fk_price_expression";
4593  $sql .= ", fk_multicurrency";
4594  $sql .= ", multicurrency_code";
4595  $sql .= ", multicurrency_tx";
4596  $sql .= ", multicurrency_price";
4597  $sql .= ", multicurrency_price_ttc";
4598  $sql .= ")";
4599  $sql .= " SELECT";
4600  $sql .= " entity";
4601  $sql .= ", ".$toId;
4602  $sql .= ", '".$this->db->idate($now)."'";
4603  $sql .= ", price_level";
4604  $sql .= ", price";
4605  $sql .= ", price_ttc";
4606  $sql .= ", price_min";
4607  $sql .= ", price_min_ttc";
4608  $sql .= ", price_base_type";
4609  $sql .= ", default_vat_code";
4610  $sql .= ", tva_tx";
4611  $sql .= ", recuperableonly";
4612  $sql .= ", localtax1_tx";
4613  $sql .= ", localtax1_type";
4614  $sql .= ", localtax2_tx";
4615  $sql .= ", localtax2_type";
4616  $sql .= ", ".$user->id;
4617  $sql .= ", tosell";
4618  $sql .= ", price_by_qty";
4619  $sql .= ", fk_price_expression";
4620  $sql .= ", fk_multicurrency";
4621  $sql .= ", multicurrency_code";
4622  $sql .= ", multicurrency_tx";
4623  $sql .= ", multicurrency_price";
4624  $sql .= ", multicurrency_price_ttc";
4625  $sql .= " FROM ".$this->db->prefix()."product_price";
4626  $sql .= " WHERE fk_product = ".((int) $fromId);
4627  $sql .= " ORDER BY date_price DESC";
4628  if ($conf->global->PRODUIT_MULTIPRICES_LIMIT > 0) {
4629  $sql .= " LIMIT ".$conf->global->PRODUIT_MULTIPRICES_LIMIT;
4630  }
4631 
4632  dol_syslog(__METHOD__, LOG_DEBUG);
4633  $resql = $this->db->query($sql);
4634  if (!$resql) {
4635  $this->db->rollback();
4636  return -1;
4637  }
4638 
4639  $this->db->commit();
4640  return 1;
4641  }
4642 
4643  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4651  public function clone_associations($fromId, $toId)
4652  {
4653  // phpcs:enable
4654  $this->db->begin();
4655 
4656  $sql = 'INSERT INTO '.$this->db->prefix().'product_association (fk_product_pere, fk_product_fils, qty)';
4657  $sql .= " SELECT ".$toId.", fk_product_fils, qty FROM ".$this->db->prefix()."product_association";
4658  $sql .= " WHERE fk_product_pere = ".((int) $fromId);
4659 
4660  dol_syslog(get_class($this).'::clone_association', LOG_DEBUG);
4661  if (!$this->db->query($sql)) {
4662  $this->db->rollback();
4663  return -1;
4664  }
4665 
4666  $this->db->commit();
4667  return 1;
4668  }
4669 
4670  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4678  public function clone_fournisseurs($fromId, $toId)
4679  {
4680  // phpcs:enable
4681  $this->db->begin();
4682 
4683  $now = dol_now();
4684 
4685  // les fournisseurs
4686  /*$sql = "INSERT ".$this->db->prefix()."product_fournisseur ("
4687  . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )"
4688  . " SELECT '".$this->db->idate($now)."', ".$toId.", fk_soc, ref_fourn, fk_user_author"
4689  . " FROM ".$this->db->prefix()."product_fournisseur"
4690  . " WHERE fk_product = ".((int) $fromId);
4691 
4692  if ( ! $this->db->query($sql ) )
4693  {
4694  $this->db->rollback();
4695  return -1;
4696  }*/
4697 
4698  // les prix de fournisseurs.
4699  $sql = "INSERT ".$this->db->prefix()."product_fournisseur_price (";
4700  $sql .= " datec, fk_product, fk_soc, price, quantity, fk_user)";
4701  $sql .= " SELECT '".$this->db->idate($now)."', ".((int) $toId).", fk_soc, price, quantity, fk_user";
4702  $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price";
4703  $sql .= " WHERE fk_product = ".((int) $fromId);
4704 
4705  dol_syslog(get_class($this).'::clone_fournisseurs', LOG_DEBUG);
4706  $resql = $this->db->query($sql);
4707  if (!$resql) {
4708  $this->db->rollback();
4709  return -1;
4710  } else {
4711  $this->db->commit();
4712  return 1;
4713  }
4714  }
4715 
4716  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4729  public function fetch_prod_arbo($prod, $compl_path = '', $multiply = 1, $level = 1, $id_parent = 0, $ignore_stock_load = 0)
4730  {
4731  // phpcs:enable
4732  global $conf, $langs;
4733 
4734  $tmpproduct = null;
4735  //var_dump($prod);
4736  foreach ($prod as $id_product => $desc_pere) { // $id_product is 0 (first call starting with root top) or an id of a sub_product
4737  if (is_array($desc_pere)) { // If desc_pere is an array, this means it's a child
4738  $id = (!empty($desc_pere[0]) ? $desc_pere[0] : '');
4739  $nb = (!empty($desc_pere[1]) ? $desc_pere[1] : '');
4740  $type = (!empty($desc_pere[2]) ? $desc_pere[2] : '');
4741  $label = (!empty($desc_pere[3]) ? $desc_pere[3] : '');
4742  $incdec = (!empty($desc_pere[4]) ? $desc_pere[4] : 0);
4743 
4744  if ($multiply < 1) {
4745  $multiply = 1;
4746  }
4747 
4748  //print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n";
4749  if (is_null($tmpproduct)) {
4750  $tmpproduct = new Product($this->db); // So we initialize tmpproduct only once for all loop.
4751  }
4752  $tmpproduct->fetch($id); // Load product to get ->ref
4753 
4754  if (empty($ignore_stock_load) && ($tmpproduct->isProduct() || !empty($conf->global->STOCK_SUPPORTS_SERVICES))) {
4755  $tmpproduct->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel
4756  }
4757 
4758  $this->res[] = array(
4759  'id'=>$id, // Id product
4760  'id_parent'=>$id_parent,
4761  'ref'=>$tmpproduct->ref, // Ref product
4762  'nb'=>$nb, // Nb of units that compose parent product
4763  'nb_total'=>$nb * $multiply, // Nb of units for all nb of product
4764  'stock'=>$tmpproduct->stock_reel, // Stock
4765  'stock_alert'=>$tmpproduct->seuil_stock_alerte, // Stock alert
4766  'label'=>$label,
4767  'fullpath'=>$compl_path.$label, // Label
4768  'type'=>$type, // Nb of units that compose parent product
4769  'desiredstock'=>$tmpproduct->desiredstock,
4770  'level'=>$level,
4771  'incdec'=>$incdec,
4772  'entity'=>$tmpproduct->entity
4773  );
4774 
4775  // Recursive call if there is childs to child
4776  if (isset($desc_pere['childs']) && is_array($desc_pere['childs'])) {
4777  //print 'YYY We go down for '.$desc_pere[3]." -> \n";
4778  $this->fetch_prod_arbo($desc_pere['childs'], $compl_path.$desc_pere[3]." -> ", $desc_pere[1] * $multiply, $level + 1, $id, $ignore_stock_load);
4779  }
4780  }
4781  }
4782  }
4783 
4784  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
4793  public function get_arbo_each_prod($multiply = 1, $ignore_stock_load = 0)
4794  {
4795  // phpcs:enable
4796  $this->res = array();
4797  if (isset($this->sousprods) && is_array($this->sousprods)) {
4798  foreach ($this->sousprods as $prod_name => $desc_product) {
4799  if (is_array($desc_product)) {
4800  $this->fetch_prod_arbo($desc_product, "", $multiply, 1, $this->id, $ignore_stock_load);
4801  }
4802  }
4803  }
4804  //var_dump($this->res);
4805  return $this->res;
4806  }
4807 
4815  public function hasFatherOrChild($mode = 0)
4816  {
4817  $nb = 0;
4818 
4819  $sql = "SELECT COUNT(pa.rowid) as nb";
4820  $sql .= " FROM ".$this->db->prefix()."product_association as pa";
4821  if ($mode == 0) {
4822  $sql .= " WHERE pa.fk_product_fils = ".((int) $this->id)." OR pa.fk_product_pere = ".((int) $this->id);
4823  } elseif ($mode == -1) {
4824  $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)
4825  } elseif ($mode == 1) {
4826  $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)
4827  }
4828 
4829  $resql = $this->db->query($sql);
4830  if ($resql) {
4831  $obj = $this->db->fetch_object($resql);
4832  if ($obj) {
4833  $nb = $obj->nb;
4834  }
4835  } else {
4836  return -1;
4837  }
4838 
4839  return $nb;
4840  }
4841 
4847  public function hasVariants()
4848  {
4849  $nb = 0;
4850  $sql = "SELECT count(rowid) as nb FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_parent = ".((int) $this->id);
4851  $sql .= " AND entity IN (".getEntity('product').")";
4852 
4853  $resql = $this->db->query($sql);
4854  if ($resql) {
4855  $obj = $this->db->fetch_object($resql);
4856  if ($obj) {
4857  $nb = $obj->nb;
4858  }
4859  }
4860 
4861  return $nb;
4862  }
4863 
4864 
4870  public function isVariant()
4871  {
4872  global $conf;
4873  if (isModEnabled('variants')) {
4874  $sql = "SELECT rowid FROM ".$this->db->prefix()."product_attribute_combination WHERE fk_product_child = ".((int) $this->id)." AND entity IN (".getEntity('product').")";
4875 
4876  $query = $this->db->query($sql);
4877 
4878  if ($query) {
4879  if (!$this->db->num_rows($query)) {
4880  return false;
4881  }
4882  return true;
4883  } else {
4884  dol_print_error($this->db);
4885  return -1;
4886  }
4887  } else {
4888  return false;
4889  }
4890  }
4891 
4898  public function getFather()
4899  {
4900  $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";
4901  $sql .= ", p.tosell as status, p.tobuy as status_buy";
4902  $sql .= " FROM ".$this->db->prefix()."product_association as pa,";
4903  $sql .= " ".$this->db->prefix()."product as p";
4904  $sql .= " WHERE p.rowid = pa.fk_product_pere";
4905  $sql .= " AND pa.fk_product_fils = ".((int) $this->id);
4906 
4907  $res = $this->db->query($sql);
4908  if ($res) {
4909  $prods = array();
4910  while ($record = $this->db->fetch_array($res)) {
4911  // $record['id'] = $record['rowid'] = id of father
4912  $prods[$record['id']]['id'] = $record['rowid'];
4913  $prods[$record['id']]['ref'] = $record['ref'];
4914  $prods[$record['id']]['label'] = $record['label'];
4915  $prods[$record['id']]['qty'] = $record['qty'];
4916  $prods[$record['id']]['incdec'] = $record['incdec'];
4917  $prods[$record['id']]['fk_product_type'] = $record['fk_product_type'];
4918  $prods[$record['id']]['entity'] = $record['entity'];
4919  $prods[$record['id']]['status'] = $record['status'];
4920  $prods[$record['id']]['status_buy'] = $record['status_buy'];
4921  }
4922  return $prods;
4923  } else {
4924  dol_print_error($this->db);
4925  return -1;
4926  }
4927  }
4928 
4929 
4939  public function getChildsArbo($id, $firstlevelonly = 0, $level = 1, $parents = array())
4940  {
4941  global $alreadyfound;
4942 
4943  if (empty($id)) {
4944  return array();
4945  }
4946 
4947  $sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,";
4948  $sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec,";
4949  $sql .= " pa.rowid as fk_association, pa.rang";
4950  $sql .= " FROM ".$this->db->prefix()."product as p,";
4951  $sql .= " ".$this->db->prefix()."product_association as pa";
4952  $sql .= " WHERE p.rowid = pa.fk_product_fils";
4953  $sql .= " AND pa.fk_product_pere = ".((int) $id);
4954  $sql .= " AND pa.fk_product_fils <> ".((int) $id); // This should not happens, it is to avoid infinite loop if it happens
4955  $sql.= " ORDER BY pa.rang";
4956 
4957  dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level. ' parents='.(is_array($parents)?implode(',', $parents):$parents), LOG_DEBUG);
4958 
4959  if ($level == 1) {
4960  $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
4961  }
4962  // Protection against infinite loop
4963  if ($level > 30) {
4964  return array();
4965  }
4966 
4967  $res = $this->db->query($sql);
4968  if ($res) {
4969  $prods = array();
4970  while ($rec = $this->db->fetch_array($res)) {
4971  if (!empty($alreadyfound[$rec['rowid']])) {
4972  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);
4973  if (in_array($rec['id'], $parents)) {
4974  continue; // We discard this child if it is already found at a higher level in tree in the same branch.
4975  }
4976  }
4977  $alreadyfound[$rec['rowid']] = 1;
4978  $prods[$rec['rowid']] = array(
4979  0=>$rec['rowid'],
4980  1=>$rec['qty'],
4981  2=>$rec['fk_product_type'],
4982  3=>$this->db->escape($rec['label']),
4983  4=>$rec['incdec'],
4984  5=>$rec['ref'],
4985  6=>$rec['fk_association'],
4986  7=>$rec['rang']
4987  );
4988  //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']);
4989  //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']);
4990  if (empty($firstlevelonly)) {
4991  $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1, array_push($parents, $rec['rowid']));
4992  foreach ($listofchilds as $keyChild => $valueChild) {
4993  $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild;
4994  }
4995  }
4996  }
4997 
4998  return $prods;
4999  } else {
5000  dol_print_error($this->db);
5001  return -1;
5002  }
5003  }
5004 
5005  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5012  public function get_sousproduits_arbo()
5013  {
5014  // phpcs:enable
5015  $parent = array();
5016 
5017  foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) { // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product
5018  $parent[$this->label][$keyChild] = $valueChild;
5019  }
5020  foreach ($parent as $key => $value) { // key=label, value is array of childs
5021  $this->sousprods[$key] = $value;
5022  }
5023  }
5024 
5031  public function getTooltipContentArray($params)
5032  {
5033  global $conf, $langs;
5034 
5035  $langs->load('products', 'other');
5036 
5037  $datas = array();
5038  $nofetch = !empty($params['nofetch']);
5039 
5040  if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER)) {
5041  return ['optimize' => $langs->trans("ShowProduct")];
5042  }
5043 
5044  if (!empty($this->entity)) {
5045  $tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80, 0, 0, 0, 0, 1);
5046  if ($this->nbphoto > 0) {
5047  $datas['photo'] = '<div class="photointooltip floatright">'."\n" . $tmpphoto . '</div>';
5048  }
5049  }
5050 
5051  if ($this->type == Product::TYPE_PRODUCT) {
5052  $datas['picto'] = img_picto('', 'product').' <u class="paddingrightonly">'.$langs->trans("Product").'</u>';
5053  } elseif ($this->type == Product::TYPE_SERVICE) {
5054  $datas['picto']= img_picto('', 'service').' <u class="paddingrightonly">'.$langs->trans("Service").'</u>';
5055  }
5056  if (isset($this->status) && isset($this->status_buy)) {
5057  $datas['status']= ' '.$this->getLibStatut(5, 0) . ' '.$this->getLibStatut(5, 1);
5058  }
5059 
5060  if (!empty($this->ref)) {
5061  $datas['ref']= '<br><b>'.$langs->trans('ProductRef').':</b> '.$this->ref;
5062  }
5063  if (!empty($this->label)) {
5064  $datas['label']= '<br><b>'.$langs->trans('ProductLabel').':</b> '.$this->label;
5065  }
5066  if ($this->type == Product::TYPE_PRODUCT || !empty($conf->global->STOCK_SUPPORTS_SERVICES)) {
5067  if (isModEnabled('productbatch')) {
5068  $langs->load("productbatch");
5069  $datas['batchstatus']= "<br><b>".$langs->trans("ManageLotSerial").'</b>: '.$this->getLibStatut(0, 2);
5070  }
5071  }
5072  if (isModEnabled('barcode')) {
5073  $datas['barcode']= '<br><b>'.$langs->trans('BarCode').':</b> '.$this->barcode;
5074  }
5075 
5076  if ($this->type == Product::TYPE_PRODUCT) {
5077  if ($this->weight) {
5078  $datas['weight']= "<br><b>".$langs->trans("Weight").'</b>: '.$this->weight.' '.measuringUnitString(0, "weight", $this->weight_units);
5079  }
5080  $labelsize = "";
5081  if ($this->length) {
5082  $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Length").'</b>: '.$this->length.' '.measuringUnitString(0, 'size', $this->length_units);
5083  }
5084  if ($this->width) {
5085  $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Width").'</b>: '.$this->width.' '.measuringUnitString(0, 'size', $this->width_units);
5086  }
5087  if ($this->height) {
5088  $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Height").'</b>: '.$this->height.' '.measuringUnitString(0, 'size', $this->height_units);
5089  }
5090  if ($labelsize) {
5091  $datas['size']= "<br>".$labelsize;
5092  }
5093 
5094  $labelsurfacevolume = "";
5095  if ($this->surface) {
5096  $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Surface").'</b>: '.$this->surface.' '.measuringUnitString(0, 'surface', $this->surface_units);
5097  }
5098  if ($this->volume) {
5099  $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Volume").'</b>: '.$this->volume.' '.measuringUnitString(0, 'volume', $this->volume_units);
5100  }
5101  if ($labelsurfacevolume) {
5102  $datas['surface']= "<br>" . $labelsurfacevolume;
5103  }
5104  }
5105  if (!empty($this->pmp) && $this->pmp) {
5106  $datas['pmp'] = "<br><b>".$langs->trans("PMPValue").'</b>: '.price($this->pmp, 0, '', 1, -1, -1, $conf->currency);
5107  }
5108 
5109  if (isModEnabled('accounting')) {
5110  if ($this->status && isset($this->accountancy_code_sell)) {
5111  include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5112  $selllabel = '<br>';
5113  $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellCode').':</b> '.length_accountg($this->accountancy_code_sell);
5114  $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellIntraCode').':</b> '.length_accountg($this->accountancy_code_sell_intra);
5115  $selllabel .= '<br><b>'.$langs->trans('ProductAccountancySellExportCode').':</b> '.length_accountg($this->accountancy_code_sell_export);
5116  $datas['accountancysell'] = $selllabel;
5117  }
5118  if ($this->status_buy && isset($this->accountancy_code_buy)) {
5119  include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php';
5120  $buylabel = '';
5121  if (empty($this->status)) {
5122  $buylabel .= '<br>';
5123  }
5124  $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyCode').':</b> '.length_accountg($this->accountancy_code_buy);
5125  $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyIntraCode').':</b> '.length_accountg($this->accountancy_code_buy_intra);
5126  $buylabel .= '<br><b>'.$langs->trans('ProductAccountancyBuyExportCode').':</b> '.length_accountg($this->accountancy_code_buy_export);
5127  $datas['accountancybuy'] = $buylabel;
5128  }
5129  }
5130  // show categories for this record only in ajax to not overload lists
5131  if (isModEnabled('categorie') && !$nofetch) {
5132  require_once DOL_DOCUMENT_ROOT . '/categories/class/categorie.class.php';
5133  $form = new Form($this->db);
5134  $datas['categories'] = '<br>' . $form->showCategories($this->id, Categorie::TYPE_PRODUCT, 1);
5135  }
5136 
5137  return $datas;
5138  }
5139 
5153  public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0, $morecss = '', $add_label = 0, $sep = ' - ')
5154  {
5155  global $conf, $langs, $hookmanager;
5156  include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
5157 
5158  $result = '';
5159 
5160  $newref = $this->ref;
5161  if ($maxlength) {
5162  $newref = dol_trunc($newref, $maxlength, 'middle');
5163  }
5164  $params = [
5165  'id' => $this->id,
5166  'objecttype' => $this->element,
5167  'option' => $option,
5168  'nofetch' => 1,
5169  ];
5170  $classfortooltip = 'classfortooltip';
5171  $dataparams = '';
5172  if (getDolGlobalInt('MAIN_ENABLE_AJAX_TOOLTIP')) {
5173  $classfortooltip = 'classforajaxtooltip';
5174  $dataparams = ' data-params="'.dol_escape_htmltag(json_encode($params)).'"';
5175  $label = '';
5176  } else {
5177  $label = implode($this->getTooltipContentArray($params));
5178  }
5179 
5180  $linkclose = '';
5181  if (empty($notooltip)) {
5182  if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER)) {
5183  $label = $langs->trans("ShowProduct");
5184  $linkclose .= ' alt="'.dol_escape_htmltag($label, 1, 1).'"';
5185  }
5186  $linkclose .= ($label ? ' title="'.dol_escape_htmltag($label, 1, 1).'"' : ' title="tocomplete"');
5187  $linkclose .= $dataparams.' class="nowraponall '.$classfortooltip.($morecss ? ' '.$morecss : '').'"';
5188  } else {
5189  $linkclose = ' class="nowraponall'.($morecss ? ' '.$morecss : '').'"';
5190  }
5191 
5192  if ($option == 'supplier' || $option == 'category') {
5193  $url = DOL_URL_ROOT.'/product/fournisseurs.php?id='.$this->id;
5194  } elseif ($option == 'stock') {
5195  $url = DOL_URL_ROOT.'/product/stock/product.php?id='.$this->id;
5196  } elseif ($option == 'composition') {
5197  $url = DOL_URL_ROOT.'/product/composition/card.php?id='.$this->id;
5198  } else {
5199  $url = DOL_URL_ROOT.'/product/card.php?id='.$this->id;
5200  }
5201 
5202  if ($option !== 'nolink') {
5203  // Add param to save lastsearch_values or not
5204  $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
5205  if ($save_lastsearch_value == -1 && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) {
5206  $add_save_lastsearch_values = 1;
5207  }
5208  if ($add_save_lastsearch_values) {
5209  $url .= '&save_lastsearch_values=1';
5210  }
5211  }
5212 
5213  $linkstart = '<a href="'.$url.'"';
5214  $linkstart .= $linkclose.'>';
5215  $linkend = '</a>';
5216 
5217  $result .= $linkstart;
5218  if ($withpicto) {
5219  if ($this->type == Product::TYPE_PRODUCT) {
5220  $result .= (img_object(($notooltip ? '' : $label), 'product', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5221  }
5222  if ($this->type == Product::TYPE_SERVICE) {
5223  $result .= (img_object(($notooltip ? '' : $label), 'service', 'class="paddingright"', 0, 0, $notooltip ? 0 : 1));
5224  }
5225  }
5226  $result .= dol_escape_htmltag($newref);
5227  $result .= $linkend;
5228  if ($withpicto != 2) {
5229  $result .= (($add_label && $this->label) ? $sep.dol_trunc($this->label, ($add_label > 1 ? $add_label : 0)) : '');
5230  }
5231 
5232  global $action;
5233  $hookmanager->initHooks(array('productdao'));
5234  $parameters = array('id'=>$this->id, 'getnomurl' => &$result, 'label' => &$label);
5235  $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks
5236  if ($reshook > 0) {
5237  $result = $hookmanager->resPrint;
5238  } else {
5239  $result .= $hookmanager->resPrint;
5240  }
5241 
5242  return $result;
5243  }
5244 
5245 
5256  public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0)
5257  {
5258  global $conf, $user, $langs;
5259 
5260  $langs->load("products");
5261  $outputlangs->load("products");
5262 
5263  // Positionne le modele sur le nom du modele a utiliser
5264  if (!dol_strlen($modele)) {
5265  $modele = getDolGlobalString('PRODUCT_ADDON_PDF', 'strato');
5266  }
5267 
5268  $modelpath = "core/modules/product/doc/";
5269 
5270  return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref);
5271  }
5272 
5280  public function getLibStatut($mode = 0, $type = 0)
5281  {
5282  switch ($type) {
5283  case 0:
5284  return $this->LibStatut($this->status, $mode, $type);
5285  case 1:
5286  return $this->LibStatut($this->status_buy, $mode, $type);
5287  case 2:
5288  return $this->LibStatut($this->status_batch, $mode, $type);
5289  default:
5290  //Simulate previous behavior but should return an error string
5291  return $this->LibStatut($this->status_buy, $mode, $type);
5292  }
5293  }
5294 
5295  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5304  public function LibStatut($status, $mode = 0, $type = 0)
5305  {
5306  // phpcs:enable
5307  global $conf, $langs;
5308 
5309  $labelStatus = $labelStatusShort = '';
5310 
5311  $langs->load('products');
5312  if (isModEnabled('productbatch')) {
5313  $langs->load("productbatch");
5314  }
5315 
5316  if ($type == 2) {
5317  switch ($mode) {
5318  case 0:
5319  $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatch') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial')));
5320  return dolGetStatus($label);
5321  case 1:
5322  $label = ($status == 0 ? $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort') : ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort')));
5323  return dolGetStatus($label);
5324  case 2:
5325  return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 1, 2);
5326  case 3:
5327  return dolGetStatus($langs->transnoentitiesnoconv('ProductStatusNotOnBatch'), '', '', empty($status) ? 'status5' : 'status4', 3, 'dot');
5328  case 4:
5329  return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 0, 2);
5330  case 5:
5331  return $this->LibStatut($status, 1, 2).' '.$this->LibStatut($status, 3, 2);
5332  default:
5333  return dolGetStatus($langs->transnoentitiesnoconv('Unknown'));
5334  }
5335  }
5336 
5337  $statuttrans = empty($status) ? 'status5' : 'status4';
5338 
5339  if ($status == 0) {
5340  // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
5341  if ($type == 0) {
5342  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnSellShort');
5343  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnSell');
5344  } elseif ($type == 1) {
5345  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBuyShort');
5346  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBuy');
5347  } elseif ($type == 2) {
5348  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusNotOnBatch');
5349  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusNotOnBatchShort');
5350  }
5351  } elseif ($status == 1) {
5352  // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch"
5353  if ($type == 0) {
5354  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSellShort');
5355  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSell');
5356  } elseif ($type == 1) {
5357  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnBuyShort');
5358  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnBuy');
5359  } elseif ($type == 2) {
5360  $labelStatus = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatch') : $langs->transnoentitiesnoconv('ProductStatusOnSerial'));
5361  $labelStatusShort = ($status == 1 ? $langs->transnoentitiesnoconv('ProductStatusOnBatchShort') : $langs->transnoentitiesnoconv('ProductStatusOnSerialShort'));
5362  }
5363  } elseif ( $type == 2 && $status == 2 ) {
5364  $labelStatus = $langs->transnoentitiesnoconv('ProductStatusOnSerial');
5365  $labelStatusShort = $langs->transnoentitiesnoconv('ProductStatusOnSerialShort');
5366  }
5367 
5368  if ($mode > 6) {
5369  return dolGetStatus($langs->transnoentitiesnoconv('Unknown'), '', '', 'status0', 0);
5370  } else {
5371  return dolGetStatus($labelStatus, $labelStatusShort, '', $statuttrans, $mode);
5372  }
5373  }
5374 
5375 
5381  public function getLibFinished()
5382  {
5383  global $langs;
5384  $langs->load('products');
5385 
5386  if (isset($this->finished) && $this->finished >= 0) {
5387  $sql = "SELECT label, code FROM ".$this->db->prefix()."c_product_nature where code = ".((int) $this->finished)." AND active=1";
5388  $resql = $this->db->query($sql);
5389  if ($resql && $this->db->num_rows($resql) > 0) {
5390  $res = $this->db->fetch_array($resql);
5391  $label = $langs->trans($res['label']);
5392  $this->db->free($resql);
5393  return $label;
5394  } else {
5395  $this->error = $this->db->error().' sql='.$sql;
5396  dol_syslog(__METHOD__.' Error '.$this->error, LOG_ERR);
5397  return -1;
5398  }
5399  }
5400 
5401  return '';
5402  }
5403 
5404 
5405  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5422  public function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0, $extrafields = null)
5423  {
5424  // phpcs:enable
5425  if ($id_entrepot) {
5426  $this->db->begin();
5427 
5428  include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
5429 
5430  if ($nbpiece < 0) {
5431  if (!$movement) {
5432  $movement = 1;
5433  }
5434  $nbpiece = abs($nbpiece);
5435  }
5436 
5437  $op[0] = "+".trim($nbpiece);
5438  $op[1] = "-".trim($nbpiece);
5439 
5440  $movementstock = new MouvementStock($this->db);
5441  $movementstock->setOrigin($origin_element, $origin_id); // Set ->origin_type and ->origin_id
5442  $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', '', '', '', false, 0, $disablestockchangeforsubproduct);
5443 
5444  if ($result >= 0) {
5445  if ($extrafields) {
5446  $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
5447  $movementstock->array_options = $array_options;
5448  $movementstock->insertExtraFields();
5449  }
5450  $this->db->commit();
5451  return 1;
5452  } else {
5453  $this->error = $movementstock->error;
5454  $this->errors = $movementstock->errors;
5455 
5456  $this->db->rollback();
5457  return -1;
5458  }
5459  }
5460  }
5461 
5462  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5482  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)
5483  {
5484  // phpcs:enable
5485  if ($id_entrepot) {
5486  $this->db->begin();
5487 
5488  include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php';
5489 
5490  if ($nbpiece < 0) {
5491  if (!$movement) {
5492  $movement = 1;
5493  }
5494  $nbpiece = abs($nbpiece);
5495  }
5496 
5497  $op[0] = "+".trim($nbpiece);
5498  $op[1] = "-".trim($nbpiece);
5499 
5500  $movementstock = new MouvementStock($this->db);
5501  $movementstock->setOrigin($origin_element, $origin_id); // Set ->origin_type and ->fk_origin
5502  $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', $dlc, $dluo, $lot, false, 0, $disablestockchangeforsubproduct);
5503 
5504  if ($result >= 0) {
5505  if ($extrafields) {
5506  $array_options = $extrafields->getOptionalsFromPost('stock_mouvement');
5507  $movementstock->array_options = $array_options;
5508  $movementstock->insertExtraFields();
5509  }
5510  $this->db->commit();
5511  return 1;
5512  } else {
5513  $this->error = $movementstock->error;
5514  $this->errors = $movementstock->errors;
5515 
5516  $this->db->rollback();
5517  return -1;
5518  }
5519  }
5520  }
5521 
5522  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5535  public function load_stock($option = '', $includedraftpoforvirtual = null, $dateofvirtualstock = null)
5536  {
5537  // phpcs:enable
5538  global $conf;
5539 
5540  $this->stock_reel = 0;
5541  $this->stock_warehouse = array();
5542  $this->stock_theorique = 0;
5543 
5544  // Set filter on warehouse status
5545  $warehouseStatus = array();
5546  if (preg_match('/warehouseclosed/', $option)) {
5548  }
5549  if (preg_match('/warehouseopen/', $option)) {
5551  }
5552  if (preg_match('/warehouseinternal/', $option)) {
5553  if (!empty($conf->global->ENTREPOT_EXTRA_STATUS)) {
5555  } else {
5557  }
5558  }
5559 
5560  $sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot";
5561  $sql .= " FROM ".$this->db->prefix()."product_stock as ps";
5562  $sql .= ", ".$this->db->prefix()."entrepot as w";
5563  $sql .= " WHERE w.entity IN (".getEntity('stock').")";
5564  $sql .= " AND w.rowid = ps.fk_entrepot";
5565  $sql .= " AND ps.fk_product = ".((int) $this->id);
5566  if (count($warehouseStatus)) {
5567  $sql .= " AND w.statut IN (".$this->db->sanitize(implode(',', $warehouseStatus)).")";
5568  }
5569 
5570  $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;
5571 
5572  dol_syslog(get_class($this)."::load_stock", LOG_DEBUG);
5573  $result = $this->db->query($sql);
5574  if ($result) {
5575  $num = $this->db->num_rows($result);
5576  $i = 0;
5577  if ($num > 0) {
5578  while ($i < $num) {
5579  $row = $this->db->fetch_object($result);
5580  $this->stock_warehouse[$row->fk_entrepot] = new stdClass();
5581  $this->stock_warehouse[$row->fk_entrepot]->real = $row->reel;
5582  $this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid;
5583  if ((!preg_match('/nobatch/', $option)) && $this->hasbatch()) {
5584  $this->stock_warehouse[$row->fk_entrepot]->detail_batch = Productbatch::findAll($this->db, $row->rowid, 1, $this->id);
5585  }
5586  $this->stock_reel += $row->reel;
5587  $i++;
5588  }
5589  }
5590  $this->db->free($result);
5591 
5592  if (!preg_match('/novirtual/', $option)) {
5593  $this->load_virtual_stock($includedraftpoforvirtual, $dateofvirtualstock); // This load stock_theorique and also load all arrays stats_xxx...
5594  }
5595 
5596  return 1;
5597  } else {
5598  $this->error = $this->db->lasterror();
5599  return -1;
5600  }
5601  }
5602 
5603 
5604  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5614  public function load_virtual_stock($includedraftpoforvirtual = null, $dateofvirtualstock = null)
5615  {
5616  // phpcs:enable
5617  global $conf, $hookmanager, $action;
5618 
5619  $stock_commande_client = 0;
5620  $stock_commande_fournisseur = 0;
5621  $stock_sending_client = 0;
5622  $stock_reception_fournisseur = 0;
5623  $stock_inproduction = 0;
5624 
5625  //dol_syslog("load_virtual_stock");
5626 
5627  if (isModEnabled('commande')) {
5628  $result = $this->load_stats_commande(0, '1,2', 1);
5629  if ($result < 0) {
5630  dol_print_error($this->db, $this->error);
5631  }
5632  $stock_commande_client = $this->stats_commande['qty'];
5633  }
5634  if (isModEnabled("expedition")) {
5635  require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
5636  $filterShipmentStatus = '';
5637  if (!empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT)) {
5638  $filterShipmentStatus = Expedition::STATUS_VALIDATED.','.Expedition::STATUS_CLOSED;
5639  } elseif (!empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE)) {
5640  $filterShipmentStatus = Expedition::STATUS_CLOSED;
5641  }
5642  $result = $this->load_stats_sending(0, '1,2', 1, $filterShipmentStatus);
5643  if ($result < 0) {
5644  dol_print_error($this->db, $this->error);
5645  }
5646  $stock_sending_client = $this->stats_expedition['qty'];
5647  }
5648  if (isModEnabled("supplier_order")) {
5649  $filterStatus = empty($conf->global->SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK) ? '3,4' : $conf->global->SUPPLIER_ORDER_STATUS_FOR_VIRTUAL_STOCK;
5650  if (isset($includedraftpoforvirtual)) {
5651  $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
5652  }
5653  $result = $this->load_stats_commande_fournisseur(0, $filterStatus, 1, $dateofvirtualstock);
5654  if ($result < 0) {
5655  dol_print_error($this->db, $this->error);
5656  }
5657  $stock_commande_fournisseur = $this->stats_commande_fournisseur['qty'];
5658  }
5659  if ((isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) && empty($conf->reception->enabled)) {
5660  // Case module reception is not used
5661  $filterStatus = '4';
5662  if (isset($includedraftpoforvirtual)) {
5663  $filterStatus = '0,'.$filterStatus;
5664  }
5665  $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock);
5666  if ($result < 0) {
5667  dol_print_error($this->db, $this->error);
5668  }
5669  $stock_reception_fournisseur = $this->stats_reception['qty'];
5670  }
5671  if ((isModEnabled("supplier_order") || isModEnabled("supplier_invoice")) && isModEnabled("reception")) {
5672  // Case module reception is used
5673  $filterStatus = '4';
5674  if (isset($includedraftpoforvirtual)) {
5675  $filterStatus = '0,'.$filterStatus;
5676  }
5677  $result = $this->load_stats_reception(0, $filterStatus, 1, $dateofvirtualstock); // Use same tables than when module reception is not used.
5678  if ($result < 0) {
5679  dol_print_error($this->db, $this->error);
5680  }
5681  $stock_reception_fournisseur = $this->stats_reception['qty'];
5682  }
5683  if (isModEnabled('mrp')) {
5684  $result = $this->load_stats_inproduction(0, '1,2', 1, $dateofvirtualstock);
5685  if ($result < 0) {
5686  dol_print_error($this->db, $this->error);
5687  }
5688  $stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty'];
5689  }
5690 
5691  $this->stock_theorique = $this->stock_reel + $stock_inproduction;
5692 
5693  // Stock decrease mode
5694  if (!empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT) || !empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE)) {
5695  $this->stock_theorique -= ($stock_commande_client - $stock_sending_client);
5696  } elseif (!empty($conf->global->STOCK_CALCULATE_ON_VALIDATE_ORDER)) {
5697  $this->stock_theorique += 0;
5698  } elseif (!empty($conf->global->STOCK_CALCULATE_ON_BILL)) {
5699  $this->stock_theorique -= $stock_commande_client;
5700  }
5701  // Stock Increase mode
5702  if (!empty($conf->global->STOCK_CALCULATE_ON_RECEPTION) || !empty($conf->global->STOCK_CALCULATE_ON_RECEPTION_CLOSE)) {
5703  $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
5704  } elseif (!empty($conf->global->STOCK_CALCULATE_ON_SUPPLIER_DISPATCH_ORDER)) {
5705  $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
5706  } elseif (!empty($conf->global->STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER)) {
5707  $this->stock_theorique -= $stock_reception_fournisseur;
5708  } elseif (!empty($conf->global->STOCK_CALCULATE_ON_SUPPLIER_BILL)) {
5709  $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur);
5710  }
5711 
5712  $hookmanager->initHooks(array('productdao'));
5713  $parameters = array('id'=>$this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual);
5714  // Note that $action and $object may have been modified by some hooks
5715  $reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action);
5716  if ($reshook > 0) {
5717  $this->stock_theorique = $hookmanager->resArray['stock_theorique'];
5718  }
5719 
5720  return 1;
5721  }
5722 
5723 
5731  public function loadBatchInfo($batch)
5732  {
5733  $result = array();
5734 
5735  $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";
5736  $sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = ".((int) $this->id)." AND pb.batch = '".$this->db->escape($batch)."'";
5737  $sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby";
5738  dol_syslog(get_class($this)."::loadBatchInfo load first entry found for lot/serial = ".$batch, LOG_DEBUG);
5739  $resql = $this->db->query($sql);
5740  if ($resql) {
5741  $num = $this->db->num_rows($resql);
5742  $i = 0;
5743  while ($i < $num) {
5744  $obj = $this->db->fetch_object($resql);
5745  $result[] = array('batch'=>$batch, 'eatby'=>$this->db->jdate($obj->eatby), 'sellby'=>$this->db->jdate($obj->sellby), 'qty'=>$obj->qty);
5746  $i++;
5747  }
5748  return $result;
5749  } else {
5750  dol_print_error($this->db);
5751  $this->db->rollback();
5752  return array();
5753  }
5754  }
5755 
5756  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5764  public function add_photo($sdir, $file)
5765  {
5766  // phpcs:enable
5767  global $conf;
5768 
5769  include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
5770 
5771  $result = 0;
5772 
5773  $dir = $sdir;
5774  if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
5775  $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos";
5776  } else {
5777  $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product').dol_sanitizeFileName($this->ref);
5778  }
5779 
5780  dol_mkdir($dir);
5781 
5782  $dir_osencoded = $dir;
5783 
5784  if (is_dir($dir_osencoded)) {
5785  $originImage = $dir.'/'.$file['name'];
5786 
5787  // Cree fichier en taille origine
5788  $result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1);
5789 
5790  if (file_exists(dol_osencode($originImage))) {
5791  // Create thumbs
5792  $this->addThumbs($originImage);
5793  }
5794  }
5795 
5796  if (is_numeric($result) && $result > 0) {
5797  return 1;
5798  } else {
5799  return -1;
5800  }
5801  }
5802 
5803  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5810  public function is_photo_available($sdir)
5811  {
5812  // phpcs:enable
5813  include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
5814  include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
5815 
5816  global $conf;
5817 
5818  $dir = $sdir;
5819  if (getDolGlobalInt('PRODUCT_USE_OLD_PATH_FOR_PHOTO')) {
5820  $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos/";
5821  } else {
5822  $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product');
5823  }
5824 
5825  $nbphoto = 0;
5826 
5827  $dir_osencoded = dol_osencode($dir);
5828  if (file_exists($dir_osencoded)) {
5829  $handle = opendir($dir_osencoded);
5830  if (is_resource($handle)) {
5831  while (($file = readdir($handle)) !== false) {
5832  if (!utf8_check($file)) {
5833  $file = utf8_encode($file); // To be sure data is stored in UTF8 in memory
5834  }
5835  if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
5836  return true;
5837  }
5838  }
5839  }
5840  }
5841  return false;
5842  }
5843 
5844  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5852  public function liste_photos($dir, $nbmax = 0)
5853  {
5854  // phpcs:enable
5855  include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
5856  include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
5857 
5858  $nbphoto = 0;
5859  $tabobj = array();
5860 
5861  $dir_osencoded = dol_osencode($dir);
5862  $handle = @opendir($dir_osencoded);
5863  if (is_resource($handle)) {
5864  while (($file = readdir($handle)) !== false) {
5865  if (!utf8_check($file)) {
5866  $file = utf8_encode($file); // readdir returns ISO
5867  }
5868  if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) {
5869  $nbphoto++;
5870 
5871  // We forge name of thumb.
5872  $photo = $file;
5873  $photo_vignette = '';
5874  $regs = array();
5875  if (preg_match('/('.$this->regeximgext.')$/i', $photo, $regs)) {
5876  $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $photo).'_small'.$regs[0];
5877  }
5878 
5879  $dirthumb = $dir.'thumbs/';
5880 
5881  // Objet
5882  $obj = array();
5883  $obj['photo'] = $photo;
5884  if ($photo_vignette && dol_is_file($dirthumb.$photo_vignette)) {
5885  $obj['photo_vignette'] = 'thumbs/'.$photo_vignette;
5886  } else {
5887  $obj['photo_vignette'] = "";
5888  }
5889 
5890  $tabobj[$nbphoto - 1] = $obj;
5891 
5892  // Do we have to continue with next photo ?
5893  if ($nbmax && $nbphoto >= $nbmax) {
5894  break;
5895  }
5896  }
5897  }
5898 
5899  closedir($handle);
5900  }
5901 
5902  return $tabobj;
5903  }
5904 
5905  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5912  public function delete_photo($file)
5913  {
5914  // phpcs:enable
5915  include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
5916  include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
5917 
5918  $dir = dirname($file).'/'; // Chemin du dossier contenant l'image d'origine
5919  $dirthumb = $dir.'/thumbs/'; // Chemin du dossier contenant la vignette
5920  $filename = preg_replace('/'.preg_quote($dir, '/').'/i', '', $file); // Nom du fichier
5921 
5922  // On efface l'image d'origine
5923  dol_delete_file($file, 0, 0, 0, $this); // For triggers
5924 
5925  // Si elle existe, on efface la vignette
5926  if (preg_match('/('.$this->regeximgext.')$/i', $filename, $regs)) {
5927  $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_small'.$regs[0];
5928  if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
5929  dol_delete_file($dirthumb.$photo_vignette);
5930  }
5931 
5932  $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_mini'.$regs[0];
5933  if (file_exists(dol_osencode($dirthumb.$photo_vignette))) {
5934  dol_delete_file($dirthumb.$photo_vignette);
5935  }
5936  }
5937  }
5938 
5939  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5946  public function get_image_size($file)
5947  {
5948  // phpcs:enable
5949  $file_osencoded = dol_osencode($file);
5950  $infoImg = getimagesize($file_osencoded); // Get information on image
5951  $this->imgWidth = $infoImg[0]; // Largeur de l'image
5952  $this->imgHeight = $infoImg[1]; // Hauteur de l'image
5953  }
5954 
5955  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
5961  public function load_state_board()
5962  {
5963  // phpcs:enable
5964  global $hookmanager;
5965 
5966  $this->nb = array();
5967 
5968  $sql = "SELECT count(p.rowid) as nb, fk_product_type";
5969  $sql .= " FROM ".$this->db->prefix()."product as p";
5970  $sql .= ' WHERE p.entity IN ('.getEntity($this->element, 1).')';
5971  // Add where from hooks
5972  if (is_object($hookmanager)) {
5973  $parameters = array();
5974  $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters, $this); // Note that $action and $object may have been modified by hook
5975  $sql .= $hookmanager->resPrint;
5976  }
5977  $sql .= ' GROUP BY fk_product_type';
5978 
5979  $resql = $this->db->query($sql);
5980  if ($resql) {
5981  while ($obj = $this->db->fetch_object($resql)) {
5982  if ($obj->fk_product_type == 1) {
5983  $this->nb["services"] = $obj->nb;
5984  } else {
5985  $this->nb["products"] = $obj->nb;
5986  }
5987  }
5988  $this->db->free($resql);
5989  return 1;
5990  } else {
5991  dol_print_error($this->db);
5992  $this->error = $this->db->error();
5993  return -1;
5994  }
5995  }
5996 
6002  public function isProduct()
6003  {
6004  return ($this->type == Product::TYPE_PRODUCT ? true : false);
6005  }
6006 
6012  public function isService()
6013  {
6014  return ($this->type == Product::TYPE_SERVICE ? true : false);
6015  }
6016 
6017 
6023  public function isMandatoryPeriod()
6024  {
6025  return ($this->mandatory_period == 1 ? true : false);
6026  }
6027  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6036  public function get_barcode($object, $type = '')
6037  {
6038  // phpcs:enable
6039  global $conf;
6040 
6041  $result = '';
6042  if (!empty($conf->global->BARCODE_PRODUCT_ADDON_NUM)) {
6043  $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']);
6044  foreach ($dirsociete as $dirroot) {
6045  $res = dol_include_once($dirroot.$conf->global->BARCODE_PRODUCT_ADDON_NUM.'.php');
6046  if ($res) {
6047  break;
6048  }
6049  }
6050  $var = $conf->global->BARCODE_PRODUCT_ADDON_NUM;
6051  $mod = new $var;
6052 
6053  $result = $mod->getNextValue($object, $type);
6054 
6055  dol_syslog(get_class($this)."::get_barcode barcode=".$result." module=".$var);
6056  }
6057  return $result;
6058  }
6059 
6067  public function initAsSpecimen()
6068  {
6069  global $user, $langs, $conf, $mysoc;
6070 
6071  $now = dol_now();
6072 
6073  // Initialize parameters
6074  $this->specimen = 1;
6075  $this->id = 0;
6076  $this->ref = 'PRODUCT_SPEC';
6077  $this->label = 'PRODUCT SPECIMEN';
6078  $this->description = 'This is description of this product specimen that was created the '.dol_print_date($now, 'dayhourlog').'.';
6079  $this->specimen = 1;
6080  $this->country_id = 1;
6081  $this->status = 1;
6082  $this->status_buy = 1;
6083  $this->tobatch = 0;
6084  $this->note_private = 'This is a comment (private)';
6085  $this->note_public = 'This is a comment (public)';
6086  $this->date_creation = $now;
6087  $this->date_modification = $now;
6088 
6089  $this->weight = 4;
6090  $this->weight_units = 3;
6091 
6092  $this->length = 5;
6093  $this->length_units = 1;
6094  $this->width = 6;
6095  $this->width_units = 0;
6096  $this->height = null;
6097  $this->height_units = null;
6098 
6099  $this->surface = 30;
6100  $this->surface_units = 0;
6101  $this->volume = 300;
6102  $this->volume_units = 0;
6103 
6104  $this->barcode = -1; // Create barcode automatically
6105  }
6106 
6113  public function getLabelOfUnit($type = 'long')
6114  {
6115  global $langs;
6116 
6117  if (!$this->fk_unit) {
6118  return '';
6119  }
6120 
6121  $langs->load('products');
6122 
6123  $label_type = 'label';
6124  if ($type == 'short') {
6125  $label_type = 'short_label';
6126  }
6127 
6128  $sql = "SELECT ".$label_type.", code from ".$this->db->prefix()."c_units where rowid = ".((int) $this->fk_unit);
6129 
6130  $resql = $this->db->query($sql);
6131  if ($resql && $this->db->num_rows($resql) > 0) {
6132  $res = $this->db->fetch_array($resql);
6133  $label = ($label_type == 'short_label' ? $res[$label_type] : 'unit'.$res['code']);
6134  $this->db->free($resql);
6135  return $label;
6136  } else {
6137  $this->error = $this->db->error();
6138  dol_syslog(get_class($this)."::getLabelOfUnit Error ".$this->error, LOG_ERR);
6139  return -1;
6140  }
6141  }
6142 
6148  public function hasbatch()
6149  {
6150  return ($this->status_batch > 0 ? true : false);
6151  }
6152 
6153 
6154  // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
6160  public function min_recommended_price()
6161  {
6162  // phpcs:enable
6163  global $conf;
6164 
6165  $maxpricesupplier = 0;
6166 
6167  if (!empty($conf->global->PRODUCT_MINIMUM_RECOMMENDED_PRICE)) {
6168  include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
6169  $product_fourn = new ProductFournisseur($this->db);
6170  $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', '');
6171 
6172  if (is_array($product_fourn_list) && count($product_fourn_list) > 0) {
6173  foreach ($product_fourn_list as $productfourn) {
6174  if ($productfourn->fourn_unitprice > $maxpricesupplier) {
6175  $maxpricesupplier = $productfourn->fourn_unitprice;
6176  }
6177  }
6178 
6179  $maxpricesupplier *= $conf->global->PRODUCT_MINIMUM_RECOMMENDED_PRICE;
6180  }
6181  }
6182 
6183  return $maxpricesupplier;
6184  }
6185 
6186 
6197  public function setCategories($categories)
6198  {
6199  require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
6200  return parent::setCategoriesCommon($categories, Categorie::TYPE_PRODUCT);
6201  }
6202 
6211  public static function replaceThirdparty(DoliDB $dbs, $origin_id, $dest_id)
6212  {
6213  $tables = array(
6214  'product_customer_price',
6215  'product_customer_price_log'
6216  );
6217 
6218  return CommonObject::commonReplaceThirdparty($dbs, $origin_id, $dest_id, $tables);
6219  }
6220 
6232  public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq)
6233  {
6234  global $conf, $db;
6235 
6236  $sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM ".$this->db->prefix()."product_pricerules";
6237  $query = $this->db->query($sql);
6238 
6239  $rules = array();
6240 
6241  while ($result = $this->db->fetch_object($query)) {
6242  $rules[$result->level] = $result;
6243  }
6244 
6245  //Because prices can be based on other level's prices, we temporarily store them
6246  $prices = array(
6247  1 => $baseprice
6248  );
6249 
6250  for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) {
6251  $price = $baseprice;
6252  $price_min = $baseprice;
6253 
6254  //We have to make sure it does exist and it is > 0
6255  //First price level only allows changing min_price
6256  if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) {
6257  $price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent / 100));
6258  }
6259 
6260  $prices[$i] = $price;
6261 
6262  //We have to make sure it does exist and it is > 0
6263  if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) {
6264  $price_min = $price * (1 - ($rules[$i]->var_min_percent / 100));
6265  }
6266 
6267  //Little check to make sure the price is modified before triggering generation
6268  $check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i]));
6269  $check_type = ($baseprice == $this->multiprices_base_type[$i]);
6270 
6271  if ($check_amount && $check_type) {
6272  continue;
6273  }
6274 
6275  if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, true) < 0) {
6276  return -1;
6277  }
6278  }
6279 
6280  return 1;
6281  }
6282 
6288  public function getRights()
6289  {
6290  global $user;
6291 
6292  if ($this->isProduct()) {
6293  return $user->rights->produit;
6294  } else {
6295  return $user->rights->service;
6296  }
6297  }
6298 
6305  public function info($id)
6306  {
6307  $sql = "SELECT p.rowid, p.ref, p.datec as date_creation, p.tms as date_modification,";
6308  $sql .= " p.fk_user_author, p.fk_user_modif";
6309  $sql .= " FROM ".$this->db->prefix().$this->table_element." as p";
6310  $sql .= " WHERE p.rowid = ".((int) $id);
6311 
6312  $result = $this->db->query($sql);
6313  if ($result) {
6314  if ($this->db->num_rows($result)) {
6315  $obj = $this->db->fetch_object($result);
6316 
6317  $this->id = $obj->rowid;
6318 
6319  if ($obj->fk_user_author) {
6320  $cuser = new User($this->db);
6321  $cuser->fetch($obj->fk_user_author);
6322  $this->user_creation = $cuser;
6323  }
6324 
6325  if ($obj->fk_user_modif) {
6326  $muser = new User($this->db);
6327  $muser->fetch($obj->fk_user_modif);
6328  $this->user_modification = $muser;
6329  }
6330 
6331  $this->ref = $obj->ref;
6332  $this->date_creation = $this->db->jdate($obj->date_creation);
6333  $this->date_modification = $this->db->jdate($obj->date_modification);
6334  }
6335 
6336  $this->db->free($result);
6337  } else {
6338  dol_print_error($this->db);
6339  }
6340  }
6341 
6342 
6347  public function getProductDurationHours()
6348  {
6349  global $langs;
6350 
6351  if (empty($this->duration_value)) {
6352  $this->errors[]='ErrorDurationForServiceNotDefinedCantCalculateHourlyPrice';
6353  return -1;
6354  }
6355 
6356  if ($this->duration_unit == 'i') {
6357  $prodDurationHours = 1. / 60;
6358  }
6359  if ($this->duration_unit == 'h') {
6360  $prodDurationHours = 1.;
6361  }
6362  if ($this->duration_unit == 'd') {
6363  $prodDurationHours = 24.;
6364  }
6365  if ($this->duration_unit == 'w') {
6366  $prodDurationHours = 24. * 7;
6367  }
6368  if ($this->duration_unit == 'm') {
6369  $prodDurationHours = 24. * 30;
6370  }
6371  if ($this->duration_unit == 'y') {
6372  $prodDurationHours = 24. * 365;
6373  }
6374  $prodDurationHours *= $this->duration_value;
6375 
6376  return $prodDurationHours;
6377  }
6378 
6379 
6387  public function getKanbanView($option = '', $arraydata = null)
6388  {
6389  global $langs,$conf;
6390 
6391  $selected = (empty($arraydata['selected']) ? 0 : $arraydata['selected']);
6392 
6393  $return = '<div class="box-flex-item box-flex-grow-zero">';
6394  $return .= '<div class="info-box info-box-sm">';
6395  $return .= '<div class="info-box-img">';
6396  $label = '';
6397  if ($this->is_photo_available($conf->product->multidir_output[$this->entity])) {
6398  $label .= $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 120, 160, 0, 0, 0, '', 'photoref photokanban');
6399  $return .= $label;
6400  } else {
6401  if ($this->type == Product::TYPE_PRODUCT) {
6402  $label .= img_picto('', 'product');
6403  } elseif ($this->type == Product::TYPE_SERVICE) {
6404  $label .= img_picto('', 'service');
6405  }
6406  $return .= $label;
6407  }
6408  $return .= '</div>';
6409  $return .= '<div class="info-box-content">';
6410  $return .= '<span class="info-box-ref inline-block tdoverflowmax150 valignmiddle">'.(method_exists($this, 'getNomUrl') ? $this->getNomUrl() : $this->ref).'</span>';
6411  $return .= '<input id="cb'.$this->id.'" class="flat checkforselect fright" type="checkbox" name="toselect[]" value="'.$this->id.'"'.($selected ? ' checked="checked"' : '').'>';
6412  if (property_exists($this, 'label')) {
6413  $return .= '<br><span class="info-box-label opacitymedium">'.$this->label.'</span>';
6414  }
6415  if (property_exists($this, 'price') && property_exists($this, 'price_ttc')) {
6416  if ($this->price_base_type == 'TTC') {
6417  $return .= '<br><span class="info-box-status amount">'.price($this->price_ttc).' '.$langs->trans("TTC").'</span>';
6418  } else {
6419  if ($this->status) {
6420  $return .= '<br><span class="info-box-status amount">'.price($this->price).' '.$langs->trans("HT").'</span>';
6421  }
6422  }
6423  }
6424  if (property_exists($this, 'stock_reel')) {
6425  $return .= '<br><span class="info-box-status opacitymedium">'.$langs->trans('PhysicalStock').' : <span class="bold">'.$this->stock_reel.'</span></span>';
6426  }
6427  if (method_exists($this, 'getLibStatut')) {
6428  $return .='<br><span class="info-box-status margintoponly">'.$this->getLibStatut(3, 1).' '.$this->getLibStatut(3, 0).'</span>';
6429  }
6430  $return .= '</div>';
6431  $return .= '</div>';
6432  $return .= '</div>';
6433  return $return;
6434  }
6435 }
6436 
6442 {
6443  public $picto = 'service';
6444 }
Facture\STATUS_DRAFT
const STATUS_DRAFT
Draft status.
Definition: facture.class.php:430
Productbatch\findAll
static findAll($dbs, $fk_product_stock, $with_qty=0, $fk_product=0)
Return all batch detail records for a given product and warehouse.
Definition: productbatch.class.php:437
Product\get_nb_ordersupplier
get_nb_ordersupplier($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units in orders in which product is included.
Definition: product.class.php:4089
Product\$localtax1_tx
$localtax1_tx
Other local taxes.
Definition: product.class.php:203
Product\setCategories
setCategories($categories)
Sets object to supplied categories.
Definition: product.class.php:6197
Product\getLibFinished
getLibFinished()
Retour label of nature of product.
Definition: product.class.php:5381
Product\isProduct
isProduct()
Return if object is a product.
Definition: product.class.php:6002
dol_string_nospecial
dol_string_nospecial($str, $newstr='_', $badcharstoreplace='', $badcharstoremove='', $keepspaces=0)
Clean a string from all punctuation characters to use it as a ref or login.
Definition: functions.lib.php:1495
Productbatch
Manage record for batch number management.
Definition: productbatch.class.php:32
Product\load_stats_facture_fournisseur
load_stats_facture_fournisseur($socid=0)
Charge tableau des stats facture pour le produit/service.
Definition: product.class.php:3692
dol_escape_htmltag
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0)
Returns text escaped for inclusion in HTML alt or title or value tags, or into values of HTML input f...
Definition: functions.lib.php:1594
Product\$ref_fourn
$ref_fourn
Definition: product.class.php:453
dol_trunc
dol_trunc($string, $size=40, $trunc='right', $stringencoding='UTF-8', $nodot=0, $display=0)
Truncate a string to a particular length adding '…' if string larger than length.
Definition: functions.lib.php:4049
Product\update
update($id, $user, $notrigger=false, $action='update', $updatetype=false)
Update a record into database.
Definition: product.class.php:1009
dol_sanitizeFileName
dol_sanitizeFileName($str, $newstr='_', $unaccent=1)
Clean a string to use it as a file name.
Definition: functions.lib.php:1322
description
print *****$script_file(".$version.") pid cd cd cd description as description
Definition: email_expire_services_to_customers.php:83
Product\load_stats_bom
load_stats_bom($socid=0)
Charge tableau des stats OF pour le produit/service.
Definition: product.class.php:2842
Product\get_nb_order
get_nb_order($socid, $mode, $filteronproducttype=-1, $year=0, $morefilter='')
Return nb of units in orders in which product is included.
Definition: product.class.php:4037
Product\load_stock
load_stock($option='', $includedraftpoforvirtual=null, $dateofvirtualstock=null)
Load information about stock of a product into ->stock_reel, ->stock_warehouse[] (including stock_war...
Definition: product.class.php:5535
dol_delete_dir_recursive
dol_delete_dir_recursive($dir, $count=0, $nophperrors=0, $onlysub=0, &$countdeleted=0, $indexdatabase=1, $nolog=0)
Remove a directory $dir and its subdirectories (or only files and subdirectories)
Definition: files.lib.php:1483
Product\clone_associations
clone_associations($fromId, $toId)
Clone links between products.
Definition: product.class.php:4651
Product\$desiredstock
$desiredstock
Ask for replenishment when $desiredstock < $stock_reel.
Definition: product.class.php:259
ProductCombination
Class ProductCombination Used to represent a product combination.
Definition: ProductCombination.class.php:24
DoliDB
Class to manage Dolibarr database access.
Definition: DoliDB.class.php:30
Product\add_sousproduit
add_sousproduit($id_pere, $id_fils, $qty, $incdec=1)
Link a product/service to a parent product/service.
Definition: product.class.php:4248
Productcustomerprice
File of class to manage predefined price products or services by customer.
Definition: productcustomerprice.class.php:29
dol_osencode
dol_osencode($str)
Return a string encoded into OS filesystem encoding.
Definition: functions.lib.php:9026
ProductFournisseur
Class to manage predefined suppliers products.
Definition: fournisseur.product.class.php:40
Product\getSellPrice
getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp=0)
Return price of sell of a product for a seller/buyer/product.
Definition: product.class.php:1878
Product\isService
isService()
Return if object is a product.
Definition: product.class.php:6012
Product\hasbatch
hasbatch()
Return if object has a sell-by date or eat-by date.
Definition: product.class.php:6148
Product\load_stats_commande_fournisseur
load_stats_commande_fournisseur($socid=0, $filtrestatut='', $forVirtualStock=0, $dateofvirtualstock=null)
Charge tableau des stats commande fournisseur pour le produit/service.
Definition: product.class.php:3165
Product\fetch_prod_arbo
fetch_prod_arbo($prod, $compl_path='', $multiply=1, $level=1, $id_parent=0, $ignore_stock_load=0)
Fonction recursive uniquement utilisee par get_arbo_each_prod, recompose l'arborescence des sousprodu...
Definition: product.class.php:4729
Product\$multiprices
$multiprices
Arrays for multiprices.
Definition: product.class.php:173
dol_print_error
dol_print_error($db='', $error='', $errors=null)
Displays error message system with all the information to facilitate the diagnosis and the escalation...
Definition: functions.lib.php:5096
Product\isMandatoryPeriod
isMandatoryPeriod()
Return if object have a constraint on mandatory_period.
Definition: product.class.php:6023
Product\verify
verify()
Check properties of product are ok (like name, barcode, ...).
Definition: product.class.php:921
Product\getProductDurationHours
getProductDurationHours()
Return the duration in Hours of a service base on duration fields.
Definition: product.class.php:6347
Product\check_barcode
check_barcode($valuetotest, $typefortest)
Check barcode.
Definition: product.class.php:973
Product\get_barcode
get_barcode($object, $type='')
Get a barcode from the module to generate barcode values.
Definition: product.class.php:6036
Entrepot\STATUS_OPEN_INTERNAL
const STATUS_OPEN_INTERNAL
Warehouse open and only operations for stock transfers/corrections allowed (not for customer shipping...
Definition: entrepot.class.php:160
measuring_units_squared
measuring_units_squared($unit)
Transform a given unit scale into the square of that unit, if known.
Definition: product.lib.php:865
Product\load_stats_contrat
load_stats_contrat($socid=0)
Charge tableau des stats contrat pour le produit/service.
Definition: product.class.php:3469
dol_include_once
if(!function_exists('dol_getprefix')) dol_include_once($relpath, $classname='')
Make an include_once using default root and alternate root if it fails.
Definition: functions.lib.php:1128
Product\$libelle
$libelle
Definition: product.class.php:104
Product\getFather
getFather()
Return all parent products for current product (first level only)
Definition: product.class.php:4898
CommonObject\addThumbs
addThumbs($file)
Build thumb.
Definition: commonobject.class.php:5633
Product\add_fournisseur
add_fournisseur($user, $id_fourn, $ref_fourn, $quantity)
Add a supplier price for the product.
Definition: product.class.php:4434
Product\clone_fournisse