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

Products::getSubproducts()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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