Passed
Branch develop (302684)
by
unknown
118:20
created

ProductCombination   F

Complexity

Total Complexity 127

Size/Duplication

Total Lines 970
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 449
dl 0
loc 970
rs 2
c 0
b 0
f 0
wmc 127

17 Methods

Rating   Name   Duplication   Size   Complexity  
A fetch() 0 31 4
A __construct() 0 6 1
A countNbOfCombinationForFkProductParent() 0 14 3
A delete() 0 22 3
A create() 0 28 4
A update() 0 28 4
F updateProperties() 0 113 25
A deleteByFkProductParent() 0 25 6
A copyAll() 0 35 5
A getUniqueAttributesAndValuesByFkProductParent() 0 37 3
A fetchByProductCombination2ValuePairs() 0 33 6
F createProductCombination() 0 211 31
A fetchByFkProductChild() 0 31 5
A fetchAllByFkProductParent() 0 34 4
C fetchCombinationPriceLevels() 0 40 12
B saveCombinationPriceLevels() 0 36 7
A getCombinationLabel() 0 25 4

How to fix   Complexity   

Complex Class

Complex classes like ProductCombination often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ProductCombination, and based on these observations, apply Extract Interface, too.

1
<?php
2
/* Copyright (C) 2016	Marcos García	<[email protected]>
3
 * Copyright (C) 2018	Juanjo Menent	<[email protected]>
4
 * Copyright (C) 2022   Open-Dsi		<[email protected]>
5
 *
6
 * This program is free software; you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
/**
21
 * Class ProductCombination
22
 * Used to represent a product combination
23
 */
24
class ProductCombination
25
{
26
	/**
27
	 * Database handler
28
	 * @var DoliDB
29
	 */
30
	public $db;
31
32
	/**
33
	 * Rowid of combination
34
	 * @var int
35
	 */
36
	public $id;
37
38
	/**
39
	 * Rowid of parent product
40
	 * @var int
41
	 */
42
	public $fk_product_parent;
43
44
	/**
45
	 * Rowid of child product
46
	 * @var int
47
	 */
48
	public $fk_product_child;
49
50
	/**
51
	 * Price variation
52
	 * @var float
53
	 */
54
	public $variation_price;
55
56
	/**
57
	 * Is the price variation a relative variation? Can be an array if multiprice feature per level is enabled.
58
	 * @var bool|array
59
	 */
60
	public $variation_price_percentage = false;
61
62
	/**
63
	 * Weight variation
64
	 * @var float
65
	 */
66
	public $variation_weight;
67
68
	/**
69
	 * Combination entity
70
	 * @var int
71
	 */
72
	public $entity;
73
74
	/**
75
	 * Combination price level
76
	 * @var ProductCombinationLevel[]
77
	 */
78
	public $combination_price_levels;
79
80
	/**
81
	 * External ref
82
	 * @var string
83
	 */
84
	public $variation_ref_ext = '';
85
86
	/**
87
	 * Constructor
88
	 *
89
	 * @param   DoliDB $db     Database handler
90
	 */
91
	public function __construct(DoliDB $db)
92
	{
93
		global $conf;
94
95
		$this->db = $db;
96
		$this->entity = $conf->entity;
97
	}
98
99
	/**
100
	 * Retrieves a combination by its rowid
101
	 *
102
	 * @param 	int 	$rowid 		Row id
103
	 * @return 	int 				<0 KO, >0 OK
104
	 */
105
	public function fetch($rowid)
106
	{
107
		global $conf;
108
109
		$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').")";
110
111
		$query = $this->db->query($sql);
112
113
		if (!$query) {
114
			return -1;
115
		}
116
117
		if (!$this->db->num_rows($query)) {
118
			return -1;
119
		}
120
121
		$obj = $this->db->fetch_object($query);
122
123
		$this->id = $obj->rowid;
124
		$this->fk_product_parent = $obj->fk_product_parent;
125
		$this->fk_product_child = $obj->fk_product_child;
126
		$this->variation_price = $obj->variation_price;
127
		$this->variation_price_percentage = $obj->variation_price_percentage;
128
		$this->variation_weight = $obj->variation_weight;
129
		$this->variation_ref_ext = $obj->variation_ref_ext;
130
131
		if (!empty($conf->global->PRODUIT_MULTIPRICES)) {
132
			$this->fetchCombinationPriceLevels();
133
		}
134
135
		return 1;
136
	}
137
138
139
	/**
140
	 * Retrieves combination price levels
141
	 *
142
	 * @param 	int 	$fk_price_level The price level to fetch, use 0 for all
143
	 * @param 	bool 	$useCache 		To use cache or not
144
	 * @return 	int 					<0 KO, >0 OK
145
	 */
146
	public function fetchCombinationPriceLevels($fk_price_level = 0, $useCache = true)
147
	{
148
		global $conf;
149
150
		// Check cache
151
		if (!empty($this->combination_price_levels) && $useCache) {
152
			if ((!empty($fk_price_level) && isset($this->combination_price_levels[$fk_price_level])) || empty($fk_price_level)) {
153
				return 1;
154
			}
155
		}
156
157
		if (!is_array($this->combination_price_levels)
158
			|| empty($fk_price_level) // if fetch an unique level dont erase all already fetched
159
		) {
160
			$this->combination_price_levels = array();
161
		}
162
163
		$staticProductCombinationLevel = new ProductCombinationLevel($this->db);
164
		$combination_price_levels = $staticProductCombinationLevel->fetchAll($this->id, $fk_price_level);
165
166
		if (!is_array($combination_price_levels)) {
167
			return -1;
168
		}
169
170
		if (empty($combination_price_levels)) {
171
			/**
172
			 * for auto retrocompatibility with last behavior
173
			 */
174
			if ($fk_price_level > 0) {
175
				$combination_price_levels[$fk_price_level] = ProductCombinationLevel::createFromParent($this->db, $this, $fk_price_level);
176
			} else {
177
				for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) {
178
					$combination_price_levels[$i] = ProductCombinationLevel::createFromParent($this->db, $this, $i);
179
				}
180
			}
181
		}
182
183
		$this->combination_price_levels = $combination_price_levels;
184
185
		return 1;
186
	}
187
188
	/**
189
	 * Retrieves combination price levels
190
	 *
191
	 * @param 	int 	$clean 		Levels of PRODUIT_MULTIPRICES_LIMIT
192
	 * @return 	int 				<0 KO, >0 OK
193
	 */
194
	public function saveCombinationPriceLevels($clean = 1)
195
	{
196
		global $conf;
197
198
		$error = 0;
199
200
		$staticProductCombinationLevel = new ProductCombinationLevel($this->db);
201
202
		// Delete all
203
		if (empty($this->combination_price_levels)) {
204
			return $staticProductCombinationLevel->deleteAllForCombination($this->id);
205
		}
206
207
		// Clean not needed price levels (level higher than number max defined into setup)
208
		if ($clean) {
209
			$res = $staticProductCombinationLevel->clean($this->id);
210
			if ($res < 0) {
211
				$this->errors[] = 'Fail to clean not needed price levels';
212
				return -1;
213
			}
214
		}
215
216
		foreach ($this->combination_price_levels as $fk_price_level => $combination_price_level) {
217
			$res = $combination_price_level->save();
218
			if ($res < 1) {
219
				$this->error = 'Error saving combination price level '.$fk_price_level.' : '.$combination_price_level->error;
220
				$this->errors[] = $this->error;
221
				$error++;
222
				break;
223
			}
224
		}
225
226
		if ($error) {
227
			return $error * -1;
228
		} else {
229
			return 1;
230
		}
231
	}
232
233
	/**
234
	 * Retrieves information of a variant product and ID of its parent product.
235
	 *
236
	 * @param 	int 	$productid 				Product ID of variant
237
	 * @param	int		$donotloadpricelevel	Avoid loading price impact for each level. If PRODUIT_MULTIPRICES is not set, this has no effect.
238
	 * @return 	int 							<0 if KO, 0 if product ID is not ID of a variant product (so parent not found), >0 if OK (ID of parent)
239
	 */
240
	public function fetchByFkProductChild($productid, $donotloadpricelevel = 0)
241
	{
242
		global $conf;
243
244
		$sql = "SELECT rowid, fk_product_parent, fk_product_child, variation_price, variation_price_percentage, variation_weight";
245
		$sql .= " FROM ".MAIN_DB_PREFIX."product_attribute_combination WHERE fk_product_child = ".((int) $productid)." AND entity IN (".getEntity('product').")";
246
247
		$query = $this->db->query($sql);
248
249
		if (!$query) {
250
			return -1;
251
		}
252
253
		if (!$this->db->num_rows($query)) {
254
			return 0;
255
		}
256
257
		$result = $this->db->fetch_object($query);
258
259
		$this->id = $result->rowid;
260
		$this->fk_product_parent = $result->fk_product_parent;
261
		$this->fk_product_child = $result->fk_product_child;
262
		$this->variation_price = $result->variation_price;
263
		$this->variation_price_percentage = $result->variation_price_percentage;
264
		$this->variation_weight = $result->variation_weight;
265
266
		if (empty($donotloadpricelevel) && !empty($conf->global->PRODUIT_MULTIPRICES)) {
267
			$this->fetchCombinationPriceLevels();
268
		}
269
270
		return (int) $this->fk_product_parent;
271
	}
272
273
	/**
274
	 * Retrieves all product combinations by the product parent row id
275
	 *
276
	 * @param int $fk_product_parent Rowid of parent product
277
	 * @return int|ProductCombination[] <0 KO
278
	 */
279
	public function fetchAllByFkProductParent($fk_product_parent)
280
	{
281
		global $conf;
282
283
		$sql = "SELECT rowid, fk_product_parent, fk_product_child, variation_price, variation_price_percentage, variation_ref_ext, variation_weight";
284
		$sql.= " FROM ".MAIN_DB_PREFIX."product_attribute_combination";
285
		$sql.= " WHERE fk_product_parent = ".((int) $fk_product_parent)." AND entity IN (".getEntity('product').")";
286
287
		$query = $this->db->query($sql);
288
289
		if (!$query) {
290
			return -1;
291
		}
292
293
		$return = array();
294
295
		while ($result = $this->db->fetch_object($query)) {
296
			$tmp = new ProductCombination($this->db);
297
			$tmp->id = $result->rowid;
298
			$tmp->fk_product_parent = $result->fk_product_parent;
299
			$tmp->fk_product_child = $result->fk_product_child;
300
			$tmp->variation_price = $result->variation_price;
301
			$tmp->variation_price_percentage = $result->variation_price_percentage;
302
			$tmp->variation_weight = $result->variation_weight;
303
			$tmp->variation_ref_ext = $result->variation_ref_ext;
304
305
			if (!empty($conf->global->PRODUIT_MULTIPRICES)) {
306
				$tmp->fetchCombinationPriceLevels();
307
			}
308
309
			$return[] = $tmp;
310
		}
311
312
		return $return;
313
	}
314
315
	/**
316
	 * Retrieves all product combinations by the product parent row id
317
	 *
318
	 * @param  int     $fk_product_parent  Id of parent product
319
	 * @return int                         Nb of record
320
	 */
321
	public function countNbOfCombinationForFkProductParent($fk_product_parent)
322
	{
323
		$nb = 0;
324
		$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').")";
325
326
		$resql = $this->db->query($sql);
327
		if ($resql) {
328
			$obj = $this->db->fetch_object($resql);
329
			if ($obj) {
330
				$nb = $obj->nb;
331
			}
332
		}
333
334
		return $nb;
335
	}
336
337
	/**
338
	 * Creates a product attribute combination
339
	 *
340
	 * @param	User	$user	Object user
341
	 * @return 	int				<0 if KO, >0 if OK
342
	 */
343
	public function create($user)
344
	{
345
		global $conf;
346
347
		/* $this->fk_product_child may be empty and will be filled later after subproduct has been created */
348
349
		$sql = "INSERT INTO ".MAIN_DB_PREFIX."product_attribute_combination";
350
		$sql .= " (fk_product_parent, fk_product_child, variation_price, variation_price_percentage, variation_weight, variation_ref_ext, entity)";
351
		$sql .= " VALUES (".((int) $this->fk_product_parent).", ".((int) $this->fk_product_child).",";
352
		$sql .= (float) $this->variation_price.", ".(int) $this->variation_price_percentage.",";
353
		$sql .= (float) $this->variation_weight.", '".$this->db->escape($this->variation_ref_ext)."', ".(int) $this->entity.")";
354
355
		$resql = $this->db->query($sql);
356
		if ($resql) {
357
			$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.'product_attribute_combination');
358
		} else {
359
			$this->error = $this->db->lasterror();
360
			return -1;
361
		}
362
363
		if (!empty($conf->global->PRODUIT_MULTIPRICES)) {
364
			$res = $this->saveCombinationPriceLevels();
365
			if ($res < 0) {
366
				return -2;
367
			}
368
		}
369
370
		return 1;
371
	}
372
373
	/**
374
	 * Updates a product combination
375
	 *
376
	 * @param	User	$user		Object user
377
	 * @return 						int <0 KO, >0 OK
378
	 */
379
	public function update(User $user)
380
	{
381
		global $conf;
382
383
		$sql = "UPDATE ".MAIN_DB_PREFIX."product_attribute_combination";
384
		$sql .= " SET fk_product_parent = ".(int) $this->fk_product_parent.", fk_product_child = ".(int) $this->fk_product_child.",";
385
		$sql .= " variation_price = ".(float) $this->variation_price.", variation_price_percentage = ".(int) $this->variation_price_percentage.",";
386
		$sql .= " variation_ref_ext = '".$this->db->escape($this->variation_ref_ext)."',";
387
		$sql .= " variation_weight = ".(float) $this->variation_weight." WHERE rowid = ".((int) $this->id);
388
389
		$resql = $this->db->query($sql);
390
		if (!$resql) {
391
			return -1;
392
		}
393
394
		if (!empty($conf->global->PRODUIT_MULTIPRICES)) {
395
			$res = $this->saveCombinationPriceLevels();
396
			if ($res < 0) {
397
				return -2;
398
			}
399
		}
400
401
		$parent = new Product($this->db);
402
		$parent->fetch($this->fk_product_parent);
403
404
		$this->updateProperties($parent, $user);
405
406
		return 1;
407
	}
408
409
	/**
410
	 * Deletes a product combination
411
	 *
412
	 * @param 	User 	$user	Object user
413
	 * @return 	int 			<0 if KO, >0 if OK
414
	 */
415
	public function delete(User $user)
416
	{
417
		$this->db->begin();
418
419
		$comb2val = new ProductCombination2ValuePair($this->db);
420
		$comb2val->deleteByFkCombination($this->id);
421
422
		// remove combination price levels
423
		if (!$this->db->query("DELETE FROM ".MAIN_DB_PREFIX."product_attribute_combination_price_level WHERE fk_product_attribute_combination = ".(int) $this->id)) {
424
			$this->db->rollback();
425
			return -1;
426
		}
427
428
		$sql = "DELETE FROM ".MAIN_DB_PREFIX."product_attribute_combination WHERE rowid = ".(int) $this->id;
429
430
		if ($this->db->query($sql)) {
431
			$this->db->commit();
432
			return 1;
433
		}
434
435
		$this->db->rollback();
436
		return -1;
437
	}
438
439
	/**
440
	 * Deletes all product combinations of a parent product
441
	 *
442
	 * @param User		$user Object user
443
	 * @param int 		$fk_product_parent Rowid of parent product
444
	 * @return int <0 KO >0 OK
445
	 */
446
	public function deleteByFkProductParent($user, $fk_product_parent)
447
	{
448
		$this->db->begin();
449
450
		foreach ($this->fetchAllByFkProductParent($fk_product_parent) as $prodcomb) {
451
			$prodstatic = new Product($this->db);
452
453
			$res = $prodstatic->fetch($prodcomb->fk_product_child);
454
455
			if ($res > 0) {
456
				$res = $prodcomb->delete($user);
457
			}
458
459
			if ($res > 0 && !$prodstatic->isObjectUsed($prodstatic->id)) {
460
				$res = $prodstatic->delete($user);
461
			}
462
463
			if ($res < 0) {
464
				$this->db->rollback();
465
				return -1;
466
			}
467
		}
468
469
		$this->db->commit();
470
		return 1;
471
	}
472
473
	/**
474
	 * Updates the weight of the child product. The price must be updated using Product::updatePrices.
475
	 * This method is called by the update() of a product.
476
	 *
477
	 * @param	Product $parent 	Parent product
478
	 * @param	User	$user		Object user
479
	 * @return 	int 				>0 if OK, <0 if KO
480
	 */
481
	public function updateProperties(Product $parent, User $user)
482
	{
483
		global $conf;
484
485
		$this->db->begin();
486
487
		$child = new Product($this->db);
488
		$child->fetch($this->fk_product_child);
489
490
		$child->price_autogen = $parent->price_autogen;
491
		$child->weight = $parent->weight;
492
		// Only when Parent Status are updated
493
		if (!empty($parent->oldcopy) && ($parent->status != $parent->oldcopy->status)) {
494
			$child->status = $parent->status;
495
		}
496
		if (!empty($parent->oldcopy) && ($parent->status_buy != $parent->oldcopy->status_buy)) {
497
			$child->status_buy = $parent->status_buy;
498
		}
499
500
		if ($this->variation_weight) {	// If we must add a delta on weight
501
			$child->weight = ($child->weight ? $child->weight : 0) + $this->variation_weight;
502
		}
503
		$child->weight_units = $parent->weight_units;
504
505
		// Don't update the child label if the user has already modified it.
506
		if ($child->label == $parent->label) {
507
			// This will trigger only at variant creation time
508
			$varlabel               = $this->getCombinationLabel($this->fk_product_child);
509
			$child->label           = $parent->label.$varlabel;
510
		}
511
512
513
		if ($child->update($child->id, $user) > 0) {
514
			$new_vat = $parent->tva_tx;
515
			$new_npr = $parent->tva_npr;
516
517
			// MultiPrix
518
			if (!empty($conf->global->PRODUIT_MULTIPRICES)) {
519
				for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) {
520
					if ($parent->multiprices[$i] != '' || isset($this->combination_price_levels[$i]->variation_price)) {
521
						$new_type = empty($parent->multiprices_base_type[$i]) ? 'HT' : $parent->multiprices_base_type[$i];
522
						$new_min_price = $parent->multiprices_min[$i];
523
						$variation_price = floatval(!isset($this->combination_price_levels[$i]->variation_price) ? $this->variation_price : $this->combination_price_levels[$i]->variation_price);
524
						$variation_price_percentage = floatval(!isset($this->combination_price_levels[$i]->variation_price_percentage) ? $this->variation_price_percentage : $this->combination_price_levels[$i]->variation_price_percentage);
525
526
						if ($parent->prices_by_qty_list[$i]) {
527
							$new_psq = 1;
528
						} else {
529
							$new_psq = 0;
530
						}
531
532
						if ($new_type == 'TTC') {
533
							$new_price = $parent->multiprices_ttc[$i];
534
						} else {
535
							$new_price = $parent->multiprices[$i];
536
						}
537
538
						if ($variation_price_percentage) {
539
							if ($new_price != 0) {
540
								$new_price *= 1 + ($variation_price / 100);
541
							}
542
						} else {
543
							$new_price += $variation_price;
544
						}
545
546
						$ret = $child->updatePrice($new_price, $new_type, $user, $new_vat, $new_min_price, $i, $new_npr, $new_psq, 0, array(), $parent->default_vat_code);
547
548
						if ($ret < 0) {
549
							$this->db->rollback();
550
							$this->error = $child->error;
551
							$this->errors = $child->errors;
552
							return $ret;
553
						}
554
					}
555
				}
556
			} else {
557
				$new_type = $parent->price_base_type;
558
				$new_min_price = $parent->price_min;
559
				$new_psq = $parent->price_by_qty;
560
561
				if ($new_type == 'TTC') {
562
					$new_price = $parent->price_ttc;
563
				} else {
564
					$new_price = $parent->price;
565
				}
566
567
				if ($this->variation_price_percentage) {
568
					if ($new_price != 0) {
569
						$new_price *= 1 + ($this->variation_price / 100);
570
					}
571
				} else {
572
					$new_price += $this->variation_price;
573
				}
574
575
				$ret = $child->updatePrice($new_price, $new_type, $user, $new_vat, $new_min_price, 1, $new_npr, $new_psq);
576
577
				if ($ret < 0) {
578
					$this->db->rollback();
579
					$this->error = $child->error;
580
					$this->errors = $child->errors;
581
					return $ret;
582
				}
583
			}
584
585
			$this->db->commit();
586
587
			return 1;
588
		}
589
590
		$this->db->rollback();
591
		$this->error = $child->error;
592
		$this->errors = $child->errors;
593
		return -1;
594
	}
595
596
	/**
597
	 * Retrieves the combination that matches the given features.
598
	 *
599
	 * @param 	int 						$prodid 	Id of parent product
600
	 * @param 	array 						$features 	Format: [$attr] => $attr_val
601
	 * @return 	false|ProductCombination 				False if not found
602
	 */
603
	public function fetchByProductCombination2ValuePairs($prodid, array $features)
604
	{
605
		require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
606
607
		$actual_comp = array();
608
609
		$prodcomb2val = new ProductCombination2ValuePair($this->db);
610
		$prodcomb = new ProductCombination($this->db);
611
612
		$features = array_filter($features, function ($v) {
613
			return !empty($v);
614
		});
615
616
		foreach ($features as $attr => $attr_val) {
617
			$actual_comp[$attr] = $attr_val;
618
		}
619
620
		foreach ($prodcomb->fetchAllByFkProductParent($prodid) as $prc) {
621
			$values = array();
622
623
			foreach ($prodcomb2val->fetchByFkCombination($prc->id) as $value) {
624
				$values[$value->fk_prod_attr] = $value->fk_prod_attr_val;
625
			}
626
627
			$check1 = count(array_diff_assoc($values, $actual_comp));
628
			$check2 = count(array_diff_assoc($actual_comp, $values));
629
630
			if (!$check1 && !$check2) {
631
				return $prc;
632
			}
633
		}
634
635
		return false;
636
	}
637
638
	/**
639
	 * Retrieves all unique attributes for a parent product
640
	 *
641
	 * @param int $productid 			Product rowid
642
	 * @return ProductAttribute[]		Array of attributes
643
	 */
644
	public function getUniqueAttributesAndValuesByFkProductParent($productid)
645
	{
646
		require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttribute.class.php';
647
		require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttributeValue.class.php';
648
649
		$variants = array();
650
651
		//Attributes
652
		$sql = "SELECT DISTINCT fk_prod_attr, a.position";
653
		$sql .= " FROM ".MAIN_DB_PREFIX."product_attribute_combination2val c2v LEFT JOIN ".MAIN_DB_PREFIX."product_attribute_combination c ON c2v.fk_prod_combination = c.rowid";
654
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = c.fk_product_child";
655
		$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product_attribute a ON a.rowid = fk_prod_attr";
656
		$sql .= " WHERE c.fk_product_parent = ".((int) $productid)." AND p.tosell = 1";
657
		$sql .= $this->db->order('a.position', 'asc');
658
659
		$query = $this->db->query($sql);
660
661
		//Values
662
		while ($result = $this->db->fetch_object($query)) {
663
			$attr = new ProductAttribute($this->db);
664
			$attr->fetch($result->fk_prod_attr);
665
666
			$tmp = new stdClass();
667
			$tmp->id = $attr->id;
668
			$tmp->ref = $attr->ref;
669
			$tmp->label = $attr->label;
670
			$tmp->values = array();
671
672
			$attrval = new ProductAttributeValue($this->db);
673
			foreach ($res = $attrval->fetchAllByProductAttribute($attr->id, true) as $val) {
674
				$tmp->values[] = $val;
675
			}
676
677
			$variants[] = $tmp;
678
		}
679
680
		return $variants;
681
	}
682
683
	/**
684
	 * Creates a product combination. Check usages to find more about its use
685
	 * Format of $combinations array:
686
	 * array(
687
	 * 	0 => array(
688
	 * 		attr => value,
689
	 * 		attr2 => value
690
	 * 		[...]
691
	 * 		),
692
	 * [...]
693
	 * )
694
	 *
695
	 * @param User 			$user 				Object user
696
	 * @param Product 		$product 			Parent product
697
	 * @param array 		$combinations 		Attribute and value combinations.
698
	 * @param array 		$variations 		Price and weight variations
699
	 * @param bool|array 	$price_var_percent 	Is the price variation a relative variation?
700
	 * @param bool|float 	$forced_pricevar 	If the price variation is forced
701
	 * @param bool|float 	$forced_weightvar 	If the weight variation is forced
702
	 * @param bool|string 	$forced_refvar 		If the reference is forced
703
	 * @param string 	    $ref_ext            External reference
704
	 * @return int 								<0 KO, >0 OK
705
	 */
706
	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 = '')
707
	{
708
		global $conf;
709
710
		require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttribute.class.php';
711
		require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttributeValue.class.php';
712
713
		$this->db->begin();
714
715
		$price_impact = array(1=>0); // init level price impact
716
717
		$forced_refvar = trim($forced_refvar);
718
719
		if (!empty($forced_refvar) && $forced_refvar != $product->ref) {
720
			$existingProduct = new Product($this->db);
721
			$result = $existingProduct->fetch('', $forced_refvar);
722
			if ($result > 0) {
723
				$newproduct = $existingProduct;
724
			} else {
725
				$existingProduct = false;
726
				$newproduct = clone $product;
727
				$newproduct->ref = $forced_refvar;
728
			}
729
		} else {
730
			$forced_refvar = false;
731
			$existingProduct = false;
732
			$newproduct = clone $product;
733
		}
734
735
		//Final weight impact
736
		$weight_impact = (float) $forced_weightvar; // If false, return 0
737
738
		//Final price impact
739
		if (!is_array($forced_pricevar)) {
0 ignored issues
show
introduced by
The condition is_array($forced_pricevar) is always false.
Loading history...
740
			$price_impact[1] = (float) $forced_pricevar; // If false, return 0
741
		} else {
742
			$price_impact = $forced_pricevar;
743
		}
744
745
		if (!array($price_var_percent)) {
746
			$price_var_percent[1] = (float) $price_var_percent;
747
		}
748
749
		$newcomb = new ProductCombination($this->db);
750
		$existingCombination = $newcomb->fetchByProductCombination2ValuePairs($product->id, $combinations);
751
752
		if ($existingCombination) {
753
			$newcomb = $existingCombination;
754
		} else {
755
			$newcomb->fk_product_parent = $product->id;
756
757
			// Create 1 entry into product_attribute_combination (1 entry for each combinations). This init also $newcomb->id
758
			$result = $newcomb->create($user);
759
			if ($result < 0) {
760
				$this->error = $newcomb->error;
761
				$this->errors = $newcomb->errors;
762
				$this->db->rollback();
763
				return -1;
764
			}
765
		}
766
767
		$prodattr = new ProductAttribute($this->db);
768
		$prodattrval = new ProductAttributeValue($this->db);
769
770
		// $combination contains list of attributes pairs key->value. Example: array('id Color'=>id Blue, 'id Size'=>id Small, 'id Option'=>id val a, ...)
771
		//var_dump($combinations);
772
		foreach ($combinations as $currcombattr => $currcombval) {
773
			//This was checked earlier, so no need to double check
774
			$prodattr->fetch($currcombattr);
775
			$prodattrval->fetch($currcombval);
776
777
			//If there is an existing combination, there is no need to duplicate the valuepair
778
			if (!$existingCombination) {
779
				$tmp = new ProductCombination2ValuePair($this->db);
780
				$tmp->fk_prod_attr = $currcombattr;
781
				$tmp->fk_prod_attr_val = $currcombval;
782
				$tmp->fk_prod_combination = $newcomb->id;
783
784
				if ($tmp->create($user) < 0) {		// Create 1 entry into product_attribute_combination2val
785
					$this->error = $tmp->error;
0 ignored issues
show
Bug introduced by
The property error does not seem to exist on ProductCombination2ValuePair.
Loading history...
786
					$this->errors = $tmp->errors;
0 ignored issues
show
Bug introduced by
The property errors does not seem to exist on ProductCombination2ValuePair.
Loading history...
787
					$this->db->rollback();
788
					return -1;
789
				}
790
			}
791
792
			if ($forced_weightvar === false) {
793
				$weight_impact += (float) price2num($variations[$currcombattr][$currcombval]['weight']);
794
			}
795
			if ($forced_pricevar === false) {
796
				$price_impact[1] += (float) price2num($variations[$currcombattr][$currcombval]['price']);
797
798
				// Manage Price levels
799
				if ($conf->global->PRODUIT_MULTIPRICES) {
800
					for ($i = 2; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) {
801
						$price_impact[$i] += (float) price2num($variations[$currcombattr][$currcombval]['price']);
802
					}
803
				}
804
			}
805
806
			if ($forced_refvar === false) {
807
				if (isset($conf->global->PRODUIT_ATTRIBUTES_SEPARATOR)) {
808
					$newproduct->ref .= $conf->global->PRODUIT_ATTRIBUTES_SEPARATOR.$prodattrval->ref;
809
				} else {
810
					$newproduct->ref .= '_'.$prodattrval->ref;
811
				}
812
			}
813
814
			//The first one should not contain a linebreak
815
			if ($newproduct->description) {
816
				$newproduct->description .= '<br>';
817
			}
818
			$newproduct->description .= '<strong>'.$prodattr->label.':</strong> '.$prodattrval->value;
819
		}
820
821
		$newcomb->variation_price_percentage = $price_var_percent[1];
822
		$newcomb->variation_price = $price_impact[1];
823
		$newcomb->variation_weight = $weight_impact;
824
		$newcomb->variation_ref_ext = $this->db->escape($ref_ext);
825
826
		// Init price level
827
		if ($conf->global->PRODUIT_MULTIPRICES) {
828
			for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++) {
829
				$productCombinationLevel = new ProductCombinationLevel($this->db);
830
				$productCombinationLevel->fk_product_attribute_combination = $newcomb->id;
831
				$productCombinationLevel->fk_price_level = $i;
832
				$productCombinationLevel->variation_price = $price_impact[$i];
833
834
				if (is_array($price_var_percent)) {
835
					$productCombinationLevel->variation_price_percentage = (empty($price_var_percent[$i]) ? false : $price_var_percent[$i]);
836
				} else {
837
					$productCombinationLevel->variation_price_percentage = $price_var_percent;
838
				}
839
840
				$newcomb->combination_price_levels[$i] = $productCombinationLevel;
841
			}
842
		}
843
		//var_dump($newcomb->combination_price_levels);
844
845
		$newproduct->weight += $weight_impact;
846
847
		// Now create the product
848
		//print 'Create prod '.$newproduct->ref.'<br>'."\n";
849
		if ($existingProduct === false) {
850
			//To avoid wrong information in price history log
851
			$newproduct->price = 0;
852
			$newproduct->price_ttc = 0;
853
			$newproduct->price_min = 0;
854
			$newproduct->price_min_ttc = 0;
855
856
			// A new variant must use a new barcode (not same product)
857
			$newproduct->barcode = -1;
858
			$result = $newproduct->create($user);
859
860
			if ($result < 0) {
861
				//In case the error is not related with an already existing product
862
				if ($newproduct->error != 'ErrorProductAlreadyExists') {
863
					$this->error[] = $newproduct->error;
864
					$this->errors = $newproduct->errors;
865
					$this->db->rollback();
866
					return -1;
867
				}
868
869
				/**
870
				 * If there is an existing combination, then we update the prices and weight
871
				 * Otherwise, we try adding a random number to the ref
872
				 */
873
874
				if ($newcomb->fk_product_child) {
875
					$res = $newproduct->fetch($existingCombination->fk_product_child);
876
				} else {
877
					$orig_prod_ref = $newproduct->ref;
878
					$i = 1;
879
880
					do {
881
						$newproduct->ref = $orig_prod_ref.$i;
882
						$res = $newproduct->create($user);
883
884
						if ($newproduct->error != 'ErrorProductAlreadyExists') {
885
							$this->errors[] = $newproduct->error;
886
							break;
887
						}
888
889
						$i++;
890
					} while ($res < 0);
891
				}
892
893
				if ($res < 0) {
894
					$this->db->rollback();
895
					return -1;
896
				}
897
			}
898
		} else {
899
			$result = $newproduct->update($newproduct->id, $user);
900
			if ($result < 0) {
901
				$this->db->rollback();
902
				return -1;
903
			}
904
		}
905
906
		$newcomb->fk_product_child = $newproduct->id;
907
908
		if ($newcomb->update($user) < 0) {
909
			$this->error = $newcomb->error;
910
			$this->errors = $newcomb->errors;
911
			$this->db->rollback();
912
			return -1;
913
		}
914
915
		$this->db->commit();
916
		return $newproduct->id;
917
	}
918
919
	/**
920
	 * Copies all product combinations from the origin product to the destination product
921
	 *
922
	 * @param 	User 	$user	Object user
923
	 * @param   int     $origProductId  Origin product id
924
	 * @param   Product $destProduct    Destination product
925
	 * @return  int                     >0 OK <0 KO
926
	 */
927
	public function copyAll(User $user, $origProductId, Product $destProduct)
928
	{
929
		require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
930
931
		//To prevent a loop
932
		if ($origProductId == $destProduct->id) {
933
			return -1;
934
		}
935
936
		$prodcomb2val = new ProductCombination2ValuePair($this->db);
937
938
		//Retrieve all product combinations
939
		$combinations = $this->fetchAllByFkProductParent($origProductId);
940
941
		foreach ($combinations as $combination) {
942
			$variations = array();
943
944
			foreach ($prodcomb2val->fetchByFkCombination($combination->id) as $tmp_pc2v) {
945
				$variations[$tmp_pc2v->fk_prod_attr] = $tmp_pc2v->fk_prod_attr_val;
946
			}
947
948
			if ($this->createProductCombination(
949
				$user,
950
				$destProduct,
951
				$variations,
952
				array(),
953
				$combination->variation_price_percentage,
954
				$combination->variation_price,
955
				$combination->variation_weight
956
			) < 0) {
957
				return -1;
958
			}
959
		}
960
961
		return 1;
962
	}
963
964
	/**
965
	 * Return label for combinations
966
	 * @param   int 	$prod_child		id of child
967
	 * @return  string					combination label
968
	 */
969
	public function getCombinationLabel($prod_child)
970
	{
971
		$label = '';
972
		$sql = 'SELECT pav.value AS label';
973
		$sql .= ' FROM '.MAIN_DB_PREFIX.'product_attribute_combination pac';
974
		$sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'product_attribute_combination2val pac2v ON pac2v.fk_prod_combination=pac.rowid';
975
		$sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'product_attribute_value pav ON pav.rowid=pac2v.fk_prod_attr_val';
976
		$sql .= ' WHERE pac.fk_product_child='.((int) $prod_child);
977
978
		$resql = $this->db->query($sql);
979
		if ($resql) {
980
			$num = $this->db->num_rows($resql);
981
982
			$i = 0;
983
984
			while ($i < $num) {
985
				$obj = $this->db->fetch_object($resql);
986
987
				if ($obj->label) {
988
					$label .= ' '.$obj->label;
989
				}
990
				$i++;
991
			}
992
		}
993
		return $label;
994
	}
995
}
996
997
998
999
/**
1000
 * Class ProductCombinationLevel
1001
 * Used to represent a product combination Level
1002
 */
1003
class ProductCombinationLevel
1004
{
1005
	/**
1006
	 * Database handler
1007
	 * @var DoliDB
1008
	 */
1009
	public $db;
1010
1011
	/**
1012
	 * @var string Name of table without prefix where object is stored
1013
	 */
1014
	public $table_element = 'product_attribute_combination_price_level';
1015
1016
	/**
1017
	 * Rowid of combination
1018
	 * @var int
1019
	 */
1020
	public $id;
1021
1022
	/**
1023
	 * Rowid of parent product combination
1024
	 * @var int
1025
	 */
1026
	public $fk_product_attribute_combination;
1027
1028
	/**
1029
	 * Combination price level
1030
	 * @var int
1031
	 */
1032
	public $fk_price_level;
1033
1034
	/**
1035
	 * Price variation
1036
	 * @var float
1037
	 */
1038
	public $variation_price;
1039
1040
	/**
1041
	 * Is the price variation a relative variation?
1042
	 * @var bool
1043
	 */
1044
	public $variation_price_percentage = false;
1045
1046
	/**
1047
	 * Constructor
1048
	 *
1049
	 * @param DoliDB $db Database handler
1050
	 */
1051
	public function __construct(DoliDB $db)
1052
	{
1053
		$this->db = $db;
1054
	}
1055
1056
	/**
1057
	 * Retrieves a combination level by its rowid
1058
	 *
1059
	 * @param int $rowid Row id
1060
	 * @return int <0 KO, >0 OK
1061
	 */
1062
	public function fetch($rowid)
1063
	{
1064
		$sql = "SELECT rowid, fk_product_attribute_combination, fk_price_level, variation_price, variation_price_percentage";
1065
		$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element;
1066
		$sql .= " WHERE rowid = ".(int) $rowid;
1067
1068
		$resql = $this->db->query($sql);
1069
		if ($resql) {
1070
			$obj = $this->db->fetch_object($resql);
1071
			if ($obj) {
1072
				return $this->fetchFormObj($obj);
1073
			}
1074
		}
1075
1076
		return -1;
1077
	}
1078
1079
1080
	/**
1081
	 * Retrieves combination price levels
1082
	 *
1083
	 * @param 	int 	$fk_product_attribute_combination		Id of product combination
1084
	 * @param 	int 	$fk_price_level 						The price level to fetch, use 0 for all
1085
	 * @return  mixed											self[] | -1 on KO
1086
	 */
1087
	public function fetchAll($fk_product_attribute_combination, $fk_price_level = 0)
1088
	{
1089
		$result = array();
1090
1091
		$sql = "SELECT rowid, fk_product_attribute_combination, fk_price_level, variation_price, variation_price_percentage";
1092
		$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element;
1093
		$sql .= " WHERE fk_product_attribute_combination = ".intval($fk_product_attribute_combination);
1094
		if (!empty($fk_price_level)) {
1095
			$sql .= ' AND fk_price_level = '.intval($fk_price_level);
1096
		}
1097
1098
		$res = $this->db->query($sql);
1099
		if ($res) {
1100
			if ($this->db->num_rows($res) > 0) {
1101
				while ($obj = $this->db->fetch_object($res)) {
1102
					$productCombinationLevel = new ProductCombinationLevel($this->db);
1103
					$productCombinationLevel->fetchFormObj($obj);
1104
					$result[$obj->fk_price_level] = $productCombinationLevel;
1105
				}
1106
			}
1107
		} else {
1108
			return -1;
1109
		}
1110
1111
		return $result;
1112
	}
1113
1114
	/**
1115
	 * Assign vars form an stdclass like sql obj
1116
	 *
1117
	 * @param 	int 	$obj		Object resultset
1118
	 * @return 	int 				<0 KO, >0 OK
1119
	 */
1120
	public function fetchFormObj($obj)
1121
	{
1122
		if (!$obj) {
1123
			return -1;
1124
		}
1125
1126
		$this->id = $obj->rowid;
0 ignored issues
show
Bug introduced by
The property rowid does not exist on integer.
Loading history...
1127
		$this->fk_product_attribute_combination = floatval($obj->fk_product_attribute_combination);
0 ignored issues
show
Bug introduced by
The property fk_product_attribute_combination does not exist on integer.
Loading history...
1128
		$this->fk_price_level = intval($obj->fk_price_level);
0 ignored issues
show
Bug introduced by
The property fk_price_level does not exist on integer.
Loading history...
1129
		$this->variation_price = floatval($obj->variation_price);
0 ignored issues
show
Bug introduced by
The property variation_price does not exist on integer.
Loading history...
1130
		$this->variation_price_percentage = (bool) $obj->variation_price_percentage;
0 ignored issues
show
Bug introduced by
The property variation_price_percentage does not exist on integer.
Loading history...
1131
1132
		return 1;
1133
	}
1134
1135
1136
	/**
1137
	 * Save a price impact of a product combination for a price level
1138
	 *
1139
	 * @return int 		<0 KO, >0 OK
1140
	 */
1141
	public function save()
1142
	{
1143
		if (($this->id > 0 && empty($this->fk_product_attribute_combination)) || empty($this->fk_price_level)) {
1144
			return -1;
1145
		}
1146
1147
		// Check if level exist in DB before add
1148
		if ($this->fk_product_attribute_combination > 0 && empty($this->id)) {
1149
			$sql = "SELECT rowid id";
1150
			$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element;
1151
			$sql .= " WHERE fk_product_attribute_combination = ".(int) $this->fk_product_attribute_combination;
1152
			$sql .= ' AND fk_price_level = '.((int) $this->fk_price_level);
1153
1154
			$resql = $this->db->query($sql);
1155
			if ($resql) {
1156
				$obj = $this->db->fetch_object($resql);
1157
				if ($obj) {
1158
					$this->id = $obj->id;
1159
				}
1160
			}
1161
		}
1162
1163
		// Update
1164
		if (!empty($this->id)) {
1165
			$sql = 'UPDATE '.MAIN_DB_PREFIX.$this->table_element;
1166
			$sql .= ' SET variation_price = '.floatval($this->variation_price).' , variation_price_percentage = '.intval($this->variation_price_percentage);
1167
			$sql .= ' WHERE rowid = '.((int) $this->id);
1168
1169
			$res = $this->db->query($sql);
1170
			if ($res > 0) {
1171
				return $this->id;
1172
			} else {
1173
				$this->error = $this->db->error();
1174
				$this->errors[] = $this->error;
1175
				return -1;
1176
			}
1177
		} else {
1178
			// Add
1179
			$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
1180
			$sql .= "fk_product_attribute_combination, fk_price_level, variation_price, variation_price_percentage";
1181
			$sql .= ") VALUES (";
1182
			$sql .= (int) $this->fk_product_attribute_combination;
1183
			$sql .= ", ".intval($this->fk_price_level);
1184
			$sql .= ", ".floatval($this->variation_price);
1185
			$sql .= ", ".intval($this->variation_price_percentage);
1186
			$sql .= ")";
1187
1188
			$res = $this->db->query($sql);
1189
			if ($res) {
1190
				$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
1191
			} else {
1192
				$this->error = $this->db->error();
1193
				$this->errors[] = $this->error;
1194
				return -1;
1195
			}
1196
		}
1197
1198
		return $this->id;
1199
	}
1200
1201
1202
	/**
1203
	 * delete
1204
	 *
1205
	 * @return int <0 KO, >0 OK
1206
	 */
1207
	public function delete()
1208
	{
1209
		$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".(int) $this->id;
1210
		$res = $this->db->query($sql);
1211
1212
		return $res ? 1 : -1;
1213
	}
1214
1215
1216
	/**
1217
	 * delete all for a combination
1218
	 *
1219
	 * @param 	int		$fk_product_attribute_combination	Id of combination
1220
	 * @return 	int 										<0 KO, >0 OK
1221
	 */
1222
	public function deleteAllForCombination($fk_product_attribute_combination)
1223
	{
1224
		$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE fk_product_attribute_combination = ".(int) $fk_product_attribute_combination;
1225
		$res = $this->db->query($sql);
1226
1227
		return $res ? 1 : -1;
1228
	}
1229
1230
1231
	/**
1232
	 * Clean not needed price levels for a combination
1233
	 *
1234
	 * @param 	int		$fk_product_attribute_combination	Id of combination
1235
	 * @return 	int 										<0 KO, >0 OK
1236
	 */
1237
	public function clean($fk_product_attribute_combination)
1238
	{
1239
		global $conf;
1240
1241
		$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element;
1242
		$sql .= " WHERE fk_product_attribute_combination = ".(int) $fk_product_attribute_combination;
1243
		$sql .= " AND fk_price_level > ".intval($conf->global->PRODUIT_MULTIPRICES_LIMIT);
1244
		$res = $this->db->query($sql);
1245
1246
		return $res ? 1 : -1;
1247
	}
1248
1249
1250
	/**
1251
	 * Create new Product Combination Price level from Parent
1252
	 *
1253
	 * @param DoliDB 				$db						Database handler
1254
	 * @param ProductCombination 	$productCombination		Product combination
1255
	 * @param int					$fkPriceLevel			Price level
1256
	 * @return ProductCombinationLevel
1257
	 */
1258
	public static function createFromParent(DoliDB $db, ProductCombination $productCombination, $fkPriceLevel)
1259
	{
1260
		$productCombinationLevel = new self($db);
1261
		$productCombinationLevel->fk_price_level = $fkPriceLevel;
1262
		$productCombinationLevel->fk_product_attribute_combination = $productCombination->id;
1263
		$productCombinationLevel->variation_price = $productCombination->variation_price;
1264
		$productCombinationLevel->variation_price_percentage = (bool) $productCombination->variation_price_percentage;
1265
1266
		return $productCombinationLevel;
1267
	}
1268
}
1269