dolibarr 21.0.3
ProductCombination.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2016 Marcos García <marcosgdf@gmail.com>
3 * Copyright (C) 2018 Juanjo Menent <jmenent@2byte.es>
4 * Copyright (C) 2022 Open-Dsi <support@open-dsi.fr>
5 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
6 * Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
7 * Copyright (C) 2025 William Mead <william@m34d.com>
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 */
22
43{
48 public $db;
49
54 public $id;
55
60 public $fk_product_parent;
61
66 public $fk_product_child;
67
72 public $variation_price;
73
79 public $variation_price_percentage = false;
80
85 public $variation_weight;
86
91 public $entity;
92
97 public $combination_price_levels;
98
103 public $variation_ref_ext = '';
104
109 public $error;
110
115 public $errors = array();
116
122 public function __construct(DoliDB $db)
123 {
124 global $conf;
125
126 $this->db = $db;
127 $this->entity = $conf->entity;
128 }
129
136 public function fetch($rowid)
137 {
138 $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').")";
139
140 $query = $this->db->query($sql);
141
142 if (!$query) {
143 return -1;
144 }
145
146 if (!$this->db->num_rows($query)) {
147 return -1;
148 }
149
150 $obj = $this->db->fetch_object($query);
151
152 $this->id = $obj->rowid;
153 $this->fk_product_parent = $obj->fk_product_parent;
154 $this->fk_product_child = $obj->fk_product_child;
155 $this->variation_price = $obj->variation_price;
156 $this->variation_price_percentage = $obj->variation_price_percentage;
157 $this->variation_weight = $obj->variation_weight;
158 $this->variation_ref_ext = $obj->variation_ref_ext;
159
160 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
162 }
163
164 return 1;
165 }
166
167
175 public function fetchCombinationPriceLevels($fk_price_level = 0, $useCache = true)
176 {
177 // Check cache
178 if (!empty($this->combination_price_levels) && $useCache) {
179 if ((!empty($fk_price_level) && isset($this->combination_price_levels[$fk_price_level])) || empty($fk_price_level)) {
180 return 1;
181 }
182 }
183
184 if (!is_array($this->combination_price_levels)
185 || empty($fk_price_level) // if fetch an unique level don't erase all already fetched
186 ) {
187 $this->combination_price_levels = array();
188 }
189
190 $staticProductCombinationLevel = new ProductCombinationLevel($this->db);
191 $combination_price_levels = $staticProductCombinationLevel->fetchAll($this->id, $fk_price_level);
192
193 if (!is_array($combination_price_levels)) {
194 return -1;
195 }
196
197 if (empty($combination_price_levels)) {
201 if ($fk_price_level > 0) {
202 $combination_price_levels[$fk_price_level] = ProductCombinationLevel::createFromParent($this->db, $this, $fk_price_level);
203 } else {
204 $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
205 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
206 $combination_price_levels[$i] = ProductCombinationLevel::createFromParent($this->db, $this, $i);
207 }
208 }
209 }
210
211 $this->combination_price_levels = $combination_price_levels;
212
213 return 1;
214 }
215
222 public function saveCombinationPriceLevels($clean = 1)
223 {
224 global $conf;
225
226 $error = 0;
227
228 $staticProductCombinationLevel = new ProductCombinationLevel($this->db);
229
230 // Delete all
231 if (empty($this->combination_price_levels)) {
232 return $staticProductCombinationLevel->deleteAllForCombination($this->id);
233 }
234
235 // Clean not needed price levels (level higher than number max defined into setup)
236 if ($clean) {
237 $res = $staticProductCombinationLevel->clean($this->id);
238 if ($res < 0) {
239 $this->errors[] = 'Fail to clean not needed price levels';
240 return -1;
241 }
242 }
243
244 foreach ($this->combination_price_levels as $fk_price_level => $combination_price_level) {
245 $res = $combination_price_level->save();
246 if ($res < 1) {
247 $this->error = 'Error saving combination price level '.$fk_price_level.' : '.$combination_price_level->error;
248 $this->errors[] = $this->error;
249 $error++;
250 break;
251 }
252 }
253
254 if ($error) {
255 return $error * -1;
256 } else {
257 return 1;
258 }
259 }
260
268 public function fetchByFkProductChild($productid, $donotloadpricelevel = 0)
269 {
270 global $conf;
271
272 $sql = "SELECT rowid, fk_product_parent, fk_product_child, variation_price, variation_price_percentage, variation_weight";
273 $sql .= " FROM ".MAIN_DB_PREFIX."product_attribute_combination WHERE fk_product_child = ".((int) $productid)." AND entity IN (".getEntity('product').")";
274
275 $query = $this->db->query($sql);
276
277 if (!$query) {
278 return -1;
279 }
280
281 if (!$this->db->num_rows($query)) {
282 return 0;
283 }
284
285 $result = $this->db->fetch_object($query);
286
287 $this->id = $result->rowid;
288 $this->fk_product_parent = $result->fk_product_parent;
289 $this->fk_product_child = $result->fk_product_child;
290 $this->variation_price = $result->variation_price;
291 $this->variation_price_percentage = $result->variation_price_percentage;
292 $this->variation_weight = $result->variation_weight;
293
294 if (empty($donotloadpricelevel) && getDolGlobalString('PRODUIT_MULTIPRICES')) {
296 }
297
298 return (int) $this->fk_product_parent;
299 }
300
308 public function fetchAllByFkProductParent($fk_product_parent, $sort_by_ref = false)
309 {
310 global $conf;
311
312 $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";
313 $sql .= " FROM ".MAIN_DB_PREFIX."product_attribute_combination AS pac";
314 if ($sort_by_ref) {
315 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product AS p ON p.rowid = pac.fk_product_child";
316 }
317 $sql .= " WHERE pac.fk_product_parent = ".((int) $fk_product_parent)." AND pac.entity IN (".getEntity('product').")";
318 if ($sort_by_ref) {
319 $sql .= $this->db->order('p.ref', 'ASC');
320 }
321
322 $query = $this->db->query($sql);
323
324 if (!$query) {
325 return -1;
326 }
327
328 $return = array();
329
330 while ($result = $this->db->fetch_object($query)) {
331 $tmp = new ProductCombination($this->db);
332 $tmp->id = $result->rowid;
333 $tmp->fk_product_parent = $result->fk_product_parent;
334 $tmp->fk_product_child = $result->fk_product_child;
335 $tmp->variation_price = $result->variation_price;
336 $tmp->variation_price_percentage = $result->variation_price_percentage;
337 $tmp->variation_weight = $result->variation_weight;
338 $tmp->variation_ref_ext = $result->variation_ref_ext;
339
340 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
341 $tmp->fetchCombinationPriceLevels();
342 }
343
344 $return[] = $tmp;
345 }
346
347 return $return;
348 }
349
356 public function countNbOfCombinationForFkProductParent($fk_product_parent)
357 {
358 $nb = 0;
359 $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').")";
360
361 $resql = $this->db->query($sql);
362 if ($resql) {
363 $obj = $this->db->fetch_object($resql);
364 if ($obj) {
365 $nb = $obj->nb;
366 }
367 }
368
369 return $nb;
370 }
371
378 public function create($user)
379 {
380 global $conf;
381
382 /* $this->fk_product_child may be empty and will be filled later after subproduct has been created */
383
384 $sql = "INSERT INTO ".MAIN_DB_PREFIX."product_attribute_combination";
385 $sql .= " (fk_product_parent, fk_product_child, variation_price, variation_price_percentage, variation_weight, variation_ref_ext, entity)";
386 $sql .= " VALUES (".((int) $this->fk_product_parent).", ".((int) $this->fk_product_child).",";
387 $sql .= (float) $this->variation_price.", ".(int) $this->variation_price_percentage.",";
388 $sql .= (float) $this->variation_weight.", '".$this->db->escape($this->variation_ref_ext)."', ".(int) $this->entity.")";
389
390 $resql = $this->db->query($sql);
391 if ($resql) {
392 $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.'product_attribute_combination');
393 } else {
394 $this->error = $this->db->lasterror();
395 return -1;
396 }
397
398 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
399 $res = $this->saveCombinationPriceLevels();
400 if ($res < 0) {
401 return -2;
402 }
403 }
404
405 return 1;
406 }
407
414 public function update(User $user)
415 {
416 global $conf;
417
418 $sql = "UPDATE ".MAIN_DB_PREFIX."product_attribute_combination";
419 $sql .= " SET fk_product_parent = ".(int) $this->fk_product_parent.", fk_product_child = ".(int) $this->fk_product_child.",";
420 $sql .= " variation_price = ".(float) $this->variation_price.", variation_price_percentage = ".(int) $this->variation_price_percentage.",";
421 $sql .= " variation_ref_ext = '".$this->db->escape($this->variation_ref_ext)."',";
422 $sql .= " variation_weight = ".(float) $this->variation_weight." WHERE rowid = ".((int) $this->id);
423
424 $resql = $this->db->query($sql);
425 if (!$resql) {
426 return -1;
427 }
428
429 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
430 $res = $this->saveCombinationPriceLevels();
431 if ($res < 0) {
432 return -2;
433 }
434 }
435
436 $parent = new Product($this->db);
437 $parent->fetch($this->fk_product_parent);
438
439 $this->updateProperties($parent, $user);
440
441 return 1;
442 }
443
450 public function delete(User $user)
451 {
452 $this->db->begin();
453
454 $comb2val = new ProductCombination2ValuePair($this->db);
455 $comb2val->deleteByFkCombination($this->id);
456
457 // remove combination price levels
458 if (!$this->db->query("DELETE FROM ".MAIN_DB_PREFIX."product_attribute_combination_price_level WHERE fk_product_attribute_combination = ".(int) $this->id)) {
459 $this->db->rollback();
460 return -1;
461 }
462
463 $sql = "DELETE FROM ".MAIN_DB_PREFIX."product_attribute_combination WHERE rowid = ".(int) $this->id;
464
465 if ($this->db->query($sql)) {
466 $this->db->commit();
467 return 1;
468 }
469
470 $this->db->rollback();
471 return -1;
472 }
473
481 public function deleteByFkProductParent($user, $fk_product_parent)
482 {
483 $combinations = $this->fetchAllByFkProductParent($fk_product_parent);
484
485 if (!is_array($combinations)) { // No combinations found, return success
486 return 1;
487 }
488
489 $this->db->begin();
490
491 foreach ($combinations as $prodcomb) {
492 $prodstatic = new Product($this->db);
493
494 $res = $prodstatic->fetch($prodcomb->fk_product_child);
495
496 if ($res > 0) {
497 $res = $prodcomb->delete($user);
498 }
499
500 if ($res > 0 && !$prodstatic->isObjectUsed($prodstatic->id)) {
501 $res = $prodstatic->delete($user);
502 }
503
504 if ($res < 0) {
505 $this->db->rollback();
506 return -1;
507 }
508 }
509
510 $this->db->commit();
511 return 1;
512 }
513
522 public function updateProperties(Product $parent, User $user)
523 {
524 global $conf;
525
526 $this->db->begin();
527
528 $child = new Product($this->db);
529 $child->fetch($this->fk_product_child);
530
531 $child->price_autogen = $parent->price_autogen;
532 $child->weight = $parent->weight;
533 // Only when Parent Status are updated
534 if (is_object($parent->oldcopy) && !$parent->oldcopy->isEmpty() && ($parent->status != $parent->oldcopy->status)) {
535 $child->status = $parent->status;
536 }
537 if (is_object($parent->oldcopy) && !$parent->oldcopy->isEmpty() && ($parent->status_buy != $parent->oldcopy->status_buy)) {
538 $child->status_buy = $parent->status_buy;
539 }
540
541 if ($this->variation_weight) { // If we must add a delta on weight
542 $child->weight = ($child->weight ? $child->weight : 0) + $this->variation_weight;
543 }
544 $child->weight_units = $parent->weight_units;
545
546 // Don't update the child label if the user has already modified it.
547 if ($child->label == $parent->label) {
548 // This will trigger only at variant creation time
549 $varlabel = $this->getCombinationLabel($this->fk_product_child);
550 $child->label = $parent->label.$varlabel;
551 }
552
553
554 if ($child->update($child->id, $user) > 0) {
555 $new_vat = $parent->tva_tx;
556 $new_npr = $parent->tva_npr;
557
558 // MultiPrix
559 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
560 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
561 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
562 if ($parent->multiprices[$i] != '' || isset($this->combination_price_levels[$i]->variation_price)) {
563 $new_type = empty($parent->multiprices_base_type[$i]) ? 'HT' : $parent->multiprices_base_type[$i];
564 $new_min_price = $parent->multiprices_min[$i];
565 $variation_price = (float) (!isset($this->combination_price_levels[$i]->variation_price) ? $this->variation_price : $this->combination_price_levels[$i]->variation_price);
566 $variation_price_percentage = (bool) (!isset($this->combination_price_levels[$i]->variation_price_percentage) ? $this->variation_price_percentage : $this->combination_price_levels[$i]->variation_price_percentage);
567
568 if ($parent->prices_by_qty_list[$i]) {
569 $new_psq = 1;
570 } else {
571 $new_psq = 0;
572 }
573
574 if ($new_type == 'TTC') {
575 $new_price = $parent->multiprices_ttc[$i];
576 } else {
577 $new_price = $parent->multiprices[$i];
578 }
579
580 if ($variation_price_percentage) {
581 if ($new_price != 0) {
582 $new_price *= 1 + ($variation_price / 100);
583 }
584 } else {
585 $new_price += $variation_price;
586 }
587
588 $ret = $child->updatePrice($new_price, $new_type, $user, $new_vat, $new_min_price, $i, $new_npr, $new_psq, 0, array(), $parent->default_vat_code);
589
590 if ($ret < 0) {
591 $this->db->rollback();
592 $this->error = $child->error;
593 $this->errors = $child->errors;
594 return $ret;
595 }
596 }
597 }
598 } else {
599 $new_type = $parent->price_base_type;
600 $new_min_price = $parent->price_min;
601 $new_psq = $parent->price_by_qty;
602
603 if ($new_type == 'TTC') {
604 $new_price = $parent->price_ttc;
605 } else {
606 $new_price = $parent->price;
607 }
608
609 if ($this->variation_price_percentage) {
610 if ($new_price != 0) {
611 $new_price *= 1 + ($this->variation_price / 100);
612 }
613 } else {
614 $new_price += $this->variation_price;
615 }
616
617 $ret = $child->updatePrice($new_price, $new_type, $user, $new_vat, $new_min_price, 1, $new_npr, $new_psq);
618
619 if ($ret < 0) {
620 $this->db->rollback();
621 $this->error = $child->error;
622 $this->errors = $child->errors;
623 return $ret;
624 }
625 }
626
627 $this->db->commit();
628
629 return 1;
630 }
631
632 $this->db->rollback();
633 $this->error = $child->error;
634 $this->errors = $child->errors;
635 return -1;
636 }
637
645 public function fetchByProductCombination2ValuePairs($prodid, array $features)
646 {
647 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
648
649 $actual_comp = array();
650
651 $prodcomb2val = new ProductCombination2ValuePair($this->db);
652 $prodcomb = new ProductCombination($this->db);
653
654 $features = array_filter(
655 $features,
660 static function ($v) {
661 return !empty($v);
662 }
663 );
664
665 foreach ($features as $attr => $attr_val) {
666 $actual_comp[$attr] = $attr_val;
667 }
668
669 foreach ($prodcomb->fetchAllByFkProductParent($prodid) as $prc) {
670 $values = array();
671
672 foreach ($prodcomb2val->fetchByFkCombination($prc->id) as $value) {
673 $values[$value->fk_prod_attr] = $value->fk_prod_attr_val;
674 }
675
676 $check1 = count(array_diff_assoc($values, $actual_comp));
677 $check2 = count(array_diff_assoc($actual_comp, $values));
678
679 if (!$check1 && !$check2) {
680 return $prc;
681 }
682 }
683
684 return false;
685 }
686
695 {
696 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttribute.class.php';
697 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttributeValue.class.php';
698
699 // Attributes
700 // Select all unique attributes of the variants (which are to sell) of a given parent product.
701 $sql = "SELECT DISTINCT c2v.fk_prod_attr, a.position";
702 $sql .= " FROM ".MAIN_DB_PREFIX."product_attribute_combination2val c2v";
703 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product_attribute_combination c";
704 $sql .= " ON c2v.fk_prod_combination = c.rowid";
705 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product p";
706 $sql .= " ON p.rowid = c.fk_product_child";
707 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product_attribute a";
708 $sql .= " ON a.rowid = fk_prod_attr";
709 $sql .= " WHERE c.fk_product_parent = ".((int) $productid);
710 $sql .= " AND p.tosell = 1";
711 $sql .= $this->db->order('a.position', 'asc');
712
713 $resql = $this->db->query($sql);
714
715 // Values
716 $variants = array();
717 while ($obj = $this->db->fetch_object($resql)) {
718 $attr = new ProductAttribute($this->db);
719 $attr->fetch($obj->fk_prod_attr);
720
721 $tmp = new stdClass();
722 $tmp->id = $attr->id;
723 $tmp->ref = $attr->ref;
724 $tmp->label = $attr->label;
725 $tmp->values = array();
726
727 $attrval = new ProductAttributeValue($this->db);
728 // fetch only the used values of this attribute
729 foreach ($attrval->fetchAllByProductAttribute($attr->id, true) as $val) {
730 '@phan-var-force ProductAttributeValue $val';
731 $tmp->values[] = $val;
732 }
733
734 $variants[] = $tmp;
735 }
736
737 return $variants;
738 }
739
763 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 = '')
764 {
765 global $conf;
766
767 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttribute.class.php';
768 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttributeValue.class.php';
769
770 $this->db->begin();
771
772 $price_impact = array(1 => 0); // init level price impact
773
774 $forced_refvar = trim((string) $forced_refvar);
775
776 if (!empty($forced_refvar) && $forced_refvar != $product->ref) {
777 $existingProduct = new Product($this->db);
778 $result = $existingProduct->fetch(0, $forced_refvar);
779 if ($result > 0) {
780 $newproduct = $existingProduct;
781 } else {
782 $existingProduct = false;
783 $newproduct = clone $product;
784 $newproduct->ref = $forced_refvar;
785 }
786 } else {
787 $forced_refvar = false;
788 $existingProduct = false;
789 $newproduct = clone $product;
790 }
791
792 // To avoid warning with unique extrafields values
793 $newproduct->context['createproductcombination'] = 'createproductcombination';
794
795 //Final weight impact
796 $weight_impact = (float) $forced_weightvar; // If false, return 0
797
798 //Final price impact
799 if (!is_array($forced_pricevar)) {
800 $price_impact[1] = (float) $forced_pricevar; // If false, return 0
801 } else {
802 $price_impact = $forced_pricevar;
803 }
804
805 if (!is_array($price_var_percent)) {
806 $price_var_percent = array(1 => (bool) $price_var_percent);
807 }
808
809 $newcomb = new ProductCombination($this->db);
810 $existingCombination = $newcomb->fetchByProductCombination2ValuePairs($product->id, $combinations);
811
812 if ($existingCombination) {
813 $newcomb = $existingCombination;
814 } else {
815 $newcomb->fk_product_parent = $product->id;
816
817 // Create 1 entry into product_attribute_combination (1 entry for each combinations). This init also $newcomb->id
818 $result = $newcomb->create($user);
819 if ($result < 0) {
820 $this->error = $newcomb->error;
821 $this->errors = $newcomb->errors;
822 $this->db->rollback();
823 return -1;
824 }
825 }
826
827 $prodattr = new ProductAttribute($this->db);
828 $prodattrval = new ProductAttributeValue($this->db);
829
830 // $combination contains list of attributes pairs key->value. Example: array('id Color'=>id Blue, 'id Size'=>id Small, 'id Option'=>id val a, ...)
831 foreach ($combinations as $currcombattr => $currcombval) {
832 //This was checked earlier, so no need to double check
833 $prodattr->fetch($currcombattr);
834 $prodattrval->fetch($currcombval);
835
836 //If there is an existing combination, there is no need to duplicate the valuepair
837 if (!$existingCombination) {
838 $tmp = new ProductCombination2ValuePair($this->db);
839 $tmp->fk_prod_attr = $currcombattr;
840 $tmp->fk_prod_attr_val = $currcombval;
841 $tmp->fk_prod_combination = $newcomb->id;
842
843 if ($tmp->create($user) < 0) { // Create 1 entry into product_attribute_combination2val
844 $this->error = $tmp->error;
845 $this->errors = $tmp->errors;
846 $this->db->rollback();
847 return -1;
848 }
849 }
850 if ($forced_weightvar === false) {
851 $weight_impact += (float) price2num($variations[$currcombattr][$currcombval]['weight']);
852 }
853 if ($forced_pricevar === false) {
854 $price_impact[1] += (float) price2num($variations[$currcombattr][$currcombval]['price']);
855
856 // Manage Price levels
857 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
858 $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
859 for ($i = 2; $i <= $produit_multiprices_limit; $i++) {
860 $price_impact[$i] += (float) price2num($variations[$currcombattr][$currcombval]['price']);
861 }
862 }
863 }
864
865 if ($forced_refvar === false) {
866 if (isset($conf->global->PRODUIT_ATTRIBUTES_SEPARATOR)) {
867 $newproduct->ref .= getDolGlobalString('PRODUIT_ATTRIBUTES_SEPARATOR') . $prodattrval->ref;
868 } else {
869 $newproduct->ref .= '_'.$prodattrval->ref;
870 }
871 }
872
873 //The first one should not contain a linebreak
874 if ($newproduct->description) {
875 $newproduct->description .= '<br>';
876 }
877 $newproduct->description .= '<strong>'.$prodattr->label.':</strong> '.$prodattrval->value;
878 }
879
880 $newcomb->variation_price_percentage = (bool) $price_var_percent[1];
881 $newcomb->variation_price = $price_impact[1];
882 $newcomb->variation_weight = $weight_impact;
883 $newcomb->variation_ref_ext = $this->db->escape($ref_ext);
884
885 // Init price level
886 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
887 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
888 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
889 $productCombinationLevel = new ProductCombinationLevel($this->db);
890 $productCombinationLevel->fk_product_attribute_combination = $newcomb->id;
891 $productCombinationLevel->fk_price_level = $i;
892 $productCombinationLevel->variation_price = $price_impact[$i];
893
894 if (is_array($price_var_percent)) {
895 $productCombinationLevel->variation_price_percentage = (bool) $price_var_percent[$i] ;
896 } else {
897 $productCombinationLevel->variation_price_percentage = $price_var_percent;
898 }
899
900 $newcomb->combination_price_levels[$i] = $productCombinationLevel;
901 }
902 }
903 //var_dump($newcomb->combination_price_levels);
904
905 $newproduct->weight += $weight_impact;
906
907 // Now create the product
908 //print 'Create prod '.$newproduct->ref.'<br>'."\n";
909 if ($existingProduct === false) {
910 //To avoid wrong information in price history log
911 $newproduct->price = 0;
912 $newproduct->price_ttc = 0;
913 $newproduct->price_min = 0;
914 $newproduct->price_min_ttc = 0;
915
916 // A new variant must use a new barcode (not same product)
917 $newproduct->barcode = -1;
918 $result = $newproduct->create($user);
919
920 if ($result < 0) {
921 //In case the error is not related with an already existing product
922 if ($newproduct->error != 'ErrorProductAlreadyExists') {
923 $this->error = $newproduct->error;
924 $this->errors = $newproduct->errors;
925 $this->db->rollback();
926 return -1;
927 }
928
934 if ($newcomb->fk_product_child) {
935 $res = $newproduct->fetch($existingCombination->fk_product_child);
936 } else {
937 $orig_prod_ref = $newproduct->ref;
938 $i = 1;
939
940 do {
941 $newproduct->ref = $orig_prod_ref.$i;
942 $res = $newproduct->create($user);
943
944 if ($newproduct->error != 'ErrorProductAlreadyExists') {
945 $this->errors[] = $newproduct->error;
946 break;
947 }
948
949 $i++;
950 } while ($res < 0);
951 }
952
953 if ($res < 0) {
954 $this->db->rollback();
955 return -1;
956 }
957 }
958 } else {
959 $result = $newproduct->update($newproduct->id, $user);
960 if ($result < 0) {
961 $this->db->rollback();
962 return -1;
963 }
964 }
965
966 $newcomb->fk_product_child = $newproduct->id;
967
968 if ($newcomb->update($user) < 0) {
969 $this->error = $newcomb->error;
970 $this->errors = $newcomb->errors;
971 $this->db->rollback();
972 return -1;
973 }
974
975 $this->db->commit();
976 return $newproduct->id;
977 }
978
987 public function copyAll(User $user, $origProductId, Product $destProduct)
988 {
989 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
990
991 //To prevent a loop
992 if ($origProductId == $destProduct->id) {
993 return -1;
994 }
995
996 $prodcomb2val = new ProductCombination2ValuePair($this->db);
997
998 //Retrieve all product combinations
999 $combinations = $this->fetchAllByFkProductParent($origProductId);
1000
1001 foreach ($combinations as $combination) {
1002 $variations = array();
1003
1004 foreach ($prodcomb2val->fetchByFkCombination($combination->id) as $tmp_pc2v) {
1005 $variations[$tmp_pc2v->fk_prod_attr] = $tmp_pc2v->fk_prod_attr_val;
1006 }
1007
1008 $variation_price_percentage = $combination->variation_price_percentage;
1009 $variation_price = $combination->variation_price;
1010
1011 if (getDolGlobalInt('PRODUIT_MULTIPRICES') && getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT') > 1) {
1012 $variation_price_percentage = [ ];
1013 $variation_price = [ ];
1014
1015 foreach ($combination->combination_price_levels as $productCombinationLevel) {
1016 $variation_price_percentage[$productCombinationLevel->fk_price_level] = $productCombinationLevel->variation_price_percentage;
1017 $variation_price[$productCombinationLevel->fk_price_level] = $productCombinationLevel->variation_price;
1018 }
1019 }
1020
1021 if ($this->createProductCombination(
1022 $user,
1023 $destProduct,
1024 $variations,
1025 array(),
1026 $variation_price_percentage,
1027 $variation_price,
1028 $combination->variation_weight
1029 ) < 0) {
1030 return -1;
1031 }
1032 }
1033
1034 return 1;
1035 }
1036
1042 public function getCombinationLabel($prod_child)
1043 {
1044 $label = '';
1045 $sql = 'SELECT pav.value AS label';
1046 $sql .= ' FROM '.MAIN_DB_PREFIX.'product_attribute_combination pac';
1047 $sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'product_attribute_combination2val pac2v ON pac2v.fk_prod_combination=pac.rowid';
1048 $sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'product_attribute_value pav ON pav.rowid=pac2v.fk_prod_attr_val';
1049 $sql .= ' WHERE pac.fk_product_child='.((int) $prod_child);
1050
1051 $resql = $this->db->query($sql);
1052 if ($resql) {
1053 $num = $this->db->num_rows($resql);
1054
1055 $i = 0;
1056
1057 while ($i < $num) {
1058 $obj = $this->db->fetch_object($resql);
1059
1060 if ($obj->label) {
1061 $label .= ' '.$obj->label;
1062 }
1063 $i++;
1064 }
1065 }
1066 return $label;
1067 }
1068}
1069
1070
1071
1077{
1082 public $db;
1083
1087 public $table_element = 'product_attribute_combination_price_level';
1088
1093 public $id;
1094
1099 public $fk_product_attribute_combination;
1100
1105 public $fk_price_level;
1106
1111 public $variation_price;
1112
1117 public $variation_price_percentage = false;
1118
1122 public $error;
1123
1127 public $errors = array();
1128
1134 public function __construct(DoliDB $db)
1135 {
1136 $this->db = $db;
1137 }
1138
1145 public function fetch($rowid)
1146 {
1147 $sql = "SELECT rowid, fk_product_attribute_combination, fk_price_level, variation_price, variation_price_percentage";
1148 $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element;
1149 $sql .= " WHERE rowid = ".(int) $rowid;
1150
1151 $resql = $this->db->query($sql);
1152 if ($resql) {
1153 $obj = $this->db->fetch_object($resql);
1154 if ($obj) {
1155 return $this->fetchFormObj($obj);
1156 }
1157 }
1158
1159 return -1;
1160 }
1161
1162
1170 public function fetchAll($fk_product_attribute_combination, $fk_price_level = 0)
1171 {
1172 $result = array();
1173
1174 $sql = "SELECT rowid, fk_product_attribute_combination, fk_price_level, variation_price, variation_price_percentage";
1175 $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element;
1176 $sql .= " WHERE fk_product_attribute_combination = ".intval($fk_product_attribute_combination);
1177 if (!empty($fk_price_level)) {
1178 $sql .= ' AND fk_price_level = '.intval($fk_price_level);
1179 }
1180
1181 $res = $this->db->query($sql);
1182 if ($res) {
1183 if ($this->db->num_rows($res) > 0) {
1184 while ($obj = $this->db->fetch_object($res)) {
1185 $productCombinationLevel = new ProductCombinationLevel($this->db);
1186 $productCombinationLevel->fetchFormObj($obj);
1187 $result[$obj->fk_price_level] = $productCombinationLevel;
1188 }
1189 }
1190 } else {
1191 return -1;
1192 }
1193
1194 return $result;
1195 }
1196
1203 public function fetchFormObj($obj)
1204 {
1205 if (!$obj) {
1206 return -1;
1207 }
1208
1209 $this->id = $obj->rowid;
1210 $this->fk_product_attribute_combination = (int) $obj->fk_product_attribute_combination;
1211 $this->fk_price_level = intval($obj->fk_price_level);
1212 $this->variation_price = (float) $obj->variation_price;
1213 $this->variation_price_percentage = (bool) $obj->variation_price_percentage;
1214
1215 return 1;
1216 }
1217
1218
1224 public function save()
1225 {
1226 if (($this->id > 0 && empty($this->fk_product_attribute_combination)) || empty($this->fk_price_level)) {
1227 return -1;
1228 }
1229
1230 // Check if level exist in DB before add
1231 if ($this->fk_product_attribute_combination > 0 && empty($this->id)) {
1232 $sql = "SELECT rowid id";
1233 $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element;
1234 $sql .= " WHERE fk_product_attribute_combination = ".(int) $this->fk_product_attribute_combination;
1235 $sql .= ' AND fk_price_level = '.((int) $this->fk_price_level);
1236
1237 $resql = $this->db->query($sql);
1238 if ($resql) {
1239 $obj = $this->db->fetch_object($resql);
1240 if ($obj) {
1241 $this->id = $obj->id;
1242 }
1243 }
1244 }
1245
1246 // Update
1247 if (!empty($this->id)) {
1248 $sql = 'UPDATE '.MAIN_DB_PREFIX.$this->table_element;
1249 $sql .= ' SET variation_price = '.(float) $this->variation_price.' , variation_price_percentage = '.intval($this->variation_price_percentage);
1250 $sql .= ' WHERE rowid = '.((int) $this->id);
1251
1252 $res = $this->db->query($sql);
1253 if ($res > 0) {
1254 return $this->id;
1255 } else {
1256 $this->error = $this->db->error();
1257 $this->errors[] = $this->error;
1258 return -1;
1259 }
1260 } else {
1261 // Add
1262 $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
1263 $sql .= "fk_product_attribute_combination, fk_price_level, variation_price, variation_price_percentage";
1264 $sql .= ") VALUES (";
1265 $sql .= (int) $this->fk_product_attribute_combination;
1266 $sql .= ", ".intval($this->fk_price_level);
1267 $sql .= ", ".(float) $this->variation_price;
1268 $sql .= ", ".intval($this->variation_price_percentage);
1269 $sql .= ")";
1270
1271 $res = $this->db->query($sql);
1272 if ($res) {
1273 $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
1274 } else {
1275 $this->error = $this->db->error();
1276 $this->errors[] = $this->error;
1277 return -1;
1278 }
1279 }
1280
1281 return $this->id;
1282 }
1283
1284
1290 public function delete()
1291 {
1292 $sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".(int) $this->id;
1293 $res = $this->db->query($sql);
1294
1295 return $res ? 1 : -1;
1296 }
1297
1298
1305 public function deleteAllForCombination($fk_product_attribute_combination)
1306 {
1307 $sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE fk_product_attribute_combination = ".(int) $fk_product_attribute_combination;
1308 $res = $this->db->query($sql);
1309
1310 return $res ? 1 : -1;
1311 }
1312
1313
1320 public function clean($fk_product_attribute_combination)
1321 {
1322 global $conf;
1323
1324 $sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element;
1325 $sql .= " WHERE fk_product_attribute_combination = ".(int) $fk_product_attribute_combination;
1326 $sql .= " AND fk_price_level > ".(int) getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
1327 $res = $this->db->query($sql);
1328
1329 return $res ? 1 : -1;
1330 }
1331
1332
1341 public static function createFromParent(DoliDB $db, ProductCombination $productCombination, $fkPriceLevel)
1342 {
1343 $productCombinationLevel = new self($db);
1344 $productCombinationLevel->fk_price_level = $fkPriceLevel;
1345 $productCombinationLevel->fk_product_attribute_combination = $productCombination->id;
1346 $productCombinationLevel->variation_price = $productCombination->variation_price;
1347 $productCombinationLevel->variation_price_percentage = (bool) $productCombination->variation_price_percentage;
1348
1349 return $productCombinationLevel;
1350 }
1351}
Class to manage Dolibarr database access.
Class ProductAttribute Used to represent a Product attribute Examples:
Class ProductAttributeValue Used to represent a product attribute value.
Class ProductCombination2ValuePair Used to represent the relation between a variant and its attribute...
Class ProductCombination Used to represent the relation between a product and one of its variants.
update(User $user)
Updates a product combination.
countNbOfCombinationForFkProductParent($fk_product_parent)
Retrieves all product combinations by the product parent row id.
getUniqueAttributesAndValuesByFkProductParent($productid)
Retrieves all unique attributes for a parent product (filtered on its 'to sell' variants)
fetchCombinationPriceLevels($fk_price_level=0, $useCache=true)
Retrieves combination price levels.
deleteByFkProductParent($user, $fk_product_parent)
Deletes all product combinations of a parent product.
updateProperties(Product $parent, User $user)
Updates the weight of the child product.
fetchByFkProductChild($productid, $donotloadpricelevel=0)
Retrieves information of a variant product and ID of its parent product.
copyAll(User $user, $origProductId, Product $destProduct)
Copies all product combinations from the origin product to the destination product.
__construct(DoliDB $db)
Constructor.
createProductCombination(User $user, Product $product, array $combinations, array $variations, $price_var_percent=false, $forced_pricevar=false, $forced_weightvar=false, $forced_refvar=false, $ref_ext='')
Creates a product combination.
create($user)
Creates a product attribute combination.
fetchAllByFkProductParent($fk_product_parent, $sort_by_ref=false)
Retrieves all product combinations by the product parent row id.
saveCombinationPriceLevels($clean=1)
Retrieves combination price levels.
getCombinationLabel($prod_child)
Return label for combinations.
fetchByProductCombination2ValuePairs($prodid, array $features)
Retrieves the combination that matches the given features.
fetch($rowid)
Retrieves a ProductCombination by its rowid.
Class ProductCombinationLevel Used to represent a product combination Level.
static createFromParent(DoliDB $db, ProductCombination $productCombination, $fkPriceLevel)
Create new Product Combination Price level from Parent.
save()
Save a price impact of a product combination for a price level.
clean($fk_product_attribute_combination)
Clean not needed price levels for a combination.
fetchAll($fk_product_attribute_combination, $fk_price_level=0)
Retrieves combination price levels.
deleteAllForCombination($fk_product_attribute_combination)
delete all for a combination
fetchFormObj($obj)
Assign vars form an stdclass like sql obj.
__construct(DoliDB $db)
Constructor.
fetch($rowid)
Retrieves a combination level by its rowid.
Class to manage products or services.
Class to manage Dolibarr users.
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
getEntity($element, $shared=1, $currentobject=null)
Get list of entity id to use.
global $conf
The following vars must be defined: $type2label $form $conf, $lang, The following vars may also be de...
Definition member.php:79