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