Passed
Push — EXTRACT_CLASSES ( a2ff75...ae6b5c )
by Rafael
34:15
created

ProductCombination::createProductCombination()   F

Complexity

Conditions 31
Paths > 20000

Size

Total Lines 211
Code Lines 128

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 31
eloc 128
nc 52596
nop 9
dl 0
loc 211
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/* Copyright (C) 2016       Marcos García               <[email protected]>
4
 * Copyright (C) 2018		Juanjo Menent			    <[email protected]>
5
 * Copyright (C) 2022   	Open-Dsi				    <[email protected]>
6
 * Copyright (C) 2024		MDW						    <[email protected]>
7
 * Copyright (C) 2024       Frédéric France             <[email protected]>
8
 * Copyright (C) 2024       Rafael San José             <[email protected]>
9
 *
10
 * This program is free software; you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation; either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 */
23
24
namespace Dolibarr\Code\Variants\Classes;
25
26
use DoliDB;
27
28
/**
29
 *  \file       htdocs/variants/class/ProductCombination.class.php
30
 *  \ingroup    variants
31
 *  \brief      File of the ProductCombination class
32
 */
33
34
/**
35
 * Class ProductCombination
36
 * Used to represent the relation between a product and one of its variants.
37
 *
38
 * Example: a product "shirt" has two variants "shirt XL white" and "shirt XL grey".
39
 * This is represented with two ProductCombination objects:
40
 * - One for "shirt XL white":
41
 *     * $object->fk_product_parent     ID of the Product object "shirt"
42
 *     * $object->fk_product_child      ID of the Product object "shirt XL white"
43
 * - Another for "shirt XL grey":
44
 *     * $object->fk_product_parent     ID of the Product object "shirt"
45
 *     * $object->fk_product_child      ID of the Product object "shirt XL grey"
46
 */
47
class ProductCombination
48
{
49
    /**
50
     * Database handler
51
     * @var DoliDB
52
     */
53
    public $db;
54
55
    /**
56
     * Rowid of this ProductCombination
57
     * @var int
58
     */
59
    public $id;
60
61
    /**
62
     * Rowid of the parent Product
63
     * @var int
64
     */
65
    public $fk_product_parent;
66
67
    /**
68
     * Rowid of the variant Product
69
     * @var int
70
     */
71
    public $fk_product_child;
72
73
    /**
74
     * Price variation
75
     * @var float
76
     */
77
    public $variation_price;
78
79
    /**
80
     * Is the price variation a relative variation?
81
     * Can be an array if multiprice feature per level is enabled.
82
     * @var bool|array
83
     */
84
    public $variation_price_percentage = false;
85
86
    /**
87
     * Weight variation
88
     * @var float
89
     */
90
    public $variation_weight;
91
92
    /**
93
     * Combination entity
94
     * @var int
95
     */
96
    public $entity;
97
98
    /**
99
     * Combination price level
100
     * @var ProductCombinationLevel[]
101
     */
102
    public $combination_price_levels;
103
104
    /**
105
     * External ref
106
     * @var string
107
     */
108
    public $variation_ref_ext = '';
109
110
    /**
111
     * Error message
112
     * @var string
113
     */
114
    public $error;
115
116
    /**
117
     * Array of error messages
118
     * @var string[]
119
     */
120
    public $errors = array();
121
122
    /**
123
     * Constructor
124
     *
125
     * @param   DoliDB $db     Database handler
126
     */
127
    public function __construct(DoliDB $db)
128
    {
129
        global $conf;
130
131
        $this->db = $db;
132
        $this->entity = $conf->entity;
133
    }
134
135
    /**
136
     * Retrieves a ProductCombination by its rowid
137
     *
138
     * @param   int     $rowid      ID of the ProductCombination
139
     * @return  -1|1                -1 if KO, 1 if OK
0 ignored issues
show
Documentation Bug introduced by
The doc comment -1|1 at position 0 could not be parsed: Unknown type name '-1' at position 0 in -1|1.
Loading history...
140
     */
141
    public function fetch($rowid)
142
    {
143
        global $conf;
144
145
        $sql = "SELECT rowid, fk_product_parent, fk_product_child, variation_price, variation_price_percentage, variation_weight, variation_ref_ext FROM " . MAIN_DB_PREFIX . "product_attribute_combination WHERE rowid = " . ((int) $rowid) . " AND entity IN (" . getEntity('product') . ")";
146
147
        $query = $this->db->query($sql);
148
149
        if (!$query) {
150
            return -1;
151
        }
152
153
        if (!$this->db->num_rows($query)) {
154
            return -1;
155
        }
156
157
        $obj = $this->db->fetch_object($query);
158
159
        $this->id = $obj->rowid;
160
        $this->fk_product_parent = $obj->fk_product_parent;
161
        $this->fk_product_child = $obj->fk_product_child;
162
        $this->variation_price = $obj->variation_price;
163
        $this->variation_price_percentage = $obj->variation_price_percentage;
164
        $this->variation_weight = $obj->variation_weight;
165
        $this->variation_ref_ext = $obj->variation_ref_ext;
166
167
        if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
168
            $this->fetchCombinationPriceLevels();
169
        }
170
171
        return 1;
172
    }
173
174
175
    /**
176
     * Retrieves combination price levels
177
     *
178
     * @param   int     $fk_price_level The price level to fetch, use 0 for all
179
     * @param   bool    $useCache       To use cache or not
180
     * @return  -1|1                    -1 if KO, 1 if OK
0 ignored issues
show
Documentation Bug introduced by
The doc comment -1|1 at position 0 could not be parsed: Unknown type name '-1' at position 0 in -1|1.
Loading history...
181
     */
182
    public function fetchCombinationPriceLevels($fk_price_level = 0, $useCache = true)
183
    {
184
        global $conf;
185
186
        // Check cache
187
        if (!empty($this->combination_price_levels) && $useCache) {
188
            if ((!empty($fk_price_level) && isset($this->combination_price_levels[$fk_price_level])) || empty($fk_price_level)) {
189
                return 1;
190
            }
191
        }
192
193
        if (
194
            !is_array($this->combination_price_levels)
195
            || empty($fk_price_level) // if fetch an unique level don't erase all already fetched
196
        ) {
197
            $this->combination_price_levels = array();
198
        }
199
200
        $staticProductCombinationLevel = new ProductCombinationLevel($this->db);
201
        $combination_price_levels = $staticProductCombinationLevel->fetchAll($this->id, $fk_price_level);
202
203
        if (!is_array($combination_price_levels)) {
204
            return -1;
205
        }
206
207
        if (empty($combination_price_levels)) {
208
            /**
209
             * for auto retrocompatibility with last behavior
210
             */
211
            if ($fk_price_level > 0) {
212
                $combination_price_levels[$fk_price_level] = ProductCombinationLevel::createFromParent($this->db, $this, $fk_price_level);
213
            } else {
214
                $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
215
                for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
216
                    $combination_price_levels[$i] = ProductCombinationLevel::createFromParent($this->db, $this, $i);
217
                }
218
            }
219
        }
220
221
        $this->combination_price_levels = $combination_price_levels;
0 ignored issues
show
Documentation Bug introduced by
It seems like $combination_price_levels can also be of type integer. However, the property $combination_price_levels is declared as type Dolibarr\Code\Variants\C...oductCombinationLevel[]. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
222
223
        return 1;
224
    }
225
226
    /**
227
     * Retrieves combination price levels
228
     *
229
     * @param   int     $clean      Levels of PRODUIT_MULTIPRICES_LIMIT
230
     * @return  int                 Return integer <0 KO, >0 OK
231
     */
232
    public function saveCombinationPriceLevels($clean = 1)
233
    {
234
        global $conf;
235
236
        $error = 0;
237
238
        $staticProductCombinationLevel = new ProductCombinationLevel($this->db);
239
240
        // Delete all
241
        if (empty($this->combination_price_levels)) {
242
            return $staticProductCombinationLevel->deleteAllForCombination($this->id);
243
        }
244
245
        // Clean not needed price levels (level higher than number max defined into setup)
246
        if ($clean) {
247
            $res = $staticProductCombinationLevel->clean($this->id);
248
            if ($res < 0) {
249
                $this->errors[] = 'Fail to clean not needed price levels';
250
                return -1;
251
            }
252
        }
253
254
        foreach ($this->combination_price_levels as $fk_price_level => $combination_price_level) {
255
            $res = $combination_price_level->save();
256
            if ($res < 1) {
257
                $this->error = 'Error saving combination price level ' . $fk_price_level . ' : ' . $combination_price_level->error;
258
                $this->errors[] = $this->error;
259
                $error++;
260
                break;
261
            }
262
        }
263
264
        if ($error) {
265
            return $error * -1;
266
        } else {
267
            return 1;
268
        }
269
    }
270
271
    /**
272
     * Retrieves information of a variant product and ID of its parent product.
273
     *
274
     * @param   int     $productid              Product ID of variant
275
     * @param   int     $donotloadpricelevel    Avoid loading price impact for each level. If PRODUIT_MULTIPRICES is not set, this has no effect.
276
     * @return  int                             Return integer <0 if KO, 0 if product ID is not ID of a variant product (so parent not found), >0 if OK (ID of parent)
277
     */
278
    public function fetchByFkProductChild($productid, $donotloadpricelevel = 0)
279
    {
280
        global $conf;
281
282
        $sql = "SELECT rowid, fk_product_parent, fk_product_child, variation_price, variation_price_percentage, variation_weight";
283
        $sql .= " FROM " . MAIN_DB_PREFIX . "product_attribute_combination WHERE fk_product_child = " . ((int) $productid) . " AND entity IN (" . getEntity('product') . ")";
284
285
        $query = $this->db->query($sql);
286
287
        if (!$query) {
288
            return -1;
289
        }
290
291
        if (!$this->db->num_rows($query)) {
292
            return 0;
293
        }
294
295
        $result = $this->db->fetch_object($query);
296
297
        $this->id = $result->rowid;
298
        $this->fk_product_parent = $result->fk_product_parent;
299
        $this->fk_product_child = $result->fk_product_child;
300
        $this->variation_price = $result->variation_price;
301
        $this->variation_price_percentage = $result->variation_price_percentage;
302
        $this->variation_weight = $result->variation_weight;
303
304
        if (empty($donotloadpricelevel) && getDolGlobalString('PRODUIT_MULTIPRICES')) {
305
            $this->fetchCombinationPriceLevels();
306
        }
307
308
        return (int) $this->fk_product_parent;
309
    }
310
311
    /**
312
     * Retrieves all product combinations by the product parent row id
313
     *
314
     * @param   int                         $fk_product_parent  Rowid of parent product
315
     * @param   bool                        $sort_by_ref        Sort result by product child reference
316
     * @return  int|ProductCombination[]                        Return integer <0 KO
317
     */
318
    public function fetchAllByFkProductParent($fk_product_parent, $sort_by_ref = false)
319
    {
320
        global $conf;
321
322
        $sql = "SELECT pac.rowid, pac.fk_product_parent, pac.fk_product_child, pac.variation_price, pac.variation_price_percentage, pac.variation_ref_ext, pac.variation_weight";
323
        $sql .= " FROM " . MAIN_DB_PREFIX . "product_attribute_combination AS pac";
324
        if ($sort_by_ref) {
325
            $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "product AS p ON p.rowid = pac.fk_product_child";
326
        }
327
        $sql .= " WHERE pac.fk_product_parent = " . ((int) $fk_product_parent) . " AND pac.entity IN (" . getEntity('product') . ")";
328
        if ($sort_by_ref) {
329
            $sql .= $this->db->order('p.ref', 'ASC');
330
        }
331
332
        $query = $this->db->query($sql);
333
334
        if (!$query) {
335
            return -1;
336
        }
337
338
        $return = array();
339
340
        while ($result = $this->db->fetch_object($query)) {
341
            $tmp = new ProductCombination($this->db);
342
            $tmp->id = $result->rowid;
343
            $tmp->fk_product_parent = $result->fk_product_parent;
344
            $tmp->fk_product_child = $result->fk_product_child;
345
            $tmp->variation_price = $result->variation_price;
346
            $tmp->variation_price_percentage = $result->variation_price_percentage;
347
            $tmp->variation_weight = $result->variation_weight;
348
            $tmp->variation_ref_ext = $result->variation_ref_ext;
349
350
            if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
351
                $tmp->fetchCombinationPriceLevels();
352
            }
353
354
            $return[] = $tmp;
355
        }
356
357
        return $return;
358
    }
359
360
    /**
361
     * Retrieves all product combinations by the product parent row id
362
     *
363
     * @param  int     $fk_product_parent  Id of parent product
364
     * @return int                         Nb of record
365
     */
366
    public function countNbOfCombinationForFkProductParent($fk_product_parent)
367
    {
368
        $nb = 0;
369
        $sql = "SELECT count(rowid) as nb FROM " . MAIN_DB_PREFIX . "product_attribute_combination WHERE fk_product_parent = " . ((int) $fk_product_parent) . " AND entity IN (" . getEntity('product') . ")";
370
371
        $resql = $this->db->query($sql);
372
        if ($resql) {
373
            $obj = $this->db->fetch_object($resql);
374
            if ($obj) {
375
                $nb = $obj->nb;
376
            }
377
        }
378
379
        return $nb;
380
    }
381
382
    /**
383
     * Creates a product attribute combination
384
     *
385
     * @param   User    $user   Object user
386
     * @return  int             Return integer <0 if KO, >0 if OK
387
     */
388
    public function create($user)
389
    {
390
        global $conf;
391
392
        /* $this->fk_product_child may be empty and will be filled later after subproduct has been created */
393
394
        $sql = "INSERT INTO " . MAIN_DB_PREFIX . "product_attribute_combination";
395
        $sql .= " (fk_product_parent, fk_product_child, variation_price, variation_price_percentage, variation_weight, variation_ref_ext, entity)";
396
        $sql .= " VALUES (" . ((int) $this->fk_product_parent) . ", " . ((int) $this->fk_product_child) . ",";
397
        $sql .= (float) $this->variation_price . ", " . (int) $this->variation_price_percentage . ",";
398
        $sql .= (float) $this->variation_weight . ", '" . $this->db->escape($this->variation_ref_ext) . "', " . (int) $this->entity . ")";
399
400
        $resql = $this->db->query($sql);
401
        if ($resql) {
402
            $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX . 'product_attribute_combination');
403
        } else {
404
            $this->error = $this->db->lasterror();
405
            return -1;
406
        }
407
408
        if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
409
            $res = $this->saveCombinationPriceLevels();
410
            if ($res < 0) {
411
                return -2;
412
            }
413
        }
414
415
        return 1;
416
    }
417
418
    /**
419
     * Updates a product combination
420
     *
421
     * @param   User    $user       Object user
422
     * @return                      int Return integer <0 KO, >0 OK
423
     */
424
    public function update(User $user)
425
    {
426
        global $conf;
427
428
        $sql = "UPDATE " . MAIN_DB_PREFIX . "product_attribute_combination";
429
        $sql .= " SET fk_product_parent = " . (int) $this->fk_product_parent . ", fk_product_child = " . (int) $this->fk_product_child . ",";
430
        $sql .= " variation_price = " . (float) $this->variation_price . ", variation_price_percentage = " . (int) $this->variation_price_percentage . ",";
431
        $sql .= " variation_ref_ext = '" . $this->db->escape($this->variation_ref_ext) . "',";
432
        $sql .= " variation_weight = " . (float) $this->variation_weight . " WHERE rowid = " . ((int) $this->id);
433
434
        $resql = $this->db->query($sql);
435
        if (!$resql) {
436
            return -1;
437
        }
438
439
        if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
440
            $res = $this->saveCombinationPriceLevels();
441
            if ($res < 0) {
442
                return -2;
443
            }
444
        }
445
446
        $parent = new Product($this->db);
447
        $parent->fetch($this->fk_product_parent);
448
449
        $this->updateProperties($parent, $user);
450
451
        return 1;
452
    }
453
454
    /**
455
     * Deletes a product combination
456
     *
457
     * @param   User    $user   Object user
458
     * @return  int             Return integer <0 if KO, >0 if OK
459
     */
460
    public function delete(User $user)
461
    {
462
        $this->db->begin();
463
464
        $comb2val = new ProductCombination2ValuePair($this->db);
465
        $comb2val->deleteByFkCombination($this->id);
466
467
        // remove combination price levels
468
        if (!$this->db->query("DELETE FROM " . MAIN_DB_PREFIX . "product_attribute_combination_price_level WHERE fk_product_attribute_combination = " . (int) $this->id)) {
469
            $this->db->rollback();
470
            return -1;
471
        }
472
473
        $sql = "DELETE FROM " . MAIN_DB_PREFIX . "product_attribute_combination WHERE rowid = " . (int) $this->id;
474
475
        if ($this->db->query($sql)) {
476
            $this->db->commit();
477
            return 1;
478
        }
479
480
        $this->db->rollback();
481
        return -1;
482
    }
483
484
    /**
485
     * Deletes all product combinations of a parent product
486
     *
487
     * @param User      $user Object user
488
     * @param int       $fk_product_parent Rowid of parent product
489
     * @return int Return integer <0 KO >0 OK
490
     */
491
    public function deleteByFkProductParent($user, $fk_product_parent)
492
    {
493
        $this->db->begin();
494
495
        foreach ($this->fetchAllByFkProductParent($fk_product_parent) as $prodcomb) {
496
            $prodstatic = new Product($this->db);
497
498
            $res = $prodstatic->fetch($prodcomb->fk_product_child);
499
500
            if ($res > 0) {
501
                $res = $prodcomb->delete($user);
502
            }
503
504
            if ($res > 0 && !$prodstatic->isObjectUsed($prodstatic->id)) {
505
                $res = $prodstatic->delete($user);
506
            }
507
508
            if ($res < 0) {
509
                $this->db->rollback();
510
                return -1;
511
            }
512
        }
513
514
        $this->db->commit();
515
        return 1;
516
    }
517
518
    /**
519
     * Updates the weight of the child product. The price must be updated using Product::updatePrices.
520
     * This method is called by the update() of a product.
521
     *
522
     * @param   Product $parent     Parent product
523
     * @param   User    $user       Object user
524
     * @return  int                 >0 if OK, <0 if KO
525
     */
526
    public function updateProperties(Product $parent, User $user)
527
    {
528
        global $conf;
529
530
        $this->db->begin();
531
532
        $child = new Product($this->db);
533
        $child->fetch($this->fk_product_child);
534
535
        $child->price_autogen = $parent->price_autogen;
536
        $child->weight = $parent->weight;
537
        // Only when Parent Status are updated
538
        if (is_object($parent->oldcopy) && !$parent->oldcopy->isEmpty() && ($parent->status != $parent->oldcopy->status)) {
539
            $child->status = $parent->status;
540
        }
541
        if (is_object($parent->oldcopy) && !$parent->oldcopy->isEmpty() && ($parent->status_buy != $parent->oldcopy->status_buy)) {
542
            $child->status_buy = $parent->status_buy;
543
        }
544
545
        if ($this->variation_weight) {  // If we must add a delta on weight
546
            $child->weight = ($child->weight ? $child->weight : 0) + $this->variation_weight;
547
        }
548
        $child->weight_units = $parent->weight_units;
549
550
        // Don't update the child label if the user has already modified it.
551
        if ($child->label == $parent->label) {
552
            // This will trigger only at variant creation time
553
            $varlabel               = $this->getCombinationLabel($this->fk_product_child);
554
            $child->label           = $parent->label . $varlabel;
555
        }
556
557
558
        if ($child->update($child->id, $user) > 0) {
559
            $new_vat = $parent->tva_tx;
560
            $new_npr = $parent->tva_npr;
561
562
            // MultiPrix
563
            if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
564
                $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
565
                for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
566
                    if ($parent->multiprices[$i] != '' || isset($this->combination_price_levels[$i]->variation_price)) {
567
                        $new_type = empty($parent->multiprices_base_type[$i]) ? 'HT' : $parent->multiprices_base_type[$i];
568
                        $new_min_price = $parent->multiprices_min[$i];
569
                        $variation_price = (float) (!isset($this->combination_price_levels[$i]->variation_price) ? $this->variation_price : $this->combination_price_levels[$i]->variation_price);
570
                        $variation_price_percentage = (float) (!isset($this->combination_price_levels[$i]->variation_price_percentage) ? $this->variation_price_percentage : $this->combination_price_levels[$i]->variation_price_percentage);
571
572
                        if ($parent->prices_by_qty_list[$i]) {
573
                            $new_psq = 1;
574
                        } else {
575
                            $new_psq = 0;
576
                        }
577
578
                        if ($new_type == 'TTC') {
579
                            $new_price = $parent->multiprices_ttc[$i];
580
                        } else {
581
                            $new_price = $parent->multiprices[$i];
582
                        }
583
584
                        if ($variation_price_percentage) {
585
                            if ($new_price != 0) {
586
                                $new_price *= 1 + ($variation_price / 100);
587
                            }
588
                        } else {
589
                            $new_price += $variation_price;
590
                        }
591
592
                        $ret = $child->updatePrice($new_price, $new_type, $user, $new_vat, $new_min_price, $i, $new_npr, $new_psq, 0, array(), $parent->default_vat_code);
593
594
                        if ($ret < 0) {
595
                            $this->db->rollback();
596
                            $this->error = $child->error;
597
                            $this->errors = $child->errors;
598
                            return $ret;
599
                        }
600
                    }
601
                }
602
            } else {
603
                $new_type = $parent->price_base_type;
604
                $new_min_price = $parent->price_min;
605
                $new_psq = $parent->price_by_qty;
606
607
                if ($new_type == 'TTC') {
608
                    $new_price = $parent->price_ttc;
609
                } else {
610
                    $new_price = $parent->price;
611
                }
612
613
                if ($this->variation_price_percentage) {
614
                    if ($new_price != 0) {
615
                        $new_price *= 1 + ($this->variation_price / 100);
616
                    }
617
                } else {
618
                    $new_price += $this->variation_price;
619
                }
620
621
                $ret = $child->updatePrice($new_price, $new_type, $user, $new_vat, $new_min_price, 1, $new_npr, $new_psq);
622
623
                if ($ret < 0) {
624
                    $this->db->rollback();
625
                    $this->error = $child->error;
626
                    $this->errors = $child->errors;
627
                    return $ret;
628
                }
629
            }
630
631
            $this->db->commit();
632
633
            return 1;
634
        }
635
636
        $this->db->rollback();
637
        $this->error = $child->error;
638
        $this->errors = $child->errors;
639
        return -1;
640
    }
641
642
    /**
643
     * Retrieves the combination that matches the given features.
644
     *
645
     * @param   int                         $prodid     Id of parent product
646
     * @param   array<string,string>        $features   Format: [$attr] => $attr_val
647
     * @return  false|ProductCombination                False if not found
648
     */
649
    public function fetchByProductCombination2ValuePairs($prodid, array $features)
650
    {
651
        require_once constant('DOL_DOCUMENT_ROOT') . '/variants/class/ProductCombination2ValuePair.class.php';
652
653
        $actual_comp = array();
654
655
        $prodcomb2val = new ProductCombination2ValuePair($this->db);
656
        $prodcomb = new ProductCombination($this->db);
657
658
        $features = array_filter(
659
            $features,
660
            /**
661
             * @param mixed $v Feature information of a product.
662
             * @return bool
663
             */
664
            static function ($v) {
665
                return !empty($v);
666
            }
667
        );
668
669
        foreach ($features as $attr => $attr_val) {
670
            $actual_comp[$attr] = $attr_val;
671
        }
672
673
        foreach ($prodcomb->fetchAllByFkProductParent($prodid) as $prc) {
674
            $values = array();
675
676
            foreach ($prodcomb2val->fetchByFkCombination($prc->id) as $value) {
677
                $values[$value->fk_prod_attr] = $value->fk_prod_attr_val;
678
            }
679
680
            $check1 = count(array_diff_assoc($values, $actual_comp));
681
            $check2 = count(array_diff_assoc($actual_comp, $values));
682
683
            if (!$check1 && !$check2) {
684
                return $prc;
685
            }
686
        }
687
688
        return false;
689
    }
690
691
    /**
692
     * Retrieves all unique attributes for a parent product
693
     * (filtered on its 'to sell' variants)
694
     *
695
     * @param   int $productid          Parent Product rowid
696
     * @return  array<object{id:int,ref:string,label:string,values:ProductAttributeValue[]}>        Array of attributes
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{id:int,ref:...oductAttributeValue[]}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
697
     */
698
    public function getUniqueAttributesAndValuesByFkProductParent($productid)
699
    {
700
        require_once constant('DOL_DOCUMENT_ROOT') . '/variants/class/ProductAttribute.class.php';
701
        require_once constant('DOL_DOCUMENT_ROOT') . '/variants/class/ProductAttributeValue.class.php';
702
703
        // Attributes
704
        // Select all unique attributes of the variants (which are to sell) of a given parent product.
705
        $sql = "SELECT DISTINCT c2v.fk_prod_attr, a.position";
706
        $sql .= " FROM " . MAIN_DB_PREFIX . "product_attribute_combination2val c2v";
707
        $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "product_attribute_combination c";
708
        $sql .= "   ON c2v.fk_prod_combination = c.rowid";
709
        $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "product p";
710
        $sql .= "   ON p.rowid = c.fk_product_child";
711
        $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "product_attribute a";
712
        $sql .= "   ON a.rowid = fk_prod_attr";
713
        $sql .= " WHERE c.fk_product_parent = " . ((int) $productid);
714
        $sql .= " AND p.tosell = 1";
715
        $sql .= $this->db->order('a.position', 'asc');
716
717
        $resql = $this->db->query($sql);
718
719
        // Values
720
        $variants = array();
721
        while ($obj = $this->db->fetch_object($resql)) {
722
            $attr = new ProductAttribute($this->db);
723
            $attr->fetch($obj->fk_prod_attr);
724
725
            $tmp = new stdClass();
0 ignored issues
show
Bug introduced by
The type Dolibarr\Code\Variants\Classes\stdClass was not found. Did you mean stdClass? If so, make sure to prefix the type with \.
Loading history...
726
            $tmp->id = $attr->id;
727
            $tmp->ref = $attr->ref;
728
            $tmp->label = $attr->label;
729
            $tmp->values = array();
730
731
            $attrval = new ProductAttributeValue($this->db);
732
            // fetch only the used values of this attribute
733
            foreach ($attrval->fetchAllByProductAttribute($attr->id, true) as $val) {
734
                '@phan-var-force ProductAttributeValue $val';
735
                $tmp->values[] = $val;
736
            }
737
738
            $variants[] = $tmp;
739
        }
740
741
        return $variants;
742
    }
743
744
    /**
745
     * Creates a product combination. Check usages to find more about its use
746
     * Format of $combinations array:
747
     * array(
748
     *  0 => array(
749
     *      attr => value,
750
     *      attr2 => value
751
     *      [...]
752
     *      ),
753
     * [...]
754
     * )
755
     *
756
     * @param User                      $user                   User
757
     * @param Product                   $product                Parent Product
758
     * @param array<int,int>            $combinations           Attribute and value combinations.
759
     * @param array<int,array<int,array{weight:string|float,price:string|float}>> $variations   Price and weight variations (example: $variations[fk_product_attribute][fk_product_attribute_value]['weight'])
760
     * @param bool|bool[]               $price_var_percent      Is the price variation value a relative variation (in %)? (it is an array if global constant "PRODUIT_MULTIPRICES" is on)
761
     * @param false|float|float[]       $forced_pricevar        Value of the price variation if it is forced ; in currency or percent. (it is an array if global constant "PRODUIT_MULTIPRICES" is on)
762
     * @param false|float               $forced_weightvar       Value of the weight variation if it is forced
763
     * @param false|string              $forced_refvar          Value of the reference if it is forced
764
     * @param string                    $ref_ext                External reference
765
     * @return int<-1,1>                                        Return integer <0 KO, >0 OK
766
     */
767
    public function createProductCombination(User $user, Product $product, array $combinations, array $variations, $price_var_percent = false, $forced_pricevar = false, $forced_weightvar = false, $forced_refvar = false, $ref_ext = '')
768
    {
769
        global $conf;
770
771
        require_once constant('DOL_DOCUMENT_ROOT') . '/variants/class/ProductAttribute.class.php';
772
        require_once constant('DOL_DOCUMENT_ROOT') . '/variants/class/ProductAttributeValue.class.php';
773
774
        $this->db->begin();
775
776
        $price_impact = array(1 => 0); // init level price impact
777
778
        $forced_refvar = trim((string) $forced_refvar);
779
780
        if (!empty($forced_refvar) && $forced_refvar != $product->ref) {
781
            $existingProduct = new Product($this->db);
782
            $result = $existingProduct->fetch(0, $forced_refvar);
783
            if ($result > 0) {
784
                $newproduct = $existingProduct;
785
            } else {
786
                $existingProduct = false;
787
                $newproduct = clone $product;
788
                $newproduct->ref = $forced_refvar;
789
            }
790
        } else {
791
            $forced_refvar = false;
792
            $existingProduct = false;
793
            $newproduct = clone $product;
794
        }
795
796
        //Final weight impact
797
        $weight_impact = (float) $forced_weightvar; // If false, return 0
798
799
        //Final price impact
800
        if (!is_array($forced_pricevar)) {
801
            $price_impact[1] = (float) $forced_pricevar; // If false, return 0
802
        } else {
803
            $price_impact = $forced_pricevar;
804
        }
805
806
        if (!array($price_var_percent)) {
807
            $price_var_percent[1] = (float) $price_var_percent;
808
        }
809
810
        $newcomb = new ProductCombination($this->db);
811
        $existingCombination = $newcomb->fetchByProductCombination2ValuePairs($product->id, $combinations);
812
813
        if ($existingCombination) {
814
            $newcomb = $existingCombination;
815
        } else {
816
            $newcomb->fk_product_parent = $product->id;
817
818
            // Create 1 entry into product_attribute_combination (1 entry for each combinations). This init also $newcomb->id
819
            $result = $newcomb->create($user);
820
            if ($result < 0) {
821
                $this->error = $newcomb->error;
822
                $this->errors = $newcomb->errors;
823
                $this->db->rollback();
824
                return -1;
825
            }
826
        }
827
828
        $prodattr = new ProductAttribute($this->db);
829
        $prodattrval = new ProductAttributeValue($this->db);
830
831
        // $combination contains list of attributes pairs key->value. Example: array('id Color'=>id Blue, 'id Size'=>id Small, 'id Option'=>id val a, ...)
832
        foreach ($combinations as $currcombattr => $currcombval) {
833
            //This was checked earlier, so no need to double check
834
            $prodattr->fetch($currcombattr);
835
            $prodattrval->fetch($currcombval);
836
837
            //If there is an existing combination, there is no need to duplicate the valuepair
838
            if (!$existingCombination) {
839
                $tmp = new ProductCombination2ValuePair($this->db);
840
                $tmp->fk_prod_attr = $currcombattr;
841
                $tmp->fk_prod_attr_val = $currcombval;
842
                $tmp->fk_prod_combination = $newcomb->id;
843
844
                if ($tmp->create($user) < 0) {      // Create 1 entry into product_attribute_combination2val
845
                    $this->error = $tmp->error;
846
                    $this->errors = $tmp->errors;
847
                    $this->db->rollback();
848
                    return -1;
849
                }
850
            }
851
            if ($forced_weightvar === false) {
852
                $weight_impact += (float) price2num($variations[$currcombattr][$currcombval]['weight']);
853
            }
854
            if ($forced_pricevar === false) {
855
                $price_impact[1] += (float) price2num($variations[$currcombattr][$currcombval]['price']);
856
857
                // Manage Price levels
858
                if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
859
                    $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
860
                    for ($i = 2; $i <= $produit_multiprices_limit; $i++) {
861
                        $price_impact[$i] += (float) price2num($variations[$currcombattr][$currcombval]['price']);
862
                    }
863
                }
864
            }
865
866
            if ($forced_refvar === false) {
867
                if (isset($conf->global->PRODUIT_ATTRIBUTES_SEPARATOR)) {
868
                    $newproduct->ref .= getDolGlobalString('PRODUIT_ATTRIBUTES_SEPARATOR') . $prodattrval->ref;
869
                } else {
870
                    $newproduct->ref .= '_' . $prodattrval->ref;
871
                }
872
            }
873
874
            //The first one should not contain a linebreak
875
            if ($newproduct->description) {
876
                $newproduct->description .= '<br>';
877
            }
878
            $newproduct->description .= '<strong>' . $prodattr->label . ':</strong> ' . $prodattrval->value;
879
        }
880
881
        $newcomb->variation_price_percentage = $price_var_percent[1];
882
        $newcomb->variation_price = $price_impact[1];
883
        $newcomb->variation_weight = $weight_impact;
884
        $newcomb->variation_ref_ext = $this->db->escape($ref_ext);
885
886
        // Init price level
887
        if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
888
            $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
889
            for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
890
                $productCombinationLevel = new ProductCombinationLevel($this->db);
891
                $productCombinationLevel->fk_product_attribute_combination = $newcomb->id;
892
                $productCombinationLevel->fk_price_level = $i;
893
                $productCombinationLevel->variation_price = $price_impact[$i];
894
895
                if (is_array($price_var_percent)) {
896
                    $productCombinationLevel->variation_price_percentage = (empty($price_var_percent[$i]) ? false : $price_var_percent[$i]);
897
                } else {
898
                    $productCombinationLevel->variation_price_percentage = $price_var_percent;
899
                }
900
901
                $newcomb->combination_price_levels[$i] = $productCombinationLevel;
902
            }
903
        }
904
        //var_dump($newcomb->combination_price_levels);
905
906
        $newproduct->weight += $weight_impact;
907
908
        // Now create the product
909
        //print 'Create prod '.$newproduct->ref.'<br>'."\n";
910
        if ($existingProduct === false) {
911
            //To avoid wrong information in price history log
912
            $newproduct->price = 0;
913
            $newproduct->price_ttc = 0;
914
            $newproduct->price_min = 0;
915
            $newproduct->price_min_ttc = 0;
916
917
            // A new variant must use a new barcode (not same product)
918
            $newproduct->barcode = -1;
919
            $result = $newproduct->create($user);
920
921
            if ($result < 0) {
922
                //In case the error is not related with an already existing product
923
                if ($newproduct->error != 'ErrorProductAlreadyExists') {
924
                    $this->error = $newproduct->error;
925
                    $this->errors = $newproduct->errors;
926
                    $this->db->rollback();
927
                    return -1;
928
                }
929
930
                /**
931
                 * If there is an existing combination, then we update the prices and weight
932
                 * Otherwise, we try adding a random number to the ref
933
                 */
934
935
                if ($newcomb->fk_product_child) {
936
                    $res = $newproduct->fetch($existingCombination->fk_product_child);
937
                } else {
938
                    $orig_prod_ref = $newproduct->ref;
939
                    $i = 1;
940
941
                    do {
942
                        $newproduct->ref = $orig_prod_ref . $i;
943
                        $res = $newproduct->create($user);
944
945
                        if ($newproduct->error != 'ErrorProductAlreadyExists') {
946
                            $this->errors[] = $newproduct->error;
947
                            break;
948
                        }
949
950
                        $i++;
951
                    } while ($res < 0);
952
                }
953
954
                if ($res < 0) {
955
                    $this->db->rollback();
956
                    return -1;
957
                }
958
            }
959
        } else {
960
            $result = $newproduct->update($newproduct->id, $user);
961
            if ($result < 0) {
962
                $this->db->rollback();
963
                return -1;
964
            }
965
        }
966
967
        $newcomb->fk_product_child = $newproduct->id;
968
969
        if ($newcomb->update($user) < 0) {
970
            $this->error = $newcomb->error;
971
            $this->errors = $newcomb->errors;
972
            $this->db->rollback();
973
            return -1;
974
        }
975
976
        $this->db->commit();
977
        return $newproduct->id;
978
    }
979
980
    /**
981
     * Copies all product combinations from the origin product to the destination product
982
     *
983
     * @param   User    $user   Object user
984
     * @param   int     $origProductId  Origin product id
985
     * @param   Product $destProduct    Destination product
986
     * @return  int                     >0 OK <0 KO
987
     */
988
    public function copyAll(User $user, $origProductId, Product $destProduct)
989
    {
990
        require_once constant('DOL_DOCUMENT_ROOT') . '/variants/class/ProductCombination2ValuePair.class.php';
991
992
        //To prevent a loop
993
        if ($origProductId == $destProduct->id) {
994
            return -1;
995
        }
996
997
        $prodcomb2val = new ProductCombination2ValuePair($this->db);
998
999
        //Retrieve all product combinations
1000
        $combinations = $this->fetchAllByFkProductParent($origProductId);
1001
1002
        foreach ($combinations as $combination) {
1003
            $variations = array();
1004
1005
            foreach ($prodcomb2val->fetchByFkCombination($combination->id) as $tmp_pc2v) {
1006
                $variations[$tmp_pc2v->fk_prod_attr] = $tmp_pc2v->fk_prod_attr_val;
1007
            }
1008
1009
            if (
1010
                $this->createProductCombination(
1011
                $user,
1012
                $destProduct,
1013
                $variations,
1014
                array(),
1015
                $combination->variation_price_percentage,
1016
                $combination->variation_price,
1017
                $combination->variation_weight
1018
                ) < 0
1019
            ) {
1020
                return -1;
1021
            }
1022
        }
1023
1024
        return 1;
1025
    }
1026
1027
    /**
1028
     * Return label for combinations
1029
     * @param   int     $prod_child     id of child
1030
     * @return  string                  combination label
1031
     */
1032
    public function getCombinationLabel($prod_child)
1033
    {
1034
        $label = '';
1035
        $sql = 'SELECT pav.value AS label';
1036
        $sql .= ' FROM ' . MAIN_DB_PREFIX . 'product_attribute_combination pac';
1037
        $sql .= ' INNER JOIN ' . MAIN_DB_PREFIX . 'product_attribute_combination2val pac2v ON pac2v.fk_prod_combination=pac.rowid';
1038
        $sql .= ' INNER JOIN ' . MAIN_DB_PREFIX . 'product_attribute_value pav ON pav.rowid=pac2v.fk_prod_attr_val';
1039
        $sql .= ' WHERE pac.fk_product_child=' . ((int) $prod_child);
1040
1041
        $resql = $this->db->query($sql);
1042
        if ($resql) {
1043
            $num = $this->db->num_rows($resql);
1044
1045
            $i = 0;
1046
1047
            while ($i < $num) {
1048
                $obj = $this->db->fetch_object($resql);
1049
1050
                if ($obj->label) {
1051
                    $label .= ' ' . $obj->label;
1052
                }
1053
                $i++;
1054
            }
1055
        }
1056
        return $label;
1057
    }
1058
}
1059