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-2020 Alexandre Spangaro <aspangaro@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-2020 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 33/** 34 * \file htdocs/product/class/product.class.php 35 * \ingroup produit 36 * \brief File of class to manage predefined products or services 37 */ 38require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php'; 39require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php'; 40require_once DOL_DOCUMENT_ROOT.'/product/class/productbatch.class.php'; 41require_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php'; 42 43/** 44 * Class to manage products or services 45 */ 46class Product extends CommonObject 47{ 48 /** 49 * @var string ID to identify managed object 50 */ 51 public $element = 'product'; 52 53 /** 54 * @var string Name of table without prefix where object is stored 55 */ 56 public $table_element = 'product'; 57 58 /** 59 * @var string Field with ID of parent key if this field has a parent 60 */ 61 public $fk_element = 'fk_product'; 62 63 /** 64 * @var array List of child tables. To test if we can delete object. 65 */ 66 protected $childtables = array( 67 'supplier_proposaldet', 68 'propaldet', 69 'commandedet', 70 'facturedet', 71 'contratdet', 72 'facture_fourn_det', 73 'commande_fournisseurdet' 74 ); 75 76 /** 77 * 0=No test on entity, 1=Test with field entity, 2=Test with link by societe 78 * 79 * @var int 80 */ 81 public $ismultientitymanaged = 1; 82 83 /** 84 * @var string picto 85 */ 86 public $picto = 'product'; 87 88 /** 89 * {@inheritdoc} 90 */ 91 protected $table_ref_field = 'ref'; 92 93 public $regeximgext = '\.gif|\.jpg|\.jpeg|\.png|\.bmp|\.webp|\.xpm|\.xbm'; // See also into images.lib.php 94 95 /* 96 * @deprecated 97 * @see label 98 */ 99 public $libelle; 100 101 /** 102 * Product label 103 * 104 * @var string 105 */ 106 public $label; 107 108 /** 109 * Product description 110 * 111 * @var string 112 */ 113 public $description; 114 115 /** 116 * Product other fields PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION 117 * 118 * @var string 119 */ 120 public $other; 121 122 /** 123 * Check TYPE constants 124 * 125 * @var int 126 */ 127 public $type = self::TYPE_PRODUCT; 128 129 /** 130 * Selling price 131 * 132 * @var float 133 */ 134 public $price; // Price net 135 136 /** 137 * Price with tax 138 * 139 * @var float 140 */ 141 public $price_ttc; 142 143 /** 144 * Minimum price net 145 * 146 * @var float 147 */ 148 public $price_min; 149 150 /** 151 * Minimum price with tax 152 * 153 * @var float 154 */ 155 public $price_min_ttc; 156 157 /** 158 * Base price ('TTC' for price including tax or 'HT' for net price) 159 * @var string 160 */ 161 public $price_base_type; 162 163 //! Arrays for multiprices 164 public $multiprices = array(); 165 public $multiprices_ttc = array(); 166 public $multiprices_base_type = array(); 167 public $multiprices_min = array(); 168 public $multiprices_min_ttc = array(); 169 public $multiprices_tva_tx = array(); 170 public $multiprices_recuperableonly = array(); 171 172 //! Price by quantity arrays 173 public $price_by_qty; 174 public $prices_by_qty = array(); 175 public $prices_by_qty_id = array(); 176 public $prices_by_qty_list = array(); 177 178 //! Default VAT code for product (link to code into llx_c_tva but without foreign keys) 179 public $default_vat_code; 180 181 //! Default VAT rate of product 182 public $tva_tx; 183 184 //! French VAT NPR (0 or 1) 185 public $tva_npr = 0; 186 187 //! Other local taxes 188 public $localtax1_tx; 189 public $localtax2_tx; 190 public $localtax1_type; 191 public $localtax2_type; 192 193 /** 194 * Stock real 195 * 196 * @var int 197 */ 198 public $stock_reel = 0; 199 200 /** 201 * Stock virtual 202 * 203 * @var int 204 */ 205 public $stock_theorique; 206 207 /** 208 * Cost price 209 * 210 * @var float 211 */ 212 public $cost_price; 213 214 //! Average price value for product entry into stock (PMP) 215 public $pmp; 216 217 /** 218 * Stock alert 219 * 220 * @var float 221 */ 222 public $seuil_stock_alerte = 0; 223 224 /** 225 * Ask for replenishment when $desiredstock < $stock_reel 226 */ 227 public $desiredstock = 0; 228 229 /* 230 * Service expiration 231 */ 232 public $duration_value; 233 234 /** 235 * Exoiration unit 236 */ 237 public $duration_unit; 238 239 /** 240 * Status indicates whether the product is on sale '1' or not '0' 241 * 242 * @var int 243 */ 244 public $status = 0; 245 246 /** 247 * Status indicate whether the product is available for purchase '1' or not '0' 248 * 249 * @var int 250 */ 251 public $status_buy = 0; 252 253 /** 254 * Status indicates whether the product is a finished product '1' or a raw material '0' 255 * 256 * @var int 257 */ 258 public $finished; 259 260 /** 261 * We must manage lot/batch number, sell-by date and so on : '1':yes '0':no 262 * 263 * @var int 264 */ 265 public $status_batch = 0; 266 267 /** 268 * Customs code 269 * 270 * @var string 271 */ 272 public $customcode; 273 274 /** 275 * Product URL 276 * 277 * @var string 278 */ 279 public $url; 280 281 //! Metric of products 282 public $weight; 283 public $weight_units; // scale -3, 0, 3, 6 284 public $length; 285 public $length_units; // scale -3, 0, 3, 6 286 public $width; 287 public $width_units; // scale -3, 0, 3, 6 288 public $height; 289 public $height_units; // scale -3, 0, 3, 6 290 public $surface; 291 public $surface_units; // scale -3, 0, 3, 6 292 public $volume; 293 public $volume_units; // scale -3, 0, 3, 6 294 295 public $net_measure; 296 public $net_measure_units; // scale -3, 0, 3, 6 297 298 public $accountancy_code_sell; 299 public $accountancy_code_sell_intra; 300 public $accountancy_code_sell_export; 301 public $accountancy_code_buy; 302 public $accountancy_code_buy_intra; 303 public $accountancy_code_buy_export; 304 305 /** 306 * Main Barcode value 307 * 308 * @var string 309 */ 310 public $barcode; 311 312 /** 313 * Main Barcode type ID 314 * 315 * @var int 316 */ 317 public $barcode_type; 318 319 /** 320 * Main Barcode type code 321 * 322 * @var string 323 */ 324 public $barcode_type_code; 325 326 /** 327 * Additional barcodes (Some products have different barcodes according to the country of origin of manufacture) 328 * 329 * @var array 330 */ 331 public $barcodes_extra = array(); 332 333 public $stats_propale = array(); 334 public $stats_commande = array(); 335 public $stats_contrat = array(); 336 public $stats_facture = array(); 337 public $stats_commande_fournisseur = array(); 338 public $stats_reception = array(); 339 public $stats_mrptoconsume = array(); 340 public $stats_mrptoproduce = array(); 341 342 public $multilangs = array(); 343 344 //! Size of image 345 public $imgWidth; 346 public $imgHeight; 347 348 /** 349 * @var integer|string date_creation 350 */ 351 public $date_creation; 352 353 /** 354 * @var integer|string date_modification 355 */ 356 public $date_modification; 357 358 //! Id du fournisseur 359 public $product_fourn_id; 360 361 //! Product ID already linked to a reference supplier 362 public $product_id_already_linked; 363 364 public $nbphoto = 0; 365 366 //! Contains detail of stock of product into each warehouse 367 public $stock_warehouse = array(); 368 369 public $oldcopy; 370 371 public $fk_default_warehouse; 372 /** 373 * @var int ID 374 */ 375 public $fk_price_expression; 376 377 /* To store supplier price found */ 378 public $fourn_pu; 379 public $fourn_price_base_type; 380 public $fourn_socid; 381 382 /** 383 * @deprecated 384 * @see $ref_supplier 385 */ 386 public $ref_fourn; 387 public $ref_supplier; 388 389 /** 390 * Unit code ('km', 'm', 'l', 'p', ...) 391 * 392 * @var string 393 */ 394 public $fk_unit; 395 396 /** 397 * Price is generated using multiprice rules 398 * 399 * @var int 400 */ 401 public $price_autogen = 0; 402 403 /** 404 * Array with list of supplier prices of product 405 * 406 * @var array 407 */ 408 public $supplierprices; 409 410 /** 411 * @var array fields of object product 412 */ 413 public $fields = array( 414 'rowid' => array('type'=>'integer', 'label'=>'TechnicalID', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'index'=>1, 'position'=>1, 'comment'=>'Id'), 415 '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'), 416 'barcode' =>array('type'=>'varchar(255)', 'label'=>'Barcode', 'enabled'=>'!empty($conf->barcode->enabled)', 'visible'=>-1, 'showoncombobox'=>1), 417 'entity' =>array('type'=>'integer', 'label'=>'Entity', 'enabled'=>1, 'visible'=>0, 'default'=>1, 'notnull'=>1, 'index'=>1, 'position'=>20), 418 'label' =>array('type'=>'varchar(255)', 'label'=>'Label', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'showoncombobox'=>1), 419 'note_public' =>array('type'=>'html', 'label'=>'NotePublic', 'enabled'=>1, 'visible'=>0, 'position'=>61), 420 'note' =>array('type'=>'html', 'label'=>'NotePrivate', 'enabled'=>1, 'visible'=>0, 'position'=>62), 421 'datec' =>array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'position'=>500), 422 'tms' =>array('type'=>'timestamp', 'label'=>'DateModification', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'position'=>501), 423 //'date_valid' =>array('type'=>'datetime', 'label'=>'DateCreation', 'enabled'=>1, 'visible'=>-2, 'position'=>502), 424 'fk_user_author'=>array('type'=>'integer', 'label'=>'UserAuthor', 'enabled'=>1, 'visible'=>-2, 'notnull'=>1, 'position'=>510, 'foreignkey'=>'llx_user.rowid'), 425 'fk_user_modif' =>array('type'=>'integer', 'label'=>'UserModif', 'enabled'=>1, 'visible'=>-2, 'notnull'=>-1, 'position'=>511), 426 //'fk_user_valid' =>array('type'=>'integer', 'label'=>'UserValidation', 'enabled'=>1, 'visible'=>-1, 'position'=>512), 427 'import_key' =>array('type'=>'varchar(14)', 'label'=>'ImportId', 'enabled'=>1, 'visible'=>-2, 'notnull'=>-1, 'index'=>0, 'position'=>1000), 428 //'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')), 429 //'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')), 430 ); 431 432 /** 433 * Regular product 434 */ 435 const TYPE_PRODUCT = 0; 436 /** 437 * Service 438 */ 439 const TYPE_SERVICE = 1; 440 /** 441 * Advanced feature: assembly kit 442 */ 443 const TYPE_ASSEMBLYKIT = 2; 444 /** 445 * Advanced feature: stock kit 446 */ 447 const TYPE_STOCKKIT = 3; 448 449 450 /** 451 * Constructor 452 * 453 * @param DoliDB $db Database handler 454 */ 455 public function __construct($db) 456 { 457 $this->db = $db; 458 $this->canvas = ''; 459 } 460 461 /** 462 * Check that ref and label are ok 463 * 464 * @return int >1 if OK, <=0 if KO 465 */ 466 public function check() 467 { 468 $this->ref = dol_sanitizeFileName(stripslashes($this->ref)); 469 470 $err = 0; 471 if (dol_strlen(trim($this->ref)) == 0) { 472 $err++; 473 } 474 475 if (dol_strlen(trim($this->label)) == 0) { 476 $err++; 477 } 478 479 if ($err > 0) { 480 return 0; 481 } else { 482 return 1; 483 } 484 } 485 486 /** 487 * Insert product into database 488 * 489 * @param User $user User making insert 490 * @param int $notrigger Disable triggers 491 * @return int Id of product/service if OK, < 0 if KO 492 */ 493 public function create($user, $notrigger = 0) 494 { 495 global $conf, $langs; 496 497 $error = 0; 498 499 // Clean parameters 500 $this->ref = dol_sanitizeFileName(dol_string_nospecial(trim($this->ref))); 501 $this->label = trim($this->label); 502 $this->price_ttc = price2num($this->price_ttc); 503 $this->price = price2num($this->price); 504 $this->price_min_ttc = price2num($this->price_min_ttc); 505 $this->price_min = price2num($this->price_min); 506 if (empty($this->tva_tx)) { 507 $this->tva_tx = 0; 508 } 509 if (empty($this->tva_npr)) { 510 $this->tva_npr = 0; 511 } 512 //Local taxes 513 if (empty($this->localtax1_tx)) { 514 $this->localtax1_tx = 0; 515 } 516 if (empty($this->localtax2_tx)) { 517 $this->localtax2_tx = 0; 518 } 519 if (empty($this->localtax1_type)) { 520 $this->localtax1_type = '0'; 521 } 522 if (empty($this->localtax2_type)) { 523 $this->localtax2_type = '0'; 524 } 525 if (empty($this->price)) { 526 $this->price = 0; 527 } 528 if (empty($this->price_min)) { 529 $this->price_min = 0; 530 } 531 // Price by quantity 532 if (empty($this->price_by_qty)) { 533 $this->price_by_qty = 0; 534 } 535 536 if (empty($this->status)) { 537 $this->status = 0; 538 } 539 if (empty($this->status_buy)) { 540 $this->status_buy = 0; 541 } 542 543 $price_ht = 0; 544 $price_ttc = 0; 545 $price_min_ht = 0; 546 $price_min_ttc = 0; 547 548 // 549 if ($this->price_base_type == 'TTC' && $this->price_ttc > 0) { 550 $price_ttc = price2num($this->price_ttc, 'MU'); 551 $price_ht = price2num($this->price_ttc / (1 + ($this->tva_tx / 100)), 'MU'); 552 } 553 554 // 555 if ($this->price_base_type != 'TTC' && $this->price > 0) { 556 $price_ht = price2num($this->price, 'MU'); 557 $price_ttc = price2num($this->price * (1 + ($this->tva_tx / 100)), 'MU'); 558 } 559 560 // 561 if (($this->price_min_ttc > 0) && ($this->price_base_type == 'TTC')) { 562 $price_min_ttc = price2num($this->price_min_ttc, 'MU'); 563 $price_min_ht = price2num($this->price_min_ttc / (1 + ($this->tva_tx / 100)), 'MU'); 564 } 565 566 // 567 if (($this->price_min > 0) && ($this->price_base_type != 'TTC')) { 568 $price_min_ht = price2num($this->price_min, 'MU'); 569 $price_min_ttc = price2num($this->price_min * (1 + ($this->tva_tx / 100)), 'MU'); 570 } 571 572 $this->accountancy_code_buy = trim($this->accountancy_code_buy); 573 $this->accountancy_code_buy_intra = trim($this->accountancy_code_buy_intra); 574 $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export); 575 $this->accountancy_code_sell = trim($this->accountancy_code_sell); 576 $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra); 577 $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export); 578 579 // Barcode value 580 $this->barcode = trim($this->barcode); 581 582 // Check parameters 583 if (empty($this->label)) { 584 $this->error = 'ErrorMandatoryParametersNotProvided'; 585 return -1; 586 } 587 588 if (empty($this->ref) || $this->ref == 'auto') { 589 // Load object modCodeProduct 590 $module = (!empty($conf->global->PRODUCT_CODEPRODUCT_ADDON) ? $conf->global->PRODUCT_CODEPRODUCT_ADDON : 'mod_codeproduct_leopard'); 591 if ($module != 'mod_codeproduct_leopard') // Do not load module file for leopard 592 { 593 if (substr($module, 0, 16) == 'mod_codeproduct_' && substr($module, -3) == 'php') { 594 $module = substr($module, 0, dol_strlen($module) - 4); 595 } 596 dol_include_once('/core/modules/product/'.$module.'.php'); 597 $modCodeProduct = new $module; 598 if (!empty($modCodeProduct->code_auto)) { 599 $this->ref = $modCodeProduct->getNextValue($this, $this->type); 600 } 601 unset($modCodeProduct); 602 } 603 604 if (empty($this->ref)) { 605 $this->error = 'ProductModuleNotSetupForAutoRef'; 606 return -2; 607 } 608 } 609 610 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); 611 612 $now = dol_now(); 613 614 $this->db->begin(); 615 616 // For automatic creation during create action (not used by Dolibarr GUI, can be used by scripts) 617 if ($this->barcode == -1) { 618 $this->barcode = $this->get_barcode($this, $this->barcode_type_code); 619 } 620 621 // Check more parameters 622 // If error, this->errors[] is filled 623 $result = $this->verify(); 624 625 if ($result >= 0) { 626 $sql = "SELECT count(*) as nb"; 627 $sql .= " FROM ".MAIN_DB_PREFIX."product"; 628 $sql .= " WHERE entity IN (".getEntity('product').")"; 629 $sql .= " AND ref = '".$this->db->escape($this->ref)."'"; 630 631 $result = $this->db->query($sql); 632 if ($result) { 633 $obj = $this->db->fetch_object($result); 634 if ($obj->nb == 0) { 635 // Produit non deja existant 636 $sql = "INSERT INTO ".MAIN_DB_PREFIX."product ("; 637 $sql .= "datec"; 638 $sql .= ", entity"; 639 $sql .= ", ref"; 640 $sql .= ", ref_ext"; 641 $sql .= ", price_min"; 642 $sql .= ", price_min_ttc"; 643 $sql .= ", label"; 644 $sql .= ", fk_user_author"; 645 $sql .= ", fk_product_type"; 646 $sql .= ", price"; 647 $sql .= ", price_ttc"; 648 $sql .= ", price_base_type"; 649 $sql .= ", tobuy"; 650 $sql .= ", tosell"; 651 $sql .= ", accountancy_code_buy"; 652 $sql .= ", accountancy_code_buy_intra"; 653 $sql .= ", accountancy_code_buy_export"; 654 $sql .= ", accountancy_code_sell"; 655 $sql .= ", accountancy_code_sell_intra"; 656 $sql .= ", accountancy_code_sell_export"; 657 $sql .= ", canvas"; 658 $sql .= ", finished"; 659 $sql .= ", tobatch"; 660 $sql .= ", fk_unit"; 661 $sql .= ") VALUES ("; 662 $sql .= "'".$this->db->idate($now)."'"; 663 $sql .= ", ".$conf->entity; 664 $sql .= ", '".$this->db->escape($this->ref)."'"; 665 $sql .= ", ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null"); 666 $sql .= ", ".price2num($price_min_ht); 667 $sql .= ", ".price2num($price_min_ttc); 668 $sql .= ", ".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null"); 669 $sql .= ", ".$user->id; 670 $sql .= ", ".$this->type; 671 $sql .= ", ".price2num($price_ht); 672 $sql .= ", ".price2num($price_ttc); 673 $sql .= ", '".$this->db->escape($this->price_base_type)."'"; 674 $sql .= ", ".$this->status; 675 $sql .= ", ".$this->status_buy; 676 $sql .= ", '".$this->db->escape($this->accountancy_code_buy)."'"; 677 $sql .= ", '".$this->db->escape($this->accountancy_code_buy_intra)."'"; 678 $sql .= ", '".$this->db->escape($this->accountancy_code_buy_export)."'"; 679 $sql .= ", '".$this->db->escape($this->accountancy_code_sell)."'"; 680 $sql .= ", '".$this->db->escape($this->accountancy_code_sell_intra)."'"; 681 $sql .= ", '".$this->db->escape($this->accountancy_code_sell_export)."'"; 682 $sql .= ", '".$this->db->escape($this->canvas)."'"; 683 $sql .= ", ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? 'null' : (int) $this->finished); 684 $sql .= ", ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : $this->status_batch); 685 $sql .= ", ".(!$this->fk_unit ? 'NULL' : $this->fk_unit); 686 $sql .= ")"; 687 688 dol_syslog(get_class($this)."::Create", LOG_DEBUG); 689 $result = $this->db->query($sql); 690 if ($result) { 691 $id = $this->db->last_insert_id(MAIN_DB_PREFIX."product"); 692 693 if ($id > 0) { 694 $this->id = $id; 695 $this->price = $price_ht; 696 $this->price_ttc = $price_ttc; 697 $this->price_min = $price_min_ht; 698 $this->price_min_ttc = $price_min_ttc; 699 700 $result = $this->_log_price($user); 701 if ($result > 0) { 702 if ($this->update($id, $user, true, 'add') <= 0) { 703 $error++; 704 } 705 } else { 706 $error++; 707 $this->error = $this->db->lasterror(); 708 } 709 } else { 710 $error++; 711 $this->error = 'ErrorFailedToGetInsertedId'; 712 } 713 } else { 714 $error++; 715 $this->error = $this->db->lasterror(); 716 } 717 } else { 718 // Product already exists with this ref 719 $langs->load("products"); 720 $error++; 721 $this->error = "ErrorProductAlreadyExists"; 722 } 723 } else { 724 $error++; 725 $this->error = $this->db->lasterror(); 726 } 727 728 if (!$error && !$notrigger) { 729 // Call trigger 730 $result = $this->call_trigger('PRODUCT_CREATE', $user); 731 if ($result < 0) { $error++; 732 } 733 // End call triggers 734 } 735 736 if (!$error) { 737 $this->db->commit(); 738 return $this->id; 739 } else { 740 $this->db->rollback(); 741 return -$error; 742 } 743 } else { 744 $this->db->rollback(); 745 dol_syslog(get_class($this)."::Create fails verify ".join(',', $this->errors), LOG_WARNING); 746 return -3; 747 } 748 } 749 750 751 /** 752 * Check properties of product are ok (like name, barcode, ...). 753 * All properties must be already loaded on object (this->barcode, this->barcode_type_code, ...). 754 * 755 * @return int 0 if OK, <0 if KO 756 */ 757 public function verify() 758 { 759 $this->errors = array(); 760 761 $result = 0; 762 $this->ref = trim($this->ref); 763 764 if (!$this->ref) { 765 $this->errors[] = 'ErrorBadRef'; 766 $result = -2; 767 } 768 769 $rescode = $this->check_barcode($this->barcode, $this->barcode_type_code); 770 if ($rescode) { 771 if ($rescode == -1) { 772 $this->errors[] = 'ErrorBadBarCodeSyntax'; 773 } elseif ($rescode == -2) { 774 $this->errors[] = 'ErrorBarCodeRequired'; 775 } elseif ($rescode == -3) { 776 // Note: Common usage is to have barcode unique. For variants, we should have a different barcode. 777 $this->errors[] = 'ErrorBarCodeAlreadyUsed'; 778 } 779 780 $result = -3; 781 } 782 783 return $result; 784 } 785 786 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 787 /** 788 * Check barcode 789 * 790 * @param string $valuetotest Value to test 791 * @param string $typefortest Type of barcode (ISBN, EAN, ...) 792 * @return int 0 if OK 793 * -1 ErrorBadBarCodeSyntax 794 * -2 ErrorBarCodeRequired 795 * -3 ErrorBarCodeAlreadyUsed 796 */ 797 public function check_barcode($valuetotest, $typefortest) 798 { 799 // phpcs:enable 800 global $conf; 801 if (!empty($conf->barcode->enabled) && !empty($conf->global->BARCODE_PRODUCT_ADDON_NUM)) { 802 $module = strtolower($conf->global->BARCODE_PRODUCT_ADDON_NUM); 803 804 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']); 805 foreach ($dirsociete as $dirroot) 806 { 807 $res = dol_include_once($dirroot.$module.'.php'); 808 if ($res) { break; 809 } 810 } 811 812 $mod = new $module(); 813 814 dol_syslog(get_class($this)."::check_barcode value=".$valuetotest." type=".$typefortest." module=".$module); 815 $result = $mod->verif($this->db, $valuetotest, $this, 0, $typefortest); 816 return $result; 817 } else { 818 return 0; 819 } 820 } 821 822 /** 823 * Update a record into database. 824 * If batch flag is set to on, we create records into llx_product_batch 825 * 826 * @param int $id Id of product 827 * @param User $user Object user making update 828 * @param int $notrigger Disable triggers 829 * @param string $action Current action for hookmanager ('add' or 'update') 830 * @param boolean $updatetype Update product type 831 * @return int 1 if OK, -1 if ref already exists, -2 if other error 832 */ 833 public function update($id, $user, $notrigger = false, $action = 'update', $updatetype = false) 834 { 835 global $langs, $conf, $hookmanager; 836 837 $error = 0; 838 839 // Check parameters 840 if (!$this->label) { 841 $this->label = 'MISSING LABEL'; 842 } 843 844 // Clean parameters 845 $this->ref = dol_string_nospecial(trim($this->ref)); 846 $this->label = trim($this->label); 847 $this->description = trim($this->description); 848 $this->note = (isset($this->note) ? trim($this->note) : null); 849 $this->net_measure = price2num($this->net_measure); 850 $this->net_measure_units = trim($this->net_measure_units); 851 $this->weight = price2num($this->weight); 852 $this->weight_units = trim($this->weight_units); 853 $this->length = price2num($this->length); 854 $this->length_units = trim($this->length_units); 855 $this->width = price2num($this->width); 856 $this->width_units = trim($this->width_units); 857 $this->height = price2num($this->height); 858 $this->height_units = trim($this->height_units); 859 $this->surface = price2num($this->surface); 860 $this->surface_units = trim($this->surface_units); 861 $this->volume = price2num($this->volume); 862 $this->volume_units = trim($this->volume_units); 863 864 // set unit not defined 865 if (is_numeric($this->length_units)) { 866 $this->width_units = $this->length_units; // Not used yet 867 } 868 if (is_numeric($this->length_units)) { 869 $this->height_units = $this->length_units; // Not used yet 870 } 871 872 // Automated compute surface and volume if not filled 873 if (empty($this->surface) && !empty($this->length) && !empty($this->width) && $this->length_units == $this->width_units) { 874 $this->surface = $this->length * $this->width; 875 $this->surface_units = measuring_units_squared($this->length_units); 876 } 877 if (empty($this->volume) && !empty($this->surface) && !empty($this->height) && $this->length_units == $this->height_units) { 878 $this->volume = $this->surface * $this->height; 879 $this->volume_units = measuring_units_cubed($this->height_units); 880 } 881 882 if (empty($this->tva_tx)) { 883 $this->tva_tx = 0; 884 } 885 if (empty($this->tva_npr)) { 886 $this->tva_npr = 0; 887 } 888 if (empty($this->localtax1_tx)) { 889 $this->localtax1_tx = 0; 890 } 891 if (empty($this->localtax2_tx)) { 892 $this->localtax2_tx = 0; 893 } 894 if (empty($this->localtax1_type)) { 895 $this->localtax1_type = '0'; 896 } 897 if (empty($this->localtax2_type)) { 898 $this->localtax2_type = '0'; 899 } 900 if (empty($this->status)) { 901 $this->status = 0; 902 } 903 if (empty($this->status_buy)) { 904 $this->status_buy = 0; 905 } 906 907 if (empty($this->country_id)) { 908 $this->country_id = 0; 909 } 910 911 if (empty($this->state_id)) { 912 $this->state_id = 0; 913 } 914 915 // Barcode value 916 $this->barcode = trim($this->barcode); 917 918 $this->accountancy_code_buy = trim($this->accountancy_code_buy); 919 $this->accountancy_code_buy_intra = trim($this->accountancy_code_buy_intra); 920 $this->accountancy_code_buy_export = trim($this->accountancy_code_buy_export); 921 $this->accountancy_code_sell = trim($this->accountancy_code_sell); 922 $this->accountancy_code_sell_intra = trim($this->accountancy_code_sell_intra); 923 $this->accountancy_code_sell_export = trim($this->accountancy_code_sell_export); 924 925 926 927 $this->db->begin(); 928 929 $result = 0; 930 // Check name is required and codes are ok or unique. If error, this->errors[] is filled 931 if ($action != 'add') { 932 $result = $this->verify(); // We don't check when update called during a create because verify was already done 933 } else { 934 // we can continue 935 $result = 0; 936 } 937 938 if ($result >= 0) { 939 if (empty($this->oldcopy)) { 940 $org = new self($this->db); 941 $org->fetch($this->id); 942 $this->oldcopy = $org; 943 } 944 945 // Test if batch management is activated on existing product 946 // If yes, we create missing entries into product_batch 947 if ($this->hasbatch() && !$this->oldcopy->hasbatch()) { 948 //$valueforundefinedlot = 'Undefined'; // In previous version, 39 and lower 949 $valueforundefinedlot = '000000'; 950 if (!empty($conf->global->STOCK_DEFAULT_BATCH)) $valueforundefinedlot = $conf->global->STOCK_DEFAULT_BATCH; 951 952 dol_syslog("Flag batch of product id=".$this->id." is set to ON, so we will create missing records into product_batch"); 953 954 $this->load_stock(); 955 foreach ($this->stock_warehouse as $idW => $ObjW) // For each warehouse where we have stocks defined for this product (for each lines in product_stock) 956 { 957 $qty_batch = 0; 958 foreach ($ObjW->detail_batch as $detail) // Each lines of detail in product_batch of the current $ObjW = product_stock 959 { 960 if ($detail->batch == $valueforundefinedlot || $detail->batch == 'Undefined') { 961 // We discard this line, we will create it later 962 $sqlclean = "DELETE FROM ".MAIN_DB_PREFIX."product_batch WHERE batch in('Undefined', '".$this->db->escape($valueforundefinedlot)."') AND fk_product_stock = ".$ObjW->id; 963 $result = $this->db->query($sqlclean); 964 if (!$result) { 965 dol_print_error($this->db); 966 exit; 967 } 968 continue; 969 } 970 971 $qty_batch += $detail->qty; 972 } 973 // Quantities in batch details are not same as stock quantity, 974 // so we add a default batch record to complete and get same qty in parent and child table 975 if ($ObjW->real <> $qty_batch) { 976 $ObjBatch = new Productbatch($this->db); 977 $ObjBatch->batch = $valueforundefinedlot; 978 $ObjBatch->qty = ($ObjW->real - $qty_batch); 979 $ObjBatch->fk_product_stock = $ObjW->id; 980 981 if ($ObjBatch->create($user, 1) < 0) { 982 $error++; 983 $this->errors = $ObjBatch->errors; 984 } 985 } 986 } 987 } 988 989 // For automatic creation 990 if ($this->barcode == -1) { $this->barcode = $this->get_barcode($this, $this->barcode_type_code); 991 } 992 993 $sql = "UPDATE ".MAIN_DB_PREFIX."product"; 994 $sql .= " SET label = '".$this->db->escape($this->label)."'"; 995 996 if ($updatetype && ($this->isProduct() || $this->isService())) { 997 $sql .= ", fk_product_type = ".$this->type; 998 } 999 1000 $sql .= ", ref = '".$this->db->escape($this->ref)."'"; 1001 $sql .= ", ref_ext = ".(!empty($this->ref_ext) ? "'".$this->db->escape($this->ref_ext)."'" : "null"); 1002 $sql .= ", default_vat_code = ".($this->default_vat_code ? "'".$this->db->escape($this->default_vat_code)."'" : "null"); 1003 $sql .= ", tva_tx = ".$this->tva_tx; 1004 $sql .= ", recuperableonly = ".$this->tva_npr; 1005 $sql .= ", localtax1_tx = ".$this->localtax1_tx; 1006 $sql .= ", localtax2_tx = ".$this->localtax2_tx; 1007 $sql .= ", localtax1_type = ".($this->localtax1_type != '' ? "'".$this->db->escape($this->localtax1_type)."'" : "'0'"); 1008 $sql .= ", localtax2_type = ".($this->localtax2_type != '' ? "'".$this->db->escape($this->localtax2_type)."'" : "'0'"); 1009 1010 $sql .= ", barcode = ".(empty($this->barcode) ? "null" : "'".$this->db->escape($this->barcode)."'"); 1011 $sql .= ", fk_barcode_type = ".(empty($this->barcode_type) ? "null" : $this->db->escape($this->barcode_type)); 1012 1013 $sql .= ", tosell = ".(int) $this->status; 1014 $sql .= ", tobuy = ".(int) $this->status_buy; 1015 $sql .= ", tobatch = ".((empty($this->status_batch) || $this->status_batch < 0) ? '0' : (int) $this->status_batch); 1016 $sql .= ", finished = ".((!isset($this->finished) || $this->finished < 0 || $this->finished == '') ? "null" : (int) $this->finished); 1017 $sql .= ", net_measure = ".($this->net_measure != '' ? "'".$this->db->escape($this->net_measure)."'" : 'null'); 1018 $sql .= ", net_measure_units = ".($this->net_measure_units != '' ? "'".$this->db->escape($this->net_measure_units)."'" : 'null'); 1019 $sql .= ", weight = ".($this->weight != '' ? "'".$this->db->escape($this->weight)."'" : 'null'); 1020 $sql .= ", weight_units = ".($this->weight_units != '' ? "'".$this->db->escape($this->weight_units)."'" : 'null'); 1021 $sql .= ", length = ".($this->length != '' ? "'".$this->db->escape($this->length)."'" : 'null'); 1022 $sql .= ", length_units = ".($this->length_units != '' ? "'".$this->db->escape($this->length_units)."'" : 'null'); 1023 $sql .= ", width= ".($this->width != '' ? "'".$this->db->escape($this->width)."'" : 'null'); 1024 $sql .= ", width_units = ".($this->width_units != '' ? "'".$this->db->escape($this->width_units)."'" : 'null'); 1025 $sql .= ", height = ".($this->height != '' ? "'".$this->db->escape($this->height)."'" : 'null'); 1026 $sql .= ", height_units = ".($this->height_units != '' ? "'".$this->db->escape($this->height_units)."'" : 'null'); 1027 $sql .= ", surface = ".($this->surface != '' ? "'".$this->db->escape($this->surface)."'" : 'null'); 1028 $sql .= ", surface_units = ".($this->surface_units != '' ? "'".$this->db->escape($this->surface_units)."'" : 'null'); 1029 $sql .= ", volume = ".($this->volume != '' ? "'".$this->db->escape($this->volume)."'" : 'null'); 1030 $sql .= ", volume_units = ".($this->volume_units != '' ? "'".$this->db->escape($this->volume_units)."'" : 'null'); 1031 $sql .= ", fk_default_warehouse = ".($this->fk_default_warehouse > 0 ? $this->db->escape($this->fk_default_warehouse) : 'null'); 1032 $sql .= ", seuil_stock_alerte = ".((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (float) $this->seuil_stock_alerte : 'null'); 1033 $sql .= ", description = '".$this->db->escape($this->description)."'"; 1034 $sql .= ", url = ".($this->url ? "'".$this->db->escape($this->url)."'" : 'null'); 1035 $sql .= ", customcode = '".$this->db->escape($this->customcode)."'"; 1036 $sql .= ", fk_country = ".($this->country_id > 0 ? (int) $this->country_id : 'null'); 1037 $sql .= ", fk_state = ".($this->state_id > 0 ? (int) $this->state_id : 'null'); 1038 $sql .= ", note = ".(isset($this->note) ? "'".$this->db->escape($this->note)."'" : 'null'); 1039 $sql .= ", duration = '".$this->db->escape($this->duration_value.$this->duration_unit)."'"; 1040 $sql .= ", accountancy_code_buy = '".$this->db->escape($this->accountancy_code_buy)."'"; 1041 $sql .= ", accountancy_code_buy_intra = '".$this->db->escape($this->accountancy_code_buy_intra)."'"; 1042 $sql .= ", accountancy_code_buy_export = '".$this->db->escape($this->accountancy_code_buy_export)."'"; 1043 $sql .= ", accountancy_code_sell= '".$this->db->escape($this->accountancy_code_sell)."'"; 1044 $sql .= ", accountancy_code_sell_intra= '".$this->db->escape($this->accountancy_code_sell_intra)."'"; 1045 $sql .= ", accountancy_code_sell_export= '".$this->db->escape($this->accountancy_code_sell_export)."'"; 1046 $sql .= ", desiredstock = ".((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (float) $this->desiredstock : "null"); 1047 $sql .= ", cost_price = ".($this->cost_price != '' ? $this->db->escape($this->cost_price) : 'null'); 1048 $sql .= ", fk_unit= ".(!$this->fk_unit ? 'NULL' : (int) $this->fk_unit); 1049 $sql .= ", price_autogen = ".(!$this->price_autogen ? 0 : 1); 1050 $sql .= ", fk_price_expression = ".($this->fk_price_expression != 0 ? (int) $this->fk_price_expression : 'NULL'); 1051 $sql .= ", fk_user_modif = ".($user->id > 0 ? $user->id : 'NULL'); 1052 1053 // stock field is not here because it is a denormalized value from product_stock. 1054 $sql .= " WHERE rowid = ".$id; 1055 1056 dol_syslog(get_class($this)."::update", LOG_DEBUG); 1057 1058 $resql = $this->db->query($sql); 1059 if ($resql) { 1060 $this->id = $id; 1061 1062 // Multilangs 1063 if (!empty($conf->global->MAIN_MULTILANGS)) { 1064 if ($this->setMultiLangs($user) < 0) { 1065 $this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql; 1066 return -2; 1067 } 1068 } 1069 1070 $action = 'update'; 1071 1072 // Actions on extra fields 1073 if (!$error) { 1074 $result = $this->insertExtraFields(); 1075 if ($result < 0) { 1076 $error++; 1077 } 1078 } 1079 1080 if (!$error && !$notrigger) { 1081 // Call trigger 1082 $result = $this->call_trigger('PRODUCT_MODIFY', $user); 1083 if ($result < 0) { 1084 $error++; 1085 } 1086 // End call triggers 1087 } 1088 1089 if (!$error && (is_object($this->oldcopy) && $this->oldcopy->ref !== $this->ref)) { 1090 // We remove directory 1091 if ($conf->product->dir_output) { 1092 $olddir = $conf->product->dir_output."/".dol_sanitizeFileName($this->oldcopy->ref); 1093 $newdir = $conf->product->dir_output."/".dol_sanitizeFileName($this->ref); 1094 if (file_exists($olddir)) { 1095 //include_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php'; 1096 //$res = dol_move($olddir, $newdir); 1097 // do not use dol_move with directory 1098 $res = @rename($olddir, $newdir); 1099 if (!$res) { 1100 $langs->load("errors"); 1101 $this->error = $langs->trans('ErrorFailToRenameDir', $olddir, $newdir); 1102 $error++; 1103 } 1104 } 1105 } 1106 } 1107 1108 if (!$error) { 1109 if (!empty($conf->variants->enabled)) { 1110 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php'; 1111 1112 $comb = new ProductCombination($this->db); 1113 1114 foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) { 1115 $currcomb->updateProperties($this, $user); 1116 } 1117 } 1118 1119 $this->db->commit(); 1120 return 1; 1121 } else { 1122 $this->db->rollback(); 1123 return -$error; 1124 } 1125 } else { 1126 if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') { 1127 $langs->load("errors"); 1128 if (empty($conf->barcode->enabled) || empty($this->barcode)) { 1129 $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductAlreadyExists", $this->ref); 1130 } else { 1131 $this->error = $langs->trans("Error")." : ".$langs->trans("ErrorProductBarCodeAlreadyExists", $this->barcode); 1132 } 1133 $this->errors[] = $this->error; 1134 $this->db->rollback(); 1135 return -1; 1136 } else { 1137 $this->error = $langs->trans("Error")." : ".$this->db->error()." - ".$sql; 1138 $this->errors[] = $this->error; 1139 $this->db->rollback(); 1140 return -2; 1141 } 1142 } 1143 } else { 1144 $this->db->rollback(); 1145 dol_syslog(get_class($this)."::Update fails verify ".join(',', $this->errors), LOG_WARNING); 1146 return -3; 1147 } 1148 } 1149 1150 /** 1151 * Delete a product from database (if not used) 1152 * 1153 * @param User $user User (object) deleting product 1154 * @param int $notrigger Do not execute trigger 1155 * @return int < 0 if KO, 0 = Not possible, > 0 if OK 1156 */ 1157 public function delete(User $user, $notrigger = 0) 1158 { 1159 global $conf, $langs; 1160 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; 1161 1162 $error = 0; 1163 1164 // Check parameters 1165 if (empty($this->id)) { 1166 $this->error = "Object must be fetched before calling delete"; 1167 return -1; 1168 } 1169 if (($this->type == Product::TYPE_PRODUCT && empty($user->rights->produit->supprimer)) || ($this->type == Product::TYPE_SERVICE && empty($user->rights->service->supprimer))) { 1170 $this->error = "ErrorForbidden"; 1171 return 0; 1172 } 1173 1174 $objectisused = $this->isObjectUsed($this->id); 1175 if (empty($objectisused)) { 1176 $this->db->begin(); 1177 1178 if (!$error && empty($notrigger)) { 1179 // Call trigger 1180 $result = $this->call_trigger('PRODUCT_DELETE', $user); 1181 if ($result < 0) { 1182 $error++; 1183 } 1184 // End call triggers 1185 } 1186 1187 // Delete from product_batch on product delete 1188 if (!$error) { 1189 $sql = "DELETE FROM ".MAIN_DB_PREFIX.'product_batch'; 1190 $sql .= " WHERE fk_product_stock IN ("; 1191 $sql .= "SELECT rowid FROM ".MAIN_DB_PREFIX.'product_stock'; 1192 $sql .= " WHERE fk_product = ".(int) $this->id.")"; 1193 1194 $result = $this->db->query($sql); 1195 if (!$result) { 1196 $error++; 1197 $this->errors[] = $this->db->lasterror(); 1198 } 1199 } 1200 1201 // Delete all child tables 1202 if (!$error) { 1203 $elements = array('product_fournisseur_price', 'product_price', 'product_lang', 'categorie_product', 'product_stock', 'product_customer_price', 'product_lot'); // product_batch is done before 1204 foreach ($elements as $table) 1205 { 1206 if (!$error) { 1207 $sql = "DELETE FROM ".MAIN_DB_PREFIX.$table; 1208 $sql .= " WHERE fk_product = ".(int) $this->id; 1209 1210 $result = $this->db->query($sql); 1211 if (!$result) { 1212 $error++; 1213 $this->errors[] = $this->db->lasterror(); 1214 } 1215 } 1216 } 1217 } 1218 1219 if (!$error) { 1220 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php'; 1221 include_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php'; 1222 1223 //If it is a parent product, then we remove the association with child products 1224 $prodcomb = new ProductCombination($this->db); 1225 1226 if ($prodcomb->deleteByFkProductParent($user, $this->id) < 0) { 1227 $error++; 1228 $this->errors[] = 'Error deleting combinations'; 1229 } 1230 1231 //We also check if it is a child product 1232 if (!$error && ($prodcomb->fetchByFkProductChild($this->id) > 0) && ($prodcomb->delete($user) < 0)) { 1233 $error++; 1234 $this->errors[] = 'Error deleting child combination'; 1235 } 1236 } 1237 1238 // Delete from product_association 1239 if (!$error) { 1240 $sql = "DELETE FROM ".MAIN_DB_PREFIX."product_association"; 1241 $sql .= " WHERE fk_product_pere = ".(int) $this->id." OR fk_product_fils = ".(int) $this->id; 1242 1243 $result = $this->db->query($sql); 1244 if (!$result) { 1245 $error++; 1246 $this->errors[] = $this->db->lasterror(); 1247 } 1248 } 1249 1250 // Remove extrafields 1251 if (!$error) { 1252 $result = $this->deleteExtraFields(); 1253 if ($result < 0) { 1254 $error++; 1255 dol_syslog(get_class($this)."::delete error -4 ".$this->error, LOG_ERR); 1256 } 1257 } 1258 1259 // Delete product 1260 if (!$error) { 1261 $sqlz = "DELETE FROM ".MAIN_DB_PREFIX."product"; 1262 $sqlz .= " WHERE rowid = ".(int) $this->id; 1263 1264 $resultz = $this->db->query($sqlz); 1265 if (!$resultz) { 1266 $error++; 1267 $this->errors[] = $this->db->lasterror(); 1268 } 1269 } 1270 1271 if (!$error) { 1272 // We remove directory 1273 $ref = dol_sanitizeFileName($this->ref); 1274 if ($conf->product->dir_output) { 1275 $dir = $conf->product->dir_output."/".$ref; 1276 if (file_exists($dir)) { 1277 $res = @dol_delete_dir_recursive($dir); 1278 if (!$res) { 1279 $this->errors[] = 'ErrorFailToDeleteDir'; 1280 $error++; 1281 } 1282 } 1283 } 1284 } 1285 1286 if (!$error) { 1287 $this->db->commit(); 1288 return 1; 1289 } else { 1290 foreach ($this->errors as $errmsg) 1291 { 1292 dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR); 1293 $this->error .= ($this->error ? ', '.$errmsg : $errmsg); 1294 } 1295 $this->db->rollback(); 1296 return -$error; 1297 } 1298 } else { 1299 $this->error = "ErrorRecordIsUsedCantDelete"; 1300 return 0; 1301 } 1302 } 1303 1304 /** 1305 * Update or add a translation for a product 1306 * 1307 * @param User $user Object user making update 1308 * @return int <0 if KO, >0 if OK 1309 */ 1310 public function setMultiLangs($user) 1311 { 1312 global $conf, $langs; 1313 1314 $langs_available = $langs->get_available_languages(DOL_DOCUMENT_ROOT, 0, 2); 1315 $current_lang = $langs->getDefaultLang(); 1316 1317 foreach ($langs_available as $key => $value) 1318 { 1319 if ($key == $current_lang) { 1320 $sql = "SELECT rowid"; 1321 $sql .= " FROM ".MAIN_DB_PREFIX."product_lang"; 1322 $sql .= " WHERE fk_product=".$this->id; 1323 $sql .= " AND lang='".$this->db->escape($key)."'"; 1324 1325 $result = $this->db->query($sql); 1326 1327 if ($this->db->num_rows($result)) // if there is already a description line for this language 1328 { 1329 $sql2 = "UPDATE ".MAIN_DB_PREFIX."product_lang"; 1330 $sql2 .= " SET "; 1331 $sql2 .= " label='".$this->db->escape($this->label)."',"; 1332 $sql2 .= " description='".$this->db->escape($this->description)."'"; 1333 if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) { $sql2 .= ", note='".$this->db->escape($this->other)."'"; 1334 } 1335 $sql2 .= " WHERE fk_product=".$this->id." AND lang='".$this->db->escape($key)."'"; 1336 } else { 1337 $sql2 = "INSERT INTO ".MAIN_DB_PREFIX."product_lang (fk_product, lang, label, description"; 1338 if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) { $sql2 .= ", note"; 1339 } 1340 $sql2 .= ")"; 1341 $sql2 .= " VALUES(".$this->id.",'".$this->db->escape($key)."','".$this->db->escape($this->label)."',"; 1342 $sql2 .= " '".$this->db->escape($this->description)."'"; 1343 if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) { 1344 $sql2 .= ", '".$this->db->escape($this->other)."'"; 1345 } 1346 $sql2 .= ")"; 1347 } 1348 dol_syslog(get_class($this).'::setMultiLangs key = current_lang = '.$key); 1349 if (!$this->db->query($sql2)) { 1350 $this->error = $this->db->lasterror(); 1351 return -1; 1352 } 1353 } elseif (isset($this->multilangs[$key])) { 1354 $sql = "SELECT rowid"; 1355 $sql .= " FROM ".MAIN_DB_PREFIX."product_lang"; 1356 $sql .= " WHERE fk_product=".$this->id; 1357 $sql .= " AND lang='".$this->db->escape($key)."'"; 1358 1359 $result = $this->db->query($sql); 1360 1361 if ($this->db->num_rows($result)) // if there is already a description line for this language 1362 { 1363 $sql2 = "UPDATE ".MAIN_DB_PREFIX."product_lang"; 1364 $sql2 .= " SET "; 1365 $sql2 .= " label='".$this->db->escape($this->multilangs["$key"]["label"])."',"; 1366 $sql2 .= " description='".$this->db->escape($this->multilangs["$key"]["description"])."'"; 1367 if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) { 1368 $sql2 .= ", note='".$this->db->escape($this->multilangs["$key"]["other"])."'"; 1369 } 1370 $sql2 .= " WHERE fk_product=".$this->id." AND lang='".$this->db->escape($key)."'"; 1371 } else { 1372 $sql2 = "INSERT INTO ".MAIN_DB_PREFIX."product_lang (fk_product, lang, label, description"; 1373 if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) { $sql2 .= ", note"; 1374 } 1375 $sql2 .= ")"; 1376 $sql2 .= " VALUES(".$this->id.",'".$this->db->escape($key)."','".$this->db->escape($this->multilangs["$key"]["label"])."',"; 1377 $sql2 .= " '".$this->db->escape($this->multilangs["$key"]["description"])."'"; 1378 if (!empty($conf->global->PRODUCT_USE_OTHER_FIELD_IN_TRANSLATION)) { 1379 $sql2 .= ", '".$this->db->escape($this->multilangs["$key"]["other"])."'"; 1380 } 1381 $sql2 .= ")"; 1382 } 1383 1384 // We do not save if main fields are empty 1385 if ($this->multilangs["$key"]["label"] || $this->multilangs["$key"]["description"]) { 1386 if (!$this->db->query($sql2)) { 1387 $this->error = $this->db->lasterror(); 1388 return -1; 1389 } 1390 } 1391 } else { 1392 // language is not current language and we didn't provide a multilang description for this language 1393 } 1394 } 1395 1396 // Call trigger 1397 $result = $this->call_trigger('PRODUCT_SET_MULTILANGS', $user); 1398 if ($result < 0) { 1399 $this->error = $this->db->lasterror(); 1400 return -1; 1401 } 1402 // End call triggers 1403 1404 return 1; 1405 } 1406 1407 /** 1408 * Delete a language for this product 1409 * 1410 * @param string $langtodelete Language code to delete 1411 * @param User $user Object user making delete 1412 * 1413 * @return int <0 if KO, >0 if OK 1414 */ 1415 public function delMultiLangs($langtodelete, $user) 1416 { 1417 $sql = "DELETE FROM ".MAIN_DB_PREFIX."product_lang"; 1418 $sql .= " WHERE fk_product=".$this->id." AND lang='".$this->db->escape($langtodelete)."'"; 1419 1420 dol_syslog(get_class($this).'::delMultiLangs', LOG_DEBUG); 1421 $result = $this->db->query($sql); 1422 if ($result) { 1423 // Call trigger 1424 $result = $this->call_trigger('PRODUCT_DEL_MULTILANGS', $user); 1425 if ($result < 0) { 1426 $this->error = $this->db->lasterror(); 1427 dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR); 1428 return -1; 1429 } 1430 // End call triggers 1431 return 1; 1432 } else { 1433 $this->error = $this->db->lasterror(); 1434 dol_syslog(get_class($this).'::delMultiLangs error='.$this->error, LOG_ERR); 1435 return -1; 1436 } 1437 } 1438 1439 /** 1440 * Sets an accountancy code for a product. 1441 * Also calls PRODUCT_MODIFY trigger when modified 1442 * 1443 * @param string $type It can be 'buy', 'buy_intra', 'buy_export', 'sell', 'sell_intra' or 'sell_export' 1444 * @param string $value Accountancy code 1445 * @return int <0 KO >0 OK 1446 */ 1447 public function setAccountancyCode($type, $value) 1448 { 1449 global $user, $langs, $conf; 1450 1451 $error = 0; 1452 1453 $this->db->begin(); 1454 1455 if ($type == 'buy') { 1456 $field = 'accountancy_code_buy'; 1457 } elseif ($type == 'buy_intra') { 1458 $field = 'accountancy_code_buy_intra'; 1459 } elseif ($type == 'buy_export') { 1460 $field = 'accountancy_code_buy_export'; 1461 } elseif ($type == 'sell') { 1462 $field = 'accountancy_code_sell'; 1463 } elseif ($type == 'sell_intra') { 1464 $field = 'accountancy_code_sell_intra'; 1465 } elseif ($type == 'sell_export') { 1466 $field = 'accountancy_code_sell_export'; 1467 } else { 1468 return -1; 1469 } 1470 1471 $sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET "; 1472 $sql .= "$field = '".$this->db->escape($value)."'"; 1473 $sql .= " WHERE rowid = ".$this->id; 1474 1475 dol_syslog(__METHOD__." sql=".$sql, LOG_DEBUG); 1476 $resql = $this->db->query($sql); 1477 1478 if ($resql) { 1479 // Call trigger 1480 $result = $this->call_trigger('PRODUCT_MODIFY', $user); 1481 if ($result < 0) $error++; 1482 // End call triggers 1483 1484 if ($error) { 1485 $this->db->rollback(); 1486 return -1; 1487 } 1488 1489 $this->$field = $value; 1490 1491 $this->db->commit(); 1492 return 1; 1493 } else { 1494 $this->error = $this->db->lasterror(); 1495 $this->db->rollback(); 1496 return -1; 1497 } 1498 } 1499 1500 /** 1501 * Load array this->multilangs 1502 * 1503 * @return int <0 if KO, >0 if OK 1504 */ 1505 public function getMultiLangs() 1506 { 1507 global $langs; 1508 1509 $current_lang = $langs->getDefaultLang(); 1510 1511 $sql = "SELECT lang, label, description, note as other"; 1512 $sql .= " FROM ".MAIN_DB_PREFIX."product_lang"; 1513 $sql .= " WHERE fk_product=".$this->id; 1514 1515 $result = $this->db->query($sql); 1516 if ($result) { 1517 while ($obj = $this->db->fetch_object($result)) 1518 { 1519 //print 'lang='.$obj->lang.' current='.$current_lang.'<br>'; 1520 if ($obj->lang == $current_lang) // si on a les traduct. dans la langue courante on les charge en infos principales. 1521 { 1522 $this->label = $obj->label; 1523 $this->description = $obj->description; 1524 $this->other = $obj->other; 1525 } 1526 $this->multilangs["$obj->lang"]["label"] = $obj->label; 1527 $this->multilangs["$obj->lang"]["description"] = $obj->description; 1528 $this->multilangs["$obj->lang"]["other"] = $obj->other; 1529 } 1530 return 1; 1531 } else { 1532 $this->error = "Error: ".$this->db->lasterror()." - ".$sql; 1533 return -1; 1534 } 1535 } 1536 1537 /** 1538 * used to check if price have really change to avoid log pollution 1539 * 1540 * @param int $level price level to change 1541 * @return array 1542 */ 1543 private function getArrayForPriceCompare($level = 0) 1544 { 1545 1546 $testExit = array('multiprices','multiprices_ttc','multiprices_base_type','multiprices_min','multiprices_min_ttc','multiprices_tva_tx','multiprices_recuperableonly'); 1547 1548 foreach ($testExit as $field){ 1549 if (!isset($this->$field[$level])) { 1550 return array(); 1551 } 1552 } 1553 1554 $lastPrice = array( 1555 'level' => $level ? $level : 1, 1556 'multiprices' => doubleval($this->multiprices[$level]), 1557 'multiprices_ttc' => doubleval($this->multiprices_ttc[$level]), 1558 'multiprices_base_type' => $this->multiprices_base_type[$level], 1559 'multiprices_min' => doubleval($this->multiprices_min[$level]), 1560 'multiprices_min_ttc' => doubleval($this->multiprices_min_ttc[$level]), 1561 'multiprices_tva_tx' => doubleval($this->multiprices_tva_tx[$level]), 1562 'multiprices_recuperableonly' => doubleval($this->multiprices_recuperableonly[$level]), 1563 ); 1564 1565 return $lastPrice; 1566 } 1567 1568 1569 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 1570 /** 1571 * Insert a track that we changed a customer price 1572 * 1573 * @param User $user User making change 1574 * @param int $level price level to change 1575 * @return int <0 if KO, >0 if OK 1576 */ 1577 private function _log_price($user, $level = 0) 1578 { 1579 // phpcs:enable 1580 global $conf; 1581 1582 $now = dol_now(); 1583 1584 // Clean parameters 1585 if (empty($this->price_by_qty)) { 1586 $this->price_by_qty = 0; 1587 } 1588 1589 // Add new price 1590 $sql = "INSERT INTO ".MAIN_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,"; 1591 $sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, price_min,price_min_ttc,price_by_qty,entity,fk_price_expression) "; 1592 $sql .= " VALUES(".($level ? $level : 1).", '".$this->db->idate($now)."',".$this->id.",".$user->id.",".$this->price.",".$this->price_ttc.",'".$this->db->escape($this->price_base_type)."',".$this->status.",".$this->tva_tx.", ".($this->default_vat_code ? ("'".$this->db->escape($this->default_vat_code)."'") : "null").",".$this->tva_npr.","; 1593 $sql .= " ".$this->localtax1_tx.", ".$this->localtax2_tx.", '".$this->db->escape($this->localtax1_type)."', '".$this->db->escape($this->localtax2_type)."', ".$this->price_min.",".$this->price_min_ttc.",".$this->price_by_qty.",".$conf->entity.",".($this->fk_price_expression > 0 ? $this->fk_price_expression : 'null'); 1594 $sql .= ")"; 1595 1596 dol_syslog(get_class($this)."::_log_price", LOG_DEBUG); 1597 $resql = $this->db->query($sql); 1598 if (!$resql) { 1599 $this->error = $this->db->lasterror(); 1600 dol_print_error($this->db); 1601 return -1; 1602 } else { 1603 return 1; 1604 } 1605 } 1606 1607 1608 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 1609 /** 1610 * Delete a price line 1611 * 1612 * @param User $user Object user 1613 * @param int $rowid Line id to delete 1614 * @return int <0 if KO, >0 if OK 1615 */ 1616 public function log_price_delete($user, $rowid) 1617 { 1618 // phpcs:enable 1619 $sql = "DELETE FROM ".MAIN_DB_PREFIX."product_price_by_qty"; 1620 $sql .= " WHERE fk_product_price=".$rowid; 1621 $resql = $this->db->query($sql); 1622 1623 $sql = "DELETE FROM ".MAIN_DB_PREFIX."product_price"; 1624 $sql .= " WHERE rowid=".$rowid; 1625 $resql = $this->db->query($sql); 1626 if ($resql) { 1627 return 1; 1628 } else { 1629 $this->error = $this->db->lasterror(); 1630 return -1; 1631 } 1632 } 1633 1634 1635 /** 1636 * Return price of sell of a product for a seller/buyer/product. 1637 * 1638 * @param Societe $thirdparty_seller Seller 1639 * @param Societe $thirdparty_buyer Buyer 1640 * @param int $pqp Id of product price per quantity if a selection was done of such a price 1641 * @return array Array of price information array('pu_ht'=> , 'pu_ttc'=> , 'tva_tx'=>'X.Y (code)', ...), 'tva_npr'=>0, ...) 1642 * @see get_buyprice(), find_min_price_product_fournisseur() 1643 */ 1644 public function getSellPrice($thirdparty_seller, $thirdparty_buyer, $pqp = 0) 1645 { 1646 global $conf, $db; 1647 1648 // Update if prices fields are defined 1649 $tva_tx = get_default_tva($thirdparty_seller, $thirdparty_buyer, $this->id); 1650 $tva_npr = get_default_npr($thirdparty_seller, $thirdparty_buyer, $this->id); 1651 if (empty($tva_tx)) $tva_npr = 0; 1652 1653 $pu_ht = $this->price; 1654 $pu_ttc = $this->price_ttc; 1655 $price_min = $this->price_min; 1656 $price_base_type = $this->price_base_type; 1657 1658 // If price per segment 1659 if (!empty($conf->global->PRODUIT_MULTIPRICES) && !empty($thirdparty_buyer->price_level)) { 1660 $pu_ht = $this->multiprices[$thirdparty_buyer->price_level]; 1661 $pu_ttc = $this->multiprices_ttc[$thirdparty_buyer->price_level]; 1662 $price_min = $this->multiprices_min[$thirdparty_buyer->price_level]; 1663 $price_base_type = $this->multiprices_base_type[$thirdparty_buyer->price_level]; 1664 if (!empty($conf->global->PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL)) // using this option is a bug. kept for backward compatibility 1665 { 1666 if (isset($this->multiprices_tva_tx[$thirdparty_buyer->price_level])) $tva_tx = $this->multiprices_tva_tx[$thirdparty_buyer->price_level]; 1667 if (isset($this->multiprices_recuperableonly[$thirdparty_buyer->price_level])) $tva_npr = $this->multiprices_recuperableonly[$thirdparty_buyer->price_level]; 1668 if (empty($tva_tx)) $tva_npr = 0; 1669 } 1670 } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES)) { 1671 // If price per customer 1672 require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php'; 1673 1674 $prodcustprice = new Productcustomerprice($this->db); 1675 1676 $filter = array('t.fk_product' => $this->id, 't.fk_soc' => $thirdparty_buyer->id); 1677 1678 $result = $prodcustprice->fetch_all('', '', 0, 0, $filter); 1679 if ($result) { 1680 if (count($prodcustprice->lines) > 0) { 1681 $pu_ht = price($prodcustprice->lines[0]->price); 1682 $price_min = price($prodcustprice->lines[0]->price_min); 1683 $pu_ttc = price($prodcustprice->lines[0]->price_ttc); 1684 $price_base_type = $prodcustprice->lines[0]->price_base_type; 1685 $tva_tx = $prodcustprice->lines[0]->tva_tx; 1686 if ($prodcustprice->lines[0]->default_vat_code && !preg_match('/\(.*\)/', $tva_tx)) $tva_tx .= ' ('.$prodcustprice->lines[0]->default_vat_code.')'; 1687 $tva_npr = $prodcustprice->lines[0]->recuperableonly; 1688 if (empty($tva_tx)) $tva_npr = 0; 1689 } 1690 } 1691 } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY)) { 1692 // If price per quantity 1693 if ($this->prices_by_qty[0]) { 1694 // yes, this product has some prices per quantity 1695 // Search price into product_price_by_qty from $this->id 1696 foreach ($this->prices_by_qty_list[0] as $priceforthequantityarray) { 1697 if ($priceforthequantityarray['rowid'] != $pqp) continue; 1698 // We found the price 1699 if ($priceforthequantityarray['price_base_type'] == 'HT') 1700 { 1701 $pu_ht = $priceforthequantityarray['unitprice']; 1702 } else { 1703 $pu_ttc = $priceforthequantityarray['unitprice']; 1704 } 1705 break; 1706 } 1707 } 1708 } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES)) { 1709 // If price per quantity and customer 1710 if ($this->prices_by_qty[$thirdparty_buyer->price_level]) { 1711 // yes, this product has some prices per quantity 1712 // Search price into product_price_by_qty from $this->id 1713 foreach ($this->prices_by_qty_list[$thirdparty_buyer->price_level] as $priceforthequantityarray) 1714 { 1715 if ($priceforthequantityarray['rowid'] != $pqp) continue; 1716 // We found the price 1717 if ($priceforthequantityarray['price_base_type'] == 'HT') 1718 { 1719 $pu_ht = $priceforthequantityarray['unitprice']; 1720 } else { 1721 $pu_ttc = $priceforthequantityarray['unitprice']; 1722 } 1723 break; 1724 } 1725 } 1726 } 1727 1728 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); 1729 } 1730 1731 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 1732 /** 1733 * Read price used by a provider. 1734 * We enter as input couple prodfournprice/qty or triplet qty/product_id/fourn_ref. 1735 * This also set some properties on product like ->buyprice, ->fourn_pu, ... 1736 * 1737 * @param int $prodfournprice Id du tarif = rowid table product_fournisseur_price 1738 * @param double $qty Quantity asked or -1 to get first entry found 1739 * @param int $product_id Filter on a particular product id 1740 * @param string $fourn_ref Filter on a supplier price ref. 'none' to exclude ref in search. 1741 * @param int $fk_soc If of supplier 1742 * @return int <-1 if KO, -1 if qty not enough, 0 if OK but nothing found, id_product if OK and found. May also initialize some properties like (->ref_supplier, buyprice, fourn_pu, vatrate_supplier...) 1743 * @see getSellPrice(), find_min_price_product_fournisseur() 1744 */ 1745 public function get_buyprice($prodfournprice, $qty, $product_id = 0, $fourn_ref = '', $fk_soc = 0) 1746 { 1747 // phpcs:enable 1748 global $conf; 1749 $result = 0; 1750 1751 // We do a first seach with a select by searching with couple prodfournprice and qty only (later we will search on triplet qty/product_id/fourn_ref) 1752 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent,"; 1753 $sql .= " pfp.fk_product, pfp.ref_fourn, pfp.desc_fourn, pfp.fk_soc, pfp.tva_tx, pfp.fk_supplier_price_expression,"; 1754 $sql .= " pfp.default_vat_code,"; 1755 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code"; 1756 if (!empty($conf->global->PRODUCT_USE_SUPPLIER_PACKAGING)) $sql .= ", pfp.packaging"; 1757 $sql .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp"; 1758 $sql .= " WHERE pfp.rowid = ".$prodfournprice; 1759 if ($qty > 0) { $sql .= " AND pfp.quantity <= ".$qty; 1760 } 1761 $sql .= " ORDER BY pfp.quantity DESC"; 1762 1763 dol_syslog(get_class($this)."::get_buyprice first search by prodfournprice/qty", LOG_DEBUG); 1764 $resql = $this->db->query($sql); 1765 if ($resql) { 1766 $obj = $this->db->fetch_object($resql); 1767 if ($obj && $obj->quantity > 0) // If we found a supplier prices from the id of supplier price 1768 { 1769 if (!empty($conf->dynamicprices->enabled) && !empty($obj->fk_supplier_price_expression)) { 1770 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php'; 1771 $prod_supplier = new ProductFournisseur($this->db); 1772 $prod_supplier->product_fourn_price_id = $obj->rowid; 1773 $prod_supplier->id = $obj->fk_product; 1774 $prod_supplier->fourn_qty = $obj->quantity; 1775 $prod_supplier->fourn_tva_tx = $obj->tva_tx; 1776 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression; 1777 $priceparser = new PriceParser($this->db); 1778 $price_result = $priceparser->parseProductSupplier($prod_supplier); 1779 if ($price_result >= 0) { 1780 $obj->price = $price_result; 1781 } 1782 } 1783 $this->product_fourn_price_id = $obj->rowid; 1784 $this->buyprice = $obj->price; // deprecated 1785 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product of supplier 1786 $this->fourn_price_base_type = 'HT'; // Price base type 1787 $this->fourn_socid = $obj->fk_soc; // Company that offer this price 1788 $this->ref_fourn = $obj->ref_fourn; // deprecated 1789 $this->ref_supplier = $obj->ref_fourn; // Ref supplier 1790 $this->desc_supplier = $obj->desc_fourn; // desc supplier 1791 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed 1792 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier 1793 $this->default_vat_code = $obj->default_vat_code; // Vat code supplier 1794 $this->fourn_multicurrency_price = $obj->multicurrency_price; 1795 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice; 1796 $this->fourn_multicurrency_tx = $obj->multicurrency_tx; 1797 $this->fourn_multicurrency_id = $obj->fk_multicurrency; 1798 $this->fourn_multicurrency_code = $obj->multicurrency_code; 1799 if (!empty($conf->global->PRODUCT_USE_SUPPLIER_PACKAGING)) $this->packaging = $obj->packaging; 1800 $result = $obj->fk_product; 1801 return $result; 1802 } else { // If not found 1803 // 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. 1804 $sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.remise_percent, pfp.fk_soc,"; 1805 $sql .= " pfp.fk_product, pfp.ref_fourn as ref_supplier, pfp.desc_fourn as desc_supplier, pfp.tva_tx, pfp.fk_supplier_price_expression,"; 1806 $sql .= " pfp.default_vat_code,"; 1807 $sql .= " pfp.multicurrency_price, pfp.multicurrency_unitprice, pfp.multicurrency_tx, pfp.fk_multicurrency, pfp.multicurrency_code,"; 1808 $sql .= " pfp.packaging"; 1809 $sql .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp"; 1810 $sql .= " WHERE pfp.fk_product = ".$product_id; 1811 if ($fourn_ref != 'none') { $sql .= " AND pfp.ref_fourn = '".$this->db->escape($fourn_ref)."'"; 1812 } 1813 if ($fk_soc > 0) { $sql .= " AND pfp.fk_soc = ".$fk_soc; 1814 } 1815 if ($qty > 0) { $sql .= " AND pfp.quantity <= ".$qty; 1816 } 1817 $sql .= " ORDER BY pfp.quantity DESC"; 1818 $sql .= " LIMIT 1"; 1819 1820 dol_syslog(get_class($this)."::get_buyprice second search from qty/ref/product_id", LOG_DEBUG); 1821 $resql = $this->db->query($sql); 1822 if ($resql) { 1823 $obj = $this->db->fetch_object($resql); 1824 if ($obj && $obj->quantity > 0) // If found 1825 { 1826 if (!empty($conf->dynamicprices->enabled) && !empty($obj->fk_supplier_price_expression)) { 1827 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php'; 1828 $prod_supplier = new ProductFournisseur($this->db); 1829 $prod_supplier->product_fourn_price_id = $obj->rowid; 1830 $prod_supplier->id = $obj->fk_product; 1831 $prod_supplier->fourn_qty = $obj->quantity; 1832 $prod_supplier->fourn_tva_tx = $obj->tva_tx; 1833 $prod_supplier->fk_supplier_price_expression = $obj->fk_supplier_price_expression; 1834 $priceparser = new PriceParser($this->db); 1835 $price_result = $priceparser->parseProductSupplier($prod_supplier); 1836 if ($result >= 0) { 1837 $obj->price = $price_result; 1838 } 1839 } 1840 $this->product_fourn_price_id = $obj->rowid; 1841 $this->buyprice = $obj->price; // deprecated 1842 $this->fourn_qty = $obj->quantity; // min quantity for price for a virtual supplier 1843 $this->fourn_pu = $obj->price / $obj->quantity; // Unit price of product for a virtual supplier 1844 $this->fourn_price_base_type = 'HT'; // Price base type for a virtual supplier 1845 $this->fourn_socid = $obj->fk_soc; // Company that offer this price 1846 $this->ref_fourn = $obj->ref_supplier; // deprecated 1847 $this->ref_supplier = $obj->ref_supplier; // Ref supplier 1848 $this->desc_supplier = $obj->desc_supplier; // desc supplier 1849 $this->remise_percent = $obj->remise_percent; // remise percent if present and not typed 1850 $this->vatrate_supplier = $obj->tva_tx; // Vat ref supplier 1851 $this->default_vat_code = $obj->default_vat_code; // Vat code supplier 1852 $this->fourn_multicurrency_price = $obj->multicurrency_price; 1853 $this->fourn_multicurrency_unitprice = $obj->multicurrency_unitprice; 1854 $this->fourn_multicurrency_tx = $obj->multicurrency_tx; 1855 $this->fourn_multicurrency_id = $obj->fk_multicurrency; 1856 $this->fourn_multicurrency_code = $obj->multicurrency_code; 1857 if (!empty($conf->global->PRODUCT_USE_SUPPLIER_PACKAGING)) $this->packaging = $obj->packaging; 1858 $result = $obj->fk_product; 1859 return $result; 1860 } else { 1861 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é. 1862 } 1863 } else { 1864 $this->error = $this->db->lasterror(); 1865 return -3; 1866 } 1867 } 1868 } else { 1869 $this->error = $this->db->lasterror(); 1870 return -2; 1871 } 1872 } 1873 1874 1875 /** 1876 * Modify customer price of a product/Service 1877 * 1878 * @param double $newprice New price 1879 * @param string $newpricebase HT or TTC 1880 * @param User $user Object user that make change 1881 * @param double $newvat New VAT Rate (For example 8.5. Should not be a string) 1882 * @param double $newminprice New price min 1883 * @param int $level 0=standard, >0 = level if multilevel prices 1884 * @param int $newnpr 0=Standard vat rate, 1=Special vat rate for French NPR VAT 1885 * @param int $newpbq 1 if it has price by quantity 1886 * @param int $ignore_autogen Used to avoid infinite loops 1887 * @param array $localtaxes_array Array with localtaxes info array('0'=>type1,'1'=>rate1,'2'=>type2,'3'=>rate2) (loaded by getLocalTaxesFromRate(vatrate, 0, ...) function). 1888 * @param string $newdefaultvatcode Default vat code 1889 * @return int <0 if KO, >0 if OK 1890 */ 1891 public function updatePrice($newprice, $newpricebase, $user, $newvat = '', $newminprice = 0, $level = 0, $newnpr = 0, $newpbq = 0, $ignore_autogen = 0, $localtaxes_array = array(), $newdefaultvatcode = '') 1892 { 1893 global $conf, $langs; 1894 1895 $lastPriceData = $this->getArrayForPriceCompare($level); // temporary store current price before update 1896 1897 $id = $this->id; 1898 1899 dol_syslog(get_class($this)."::update_price id=".$id." newprice=".$newprice." newpricebase=".$newpricebase." newminprice=".$newminprice." level=".$level." npr=".$newnpr." newdefaultvatcode=".$newdefaultvatcode); 1900 1901 // Clean parameters 1902 if (empty($this->tva_tx)) { 1903 $this->tva_tx = 0; 1904 } 1905 if (empty($newnpr)) { 1906 $newnpr = 0; 1907 } 1908 if (empty($newminprice)) { 1909 $newminprice = 0; 1910 } 1911 if (empty($newminprice)) { 1912 $newminprice = 0; 1913 } 1914 1915 // Check parameters 1916 if ($newvat == '') { 1917 $newvat = $this->tva_tx; 1918 } 1919 1920 // If multiprices are enabled, then we check if the current product is subject to price autogeneration 1921 // Price will be modified ONLY when the first one is the one that is being modified 1922 if ((!empty($conf->global->PRODUIT_MULTIPRICES) || !empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES)) && !$ignore_autogen && $this->price_autogen && ($level == 1)) { 1923 return $this->generateMultiprices($user, $newprice, $newpricebase, $newvat, $newnpr, $newpbq); 1924 } 1925 1926 if (!empty($newminprice) && ($newminprice > $newprice)) { 1927 $this->error = 'ErrorPriceCantBeLowerThanMinPrice'; 1928 return -1; 1929 } 1930 1931 if ($newprice !== '' || $newprice === 0) { 1932 if ($newpricebase == 'TTC') { 1933 $price_ttc = price2num($newprice, 'MU'); 1934 $price = price2num($newprice) / (1 + ($newvat / 100)); 1935 $price = price2num($price, 'MU'); 1936 1937 if ($newminprice != '' || $newminprice == 0) { 1938 $price_min_ttc = price2num($newminprice, 'MU'); 1939 $price_min = price2num($newminprice) / (1 + ($newvat / 100)); 1940 $price_min = price2num($price_min, 'MU'); 1941 } else { 1942 $price_min = 0; 1943 $price_min_ttc = 0; 1944 } 1945 } else { 1946 $price = price2num($newprice, 'MU'); 1947 $price_ttc = ($newnpr != 1) ? price2num($newprice) * (1 + ($newvat / 100)) : $price; 1948 $price_ttc = price2num($price_ttc, 'MU'); 1949 1950 if ($newminprice !== '' || $newminprice === 0) { 1951 $price_min = price2num($newminprice, 'MU'); 1952 $price_min_ttc = price2num($newminprice) * (1 + ($newvat / 100)); 1953 $price_min_ttc = price2num($price_min_ttc, 'MU'); 1954 //print 'X'.$newminprice.'-'.$price_min; 1955 } else { 1956 $price_min = 0; 1957 $price_min_ttc = 0; 1958 } 1959 } 1960 //print 'x'.$id.'-'.$newprice.'-'.$newpricebase.'-'.$price.'-'.$price_ttc.'-'.$price_min.'-'.$price_min_ttc; 1961 1962 if (count($localtaxes_array) > 0) { 1963 $localtaxtype1 = $localtaxes_array['0']; 1964 $localtax1 = $localtaxes_array['1']; 1965 $localtaxtype2 = $localtaxes_array['2']; 1966 $localtax2 = $localtaxes_array['3']; 1967 } else // old method. deprecated because ot can't retrieve type 1968 { 1969 $localtaxtype1 = '0'; 1970 $localtax1 = get_localtax($newvat, 1); 1971 $localtaxtype2 = '0'; 1972 $localtax2 = get_localtax($newvat, 2); 1973 } 1974 if (empty($localtax1)) { 1975 $localtax1 = 0; // If = '' then = 0 1976 } 1977 if (empty($localtax2)) { 1978 $localtax2 = 0; // If = '' then = 0 1979 } 1980 1981 $this->db->begin(); 1982 1983 // Ne pas mettre de quote sur les numeriques decimaux. 1984 // Ceci provoque des stockages avec arrondis en base au lieu des valeurs exactes. 1985 $sql = "UPDATE ".MAIN_DB_PREFIX."product SET"; 1986 $sql .= " price_base_type='".$this->db->escape($newpricebase)."',"; 1987 $sql .= " price=".$price.","; 1988 $sql .= " price_ttc=".$price_ttc.","; 1989 $sql .= " price_min=".$price_min.","; 1990 $sql .= " price_min_ttc=".$price_min_ttc.","; 1991 $sql .= " localtax1_tx=".($localtax1 >= 0 ? $localtax1 : 'NULL').","; 1992 $sql .= " localtax2_tx=".($localtax2 >= 0 ? $localtax2 : 'NULL').","; 1993 $sql .= " localtax1_type=".($localtaxtype1 != '' ? "'".$this->db->escape($localtaxtype1)."'" : "'0'").","; 1994 $sql .= " localtax2_type=".($localtaxtype2 != '' ? "'".$this->db->escape($localtaxtype2)."'" : "'0'").","; 1995 $sql .= " default_vat_code=".($newdefaultvatcode ? "'".$this->db->escape($newdefaultvatcode)."'" : "null").","; 1996 $sql .= " tva_tx='".price2num($newvat)."',"; 1997 $sql .= " recuperableonly='".$this->db->escape($newnpr)."'"; 1998 $sql .= " WHERE rowid = ".$id; 1999 2000 dol_syslog(get_class($this)."::update_price", LOG_DEBUG); 2001 $resql = $this->db->query($sql); 2002 if ($resql) { 2003 $this->multiprices[$level] = $price; 2004 $this->multiprices_ttc[$level] = $price_ttc; 2005 $this->multiprices_min[$level] = $price_min; 2006 $this->multiprices_min_ttc[$level] = $price_min_ttc; 2007 $this->multiprices_base_type[$level] = $newpricebase; 2008 $this->multiprices_default_vat_code[$level] = $newdefaultvatcode; 2009 $this->multiprices_tva_tx[$level] = $newvat; 2010 $this->multiprices_recuperableonly[$level] = $newnpr; 2011 2012 $this->price = $price; 2013 $this->price_ttc = $price_ttc; 2014 $this->price_min = $price_min; 2015 $this->price_min_ttc = $price_min_ttc; 2016 $this->price_base_type = $newpricebase; 2017 $this->default_vat_code = $newdefaultvatcode; 2018 $this->tva_tx = $newvat; 2019 $this->tva_npr = $newnpr; 2020 //Local taxes 2021 $this->localtax1_tx = $localtax1; 2022 $this->localtax2_tx = $localtax2; 2023 $this->localtax1_type = $localtaxtype1; 2024 $this->localtax2_type = $localtaxtype2; 2025 2026 // Price by quantity 2027 $this->price_by_qty = $newpbq; 2028 2029 // check if price have really change before log 2030 $newPriceData = $this->getArrayForPriceCompare($level); 2031 if (!empty(array_diff_assoc($newPriceData, $lastPriceData)) || empty($conf->global->PRODUIT_MULTIPRICES)) { 2032 $this->_log_price($user, $level); // Save price for level into table product_price 2033 } 2034 2035 $this->level = $level; // Store level of price edited for trigger 2036 2037 // Call trigger 2038 $result = $this->call_trigger('PRODUCT_PRICE_MODIFY', $user); 2039 if ($result < 0) { 2040 $this->db->rollback(); 2041 return -1; 2042 } 2043 // End call triggers 2044 2045 $this->db->commit(); 2046 } else { 2047 $this->db->rollback(); 2048 dol_print_error($this->db); 2049 } 2050 } 2051 2052 return 1; 2053 } 2054 2055 /** 2056 * Sets the supplier price expression 2057 * 2058 * @param int $expression_id Expression 2059 * @return int <0 if KO, >0 if OK 2060 * @deprecated Use Product::update instead 2061 */ 2062 public function setPriceExpression($expression_id) 2063 { 2064 global $user; 2065 2066 $this->fk_price_expression = $expression_id; 2067 2068 return $this->update($this->id, $user); 2069 } 2070 2071 /** 2072 * Load a product in memory from database 2073 * 2074 * @param int $id Id of product/service to load 2075 * @param string $ref Ref of product/service to load 2076 * @param string $ref_ext Ref ext of product/service to load 2077 * @param string $barcode Barcode of product/service to load 2078 * @param int $ignore_expression Ignores the math expression for calculating price and uses the db value instead 2079 * @param int $ignore_price_load Load product without loading prices arrays (when we are sure we don't need them) 2080 * @param int $ignore_lang_load Load product without loading language arrays (when we are sure we don't need them) 2081 * @return int <0 if KO, 0 if not found, >0 if OK 2082 */ 2083 public function fetch($id = '', $ref = '', $ref_ext = '', $barcode = '', $ignore_expression = 0, $ignore_price_load = 0, $ignore_lang_load = 0) 2084 { 2085 include_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php'; 2086 2087 global $langs, $conf; 2088 2089 dol_syslog(get_class($this)."::fetch id=".$id." ref=".$ref." ref_ext=".$ref_ext); 2090 2091 // Check parameters 2092 if (!$id && !$ref && !$ref_ext && !$barcode) { 2093 $this->error = 'ErrorWrongParameters'; 2094 dol_syslog(get_class($this)."::fetch ".$this->error); 2095 return -1; 2096 } 2097 2098 $sql = "SELECT rowid, ref, ref_ext, label, description, url, note_public, note as note_private, customcode, fk_country, fk_state, price, price_ttc,"; 2099 $sql .= " price_min, price_min_ttc, price_base_type, cost_price, default_vat_code, tva_tx, recuperableonly as tva_npr, localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, tosell,"; 2100 $sql .= " tobuy, fk_product_type, duration, fk_default_warehouse, seuil_stock_alerte, canvas, net_measure, net_measure_units, weight, weight_units,"; 2101 $sql .= " length, length_units, width, width_units, height, height_units,"; 2102 $sql .= " surface, surface_units, volume, volume_units, barcode, fk_barcode_type, finished,"; 2103 $sql .= " accountancy_code_buy, accountancy_code_buy_intra, accountancy_code_buy_export,"; 2104 $sql .= " accountancy_code_sell, accountancy_code_sell_intra, accountancy_code_sell_export, stock, pmp,"; 2105 $sql .= " datec, tms, import_key, entity, desiredstock, tobatch, fk_unit,"; 2106 $sql .= " fk_price_expression, price_autogen, model_pdf"; 2107 $sql .= " FROM ".MAIN_DB_PREFIX."product"; 2108 if ($id) { 2109 $sql .= " WHERE rowid = ".(int) $id; 2110 } else { 2111 $sql .= " WHERE entity IN (".getEntity($this->element).")"; 2112 if ($ref) { 2113 $sql .= " AND ref = '".$this->db->escape($ref)."'"; 2114 } elseif ($ref_ext) { 2115 $sql .= " AND ref_ext = '".$this->db->escape($ref_ext)."'"; 2116 } elseif ($barcode) { 2117 $sql .= " AND barcode = '".$this->db->escape($barcode)."'"; 2118 } 2119 } 2120 2121 $resql = $this->db->query($sql); 2122 if ($resql) { 2123 unset($this->oldcopy); 2124 2125 if ($this->db->num_rows($resql) > 0) { 2126 $obj = $this->db->fetch_object($resql); 2127 2128 $this->id = $obj->rowid; 2129 $this->ref = $obj->ref; 2130 $this->ref_ext = $obj->ref_ext; 2131 $this->label = $obj->label; 2132 $this->description = $obj->description; 2133 $this->url = $obj->url; 2134 $this->note_public = $obj->note_public; 2135 $this->note_private = $obj->note_private; 2136 $this->note = $obj->note_private; // deprecated 2137 2138 $this->type = $obj->fk_product_type; 2139 $this->status = $obj->tosell; 2140 $this->status_buy = $obj->tobuy; 2141 $this->status_batch = $obj->tobatch; 2142 2143 $this->customcode = $obj->customcode; 2144 $this->country_id = $obj->fk_country; 2145 $this->country_code = getCountry($this->country_id, 2, $this->db); 2146 $this->state_id = $obj->fk_state; 2147 $this->price = $obj->price; 2148 $this->price_ttc = $obj->price_ttc; 2149 $this->price_min = $obj->price_min; 2150 $this->price_min_ttc = $obj->price_min_ttc; 2151 $this->price_base_type = $obj->price_base_type; 2152 $this->cost_price = $obj->cost_price; 2153 $this->default_vat_code = $obj->default_vat_code; 2154 $this->tva_tx = $obj->tva_tx; 2155 //! French VAT NPR 2156 $this->tva_npr = $obj->tva_npr; 2157 $this->recuperableonly = $obj->tva_npr; // For backward compatibility 2158 //! Local taxes 2159 $this->localtax1_tx = $obj->localtax1_tx; 2160 $this->localtax2_tx = $obj->localtax2_tx; 2161 $this->localtax1_type = $obj->localtax1_type; 2162 $this->localtax2_type = $obj->localtax2_type; 2163 2164 $this->finished = $obj->finished; 2165 $this->duration = $obj->duration; 2166 $this->duration_value = substr($obj->duration, 0, dol_strlen($obj->duration) - 1); 2167 $this->duration_unit = substr($obj->duration, -1); 2168 $this->canvas = $obj->canvas; 2169 $this->net_measure = $obj->net_measure; 2170 $this->net_measure_units = $obj->net_measure_units; 2171 $this->weight = $obj->weight; 2172 $this->weight_units = $obj->weight_units; 2173 $this->length = $obj->length; 2174 $this->length_units = $obj->length_units; 2175 $this->width = $obj->width; 2176 $this->width_units = $obj->width_units; 2177 $this->height = $obj->height; 2178 $this->height_units = $obj->height_units; 2179 2180 $this->surface = $obj->surface; 2181 $this->surface_units = $obj->surface_units; 2182 $this->volume = $obj->volume; 2183 $this->volume_units = $obj->volume_units; 2184 $this->barcode = $obj->barcode; 2185 $this->barcode_type = $obj->fk_barcode_type; 2186 2187 $this->accountancy_code_buy = $obj->accountancy_code_buy; 2188 $this->accountancy_code_buy_intra = $obj->accountancy_code_buy_intra; 2189 $this->accountancy_code_buy_export = $obj->accountancy_code_buy_export; 2190 $this->accountancy_code_sell = $obj->accountancy_code_sell; 2191 $this->accountancy_code_sell_intra = $obj->accountancy_code_sell_intra; 2192 $this->accountancy_code_sell_export = $obj->accountancy_code_sell_export; 2193 2194 $this->fk_default_warehouse = $obj->fk_default_warehouse; 2195 $this->seuil_stock_alerte = $obj->seuil_stock_alerte; 2196 $this->desiredstock = $obj->desiredstock; 2197 $this->stock_reel = $obj->stock; 2198 $this->pmp = $obj->pmp; 2199 2200 $this->date_creation = $obj->datec; 2201 $this->date_modification = $obj->tms; 2202 $this->import_key = $obj->import_key; 2203 $this->entity = $obj->entity; 2204 2205 $this->ref_ext = $obj->ref_ext; 2206 $this->fk_price_expression = $obj->fk_price_expression; 2207 $this->fk_unit = $obj->fk_unit; 2208 $this->price_autogen = $obj->price_autogen; 2209 $this->model_pdf = $obj->model_pdf; 2210 2211 $this->db->free($resql); 2212 2213 // Retrieve all extrafield 2214 // fetch optionals attributes and labels 2215 $this->fetch_optionals(); 2216 2217 // multilangs 2218 if (!empty($conf->global->MAIN_MULTILANGS) && empty($ignore_lang_load)) { 2219 $this->getMultiLangs(); 2220 } 2221 2222 // Load multiprices array 2223 if (!empty($conf->global->PRODUIT_MULTIPRICES) && empty($ignore_price_load)) // prices per segment 2224 { 2225 for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) 2226 { 2227 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,"; 2228 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly"; 2229 $sql .= " FROM ".MAIN_DB_PREFIX."product_price"; 2230 $sql .= " WHERE entity IN (".getEntity('productprice').")"; 2231 $sql .= " AND price_level=".$i; 2232 $sql .= " AND fk_product = ".$this->id; 2233 $sql .= " ORDER BY date_price DESC, rowid DESC"; 2234 $sql .= " LIMIT 1"; 2235 $resql = $this->db->query($sql); 2236 if ($resql) { 2237 $result = $this->db->fetch_array($resql); 2238 2239 $this->multiprices[$i] = $result ? $result["price"] : null; 2240 $this->multiprices_ttc[$i] = $result ? $result["price_ttc"] : null; 2241 $this->multiprices_min[$i] = $result ? $result["price_min"] : null; 2242 $this->multiprices_min_ttc[$i] = $result ? $result["price_min_ttc"] : null; 2243 $this->multiprices_base_type[$i] = $result ? $result["price_base_type"] : null; 2244 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on 2245 $this->multiprices_tva_tx[$i] = $result ? $result["tva_tx"].($result ? ' ('.$result['default_vat_code'].')' : '') : null; 2246 $this->multiprices_recuperableonly[$i] = $result ? $result["recuperableonly"] : null; 2247 2248 // Price by quantity 2249 /* 2250 $this->prices_by_qty[$i]=$result["price_by_qty"]; 2251 $this->prices_by_qty_id[$i]=$result["rowid"]; 2252 // Récuperation de la liste des prix selon qty si flag positionné 2253 if ($this->prices_by_qty[$i] == 1) 2254 { 2255 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type"; 2256 $sql.= " FROM ".MAIN_DB_PREFIX."product_price_by_qty"; 2257 $sql.= " WHERE fk_product_price = ".$this->prices_by_qty_id[$i]; 2258 $sql.= " ORDER BY quantity ASC"; 2259 $resultat=array(); 2260 $resql = $this->db->query($sql); 2261 if ($resql) 2262 { 2263 $ii=0; 2264 while ($result= $this->db->fetch_array($resql)) { 2265 $resultat[$ii]=array(); 2266 $resultat[$ii]["rowid"]=$result["rowid"]; 2267 $resultat[$ii]["price"]= $result["price"]; 2268 $resultat[$ii]["unitprice"]= $result["unitprice"]; 2269 $resultat[$ii]["quantity"]= $result["quantity"]; 2270 $resultat[$ii]["remise_percent"]= $result["remise_percent"]; 2271 $resultat[$ii]["remise"]= $result["remise"]; // deprecated 2272 $resultat[$ii]["price_base_type"]= $result["price_base_type"]; 2273 $ii++; 2274 } 2275 $this->prices_by_qty_list[$i]=$resultat; 2276 } 2277 else 2278 { 2279 dol_print_error($this->db); 2280 return -1; 2281 } 2282 }*/ 2283 } else { 2284 $this->error = $this->db->lasterror; 2285 return -1; 2286 } 2287 } 2288 } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES) && empty($ignore_price_load)) // prices per customers 2289 { 2290 // Nothing loaded by default. List may be very long. 2291 } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY) && empty($ignore_price_load)) // prices per quantity 2292 { 2293 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,"; 2294 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid"; 2295 $sql .= " FROM ".MAIN_DB_PREFIX."product_price"; 2296 $sql .= " WHERE fk_product = ".$this->id; 2297 $sql .= " ORDER BY date_price DESC, rowid DESC"; 2298 $sql .= " LIMIT 1"; 2299 $resql = $this->db->query($sql); 2300 if ($resql) { 2301 $result = $this->db->fetch_array($resql); 2302 2303 // Price by quantity 2304 $this->prices_by_qty[0] = $result["price_by_qty"]; 2305 $this->prices_by_qty_id[0] = $result["rowid"]; 2306 // Récuperation de la liste des prix selon qty si flag positionné 2307 if ($this->prices_by_qty[0] == 1) { 2308 $sql = "SELECT rowid,price, unitprice, quantity, remise_percent, remise, remise, price_base_type"; 2309 $sql .= " FROM ".MAIN_DB_PREFIX."product_price_by_qty"; 2310 $sql .= " WHERE fk_product_price = ".$this->prices_by_qty_id[0]; 2311 $sql .= " ORDER BY quantity ASC"; 2312 $resultat = array(); 2313 $resql = $this->db->query($sql); 2314 if ($resql) { 2315 $ii = 0; 2316 while ($result = $this->db->fetch_array($resql)) { 2317 $resultat[$ii] = array(); 2318 $resultat[$ii]["rowid"] = $result["rowid"]; 2319 $resultat[$ii]["price"] = $result["price"]; 2320 $resultat[$ii]["unitprice"] = $result["unitprice"]; 2321 $resultat[$ii]["quantity"] = $result["quantity"]; 2322 $resultat[$ii]["remise_percent"] = $result["remise_percent"]; 2323 //$resultat[$ii]["remise"]= $result["remise"]; // deprecated 2324 $resultat[$ii]["price_base_type"] = $result["price_base_type"]; 2325 $ii++; 2326 } 2327 $this->prices_by_qty_list[0] = $resultat; 2328 } else { 2329 $this->error = $this->db->lasterror; 2330 return -1; 2331 } 2332 } 2333 } else { 2334 $this->error = $this->db->lasterror; 2335 return -1; 2336 } 2337 } elseif (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES) && empty($ignore_price_load)) // prices per customer and quantity 2338 { 2339 for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) 2340 { 2341 $sql = "SELECT price, price_ttc, price_min, price_min_ttc,"; 2342 $sql .= " price_base_type, tva_tx, default_vat_code, tosell, price_by_qty, rowid, recuperableonly"; 2343 $sql .= " FROM ".MAIN_DB_PREFIX."product_price"; 2344 $sql .= " WHERE entity IN (".getEntity('productprice').")"; 2345 $sql .= " AND price_level=".$i; 2346 $sql .= " AND fk_product = ".$this->id; 2347 $sql .= " ORDER BY date_price DESC, rowid DESC"; 2348 $sql .= " LIMIT 1"; 2349 $resql = $this->db->query($sql); 2350 if ($resql) { 2351 $result = $this->db->fetch_array($resql); 2352 2353 $this->multiprices[$i] = $result["price"]; 2354 $this->multiprices_ttc[$i] = $result["price_ttc"]; 2355 $this->multiprices_min[$i] = $result["price_min"]; 2356 $this->multiprices_min_ttc[$i] = $result["price_min_ttc"]; 2357 $this->multiprices_base_type[$i] = $result["price_base_type"]; 2358 // Next two fields are used only if PRODUIT_MULTIPRICES_USE_VAT_PER_LEVEL is on 2359 $this->multiprices_tva_tx[$i] = $result["tva_tx"]; // TODO Add ' ('.$result['default_vat_code'].')' 2360 $this->multiprices_recuperableonly[$i] = $result["recuperableonly"]; 2361 2362 // Price by quantity 2363 $this->prices_by_qty[$i] = $result["price_by_qty"]; 2364 $this->prices_by_qty_id[$i] = $result["rowid"]; 2365 // Récuperation de la liste des prix selon qty si flag positionné 2366 if ($this->prices_by_qty[$i] == 1) { 2367 $sql = "SELECT rowid, price, unitprice, quantity, remise_percent, remise, price_base_type"; 2368 $sql .= " FROM ".MAIN_DB_PREFIX."product_price_by_qty"; 2369 $sql .= " WHERE fk_product_price = ".$this->prices_by_qty_id[$i]; 2370 $sql .= " ORDER BY quantity ASC"; 2371 $resultat = array(); 2372 $resql = $this->db->query($sql); 2373 if ($resql) { 2374 $ii = 0; 2375 while ($result = $this->db->fetch_array($resql)) { 2376 $resultat[$ii] = array(); 2377 $resultat[$ii]["rowid"] = $result["rowid"]; 2378 $resultat[$ii]["price"] = $result["price"]; 2379 $resultat[$ii]["unitprice"] = $result["unitprice"]; 2380 $resultat[$ii]["quantity"] = $result["quantity"]; 2381 $resultat[$ii]["remise_percent"] = $result["remise_percent"]; 2382 $resultat[$ii]["remise"] = $result["remise"]; // deprecated 2383 $resultat[$ii]["price_base_type"] = $result["price_base_type"]; 2384 $ii++; 2385 } 2386 $this->prices_by_qty_list[$i] = $resultat; 2387 } else { 2388 $this->error = $this->db->lasterror; 2389 return -1; 2390 } 2391 } 2392 } else { 2393 $this->error = $this->db->lasterror; 2394 return -1; 2395 } 2396 } 2397 } 2398 2399 if (!empty($conf->dynamicprices->enabled) && !empty($this->fk_price_expression) && empty($ignore_expression)) { 2400 include_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_parser.class.php'; 2401 $priceparser = new PriceParser($this->db); 2402 $price_result = $priceparser->parseProduct($this); 2403 if ($price_result >= 0) { 2404 $this->price = $price_result; 2405 // Calculate the VAT 2406 $this->price_ttc = price2num($this->price) * (1 + ($this->tva_tx / 100)); 2407 $this->price_ttc = price2num($this->price_ttc, 'MU'); 2408 } 2409 } 2410 2411 // We should not load stock during the fetch. If someone need stock of product, he must call load_stock after fetching product. 2412 // Instead we just init the stock_warehouse array 2413 $this->stock_warehouse = array(); 2414 2415 return 1; 2416 } else { 2417 return 0; 2418 } 2419 } else { 2420 $this->error = $this->db->lasterror; 2421 return -1; 2422 } 2423 } 2424 2425 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 2426 /** 2427 * Charge tableau des stats OF pour le produit/service 2428 * 2429 * @param int $socid Id societe 2430 * @return int Array of stats in $this->stats_mo, <0 if ko or >0 if ok 2431 */ 2432 public function load_stats_mo($socid = 0) 2433 { 2434 // phpcs:enable 2435 global $user, $hookmanager, $action; 2436 2437 $error = 0; 2438 2439 foreach (array('toconsume', 'consumed', 'toproduce', 'produced') as $role) { 2440 $this->stats_mo['customers_'.$role] = 0; 2441 $this->stats_mo['nb_'.$role] = 0; 2442 $this->stats_mo['qty_'.$role] = 0; 2443 2444 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,"; 2445 $sql .= " SUM(mp.qty) as qty"; 2446 $sql .= " FROM ".MAIN_DB_PREFIX."mrp_mo as c"; 2447 $sql .= " INNER JOIN ".MAIN_DB_PREFIX."mrp_production as mp ON mp.fk_mo=c.rowid"; 2448 if (empty($user->rights->societe->client->voir) && !$socid) { 2449 $sql .= "INNER JOIN ".MAIN_DB_PREFIX."societe_commerciaux as sc ON sc.fk_soc=c.fk_soc AND sc.fk_user = ".$user->id; 2450 } 2451 $sql .= " WHERE "; 2452 $sql .= " c.entity IN (".getEntity('mo').")"; 2453 2454 $sql .= " AND mp.fk_product =".$this->id; 2455 $sql .= " AND mp.role ='".$this->db->escape($role)."'"; 2456 if ($socid > 0) { 2457 $sql .= " AND c.fk_soc = ".$socid; 2458 } 2459 2460 $result = $this->db->query($sql); 2461 if ($result) { 2462 $obj = $this->db->fetch_object($result); 2463 $this->stats_mo['customers_'.$role] = $obj->nb_customers ? $obj->nb_customers : 0; 2464 $this->stats_mo['nb_'.$role] = $obj->nb ? $obj->nb : 0; 2465 $this->stats_mo['qty_'.$role] = $obj->qty ? $obj->qty : 0; 2466 } else { 2467 $this->error = $this->db->error(); 2468 $error++; 2469 } 2470 } 2471 2472 if (!empty($error)) { 2473 return -1; 2474 } 2475 2476 $parameters = array('socid' => $socid); 2477 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action); 2478 if ($reshook > 0) $this->stats_mo = $hookmanager->resArray['stats_mo']; 2479 2480 return 1; 2481 } 2482 2483 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 2484 /** 2485 * Charge tableau des stats OF pour le produit/service 2486 * 2487 * @param int $socid Id societe 2488 * @return int Array of stats in $this->stats_bom, <0 if ko or >0 if ok 2489 */ 2490 public function load_stats_bom($socid = 0) 2491 { 2492 // phpcs:enable 2493 global $user, $hookmanager; 2494 2495 $error = 0; 2496 2497 $this->stats_bom['nb_toproduce'] = 0; 2498 $this->stats_bom['nb_toconsume'] = 0; 2499 $this->stats_bom['qty_toproduce'] = 0; 2500 $this->stats_bom['qty_toconsume'] = 0; 2501 2502 $sql = "SELECT COUNT(DISTINCT b.rowid) as nb_toproduce,"; 2503 $sql .= " SUM(b.qty) as qty_toproduce"; 2504 $sql .= " FROM ".MAIN_DB_PREFIX."bom_bom as b"; 2505 $sql .= " INNER JOIN ".MAIN_DB_PREFIX."bom_bomline as bl ON bl.fk_bom=b.rowid"; 2506 $sql .= " WHERE "; 2507 $sql .= " b.entity IN (".getEntity('bom').")"; 2508 $sql .= " AND b.fk_product =".$this->id; 2509 $sql .= " GROUP BY b.rowid"; 2510 2511 $result = $this->db->query($sql); 2512 if ($result) { 2513 $obj = $this->db->fetch_object($result); 2514 $this->stats_bom['nb_toproduce'] = $obj->nb_toproduce ? $obj->nb_toproduce : 0; 2515 $this->stats_bom['qty_toproduce'] = $obj->qty_toproduce ? price2num($obj->qty_toproduce) : 0; 2516 } else { 2517 $this->error = $this->db->error(); 2518 $error++; 2519 } 2520 2521 $sql = "SELECT COUNT(DISTINCT bl.rowid) as nb_toconsume,"; 2522 $sql .= " SUM(bl.qty) as qty_toconsume"; 2523 $sql .= " FROM ".MAIN_DB_PREFIX."bom_bom as b"; 2524 $sql .= " INNER JOIN ".MAIN_DB_PREFIX."bom_bomline as bl ON bl.fk_bom=b.rowid"; 2525 $sql .= " WHERE "; 2526 $sql .= " b.entity IN (".getEntity('bom').")"; 2527 $sql .= " AND bl.fk_product =".$this->id; 2528 2529 $result = $this->db->query($sql); 2530 if ($result) { 2531 $obj = $this->db->fetch_object($result); 2532 $this->stats_bom['nb_toconsume'] = $obj->nb_toconsume ? $obj->nb_toconsume : 0; 2533 $this->stats_bom['qty_toconsume'] = $obj->qty_toconsume ? price2num($obj->qty_toconsume) : 0; 2534 } else { 2535 $this->error = $this->db->error(); 2536 $error++; 2537 } 2538 2539 if (!empty($error)) { 2540 return -1; 2541 } 2542 2543 $parameters = array('socid' => $socid); 2544 $reshook = $hookmanager->executeHooks('loadStatsCustomerMO', $parameters, $this, $action); 2545 if ($reshook > 0) $this->stats_bom = $hookmanager->resArray['stats_bom']; 2546 2547 return 1; 2548 } 2549 2550 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 2551 /** 2552 * Charge tableau des stats propale pour le produit/service 2553 * 2554 * @param int $socid Id societe 2555 * @return int Array of stats in $this->stats_propale, <0 if ko or >0 if ok 2556 */ 2557 public function load_stats_propale($socid = 0) 2558 { 2559 // phpcs:enable 2560 global $conf, $user, $hookmanager; 2561 2562 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_customers, COUNT(DISTINCT p.rowid) as nb,"; 2563 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty"; 2564 $sql .= " FROM ".MAIN_DB_PREFIX."propaldet as pd"; 2565 $sql .= ", ".MAIN_DB_PREFIX."propal as p"; 2566 $sql .= ", ".MAIN_DB_PREFIX."societe as s"; 2567 if (empty($user->rights->societe->client->voir) && !$socid) { 2568 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 2569 } 2570 $sql .= " WHERE p.rowid = pd.fk_propal"; 2571 $sql .= " AND p.fk_soc = s.rowid"; 2572 $sql .= " AND p.entity IN (".getEntity('propal').")"; 2573 $sql .= " AND pd.fk_product = ".$this->id; 2574 if (empty($user->rights->societe->client->voir) && !$socid) { 2575 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 2576 } 2577 //$sql.= " AND pr.fk_statut != 0"; 2578 if ($socid > 0) { 2579 $sql .= " AND p.fk_soc = ".$socid; 2580 } 2581 2582 $result = $this->db->query($sql); 2583 if ($result) { 2584 $obj = $this->db->fetch_object($result); 2585 $this->stats_propale['customers'] = $obj->nb_customers; 2586 $this->stats_propale['nb'] = $obj->nb; 2587 $this->stats_propale['rows'] = $obj->nb_rows; 2588 $this->stats_propale['qty'] = $obj->qty ? $obj->qty : 0; 2589 2590 // if it's a virtual product, maybe it is in proposal by extension 2591 if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) { 2592 $TFather = $this->getFather(); 2593 if (is_array($TFather) && !empty($TFather)) { 2594 foreach ($TFather as &$fatherData) { 2595 $pFather = new Product($this->db); 2596 $pFather->id = $fatherData['id']; 2597 $qtyCoef = $fatherData['qty']; 2598 2599 if ($fatherData['incdec']) { 2600 $pFather->load_stats_propale($socid); 2601 2602 $this->stats_propale['customers'] += $pFather->stats_propale['customers']; 2603 $this->stats_propale['nb'] += $pFather->stats_propale['nb']; 2604 $this->stats_propale['rows'] += $pFather->stats_propale['rows']; 2605 $this->stats_propale['qty'] += $pFather->stats_propale['qty'] * $qtyCoef; 2606 } 2607 } 2608 } 2609 } 2610 2611 $parameters = array('socid' => $socid); 2612 $reshook = $hookmanager->executeHooks('loadStatsCustomerProposal', $parameters, $this, $action); 2613 if ($reshook > 0) $this->stats_propale = $hookmanager->resArray['stats_propale']; 2614 2615 return 1; 2616 } else { 2617 $this->error = $this->db->error(); 2618 return -1; 2619 } 2620 } 2621 2622 2623 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 2624 /** 2625 * Charge tableau des stats propale pour le produit/service 2626 * 2627 * @param int $socid Id thirdparty 2628 * @return int Array of stats in $this->stats_proposal_supplier, <0 if ko or >0 if ok 2629 */ 2630 public function load_stats_proposal_supplier($socid = 0) 2631 { 2632 // phpcs:enable 2633 global $conf, $user, $hookmanager, $action; 2634 2635 $sql = "SELECT COUNT(DISTINCT p.fk_soc) as nb_suppliers, COUNT(DISTINCT p.rowid) as nb,"; 2636 $sql .= " COUNT(pd.rowid) as nb_rows, SUM(pd.qty) as qty"; 2637 $sql .= " FROM ".MAIN_DB_PREFIX."supplier_proposaldet as pd"; 2638 $sql .= ", ".MAIN_DB_PREFIX."supplier_proposal as p"; 2639 $sql .= ", ".MAIN_DB_PREFIX."societe as s"; 2640 if (empty($user->rights->societe->client->voir) && !$socid) { 2641 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 2642 } 2643 $sql .= " WHERE p.rowid = pd.fk_supplier_proposal"; 2644 $sql .= " AND p.fk_soc = s.rowid"; 2645 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")"; 2646 $sql .= " AND pd.fk_product = ".$this->id; 2647 if (empty($user->rights->societe->client->voir) && !$socid) { 2648 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 2649 } 2650 //$sql.= " AND pr.fk_statut != 0"; 2651 if ($socid > 0) { 2652 $sql .= " AND p.fk_soc = ".$socid; 2653 } 2654 2655 $result = $this->db->query($sql); 2656 if ($result) { 2657 $obj = $this->db->fetch_object($result); 2658 $this->stats_proposal_supplier['suppliers'] = $obj->nb_suppliers; 2659 $this->stats_proposal_supplier['nb'] = $obj->nb; 2660 $this->stats_proposal_supplier['rows'] = $obj->nb_rows; 2661 $this->stats_proposal_supplier['qty'] = $obj->qty ? $obj->qty : 0; 2662 2663 $parameters = array('socid' => $socid); 2664 $reshook = $hookmanager->executeHooks('loadStatsSupplierProposal', $parameters, $this, $action); 2665 if ($reshook > 0) $this->stats_proposal_supplier = $hookmanager->resArray['stats_proposal_supplier']; 2666 2667 return 1; 2668 } else { 2669 $this->error = $this->db->error(); 2670 return -1; 2671 } 2672 } 2673 2674 2675 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 2676 /** 2677 * Charge tableau des stats commande client pour le produit/service 2678 * 2679 * @param int $socid Id societe pour filtrer sur une societe 2680 * @param string $filtrestatut Id statut pour filtrer sur un statut 2681 * @param int $forVirtualStock Ignore rights filter for virtual stock calculation. 2682 * @return integer Array of stats in $this->stats_commande (nb=nb of order, qty=qty ordered), <0 if ko or >0 if ok 2683 */ 2684 public function load_stats_commande($socid = 0, $filtrestatut = '', $forVirtualStock = 0) 2685 { 2686 // phpcs:enable 2687 global $conf, $user, $hookmanager; 2688 2689 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,"; 2690 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty"; 2691 $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd"; 2692 $sql .= ", ".MAIN_DB_PREFIX."commande as c"; 2693 $sql .= ", ".MAIN_DB_PREFIX."societe as s"; 2694 if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) { 2695 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 2696 } 2697 $sql .= " WHERE c.rowid = cd.fk_commande"; 2698 $sql .= " AND c.fk_soc = s.rowid"; 2699 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'commande').")"; 2700 $sql .= " AND cd.fk_product = ".$this->id; 2701 if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) { 2702 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 2703 } 2704 if ($socid > 0) { 2705 $sql .= " AND c.fk_soc = ".$socid; 2706 } 2707 if ($filtrestatut <> '') { 2708 $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")"; 2709 } 2710 2711 $result = $this->db->query($sql); 2712 if ($result) { 2713 $obj = $this->db->fetch_object($result); 2714 $this->stats_commande['customers'] = $obj->nb_customers; 2715 $this->stats_commande['nb'] = $obj->nb; 2716 $this->stats_commande['rows'] = $obj->nb_rows; 2717 $this->stats_commande['qty'] = $obj->qty ? $obj->qty : 0; 2718 2719 // if it's a virtual product, maybe it is in order by extension 2720 if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) { 2721 $TFather = $this->getFather(); 2722 if (is_array($TFather) && !empty($TFather)) { 2723 foreach ($TFather as &$fatherData) { 2724 $pFather = new Product($this->db); 2725 $pFather->id = $fatherData['id']; 2726 $qtyCoef = $fatherData['qty']; 2727 2728 if ($fatherData['incdec']) { 2729 $pFather->load_stats_commande($socid, $filtrestatut); 2730 2731 $this->stats_commande['customers'] += $pFather->stats_commande['customers']; 2732 $this->stats_commande['nb'] += $pFather->stats_commande['nb']; 2733 $this->stats_commande['rows'] += $pFather->stats_commande['rows']; 2734 $this->stats_commande['qty'] += $pFather->stats_commande['qty'] * $qtyCoef; 2735 } 2736 } 2737 } 2738 } 2739 2740 // If stock decrease is on invoice validation, the theorical stock continue to 2741 // count the orders to ship in theorical stock when some are already removed b invoice validation. 2742 // If option DECREASE_ONLY_UNINVOICEDPRODUCTS is on, we make a compensation. 2743 if (!empty($conf->global->STOCK_CALCULATE_ON_BILL)) { 2744 if (!empty($conf->global->DECREASE_ONLY_UNINVOICEDPRODUCTS)) { 2745 $adeduire = 0; 2746 $sql = "SELECT sum(fd.qty) as count FROM ".MAIN_DB_PREFIX."facturedet fd "; 2747 $sql .= " JOIN ".MAIN_DB_PREFIX."facture f ON fd.fk_facture = f.rowid "; 2748 $sql .= " JOIN ".MAIN_DB_PREFIX."element_element el ON el.fk_target = f.rowid and el.targettype = 'facture' and sourcetype = 'commande'"; 2749 $sql .= " JOIN ".MAIN_DB_PREFIX."commande c ON el.fk_source = c.rowid "; 2750 $sql .= " WHERE c.fk_statut IN (".$filtrestatut.") AND c.facture = 0 AND fd.fk_product = ".$this->id; 2751 dol_syslog(__METHOD__.":: sql $sql", LOG_NOTICE); 2752 2753 $resql = $this->db->query($sql); 2754 if ($resql) { 2755 if ($this->db->num_rows($resql) > 0) { 2756 $obj = $this->db->fetch_object($resql); 2757 $adeduire += $obj->count; 2758 } 2759 } 2760 2761 $this->stats_commande['qty'] -= $adeduire; 2762 } 2763 } 2764 2765 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock); 2766 $reshook = $hookmanager->executeHooks('loadStatsCustomerOrder', $parameters, $this, $action); 2767 if ($reshook > 0) $this->stats_commande = $hookmanager->resArray['stats_commande']; 2768 return 1; 2769 } else { 2770 $this->error = $this->db->error(); 2771 return -1; 2772 } 2773 } 2774 2775 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 2776 /** 2777 * Charge tableau des stats commande fournisseur pour le produit/service 2778 * 2779 * @param int $socid Id societe pour filtrer sur une societe 2780 * @param string $filtrestatut Id des statuts pour filtrer sur des statuts 2781 * @param int $forVirtualStock Ignore rights filter for virtual stock calculation. 2782 * @return int Array of stats in $this->stats_commande_fournisseur, <0 if ko or >0 if ok 2783 */ 2784 public function load_stats_commande_fournisseur($socid = 0, $filtrestatut = '', $forVirtualStock = 0) 2785 { 2786 // phpcs:enable 2787 global $conf, $user, $hookmanager, $action; 2788 2789 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_suppliers, COUNT(DISTINCT c.rowid) as nb,"; 2790 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty"; 2791 $sql .= " FROM ".MAIN_DB_PREFIX."commande_fournisseurdet as cd"; 2792 $sql .= ", ".MAIN_DB_PREFIX."commande_fournisseur as c"; 2793 $sql .= ", ".MAIN_DB_PREFIX."societe as s"; 2794 if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) { 2795 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 2796 } 2797 $sql .= " WHERE c.rowid = cd.fk_commande"; 2798 $sql .= " AND c.fk_soc = s.rowid"; 2799 $sql .= " AND c.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'supplier_order').")"; 2800 $sql .= " AND cd.fk_product = ".$this->id; 2801 if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) { 2802 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 2803 } 2804 if ($socid > 0) { 2805 $sql .= " AND c.fk_soc = ".$socid; 2806 } 2807 if ($filtrestatut != '') { 2808 $sql .= " AND c.fk_statut in (".$this->db->sanitize($filtrestatut).")"; // Peut valoir 0 2809 } 2810 2811 $result = $this->db->query($sql); 2812 if ($result) { 2813 $obj = $this->db->fetch_object($result); 2814 $this->stats_commande_fournisseur['suppliers'] = $obj->nb_suppliers; 2815 $this->stats_commande_fournisseur['nb'] = $obj->nb; 2816 $this->stats_commande_fournisseur['rows'] = $obj->nb_rows; 2817 $this->stats_commande_fournisseur['qty'] = $obj->qty ? $obj->qty : 0; 2818 2819 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock); 2820 $reshook = $hookmanager->executeHooks('loadStatsSupplierOrder', $parameters, $this, $action); 2821 if ($reshook > 0) $this->stats_commande_fournisseur = $hookmanager->resArray['stats_commande_fournisseur']; 2822 2823 return 1; 2824 } else { 2825 $this->error = $this->db->error().' sql='.$sql; 2826 return -1; 2827 } 2828 } 2829 2830 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 2831 /** 2832 * Charge tableau des stats expedition client pour le produit/service 2833 * 2834 * @param int $socid Id societe pour filtrer sur une societe 2835 * @param string $filtrestatut [=''] Ids order status separated by comma 2836 * @param int $forVirtualStock Ignore rights filter for virtual stock calculation. 2837 * @param string $filterShipmentStatus [=''] Ids shipment status separated by comma 2838 * @return int Array of stats in $this->stats_expedition, <0 if ko or >0 if ok 2839 */ 2840 public function load_stats_sending($socid = 0, $filtrestatut = '', $forVirtualStock = 0, $filterShipmentStatus = '') 2841 { 2842 // phpcs:enable 2843 global $conf, $user, $hookmanager; 2844 2845 $sql = "SELECT COUNT(DISTINCT e.fk_soc) as nb_customers, COUNT(DISTINCT e.rowid) as nb,"; 2846 $sql .= " COUNT(ed.rowid) as nb_rows, SUM(ed.qty) as qty"; 2847 $sql .= " FROM ".MAIN_DB_PREFIX."expeditiondet as ed"; 2848 $sql .= ", ".MAIN_DB_PREFIX."commandedet as cd"; 2849 $sql .= ", ".MAIN_DB_PREFIX."commande as c"; 2850 $sql .= ", ".MAIN_DB_PREFIX."expedition as e"; 2851 $sql .= ", ".MAIN_DB_PREFIX."societe as s"; 2852 if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) { 2853 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 2854 } 2855 $sql .= " WHERE e.rowid = ed.fk_expedition"; 2856 $sql .= " AND c.rowid = cd.fk_commande"; 2857 $sql .= " AND e.fk_soc = s.rowid"; 2858 $sql .= " AND e.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'expedition').")"; 2859 $sql .= " AND ed.fk_origin_line = cd.rowid"; 2860 $sql .= " AND cd.fk_product = ".$this->id; 2861 if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) { 2862 $sql .= " AND e.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 2863 } 2864 if ($socid > 0) { 2865 $sql .= " AND e.fk_soc = ".$socid; 2866 } 2867 if ($filtrestatut <> '') { 2868 $sql .= " AND c.fk_statut IN (".$this->db->sanitize($filtrestatut).")"; 2869 } 2870 if (!empty($filterShipmentStatus)) $sql .= " AND e.fk_statut IN (".$this->db->sanitize($filterShipmentStatus).")"; 2871 2872 $result = $this->db->query($sql); 2873 if ($result) { 2874 $obj = $this->db->fetch_object($result); 2875 $this->stats_expedition['customers'] = $obj->nb_customers; 2876 $this->stats_expedition['nb'] = $obj->nb; 2877 $this->stats_expedition['rows'] = $obj->nb_rows; 2878 $this->stats_expedition['qty'] = $obj->qty ? $obj->qty : 0; 2879 2880 // if it's a virtual product, maybe it is in sending by extension 2881 if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) { 2882 $TFather = $this->getFather(); 2883 if (is_array($TFather) && !empty($TFather)) { 2884 foreach ($TFather as &$fatherData) { 2885 $pFather = new Product($this->db); 2886 $pFather->id = $fatherData['id']; 2887 $qtyCoef = $fatherData['qty']; 2888 2889 if ($fatherData['incdec']) { 2890 $pFather->load_stats_sending($socid, $filtrestatut, $forVirtualStock); 2891 2892 $this->stats_expedition['customers'] += $pFather->stats_expedition['customers']; 2893 $this->stats_expedition['nb'] += $pFather->stats_expedition['nb']; 2894 $this->stats_expedition['rows'] += $pFather->stats_expedition['rows']; 2895 $this->stats_expedition['qty'] += $pFather->stats_expedition['qty'] * $qtyCoef; 2896 } 2897 } 2898 } 2899 } 2900 2901 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock, 'filterShipmentStatus' => $filterShipmentStatus); 2902 $reshook = $hookmanager->executeHooks('loadStatsSending', $parameters, $this, $action); 2903 if ($reshook > 0) $this->stats_expedition = $hookmanager->resArray['stats_expedition']; 2904 2905 return 1; 2906 } else { 2907 $this->error = $this->db->error(); 2908 return -1; 2909 } 2910 } 2911 2912 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 2913 /** 2914 * Charge tableau des stats réception fournisseur pour le produit/service 2915 * 2916 * @param int $socid Id societe pour filtrer sur une societe 2917 * @param string $filtrestatut Id statut pour filtrer sur un statut 2918 * @param int $forVirtualStock Ignore rights filter for virtual stock calculation. 2919 * @return int Array of stats in $this->stats_reception, <0 if ko or >0 if ok 2920 */ 2921 public function load_stats_reception($socid = 0, $filtrestatut = '', $forVirtualStock = 0) 2922 { 2923 // phpcs:enable 2924 global $conf, $user, $hookmanager, $action; 2925 2926 $sql = "SELECT COUNT(DISTINCT cf.fk_soc) as nb_suppliers, COUNT(DISTINCT cf.rowid) as nb,"; 2927 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty"; 2928 $sql .= " FROM ".MAIN_DB_PREFIX."commande_fournisseur_dispatch as fd"; 2929 $sql .= ", ".MAIN_DB_PREFIX."commande_fournisseur as cf"; 2930 $sql .= ", ".MAIN_DB_PREFIX."societe as s"; 2931 if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) { 2932 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 2933 } 2934 $sql .= " WHERE cf.rowid = fd.fk_commande"; 2935 $sql .= " AND cf.fk_soc = s.rowid"; 2936 $sql .= " AND cf.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'supplier_order').")"; 2937 $sql .= " AND fd.fk_product = ".$this->id; 2938 if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) { 2939 $sql .= " AND cf.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 2940 } 2941 if ($socid > 0) { 2942 $sql .= " AND cf.fk_soc = ".$socid; 2943 } 2944 if ($filtrestatut <> '') { 2945 $sql .= " AND cf.fk_statut IN (".$this->db->sanitize($filtrestatut).")"; 2946 } 2947 2948 $result = $this->db->query($sql); 2949 if ($result) { 2950 $obj = $this->db->fetch_object($result); 2951 $this->stats_reception['suppliers'] = $obj->nb_suppliers; 2952 $this->stats_reception['nb'] = $obj->nb; 2953 $this->stats_reception['rows'] = $obj->nb_rows; 2954 $this->stats_reception['qty'] = $obj->qty ? $obj->qty : 0; 2955 2956 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock); 2957 $reshook = $hookmanager->executeHooks('loadStatsReception', $parameters, $this, $action); 2958 if ($reshook > 0) $this->stats_reception = $hookmanager->resArray['stats_reception']; 2959 2960 return 1; 2961 } else { 2962 $this->error = $this->db->error(); 2963 return -1; 2964 } 2965 } 2966 2967 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 2968 /** 2969 * Charge tableau des stats production pour le produit/service 2970 * 2971 * @param int $socid Id societe pour filtrer sur une societe 2972 * @param string $filtrestatut Id statut pour filtrer sur un statut 2973 * @param int $forVirtualStock Ignore rights filter for virtual stock calculation. 2974 * @return integer Array of stats in $this->stats_mrptoproduce (nb=nb of order, qty=qty ordered), <0 if ko or >0 if ok 2975 */ 2976 public function load_stats_inproduction($socid = 0, $filtrestatut = '', $forVirtualStock = 0) 2977 { 2978 // phpcs:enable 2979 global $conf, $user, $hookmanager; 2980 2981 $sql = "SELECT COUNT(DISTINCT m.fk_soc) as nb_customers, COUNT(DISTINCT m.rowid) as nb,"; 2982 $sql .= " COUNT(mp.rowid) as nb_rows, SUM(mp.qty) as qty, role"; 2983 $sql .= " FROM ".MAIN_DB_PREFIX."mrp_production as mp"; 2984 $sql .= ", ".MAIN_DB_PREFIX."mrp_mo as m"; 2985 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON s.rowid = m.fk_soc"; 2986 if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) { 2987 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 2988 } 2989 $sql .= " WHERE m.rowid = mp.fk_mo"; 2990 $sql .= " AND m.entity IN (".getEntity($forVirtualStock && !empty($conf->global->STOCK_CALCULATE_VIRTUAL_STOCK_TRANSVERSE_MODE) ? 'stock' : 'mrp').")"; 2991 $sql .= " AND mp.fk_product = ".$this->id; 2992 if (empty($user->rights->societe->client->voir) && !$socid && !$forVirtualStock) { 2993 $sql .= " AND m.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 2994 } 2995 if ($socid > 0) { 2996 $sql .= " AND m.fk_soc = ".$socid; 2997 } 2998 if ($filtrestatut <> '') { 2999 $sql .= " AND m.status IN (".$this->db->sanitize($filtrestatut).")"; 3000 } 3001 $sql .= " GROUP BY role"; 3002 3003 $this->stats_mrptoconsume['customers'] = 0; 3004 $this->stats_mrptoconsume['nb'] = 0; 3005 $this->stats_mrptoconsume['rows'] = 0; 3006 $this->stats_mrptoconsume['qty'] = 0; 3007 $this->stats_mrptoproduce['customers'] = 0; 3008 $this->stats_mrptoproduce['nb'] = 0; 3009 $this->stats_mrptoproduce['rows'] = 0; 3010 $this->stats_mrptoproduce['qty'] = 0; 3011 3012 $result = $this->db->query($sql); 3013 if ($result) { 3014 while ($obj = $this->db->fetch_object($result)) { 3015 if ($obj->role == 'toconsume') { 3016 $this->stats_mrptoconsume['customers'] += $obj->nb_customers; 3017 $this->stats_mrptoconsume['nb'] += $obj->nb; 3018 $this->stats_mrptoconsume['rows'] += $obj->nb_rows; 3019 $this->stats_mrptoconsume['qty'] += ($obj->qty ? $obj->qty : 0); 3020 } 3021 if ($obj->role == 'consumed') { 3022 //$this->stats_mrptoconsume['customers'] += $obj->nb_customers; 3023 //$this->stats_mrptoconsume['nb'] += $obj->nb; 3024 //$this->stats_mrptoconsume['rows'] += $obj->nb_rows; 3025 $this->stats_mrptoconsume['qty'] -= ($obj->qty ? $obj->qty : 0); 3026 } 3027 if ($obj->role == 'toproduce') { 3028 $this->stats_mrptoproduce['customers'] += $obj->nb_customers; 3029 $this->stats_mrptoproduce['nb'] += $obj->nb; 3030 $this->stats_mrptoproduce['rows'] += $obj->nb_rows; 3031 $this->stats_mrptoproduce['qty'] += ($obj->qty ? $obj->qty : 0); 3032 } 3033 if ($obj->role == 'produced') { 3034 //$this->stats_mrptoproduce['customers'] += $obj->nb_customers; 3035 //$this->stats_mrptoproduce['nb'] += $obj->nb; 3036 //$this->stats_mrptoproduce['rows'] += $obj->nb_rows; 3037 $this->stats_mrptoproduce['qty'] -= ($obj->qty ? $obj->qty : 0); 3038 } 3039 } 3040 3041 // Clean data 3042 if ($this->stats_mrptoconsume['qty'] < 0) $this->stats_mrptoconsume['qty'] = 0; 3043 if ($this->stats_mrptoproduce['qty'] < 0) $this->stats_mrptoproduce['qty'] = 0; 3044 3045 $parameters = array('socid' => $socid, 'filtrestatut' => $filtrestatut, 'forVirtualStock' => $forVirtualStock); 3046 $reshook = $hookmanager->executeHooks('loadStatsInProduction', $parameters, $this, $action); 3047 if ($reshook > 0) $this->stats_mrptoproduce = $hookmanager->resArray['stats_mrptoproduce']; 3048 3049 return 1; 3050 } else { 3051 $this->error = $this->db->error(); 3052 return -1; 3053 } 3054 } 3055 3056 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3057 /** 3058 * Charge tableau des stats contrat pour le produit/service 3059 * 3060 * @param int $socid Id societe 3061 * @return int Array of stats in $this->stats_contrat, <0 if ko or >0 if ok 3062 */ 3063 public function load_stats_contrat($socid = 0) 3064 { 3065 // phpcs:enable 3066 global $conf, $user, $hookmanager; 3067 3068 $sql = "SELECT COUNT(DISTINCT c.fk_soc) as nb_customers, COUNT(DISTINCT c.rowid) as nb,"; 3069 $sql .= " COUNT(cd.rowid) as nb_rows, SUM(cd.qty) as qty"; 3070 $sql .= " FROM ".MAIN_DB_PREFIX."contratdet as cd"; 3071 $sql .= ", ".MAIN_DB_PREFIX."contrat as c"; 3072 $sql .= ", ".MAIN_DB_PREFIX."societe as s"; 3073 if (empty($user->rights->societe->client->voir) && !$socid) { 3074 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 3075 } 3076 $sql .= " WHERE c.rowid = cd.fk_contrat"; 3077 $sql .= " AND c.fk_soc = s.rowid"; 3078 $sql .= " AND c.entity IN (".getEntity('contract').")"; 3079 $sql .= " AND cd.fk_product = ".$this->id; 3080 if (empty($user->rights->societe->client->voir) && !$socid) { 3081 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 3082 } 3083 //$sql.= " AND c.statut != 0"; 3084 if ($socid > 0) { 3085 $sql .= " AND c.fk_soc = ".$socid; 3086 } 3087 3088 $result = $this->db->query($sql); 3089 if ($result) { 3090 $obj = $this->db->fetch_object($result); 3091 $this->stats_contrat['customers'] = $obj->nb_customers; 3092 $this->stats_contrat['nb'] = $obj->nb; 3093 $this->stats_contrat['rows'] = $obj->nb_rows; 3094 $this->stats_contrat['qty'] = $obj->qty ? $obj->qty : 0; 3095 3096 // if it's a virtual product, maybe it is in contract by extension 3097 if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) { 3098 $TFather = $this->getFather(); 3099 if (is_array($TFather) && !empty($TFather)) { 3100 foreach ($TFather as &$fatherData) { 3101 $pFather = new Product($this->db); 3102 $pFather->id = $fatherData['id']; 3103 $qtyCoef = $fatherData['qty']; 3104 3105 if ($fatherData['incdec']) { 3106 $pFather->load_stats_contrat($socid); 3107 3108 $this->stats_contrat['customers'] += $pFather->stats_contrat['customers']; 3109 $this->stats_contrat['nb'] += $pFather->stats_contrat['nb']; 3110 $this->stats_contrat['rows'] += $pFather->stats_contrat['rows']; 3111 $this->stats_contrat['qty'] += $pFather->stats_contrat['qty'] * $qtyCoef; 3112 } 3113 } 3114 } 3115 } 3116 3117 $parameters = array('socid' => $socid); 3118 $reshook = $hookmanager->executeHooks('loadStatsContract', $parameters, $this, $action); 3119 if ($reshook > 0) $this->stats_contrat = $hookmanager->resArray['stats_contrat']; 3120 3121 return 1; 3122 } else { 3123 $this->error = $this->db->error().' sql='.$sql; 3124 return -1; 3125 } 3126 } 3127 3128 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3129 /** 3130 * Charge tableau des stats facture pour le produit/service 3131 * 3132 * @param int $socid Id societe 3133 * @return int Array of stats in $this->stats_facture, <0 if ko or >0 if ok 3134 */ 3135 public function load_stats_facture($socid = 0) 3136 { 3137 // phpcs:enable 3138 global $db, $conf, $user, $hookmanager; 3139 3140 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,"; 3141 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(".$this->db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1').") as qty"; 3142 $sql .= " FROM ".MAIN_DB_PREFIX."facturedet as fd"; 3143 $sql .= ", ".MAIN_DB_PREFIX."facture as f"; 3144 $sql .= ", ".MAIN_DB_PREFIX."societe as s"; 3145 if (empty($user->rights->societe->client->voir) && !$socid) { 3146 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 3147 } 3148 $sql .= " WHERE f.rowid = fd.fk_facture"; 3149 $sql .= " AND f.fk_soc = s.rowid"; 3150 $sql .= " AND f.entity IN (".getEntity('invoice').")"; 3151 $sql .= " AND fd.fk_product = ".$this->id; 3152 if (empty($user->rights->societe->client->voir) && !$socid) { 3153 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 3154 } 3155 //$sql.= " AND f.fk_statut != 0"; 3156 if ($socid > 0) { 3157 $sql .= " AND f.fk_soc = ".$socid; 3158 } 3159 3160 $result = $this->db->query($sql); 3161 if ($result) { 3162 $obj = $this->db->fetch_object($result); 3163 $this->stats_facture['customers'] = $obj->nb_customers; 3164 $this->stats_facture['nb'] = $obj->nb; 3165 $this->stats_facture['rows'] = $obj->nb_rows; 3166 $this->stats_facture['qty'] = $obj->qty ? $obj->qty : 0; 3167 3168 // if it's a virtual product, maybe it is in invoice by extension 3169 if (!empty($conf->global->PRODUCT_STATS_WITH_PARENT_PROD_IF_INCDEC)) { 3170 $TFather = $this->getFather(); 3171 if (is_array($TFather) && !empty($TFather)) { 3172 foreach ($TFather as &$fatherData) { 3173 $pFather = new Product($this->db); 3174 $pFather->id = $fatherData['id']; 3175 $qtyCoef = $fatherData['qty']; 3176 3177 if ($fatherData['incdec']) { 3178 $pFather->load_stats_facture($socid); 3179 3180 $this->stats_facture['customers'] += $pFather->stats_facture['customers']; 3181 $this->stats_facture['nb'] += $pFather->stats_facture['nb']; 3182 $this->stats_facture['rows'] += $pFather->stats_facture['rows']; 3183 $this->stats_facture['qty'] += $pFather->stats_facture['qty'] * $qtyCoef; 3184 } 3185 } 3186 } 3187 } 3188 3189 $parameters = array('socid' => $socid); 3190 $reshook = $hookmanager->executeHooks('loadStatsCustomerInvoice', $parameters, $this, $action); 3191 if ($reshook > 0) $this->stats_facture = $hookmanager->resArray['stats_facture']; 3192 3193 return 1; 3194 } else { 3195 $this->error = $this->db->error(); 3196 return -1; 3197 } 3198 } 3199 3200 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3201 /** 3202 * Charge tableau des stats facture pour le produit/service 3203 * 3204 * @param int $socid Id societe 3205 * @return int Array of stats in $this->stats_facture_fournisseur, <0 if ko or >0 if ok 3206 */ 3207 public function load_stats_facture_fournisseur($socid = 0) 3208 { 3209 // phpcs:enable 3210 global $conf, $user, $hookmanager, $action; 3211 3212 $sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_suppliers, COUNT(DISTINCT f.rowid) as nb,"; 3213 $sql .= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty"; 3214 $sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn_det as fd"; 3215 $sql .= ", ".MAIN_DB_PREFIX."facture_fourn as f"; 3216 $sql .= ", ".MAIN_DB_PREFIX."societe as s"; 3217 if (empty($user->rights->societe->client->voir) && !$socid) { 3218 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 3219 } 3220 $sql .= " WHERE f.rowid = fd.fk_facture_fourn"; 3221 $sql .= " AND f.fk_soc = s.rowid"; 3222 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")"; 3223 $sql .= " AND fd.fk_product = ".$this->id; 3224 if (empty($user->rights->societe->client->voir) && !$socid) { 3225 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 3226 } 3227 //$sql.= " AND f.fk_statut != 0"; 3228 if ($socid > 0) { 3229 $sql .= " AND f.fk_soc = ".$socid; 3230 } 3231 3232 $result = $this->db->query($sql); 3233 if ($result) { 3234 $obj = $this->db->fetch_object($result); 3235 $this->stats_facture_fournisseur['suppliers'] = $obj->nb_suppliers; 3236 $this->stats_facture_fournisseur['nb'] = $obj->nb; 3237 $this->stats_facture_fournisseur['rows'] = $obj->nb_rows; 3238 $this->stats_facture_fournisseur['qty'] = $obj->qty ? $obj->qty : 0; 3239 3240 $parameters = array('socid' => $socid); 3241 $reshook = $hookmanager->executeHooks('loadStatsSupplierInvoice', $parameters, $this, $action); 3242 if ($reshook > 0) $this->stats_facture_fournisseur = $hookmanager->resArray['stats_facture_fournisseur']; 3243 3244 return 1; 3245 } else { 3246 $this->error = $this->db->error(); 3247 return -1; 3248 } 3249 } 3250 3251 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3252 /** 3253 * Return an array formated for showing graphs 3254 * 3255 * @param string $sql Request to execute 3256 * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities 3257 * @param int $year Year (0=current year, -1=all years) 3258 * @return array|int <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11 3259 */ 3260 private function _get_stats($sql, $mode, $year = 0) 3261 { 3262 // phpcs:enable 3263 $tab = array(); 3264 3265 $resql = $this->db->query($sql); 3266 if ($resql) { 3267 $num = $this->db->num_rows($resql); 3268 $i = 0; 3269 while ($i < $num) 3270 { 3271 $arr = $this->db->fetch_array($resql); 3272 $keyfortab = (string) $arr[1]; 3273 if ($year == -1) { 3274 $keyfortab = substr($keyfortab, -2); 3275 } 3276 3277 if ($mode == 'byunit') { 3278 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[0]; // 1st field 3279 } elseif ($mode == 'bynumber') { 3280 $tab[$keyfortab] = (empty($tab[$keyfortab]) ? 0 : $tab[$keyfortab]) + $arr[2]; // 3rd field 3281 } 3282 $i++; 3283 } 3284 } else { 3285 $this->error = $this->db->error().' sql='.$sql; 3286 return -1; 3287 } 3288 3289 if (empty($year)) { 3290 $year = strftime('%Y', time()); 3291 $month = strftime('%m', time()); 3292 } elseif ($year == -1) { 3293 $year = ''; 3294 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year. 3295 } else { 3296 $month = 12; // We imagine we are at end of year, so we get last 12 month before, so all correct year. 3297 } 3298 3299 $result = array(); 3300 3301 for ($j = 0; $j < 12; $j++) 3302 { 3303 // $ids is 'D', 'N', 'O', 'S', ... (First letter of month in user language) 3304 $idx = ucfirst(dol_trunc(dol_print_date(dol_mktime(12, 0, 0, $month, 1, 1970), "%b"), 1, 'right', 'UTF-8', 1)); 3305 3306 //print $idx.'-'.$year.'-'.$month.'<br>'; 3307 $result[$j] = array($idx, isset($tab[$year.$month]) ? $tab[$year.$month] : 0); 3308 // $result[$j] = array($monthnum,isset($tab[$year.$month])?$tab[$year.$month]:0); 3309 3310 $month = "0".($month - 1); 3311 if (dol_strlen($month) == 3) { 3312 $month = substr($month, 1); 3313 } 3314 if ($month == 0) { 3315 $month = 12; 3316 $year = $year - 1; 3317 } 3318 } 3319 3320 return array_reverse($result); 3321 } 3322 3323 3324 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3325 /** 3326 * Return nb of units or customers invoices in which product is included 3327 * 3328 * @param int $socid Limit count on a particular third party id 3329 * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities 3330 * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only 3331 * @param int $year Year (0=last 12 month, -1=all years) 3332 * @param string $morefilter More sql filters 3333 * @return array <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11 3334 */ 3335 public function get_nb_vente($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '') 3336 { 3337 // phpcs:enable 3338 global $conf; 3339 global $user; 3340 3341 $sql = "SELECT sum(d.qty), date_format(f.datef, '%Y%m')"; 3342 if ($mode == 'bynumber') { 3343 $sql .= ", count(DISTINCT f.rowid)"; 3344 } 3345 $sql .= " FROM ".MAIN_DB_PREFIX."facturedet as d, ".MAIN_DB_PREFIX."facture as f, ".MAIN_DB_PREFIX."societe as s"; 3346 if ($filteronproducttype >= 0) { 3347 $sql .= ", ".MAIN_DB_PREFIX."product as p"; 3348 } 3349 if (empty($user->rights->societe->client->voir) && !$socid) { 3350 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 3351 } 3352 $sql .= " WHERE f.rowid = d.fk_facture"; 3353 if ($this->id > 0) { 3354 $sql .= " AND d.fk_product =".$this->id; 3355 } else { 3356 $sql .= " AND d.fk_product > 0"; 3357 } 3358 if ($filteronproducttype >= 0) { 3359 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype); 3360 } 3361 $sql .= " AND f.fk_soc = s.rowid"; 3362 $sql .= " AND f.entity IN (".getEntity('invoice').")"; 3363 if (empty($user->rights->societe->client->voir) && !$socid) { 3364 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 3365 } 3366 if ($socid > 0) { 3367 $sql .= " AND f.fk_soc = $socid"; 3368 } 3369 $sql .= $morefilter; 3370 $sql .= " GROUP BY date_format(f.datef,'%Y%m')"; 3371 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC"; 3372 3373 return $this->_get_stats($sql, $mode, $year); 3374 } 3375 3376 3377 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3378 /** 3379 * Return nb of units or supplier invoices in which product is included 3380 * 3381 * @param int $socid Limit count on a particular third party id 3382 * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities 3383 * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only 3384 * @param int $year Year (0=last 12 month, -1=all years) 3385 * @param string $morefilter More sql filters 3386 * @return array <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11 3387 */ 3388 public function get_nb_achat($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '') 3389 { 3390 // phpcs:enable 3391 global $conf; 3392 global $user; 3393 3394 $sql = "SELECT sum(d.qty), date_format(f.datef, '%Y%m')"; 3395 if ($mode == 'bynumber') { 3396 $sql .= ", count(DISTINCT f.rowid)"; 3397 } 3398 $sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn_det as d, ".MAIN_DB_PREFIX."facture_fourn as f, ".MAIN_DB_PREFIX."societe as s"; 3399 if ($filteronproducttype >= 0) { 3400 $sql .= ", ".MAIN_DB_PREFIX."product as p"; 3401 } 3402 if (empty($user->rights->societe->client->voir) && !$socid) { 3403 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 3404 } 3405 $sql .= " WHERE f.rowid = d.fk_facture_fourn"; 3406 if ($this->id > 0) { 3407 $sql .= " AND d.fk_product =".$this->id; 3408 } else { 3409 $sql .= " AND d.fk_product > 0"; 3410 } 3411 if ($filteronproducttype >= 0) { 3412 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype); 3413 } 3414 $sql .= " AND f.fk_soc = s.rowid"; 3415 $sql .= " AND f.entity IN (".getEntity('facture_fourn').")"; 3416 if (empty($user->rights->societe->client->voir) && !$socid) { 3417 $sql .= " AND f.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 3418 } 3419 if ($socid > 0) { 3420 $sql .= " AND f.fk_soc = $socid"; 3421 } 3422 $sql .= $morefilter; 3423 $sql .= " GROUP BY date_format(f.datef,'%Y%m')"; 3424 $sql .= " ORDER BY date_format(f.datef,'%Y%m') DESC"; 3425 3426 return $this->_get_stats($sql, $mode, $year); 3427 } 3428 3429 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3430 /** 3431 * Return nb of units in proposals in which product is included 3432 * 3433 * @param int $socid Limit count on a particular third party id 3434 * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities 3435 * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only 3436 * @param int $year Year (0=last 12 month, -1=all years) 3437 * @param string $morefilter More sql filters 3438 * @return array <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11 3439 */ 3440 public function get_nb_propal($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '') 3441 { 3442 // phpcs:enable 3443 global $conf, $user; 3444 3445 $sql = "SELECT sum(d.qty), date_format(p.datep, '%Y%m')"; 3446 if ($mode == 'bynumber') { 3447 $sql .= ", count(DISTINCT p.rowid)"; 3448 } 3449 $sql .= " FROM ".MAIN_DB_PREFIX."propaldet as d, ".MAIN_DB_PREFIX."propal as p, ".MAIN_DB_PREFIX."societe as s"; 3450 if ($filteronproducttype >= 0) { 3451 $sql .= ", ".MAIN_DB_PREFIX."product as prod"; 3452 } 3453 if (empty($user->rights->societe->client->voir) && !$socid) { 3454 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 3455 } 3456 $sql .= " WHERE p.rowid = d.fk_propal"; 3457 if ($this->id > 0) { 3458 $sql .= " AND d.fk_product =".$this->id; 3459 } else { 3460 $sql .= " AND d.fk_product > 0"; 3461 } 3462 if ($filteronproducttype >= 0) { 3463 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype); 3464 } 3465 $sql .= " AND p.fk_soc = s.rowid"; 3466 $sql .= " AND p.entity IN (".getEntity('propal').")"; 3467 if (empty($user->rights->societe->client->voir) && !$socid) { 3468 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 3469 } 3470 if ($socid > 0) { 3471 $sql .= " AND p.fk_soc = ".$socid; 3472 } 3473 $sql .= $morefilter; 3474 $sql .= " GROUP BY date_format(p.datep,'%Y%m')"; 3475 $sql .= " ORDER BY date_format(p.datep,'%Y%m') DESC"; 3476 3477 return $this->_get_stats($sql, $mode, $year); 3478 } 3479 3480 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3481 /** 3482 * Return nb of units in proposals in which product is included 3483 * 3484 * @param int $socid Limit count on a particular third party id 3485 * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities 3486 * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only 3487 * @param int $year Year (0=last 12 month, -1=all years) 3488 * @param string $morefilter More sql filters 3489 * @return array <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11 3490 */ 3491 public function get_nb_propalsupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '') 3492 { 3493 // phpcs:enable 3494 global $conf; 3495 global $user; 3496 3497 $sql = "SELECT sum(d.qty), date_format(p.date_valid, '%Y%m')"; 3498 if ($mode == 'bynumber') { 3499 $sql .= ", count(DISTINCT p.rowid)"; 3500 } 3501 $sql .= " FROM ".MAIN_DB_PREFIX."supplier_proposaldet as d, ".MAIN_DB_PREFIX."supplier_proposal as p, ".MAIN_DB_PREFIX."societe as s"; 3502 if ($filteronproducttype >= 0) { 3503 $sql .= ", ".MAIN_DB_PREFIX."product as prod"; 3504 } 3505 if (empty($user->rights->societe->client->voir) && !$socid) { 3506 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 3507 } 3508 $sql .= " WHERE p.rowid = d.fk_supplier_proposal"; 3509 if ($this->id > 0) { 3510 $sql .= " AND d.fk_product =".$this->id; 3511 } else { 3512 $sql .= " AND d.fk_product > 0"; 3513 } 3514 if ($filteronproducttype >= 0) { 3515 $sql .= " AND prod.rowid = d.fk_product AND prod.fk_product_type = ".((int) $filteronproducttype); 3516 } 3517 $sql .= " AND p.fk_soc = s.rowid"; 3518 $sql .= " AND p.entity IN (".getEntity('supplier_proposal').")"; 3519 if (empty($user->rights->societe->client->voir) && !$socid) { 3520 $sql .= " AND p.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 3521 } 3522 if ($socid > 0) { 3523 $sql .= " AND p.fk_soc = ".$socid; 3524 } 3525 $sql .= $morefilter; 3526 $sql .= " GROUP BY date_format(p.date_valid,'%Y%m')"; 3527 $sql .= " ORDER BY date_format(p.date_valid,'%Y%m') DESC"; 3528 3529 return $this->_get_stats($sql, $mode, $year); 3530 } 3531 3532 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3533 /** 3534 * Return nb of units in orders in which product is included 3535 * 3536 * @param int $socid Limit count on a particular third party id 3537 * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities 3538 * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only 3539 * @param int $year Year (0=last 12 month, -1=all years) 3540 * @param string $morefilter More sql filters 3541 * @return array <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11 3542 */ 3543 public function get_nb_order($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '') 3544 { 3545 // phpcs:enable 3546 global $conf, $user; 3547 3548 $sql = "SELECT sum(d.qty), date_format(c.date_commande, '%Y%m')"; 3549 if ($mode == 'bynumber') { 3550 $sql .= ", count(DISTINCT c.rowid)"; 3551 } 3552 $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as d, ".MAIN_DB_PREFIX."commande as c, ".MAIN_DB_PREFIX."societe as s"; 3553 if ($filteronproducttype >= 0) { 3554 $sql .= ", ".MAIN_DB_PREFIX."product as p"; 3555 } 3556 if (empty($user->rights->societe->client->voir) && !$socid) { 3557 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 3558 } 3559 $sql .= " WHERE c.rowid = d.fk_commande"; 3560 if ($this->id > 0) { 3561 $sql .= " AND d.fk_product =".$this->id; 3562 } else { 3563 $sql .= " AND d.fk_product > 0"; 3564 } 3565 if ($filteronproducttype >= 0) { 3566 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype); 3567 } 3568 $sql .= " AND c.fk_soc = s.rowid"; 3569 $sql .= " AND c.entity IN (".getEntity('commande').")"; 3570 if (empty($user->rights->societe->client->voir) && !$socid) { 3571 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 3572 } 3573 if ($socid > 0) { 3574 $sql .= " AND c.fk_soc = ".$socid; 3575 } 3576 $sql .= $morefilter; 3577 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')"; 3578 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC"; 3579 3580 return $this->_get_stats($sql, $mode, $year); 3581 } 3582 3583 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3584 /** 3585 * Return nb of units in orders in which product is included 3586 * 3587 * @param int $socid Limit count on a particular third party id 3588 * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities 3589 * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only 3590 * @param int $year Year (0=last 12 month, -1=all years) 3591 * @param string $morefilter More sql filters 3592 * @return array <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11 3593 */ 3594 public function get_nb_ordersupplier($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '') 3595 { 3596 // phpcs:enable 3597 global $conf, $user; 3598 3599 $sql = "SELECT sum(d.qty), date_format(c.date_commande, '%Y%m')"; 3600 if ($mode == 'bynumber') { 3601 $sql .= ", count(DISTINCT c.rowid)"; 3602 } 3603 $sql .= " FROM ".MAIN_DB_PREFIX."commande_fournisseurdet as d, ".MAIN_DB_PREFIX."commande_fournisseur as c, ".MAIN_DB_PREFIX."societe as s"; 3604 if ($filteronproducttype >= 0) { 3605 $sql .= ", ".MAIN_DB_PREFIX."product as p"; 3606 } 3607 if (empty($user->rights->societe->client->voir) && !$socid) { 3608 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 3609 } 3610 $sql .= " WHERE c.rowid = d.fk_commande"; 3611 if ($this->id > 0) { 3612 $sql .= " AND d.fk_product =".$this->id; 3613 } else { 3614 $sql .= " AND d.fk_product > 0"; 3615 } 3616 if ($filteronproducttype >= 0) { 3617 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype); 3618 } 3619 $sql .= " AND c.fk_soc = s.rowid"; 3620 $sql .= " AND c.entity IN (".getEntity('supplier_order').")"; 3621 if (empty($user->rights->societe->client->voir) && !$socid) { 3622 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 3623 } 3624 if ($socid > 0) { 3625 $sql .= " AND c.fk_soc = ".$socid; 3626 } 3627 $sql .= $morefilter; 3628 $sql .= " GROUP BY date_format(c.date_commande,'%Y%m')"; 3629 $sql .= " ORDER BY date_format(c.date_commande,'%Y%m') DESC"; 3630 3631 return $this->_get_stats($sql, $mode, $year); 3632 } 3633 3634 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3635 /** 3636 * Return nb of units in orders in which product is included 3637 * 3638 * @param int $socid Limit count on a particular third party id 3639 * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities 3640 * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only 3641 * @param int $year Year (0=last 12 month, -1=all years) 3642 * @param string $morefilter More sql filters 3643 * @return array <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11 3644 */ 3645 public function get_nb_contract($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '') 3646 { 3647 // phpcs:enable 3648 global $conf, $user; 3649 3650 $sql = "SELECT sum(d.qty), date_format(c.date_contrat, '%Y%m')"; 3651 if ($mode == 'bynumber') { 3652 $sql .= ", count(DISTINCT c.rowid)"; 3653 } 3654 $sql .= " FROM ".MAIN_DB_PREFIX."contratdet as d, ".MAIN_DB_PREFIX."contrat as c, ".MAIN_DB_PREFIX."societe as s"; 3655 if ($filteronproducttype >= 0) { 3656 $sql .= ", ".MAIN_DB_PREFIX."product as p"; 3657 } 3658 if (empty($user->rights->societe->client->voir) && !$socid) { 3659 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 3660 } 3661 3662 $sql .= " WHERE c.entity IN (".getEntity('contract').")"; 3663 $sql .= " AND c.rowid = d.fk_contrat"; 3664 3665 if ($this->id > 0) { 3666 $sql .= " AND d.fk_product =".$this->id; 3667 } else { 3668 $sql .= " AND d.fk_product > 0"; 3669 } 3670 if ($filteronproducttype >= 0) { 3671 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype); 3672 } 3673 $sql .= " AND c.fk_soc = s.rowid"; 3674 3675 if (empty($user->rights->societe->client->voir) && !$socid) { 3676 $sql .= " AND c.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 3677 } 3678 if ($socid > 0) { 3679 $sql .= " AND c.fk_soc = ".$socid; 3680 } 3681 $sql .= $morefilter; 3682 $sql .= " GROUP BY date_format(c.date_contrat,'%Y%m')"; 3683 $sql .= " ORDER BY date_format(c.date_contrat,'%Y%m') DESC"; 3684 3685 return $this->_get_stats($sql, $mode, $year); 3686 } 3687 3688 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3689 /** 3690 * Return nb of units in orders in which product is included 3691 * 3692 * @param int $socid Limit count on a particular third party id 3693 * @param string $mode 'byunit'=number of unit, 'bynumber'=nb of entities 3694 * @param int $filteronproducttype 0=To filter on product only, 1=To filter on services only 3695 * @param int $year Year (0=last 12 month, -1=all years) 3696 * @param string $morefilter More sql filters 3697 * @return array <0 if KO, result[month]=array(valuex,valuey) where month is 0 to 11 3698 */ 3699 public function get_nb_mos($socid, $mode, $filteronproducttype = -1, $year = 0, $morefilter = '') 3700 { 3701 // phpcs:enable 3702 global $conf, $user; 3703 3704 $sql = "SELECT sum(d.qty), date_format(d.date_valid, '%Y%m')"; 3705 if ($mode == 'bynumber') { 3706 $sql .= ", count(DISTINCT d.rowid)"; 3707 } 3708 $sql .= " FROM ".MAIN_DB_PREFIX."mrp_mo as d LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON d.fk_soc = s.rowid"; 3709 if ($filteronproducttype >= 0) { 3710 $sql .= ", ".MAIN_DB_PREFIX."product as p"; 3711 } 3712 if (empty($user->rights->societe->client->voir) && !$socid) { 3713 $sql .= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; 3714 } 3715 3716 $sql .= " WHERE d.entity IN (".getEntity('mo').")"; 3717 $sql .= " AND d.status > 0"; 3718 3719 if ($this->id > 0) { 3720 $sql .= " AND d.fk_product =".$this->id; 3721 } else { 3722 $sql .= " AND d.fk_product > 0"; 3723 } 3724 if ($filteronproducttype >= 0) { 3725 $sql .= " AND p.rowid = d.fk_product AND p.fk_product_type = ".((int) $filteronproducttype); 3726 } 3727 3728 if (empty($user->rights->societe->client->voir) && !$socid) { 3729 $sql .= " AND d.fk_soc = sc.fk_soc AND sc.fk_user = ".$user->id; 3730 } 3731 if ($socid > 0) { 3732 $sql .= " AND d.fk_soc = ".$socid; 3733 } 3734 $sql .= $morefilter; 3735 $sql .= " GROUP BY date_format(d.date_valid,'%Y%m')"; 3736 $sql .= " ORDER BY date_format(d.date_valid,'%Y%m') DESC"; 3737 3738 return $this->_get_stats($sql, $mode, $year); 3739 } 3740 3741 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3742 /** 3743 * Link a product/service to a parent product/service 3744 * 3745 * @param int $id_pere Id of parent product/service 3746 * @param int $id_fils Id of child product/service 3747 * @param int $qty Quantity 3748 * @param int $incdec 1=Increase/decrease stock of child when parent stock increase/decrease 3749 * @return int < 0 if KO, > 0 if OK 3750 */ 3751 public function add_sousproduit($id_pere, $id_fils, $qty, $incdec = 1) 3752 { 3753 // phpcs:enable 3754 // Clean parameters 3755 if (!is_numeric($id_pere)) { 3756 $id_pere = 0; 3757 } 3758 if (!is_numeric($id_fils)) { 3759 $id_fils = 0; 3760 } 3761 if (!is_numeric($incdec)) { 3762 $incdec = 0; 3763 } 3764 3765 $result = $this->del_sousproduit($id_pere, $id_fils); 3766 if ($result < 0) { 3767 return $result; 3768 } 3769 3770 // Check not already father of id_pere (to avoid father -> child -> father links) 3771 $sql = 'SELECT fk_product_pere from '.MAIN_DB_PREFIX.'product_association'; 3772 $sql .= ' WHERE fk_product_pere = '.$id_fils.' AND fk_product_fils = '.$id_pere; 3773 if (!$this->db->query($sql)) { 3774 dol_print_error($this->db); 3775 return -1; 3776 } else { 3777 $result = $this->db->query($sql); 3778 if ($result) { 3779 $num = $this->db->num_rows($result); 3780 if ($num > 0) { 3781 $this->error = "isFatherOfThis"; 3782 return -1; 3783 } else { 3784 $sql = 'INSERT INTO '.MAIN_DB_PREFIX.'product_association(fk_product_pere,fk_product_fils,qty,incdec)'; 3785 $sql .= ' VALUES ('.$id_pere.', '.$id_fils.', '.$qty.', '.$incdec.')'; 3786 if (!$this->db->query($sql)) { 3787 dol_print_error($this->db); 3788 return -1; 3789 } else { 3790 return 1; 3791 } 3792 } 3793 } 3794 } 3795 } 3796 3797 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3798 /** 3799 * Modify composed product 3800 * 3801 * @param int $id_pere Id of parent product/service 3802 * @param int $id_fils Id of child product/service 3803 * @param int $qty Quantity 3804 * @param int $incdec 1=Increase/decrease stock of child when parent stock increase/decrease 3805 * @return int < 0 if KO, > 0 if OK 3806 */ 3807 public function update_sousproduit($id_pere, $id_fils, $qty, $incdec = 1) 3808 { 3809 // phpcs:enable 3810 // Clean parameters 3811 if (!is_numeric($id_pere)) { 3812 $id_pere = 0; 3813 } 3814 if (!is_numeric($id_fils)) { 3815 $id_fils = 0; 3816 } 3817 if (!is_numeric($incdec)) { 3818 $incdec = 1; 3819 } 3820 if (!is_numeric($qty)) { 3821 $qty = 1; 3822 } 3823 3824 $sql = 'UPDATE '.MAIN_DB_PREFIX.'product_association SET '; 3825 $sql .= 'qty='.$qty; 3826 $sql .= ',incdec='.$incdec; 3827 $sql .= ' WHERE fk_product_pere='.$id_pere.' AND fk_product_fils='.$id_fils; 3828 3829 if (!$this->db->query($sql)) { 3830 dol_print_error($this->db); 3831 return -1; 3832 } else { 3833 return 1; 3834 } 3835 } 3836 3837 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3838 /** 3839 * Retire le lien entre un sousproduit et un produit/service 3840 * 3841 * @param int $fk_parent Id du produit auquel ne sera plus lie le produit lie 3842 * @param int $fk_child Id du produit a ne plus lie 3843 * @return int < 0 if KO, > 0 if OK 3844 */ 3845 public function del_sousproduit($fk_parent, $fk_child) 3846 { 3847 // phpcs:enable 3848 if (!is_numeric($fk_parent)) { 3849 $fk_parent = 0; 3850 } 3851 if (!is_numeric($fk_child)) { 3852 $fk_child = 0; 3853 } 3854 3855 $sql = "DELETE FROM ".MAIN_DB_PREFIX."product_association"; 3856 $sql .= " WHERE fk_product_pere = ".$fk_parent; 3857 $sql .= " AND fk_product_fils = ".$fk_child; 3858 3859 dol_syslog(get_class($this).'::del_sousproduit', LOG_DEBUG); 3860 if (!$this->db->query($sql)) { 3861 dol_print_error($this->db); 3862 return -1; 3863 } 3864 3865 return 1; 3866 } 3867 3868 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3869 /** 3870 * Check if it is a sub-product into a kit 3871 * 3872 * @param int $fk_parent Id of parent kit product 3873 * @param int $fk_child Id of child product 3874 * @return int <0 if KO, >0 if OK 3875 */ 3876 public function is_sousproduit($fk_parent, $fk_child) 3877 { 3878 // phpcs:enable 3879 $sql = "SELECT fk_product_pere, qty, incdec"; 3880 $sql .= " FROM ".MAIN_DB_PREFIX."product_association"; 3881 $sql .= " WHERE fk_product_pere = ".((int) $fk_parent); 3882 $sql .= " AND fk_product_fils = ".((int) $fk_child); 3883 3884 $result = $this->db->query($sql); 3885 if ($result) { 3886 $num = $this->db->num_rows($result); 3887 3888 if ($num > 0) { 3889 $obj = $this->db->fetch_object($result); 3890 3891 $this->is_sousproduit_qty = $obj->qty; 3892 $this->is_sousproduit_incdec = $obj->incdec; 3893 3894 return true; 3895 } else { 3896 return false; 3897 } 3898 } else { 3899 dol_print_error($this->db); 3900 return -1; 3901 } 3902 } 3903 3904 3905 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 3906 /** 3907 * Add a supplier price for the product. 3908 * Note: Duplicate ref is accepted for different quantity only, or for different companies. 3909 * 3910 * @param User $user User that make link 3911 * @param int $id_fourn Supplier id 3912 * @param string $ref_fourn Supplier ref 3913 * @param float $quantity Quantity minimum for price 3914 * @return int < 0 if KO, 0 if link already exists for this product, > 0 if OK 3915 */ 3916 public function add_fournisseur($user, $id_fourn, $ref_fourn, $quantity) 3917 { 3918 // phpcs:enable 3919 global $conf; 3920 3921 $now = dol_now(); 3922 3923 dol_syslog(get_class($this)."::add_fournisseur id_fourn = ".$id_fourn." ref_fourn=".$ref_fourn." quantity=".$quantity, LOG_DEBUG); 3924 3925 // Clean parameters 3926 $quantity = price2num($quantity, 'MS'); 3927 3928 if ($ref_fourn) { 3929 $sql = "SELECT rowid, fk_product"; 3930 $sql .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price"; 3931 $sql .= " WHERE fk_soc = ".((int) $id_fourn); 3932 $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'"; 3933 $sql .= " AND fk_product <> ".((int) $this->id); 3934 $sql .= " AND entity IN (".getEntity('productsupplierprice').")"; 3935 3936 $resql = $this->db->query($sql); 3937 if ($resql) { 3938 $obj = $this->db->fetch_object($resql); 3939 if ($obj) { 3940 // If the supplier ref already exists but for another product (duplicate ref is accepted for different quantity only or different companies) 3941 $this->product_id_already_linked = $obj->fk_product; 3942 return -3; 3943 } 3944 $this->db->free($resql); 3945 } 3946 } 3947 3948 $sql = "SELECT rowid"; 3949 $sql .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price"; 3950 $sql .= " WHERE fk_soc = ".$id_fourn; 3951 if ($ref_fourn) { $sql .= " AND ref_fourn = '".$this->db->escape($ref_fourn)."'"; 3952 } else { $sql .= " AND (ref_fourn = '' OR ref_fourn IS NULL)"; 3953 } 3954 $sql .= " AND quantity = ".$quantity; 3955 $sql .= " AND fk_product = ".$this->id; 3956 $sql .= " AND entity IN (".getEntity('productsupplierprice').")"; 3957 3958 $resql = $this->db->query($sql); 3959 if ($resql) { 3960 $obj = $this->db->fetch_object($resql); 3961 3962 // The reference supplier does not exist, we create it for this product. 3963 if (empty($obj)) { 3964 $sql = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price("; 3965 $sql .= "datec"; 3966 $sql .= ", entity"; 3967 $sql .= ", fk_product"; 3968 $sql .= ", fk_soc"; 3969 $sql .= ", ref_fourn"; 3970 $sql .= ", quantity"; 3971 $sql .= ", fk_user"; 3972 $sql .= ", tva_tx"; 3973 $sql .= ") VALUES ("; 3974 $sql .= "'".$this->db->idate($now)."'"; 3975 $sql .= ", ".$conf->entity; 3976 $sql .= ", ".$this->id; 3977 $sql .= ", ".$id_fourn; 3978 $sql .= ", '".$this->db->escape($ref_fourn)."'"; 3979 $sql .= ", ".$quantity; 3980 $sql .= ", ".$user->id; 3981 $sql .= ", 0"; 3982 $sql .= ")"; 3983 3984 if ($this->db->query($sql)) { 3985 $this->product_fourn_price_id = $this->db->last_insert_id(MAIN_DB_PREFIX."product_fournisseur_price"); 3986 return 1; 3987 } else { 3988 $this->error = $this->db->lasterror(); 3989 return -1; 3990 } 3991 } else { 3992 // If the supplier price already exists for this product and quantity 3993 $this->product_fourn_price_id = $obj->rowid; 3994 return 0; 3995 } 3996 } else { 3997 $this->error = $this->db->lasterror(); 3998 return -2; 3999 } 4000 } 4001 4002 4003 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 4004 /** 4005 * Return list of suppliers providing the product or service 4006 * 4007 * @return array Array of vendor ids 4008 */ 4009 public function list_suppliers() 4010 { 4011 // phpcs:enable 4012 global $conf; 4013 4014 $list = array(); 4015 4016 $sql = "SELECT DISTINCT p.fk_soc"; 4017 $sql .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price as p"; 4018 $sql .= " WHERE p.fk_product = ".$this->id; 4019 $sql .= " AND p.entity = ".$conf->entity; 4020 4021 $result = $this->db->query($sql); 4022 if ($result) { 4023 $num = $this->db->num_rows($result); 4024 $i = 0; 4025 while ($i < $num) 4026 { 4027 $obj = $this->db->fetch_object($result); 4028 $list[$i] = $obj->fk_soc; 4029 $i++; 4030 } 4031 } 4032 4033 return $list; 4034 } 4035 4036 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 4037 /** 4038 * Recopie les prix d'un produit/service sur un autre 4039 * 4040 * @param int $fromId Id product source 4041 * @param int $toId Id product target 4042 * @return int < 0 if KO, > 0 if OK 4043 */ 4044 public function clone_price($fromId, $toId) 4045 { 4046 global $conf, $user; 4047 4048 $now = dol_now(); 4049 4050 $this->db->begin(); 4051 4052 // prices 4053 $sql = "INSERT INTO ".MAIN_DB_PREFIX."product_price ("; 4054 $sql .= " entity"; 4055 $sql .= ", fk_product"; 4056 $sql .= ", date_price"; 4057 $sql .= ", price_level"; 4058 $sql .= ", price"; 4059 $sql .= ", price_ttc"; 4060 $sql .= ", price_min"; 4061 $sql .= ", price_min_ttc"; 4062 $sql .= ", price_base_type"; 4063 $sql .= ", default_vat_code"; 4064 $sql .= ", tva_tx"; 4065 $sql .= ", recuperableonly"; 4066 $sql .= ", localtax1_tx"; 4067 $sql .= ", localtax1_type"; 4068 $sql .= ", localtax2_tx"; 4069 $sql .= ", localtax2_type"; 4070 $sql .= ", fk_user_author"; 4071 $sql .= ", tosell"; 4072 $sql .= ", price_by_qty"; 4073 $sql .= ", fk_price_expression"; 4074 $sql .= ", fk_multicurrency"; 4075 $sql .= ", multicurrency_code"; 4076 $sql .= ", multicurrency_tx"; 4077 $sql .= ", multicurrency_price"; 4078 $sql .= ", multicurrency_price_ttc"; 4079 $sql .= ")"; 4080 $sql .= " SELECT"; 4081 $sql .= " entity"; 4082 $sql .= ", ".$toId; 4083 $sql .= ", '".$this->db->idate($now)."'"; 4084 $sql .= ", price_level"; 4085 $sql .= ", price"; 4086 $sql .= ", price_ttc"; 4087 $sql .= ", price_min"; 4088 $sql .= ", price_min_ttc"; 4089 $sql .= ", price_base_type"; 4090 $sql .= ", default_vat_code"; 4091 $sql .= ", tva_tx"; 4092 $sql .= ", recuperableonly"; 4093 $sql .= ", localtax1_tx"; 4094 $sql .= ", localtax1_type"; 4095 $sql .= ", localtax2_tx"; 4096 $sql .= ", localtax2_type"; 4097 $sql .= ", ".$user->id; 4098 $sql .= ", tosell"; 4099 $sql .= ", price_by_qty"; 4100 $sql .= ", fk_price_expression"; 4101 $sql .= ", fk_multicurrency"; 4102 $sql .= ", multicurrency_code"; 4103 $sql .= ", multicurrency_tx"; 4104 $sql .= ", multicurrency_price"; 4105 $sql .= ", multicurrency_price_ttc"; 4106 $sql .= " FROM ".MAIN_DB_PREFIX."product_price"; 4107 $sql .= " WHERE fk_product = ".$fromId; 4108 $sql .= " ORDER BY date_price DESC"; 4109 if ($conf->global->PRODUIT_MULTIPRICES_LIMIT > 0) { 4110 $sql .= " LIMIT ".$conf->global->PRODUIT_MULTIPRICES_LIMIT; 4111 } 4112 4113 dol_syslog(__METHOD__, LOG_DEBUG); 4114 $resql = $this->db->query($sql); 4115 if (!$resql) { 4116 $this->db->rollback(); 4117 return -1; 4118 } 4119 4120 $this->db->commit(); 4121 return 1; 4122 } 4123 4124 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 4125 /** 4126 * Clone links between products 4127 * 4128 * @param int $fromId Product id 4129 * @param int $toId Product id 4130 * @return int <0 if KO, >0 if OK 4131 */ 4132 public function clone_associations($fromId, $toId) 4133 { 4134 // phpcs:enable 4135 $this->db->begin(); 4136 4137 $sql = 'INSERT INTO '.MAIN_DB_PREFIX.'product_association (fk_product_pere, fk_product_fils, qty)'; 4138 $sql .= " SELECT ".$toId.", fk_product_fils, qty FROM ".MAIN_DB_PREFIX."product_association"; 4139 $sql .= " WHERE fk_product_pere = ".$fromId; 4140 4141 dol_syslog(get_class($this).'::clone_association', LOG_DEBUG); 4142 if (!$this->db->query($sql)) { 4143 $this->db->rollback(); 4144 return -1; 4145 } 4146 4147 $this->db->commit(); 4148 return 1; 4149 } 4150 4151 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 4152 /** 4153 * Recopie les fournisseurs et prix fournisseurs d'un produit/service sur un autre 4154 * 4155 * @param int $fromId Id produit source 4156 * @param int $toId Id produit cible 4157 * @return int < 0 si erreur, > 0 si ok 4158 */ 4159 public function clone_fournisseurs($fromId, $toId) 4160 { 4161 // phpcs:enable 4162 $this->db->begin(); 4163 4164 $now = dol_now(); 4165 4166 // les fournisseurs 4167 /*$sql = "INSERT ".MAIN_DB_PREFIX."product_fournisseur (" 4168 . " datec, fk_product, fk_soc, ref_fourn, fk_user_author )" 4169 . " SELECT '".$this->db->idate($now)."', ".$toId.", fk_soc, ref_fourn, fk_user_author" 4170 . " FROM ".MAIN_DB_PREFIX."product_fournisseur" 4171 . " WHERE fk_product = ".$fromId; 4172 4173 if ( ! $this->db->query($sql ) ) 4174 { 4175 $this->db->rollback(); 4176 return -1; 4177 }*/ 4178 4179 // les prix de fournisseurs. 4180 $sql = "INSERT ".MAIN_DB_PREFIX."product_fournisseur_price ("; 4181 $sql .= " datec, fk_product, fk_soc, price, quantity, fk_user)"; 4182 $sql .= " SELECT '".$this->db->idate($now)."', ".$toId.", fk_soc, price, quantity, fk_user"; 4183 $sql .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price"; 4184 $sql .= " WHERE fk_product = ".$fromId; 4185 4186 dol_syslog(get_class($this).'::clone_fournisseurs', LOG_DEBUG); 4187 $resql = $this->db->query($sql); 4188 if (!$resql) { 4189 $this->db->rollback(); 4190 return -1; 4191 } else { 4192 $this->db->commit(); 4193 return 1; 4194 } 4195 } 4196 4197 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 4198 /** 4199 * Fonction recursive uniquement utilisee par get_arbo_each_prod, recompose l'arborescence des sousproduits 4200 * Define value of this->res 4201 * 4202 * @param array $prod Products array 4203 * @param string $compl_path Directory path of parents to add before 4204 * @param int $multiply Because each sublevel must be multiplicated by parent nb 4205 * @param int $level Init level 4206 * @param int $id_parent Id parent 4207 * @return void 4208 */ 4209 public function fetch_prod_arbo($prod, $compl_path = "", $multiply = 1, $level = 1, $id_parent = 0) 4210 { 4211 // phpcs:enable 4212 global $conf, $langs; 4213 4214 $tmpproduct = null; 4215 //var_dump($prod); 4216 foreach ($prod as $id_product => $desc_pere) // $id_product is 0 (first call starting with root top) or an id of a sub_product 4217 { 4218 if (is_array($desc_pere)) // If desc_pere is an array, this means it's a child 4219 { 4220 $id = (!empty($desc_pere[0]) ? $desc_pere[0] : ''); 4221 $nb = (!empty($desc_pere[1]) ? $desc_pere[1] : ''); 4222 $type = (!empty($desc_pere[2]) ? $desc_pere[2] : ''); 4223 $label = (!empty($desc_pere[3]) ? $desc_pere[3] : ''); 4224 $incdec = !empty($desc_pere[4]) ? $desc_pere[4] : 0; 4225 4226 if ($multiply < 1) { $multiply = 1; 4227 } 4228 4229 //print "XXX We add id=".$id." - label=".$label." - nb=".$nb." - multiply=".$multiply." fullpath=".$compl_path.$label."\n"; 4230 if (is_null($tmpproduct)) $tmpproduct = new Product($this->db); // So we initialize tmpproduct only once for all loop. 4231 $tmpproduct->fetch($id); // Load product to get ->ref 4232 $tmpproduct->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel 4233 //$this->fetch($id); // Load product to get ->ref 4234 //$this->load_stock('nobatch,novirtual'); // Load stock to get true ->stock_reel 4235 $this->res[] = array( 4236 'id'=>$id, // Id product 4237 'id_parent'=>$id_parent, 4238 'ref'=>$tmpproduct->ref, // Ref product 4239 'nb'=>$nb, // Nb of units that compose parent product 4240 'nb_total'=>$nb * $multiply, // Nb of units for all nb of product 4241 'stock'=>$tmpproduct->stock_reel, // Stock 4242 'stock_alert'=>$tmpproduct->seuil_stock_alerte, // Stock alert 4243 'label'=>$label, 4244 'fullpath'=>$compl_path.$label, // Label 4245 'type'=>$type, // Nb of units that compose parent product 4246 'desiredstock'=>$tmpproduct->desiredstock, 4247 'level'=>$level, 4248 'incdec'=>$incdec, 4249 'entity'=>$tmpproduct->entity 4250 ); 4251 4252 // Recursive call if there is childs to child 4253 if (is_array($desc_pere['childs'])) { 4254 //print 'YYY We go down for '.$desc_pere[3]." -> \n"; 4255 $this->fetch_prod_arbo($desc_pere['childs'], $compl_path.$desc_pere[3]." -> ", $desc_pere[1] * $multiply, $level + 1, $id); 4256 } 4257 } 4258 } 4259 } 4260 4261 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 4262 /** 4263 * Build the tree of subproducts into an array 4264 * this->sousprods is loaded by this->get_sousproduits_arbo() 4265 * 4266 * @param int $multiply Because each sublevel must be multiplicated by parent nb 4267 * @return array $this->res 4268 */ 4269 public function get_arbo_each_prod($multiply = 1) 4270 { 4271 // phpcs:enable 4272 $this->res = array(); 4273 if (isset($this->sousprods) && is_array($this->sousprods)) { 4274 foreach ($this->sousprods as $prod_name => $desc_product) { 4275 if (is_array($desc_product)) { 4276 $this->fetch_prod_arbo($desc_product, "", $multiply, 1, $this->id); 4277 } 4278 } 4279 } 4280 //var_dump($this->res); 4281 return $this->res; 4282 } 4283 4284 /** 4285 * Count all parent and children products for current product (first level only) 4286 * 4287 * @param int $mode 0=Both parent and child, -1=Parents only, 1=Children only 4288 * @return int Nb of father + child 4289 * @see getFather(), get_sousproduits_arbo() 4290 */ 4291 public function hasFatherOrChild($mode = 0) 4292 { 4293 $nb = 0; 4294 4295 $sql = "SELECT COUNT(pa.rowid) as nb"; 4296 $sql .= " FROM ".MAIN_DB_PREFIX."product_association as pa"; 4297 if ($mode == 0) { 4298 $sql .= " WHERE pa.fk_product_fils = ".$this->id." OR pa.fk_product_pere = ".$this->id; 4299 } elseif ($mode == -1) { 4300 $sql .= " WHERE pa.fk_product_fils = ".$this->id; // We are a child, so we found lines that link to parents (can have several parents) 4301 } elseif ($mode == 1) { 4302 $sql .= " WHERE pa.fk_product_pere = ".$this->id; // We are a parent, so we found lines that link to children (can have several children) 4303 } 4304 4305 $resql = $this->db->query($sql); 4306 if ($resql) { 4307 $obj = $this->db->fetch_object($resql); 4308 if ($obj) { $nb = $obj->nb; } 4309 } else { 4310 return -1; 4311 } 4312 4313 return $nb; 4314 } 4315 4316 /** 4317 * Return if a product has variants or not 4318 * 4319 * @return int Number of variants 4320 */ 4321 public function hasVariants() 4322 { 4323 $nb = 0; 4324 $sql = "SELECT count(rowid) as nb FROM ".MAIN_DB_PREFIX."product_attribute_combination WHERE fk_product_parent = ".$this->id; 4325 $sql .= " AND entity IN (".getEntity('product').")"; 4326 4327 $resql = $this->db->query($sql); 4328 if ($resql) { 4329 $obj = $this->db->fetch_object($resql); 4330 if ($obj) { $nb = $obj->nb; 4331 } 4332 } 4333 4334 return $nb; 4335 } 4336 4337 4338 /** 4339 * Return if loaded product is a variant 4340 * 4341 * @return int 4342 */ 4343 public function isVariant() 4344 { 4345 global $conf; 4346 if (!empty($conf->variants->enabled)) { 4347 $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_attribute_combination WHERE fk_product_child = ".$this->id." AND entity IN (".getEntity('product').")"; 4348 4349 $query = $this->db->query($sql); 4350 4351 if ($query) { 4352 if (!$this->db->num_rows($query)) { 4353 return false; 4354 } 4355 return true; 4356 } else { 4357 dol_print_error($this->db); 4358 return -1; 4359 } 4360 } else { 4361 return false; 4362 } 4363 } 4364 4365 /** 4366 * Return all parent products for current product (first level only) 4367 * 4368 * @return array Array of product 4369 * @see hasFatherOrChild() 4370 */ 4371 public function getFather() 4372 { 4373 $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"; 4374 $sql .= " FROM ".MAIN_DB_PREFIX."product_association as pa,"; 4375 $sql .= " ".MAIN_DB_PREFIX."product as p"; 4376 $sql .= " WHERE p.rowid = pa.fk_product_pere"; 4377 $sql .= " AND pa.fk_product_fils = ".$this->id; 4378 4379 $res = $this->db->query($sql); 4380 if ($res) { 4381 $prods = array(); 4382 while ($record = $this->db->fetch_array($res)) 4383 { 4384 // $record['id'] = $record['rowid'] = id of father 4385 $prods[$record['id']]['id'] = $record['rowid']; 4386 $prods[$record['id']]['ref'] = $record['ref']; 4387 $prods[$record['id']]['label'] = $record['label']; 4388 $prods[$record['id']]['qty'] = $record['qty']; 4389 $prods[$record['id']]['incdec'] = $record['incdec']; 4390 $prods[$record['id']]['fk_product_type'] = $record['fk_product_type']; 4391 $prods[$record['id']]['entity'] = $record['entity']; 4392 } 4393 return $prods; 4394 } else { 4395 dol_print_error($this->db); 4396 return -1; 4397 } 4398 } 4399 4400 4401 /** 4402 * Return childs of product $id 4403 * 4404 * @param int $id Id of product to search childs of 4405 * @param int $firstlevelonly Return only direct child 4406 * @param int $level Level of recursing call (start to 1) 4407 * @return array Return array(prodid=>array(0=prodid, 1=>qty, 2=>product type, 3=>label, 4=>incdec, 5=>product ref) 4408 */ 4409 public function getChildsArbo($id, $firstlevelonly = 0, $level = 1) 4410 { 4411 global $alreadyfound; 4412 4413 if (empty($id)) { 4414 return array(); 4415 } 4416 4417 $sql = "SELECT p.rowid, p.ref, p.label as label, p.fk_product_type,"; 4418 $sql .= " pa.qty as qty, pa.fk_product_fils as id, pa.incdec"; 4419 $sql .= " FROM ".MAIN_DB_PREFIX."product as p,"; 4420 $sql .= " ".MAIN_DB_PREFIX."product_association as pa"; 4421 $sql .= " WHERE p.rowid = pa.fk_product_fils"; 4422 $sql .= " AND pa.fk_product_pere = ".$id; 4423 $sql .= " AND pa.fk_product_fils != ".$id; // This should not happens, it is to avoid infinite loop if it happens 4424 4425 dol_syslog(get_class($this).'::getChildsArbo id='.$id.' level='.$level, LOG_DEBUG); 4426 4427 if ($level == 1) { $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 4428 } 4429 // Protection against infinite loop 4430 if ($level > 30) { 4431 return array(); 4432 } 4433 4434 $res = $this->db->query($sql); 4435 if ($res) { 4436 $prods = array(); 4437 while ($rec = $this->db->fetch_array($res)) 4438 { 4439 if (!empty($alreadyfound[$rec['rowid']])) { 4440 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); 4441 continue; 4442 } 4443 $alreadyfound[$rec['rowid']] = 1; 4444 $prods[$rec['rowid']] = array( 4445 0=>$rec['rowid'], 4446 1=>$rec['qty'], 4447 2=>$rec['fk_product_type'], 4448 3=>$this->db->escape($rec['label']), 4449 4=>$rec['incdec'], 4450 5=>$rec['ref'] 4451 ); 4452 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty'],2=>$rec['fk_product_type']); 4453 //$prods[$this->db->escape($rec['label'])]= array(0=>$rec['id'],1=>$rec['qty']); 4454 if (empty($firstlevelonly)) { 4455 $listofchilds = $this->getChildsArbo($rec['rowid'], 0, $level + 1); 4456 foreach ($listofchilds as $keyChild => $valueChild) 4457 { 4458 $prods[$rec['rowid']]['childs'][$keyChild] = $valueChild; 4459 } 4460 } 4461 } 4462 4463 return $prods; 4464 } else { 4465 dol_print_error($this->db); 4466 return -1; 4467 } 4468 } 4469 4470 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 4471 /** 4472 * Return tree of all subproducts for product. Tree contains array of array(0=prodid, 1=>qty, 2=>product type, 3=>label, 4=>incdec, 5=>product ref) 4473 * Set this->sousprods 4474 * 4475 * @return void 4476 */ 4477 public function get_sousproduits_arbo() 4478 { 4479 // phpcs:enable 4480 $parent = array(); 4481 4482 foreach ($this->getChildsArbo($this->id) as $keyChild => $valueChild) // Warning. getChildsArbo can call getChildsArbo recursively. Starting point is $value[0]=id of product 4483 { 4484 $parent[$this->label][$keyChild] = $valueChild; 4485 } 4486 foreach ($parent as $key => $value) // key=label, value is array of childs 4487 { 4488 $this->sousprods[$key] = $value; 4489 } 4490 } 4491 4492 /** 4493 * Return clicable link of object (with eventually picto) 4494 * 4495 * @param int $withpicto Add picto into link 4496 * @param string $option Where point the link ('stock', 'composition', 'category', 'supplier', '') 4497 * @param int $maxlength Maxlength of ref 4498 * @param int $save_lastsearch_value -1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values whenclicking 4499 * @param int $notooltip No tooltip 4500 * @return string String with URL 4501 */ 4502 public function getNomUrl($withpicto = 0, $option = '', $maxlength = 0, $save_lastsearch_value = -1, $notooltip = 0) 4503 { 4504 global $conf, $langs, $hookmanager; 4505 include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php'; 4506 4507 $result = ''; $label = ''; 4508 4509 $newref = $this->ref; 4510 if ($maxlength) { 4511 $newref = dol_trunc($newref, $maxlength, 'middle'); 4512 } 4513 4514 if (!empty($this->entity)) { 4515 $tmpphoto = $this->show_photos('product', $conf->product->multidir_output[$this->entity], 1, 1, 0, 0, 0, 80); 4516 if ($this->nbphoto > 0) { 4517 $label .= '<div class="photointooltip">'; 4518 $label .= $tmpphoto; 4519 $label .= '</div><div style="clear: both;"></div>'; 4520 } 4521 } 4522 4523 if ($this->type == Product::TYPE_PRODUCT) { 4524 $label .= img_picto('', 'product').' <u class="paddingrightonly">'.$langs->trans("Product").'</u>'; 4525 } elseif ($this->type == Product::TYPE_SERVICE) { 4526 $label .= img_picto('', 'service').' <u class="paddingrightonly">'.$langs->trans("Service").'</u>'; 4527 } 4528 if (isset($this->status) && isset($this->status_buy)) { 4529 $label .= ' '.$this->getLibStatut(5, 0); 4530 $label .= ' '.$this->getLibStatut(5, 1); 4531 } 4532 4533 if (!empty($this->ref)) { 4534 $label .= '<br><b>'.$langs->trans('ProductRef').':</b> '.$this->ref; 4535 } 4536 if (!empty($this->label)) { 4537 $label .= '<br><b>'.$langs->trans('ProductLabel').':</b> '.$this->label; 4538 } 4539 if ($this->type == Product::TYPE_PRODUCT || !empty($conf->global->STOCK_SUPPORTS_SERVICES)) { 4540 if (!empty($conf->productbatch->enabled)) { 4541 $langs->load("productbatch"); 4542 $label .= "<br><b>".$langs->trans("ManageLotSerial").'</b>: '.$this->getLibStatut(0, 2); 4543 } 4544 } 4545 if (!empty($conf->barcode->enabled)) { 4546 $label .= '<br><b>'.$langs->trans('BarCode').':</b> '.$this->barcode; 4547 } 4548 4549 if ($this->type == Product::TYPE_PRODUCT) 4550 { 4551 if ($this->weight) { 4552 $label .= "<br><b>".$langs->trans("Weight").'</b>: '.$this->weight.' '.measuringUnitString(0, "weight", $this->weight_units); 4553 } 4554 $labelsize = ""; 4555 if ($this->length) { 4556 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Length").'</b>: '.$this->length.' '.measuringUnitString(0, 'size', $this->length_units); 4557 } 4558 if ($this->width) { 4559 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Width").'</b>: '.$this->width.' '.measuringUnitString(0, 'size', $this->width_units); 4560 } 4561 if ($this->height) { 4562 $labelsize .= ($labelsize ? " - " : "")."<b>".$langs->trans("Height").'</b>: '.$this->height.' '.measuringUnitString(0, 'size', $this->height_units); 4563 } 4564 if ($labelsize) $label .= "<br>".$labelsize; 4565 4566 $labelsurfacevolume = ""; 4567 if ($this->surface) { 4568 $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Surface").'</b>: '.$this->surface.' '.measuringUnitString(0, 'surface', $this->surface_units); 4569 } 4570 if ($this->volume) { 4571 $labelsurfacevolume .= ($labelsurfacevolume ? " - " : "")."<b>".$langs->trans("Volume").'</b>: '.$this->volume.' '.measuringUnitString(0, 'volume', $this->volume_units); 4572 } 4573 if ($labelsurfacevolume) $label .= "<br>".$labelsurfacevolume; 4574 } 4575 4576 if (!empty($conf->accounting->enabled) && $this->status) { 4577 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php'; 4578 $label .= '<br><b>'.$langs->trans('ProductAccountancySellCode').':</b> '.length_accountg($this->accountancy_code_sell); 4579 $label .= '<br><b>'.$langs->trans('ProductAccountancySellIntraCode').':</b> '.length_accountg($this->accountancy_code_sell_intra); 4580 $label .= '<br><b>'.$langs->trans('ProductAccountancySellExportCode').':</b> '.length_accountg($this->accountancy_code_sell_export); 4581 } 4582 if (!empty($conf->accounting->enabled) && $this->status_buy) { 4583 include_once DOL_DOCUMENT_ROOT.'/core/lib/accounting.lib.php'; 4584 $label .= '<br><b>'.$langs->trans('ProductAccountancyBuyCode').':</b> '.length_accountg($this->accountancy_code_buy); 4585 $label .= '<br><b>'.$langs->trans('ProductAccountancyBuyIntraCode').':</b> '.length_accountg($this->accountancy_code_buy_intra); 4586 $label .= '<br><b>'.$langs->trans('ProductAccountancyBuyExportCode').':</b> '.length_accountg($this->accountancy_code_buy_export); 4587 } 4588 4589 $linkclose = ''; 4590 if (empty($notooltip)) { 4591 if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER)) { 4592 $label = $langs->trans("ShowProduct"); 4593 $linkclose .= ' alt="'.dol_escape_htmltag($label, 1).'"'; 4594 } 4595 4596 $linkclose .= ' title="'.dol_escape_htmltag($label, 1, 1).'"'; 4597 $linkclose .= ' class="nowraponall classfortooltip"'; 4598 } else { 4599 $linkclose = ' class="nowraponall"'; 4600 } 4601 4602 if ($option == 'supplier' || $option == 'category') { 4603 $url = DOL_URL_ROOT.'/product/fournisseurs.php?id='.$this->id; 4604 } elseif ($option == 'stock') { 4605 $url = DOL_URL_ROOT.'/product/stock/product.php?id='.$this->id; 4606 } elseif ($option == 'composition') { 4607 $url = DOL_URL_ROOT.'/product/composition/card.php?id='.$this->id; 4608 } else { 4609 $url = DOL_URL_ROOT.'/product/card.php?id='.$this->id; 4610 } 4611 4612 if ($option !== 'nolink') { 4613 // Add param to save lastsearch_values or not 4614 $add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0); 4615 if ($save_lastsearch_value == -1 && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) { $add_save_lastsearch_values = 1; 4616 } 4617 if ($add_save_lastsearch_values) { $url .= '&save_lastsearch_values=1'; 4618 } 4619 } 4620 4621 $linkstart = '<a href="'.$url.'"'; 4622 $linkstart .= $linkclose.'>'; 4623 $linkend = '</a>'; 4624 4625 $result .= $linkstart; 4626 if ($withpicto) 4627 { 4628 if ($this->type == Product::TYPE_PRODUCT) { 4629 $result .= (img_object(($notooltip ? '' : $label), 'product', ($notooltip ? 'class="paddingright"' : 'class="paddingright classfortooltip"'), 0, 0, $notooltip ? 0 : 1)); 4630 } 4631 if ($this->type == Product::TYPE_SERVICE) { 4632 $result .= (img_object(($notooltip ? '' : $label), 'service', ($notooltip ? 'class="paddinright"' : 'class="paddingright classfortooltip"'), 0, 0, $notooltip ? 0 : 1)); 4633 } 4634 } 4635 $result .= $newref; 4636 $result .= $linkend; 4637 4638 global $action; 4639 $hookmanager->initHooks(array('productdao')); 4640 $parameters = array('id'=>$this->id, 'getnomurl'=>$result); 4641 $reshook = $hookmanager->executeHooks('getNomUrl', $parameters, $this, $action); // Note that $action and $object may have been modified by some hooks 4642 if ($reshook > 0) { 4643 $result = $hookmanager->resPrint; 4644 } else { 4645 $result .= $hookmanager->resPrint; 4646 } 4647 4648 return $result; 4649 } 4650 4651 4652 /** 4653 * Create a document onto disk according to template module. 4654 * 4655 * @param string $modele Force model to use ('' to not force) 4656 * @param Translate $outputlangs Object langs to use for output 4657 * @param int $hidedetails Hide details of lines 4658 * @param int $hidedesc Hide description 4659 * @param int $hideref Hide ref 4660 * @return int 0 if KO, 1 if OK 4661 */ 4662 public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0) 4663 { 4664 global $conf, $user, $langs; 4665 4666 $langs->load("products"); 4667 $outputlangs->load("products"); 4668 4669 // Positionne le modele sur le nom du modele a utiliser 4670 if (!dol_strlen($modele)) { 4671 if (!empty($conf->global->PRODUCT_ADDON_PDF)) { 4672 $modele = $conf->global->PRODUCT_ADDON_PDF; 4673 } else { 4674 $modele = 'strato'; 4675 } 4676 } 4677 4678 $modelpath = "core/modules/product/doc/"; 4679 4680 return $this->commonGenerateDocument($modelpath, $modele, $outputlangs, $hidedetails, $hidedesc, $hideref); 4681 } 4682 4683 /** 4684 * Return label of status of object 4685 * 4686 * @param int $mode 0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto 4687 * @param int $type 0=Sell, 1=Buy, 2=Batch Number management 4688 * @return string Label of status 4689 */ 4690 public function getLibStatut($mode = 0, $type = 0) 4691 { 4692 switch ($type) 4693 { 4694 case 0: 4695 return $this->LibStatut($this->status, $mode, $type); 4696 case 1: 4697 return $this->LibStatut($this->status_buy, $mode, $type); 4698 case 2: 4699 return $this->LibStatut($this->status_batch, $mode, $type); 4700 default: 4701 //Simulate previous behavior but should return an error string 4702 return $this->LibStatut($this->status_buy, $mode, $type); 4703 } 4704 } 4705 4706 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 4707 /** 4708 * Return label of a given status 4709 * 4710 * @param int $status Statut 4711 * @param int $mode 0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto, 6=Long label + Picto 4712 * @param int $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch" 4713 * @return string Label of status 4714 */ 4715 public function LibStatut($status, $mode = 0, $type = 0) 4716 { 4717 // phpcs:enable 4718 global $conf, $langs; 4719 4720 $labelStatus = $labelStatusShort = ''; 4721 4722 $langs->load('products'); 4723 if (!empty($conf->productbatch->enabled)) { $langs->load("productbatch"); 4724 } 4725 4726 if ($type == 2) { 4727 switch ($mode) 4728 { 4729 case 0: 4730 $label = ($status == 0 ? $langs->trans('ProductStatusNotOnBatch') : $langs->trans('ProductStatusOnBatch')); 4731 return dolGetStatus($label); 4732 case 1: 4733 $label = ($status == 0 ? $langs->trans('ProductStatusNotOnBatchShort') : $langs->trans('ProductStatusOnBatchShort')); 4734 return dolGetStatus($label); 4735 case 2: 4736 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 1, 2); 4737 case 3: 4738 return dolGetStatus($langs->trans('ProductStatusNotOnBatch'), '', '', empty($status) ? 'status5' : 'status4', 3, 'dot'); 4739 case 4: 4740 return $this->LibStatut($status, 3, 2).' '.$this->LibStatut($status, 0, 2); 4741 case 5: 4742 return $this->LibStatut($status, 1, 2).' '.$this->LibStatut($status, 3, 2); 4743 default: 4744 return dolGetStatus($langs->trans('Unknown')); 4745 } 4746 } 4747 4748 $statuttrans = empty($status) ? 'status5' : 'status4'; 4749 4750 if ($status == 0) { 4751 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch" 4752 if ($type == 0) { 4753 $labelStatus = $langs->trans('ProductStatusNotOnSellShort'); 4754 $labelStatusShort = $langs->trans('ProductStatusNotOnSell'); 4755 } elseif ($type == 1) { 4756 $labelStatus = $langs->trans('ProductStatusNotOnBuyShort'); 4757 $labelStatusShort = $langs->trans('ProductStatusNotOnBuy'); 4758 } elseif ($type == 2) { 4759 $labelStatus = $langs->trans('ProductStatusNotOnBatch'); 4760 $labelStatusShort = $langs->trans('ProductStatusNotOnBatchShort'); 4761 } 4762 } elseif ($status == 1) { 4763 // $type 0=Status "to sell", 1=Status "to buy", 2=Status "to Batch" 4764 if ($type == 0) { 4765 $labelStatus = $langs->trans('ProductStatusOnSellShort'); 4766 $labelStatusShort = $langs->trans('ProductStatusOnSell'); 4767 } elseif ($type == 1) { 4768 $labelStatus = $langs->trans('ProductStatusOnBuyShort'); 4769 $labelStatusShort = $langs->trans('ProductStatusOnBuy'); 4770 } elseif ($type == 2) { 4771 $labelStatus = $langs->trans('ProductStatusOnBatch'); 4772 $labelStatusShort = $langs->trans('ProductStatusOnBatchShort'); 4773 } 4774 } 4775 4776 4777 if ($mode > 6) { 4778 return dolGetStatus($langs->trans('Unknown'), '', '', 'status0', 0); 4779 } else { 4780 return dolGetStatus($labelStatus, $labelStatusShort, '', $statuttrans, $mode); 4781 } 4782 } 4783 4784 4785 /** 4786 * Retour label of nature of product 4787 * 4788 * @return string Label 4789 */ 4790 public function getLibFinished() 4791 { 4792 global $langs; 4793 $langs->load('products'); 4794 4795 if (isset($this->finished) && $this->finished >= 0) { 4796 $sql = 'SELECT label, code FROM '.MAIN_DB_PREFIX.'c_product_nature where code='.((int) $this->finished).' AND active=1'; 4797 $resql = $this->db->query($sql); 4798 if ($resql && $this->db->num_rows($resql) > 0) { 4799 $res = $this->db->fetch_array($resql); 4800 $label = $langs->trans($res['label']); 4801 $this->db->free($resql); 4802 return $label; 4803 } else { 4804 $this->error = $this->db->error().' sql='.$sql; 4805 dol_syslog(__METHOD__.' Error '.$this->error, LOG_ERR); 4806 return -1; 4807 } 4808 } 4809 4810 return ''; 4811 } 4812 4813 4814 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 4815 /** 4816 * Adjust stock in a warehouse for product 4817 * 4818 * @param User $user user asking change 4819 * @param int $id_entrepot id of warehouse 4820 * @param double $nbpiece nb of units 4821 * @param int $movement 0 = add, 1 = remove 4822 * @param string $label Label of stock movement 4823 * @param double $price Unit price HT of product, used to calculate average weighted price (PMP in french). If 0, average weighted price is not changed. 4824 * @param string $inventorycode Inventory code 4825 * @param string $origin_element Origin element type 4826 * @param int $origin_id Origin id of element 4827 * @param int $disablestockchangeforsubproduct Disable stock change for sub-products of kit (usefull only if product is a subproduct) 4828 * @return int <0 if KO, >0 if OK 4829 */ 4830 public function correct_stock($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0) 4831 { 4832 // phpcs:enable 4833 if ($id_entrepot) { 4834 $this->db->begin(); 4835 4836 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php'; 4837 4838 $op[0] = "+".trim($nbpiece); 4839 $op[1] = "-".trim($nbpiece); 4840 4841 $movementstock = new MouvementStock($this->db); 4842 $movementstock->setOrigin($origin_element, $origin_id); // Set ->origin and ->origin->id 4843 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', '', '', '', false, 0, $disablestockchangeforsubproduct); 4844 4845 if ($result >= 0) { 4846 $this->db->commit(); 4847 return 1; 4848 } else { 4849 $this->error = $movementstock->error; 4850 $this->errors = $movementstock->errors; 4851 4852 $this->db->rollback(); 4853 return -1; 4854 } 4855 } 4856 } 4857 4858 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 4859 /** 4860 * Adjust stock in a warehouse for product with batch number 4861 * 4862 * @param User $user user asking change 4863 * @param int $id_entrepot id of warehouse 4864 * @param double $nbpiece nb of units 4865 * @param int $movement 0 = add, 1 = remove 4866 * @param string $label Label of stock movement 4867 * @param double $price Price to use for stock eval 4868 * @param integer $dlc eat-by date 4869 * @param integer $dluo sell-by date 4870 * @param string $lot Lot number 4871 * @param string $inventorycode Inventory code 4872 * @param string $origin_element Origin element type 4873 * @param int $origin_id Origin id of element 4874 * @param int $disablestockchangeforsubproduct Disable stock change for sub-products of kit (usefull only if product is a subproduct) 4875 * @return int <0 if KO, >0 if OK 4876 */ 4877 public function correct_stock_batch($user, $id_entrepot, $nbpiece, $movement, $label = '', $price = 0, $dlc = '', $dluo = '', $lot = '', $inventorycode = '', $origin_element = '', $origin_id = null, $disablestockchangeforsubproduct = 0) 4878 { 4879 // phpcs:enable 4880 if ($id_entrepot) { 4881 $this->db->begin(); 4882 4883 include_once DOL_DOCUMENT_ROOT.'/product/stock/class/mouvementstock.class.php'; 4884 4885 $op[0] = "+".trim($nbpiece); 4886 $op[1] = "-".trim($nbpiece); 4887 4888 $movementstock = new MouvementStock($this->db); 4889 $movementstock->setOrigin($origin_element, $origin_id); 4890 $result = $movementstock->_create($user, $this->id, $id_entrepot, $op[$movement], $movement, $price, $label, $inventorycode, '', $dlc, $dluo, $lot, false, 0, $disablestockchangeforsubproduct); 4891 4892 if ($result >= 0) { 4893 $this->db->commit(); 4894 return 1; 4895 } else { 4896 $this->error = $movementstock->error; 4897 $this->errors = $movementstock->errors; 4898 4899 $this->db->rollback(); 4900 return -1; 4901 } 4902 } 4903 } 4904 4905 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 4906 /** 4907 * Load information about stock of a product into ->stock_reel, ->stock_warehouse[] (including stock_warehouse[idwarehouse]->detail_batch for batch products) 4908 * This function need a lot of load. If you use it on list, use a cache to execute it once for each product id. 4909 * If ENTREPOT_EXTRA_STATUS set, filtering on warehouse status possible. 4910 * 4911 * @param string $option '' = Load all stock info, also from closed and internal warehouses, 'nobatch', 'novirtual' 4912 * @param int $includedraftpoforvirtual Include draft status of PO for virtual stock calculation 4913 * @return int < 0 if KO, > 0 if OK 4914 * @see load_virtual_stock(), loadBatchInfo() 4915 */ 4916 public function load_stock($option = '', $includedraftpoforvirtual = null) 4917 { 4918 // phpcs:enable 4919 global $conf; 4920 4921 $this->stock_reel = 0; 4922 $this->stock_warehouse = array(); 4923 $this->stock_theorique = 0; 4924 4925 $warehouseStatus = array(); 4926 4927 if (preg_match('/warehouseclosed/', $option)) { 4928 $warehouseStatus[] = Entrepot::STATUS_CLOSED; 4929 } 4930 if (preg_match('/warehouseopen/', $option)) { 4931 $warehouseStatus[] = Entrepot::STATUS_OPEN_ALL; 4932 } 4933 if (preg_match('/warehouseinternal/', $option)) { 4934 $warehouseStatus[] = Entrepot::STATUS_OPEN_INTERNAL; 4935 } 4936 4937 $sql = "SELECT ps.rowid, ps.reel, ps.fk_entrepot"; 4938 $sql .= " FROM ".MAIN_DB_PREFIX."product_stock as ps"; 4939 $sql .= ", ".MAIN_DB_PREFIX."entrepot as w"; 4940 $sql .= " WHERE w.entity IN (".getEntity('stock').")"; 4941 $sql .= " AND w.rowid = ps.fk_entrepot"; 4942 $sql .= " AND ps.fk_product = ".$this->id; 4943 if (!empty($conf->global->ENTREPOT_EXTRA_STATUS) && count($warehouseStatus)) { 4944 $sql .= " AND w.statut IN (".$this->db->sanitize($this->db->escape(implode(',', $warehouseStatus))).")"; 4945 } 4946 4947 dol_syslog(get_class($this)."::load_stock", LOG_DEBUG); 4948 $result = $this->db->query($sql); 4949 if ($result) { 4950 $num = $this->db->num_rows($result); 4951 $i = 0; 4952 if ($num > 0) { 4953 while ($i < $num) 4954 { 4955 $row = $this->db->fetch_object($result); 4956 $this->stock_warehouse[$row->fk_entrepot] = new stdClass(); 4957 $this->stock_warehouse[$row->fk_entrepot]->real = $row->reel; 4958 $this->stock_warehouse[$row->fk_entrepot]->id = $row->rowid; 4959 if ((!preg_match('/nobatch/', $option)) && $this->hasbatch()) { 4960 $this->stock_warehouse[$row->fk_entrepot]->detail_batch = Productbatch::findAll($this->db, $row->rowid, 1, $this->id); 4961 } 4962 $this->stock_reel += $row->reel; 4963 $i++; 4964 } 4965 } 4966 $this->db->free($result); 4967 4968 if (!preg_match('/novirtual/', $option)) { 4969 $this->load_virtual_stock($includedraftpoforvirtual); // This also load all arrays stats_xxx... 4970 } 4971 4972 return 1; 4973 } else { 4974 $this->error = $this->db->lasterror(); 4975 return -1; 4976 } 4977 } 4978 4979 4980 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 4981 /** 4982 * Load value ->stock_theorique of a product. Property this->id must be defined. 4983 * This function need a lot of load. If you use it on list, use a cache to execute it one for each product id. 4984 * 4985 * @param int $includedraftpoforvirtual Include draft status of PO for virtual stock calculation 4986 * @return int < 0 if KO, > 0 if OK 4987 * @see load_stock(), loadBatchInfo() 4988 */ 4989 public function load_virtual_stock($includedraftpoforvirtual = null) 4990 { 4991 // phpcs:enable 4992 global $conf, $hookmanager, $action; 4993 4994 $stock_commande_client = 0; 4995 $stock_commande_fournisseur = 0; 4996 $stock_sending_client = 0; 4997 $stock_reception_fournisseur = 0; 4998 $stock_inproduction = 0; 4999 5000 //dol_syslog("load_virtual_stock"); 5001 5002 if (!empty($conf->commande->enabled)) 5003 { 5004 $result = $this->load_stats_commande(0, '1,2', 1); 5005 if ($result < 0) dol_print_error($this->db, $this->error); 5006 $stock_commande_client = $this->stats_commande['qty']; 5007 } 5008 if (!empty($conf->expedition->enabled)) 5009 { 5010 require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php'; 5011 $filterShipmentStatus = ''; 5012 if (!empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT)) { 5013 $filterShipmentStatus = Expedition::STATUS_VALIDATED.','.Expedition::STATUS_CLOSED; 5014 } elseif (!empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE)) { 5015 $filterShipmentStatus = Expedition::STATUS_CLOSED; 5016 } 5017 $result = $this->load_stats_sending(0, '1,2', 1, $filterShipmentStatus); 5018 if ($result < 0) dol_print_error($this->db, $this->error); 5019 $stock_sending_client = $this->stats_expedition['qty']; 5020 } 5021 if (!empty($conf->fournisseur->enabled) && empty($conf->global->MAIN_USE_NEW_SUPPLIERMOD) || !empty($conf->supplier_order->enabled)) 5022 { 5023 $filterStatus = '1,2,3,4'; 5024 if (isset($includedraftpoforvirtual)) $filterStatus = '0,'.$filterStatus; 5025 $result = $this->load_stats_commande_fournisseur(0, $filterStatus, 1); 5026 if ($result < 0) dol_print_error($this->db, $this->error); 5027 $stock_commande_fournisseur = $this->stats_commande_fournisseur['qty']; 5028 } 5029 if ((!empty($conf->fournisseur->enabled) && empty($conf->global->MAIN_USE_NEW_SUPPLIERMOD) || !empty($conf->supplier_order->enabled) || !empty($conf->supplier_invoice->enabled)) && empty($conf->reception->enabled)) 5030 { 5031 $filterStatus = '4'; 5032 if (isset($includedraftpoforvirtual)) $filterStatus = '0,'.$filterStatus; 5033 $result = $this->load_stats_reception(0, $filterStatus, 1); 5034 if ($result < 0) dol_print_error($this->db, $this->error); 5035 $stock_reception_fournisseur = $this->stats_reception['qty']; 5036 } 5037 if ((!empty($conf->fournisseur->enabled) && empty($conf->global->MAIN_USE_NEW_SUPPLIERMOD) || !empty($conf->supplier_order->enabled) || !empty($conf->supplier_invoice->enabled)) && empty($conf->reception->enabled)) 5038 { 5039 $filterStatus = '4'; 5040 if (isset($includedraftpoforvirtual)) $filterStatus = '0,'.$filterStatus; 5041 $result = $this->load_stats_reception(0, $filterStatus, 1); // Use same tables than when module reception is not used. 5042 if ($result < 0) dol_print_error($this->db, $this->error); 5043 $stock_reception_fournisseur = $this->stats_reception['qty']; 5044 } 5045 if (!empty($conf->mrp->enabled)) 5046 { 5047 $result = $this->load_stats_inproduction(0, '1,2', 1); 5048 if ($result < 0) dol_print_error($this->db, $this->error); 5049 $stock_inproduction = $this->stats_mrptoproduce['qty'] - $this->stats_mrptoconsume['qty']; 5050 } 5051 5052 $this->stock_theorique = $this->stock_reel + $stock_inproduction; 5053 5054 // Stock decrease mode 5055 if (!empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT) || !empty($conf->global->STOCK_CALCULATE_ON_SHIPMENT_CLOSE)) { 5056 $this->stock_theorique -= ($stock_commande_client - $stock_sending_client); 5057 } elseif (!empty($conf->global->STOCK_CALCULATE_ON_VALIDATE_ORDER)) { 5058 $this->stock_theorique += 0; 5059 } elseif (!empty($conf->global->STOCK_CALCULATE_ON_BILL)) { 5060 $this->stock_theorique -= $stock_commande_client; 5061 } 5062 // Stock Increase mode 5063 if (!empty($conf->global->STOCK_CALCULATE_ON_RECEPTION) || !empty($conf->global->STOCK_CALCULATE_ON_RECEPTION_CLOSE)) { 5064 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur); 5065 } elseif (!empty($conf->global->STOCK_CALCULATE_ON_SUPPLIER_DISPATCH_ORDER)) { 5066 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur); 5067 } elseif (!empty($conf->global->STOCK_CALCULATE_ON_SUPPLIER_VALIDATE_ORDER)) { 5068 $this->stock_theorique -= $stock_reception_fournisseur; 5069 } elseif (!empty($conf->global->STOCK_CALCULATE_ON_SUPPLIER_BILL)) { 5070 $this->stock_theorique += ($stock_commande_fournisseur - $stock_reception_fournisseur); 5071 } 5072 5073 if (!is_object($hookmanager)) { 5074 include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php'; 5075 $hookmanager = new HookManager($this->db); 5076 } 5077 $hookmanager->initHooks(array('productdao')); 5078 $parameters = array('id'=>$this->id, 'includedraftpoforvirtual' => $includedraftpoforvirtual); 5079 // Note that $action and $object may have been modified by some hooks 5080 $reshook = $hookmanager->executeHooks('loadvirtualstock', $parameters, $this, $action); 5081 if ($reshook > 0) $this->stock_theorique = $hookmanager->resArray['stock_theorique']; 5082 5083 return 1; 5084 } 5085 5086 5087 /** 5088 * Load existing information about a serial 5089 * 5090 * @param string $batch Lot/serial number 5091 * @return array Array with record into product_batch 5092 * @see load_stock(), load_virtual_stock() 5093 */ 5094 public function loadBatchInfo($batch) 5095 { 5096 $result = array(); 5097 5098 $sql = "SELECT pb.batch, pb.eatby, pb.sellby, SUM(pb.qty) AS qty FROM ".MAIN_DB_PREFIX."product_batch as pb, ".MAIN_DB_PREFIX."product_stock as ps"; 5099 $sql .= " WHERE pb.fk_product_stock = ps.rowid AND ps.fk_product = ".$this->id." AND pb.batch = '".$this->db->escape($batch)."'"; 5100 $sql .= " GROUP BY pb.batch, pb.eatby, pb.sellby"; 5101 dol_syslog(get_class($this)."::loadBatchInfo load first entry found for lot/serial = ".$batch, LOG_DEBUG); 5102 $resql = $this->db->query($sql); 5103 if ($resql) { 5104 $num = $this->db->num_rows($resql); 5105 $i = 0; 5106 while ($i < $num) 5107 { 5108 $obj = $this->db->fetch_object($resql); 5109 $result[] = array('batch'=>$batch, 'eatby'=>$this->db->jdate($obj->eatby), 'sellby'=>$this->db->jdate($obj->sellby), 'qty'=>$obj->qty); 5110 $i++; 5111 } 5112 return $result; 5113 } else { 5114 dol_print_error($this->db); 5115 $this->db->rollback(); 5116 return array(); 5117 } 5118 } 5119 5120 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 5121 /** 5122 * Move an uploaded file described into $file array into target directory $sdir. 5123 * 5124 * @param string $sdir Target directory 5125 * @param string $file Array of file info of file to upload: array('name'=>..., 'tmp_name'=>...) 5126 * @return int <0 if KO, >0 if OK 5127 */ 5128 public function add_photo($sdir, $file) 5129 { 5130 // phpcs:enable 5131 global $conf; 5132 5133 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; 5134 5135 $result = 0; 5136 5137 $dir = $sdir; 5138 if (!empty($conf->global->PRODUCT_USE_OLD_PATH_FOR_PHOTO)) { 5139 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos"; 5140 } else { 5141 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product').dol_sanitizeFileName($this->ref); 5142 } 5143 5144 dol_mkdir($dir); 5145 5146 $dir_osencoded = $dir; 5147 5148 if (is_dir($dir_osencoded)) { 5149 $originImage = $dir.'/'.$file['name']; 5150 5151 // Cree fichier en taille origine 5152 $result = dol_move_uploaded_file($file['tmp_name'], $originImage, 1); 5153 5154 if (file_exists(dol_osencode($originImage))) { 5155 // Create thumbs 5156 $this->addThumbs($originImage); 5157 } 5158 } 5159 5160 if (is_numeric($result) && $result > 0) { 5161 return 1; 5162 } else { 5163 return -1; 5164 } 5165 } 5166 5167 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 5168 /** 5169 * Return if at least one photo is available 5170 * 5171 * @param string $sdir Directory to scan 5172 * @return boolean True if at least one photo is available, False if not 5173 */ 5174 public function is_photo_available($sdir) 5175 { 5176 // phpcs:enable 5177 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; 5178 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php'; 5179 5180 global $conf; 5181 5182 $dir = $sdir; 5183 if (!empty($conf->global->PRODUCT_USE_OLD_PATH_FOR_PHOTO)) { 5184 $dir .= '/'.get_exdir($this->id, 2, 0, 0, $this, 'product').$this->id."/photos/"; 5185 } else { 5186 $dir .= '/'.get_exdir(0, 0, 0, 0, $this, 'product'); 5187 } 5188 5189 $nbphoto = 0; 5190 5191 $dir_osencoded = dol_osencode($dir); 5192 if (file_exists($dir_osencoded)) { 5193 $handle = opendir($dir_osencoded); 5194 if (is_resource($handle)) { 5195 while (($file = readdir($handle)) !== false) 5196 { 5197 if (!utf8_check($file)) { 5198 $file = utf8_encode($file); // To be sure data is stored in UTF8 in memory 5199 } 5200 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) { 5201 return true; 5202 } 5203 } 5204 } 5205 } 5206 return false; 5207 } 5208 5209 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 5210 /** 5211 * Return an array with all photos of product found on disk. There is no sorting criteria. 5212 * 5213 * @param string $dir Directory to scan 5214 * @param int $nbmax Number maxium of photos (0=no maximum) 5215 * @return array Array of photos 5216 */ 5217 public function liste_photos($dir, $nbmax = 0) 5218 { 5219 // phpcs:enable 5220 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; 5221 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php'; 5222 5223 $nbphoto = 0; 5224 $tabobj = array(); 5225 5226 $dir_osencoded = dol_osencode($dir); 5227 $handle = @opendir($dir_osencoded); 5228 if (is_resource($handle)) { 5229 while (($file = readdir($handle)) !== false) { 5230 if (!utf8_check($file)) { 5231 $file = utf8_encode($file); // readdir returns ISO 5232 } 5233 if (dol_is_file($dir.$file) && image_format_supported($file) >= 0) { 5234 $nbphoto++; 5235 5236 // We forge name of thumb. 5237 $photo = $file; 5238 $photo_vignette = ''; 5239 $regs = array(); 5240 if (preg_match('/('.$this->regeximgext.')$/i', $photo, $regs)) { 5241 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $photo).'_small'.$regs[0]; 5242 } 5243 5244 $dirthumb = $dir.'thumbs/'; 5245 5246 // Objet 5247 $obj = array(); 5248 $obj['photo'] = $photo; 5249 if ($photo_vignette && dol_is_file($dirthumb.$photo_vignette)) { 5250 $obj['photo_vignette'] = 'thumbs/'.$photo_vignette; 5251 } else { 5252 $obj['photo_vignette'] = ""; 5253 } 5254 5255 $tabobj[$nbphoto - 1] = $obj; 5256 5257 // Do we have to continue with next photo ? 5258 if ($nbmax && $nbphoto >= $nbmax) { 5259 break; 5260 } 5261 } 5262 } 5263 5264 closedir($handle); 5265 } 5266 5267 return $tabobj; 5268 } 5269 5270 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 5271 /** 5272 * Delete a photo and its thumbs 5273 * 5274 * @param string $file Path to image file 5275 * @return void 5276 */ 5277 public function delete_photo($file) 5278 { 5279 // phpcs:enable 5280 include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; 5281 include_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php'; 5282 5283 $dir = dirname($file).'/'; // Chemin du dossier contenant l'image d'origine 5284 $dirthumb = $dir.'/thumbs/'; // Chemin du dossier contenant la vignette 5285 $filename = preg_replace('/'.preg_quote($dir, '/').'/i', '', $file); // Nom du fichier 5286 5287 // On efface l'image d'origine 5288 dol_delete_file($file, 0, 0, 0, $this); // For triggers 5289 5290 // Si elle existe, on efface la vignette 5291 if (preg_match('/('.$this->regeximgext.')$/i', $filename, $regs)) { 5292 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_small'.$regs[0]; 5293 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) { 5294 dol_delete_file($dirthumb.$photo_vignette); 5295 } 5296 5297 $photo_vignette = preg_replace('/'.$regs[0].'/i', '', $filename).'_mini'.$regs[0]; 5298 if (file_exists(dol_osencode($dirthumb.$photo_vignette))) { 5299 dol_delete_file($dirthumb.$photo_vignette); 5300 } 5301 } 5302 } 5303 5304 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 5305 /** 5306 * Load size of image file 5307 * 5308 * @param string $file Path to file 5309 * @return void 5310 */ 5311 public function get_image_size($file) 5312 { 5313 // phpcs:enable 5314 $file_osencoded = dol_osencode($file); 5315 $infoImg = getimagesize($file_osencoded); // Get information on image 5316 $this->imgWidth = $infoImg[0]; // Largeur de l'image 5317 $this->imgHeight = $infoImg[1]; // Hauteur de l'image 5318 } 5319 5320 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 5321 /** 5322 * Load indicators this->nb for the dashboard 5323 * 5324 * @return int <0 if KO, >0 if OK 5325 */ 5326 public function load_state_board() 5327 { 5328 // phpcs:enable 5329 global $conf, $user, $hookmanager; 5330 5331 $this->nb = array(); 5332 5333 $sql = "SELECT count(p.rowid) as nb, fk_product_type"; 5334 $sql .= " FROM ".MAIN_DB_PREFIX."product as p"; 5335 $sql .= ' WHERE p.entity IN ('.getEntity($this->element, 1).')'; 5336 // Add where from hooks 5337 if (is_object($hookmanager)) { 5338 $parameters = array(); 5339 $reshook = $hookmanager->executeHooks('printFieldListWhere', $parameters); // Note that $action and $object may have been modified by hook 5340 $sql .= $hookmanager->resPrint; 5341 } 5342 $sql .= ' GROUP BY fk_product_type'; 5343 5344 $resql = $this->db->query($sql); 5345 if ($resql) { 5346 while ($obj = $this->db->fetch_object($resql)) 5347 { 5348 if ($obj->fk_product_type == 1) { $this->nb["services"] = $obj->nb; 5349 } else { $this->nb["products"] = $obj->nb; 5350 } 5351 } 5352 $this->db->free($resql); 5353 return 1; 5354 } else { 5355 dol_print_error($this->db); 5356 $this->error = $this->db->error(); 5357 return -1; 5358 } 5359 } 5360 5361 /** 5362 * Return if object is a product 5363 * 5364 * @return boolean True if it's a product 5365 */ 5366 public function isProduct() 5367 { 5368 return ($this->type == Product::TYPE_PRODUCT ? true : false); 5369 } 5370 5371 /** 5372 * Return if object is a product 5373 * 5374 * @return boolean True if it's a service 5375 */ 5376 public function isService() 5377 { 5378 return ($this->type == Product::TYPE_SERVICE ? true : false); 5379 } 5380 5381 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 5382 /** 5383 * Get a barcode from the module to generate barcode values. 5384 * Return value is stored into this->barcode 5385 * 5386 * @param Product $object Object product or service 5387 * @param string $type Barcode type (ean, isbn, ...) 5388 * @return string 5389 */ 5390 public function get_barcode($object, $type = '') 5391 { 5392 // phpcs:enable 5393 global $conf; 5394 5395 $result = ''; 5396 if (!empty($conf->global->BARCODE_PRODUCT_ADDON_NUM)) { 5397 $dirsociete = array_merge(array('/core/modules/barcode/'), $conf->modules_parts['barcode']); 5398 foreach ($dirsociete as $dirroot) 5399 { 5400 $res = dol_include_once($dirroot.$conf->global->BARCODE_PRODUCT_ADDON_NUM.'.php'); 5401 if ($res) { break; 5402 } 5403 } 5404 $var = $conf->global->BARCODE_PRODUCT_ADDON_NUM; 5405 $mod = new $var; 5406 5407 $result = $mod->getNextValue($object, $type); 5408 5409 dol_syslog(get_class($this)."::get_barcode barcode=".$result." module=".$var); 5410 } 5411 return $result; 5412 } 5413 5414 /** 5415 * Initialise an instance with random values. 5416 * Used to build previews or test instances. 5417 * id must be 0 if object instance is a specimen. 5418 * 5419 * @return void 5420 */ 5421 public function initAsSpecimen() 5422 { 5423 global $user, $langs, $conf, $mysoc; 5424 5425 $now = dol_now(); 5426 5427 // Initialize parameters 5428 $this->specimen = 1; 5429 $this->id = 0; 5430 $this->ref = 'PRODUCT_SPEC'; 5431 $this->label = 'PRODUCT SPECIMEN'; 5432 $this->description = 'This is description of this product specimen that was created the '.dol_print_date($now, 'dayhourlog').'.'; 5433 $this->specimen = 1; 5434 $this->country_id = 1; 5435 $this->tosell = 1; 5436 $this->tobuy = 1; 5437 $this->tobatch = 0; 5438 $this->note = 'This is a comment (private)'; 5439 $this->date_creation = $now; 5440 $this->date_modification = $now; 5441 5442 $this->weight = 4; 5443 $this->weight_units = 3; 5444 5445 $this->length = 5; 5446 $this->length_units = 1; 5447 $this->width = 6; 5448 $this->width_units = 0; 5449 $this->height = null; 5450 $this->height_units = null; 5451 5452 $this->surface = 30; 5453 $this->surface_units = 0; 5454 $this->volume = 300; 5455 $this->volume_units = 0; 5456 5457 $this->barcode = -1; // Create barcode automatically 5458 } 5459 5460 /** 5461 * Returns the text label from units dictionary 5462 * 5463 * @param string $type Label type (long or short) 5464 * @return string|int <0 if ko, label if ok 5465 */ 5466 public function getLabelOfUnit($type = 'long') 5467 { 5468 global $langs; 5469 5470 if (!$this->fk_unit) { 5471 return ''; 5472 } 5473 5474 $langs->load('products'); 5475 5476 $label_type = 'label'; 5477 if ($type == 'short') { 5478 $label_type = 'short_label'; 5479 } 5480 5481 $sql = 'select '.$label_type.', code from '.MAIN_DB_PREFIX.'c_units where rowid='.$this->fk_unit; 5482 $resql = $this->db->query($sql); 5483 if ($resql && $this->db->num_rows($resql) > 0) { 5484 $res = $this->db->fetch_array($resql); 5485 $label = ($label_type == 'short_label' ? $res[$label_type] : 'unit'.$res['code']); 5486 $this->db->free($resql); 5487 return $label; 5488 } else { 5489 $this->error = $this->db->error().' sql='.$sql; 5490 dol_syslog(get_class($this)."::getLabelOfUnit Error ".$this->error, LOG_ERR); 5491 return -1; 5492 } 5493 } 5494 5495 /** 5496 * Return if object has a sell-by date or eat-by date 5497 * 5498 * @return boolean True if it's has 5499 */ 5500 public function hasbatch() 5501 { 5502 return ($this->status_batch == 1 ? true : false); 5503 } 5504 5505 5506 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps 5507 /** 5508 * Return minimum product recommended price 5509 * 5510 * @return int Minimum recommanded price that is higher price among all suppliers * PRODUCT_MINIMUM_RECOMMENDED_PRICE 5511 */ 5512 public function min_recommended_price() 5513 { 5514 // phpcs:enable 5515 global $conf; 5516 5517 $maxpricesupplier = 0; 5518 5519 if (!empty($conf->global->PRODUCT_MINIMUM_RECOMMENDED_PRICE)) { 5520 include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php'; 5521 $product_fourn = new ProductFournisseur($this->db); 5522 $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->id, '', ''); 5523 5524 if (is_array($product_fourn_list) && count($product_fourn_list) > 0) { 5525 foreach ($product_fourn_list as $productfourn) 5526 { 5527 if ($productfourn->fourn_unitprice > $maxpricesupplier) { 5528 $maxpricesupplier = $productfourn->fourn_unitprice; 5529 } 5530 } 5531 5532 $maxpricesupplier *= $conf->global->PRODUCT_MINIMUM_RECOMMENDED_PRICE; 5533 } 5534 } 5535 5536 return $maxpricesupplier; 5537 } 5538 5539 5540 /** 5541 * Sets object to supplied categories. 5542 * 5543 * Deletes object from existing categories not supplied. 5544 * Adds it to non existing supplied categories. 5545 * Existing categories are left untouch. 5546 * 5547 * @param int[]|int $categories Category or categories IDs 5548 * @return void 5549 */ 5550 public function setCategories($categories) 5551 { 5552 // Handle single category 5553 if (!is_array($categories)) { 5554 $categories = array($categories); 5555 } 5556 5557 // Get current categories 5558 include_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php'; 5559 $c = new Categorie($this->db); 5560 $existing = $c->containing($this->id, Categorie::TYPE_PRODUCT, 'id'); 5561 5562 // Diff 5563 if (is_array($existing)) { 5564 $to_del = array_diff($existing, $categories); 5565 $to_add = array_diff($categories, $existing); 5566 } else { 5567 $to_del = array(); // Nothing to delete 5568 $to_add = $categories; 5569 } 5570 5571 // Process 5572 foreach ($to_del as $del) { 5573 if ($c->fetch($del) > 0) { 5574 $c->del_type($this, Categorie::TYPE_PRODUCT); 5575 } 5576 } 5577 foreach ($to_add as $add) { 5578 if ($c->fetch($add) > 0) { 5579 $c->add_type($this, Categorie::TYPE_PRODUCT); 5580 } 5581 } 5582 5583 return; 5584 } 5585 5586 /** 5587 * Function used to replace a thirdparty id with another one. 5588 * 5589 * @param DoliDB $db Database handler 5590 * @param int $origin_id Old thirdparty id 5591 * @param int $dest_id New thirdparty id 5592 * @return bool 5593 */ 5594 public static function replaceThirdparty(DoliDB $db, $origin_id, $dest_id) 5595 { 5596 $tables = array( 5597 'product_customer_price', 5598 'product_customer_price_log' 5599 ); 5600 5601 return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables); 5602 } 5603 5604 /** 5605 * Generates prices for a product based on product multiprice generation rules 5606 * 5607 * @param User $user User that updates the prices 5608 * @param float $baseprice Base price 5609 * @param string $price_type Base price type 5610 * @param float $price_vat VAT % tax 5611 * @param int $npr NPR 5612 * @param string $psq ¿? 5613 * @return int -1 KO, 1 OK 5614 */ 5615 public function generateMultiprices(User $user, $baseprice, $price_type, $price_vat, $npr, $psq) 5616 { 5617 global $conf, $db; 5618 5619 $sql = "SELECT rowid, level, fk_level, var_percent, var_min_percent FROM ".MAIN_DB_PREFIX."product_pricerules"; 5620 $query = $this->db->query($sql); 5621 5622 $rules = array(); 5623 5624 while ($result = $this->db->fetch_object($query)) { 5625 $rules[$result->level] = $result; 5626 } 5627 5628 //Because prices can be based on other level's prices, we temporarily store them 5629 $prices = array( 5630 1 => $baseprice 5631 ); 5632 5633 for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) { 5634 $price = $baseprice; 5635 $price_min = $baseprice; 5636 5637 //We have to make sure it does exist and it is > 0 5638 //First price level only allows changing min_price 5639 if ($i > 1 && isset($rules[$i]->var_percent) && $rules[$i]->var_percent) { 5640 $price = $prices[$rules[$i]->fk_level] * (1 + ($rules[$i]->var_percent / 100)); 5641 } 5642 5643 $prices[$i] = $price; 5644 5645 //We have to make sure it does exist and it is > 0 5646 if (isset($rules[$i]->var_min_percent) && $rules[$i]->var_min_percent) { 5647 $price_min = $price * (1 - ($rules[$i]->var_min_percent / 100)); 5648 } 5649 5650 //Little check to make sure the price is modified before triggering generation 5651 $check_amount = (($price == $this->multiprices[$i]) && ($price_min == $this->multiprices_min[$i])); 5652 $check_type = ($baseprice == $this->multiprices_base_type[$i]); 5653 5654 if ($check_amount && $check_type) { 5655 continue; 5656 } 5657 5658 if ($this->updatePrice($price, $price_type, $user, $price_vat, $price_min, $i, $npr, $psq, true) < 0) { 5659 return -1; 5660 } 5661 } 5662 5663 return 1; 5664 } 5665 5666 /** 5667 * Returns the rights used for this class 5668 * 5669 * @return Object 5670 */ 5671 public function getRights() 5672 { 5673 global $user; 5674 5675 if ($this->isProduct()) { 5676 return $user->rights->produit; 5677 } else { 5678 return $user->rights->service; 5679 } 5680 } 5681 5682 /** 5683 * Load information for tab info 5684 * 5685 * @param int $id Id of thirdparty to load 5686 * @return void 5687 */ 5688 public function info($id) 5689 { 5690 $sql = "SELECT p.rowid, p.ref, p.datec as date_creation, p.tms as date_modification,"; 5691 $sql .= " p.fk_user_author, p.fk_user_modif"; 5692 $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as p"; 5693 $sql .= " WHERE p.rowid = ".$id; 5694 5695 $result = $this->db->query($sql); 5696 if ($result) { 5697 if ($this->db->num_rows($result)) { 5698 $obj = $this->db->fetch_object($result); 5699 5700 $this->id = $obj->rowid; 5701 5702 if ($obj->fk_user_author) { 5703 $cuser = new User($this->db); 5704 $cuser->fetch($obj->fk_user_author); 5705 $this->user_creation = $cuser; 5706 } 5707 5708 if ($obj->fk_user_modif) { 5709 $muser = new User($this->db); 5710 $muser->fetch($obj->fk_user_modif); 5711 $this->user_modification = $muser; 5712 } 5713 5714 $this->ref = $obj->ref; 5715 $this->date_creation = $this->db->jdate($obj->date_creation); 5716 $this->date_modification = $this->db->jdate($obj->date_modification); 5717 } 5718 5719 $this->db->free($result); 5720 } else { 5721 dol_print_error($this->db); 5722 } 5723 } 5724} 5725 5726 5727 5728/** 5729 * Class to manage products or services. 5730 * Do not use 'Service' as class name since it is already used by APIs. 5731 */ 5732class ProductService extends Product 5733{ 5734 public $picto = 'service'; 5735} 5736