1<?php 2/* Copyright (C) 2015 Jean-François Ferry <jfefe@aternatik.fr> 3 * Copyright (C) 2019 Cedric Ancelin <icedo.anc@gmail.com> 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <https://www.gnu.org/licenses/>. 17 */ 18 19use Luracast\Restler\RestException; 20 21require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; 22require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php'; 23require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php'; 24require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttribute.class.php'; 25require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttributeValue.class.php'; 26require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination.class.php'; 27require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php'; 28 29/** 30 * API class for products 31 * 32 * @access protected 33 * @class DolibarrApiAccess {@requires user,external} 34 */ 35class Products extends DolibarrApi 36{ 37 /** 38 * @var array $FIELDS Mandatory fields, checked when create and update object 39 */ 40 public static $FIELDS = array( 41 'ref', 42 'label' 43 ); 44 45 /** 46 * @var Product $product {@type Product} 47 */ 48 public $product; 49 50 /** 51 * @var ProductFournisseur $productsupplier {@type ProductFournisseur} 52 */ 53 public $productsupplier; 54 55 /** 56 * Constructor 57 */ 58 public function __construct() 59 { 60 global $db, $conf; 61 62 $this->db = $db; 63 $this->product = new Product($this->db); 64 $this->productsupplier = new ProductFournisseur($this->db); 65 } 66 67 /** 68 * Get properties of a product object by id 69 * 70 * Return an array with product information. 71 * 72 * @param int $id ID of product 73 * @param int $includestockdata Load also information about stock (slower) 74 * @param bool $includesubproducts Load information about subproducts 75 * @param bool $includeparentid Load also ID of parent product (if product is a variant of a parent product) 76 * @param bool $includetrans Load also the translations of product label and description 77 * @return array|mixed Data without useless information 78 * 79 * @throws RestException 401 80 * @throws RestException 403 81 * @throws RestException 404 82 */ 83 public function get($id, $includestockdata = 0, $includesubproducts = false, $includeparentid = false, $includetrans = false) 84 { 85 return $this->_fetch($id, '', '', '', $includestockdata, $includesubproducts, $includeparentid, false, $includetrans); 86 } 87 88 /** 89 * Get properties of a product object by ref 90 * 91 * Return an array with product information. 92 * 93 * @param string $ref Ref of element 94 * @param int $includestockdata Load also information about stock (slower) 95 * @param bool $includesubproducts Load information about subproducts 96 * @param bool $includeparentid Load also ID of parent product (if product is a variant of a parent product) 97 * @param bool $includetrans Load also the translations of product label and description 98 * 99 * @return array|mixed Data without useless information 100 * 101 * @url GET ref/{ref} 102 * 103 * @throws RestException 401 104 * @throws RestException 403 105 * @throws RestException 404 106 */ 107 public function getByRef($ref, $includestockdata = 0, $includesubproducts = false, $includeparentid = false, $includetrans = false) 108 { 109 return $this->_fetch('', $ref, '', '', $includestockdata, $includesubproducts, $includeparentid, false, $includetrans); 110 } 111 112 /** 113 * Get properties of a product object by ref_ext 114 * 115 * Return an array with product information. 116 * 117 * @param string $ref_ext Ref_ext of element 118 * @param int $includestockdata Load also information about stock (slower) 119 * @param bool $includesubproducts Load information about subproducts 120 * @param bool $includeparentid Load also ID of parent product (if product is a variant of a parent product) 121 * @param bool $includetrans Load also the translations of product label and description 122 * 123 * @return array|mixed Data without useless information 124 * 125 * @url GET ref_ext/{ref_ext} 126 * 127 * @throws RestException 401 128 * @throws RestException 403 129 * @throws RestException 404 130 */ 131 public function getByRefExt($ref_ext, $includestockdata = 0, $includesubproducts = false, $includeparentid = false, $includetrans = false) 132 { 133 return $this->_fetch('', '', $ref_ext, '', $includestockdata, $includesubproducts, $includeparentid, false, $includetrans); 134 } 135 136 /** 137 * Get properties of a product object by barcode 138 * 139 * Return an array with product information. 140 * 141 * @param string $barcode Barcode of element 142 * @param int $includestockdata Load also information about stock (slower) 143 * @param bool $includesubproducts Load information about subproducts 144 * @param bool $includeparentid Load also ID of parent product (if product is a variant of a parent product) 145 * @param bool $includetrans Load also the translations of product label and description 146 * 147 * @return array|mixed Data without useless information 148 * 149 * @url GET barcode/{barcode} 150 * 151 * @throws RestException 401 152 * @throws RestException 403 153 * @throws RestException 404 154 */ 155 public function getByBarcode($barcode, $includestockdata = 0, $includesubproducts = false, $includeparentid = false, $includetrans = false) 156 { 157 return $this->_fetch('', '', '', $barcode, $includestockdata, $includesubproducts, $includeparentid, false, $includetrans); 158 } 159 160 /** 161 * List products 162 * 163 * Get a list of products 164 * 165 * @param string $sortfield Sort field 166 * @param string $sortorder Sort order 167 * @param int $limit Limit for list 168 * @param int $page Page number 169 * @param int $mode Use this param to filter list (0 for all, 1 for only product, 2 for only service) 170 * @param int $category Use this param to filter list by category 171 * @param string $sqlfilters Other criteria to filter answers separated by a comma. Syntax example "(t.tobuy:=:0) and (t.tosell:=:1)" 172 * @param bool $ids_only Return only IDs of product instead of all properties (faster, above all if list is long) 173 * @param int $variant_filter Use this param to filter list (0 = all, 1=products without variants, 2=parent of variants, 3=variants only) 174 * @param bool $pagination_data If this parameter is set to true the response will include pagination data. Default value is false. Page starts from 0 175 * @return array Array of product objects 176 */ 177 public function index($sortfield = "t.ref", $sortorder = 'ASC', $limit = 100, $page = 0, $mode = 0, $category = 0, $sqlfilters = '', $ids_only = false, $variant_filter = 0, $pagination_data = false) 178 { 179 global $db, $conf; 180 181 if (!DolibarrApiAccess::$user->rights->produit->lire) { 182 throw new RestException(403); 183 } 184 185 $obj_ret = array(); 186 187 $socid = DolibarrApiAccess::$user->socid ? DolibarrApiAccess::$user->socid : ''; 188 189 $sql = "SELECT t.rowid, t.ref, t.ref_ext"; 190 $sql .= " FROM ".MAIN_DB_PREFIX."product as t"; 191 if ($category > 0) { 192 $sql .= ", ".MAIN_DB_PREFIX."categorie_product as c"; 193 } 194 $sql .= ' WHERE t.entity IN ('.getEntity('product').')'; 195 196 if ($variant_filter == 1) { 197 $sql .= ' AND t.rowid not in (select distinct fk_product_parent from '.MAIN_DB_PREFIX.'product_attribute_combination)'; 198 $sql .= ' AND t.rowid not in (select distinct fk_product_child from '.MAIN_DB_PREFIX.'product_attribute_combination)'; 199 } 200 if ($variant_filter == 2) { 201 $sql .= ' AND t.rowid in (select distinct fk_product_parent from '.MAIN_DB_PREFIX.'product_attribute_combination)'; 202 } 203 if ($variant_filter == 3) { 204 $sql .= ' AND t.rowid in (select distinct fk_product_child from '.MAIN_DB_PREFIX.'product_attribute_combination)'; 205 } 206 207 // Select products of given category 208 if ($category > 0) { 209 $sql .= " AND c.fk_categorie = ".((int) $category); 210 $sql .= " AND c.fk_product = t.rowid"; 211 } 212 if ($mode == 1) { 213 // Show only products 214 $sql .= " AND t.fk_product_type = 0"; 215 } elseif ($mode == 2) { 216 // Show only services 217 $sql .= " AND t.fk_product_type = 1"; 218 } 219 // Add sql filters 220 if ($sqlfilters) { 221 if (!DolibarrApi::_checkFilters($sqlfilters)) { 222 throw new RestException(503, 'Error when validating parameter sqlfilters '.$sqlfilters); 223 } 224 //var_dump($sqlfilters);exit; 225 $regexstring = '\(([^:\'\(\)]+:[^:\'\(\)]+:[^\(\)]+)\)'; // We must accept datc:<:2020-01-01 10:10:10 226 $sql .= " AND (".preg_replace_callback('/'.$regexstring.'/', 'DolibarrApi::_forge_criteria_callback', $sqlfilters).")"; 227 } 228 229 //this query will return total products with the filters given 230 $sqlTotals = str_replace('SELECT t.rowid, t.ref, t.ref_ext', 'SELECT count(t.rowid) as total', $sql); 231 232 $sql .= $this->db->order($sortfield, $sortorder); 233 if ($limit) { 234 if ($page < 0) { 235 $page = 0; 236 } 237 $offset = $limit * $page; 238 239 $sql .= $this->db->plimit($limit + 1, $offset); 240 } 241 242 $result = $this->db->query($sql); 243 if ($result) { 244 $num = $this->db->num_rows($result); 245 $min = min($num, ($limit <= 0 ? $num : $limit)); 246 $i = 0; 247 while ($i < $min) { 248 $obj = $this->db->fetch_object($result); 249 if (!$ids_only) { 250 $product_static = new Product($this->db); 251 if ($product_static->fetch($obj->rowid)) { 252 $obj_ret[] = $this->_cleanObjectDatas($product_static); 253 } 254 } else { 255 $obj_ret[] = $obj->rowid; 256 } 257 $i++; 258 } 259 } else { 260 throw new RestException(503, 'Error when retrieve product list : '.$this->db->lasterror()); 261 } 262 if (!count($obj_ret)) { 263 throw new RestException(404, 'No product found'); 264 } 265 266 //if $pagination_data is true the response will contain element data with all values and element pagination with pagination data(total,page,limit) 267 if ($pagination_data) { 268 $totalsResult = $this->db->query($sqlTotals); 269 $total = $this->db->fetch_object($totalsResult)->total; 270 271 $tmp = $obj_ret; 272 $obj_ret = []; 273 274 $obj_ret['data'] = $tmp; 275 $obj_ret['pagination'] = [ 276 'total' => (int) $total, 277 'page' => $page, //count starts from 0 278 'page_count' => ceil((int) $total/$limit), 279 'limit' => $limit 280 ]; 281 } 282 283 return $obj_ret; 284 } 285 286 /** 287 * Create product object 288 * 289 * @param array $request_data Request data 290 * @return int ID of product 291 */ 292 public function post($request_data = null) 293 { 294 if (!DolibarrApiAccess::$user->rights->produit->creer) { 295 throw new RestException(401); 296 } 297 // Check mandatory fields 298 $result = $this->_validate($request_data); 299 300 foreach ($request_data as $field => $value) { 301 $this->product->$field = $value; 302 } 303 if ($this->product->create(DolibarrApiAccess::$user) < 0) { 304 throw new RestException(500, "Error creating product", array_merge(array($this->product->error), $this->product->errors)); 305 } 306 307 return $this->product->id; 308 } 309 310 /** 311 * Update product. 312 * Price will be updated by this API only if option is set on "One price per product". See other APIs for other price modes. 313 * 314 * @param int $id Id of product to update 315 * @param array $request_data Datas 316 * @return int 317 * 318 * @throws RestException 401 319 * @throws RestException 404 320 */ 321 public function put($id, $request_data = null) 322 { 323 global $conf; 324 325 if (!DolibarrApiAccess::$user->rights->produit->creer) { 326 throw new RestException(401); 327 } 328 329 $result = $this->product->fetch($id); 330 if (!$result) { 331 throw new RestException(404, 'Product not found'); 332 } 333 334 if (!DolibarrApi::_checkAccessToResource('product', $this->product->id)) { 335 throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); 336 } 337 338 $oldproduct = dol_clone($this->product, 0); 339 340 foreach ($request_data as $field => $value) { 341 if ($field == 'id') { 342 continue; 343 } 344 if ($field == 'stock_reel') { 345 throw new RestException(400, 'Stock reel cannot be updated here. Use the /stockmovements endpoint instead'); 346 } 347 $this->product->$field = $value; 348 } 349 350 $updatetype = false; 351 if ($this->product->type != $oldproduct->type && ($this->product->isProduct() || $this->product->isService())) { 352 $updatetype = true; 353 } 354 355 $result = $this->product->update($id, DolibarrApiAccess::$user, 1, 'update', $updatetype); 356 357 // If price mode is 1 price per product 358 if ($result > 0 && !empty($conf->global->PRODUCT_PRICE_UNIQ)) { 359 // We update price only if it was changed 360 $pricemodified = false; 361 if ($this->product->price_base_type != $oldproduct->price_base_type) { 362 $pricemodified = true; 363 } else { 364 if ($this->product->tva_tx != $oldproduct->tva_tx) { 365 $pricemodified = true; 366 } 367 if ($this->product->tva_npr != $oldproduct->tva_npr) { 368 $pricemodified = true; 369 } 370 if ($this->product->default_vat_code != $oldproduct->default_vat_code) { 371 $pricemodified = true; 372 } 373 374 if ($this->product->price_base_type == 'TTC') { 375 if ($this->product->price_ttc != $oldproduct->price_ttc) { 376 $pricemodified = true; 377 } 378 if ($this->product->price_min_ttc != $oldproduct->price_min_ttc) { 379 $pricemodified = true; 380 } 381 } else { 382 if ($this->product->price != $oldproduct->price) { 383 $pricemodified = true; 384 } 385 if ($this->product->price_min != $oldproduct->price_min) { 386 $pricemodified = true; 387 } 388 } 389 } 390 391 if ($pricemodified) { 392 $newvat = $this->product->tva_tx; 393 $newnpr = $this->product->tva_npr; 394 $newvatsrccode = $this->product->default_vat_code; 395 396 $newprice = $this->product->price; 397 $newpricemin = $this->product->price_min; 398 if ($this->product->price_base_type == 'TTC') { 399 $newprice = $this->product->price_ttc; 400 $newpricemin = $this->product->price_min_ttc; 401 } 402 403 $result = $this->product->updatePrice($newprice, $this->product->price_base_type, DolibarrApiAccess::$user, $newvat, $newpricemin, 0, $newnpr, 0, 0, array(), $newvatsrccode); 404 } 405 } 406 407 if ($result <= 0) { 408 throw new RestException(500, "Error updating product", array_merge(array($this->product->error), $this->product->errors)); 409 } 410 411 return $this->get($id); 412 } 413 414 /** 415 * Delete product 416 * 417 * @param int $id Product ID 418 * @return array 419 */ 420 public function delete($id) 421 { 422 if (!DolibarrApiAccess::$user->rights->produit->supprimer) { 423 throw new RestException(401); 424 } 425 $result = $this->product->fetch($id); 426 if (!$result) { 427 throw new RestException(404, 'Product not found'); 428 } 429 430 if (!DolibarrApi::_checkAccessToResource('product', $this->product->id)) { 431 throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); 432 } 433 434 // The Product::delete() method uses the global variable $user. 435 global $user; 436 $user = DolibarrApiAccess::$user; 437 438 return $this->product->delete(DolibarrApiAccess::$user); 439 } 440 441 /** 442 * Get the list of subproducts of the product. 443 * 444 * @param int $id Id of parent product/service 445 * @return array 446 * 447 * @throws RestException 448 * @throws RestException 401 449 * @throws RestException 404 450 * 451 * @url GET {id}/subproducts 452 */ 453 public function getSubproducts($id) 454 { 455 if (!DolibarrApiAccess::$user->rights->produit->lire) { 456 throw new RestException(401); 457 } 458 459 if (!DolibarrApi::_checkAccessToResource('product', $id)) { 460 throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); 461 } 462 463 $childsArbo = $this->product->getChildsArbo($id, 1); 464 465 $keys = ['rowid', 'qty', 'fk_product_type', 'label', 'incdec']; 466 $childs = []; 467 foreach ($childsArbo as $values) { 468 $childs[] = array_combine($keys, $values); 469 } 470 471 return $childs; 472 } 473 474 /** 475 * Add subproduct. 476 * 477 * Link a product/service to a parent product/service 478 * 479 * @param int $id Id of parent product/service 480 * @param int $subproduct_id Id of child product/service 481 * @param int $qty Quantity 482 * @param int $incdec 1=Increase/decrease stock of child when parent stock increase/decrease 483 * @return int 484 * 485 * @throws RestException 486 * @throws RestException 401 487 * @throws RestException 404 488 * 489 * @url POST {id}/subproducts/add 490 */ 491 public function addSubproducts($id, $subproduct_id, $qty, $incdec = 1) 492 { 493 if (!DolibarrApiAccess::$user->rights->produit->creer) { 494 throw new RestException(401); 495 } 496 497 if (!DolibarrApi::_checkAccessToResource('product', $id)) { 498 throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); 499 } 500 501 $result = $this->product->add_sousproduit($id, $subproduct_id, $qty, $incdec); 502 if ($result <= 0) { 503 throw new RestException(500, "Error adding product child"); 504 } 505 return $result; 506 } 507 508 /** 509 * Remove subproduct. 510 * Unlink a product/service from a parent product/service 511 * 512 * @param int $id Id of parent product/service 513 * @param int $subproduct_id Id of child product/service 514 * @return int 515 * 516 * @throws RestException 401 517 * @throws RestException 404 518 * 519 * @url DELETE {id}/subproducts/remove/{subproduct_id} 520 */ 521 public function delSubproducts($id, $subproduct_id) 522 { 523 if (!DolibarrApiAccess::$user->rights->produit->creer) { 524 throw new RestException(401); 525 } 526 527 if (!DolibarrApi::_checkAccessToResource('product', $id)) { 528 throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); 529 } 530 531 $result = $this->product->del_sousproduit($id, $subproduct_id); 532 if ($result <= 0) { 533 throw new RestException(500, "Error while removing product child"); 534 } 535 return $result; 536 } 537 538 539 /** 540 * Get categories for a product 541 * 542 * @param int $id ID of product 543 * @param string $sortfield Sort field 544 * @param string $sortorder Sort order 545 * @param int $limit Limit for list 546 * @param int $page Page number 547 * 548 * @return mixed 549 * 550 * @url GET {id}/categories 551 */ 552 public function getCategories($id, $sortfield = "s.rowid", $sortorder = 'ASC', $limit = 0, $page = 0) 553 { 554 if (!DolibarrApiAccess::$user->rights->categorie->lire) { 555 throw new RestException(401); 556 } 557 558 $categories = new Categorie($this->db); 559 560 $result = $categories->getListForItem($id, 'product', $sortfield, $sortorder, $limit, $page); 561 562 if (empty($result)) { 563 throw new RestException(404, 'No category found'); 564 } 565 566 if ($result < 0) { 567 throw new RestException(503, 'Error when retrieve category list : '.array_merge(array($categories->error), $categories->errors)); 568 } 569 570 return $result; 571 } 572 573 /** 574 * Get prices per segment for a product 575 * 576 * @param int $id ID of product 577 * 578 * @return mixed 579 * 580 * @url GET {id}/selling_multiprices/per_segment 581 */ 582 public function getCustomerPricesPerSegment($id) 583 { 584 global $conf; 585 586 if (!DolibarrApiAccess::$user->rights->produit->lire) { 587 throw new RestException(401); 588 } 589 590 if (empty($conf->global->PRODUIT_MULTIPRICES)) { 591 throw new RestException(400, 'API not available: this mode of pricing is not enabled by setup'); 592 } 593 594 $result = $this->product->fetch($id); 595 if (!$result) { 596 throw new RestException(404, 'Product not found'); 597 } 598 599 if ($result < 0) { 600 throw new RestException(503, 'Error when retrieve prices list : '.array_merge(array($this->product->error), $this->product->errors)); 601 } 602 603 return array( 604 'multiprices'=>$this->product->multiprices, 605 'multiprices_inc_tax'=>$this->product->multiprices_ttc, 606 'multiprices_min'=>$this->product->multiprices_min, 607 'multiprices_min_inc_tax'=>$this->product->multiprices_min_ttc, 608 'multiprices_vat'=>$this->product->multiprices_tva_tx, 609 'multiprices_base_type'=>$this->product->multiprices_base_type, 610 //'multiprices_default_vat_code'=>$this->product->multiprices_default_vat_code 611 ); 612 } 613 614 /** 615 * Get prices per customer for a product 616 * 617 * @param int $id ID of product 618 * @param string $thirdparty_id Thirdparty id to filter orders of (example '1') {@pattern /^[0-9,]*$/i} 619 * 620 * @return mixed 621 * 622 * @url GET {id}/selling_multiprices/per_customer 623 */ 624 public function getCustomerPricesPerCustomer($id, $thirdparty_id = '') 625 { 626 global $conf; 627 628 if (!DolibarrApiAccess::$user->rights->produit->lire) { 629 throw new RestException(401); 630 } 631 632 if (empty($conf->global->PRODUIT_CUSTOMER_PRICES)) { 633 throw new RestException(400, 'API not available: this mode of pricing is not enabled by setup'); 634 } 635 636 $socid = DolibarrApiAccess::$user->socid ? DolibarrApiAccess::$user->socid : ''; 637 if ($socid > 0 && $socid != $thirdparty_id) { 638 throw new RestException(401, 'Getting prices for all customers or for the customer ID '.$thirdparty_id.' is not allowed for login '.DolibarrApiAccess::$user->login); 639 } 640 641 $result = $this->product->fetch($id); 642 if (!$result) { 643 throw new RestException(404, 'Product not found'); 644 } 645 646 if ($result > 0) { 647 require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php'; 648 $prodcustprice = new Productcustomerprice($this->db); 649 $filter = array(); 650 $filter['t.fk_product'] .= $id; 651 if ($thirdparty_id) { 652 $filter['t.fk_soc'] .= $thirdparty_id; 653 } 654 $result = $prodcustprice->fetch_all('', '', 0, 0, $filter); 655 } 656 657 if (empty($prodcustprice->lines)) { 658 throw new RestException(404, 'Prices not found'); 659 } 660 661 return $prodcustprice->lines; 662 } 663 664 /** 665 * Get prices per quantity for a product 666 * 667 * @param int $id ID of product 668 * 669 * @return mixed 670 * 671 * @url GET {id}/selling_multiprices/per_quantity 672 */ 673 public function getCustomerPricesPerQuantity($id) 674 { 675 global $conf; 676 677 if (!DolibarrApiAccess::$user->rights->produit->lire) { 678 throw new RestException(401); 679 } 680 681 if (empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY)) { 682 throw new RestException(400, 'API not available: this mode of pricing is not enabled by setup'); 683 } 684 685 $result = $this->product->fetch($id); 686 if (!$result) { 687 throw new RestException(404, 'Product not found'); 688 } 689 690 if ($result < 0) { 691 throw new RestException(503, 'Error when retrieve prices list : '.array_merge(array($this->product->error), $this->product->errors)); 692 } 693 694 return array( 695 'prices_by_qty'=>$this->product->prices_by_qty[0], // 1 if price by quantity was activated for the product 696 'prices_by_qty_list'=>$this->product->prices_by_qty_list[0] 697 ); 698 } 699 700 /** 701 * Add/Update purchase prices for a product. 702 * 703 * @param int $id ID of Product 704 * @param float $qty Min quantity for which price is valid 705 * @param float $buyprice Purchase price for the quantity min 706 * @param string $price_base_type HT or TTC 707 * @param int $fourn_id Supplier ID 708 * @param int $availability Product availability 709 * @param string $ref_fourn Supplier ref 710 * @param float $tva_tx New VAT Rate (For example 8.5. Should not be a string) 711 * @param string $charges costs affering to product 712 * @param float $remise_percent Discount regarding qty (percent) 713 * @param float $remise Discount regarding qty (amount) 714 * @param int $newnpr Set NPR or not 715 * @param int $delivery_time_days Delay in days for delivery (max). May be '' if not defined. 716 * @param string $supplier_reputation Reputation with this product to the defined supplier (empty, FAVORITE, DONOTORDER) 717 * @param array $localtaxes_array Array with localtaxes info array('0'=>type1,'1'=>rate1,'2'=>type2,'3'=>rate2) (loaded by getLocalTaxesFromRate(vatrate, 0, ...) function). 718 * @param string $newdefaultvatcode Default vat code 719 * @param float $multicurrency_buyprice Purchase price for the quantity min in currency 720 * @param string $multicurrency_price_base_type HT or TTC in currency 721 * @param float $multicurrency_tx Rate currency 722 * @param string $multicurrency_code Currency code 723 * @param string $desc_fourn Custom description for product_fourn_price 724 * @param string $barcode Barcode 725 * @param int $fk_barcode_type Barcode type 726 * @return int 727 * 728 * @throws RestException 500 729 * @throws RestException 401 730 * 731 * @url POST {id}/purchase_prices 732 */ 733 public function addPurchasePrice($id, $qty, $buyprice, $price_base_type, $fourn_id, $availability, $ref_fourn, $tva_tx, $charges = 0, $remise_percent = 0, $remise = 0, $newnpr = 0, $delivery_time_days = 0, $supplier_reputation = '', $localtaxes_array = array(), $newdefaultvatcode = '', $multicurrency_buyprice = 0, $multicurrency_price_base_type = 'HT', $multicurrency_tx = 1, $multicurrency_code = '', $desc_fourn = '', $barcode = '', $fk_barcode_type = null) 734 { 735 if (!DolibarrApiAccess::$user->rights->produit->creer) { 736 throw new RestException(401); 737 } 738 739 $result = $this->productsupplier->fetch($id); 740 if (!$result) { 741 throw new RestException(404, 'Product not found'); 742 } 743 744 if (!DolibarrApi::_checkAccessToResource('product', $this->productsupplier->id)) { 745 throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); 746 } 747 748 $socid = DolibarrApiAccess::$user->socid ? DolibarrApiAccess::$user->socid : ''; 749 if ($socid > 0 && $socid != $fourn_id) { 750 throw new RestException(401, 'Adding purchase price for the supplier ID '.$fourn_id.' is not allowed for login '.DolibarrApiAccess::$user->login); 751 } 752 753 $result = $this->productsupplier->add_fournisseur(DolibarrApiAccess::$user, $fourn_id, $ref_fourn, $qty); 754 if ($result < 0) { 755 throw new RestException(500, "Error adding supplier to product : ".$this->db->lasterror()); 756 } 757 758 $fourn = new Fournisseur($this->db); 759 $result = $fourn->fetch($fourn_id); 760 if ($result <= 0) { 761 throw new RestException(404, 'Supplier not found'); 762 } 763 764 // Clean data 765 $ref_fourn = checkVal($ref_fourn, 'alphanohtml'); 766 $desc_fourn = checkVal($desc_fourn, 'restricthtml'); 767 $barcode = checkVal($barcode, 'alphanohtml'); 768 769 $result = $this->productsupplier->update_buyprice($qty, $buyprice, DolibarrApiAccess::$user, $price_base_type, $fourn, $availability, $ref_fourn, $tva_tx, $charges, $remise_percent, $remise, $newnpr, $delivery_time_days, $supplier_reputation, $localtaxes_array, $newdefaultvatcode, $multicurrency_buyprice, $multicurrency_price_base_type, $multicurrency_tx, $multicurrency_code, $desc_fourn, $barcode, $fk_barcode_type); 770 771 if ($result <= 0) { 772 throw new RestException(500, "Error updating buy price : ".$this->db->lasterror()); 773 } 774 return (int) $this->productsupplier->product_fourn_price_id; 775 } 776 777 /** 778 * Delete purchase price for a product 779 * 780 * @param int $id Product ID 781 * @param int $priceid purchase price ID 782 * 783 * @url DELETE {id}/purchase_prices/{priceid} 784 * 785 * @return int 786 * 787 * @throws RestException 401 788 * @throws RestException 404 789 * 790 */ 791 public function deletePurchasePrice($id, $priceid) 792 { 793 if (!DolibarrApiAccess::$user->rights->produit->supprimer) { 794 throw new RestException(401); 795 } 796 $result = $this->productsupplier->fetch($id); 797 if (!$result) { 798 throw new RestException(404, 'Product not found'); 799 } 800 801 if (!DolibarrApi::_checkAccessToResource('product', $this->productsupplier->id)) { 802 throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); 803 } 804 805 $resultsupplier = 0; 806 if ($result > 0) { 807 $resultsupplier = $this->productsupplier->remove_product_fournisseur_price($priceid); 808 } 809 810 return $resultsupplier; 811 } 812 813 /** 814 * Get a list of all purchase prices of products 815 * 816 * @param string $sortfield Sort field 817 * @param string $sortorder Sort order 818 * @param int $limit Limit for list 819 * @param int $page Page number 820 * @param int $mode Use this param to filter list (0 for all, 1 for only product, 2 for only service) 821 * @param int $category Use this param to filter list by category of product 822 * @param int $supplier Use this param to filter list by supplier 823 * @param string $sqlfilters Other criteria to filter answers separated by a comma. Syntax example "(t.tobuy:=:0) and (t.tosell:=:1)" 824 * @return array Array of product objects 825 * 826 * @url GET purchase_prices 827 */ 828 public function getSupplierProducts($sortfield = "t.ref", $sortorder = 'ASC', $limit = 100, $page = 0, $mode = 0, $category = 0, $supplier = 0, $sqlfilters = '') 829 { 830 global $db, $conf; 831 832 if (!DolibarrApiAccess::$user->rights->produit->lire) { 833 throw new RestException(401); 834 } 835 836 $obj_ret = array(); 837 838 // Force id of company for external users 839 $socid = DolibarrApiAccess::$user->socid ? DolibarrApiAccess::$user->socid : ''; 840 if ($socid > 0) { 841 if ($supplier != $socid || empty($supplier)) { 842 throw new RestException(401, 'As an external user, you can request only for your supplier id = '.$socid); 843 } 844 } 845 846 $sql = "SELECT t.rowid, t.ref, t.ref_ext"; 847 $sql .= " FROM ".MAIN_DB_PREFIX."product as t"; 848 if ($category > 0) { 849 $sql .= ", ".MAIN_DB_PREFIX."categorie_product as c"; 850 } 851 $sql .= ", ".MAIN_DB_PREFIX."product_fournisseur_price as s"; 852 853 $sql .= ' WHERE t.entity IN ('.getEntity('product').')'; 854 855 if ($supplier > 0) { 856 $sql .= " AND s.fk_soc = ".((int) $supplier); 857 } 858 if ($socid > 0) { // if external user 859 $sql .= " AND s.fk_soc = ".((int) $socid); 860 } 861 $sql .= " AND s.fk_product = t.rowid"; 862 // Select products of given category 863 if ($category > 0) { 864 $sql .= " AND c.fk_categorie = ".((int) $category); 865 $sql .= " AND c.fk_product = t.rowid"; 866 } 867 if ($mode == 1) { 868 // Show only products 869 $sql .= " AND t.fk_product_type = 0"; 870 } elseif ($mode == 2) { 871 // Show only services 872 $sql .= " AND t.fk_product_type = 1"; 873 } 874 // Add sql filters 875 if ($sqlfilters) { 876 if (!DolibarrApi::_checkFilters($sqlfilters)) { 877 throw new RestException(503, 'Error when validating parameter sqlfilters '.$sqlfilters); 878 } 879 $regexstring = '\(([^:\'\(\)]+:[^:\'\(\)]+:[^\(\)]+)\)'; 880 $sql .= " AND (".preg_replace_callback('/'.$regexstring.'/', 'DolibarrApi::_forge_criteria_callback', $sqlfilters).")"; 881 } 882 883 $sql .= $this->db->order($sortfield, $sortorder); 884 if ($limit) { 885 if ($page < 0) { 886 $page = 0; 887 } 888 $offset = $limit * $page; 889 $sql .= $this->db->plimit($limit + 1, $offset); 890 } 891 $result = $this->db->query($sql); 892 if ($result) { 893 $num = $this->db->num_rows($result); 894 $min = min($num, ($limit <= 0 ? $num : $limit)); 895 $i = 0; 896 while ($i < $min) { 897 $obj = $this->db->fetch_object($result); 898 899 $product_fourn = new ProductFournisseur($this->db); 900 $product_fourn_list = $product_fourn->list_product_fournisseur_price($obj->rowid, '', '', 0, 0); 901 foreach ($product_fourn_list as $tmpobj) { 902 $this->_cleanObjectDatas($tmpobj); 903 } 904 905 //var_dump($product_fourn_list->db);exit; 906 $obj_ret[$obj->rowid] = $product_fourn_list; 907 908 $i++; 909 } 910 } else { 911 throw new RestException(503, 'Error when retrieve product list : '.$this->db->lasterror()); 912 } 913 if (!count($obj_ret)) { 914 throw new RestException(404, 'No product found'); 915 } 916 return $obj_ret; 917 } 918 919 /** 920 * Get purchase prices for a product 921 * 922 * Return an array with product information. 923 * TODO implement getting a product by ref or by $ref_ext 924 * 925 * @param int $id ID of product 926 * @param string $ref Ref of element 927 * @param string $ref_ext Ref ext of element 928 * @param string $barcode Barcode of element 929 * @return array|mixed Data without useless information 930 * 931 * @url GET {id}/purchase_prices 932 * 933 * @throws RestException 401 934 * @throws RestException 403 935 * @throws RestException 404 936 * 937 */ 938 public function getPurchasePrices($id, $ref = '', $ref_ext = '', $barcode = '') 939 { 940 if (empty($id) && empty($ref) && empty($ref_ext) && empty($barcode)) { 941 throw new RestException(400, 'bad value for parameter id, ref, ref_ext or barcode'); 942 } 943 944 $id = (empty($id) ? 0 : $id); 945 946 if (!DolibarrApiAccess::$user->rights->produit->lire) { 947 throw new RestException(403); 948 } 949 950 $socid = DolibarrApiAccess::$user->socid ? DolibarrApiAccess::$user->socid : ''; 951 952 $result = $this->product->fetch($id, $ref, $ref_ext, $barcode); 953 if (!$result) { 954 throw new RestException(404, 'Product not found'); 955 } 956 957 if (!DolibarrApi::_checkAccessToResource('product', $this->product->id)) { 958 throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); 959 } 960 961 $product_fourn_list = array(); 962 963 if ($result) { 964 $product_fourn = new ProductFournisseur($this->db); 965 $product_fourn_list = $product_fourn->list_product_fournisseur_price($this->product->id, '', '', 0, 0, ($socid > 0 ? $socid : 0)); 966 } 967 968 foreach ($product_fourn_list as $tmpobj) { 969 $this->_cleanObjectDatas($tmpobj); 970 } 971 972 return $this->_cleanObjectDatas($product_fourn_list); 973 } 974 975 /** 976 * Get attributes. 977 * 978 * @param string $sortfield Sort field 979 * @param string $sortorder Sort order 980 * @param int $limit Limit for list 981 * @param int $page Page number 982 * @param string $sqlfilters Other criteria to filter answers separated by a comma. Syntax example "(t.ref:like:color)" 983 * @return array 984 * 985 * @throws RestException 401 986 * @throws RestException 404 987 * @throws RestException 503 988 * 989 * @url GET attributes 990 */ 991 public function getAttributes($sortfield = "t.ref", $sortorder = 'ASC', $limit = 100, $page = 0, $sqlfilters = '') 992 { 993 if (!DolibarrApiAccess::$user->rights->produit->lire) { 994 throw new RestException(401); 995 } 996 997 $sql = "SELECT t.rowid, t.ref, t.ref_ext, t.label, t.rang, t.entity"; 998 $sql .= " FROM ".MAIN_DB_PREFIX."product_attribute as t"; 999 $sql .= ' WHERE t.entity IN ('.getEntity('product').')'; 1000 1001 // Add sql filters 1002 if ($sqlfilters) { 1003 if (!DolibarrApi::_checkFilters($sqlfilters)) { 1004 throw new RestException(503, 'Error when validating parameter sqlfilters '.$sqlfilters); 1005 } 1006 $regexstring = '\(([^:\'\(\)]+:[^:\'\(\)]+:[^\(\)]+)\)'; 1007 $sql .= " AND (".preg_replace_callback('/'.$regexstring.'/', 'DolibarrApi::_forge_criteria_callback', $sqlfilters).")"; 1008 } 1009 1010 $sql .= $this->db->order($sortfield, $sortorder); 1011 if ($limit) { 1012 if ($page < 0) { 1013 $page = 0; 1014 } 1015 $offset = $limit * $page; 1016 1017 $sql .= $this->db->plimit($limit, $offset); 1018 } 1019 1020 $result = $this->db->query($sql); 1021 1022 if (!$result) { 1023 throw new RestException(503, 'Error when retrieve product attribute list : '.$this->db->lasterror()); 1024 } 1025 1026 $return = []; 1027 while ($result = $this->db->fetch_object($query)) { 1028 $tmp = new ProductAttribute($this->db); 1029 $tmp->id = $result->rowid; 1030 $tmp->ref = $result->ref; 1031 $tmp->ref_ext = $result->ref_ext; 1032 $tmp->label = $result->label; 1033 $tmp->rang = $result->rang; 1034 $tmp->entity = $result->entity; 1035 1036 $return[] = $this->_cleanObjectDatas($tmp); 1037 } 1038 1039 if (!count($return)) { 1040 throw new RestException(404, 'No product attribute found'); 1041 } 1042 1043 return $return; 1044 } 1045 1046 /** 1047 * Get attribute by ID. 1048 * 1049 * @param int $id ID of Attribute 1050 * @return array 1051 * 1052 * @throws RestException 401 1053 * @throws RestException 404 1054 * 1055 * @url GET attributes/{id} 1056 */ 1057 public function getAttributeById($id) 1058 { 1059 if (!DolibarrApiAccess::$user->rights->produit->lire) { 1060 throw new RestException(401); 1061 } 1062 1063 $prodattr = new ProductAttribute($this->db); 1064 $result = $prodattr->fetch((int) $id); 1065 1066 if ($result < 0) { 1067 throw new RestException(404, "Product attribute not found"); 1068 } 1069 1070 $fields = ["id", "ref", "ref_ext", "label", "rang", "entity"]; 1071 1072 foreach ($prodattr as $field => $value) { 1073 if (!in_array($field, $fields)) { 1074 unset($prodattr->{$field}); 1075 } 1076 } 1077 1078 $sql = "SELECT COUNT(*) as nb FROM ".MAIN_DB_PREFIX."product_attribute_combination2val as pac2v"; 1079 $sql .= " JOIN ".MAIN_DB_PREFIX."product_attribute_combination as pac ON pac2v.fk_prod_combination = pac.rowid"; 1080 $sql .= " WHERE pac2v.fk_prod_attr = ".((int) $prodattr->id)." AND pac.entity IN (".getEntity('product').")"; 1081 1082 $resql = $this->db->query($sql); 1083 $obj = $this->db->fetch_object($resql); 1084 $prodattr->is_used_by_products = (int) $obj->nb; 1085 1086 return $prodattr; 1087 } 1088 1089 /** 1090 * Get attributes by ref. 1091 * 1092 * @param string $ref Reference of Attribute 1093 * @return array 1094 * 1095 * @throws RestException 401 1096 * @throws RestException 404 1097 * 1098 * @url GET attributes/ref/{ref} 1099 */ 1100 public function getAttributesByRef($ref) 1101 { 1102 if (!DolibarrApiAccess::$user->rights->produit->lire) { 1103 throw new RestException(401); 1104 } 1105 1106 $sql = "SELECT rowid, ref, ref_ext, label, rang, entity FROM ".MAIN_DB_PREFIX."product_attribute WHERE ref LIKE '".trim($ref)."' AND entity IN (".getEntity('product').")"; 1107 1108 $query = $this->db->query($sql); 1109 1110 if (!$this->db->num_rows($query)) { 1111 throw new RestException(404); 1112 } 1113 1114 $result = $this->db->fetch_object($query); 1115 1116 $attr = []; 1117 $attr['id'] = $result->rowid; 1118 $attr['ref'] = $result->ref; 1119 $attr['ref_ext'] = $result->ref_ext; 1120 $attr['label'] = $result->label; 1121 $attr['rang'] = $result->rang; 1122 $attr['entity'] = $result->entity; 1123 1124 $sql = "SELECT COUNT(*) as nb FROM ".MAIN_DB_PREFIX."product_attribute_combination2val as pac2v"; 1125 $sql .= " JOIN ".MAIN_DB_PREFIX."product_attribute_combination as pac ON pac2v.fk_prod_combination = pac.rowid"; 1126 $sql .= " WHERE pac2v.fk_prod_attr = ".((int) $result->rowid)." AND pac.entity IN (".getEntity('product').")"; 1127 1128 $resql = $this->db->query($sql); 1129 $obj = $this->db->fetch_object($resql); 1130 1131 $attr["is_used_by_products"] = (int) $obj->nb; 1132 1133 return $attr; 1134 } 1135 1136 /** 1137 * Get attributes by ref_ext. 1138 * 1139 * @param string $ref_ext External reference of Attribute 1140 * @return array 1141 * 1142 * @throws RestException 500 1143 * @throws RestException 401 1144 * 1145 * @url GET attributes/ref_ext/{ref_ext} 1146 */ 1147 public function getAttributesByRefExt($ref_ext) 1148 { 1149 if (!DolibarrApiAccess::$user->rights->produit->lire) { 1150 throw new RestException(401); 1151 } 1152 1153 $sql = "SELECT rowid, ref, ref_ext, label, rang, entity FROM ".MAIN_DB_PREFIX."product_attribute WHERE ref_ext LIKE '".trim($ref_ext)."' AND entity IN (".getEntity('product').")"; 1154 1155 $query = $this->db->query($sql); 1156 1157 if (!$this->db->num_rows($query)) { 1158 throw new RestException(404); 1159 } 1160 1161 $result = $this->db->fetch_object($query); 1162 1163 $attr = []; 1164 $attr['id'] = $result->rowid; 1165 $attr['ref'] = $result->ref; 1166 $attr['ref_ext'] = $result->ref_ext; 1167 $attr['label'] = $result->label; 1168 $attr['rang'] = $result->rang; 1169 $attr['entity'] = $result->entity; 1170 1171 $sql = "SELECT COUNT(*) as nb FROM ".MAIN_DB_PREFIX."product_attribute_combination2val as pac2v"; 1172 $sql .= " JOIN ".MAIN_DB_PREFIX."product_attribute_combination as pac ON pac2v.fk_prod_combination = pac.rowid"; 1173 $sql .= " WHERE pac2v.fk_prod_attr = ".((int) $result->rowid)." AND pac.entity IN (".getEntity('product').")"; 1174 1175 $resql = $this->db->query($sql); 1176 $obj = $this->db->fetch_object($resql); 1177 $attr["is_used_by_products"] = (int) $obj->nb; 1178 1179 return $attr; 1180 } 1181 1182 /** 1183 * Add attributes. 1184 * 1185 * @param string $ref Reference of Attribute 1186 * @param string $label Label of Attribute 1187 * @param string $ref_ext Reference of Attribute 1188 * @return int 1189 * 1190 * @throws RestException 500 1191 * @throws RestException 401 1192 * 1193 * @url POST attributes 1194 */ 1195 public function addAttributes($ref, $label, $ref_ext = '') 1196 { 1197 if (!DolibarrApiAccess::$user->rights->produit->creer) { 1198 throw new RestException(401); 1199 } 1200 1201 $prodattr = new ProductAttribute($this->db); 1202 $prodattr->label = $label; 1203 $prodattr->ref = $ref; 1204 $prodattr->ref_ext = $ref_ext; 1205 1206 $resid = $prodattr->create(DolibarrApiAccess::$user); 1207 if ($resid <= 0) { 1208 throw new RestException(500, "Error creating new attribute"); 1209 } 1210 return $resid; 1211 } 1212 1213 /** 1214 * Update attributes by id. 1215 * 1216 * @param int $id ID of Attribute 1217 * @param array $request_data Datas 1218 * @return array 1219 * 1220 * @throws RestException 1221 * @throws RestException 401 1222 * @throws RestException 404 1223 * 1224 * @url PUT attributes/{id} 1225 */ 1226 public function putAttributes($id, $request_data = null) 1227 { 1228 if (!DolibarrApiAccess::$user->rights->produit->creer) { 1229 throw new RestException(401); 1230 } 1231 1232 $prodattr = new ProductAttribute($this->db); 1233 1234 $result = $prodattr->fetch((int) $id); 1235 if ($result == 0) { 1236 throw new RestException(404, 'Attribute not found'); 1237 } elseif ($result < 0) { 1238 throw new RestException(500, "Error fetching attribute"); 1239 } 1240 1241 foreach ($request_data as $field => $value) { 1242 if ($field == 'rowid') { 1243 continue; 1244 } 1245 $prodattr->$field = $value; 1246 } 1247 1248 if ($prodattr->update(DolibarrApiAccess::$user) > 0) { 1249 $result = $prodattr->fetch((int) $id); 1250 if ($result == 0) { 1251 throw new RestException(404, 'Attribute not found'); 1252 } elseif ($result < 0) { 1253 throw new RestException(500, "Error fetching attribute"); 1254 } else { 1255 return $prodattr; 1256 } 1257 } 1258 throw new RestException(500, "Error updating attribute"); 1259 } 1260 1261 /** 1262 * Delete attributes by id. 1263 * 1264 * @param int $id ID of Attribute 1265 * @return int Result of deletion 1266 * 1267 * @throws RestException 500 1268 * @throws RestException 401 1269 * 1270 * @url DELETE attributes/{id} 1271 */ 1272 public function deleteAttributes($id) 1273 { 1274 if (!DolibarrApiAccess::$user->rights->produit->supprimer) { 1275 throw new RestException(401); 1276 } 1277 1278 $prodattr = new ProductAttribute($this->db); 1279 $prodattr->id = (int) $id; 1280 $result = $prodattr->delete(DolibarrApiAccess::$user); 1281 1282 if ($result <= 0) { 1283 throw new RestException(500, "Error deleting attribute"); 1284 } 1285 1286 return $result; 1287 } 1288 1289 /** 1290 * Get attribute value by id. 1291 * 1292 * @param int $id ID of Attribute value 1293 * @return array 1294 * 1295 * @throws RestException 500 1296 * @throws RestException 401 1297 * 1298 * @url GET attributes/values/{id} 1299 */ 1300 public function getAttributeValueById($id) 1301 { 1302 if (!DolibarrApiAccess::$user->rights->produit->lire) { 1303 throw new RestException(401); 1304 } 1305 1306 $sql = "SELECT rowid, fk_product_attribute, ref, value FROM ".MAIN_DB_PREFIX."product_attribute_value WHERE rowid = ".(int) $id." AND entity IN (".getEntity('product').")"; 1307 1308 $query = $this->db->query($sql); 1309 1310 if (!$query) { 1311 throw new RestException(401); 1312 } 1313 1314 if (!$this->db->num_rows($query)) { 1315 throw new RestException(404, 'Attribute value not found'); 1316 } 1317 1318 $result = $this->db->fetch_object($query); 1319 1320 $attrval = []; 1321 $attrval['id'] = $result->rowid; 1322 $attrval['fk_product_attribute'] = $result->fk_product_attribute; 1323 $attrval['ref'] = $result->ref; 1324 $attrval['value'] = $result->value; 1325 1326 return $attrval; 1327 } 1328 1329 /** 1330 * Get attribute value by ref. 1331 * 1332 * @param int $id ID of Attribute value 1333 * @param string $ref Ref of Attribute value 1334 * @return array 1335 * 1336 * @throws RestException 500 1337 * @throws RestException 401 1338 * 1339 * @url GET attributes/{id}/values/ref/{ref} 1340 */ 1341 public function getAttributeValueByRef($id, $ref) 1342 { 1343 if (!DolibarrApiAccess::$user->rights->produit->lire) { 1344 throw new RestException(401); 1345 } 1346 1347 $ref = trim($ref); 1348 1349 $sql = "SELECT rowid, fk_product_attribute, ref, value FROM ".MAIN_DB_PREFIX."product_attribute_value"; 1350 $sql .= " WHERE ref LIKE '".$this->db->escape($ref)."' AND fk_product_attribute = ".((int) $id)." AND entity IN (".getEntity('product').")"; 1351 1352 $query = $this->db->query($sql); 1353 1354 if (!$query) { 1355 throw new RestException(401); 1356 } 1357 1358 if (!$this->db->num_rows($query)) { 1359 throw new RestException(404, 'Attribute value not found'); 1360 } 1361 1362 $result = $this->db->fetch_object($query); 1363 1364 $attrval = []; 1365 $attrval['id'] = $result->rowid; 1366 $attrval['fk_product_attribute'] = $result->fk_product_attribute; 1367 $attrval['ref'] = $result->ref; 1368 $attrval['value'] = $result->value; 1369 1370 return $attrval; 1371 } 1372 1373 /** 1374 * Delete attribute value by ref. 1375 * 1376 * @param int $id ID of Attribute 1377 * @param string $ref Ref of Attribute value 1378 * @return int 1379 * 1380 * @throws RestException 401 1381 * 1382 * @url DELETE attributes/{id}/values/ref/{ref} 1383 */ 1384 public function deleteAttributeValueByRef($id, $ref) 1385 { 1386 if (!DolibarrApiAccess::$user->rights->produit->supprimer) { 1387 throw new RestException(401); 1388 } 1389 1390 $ref = trim($ref); 1391 1392 $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_attribute_value"; 1393 $sql .= " WHERE ref LIKE '".$this->db->escape($ref)."' AND fk_product_attribute = ".((int) $id)." AND entity IN (".getEntity('product').")"; 1394 $query = $this->db->query($sql); 1395 1396 if (!$query) { 1397 throw new RestException(401); 1398 } 1399 1400 if (!$this->db->num_rows($query)) { 1401 throw new RestException(404, 'Attribute value not found'); 1402 } 1403 1404 $result = $this->db->fetch_object($query); 1405 1406 $attrval = new ProductAttributeValue($this->db); 1407 $attrval->id = $result->rowid; 1408 $result = $attrval->delete(DolibarrApiAccess::$user); 1409 if ($result > 0) { 1410 return 1; 1411 } 1412 1413 throw new RestException(500, "Error deleting attribute value"); 1414 } 1415 1416 /** 1417 * Get all values for an attribute id. 1418 * 1419 * @param int $id ID of an Attribute 1420 * @return array 1421 * 1422 * @throws RestException 401 1423 * @throws RestException 500 1424 * 1425 * @url GET attributes/{id}/values 1426 */ 1427 public function getAttributeValues($id) 1428 { 1429 if (!DolibarrApiAccess::$user->rights->produit->lire) { 1430 throw new RestException(401); 1431 } 1432 1433 $objectval = new ProductAttributeValue($this->db); 1434 1435 $return = $objectval->fetchAllByProductAttribute((int) $id); 1436 1437 if (count($return) == 0) { 1438 throw new RestException(404, 'Attribute values not found'); 1439 } 1440 1441 foreach ($return as $key => $val) { 1442 $return[$key] = $this->_cleanObjectDatas($return[$key]); 1443 } 1444 1445 return $return; 1446 } 1447 1448 /** 1449 * Get all values for an attribute ref. 1450 * 1451 * @param string $ref Ref of an Attribute 1452 * @return array 1453 * 1454 * @throws RestException 401 1455 * 1456 * @url GET attributes/ref/{ref}/values 1457 */ 1458 public function getAttributeValuesByRef($ref) 1459 { 1460 if (!DolibarrApiAccess::$user->rights->produit->lire) { 1461 throw new RestException(401); 1462 } 1463 1464 $ref = trim($ref); 1465 1466 $return = array(); 1467 1468 $sql = 'SELECT '; 1469 $sql .= 'v.fk_product_attribute, v.rowid, v.ref, v.value FROM '.MAIN_DB_PREFIX.'product_attribute_value as v'; 1470 $sql .= " WHERE v.fk_product_attribute IN (SELECT rowid FROM ".MAIN_DB_PREFIX."product_attribute WHERE ref LIKE '".$this->db->escape($ref)."')"; 1471 1472 $resql = $this->db->query($sql); 1473 1474 while ($result = $this->db->fetch_object($resql)) { 1475 $tmp = new ProductAttributeValue($this->db); 1476 $tmp->fk_product_attribute = $result->fk_product_attribute; 1477 $tmp->id = $result->rowid; 1478 $tmp->ref = $result->ref; 1479 $tmp->value = $result->value; 1480 1481 $return[] = $this->_cleanObjectDatas($tmp); 1482 } 1483 1484 return $return; 1485 } 1486 1487 /** 1488 * Add attribute value. 1489 * 1490 * @param int $id ID of Attribute 1491 * @param string $ref Reference of Attribute value 1492 * @param string $value Value of Attribute value 1493 * @return int 1494 * 1495 * @throws RestException 500 1496 * @throws RestException 401 1497 * 1498 * @url POST attributes/{id}/values 1499 */ 1500 public function addAttributeValue($id, $ref, $value) 1501 { 1502 if (!DolibarrApiAccess::$user->rights->produit->creer) { 1503 throw new RestException(401); 1504 } 1505 1506 if (empty($ref) || empty($value)) { 1507 throw new RestException(401); 1508 } 1509 1510 $objectval = new ProductAttributeValue($this->db); 1511 $objectval->fk_product_attribute = ((int) $id); 1512 $objectval->ref = $ref; 1513 $objectval->value = $value; 1514 1515 if ($objectval->create(DolibarrApiAccess::$user) > 0) { 1516 return $objectval->id; 1517 } 1518 throw new RestException(500, "Error creating new attribute value"); 1519 } 1520 1521 /** 1522 * Update attribute value. 1523 * 1524 * @param int $id ID of Attribute 1525 * @param array $request_data Datas 1526 * @return array 1527 * 1528 * @throws RestException 401 1529 * @throws RestException 500 1530 * 1531 * @url PUT attributes/values/{id} 1532 */ 1533 public function putAttributeValue($id, $request_data) 1534 { 1535 if (!DolibarrApiAccess::$user->rights->produit->creer) { 1536 throw new RestException(401); 1537 } 1538 1539 $objectval = new ProductAttributeValue($this->db); 1540 $result = $objectval->fetch((int) $id); 1541 1542 if ($result == 0) { 1543 throw new RestException(404, 'Attribute value not found'); 1544 } elseif ($result < 0) { 1545 throw new RestException(500, "Error fetching attribute value"); 1546 } 1547 1548 foreach ($request_data as $field => $value) { 1549 if ($field == 'rowid') { 1550 continue; 1551 } 1552 $objectval->$field = $value; 1553 } 1554 1555 if ($objectval->update(DolibarrApiAccess::$user) > 0) { 1556 $result = $objectval->fetch((int) $id); 1557 if ($result == 0) { 1558 throw new RestException(404, 'Attribute not found'); 1559 } elseif ($result < 0) { 1560 throw new RestException(500, "Error fetching attribute"); 1561 } else { 1562 return $objectval; 1563 } 1564 } 1565 throw new RestException(500, "Error updating attribute"); 1566 } 1567 1568 /** 1569 * Delete attribute value by id. 1570 * 1571 * @param int $id ID of Attribute value 1572 * @return int 1573 * 1574 * @throws RestException 500 1575 * @throws RestException 401 1576 * 1577 * @url DELETE attributes/values/{id} 1578 */ 1579 public function deleteAttributeValueById($id) 1580 { 1581 if (!DolibarrApiAccess::$user->rights->produit->supprimer) { 1582 throw new RestException(401); 1583 } 1584 1585 $objectval = new ProductAttributeValue($this->db); 1586 $objectval->id = (int) $id; 1587 1588 if ($objectval->delete(DolibarrApiAccess::$user) > 0) { 1589 return 1; 1590 } 1591 throw new RestException(500, "Error deleting attribute value"); 1592 } 1593 1594 /** 1595 * Get product variants. 1596 * 1597 * @param int $id ID of Product 1598 * @param int $includestock Default value 0. If parameter is set to 1 the response will contain stock data of each variant 1599 * @return array 1600 * 1601 * @throws RestException 500 1602 * @throws RestException 401 1603 * 1604 * @url GET {id}/variants 1605 */ 1606 public function getVariants($id, $includestock = 0) 1607 { 1608 if (!DolibarrApiAccess::$user->rights->produit->lire) { 1609 throw new RestException(401); 1610 } 1611 1612 $prodcomb = new ProductCombination($this->db); 1613 $combinations = $prodcomb->fetchAllByFkProductParent((int) $id); 1614 1615 foreach ($combinations as $key => $combination) { 1616 $prodc2vp = new ProductCombination2ValuePair($this->db); 1617 $combinations[$key]->attributes = $prodc2vp->fetchByFkCombination((int) $combination->id); 1618 $combinations[$key] = $this->_cleanObjectDatas($combinations[$key]); 1619 1620 if ($includestock==1) { 1621 $productModel = new Product($this->db); 1622 $productModel->fetch((int) $combination->fk_product_child); 1623 $productModel->load_stock(); 1624 $combinations[$key]->stock_warehouse = $this->_cleanObjectDatas($productModel)->stock_warehouse; 1625 } 1626 } 1627 1628 return $combinations; 1629 } 1630 1631 /** 1632 * Get product variants by Product ref. 1633 * 1634 * @param string $ref Ref of Product 1635 * @return array 1636 * 1637 * @throws RestException 500 1638 * @throws RestException 401 1639 * 1640 * @url GET ref/{ref}/variants 1641 */ 1642 public function getVariantsByProdRef($ref) 1643 { 1644 if (!DolibarrApiAccess::$user->rights->produit->lire) { 1645 throw new RestException(401); 1646 } 1647 1648 $result = $this->product->fetch('', $ref); 1649 if (!$result) { 1650 throw new RestException(404, 'Product not found'); 1651 } 1652 1653 $prodcomb = new ProductCombination($this->db); 1654 $combinations = $prodcomb->fetchAllByFkProductParent((int) $this->product->id); 1655 1656 foreach ($combinations as $key => $combination) { 1657 $prodc2vp = new ProductCombination2ValuePair($this->db); 1658 $combinations[$key]->attributes = $prodc2vp->fetchByFkCombination((int) $combination->id); 1659 $combinations[$key] = $this->_cleanObjectDatas($combinations[$key]); 1660 } 1661 1662 return $combinations; 1663 } 1664 1665 /** 1666 * Add variant. 1667 * 1668 * "features" is a list of attributes pairs id_attribute=>id_value. Example: array(id_color=>id_Blue, id_size=>id_small, id_option=>id_val_a, ...) 1669 * 1670 * @param int $id ID of Product 1671 * @param float $weight_impact Weight impact of variant 1672 * @param float $price_impact Price impact of variant 1673 * @param bool $price_impact_is_percent Price impact in percent (true or false) 1674 * @param array $features List of attributes pairs id_attribute->id_value. Example: array(id_color=>id_Blue, id_size=>id_small, id_option=>id_val_a, ...) 1675 * @param string $reference Customized reference of variant 1676 * @param string $ref_ext External reference of variant 1677 * @return int 1678 * 1679 * @throws RestException 500 1680 * @throws RestException 401 1681 * @throws RestException 404 1682 * 1683 * @url POST {id}/variants 1684 */ 1685 public function addVariant($id, $weight_impact, $price_impact, $price_impact_is_percent, $features, $reference = '', $ref_ext = '') 1686 { 1687 if (!DolibarrApiAccess::$user->rights->produit->creer) { 1688 throw new RestException(401); 1689 } 1690 1691 if (empty($id) || empty($features) || !is_array($features)) { 1692 throw new RestException(401); 1693 } 1694 1695 $weight_impact = price2num($weight_impact); 1696 $price_impact = price2num($price_impact); 1697 1698 $prodattr = new ProductAttribute($this->db); 1699 $prodattr_val = new ProductAttributeValue($this->db); 1700 foreach ($features as $id_attr => $id_value) { 1701 if ($prodattr->fetch((int) $id_attr) < 0) { 1702 throw new RestException(401); 1703 } 1704 if ($prodattr_val->fetch((int) $id_value) < 0) { 1705 throw new RestException(401); 1706 } 1707 } 1708 1709 $result = $this->product->fetch((int) $id); 1710 if (!$result) { 1711 throw new RestException(404, 'Product not found'); 1712 } 1713 1714 $prodcomb = new ProductCombination($this->db); 1715 1716 $result = $prodcomb->createProductCombination(DolibarrApiAccess::$user, $this->product, $features, array(), $price_impact_is_percent, $price_impact, $weight_impact, $reference, $ref_ext); 1717 if ($result > 0) { 1718 return $result; 1719 } else { 1720 throw new RestException(500, "Error creating new product variant"); 1721 } 1722 } 1723 1724 /** 1725 * Add variant by product ref. 1726 * 1727 * "features" is a list of attributes pairs id_attribute=>id_value. Example: array(id_color=>id_Blue, id_size=>id_small, id_option=>id_val_a, ...) 1728 * 1729 * @param string $ref Ref of Product 1730 * @param float $weight_impact Weight impact of variant 1731 * @param float $price_impact Price impact of variant 1732 * @param bool $price_impact_is_percent Price impact in percent (true or false) 1733 * @param array $features List of attributes pairs id_attribute->id_value. Example: array(id_color=>id_Blue, id_size=>id_small, id_option=>id_val_a, ...) 1734 * @return int 1735 * 1736 * @throws RestException 500 1737 * @throws RestException 401 1738 * @throws RestException 404 1739 * 1740 * @url POST ref/{ref}/variants 1741 */ 1742 public function addVariantByProductRef($ref, $weight_impact, $price_impact, $price_impact_is_percent, $features) 1743 { 1744 if (!DolibarrApiAccess::$user->rights->produit->creer) { 1745 throw new RestException(401); 1746 } 1747 1748 if (empty($ref) || empty($features) || !is_array($features)) { 1749 throw new RestException(401); 1750 } 1751 1752 $weight_impact = price2num($weight_impact); 1753 $price_impact = price2num($price_impact); 1754 1755 $prodattr = new ProductAttribute($this->db); 1756 $prodattr_val = new ProductAttributeValue($this->db); 1757 foreach ($features as $id_attr => $id_value) { 1758 if ($prodattr->fetch((int) $id_attr) < 0) { 1759 throw new RestException(404); 1760 } 1761 if ($prodattr_val->fetch((int) $id_value) < 0) { 1762 throw new RestException(404); 1763 } 1764 } 1765 1766 $result = $this->product->fetch('', trim($ref)); 1767 if (!$result) { 1768 throw new RestException(404, 'Product not found'); 1769 } 1770 1771 $prodcomb = new ProductCombination($this->db); 1772 if (!$prodcomb->fetchByProductCombination2ValuePairs($this->product->id, $features)) { 1773 $result = $prodcomb->createProductCombination(DolibarrApiAccess::$user, $this->product, $features, array(), $price_impact_is_percent, $price_impact, $weight_impact); 1774 if ($result > 0) { 1775 return $result; 1776 } else { 1777 throw new RestException(500, "Error creating new product variant"); 1778 } 1779 } else { 1780 return $prodcomb->id; 1781 } 1782 } 1783 1784 /** 1785 * Put product variants. 1786 * 1787 * @param int $id ID of Variant 1788 * @param array $request_data Datas 1789 * @return int 1790 * 1791 * @throws RestException 500 1792 * @throws RestException 401 1793 * 1794 * @url PUT variants/{id} 1795 */ 1796 public function putVariant($id, $request_data = null) 1797 { 1798 if (!DolibarrApiAccess::$user->rights->produit->creer) { 1799 throw new RestException(401); 1800 } 1801 1802 $prodcomb = new ProductCombination($this->db); 1803 $prodcomb->fetch((int) $id); 1804 1805 foreach ($request_data as $field => $value) { 1806 if ($field == 'rowid') { 1807 continue; 1808 } 1809 $prodcomb->$field = $value; 1810 } 1811 1812 $result = $prodcomb->update(DolibarrApiAccess::$user); 1813 if ($result > 0) { 1814 return 1; 1815 } 1816 throw new RestException(500, "Error editing variant"); 1817 } 1818 1819 /** 1820 * Delete product variants. 1821 * 1822 * @param int $id ID of Variant 1823 * @return int Result of deletion 1824 * 1825 * @throws RestException 500 1826 * @throws RestException 401 1827 * 1828 * @url DELETE variants/{id} 1829 */ 1830 public function deleteVariant($id) 1831 { 1832 if (!DolibarrApiAccess::$user->rights->produit->supprimer) { 1833 throw new RestException(401); 1834 } 1835 1836 $prodcomb = new ProductCombination($this->db); 1837 $prodcomb->id = (int) $id; 1838 $result = $prodcomb->delete(DolibarrApiAccess::$user); 1839 if ($result <= 0) { 1840 throw new RestException(500, "Error deleting variant"); 1841 } 1842 return $result; 1843 } 1844 1845 /** 1846 * Get stock data for the product id given. 1847 * Optionaly with $selected_warehouse_id parameter user can get stock of specific warehouse 1848 * 1849 * @param int $id ID of Product 1850 * @param int $selected_warehouse_id ID of warehouse 1851 * @return int 1852 * 1853 * @throws RestException 500 1854 * @throws RestException 401 1855 * @throws RestException 404 1856 * 1857 * @url GET {id}/stock 1858 */ 1859 public function getStock($id, $selected_warehouse_id = null) 1860 { 1861 1862 if (!DolibarrApiAccess::$user->rights->produit->lire) { 1863 throw new RestException(401); 1864 } 1865 1866 if (!DolibarrApi::_checkAccessToResource('product', $id)) { 1867 throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); 1868 } 1869 1870 $product_model = new Product($this->db); 1871 $product_model->fetch($id); 1872 $product_model->load_stock(); 1873 1874 $stockData = $this->_cleanObjectDatas($product_model)->stock_warehouse; 1875 if ($selected_warehouse_id) { 1876 foreach ($stockData as $warehouse_id => $warehouse) { 1877 if ($warehouse_id != $selected_warehouse_id) { 1878 unset($stockData[$warehouse_id]); 1879 } 1880 } 1881 } 1882 1883 if (empty($stockData)) { 1884 throw new RestException(404, 'No stock found'); 1885 } 1886 1887 return ['stock_warehouses'=>$stockData]; 1888 } 1889 1890 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore 1891 /** 1892 * Clean sensible object datas 1893 * 1894 * @param Object $object Object to clean 1895 * @return Object Object with cleaned properties 1896 */ 1897 protected function _cleanObjectDatas($object) 1898 { 1899 // phpcs:enable 1900 $object = parent::_cleanObjectDatas($object); 1901 1902 unset($object->statut); 1903 1904 unset($object->regeximgext); 1905 unset($object->price_by_qty); 1906 unset($object->prices_by_qty_id); 1907 unset($object->libelle); 1908 unset($object->product_id_already_linked); 1909 unset($object->reputations); 1910 unset($object->db); 1911 unset($object->name); 1912 unset($object->firstname); 1913 unset($object->lastname); 1914 unset($object->civility_id); 1915 unset($object->contact); 1916 unset($object->contact_id); 1917 unset($object->thirdparty); 1918 unset($object->user); 1919 unset($object->origin); 1920 unset($object->origin_id); 1921 unset($object->fourn_pu); 1922 unset($object->fourn_price_base_type); 1923 unset($object->fourn_socid); 1924 unset($object->ref_fourn); 1925 unset($object->ref_supplier); 1926 unset($object->product_fourn_id); 1927 unset($object->fk_project); 1928 1929 unset($object->mode_reglement_id); 1930 unset($object->cond_reglement_id); 1931 unset($object->demand_reason_id); 1932 unset($object->transport_mode_id); 1933 unset($object->cond_reglement); 1934 unset($object->shipping_method_id); 1935 unset($object->model_pdf); 1936 unset($object->note); 1937 1938 unset($object->nbphoto); 1939 unset($object->recuperableonly); 1940 unset($object->multiprices_recuperableonly); 1941 unset($object->tva_npr); 1942 unset($object->lines); 1943 unset($object->fk_bank); 1944 unset($object->fk_account); 1945 1946 unset($object->supplierprices); // Mut use another API to get them 1947 1948 1949 return $object; 1950 } 1951 1952 /** 1953 * Validate fields before create or update object 1954 * 1955 * @param array $data Datas to validate 1956 * @return array 1957 * @throws RestException 1958 */ 1959 private function _validate($data) 1960 { 1961 $product = array(); 1962 foreach (Products::$FIELDS as $field) { 1963 if (!isset($data[$field])) { 1964 throw new RestException(400, "$field field missing"); 1965 } 1966 $product[$field] = $data[$field]; 1967 } 1968 return $product; 1969 } 1970 1971 /** 1972 * Get properties of 1 product object. 1973 * Return an array with product information. 1974 * 1975 * @param int $id ID of product 1976 * @param string $ref Ref of element 1977 * @param string $ref_ext Ref ext of element 1978 * @param string $barcode Barcode of element 1979 * @param int $includestockdata Load also information about stock (slower) 1980 * @param bool $includesubproducts Load information about subproducts (if product is a virtual product) 1981 * @param bool $includeparentid Load also ID of parent product (if product is a variant of a parent product) 1982 * @param bool $includeifobjectisused Check if product object is used and set property 'is_object_used' with result. 1983 * @param bool $includetrans Load also the translations of product label and description 1984 * @return array|mixed Data without useless information 1985 * 1986 * @throws RestException 401 1987 * @throws RestException 403 1988 * @throws RestException 404 1989 */ 1990 private function _fetch($id, $ref = '', $ref_ext = '', $barcode = '', $includestockdata = 0, $includesubproducts = false, $includeparentid = false, $includeifobjectisused = false, $includetrans = false) 1991 { 1992 if (empty($id) && empty($ref) && empty($ref_ext) && empty($barcode)) { 1993 throw new RestException(400, 'bad value for parameter id, ref, ref_ext or barcode'); 1994 } 1995 1996 $id = (empty($id) ? 0 : $id); 1997 1998 if (!DolibarrApiAccess::$user->rights->produit->lire) { 1999 throw new RestException(403); 2000 } 2001 2002 $result = $this->product->fetch($id, $ref, $ref_ext, $barcode, 0, 0, ($includetrans ? 0 : 1)); 2003 if (!$result) { 2004 throw new RestException(404, 'Product not found'); 2005 } 2006 2007 if (!DolibarrApi::_checkAccessToResource('product', $this->product->id)) { 2008 throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); 2009 } 2010 2011 if ($includestockdata) { 2012 $this->product->load_stock(); 2013 2014 if (is_array($this->product->stock_warehouse)) { 2015 foreach ($this->product->stock_warehouse as $keytmp => $valtmp) { 2016 if (is_array($this->product->stock_warehouse[$keytmp]->detail_batch)) { 2017 foreach ($this->product->stock_warehouse[$keytmp]->detail_batch as $keytmp2 => $valtmp2) { 2018 unset($this->product->stock_warehouse[$keytmp]->detail_batch[$keytmp2]->db); 2019 } 2020 } 2021 } 2022 } 2023 } 2024 2025 if ($includesubproducts) { 2026 $childsArbo = $this->product->getChildsArbo($id, 1); 2027 2028 $keys = ['rowid', 'qty', 'fk_product_type', 'label', 'incdec']; 2029 $childs = []; 2030 foreach ($childsArbo as $values) { 2031 $childs[] = array_combine($keys, $values); 2032 } 2033 2034 $this->product->sousprods = $childs; 2035 } 2036 2037 if ($includeparentid) { 2038 $prodcomb = new ProductCombination($this->db); 2039 $this->product->fk_product_parent = null; 2040 if (($fk_product_parent = $prodcomb->fetchByFkProductChild($this->product->id)) > 0) { 2041 $this->product->fk_product_parent = $fk_product_parent; 2042 } 2043 } 2044 2045 if ($includeifobjectisused) { 2046 $this->product->is_object_used = ($this->product->isObjectUsed() > 0); 2047 } 2048 2049 return $this->_cleanObjectDatas($this->product); 2050 } 2051} 2052