dolibarr 21.0.4
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 // Propagate failure of updateProperties (otherwise an update where the variant
440 // percentage drives the new price below the parent product's price_min returns
441 // success but leaves the variant product at the price=0 it had right after
442 // createProductCombination, see issue #32372).
443 $result = $this->updateProperties($parent, $user);
444 if ($result < 0) {
445 return $result;
446 }
447
448 return 1;
449 }
450
457 public function delete(User $user)
458 {
459 $this->db->begin();
460
461 $comb2val = new ProductCombination2ValuePair($this->db);
462 $comb2val->deleteByFkCombination($this->id);
463
464 // remove combination price levels
465 if (!$this->db->query("DELETE FROM ".MAIN_DB_PREFIX."product_attribute_combination_price_level WHERE fk_product_attribute_combination = ".(int) $this->id)) {
466 $this->db->rollback();
467 return -1;
468 }
469
470 $sql = "DELETE FROM ".MAIN_DB_PREFIX."product_attribute_combination WHERE rowid = ".(int) $this->id;
471
472 if ($this->db->query($sql)) {
473 $this->db->commit();
474 return 1;
475 }
476
477 $this->db->rollback();
478 return -1;
479 }
480
488 public function deleteByFkProductParent($user, $fk_product_parent)
489 {
490 $combinations = $this->fetchAllByFkProductParent($fk_product_parent);
491
492 if (!is_array($combinations)) { // No combinations found, return success
493 return 1;
494 }
495
496 $this->db->begin();
497
498 foreach ($combinations as $prodcomb) {
499 $prodstatic = new Product($this->db);
500
501 $res = $prodstatic->fetch($prodcomb->fk_product_child);
502
503 if ($res > 0) {
504 $res = $prodcomb->delete($user);
505 }
506
507 if ($res > 0 && !$prodstatic->isObjectUsed($prodstatic->id)) {
508 $res = $prodstatic->delete($user);
509 }
510
511 if ($res < 0) {
512 $this->db->rollback();
513 return -1;
514 }
515 }
516
517 $this->db->commit();
518 return 1;
519 }
520
529 public function updateProperties(Product $parent, User $user)
530 {
531 global $conf;
532
533 $this->db->begin();
534
535 $child = new Product($this->db);
536 $child->fetch($this->fk_product_child);
537
538 $child->price_autogen = $parent->price_autogen;
539 $child->weight = $parent->weight;
540 // Only when Parent Status are updated
541 if (is_object($parent->oldcopy) && !$parent->oldcopy->isEmpty() && ($parent->status != $parent->oldcopy->status)) {
542 $child->status = $parent->status;
543 }
544 if (is_object($parent->oldcopy) && !$parent->oldcopy->isEmpty() && ($parent->status_buy != $parent->oldcopy->status_buy)) {
545 $child->status_buy = $parent->status_buy;
546 }
547
548 if ($this->variation_weight) { // If we must add a delta on weight
549 $child->weight = ($child->weight ? $child->weight : 0) + $this->variation_weight;
550 }
551 $child->weight_units = $parent->weight_units;
552
553 // Don't update the child label if the user has already modified it.
554 if ($child->label == $parent->label) {
555 // This will trigger only at variant creation time
556 $varlabel = $this->getCombinationLabel($this->fk_product_child);
557 $child->label = $parent->label.$varlabel;
558 }
559
560
561 if ($child->update($child->id, $user) > 0) {
562 $new_vat = $parent->tva_tx;
563 $new_npr = $parent->tva_npr;
564
565 // MultiPrix
566 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
567 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
568 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
569 if ($parent->multiprices[$i] != '' || isset($this->combination_price_levels[$i]->variation_price)) {
570 $new_type = empty($parent->multiprices_base_type[$i]) ? 'HT' : $parent->multiprices_base_type[$i];
571 $new_min_price = $parent->multiprices_min[$i];
572 $variation_price = (float) (!isset($this->combination_price_levels[$i]->variation_price) ? $this->variation_price : $this->combination_price_levels[$i]->variation_price);
573 $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);
574
575 if ($parent->prices_by_qty_list[$i]) {
576 $new_psq = 1;
577 } else {
578 $new_psq = 0;
579 }
580
581 if ($new_type == 'TTC') {
582 $new_price = $parent->multiprices_ttc[$i];
583 } else {
584 $new_price = $parent->multiprices[$i];
585 }
586
587 if ($variation_price_percentage) {
588 if ($new_price != 0) {
589 $new_price *= 1 + ($variation_price / 100);
590 }
591 } else {
592 $new_price += $variation_price;
593 }
594
595 $ret = $child->updatePrice($new_price, $new_type, $user, $new_vat, $new_min_price, $i, $new_npr, $new_psq, 0, array(), $parent->default_vat_code);
596
597 if ($ret < 0) {
598 $this->db->rollback();
599 $this->error = $child->error;
600 $this->errors = $child->errors;
601 return $ret;
602 }
603 }
604 }
605 } else {
606 $new_type = $parent->price_base_type;
607 $new_min_price = $parent->price_min;
608 $new_psq = $parent->price_by_qty;
609
610 if ($new_type == 'TTC') {
611 $new_price = $parent->price_ttc;
612 } else {
613 $new_price = $parent->price;
614 }
615
616 if ($this->variation_price_percentage) {
617 if ($new_price != 0) {
618 $new_price *= 1 + ($this->variation_price / 100);
619 }
620 } else {
621 $new_price += $this->variation_price;
622 }
623
624 $ret = $child->updatePrice($new_price, $new_type, $user, $new_vat, $new_min_price, 1, $new_npr, $new_psq);
625
626 if ($ret < 0) {
627 $this->db->rollback();
628 $this->error = $child->error;
629 $this->errors = $child->errors;
630 return $ret;
631 }
632 }
633
634 $this->db->commit();
635
636 return 1;
637 }
638
639 $this->db->rollback();
640 $this->error = $child->error;
641 $this->errors = $child->errors;
642 return -1;
643 }
644
652 public function fetchByProductCombination2ValuePairs($prodid, array $features)
653 {
654 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
655
656 $actual_comp = array();
657
658 $prodcomb2val = new ProductCombination2ValuePair($this->db);
659 $prodcomb = new ProductCombination($this->db);
660
661 $features = array_filter(
662 $features,
667 static function ($v) {
668 return !empty($v);
669 }
670 );
671
672 foreach ($features as $attr => $attr_val) {
673 $actual_comp[$attr] = $attr_val;
674 }
675
676 foreach ($prodcomb->fetchAllByFkProductParent($prodid) as $prc) {
677 $values = array();
678
679 foreach ($prodcomb2val->fetchByFkCombination($prc->id) as $value) {
680 $values[$value->fk_prod_attr] = $value->fk_prod_attr_val;
681 }
682
683 $check1 = count(array_diff_assoc($values, $actual_comp));
684 $check2 = count(array_diff_assoc($actual_comp, $values));
685
686 if (!$check1 && !$check2) {
687 return $prc;
688 }
689 }
690
691 return false;
692 }
693
702 {
703 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttribute.class.php';
704 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttributeValue.class.php';
705
706 // Attributes
707 // Select all unique attributes of the variants (which are to sell) of a given parent product.
708 $sql = "SELECT DISTINCT c2v.fk_prod_attr, a.position";
709 $sql .= " FROM ".MAIN_DB_PREFIX."product_attribute_combination2val c2v";
710 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product_attribute_combination c";
711 $sql .= " ON c2v.fk_prod_combination = c.rowid";
712 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product p";
713 $sql .= " ON p.rowid = c.fk_product_child";
714 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product_attribute a";
715 $sql .= " ON a.rowid = fk_prod_attr";
716 $sql .= " WHERE c.fk_product_parent = ".((int) $productid);
717 $sql .= " AND p.tosell = 1";
718 $sql .= $this->db->order('a.position', 'asc');
719
720 $resql = $this->db->query($sql);
721
722 // Values
723 $variants = array();
724 while ($obj = $this->db->fetch_object($resql)) {
725 $attr = new ProductAttribute($this->db);
726 $attr->fetch($obj->fk_prod_attr);
727
728 $tmp = new stdClass();
729 $tmp->id = $attr->id;
730 $tmp->ref = $attr->ref;
731 $tmp->label = $attr->label;
732 $tmp->values = array();
733
734 $attrval = new ProductAttributeValue($this->db);
735 // fetch only the used values of this attribute
736 foreach ($attrval->fetchAllByProductAttribute($attr->id, true) as $val) {
737 '@phan-var-force ProductAttributeValue $val';
738 $tmp->values[] = $val;
739 }
740
741 $variants[] = $tmp;
742 }
743
744 return $variants;
745 }
746
770 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 = '')
771 {
772 global $conf;
773
774 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttribute.class.php';
775 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttributeValue.class.php';
776
777 $this->db->begin();
778
779 $price_impact = array(1 => 0); // init level price impact
780
781 $forced_refvar = trim((string) $forced_refvar);
782
783 if (!empty($forced_refvar) && $forced_refvar != $product->ref) {
784 $existingProduct = new Product($this->db);
785 $result = $existingProduct->fetch(0, $forced_refvar);
786 if ($result > 0) {
787 $newproduct = $existingProduct;
788 } else {
789 $existingProduct = false;
790 $newproduct = clone $product;
791 $newproduct->ref = $forced_refvar;
792 }
793 } else {
794 $forced_refvar = false;
795 $existingProduct = false;
796 $newproduct = clone $product;
797 }
798
799 // To avoid warning with unique extrafields values
800 $newproduct->context['createproductcombination'] = 'createproductcombination';
801
802 //Final weight impact
803 $weight_impact = (float) $forced_weightvar; // If false, return 0
804
805 //Final price impact
806 if (!is_array($forced_pricevar)) {
807 $price_impact[1] = (float) $forced_pricevar; // If false, return 0
808 } else {
809 $price_impact = $forced_pricevar;
810 }
811
812 if (!is_array($price_var_percent)) {
813 $price_var_percent = array(1 => (bool) $price_var_percent);
814 }
815
816 $newcomb = new ProductCombination($this->db);
817 $existingCombination = $newcomb->fetchByProductCombination2ValuePairs($product->id, $combinations);
818
819 if ($existingCombination) {
820 $newcomb = $existingCombination;
821 } else {
822 $newcomb->fk_product_parent = $product->id;
823
824 // Create 1 entry into product_attribute_combination (1 entry for each combinations). This init also $newcomb->id
825 $result = $newcomb->create($user);
826 if ($result < 0) {
827 $this->error = $newcomb->error;
828 $this->errors = $newcomb->errors;
829 $this->db->rollback();
830 return -1;
831 }
832 }
833
834 $prodattr = new ProductAttribute($this->db);
835 $prodattrval = new ProductAttributeValue($this->db);
836
837 // $combination contains list of attributes pairs key->value. Example: array('id Color'=>id Blue, 'id Size'=>id Small, 'id Option'=>id val a, ...)
838 foreach ($combinations as $currcombattr => $currcombval) {
839 //This was checked earlier, so no need to double check
840 $prodattr->fetch($currcombattr);
841 $prodattrval->fetch($currcombval);
842
843 //If there is an existing combination, there is no need to duplicate the valuepair
844 if (!$existingCombination) {
845 $tmp = new ProductCombination2ValuePair($this->db);
846 $tmp->fk_prod_attr = $currcombattr;
847 $tmp->fk_prod_attr_val = $currcombval;
848 $tmp->fk_prod_combination = $newcomb->id;
849
850 if ($tmp->create($user) < 0) { // Create 1 entry into product_attribute_combination2val
851 $this->error = $tmp->error;
852 $this->errors = $tmp->errors;
853 $this->db->rollback();
854 return -1;
855 }
856 }
857 if ($forced_weightvar === false) {
858 $weight_impact += (float) price2num($variations[$currcombattr][$currcombval]['weight']);
859 }
860 if ($forced_pricevar === false) {
861 $price_impact[1] += (float) price2num($variations[$currcombattr][$currcombval]['price']);
862
863 // Manage Price levels
864 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
865 $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
866 for ($i = 2; $i <= $produit_multiprices_limit; $i++) {
867 $price_impact[$i] += (float) price2num($variations[$currcombattr][$currcombval]['price']);
868 }
869 }
870 }
871
872 if ($forced_refvar === false) {
873 if (isset($conf->global->PRODUIT_ATTRIBUTES_SEPARATOR)) {
874 $newproduct->ref .= getDolGlobalString('PRODUIT_ATTRIBUTES_SEPARATOR') . $prodattrval->ref;
875 } else {
876 $newproduct->ref .= '_'.$prodattrval->ref;
877 }
878 }
879
880 //The first one should not contain a linebreak
881 if ($newproduct->description) {
882 $newproduct->description .= '<br>';
883 }
884 $newproduct->description .= '<strong>'.$prodattr->label.':</strong> '.$prodattrval->value;
885 }
886
887 $newcomb->variation_price_percentage = (bool) $price_var_percent[1];
888 $newcomb->variation_price = $price_impact[1];
889 $newcomb->variation_weight = $weight_impact;
890 $newcomb->variation_ref_ext = $this->db->escape($ref_ext);
891
892 // Init price level
893 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
894 $produit_multiprices_limit = getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT');
895 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
896 $productCombinationLevel = new ProductCombinationLevel($this->db);
897 $productCombinationLevel->fk_product_attribute_combination = $newcomb->id;
898 $productCombinationLevel->fk_price_level = $i;
899 $productCombinationLevel->variation_price = $price_impact[$i];
900
901 if (is_array($price_var_percent)) {
902 $productCombinationLevel->variation_price_percentage = (bool) $price_var_percent[$i] ;
903 } else {
904 $productCombinationLevel->variation_price_percentage = $price_var_percent;
905 }
906
907 $newcomb->combination_price_levels[$i] = $productCombinationLevel;
908 }
909 }
910 //var_dump($newcomb->combination_price_levels);
911
912 $newproduct->weight += $weight_impact;
913
914 // Now create the product
915 //print 'Create prod '.$newproduct->ref.'<br>'."\n";
916 if ($existingProduct === false) {
917 //To avoid wrong information in price history log
918 $newproduct->price = 0;
919 $newproduct->price_ttc = 0;
920 $newproduct->price_min = 0;
921 $newproduct->price_min_ttc = 0;
922
923 // A new variant must use a new barcode (not same product)
924 $newproduct->barcode = -1;
925 $result = $newproduct->create($user);
926
927 if ($result < 0) {
928 //In case the error is not related with an already existing product
929 if ($newproduct->error != 'ErrorProductAlreadyExists') {
930 $this->error = $newproduct->error;
931 $this->errors = $newproduct->errors;
932 $this->db->rollback();
933 return -1;
934 }
935
941 if ($newcomb->fk_product_child) {
942 $res = $newproduct->fetch($existingCombination->fk_product_child);
943 } else {
944 $orig_prod_ref = $newproduct->ref;
945 $i = 1;
946
947 do {
948 $newproduct->ref = $orig_prod_ref.$i;
949 $res = $newproduct->create($user);
950
951 if ($newproduct->error != 'ErrorProductAlreadyExists') {
952 $this->errors[] = $newproduct->error;
953 break;
954 }
955
956 $i++;
957 } while ($res < 0);
958 }
959
960 if ($res < 0) {
961 $this->db->rollback();
962 return -1;
963 }
964 }
965 } else {
966 $result = $newproduct->update($newproduct->id, $user);
967 if ($result < 0) {
968 $this->db->rollback();
969 return -1;
970 }
971 }
972
973 $newcomb->fk_product_child = $newproduct->id;
974
975 if ($newcomb->update($user) < 0) {
976 $this->error = $newcomb->error;
977 $this->errors = $newcomb->errors;
978 $this->db->rollback();
979 return -1;
980 }
981
982 $this->db->commit();
983 return $newproduct->id;
984 }
985
994 public function copyAll(User $user, $origProductId, Product $destProduct)
995 {
996 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
997
998 //To prevent a loop
999 if ($origProductId == $destProduct->id) {
1000 return -1;
1001 }
1002
1003 $prodcomb2val = new ProductCombination2ValuePair($this->db);
1004
1005 //Retrieve all product combinations
1006 $combinations = $this->fetchAllByFkProductParent($origProductId);
1007
1008 foreach ($combinations as $combination) {
1009 $variations = array();
1010
1011 foreach ($prodcomb2val->fetchByFkCombination($combination->id) as $tmp_pc2v) {
1012 $variations[$tmp_pc2v->fk_prod_attr] = $tmp_pc2v->fk_prod_attr_val;
1013 }
1014
1015 $variation_price_percentage = $combination->variation_price_percentage;
1016 $variation_price = $combination->variation_price;
1017
1018 if (getDolGlobalInt('PRODUIT_MULTIPRICES') && getDolGlobalInt('PRODUIT_MULTIPRICES_LIMIT') > 1) {
1019 $variation_price_percentage = [ ];
1020 $variation_price = [ ];
1021
1022 foreach ($combination->combination_price_levels as $productCombinationLevel) {
1023 $variation_price_percentage[$productCombinationLevel->fk_price_level] = $productCombinationLevel->variation_price_percentage;
1024 $variation_price[$productCombinationLevel->fk_price_level] = $productCombinationLevel->variation_price;
1025 }
1026 }
1027
1028 if ($this->createProductCombination(
1029 $user,
1030 $destProduct,
1031 $variations,
1032 array(),
1033 $variation_price_percentage,
1034 $variation_price,
1035 $combination->variation_weight
1036 ) < 0) {
1037 return -1;
1038 }
1039 }
1040
1041 return 1;
1042 }
1043
1049 public function getCombinationLabel($prod_child)
1050 {
1051 $label = '';
1052 $sql = 'SELECT pav.value AS label';
1053 $sql .= ' FROM '.MAIN_DB_PREFIX.'product_attribute_combination pac';
1054 $sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'product_attribute_combination2val pac2v ON pac2v.fk_prod_combination=pac.rowid';
1055 $sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'product_attribute_value pav ON pav.rowid=pac2v.fk_prod_attr_val';
1056 $sql .= ' WHERE pac.fk_product_child='.((int) $prod_child);
1057
1058 $resql = $this->db->query($sql);
1059 if ($resql) {
1060 $num = $this->db->num_rows($resql);
1061
1062 $i = 0;
1063
1064 while ($i < $num) {
1065 $obj = $this->db->fetch_object($resql);
1066
1067 if ($obj->label) {
1068 $label .= ' '.$obj->label;
1069 }
1070 $i++;
1071 }
1072 }
1073 return $label;
1074 }
1075}
1076
1077
1078
1084{
1089 public $db;
1090
1094 public $table_element = 'product_attribute_combination_price_level';
1095
1100 public $id;
1101
1106 public $fk_product_attribute_combination;
1107
1112 public $fk_price_level;
1113
1118 public $variation_price;
1119
1124 public $variation_price_percentage = false;
1125
1129 public $error;
1130
1134 public $errors = array();
1135
1141 public function __construct(DoliDB $db)
1142 {
1143 $this->db = $db;
1144 }
1145
1152 public function fetch($rowid)
1153 {
1154 $sql = "SELECT rowid, fk_product_attribute_combination, fk_price_level, variation_price, variation_price_percentage";
1155 $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element;
1156 $sql .= " WHERE rowid = ".(int) $rowid;
1157
1158 $resql = $this->db->query($sql);
1159 if ($resql) {
1160 $obj = $this->db->fetch_object($resql);
1161 if ($obj) {
1162 return $this->fetchFormObj($obj);
1163 }
1164 }
1165
1166 return -1;
1167 }
1168
1169
1177 public function fetchAll($fk_product_attribute_combination, $fk_price_level = 0)
1178 {
1179 $result = array();
1180
1181 $sql = "SELECT rowid, fk_product_attribute_combination, fk_price_level, variation_price, variation_price_percentage";
1182 $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element;
1183 $sql .= " WHERE fk_product_attribute_combination = ".intval($fk_product_attribute_combination);
1184 if (!empty($fk_price_level)) {
1185 $sql .= ' AND fk_price_level = '.intval($fk_price_level);
1186 }
1187
1188 $res = $this->db->query($sql);
1189 if ($res) {
1190 if ($this->db->num_rows($res) > 0) {
1191 while ($obj = $this->db->fetch_object($res)) {
1192 $productCombinationLevel = new ProductCombinationLevel($this->db);
1193 $productCombinationLevel->fetchFormObj($obj);
1194 $result[$obj->fk_price_level] = $productCombinationLevel;
1195 }
1196 }
1197 } else {
1198 return -1;
1199 }
1200
1201 return $result;
1202 }
1203
1210 public function fetchFormObj($obj)
1211 {
1212 if (!$obj) {
1213 return -1;
1214 }
1215
1216 $this->id = $obj->rowid;
1217 $this->fk_product_attribute_combination = (int) $obj->fk_product_attribute_combination;
1218 $this->fk_price_level = intval($obj->fk_price_level);
1219 $this->variation_price = (float) $obj->variation_price;
1220 $this->variation_price_percentage = (bool) $obj->variation_price_percentage;
1221
1222 return 1;
1223 }
1224
1225
1231 public function save()
1232 {
1233 if (($this->id > 0 && empty($this->fk_product_attribute_combination)) || empty($this->fk_price_level)) {
1234 return -1;
1235 }
1236
1237 // Check if level exist in DB before add
1238 if ($this->fk_product_attribute_combination > 0 && empty($this->id)) {
1239 $sql = "SELECT rowid id";
1240 $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element;
1241 $sql .= " WHERE fk_product_attribute_combination = ".(int) $this->fk_product_attribute_combination;
1242 $sql .= ' AND fk_price_level = '.((int) $this->fk_price_level);
1243
1244 $resql = $this->db->query($sql);
1245 if ($resql) {
1246 $obj = $this->db->fetch_object($resql);
1247 if ($obj) {
1248 $this->id = $obj->id;
1249 }
1250 }
1251 }
1252
1253 // Update
1254 if (!empty($this->id)) {
1255 $sql = 'UPDATE '.MAIN_DB_PREFIX.$this->table_element;
1256 $sql .= ' SET variation_price = '.(float) $this->variation_price.' , variation_price_percentage = '.intval($this->variation_price_percentage);
1257 $sql .= ' WHERE rowid = '.((int) $this->id);
1258
1259 $res = $this->db->query($sql);
1260 if ($res > 0) {
1261 return $this->id;
1262 } else {
1263 $this->error = $this->db->error();
1264 $this->errors[] = $this->error;
1265 return -1;
1266 }
1267 } else {
1268 // Add
1269 $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
1270 $sql .= "fk_product_attribute_combination, fk_price_level, variation_price, variation_price_percentage";
1271 $sql .= ") VALUES (";
1272 $sql .= (int) $this->fk_product_attribute_combination;
1273 $sql .= ", ".intval($this->fk_price_level);
1274 $sql .= ", ".(float) $this->variation_price;
1275 $sql .= ", ".intval($this->variation_price_percentage);
1276 $sql .= ")";
1277
1278 $res = $this->db->query($sql);
1279 if ($res) {
1280 $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
1281 } else {
1282 $this->error = $this->db->error();
1283 $this->errors[] = $this->error;
1284 return -1;
1285 }
1286 }
1287
1288 return $this->id;
1289 }
1290
1291
1297 public function delete()
1298 {
1299 $sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".(int) $this->id;
1300 $res = $this->db->query($sql);
1301
1302 return $res ? 1 : -1;
1303 }
1304
1305
1312 public function deleteAllForCombination($fk_product_attribute_combination)
1313 {
1314 $sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE fk_product_attribute_combination = ".(int) $fk_product_attribute_combination;
1315 $res = $this->db->query($sql);
1316
1317 return $res ? 1 : -1;
1318 }
1319
1320
1327 public function clean($fk_product_attribute_combination)
1328 {
1329 global $conf;
1330
1331 $sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element;
1332 $sql .= " WHERE fk_product_attribute_combination = ".(int) $fk_product_attribute_combination;
1333 $sql .= " AND fk_price_level > ".(int) getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
1334 $res = $this->db->query($sql);
1335
1336 return $res ? 1 : -1;
1337 }
1338
1339
1348 public static function createFromParent(DoliDB $db, ProductCombination $productCombination, $fkPriceLevel)
1349 {
1350 $productCombinationLevel = new self($db);
1351 $productCombinationLevel->fk_price_level = $fkPriceLevel;
1352 $productCombinationLevel->fk_product_attribute_combination = $productCombination->id;
1353 $productCombinationLevel->variation_price = $productCombination->variation_price;
1354 $productCombinationLevel->variation_price_percentage = (bool) $productCombination->variation_price_percentage;
1355
1356 return $productCombinationLevel;
1357 }
1358}
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