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