Passed
Push — EXTRACT_CLASSES ( ff35ec...a2ff75 )
by Rafael
48:13
created

Products::index()   F

Complexity

Conditions 27
Paths 15169

Size

Total Lines 120
Code Lines 72

Duplication

Lines 0
Ratio 0 %

Importance

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