Passed
Pull Request — dev (#8)
by Rafael
58:47
created

Products::getSupplierProducts()   F

Complexity

Conditions 20
Paths 4803

Size

Total Lines 88
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
eloc 53
nc 4803
nop 8
dl 0
loc 88
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/* Copyright (C) 2015       Jean-François Ferry         <[email protected]>
4
 * Copyright (C) 2019       Cedric Ancelin              <[email protected]>
5
 * Copyright (C) 2024       Rafael San José             <[email protected]>
6
 *
7
 * This program is free software; you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation; either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
 */
20
21
namespace Dolibarr\Code\Product\Api;
22
23
use Dolibarr\Code\Api\Classes\DolibarrApiAccess;
24
use Dolibarr\Code\Categories\Classes\Categorie;
25
use Dolibarr\Code\Fourn\Classes\Fournisseur;
26
use Dolibarr\Code\Fourn\Classes\ProductFournisseur;
27
use Dolibarr\Code\Product\Classes\Product;
28
use Dolibarr\Code\Product\Classes\ProductCustomerPrice;
29
use Dolibarr\Code\Variants\Classes\ProductCombination;
30
use Dolibarr\Core\Base\DolibarrApi;
31
use Luracast\Restler\RestException;
32
33
/**
34
 * API class for products
35
 *
36
 * @access protected
37
 * @class  DolibarrApiAccess {@requires user,external}
38
 */
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()
63
    {
64
        global $db, $conf;
65
66
        $this->db = $db;
67
        $this->product = new Product($this->db);
68
        $this->productsupplier = new ProductFournisseur($this->db);
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;
0 ignored issues
show
Bug Best Practice introduced by
The property fk_product_parent does not exist on Dolibarr\Code\Product\Classes\Product. Since you implemented __set, consider adding a @property annotation.
Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $combinations also could return the type integer which is incompatible with the documented return type array.
Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $combinations also could return the type integer which is incompatible with the documented return type array.
Loading history...
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)
2086
    {
2087
        if (!DolibarrApiAccess::$user->hasRight('produit', 'creer')) {
2088
            throw new RestException(403);
2089
        }
2090
2091
        $prodcomb = new ProductCombination($this->db);
2092
        $prodcomb->fetch((int)$id);
2093
2094
        foreach ($request_data as $field => $value) {
2095
            if ($field == 'rowid') {
2096
                continue;
2097
            }
2098
            if ($field === 'caller') {
2099
                // 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
2100
                $prodcomb->context['caller'] = sanitizeVal($request_data['caller'], 'aZ09');
2101
                continue;
2102
            }
2103
2104
            $prodcomb->$field = $this->_checkValForAPI($field, $value, $prodcomb);
2105
        }
2106
2107
        $result = $prodcomb->update(DolibarrApiAccess::$user);
2108
        if ($result > 0) {
2109
            return 1;
2110
        }
2111
        throw new RestException(500, "Error editing variant");
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)
2155
    {
2156
        if (!DolibarrApiAccess::$user->hasRight('produit', 'lire') || !DolibarrApiAccess::$user->hasRight('stock', 'lire')) {
2157
            throw new RestException(403);
2158
        }
2159
2160
        if (!DolibarrApi::_checkAccessToResource('product', $id)) {
2161
            throw new RestException(403, 'Access not allowed for login ' . DolibarrApiAccess::$user->login);
2162
        }
2163
2164
        $product_model = new Product($this->db);
2165
        $product_model->fetch($id);
2166
        $product_model->load_stock();
2167
2168
        $stockData = $this->_cleanObjectDatas($product_model)->stock_warehouse;
2169
        if ($selected_warehouse_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $selected_warehouse_id of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2170
            foreach ($stockData as $warehouse_id => $warehouse) {
2171
                if ($warehouse_id != $selected_warehouse_id) {
2172
                    unset($stockData[$warehouse_id]);
2173
                }
2174
            }
2175
        }
2176
2177
        return array('stock_warehouses' => $stockData);
2178
    }
2179
}
2180