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

Products::put()   F

Complexity

Conditions 43
Paths 9412

Size

Total Lines 136
Code Lines 86

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 43
eloc 86
nc 9412
nop 2
dl 0
loc 136
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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:

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