dolibarr 21.0.0-alpha
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 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <https://www.gnu.org/licenses/>.
20 */
21
42{
47 public $db;
48
53 public $id;
54
59 public $fk_product_parent;
60
65 public $fk_product_child;
66
71 public $variation_price;
72
78 public $variation_price_percentage = false;
79
84 public $variation_weight;
85
90 public $entity;
91
96 public $combination_price_levels;
97
102 public $variation_ref_ext = '';
103
108 public $error;
109
114 public $errors = array();
115
121 public function __construct(DoliDB $db)
122 {
123 global $conf;
124
125 $this->db = $db;
126 $this->entity = $conf->entity;
127 }
128
135 public function fetch($rowid)
136 {
137 global $conf;
138
139 $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').")";
140
141 $query = $this->db->query($sql);
142
143 if (!$query) {
144 return -1;
145 }
146
147 if (!$this->db->num_rows($query)) {
148 return -1;
149 }
150
151 $obj = $this->db->fetch_object($query);
152
153 $this->id = $obj->rowid;
154 $this->fk_product_parent = $obj->fk_product_parent;
155 $this->fk_product_child = $obj->fk_product_child;
156 $this->variation_price = $obj->variation_price;
157 $this->variation_price_percentage = $obj->variation_price_percentage;
158 $this->variation_weight = $obj->variation_weight;
159 $this->variation_ref_ext = $obj->variation_ref_ext;
160
161 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
163 }
164
165 return 1;
166 }
167
168
176 public function fetchCombinationPriceLevels($fk_price_level = 0, $useCache = true)
177 {
178 global $conf;
179
180 // Check cache
181 if (!empty($this->combination_price_levels) && $useCache) {
182 if ((!empty($fk_price_level) && isset($this->combination_price_levels[$fk_price_level])) || empty($fk_price_level)) {
183 return 1;
184 }
185 }
186
187 if (!is_array($this->combination_price_levels)
188 || empty($fk_price_level) // if fetch an unique level don't erase all already fetched
189 ) {
190 $this->combination_price_levels = array();
191 }
192
193 $staticProductCombinationLevel = new ProductCombinationLevel($this->db);
194 $combination_price_levels = $staticProductCombinationLevel->fetchAll($this->id, $fk_price_level);
195
196 if (!is_array($combination_price_levels)) {
197 return -1;
198 }
199
200 if (empty($combination_price_levels)) {
204 if ($fk_price_level > 0) {
205 $combination_price_levels[$fk_price_level] = ProductCombinationLevel::createFromParent($this->db, $this, $fk_price_level);
206 } else {
207 $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
208 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
209 $combination_price_levels[$i] = ProductCombinationLevel::createFromParent($this->db, $this, $i);
210 }
211 }
212 }
213
214 $this->combination_price_levels = $combination_price_levels;
215
216 return 1;
217 }
218
225 public function saveCombinationPriceLevels($clean = 1)
226 {
227 global $conf;
228
229 $error = 0;
230
231 $staticProductCombinationLevel = new ProductCombinationLevel($this->db);
232
233 // Delete all
234 if (empty($this->combination_price_levels)) {
235 return $staticProductCombinationLevel->deleteAllForCombination($this->id);
236 }
237
238 // Clean not needed price levels (level higher than number max defined into setup)
239 if ($clean) {
240 $res = $staticProductCombinationLevel->clean($this->id);
241 if ($res < 0) {
242 $this->errors[] = 'Fail to clean not needed price levels';
243 return -1;
244 }
245 }
246
247 foreach ($this->combination_price_levels as $fk_price_level => $combination_price_level) {
248 $res = $combination_price_level->save();
249 if ($res < 1) {
250 $this->error = 'Error saving combination price level '.$fk_price_level.' : '.$combination_price_level->error;
251 $this->errors[] = $this->error;
252 $error++;
253 break;
254 }
255 }
256
257 if ($error) {
258 return $error * -1;
259 } else {
260 return 1;
261 }
262 }
263
271 public function fetchByFkProductChild($productid, $donotloadpricelevel = 0)
272 {
273 global $conf;
274
275 $sql = "SELECT rowid, fk_product_parent, fk_product_child, variation_price, variation_price_percentage, variation_weight";
276 $sql .= " FROM ".MAIN_DB_PREFIX."product_attribute_combination WHERE fk_product_child = ".((int) $productid)." AND entity IN (".getEntity('product').")";
277
278 $query = $this->db->query($sql);
279
280 if (!$query) {
281 return -1;
282 }
283
284 if (!$this->db->num_rows($query)) {
285 return 0;
286 }
287
288 $result = $this->db->fetch_object($query);
289
290 $this->id = $result->rowid;
291 $this->fk_product_parent = $result->fk_product_parent;
292 $this->fk_product_child = $result->fk_product_child;
293 $this->variation_price = $result->variation_price;
294 $this->variation_price_percentage = $result->variation_price_percentage;
295 $this->variation_weight = $result->variation_weight;
296
297 if (empty($donotloadpricelevel) && getDolGlobalString('PRODUIT_MULTIPRICES')) {
299 }
300
301 return (int) $this->fk_product_parent;
302 }
303
311 public function fetchAllByFkProductParent($fk_product_parent, $sort_by_ref = false)
312 {
313 global $conf;
314
315 $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";
316 $sql .= " FROM ".MAIN_DB_PREFIX."product_attribute_combination AS pac";
317 if ($sort_by_ref) {
318 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product AS p ON p.rowid = pac.fk_product_child";
319 }
320 $sql .= " WHERE pac.fk_product_parent = ".((int) $fk_product_parent)." AND pac.entity IN (".getEntity('product').")";
321 if ($sort_by_ref) {
322 $sql .= $this->db->order('p.ref', 'ASC');
323 }
324
325 $query = $this->db->query($sql);
326
327 if (!$query) {
328 return -1;
329 }
330
331 $return = array();
332
333 while ($result = $this->db->fetch_object($query)) {
334 $tmp = new ProductCombination($this->db);
335 $tmp->id = $result->rowid;
336 $tmp->fk_product_parent = $result->fk_product_parent;
337 $tmp->fk_product_child = $result->fk_product_child;
338 $tmp->variation_price = $result->variation_price;
339 $tmp->variation_price_percentage = $result->variation_price_percentage;
340 $tmp->variation_weight = $result->variation_weight;
341 $tmp->variation_ref_ext = $result->variation_ref_ext;
342
343 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
344 $tmp->fetchCombinationPriceLevels();
345 }
346
347 $return[] = $tmp;
348 }
349
350 return $return;
351 }
352
359 public function countNbOfCombinationForFkProductParent($fk_product_parent)
360 {
361 $nb = 0;
362 $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').")";
363
364 $resql = $this->db->query($sql);
365 if ($resql) {
366 $obj = $this->db->fetch_object($resql);
367 if ($obj) {
368 $nb = $obj->nb;
369 }
370 }
371
372 return $nb;
373 }
374
381 public function create($user)
382 {
383 global $conf;
384
385 /* $this->fk_product_child may be empty and will be filled later after subproduct has been created */
386
387 $sql = "INSERT INTO ".MAIN_DB_PREFIX."product_attribute_combination";
388 $sql .= " (fk_product_parent, fk_product_child, variation_price, variation_price_percentage, variation_weight, variation_ref_ext, entity)";
389 $sql .= " VALUES (".((int) $this->fk_product_parent).", ".((int) $this->fk_product_child).",";
390 $sql .= (float) $this->variation_price.", ".(int) $this->variation_price_percentage.",";
391 $sql .= (float) $this->variation_weight.", '".$this->db->escape($this->variation_ref_ext)."', ".(int) $this->entity.")";
392
393 $resql = $this->db->query($sql);
394 if ($resql) {
395 $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.'product_attribute_combination');
396 } else {
397 $this->error = $this->db->lasterror();
398 return -1;
399 }
400
401 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
402 $res = $this->saveCombinationPriceLevels();
403 if ($res < 0) {
404 return -2;
405 }
406 }
407
408 return 1;
409 }
410
417 public function update(User $user)
418 {
419 global $conf;
420
421 $sql = "UPDATE ".MAIN_DB_PREFIX."product_attribute_combination";
422 $sql .= " SET fk_product_parent = ".(int) $this->fk_product_parent.", fk_product_child = ".(int) $this->fk_product_child.",";
423 $sql .= " variation_price = ".(float) $this->variation_price.", variation_price_percentage = ".(int) $this->variation_price_percentage.",";
424 $sql .= " variation_ref_ext = '".$this->db->escape($this->variation_ref_ext)."',";
425 $sql .= " variation_weight = ".(float) $this->variation_weight." WHERE rowid = ".((int) $this->id);
426
427 $resql = $this->db->query($sql);
428 if (!$resql) {
429 return -1;
430 }
431
432 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
433 $res = $this->saveCombinationPriceLevels();
434 if ($res < 0) {
435 return -2;
436 }
437 }
438
439 $parent = new Product($this->db);
440 $parent->fetch($this->fk_product_parent);
441
442 $this->updateProperties($parent, $user);
443
444 return 1;
445 }
446
453 public function delete(User $user)
454 {
455 $this->db->begin();
456
457 $comb2val = new ProductCombination2ValuePair($this->db);
458 $comb2val->deleteByFkCombination($this->id);
459
460 // remove combination price levels
461 if (!$this->db->query("DELETE FROM ".MAIN_DB_PREFIX."product_attribute_combination_price_level WHERE fk_product_attribute_combination = ".(int) $this->id)) {
462 $this->db->rollback();
463 return -1;
464 }
465
466 $sql = "DELETE FROM ".MAIN_DB_PREFIX."product_attribute_combination WHERE rowid = ".(int) $this->id;
467
468 if ($this->db->query($sql)) {
469 $this->db->commit();
470 return 1;
471 }
472
473 $this->db->rollback();
474 return -1;
475 }
476
484 public function deleteByFkProductParent($user, $fk_product_parent)
485 {
486 $this->db->begin();
487
488 foreach ($this->fetchAllByFkProductParent($fk_product_parent) as $prodcomb) {
489 $prodstatic = new Product($this->db);
490
491 $res = $prodstatic->fetch($prodcomb->fk_product_child);
492
493 if ($res > 0) {
494 $res = $prodcomb->delete($user);
495 }
496
497 if ($res > 0 && !$prodstatic->isObjectUsed($prodstatic->id)) {
498 $res = $prodstatic->delete($user);
499 }
500
501 if ($res < 0) {
502 $this->db->rollback();
503 return -1;
504 }
505 }
506
507 $this->db->commit();
508 return 1;
509 }
510
519 public function updateProperties(Product $parent, User $user)
520 {
521 global $conf;
522
523 $this->db->begin();
524
525 $child = new Product($this->db);
526 $child->fetch($this->fk_product_child);
527
528 $child->price_autogen = $parent->price_autogen;
529 $child->weight = $parent->weight;
530 // Only when Parent Status are updated
531 if (is_object($parent->oldcopy) && !$parent->oldcopy->isEmpty() && ($parent->status != $parent->oldcopy->status)) {
532 $child->status = $parent->status;
533 }
534 if (is_object($parent->oldcopy) && !$parent->oldcopy->isEmpty() && ($parent->status_buy != $parent->oldcopy->status_buy)) {
535 $child->status_buy = $parent->status_buy;
536 }
537
538 if ($this->variation_weight) { // If we must add a delta on weight
539 $child->weight = ($child->weight ? $child->weight : 0) + $this->variation_weight;
540 }
541 $child->weight_units = $parent->weight_units;
542
543 // Don't update the child label if the user has already modified it.
544 if ($child->label == $parent->label) {
545 // This will trigger only at variant creation time
546 $varlabel = $this->getCombinationLabel($this->fk_product_child);
547 $child->label = $parent->label.$varlabel;
548 }
549
550
551 if ($child->update($child->id, $user) > 0) {
552 $new_vat = $parent->tva_tx;
553 $new_npr = $parent->tva_npr;
554
555 // MultiPrix
556 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
557 $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
558 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
559 if ($parent->multiprices[$i] != '' || isset($this->combination_price_levels[$i]->variation_price)) {
560 $new_type = empty($parent->multiprices_base_type[$i]) ? 'HT' : $parent->multiprices_base_type[$i];
561 $new_min_price = $parent->multiprices_min[$i];
562 $variation_price = (float) (!isset($this->combination_price_levels[$i]->variation_price) ? $this->variation_price : $this->combination_price_levels[$i]->variation_price);
563 $variation_price_percentage = (float) (!isset($this->combination_price_levels[$i]->variation_price_percentage) ? $this->variation_price_percentage : $this->combination_price_levels[$i]->variation_price_percentage);
564
565 if ($parent->prices_by_qty_list[$i]) {
566 $new_psq = 1;
567 } else {
568 $new_psq = 0;
569 }
570
571 if ($new_type == 'TTC') {
572 $new_price = $parent->multiprices_ttc[$i];
573 } else {
574 $new_price = $parent->multiprices[$i];
575 }
576
577 if ($variation_price_percentage) {
578 if ($new_price != 0) {
579 $new_price *= 1 + ($variation_price / 100);
580 }
581 } else {
582 $new_price += $variation_price;
583 }
584
585 $ret = $child->updatePrice($new_price, $new_type, $user, $new_vat, $new_min_price, $i, $new_npr, $new_psq, 0, array(), $parent->default_vat_code);
586
587 if ($ret < 0) {
588 $this->db->rollback();
589 $this->error = $child->error;
590 $this->errors = $child->errors;
591 return $ret;
592 }
593 }
594 }
595 } else {
596 $new_type = $parent->price_base_type;
597 $new_min_price = $parent->price_min;
598 $new_psq = $parent->price_by_qty;
599
600 if ($new_type == 'TTC') {
601 $new_price = $parent->price_ttc;
602 } else {
603 $new_price = $parent->price;
604 }
605
606 if ($this->variation_price_percentage) {
607 if ($new_price != 0) {
608 $new_price *= 1 + ($this->variation_price / 100);
609 }
610 } else {
611 $new_price += $this->variation_price;
612 }
613
614 $ret = $child->updatePrice($new_price, $new_type, $user, $new_vat, $new_min_price, 1, $new_npr, $new_psq);
615
616 if ($ret < 0) {
617 $this->db->rollback();
618 $this->error = $child->error;
619 $this->errors = $child->errors;
620 return $ret;
621 }
622 }
623
624 $this->db->commit();
625
626 return 1;
627 }
628
629 $this->db->rollback();
630 $this->error = $child->error;
631 $this->errors = $child->errors;
632 return -1;
633 }
634
642 public function fetchByProductCombination2ValuePairs($prodid, array $features)
643 {
644 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
645
646 $actual_comp = array();
647
648 $prodcomb2val = new ProductCombination2ValuePair($this->db);
649 $prodcomb = new ProductCombination($this->db);
650
651 $features = array_filter(
652 $features,
657 static function ($v) {
658 return !empty($v);
659 }
660 );
661
662 foreach ($features as $attr => $attr_val) {
663 $actual_comp[$attr] = $attr_val;
664 }
665
666 foreach ($prodcomb->fetchAllByFkProductParent($prodid) as $prc) {
667 $values = array();
668
669 foreach ($prodcomb2val->fetchByFkCombination($prc->id) as $value) {
670 $values[$value->fk_prod_attr] = $value->fk_prod_attr_val;
671 }
672
673 $check1 = count(array_diff_assoc($values, $actual_comp));
674 $check2 = count(array_diff_assoc($actual_comp, $values));
675
676 if (!$check1 && !$check2) {
677 return $prc;
678 }
679 }
680
681 return false;
682 }
683
692 {
693 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttribute.class.php';
694 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttributeValue.class.php';
695
696 // Attributes
697 // Select all unique attributes of the variants (which are to sell) of a given parent product.
698 $sql = "SELECT DISTINCT c2v.fk_prod_attr, a.position";
699 $sql .= " FROM ".MAIN_DB_PREFIX."product_attribute_combination2val c2v";
700 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product_attribute_combination c";
701 $sql .= " ON c2v.fk_prod_combination = c.rowid";
702 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product p";
703 $sql .= " ON p.rowid = c.fk_product_child";
704 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product_attribute a";
705 $sql .= " ON a.rowid = fk_prod_attr";
706 $sql .= " WHERE c.fk_product_parent = ".((int) $productid);
707 $sql .= " AND p.tosell = 1";
708 $sql .= $this->db->order('a.position', 'asc');
709
710 $resql = $this->db->query($sql);
711
712 // Values
713 $variants = array();
714 while ($obj = $this->db->fetch_object($resql)) {
715 $attr = new ProductAttribute($this->db);
716 $attr->fetch($obj->fk_prod_attr);
717
718 $tmp = new stdClass();
719 $tmp->id = $attr->id;
720 $tmp->ref = $attr->ref;
721 $tmp->label = $attr->label;
722 $tmp->values = array();
723
724 $attrval = new ProductAttributeValue($this->db);
725 // fetch only the used values of this attribute
726 foreach ($attrval->fetchAllByProductAttribute($attr->id, true) as $val) {
727 '@phan-var-force ProductAttributeValue $val';
728 $tmp->values[] = $val;
729 }
730
731 $variants[] = $tmp;
732 }
733
734 return $variants;
735 }
736
760 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 = '')
761 {
762 global $conf;
763
764 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttribute.class.php';
765 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductAttributeValue.class.php';
766
767 $this->db->begin();
768
769 $price_impact = array(1 => 0); // init level price impact
770
771 $forced_refvar = trim((string) $forced_refvar);
772
773 if (!empty($forced_refvar) && $forced_refvar != $product->ref) {
774 $existingProduct = new Product($this->db);
775 $result = $existingProduct->fetch(0, $forced_refvar);
776 if ($result > 0) {
777 $newproduct = $existingProduct;
778 } else {
779 $existingProduct = false;
780 $newproduct = clone $product;
781 $newproduct->ref = $forced_refvar;
782 }
783 } else {
784 $forced_refvar = false;
785 $existingProduct = false;
786 $newproduct = clone $product;
787 }
788
789 //Final weight impact
790 $weight_impact = (float) $forced_weightvar; // If false, return 0
791
792 //Final price impact
793 if (!is_array($forced_pricevar)) {
794 $price_impact[1] = (float) $forced_pricevar; // If false, return 0
795 } else {
796 $price_impact = $forced_pricevar;
797 }
798
799 if (!array($price_var_percent)) {
800 $price_var_percent[1] = (float) $price_var_percent;
801 }
802
803 $newcomb = new ProductCombination($this->db);
804 $existingCombination = $newcomb->fetchByProductCombination2ValuePairs($product->id, $combinations);
805
806 if ($existingCombination) {
807 $newcomb = $existingCombination;
808 } else {
809 $newcomb->fk_product_parent = $product->id;
810
811 // Create 1 entry into product_attribute_combination (1 entry for each combinations). This init also $newcomb->id
812 $result = $newcomb->create($user);
813 if ($result < 0) {
814 $this->error = $newcomb->error;
815 $this->errors = $newcomb->errors;
816 $this->db->rollback();
817 return -1;
818 }
819 }
820
821 $prodattr = new ProductAttribute($this->db);
822 $prodattrval = new ProductAttributeValue($this->db);
823
824 // $combination contains list of attributes pairs key->value. Example: array('id Color'=>id Blue, 'id Size'=>id Small, 'id Option'=>id val a, ...)
825 foreach ($combinations as $currcombattr => $currcombval) {
826 //This was checked earlier, so no need to double check
827 $prodattr->fetch($currcombattr);
828 $prodattrval->fetch($currcombval);
829
830 //If there is an existing combination, there is no need to duplicate the valuepair
831 if (!$existingCombination) {
832 $tmp = new ProductCombination2ValuePair($this->db);
833 $tmp->fk_prod_attr = $currcombattr;
834 $tmp->fk_prod_attr_val = $currcombval;
835 $tmp->fk_prod_combination = $newcomb->id;
836
837 if ($tmp->create($user) < 0) { // Create 1 entry into product_attribute_combination2val
838 $this->error = $tmp->error;
839 $this->errors = $tmp->errors;
840 $this->db->rollback();
841 return -1;
842 }
843 }
844 if ($forced_weightvar === false) {
845 $weight_impact += (float) price2num($variations[$currcombattr][$currcombval]['weight']);
846 }
847 if ($forced_pricevar === false) {
848 $price_impact[1] += (float) price2num($variations[$currcombattr][$currcombval]['price']);
849
850 // Manage Price levels
851 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
852 $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
853 for ($i = 2; $i <= $produit_multiprices_limit; $i++) {
854 $price_impact[$i] += (float) price2num($variations[$currcombattr][$currcombval]['price']);
855 }
856 }
857 }
858
859 if ($forced_refvar === false) {
860 if (isset($conf->global->PRODUIT_ATTRIBUTES_SEPARATOR)) {
861 $newproduct->ref .= getDolGlobalString('PRODUIT_ATTRIBUTES_SEPARATOR') . $prodattrval->ref;
862 } else {
863 $newproduct->ref .= '_'.$prodattrval->ref;
864 }
865 }
866
867 //The first one should not contain a linebreak
868 if ($newproduct->description) {
869 $newproduct->description .= '<br>';
870 }
871 $newproduct->description .= '<strong>'.$prodattr->label.':</strong> '.$prodattrval->value;
872 }
873
874 $newcomb->variation_price_percentage = $price_var_percent[1];
875 $newcomb->variation_price = $price_impact[1];
876 $newcomb->variation_weight = $weight_impact;
877 $newcomb->variation_ref_ext = $this->db->escape($ref_ext);
878
879 // Init price level
880 if (getDolGlobalString('PRODUIT_MULTIPRICES')) {
881 $produit_multiprices_limit = getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
882 for ($i = 1; $i <= $produit_multiprices_limit; $i++) {
883 $productCombinationLevel = new ProductCombinationLevel($this->db);
884 $productCombinationLevel->fk_product_attribute_combination = $newcomb->id;
885 $productCombinationLevel->fk_price_level = $i;
886 $productCombinationLevel->variation_price = $price_impact[$i];
887
888 if (is_array($price_var_percent)) {
889 $productCombinationLevel->variation_price_percentage = (empty($price_var_percent[$i]) ? false : $price_var_percent[$i]);
890 } else {
891 $productCombinationLevel->variation_price_percentage = $price_var_percent;
892 }
893
894 $newcomb->combination_price_levels[$i] = $productCombinationLevel;
895 }
896 }
897 //var_dump($newcomb->combination_price_levels);
898
899 $newproduct->weight += $weight_impact;
900
901 // Now create the product
902 //print 'Create prod '.$newproduct->ref.'<br>'."\n";
903 if ($existingProduct === false) {
904 //To avoid wrong information in price history log
905 $newproduct->price = 0;
906 $newproduct->price_ttc = 0;
907 $newproduct->price_min = 0;
908 $newproduct->price_min_ttc = 0;
909
910 // A new variant must use a new barcode (not same product)
911 $newproduct->barcode = -1;
912 $result = $newproduct->create($user);
913
914 if ($result < 0) {
915 //In case the error is not related with an already existing product
916 if ($newproduct->error != 'ErrorProductAlreadyExists') {
917 $this->error = $newproduct->error;
918 $this->errors = $newproduct->errors;
919 $this->db->rollback();
920 return -1;
921 }
922
928 if ($newcomb->fk_product_child) {
929 $res = $newproduct->fetch($existingCombination->fk_product_child);
930 } else {
931 $orig_prod_ref = $newproduct->ref;
932 $i = 1;
933
934 do {
935 $newproduct->ref = $orig_prod_ref.$i;
936 $res = $newproduct->create($user);
937
938 if ($newproduct->error != 'ErrorProductAlreadyExists') {
939 $this->errors[] = $newproduct->error;
940 break;
941 }
942
943 $i++;
944 } while ($res < 0);
945 }
946
947 if ($res < 0) {
948 $this->db->rollback();
949 return -1;
950 }
951 }
952 } else {
953 $result = $newproduct->update($newproduct->id, $user);
954 if ($result < 0) {
955 $this->db->rollback();
956 return -1;
957 }
958 }
959
960 $newcomb->fk_product_child = $newproduct->id;
961
962 if ($newcomb->update($user) < 0) {
963 $this->error = $newcomb->error;
964 $this->errors = $newcomb->errors;
965 $this->db->rollback();
966 return -1;
967 }
968
969 $this->db->commit();
970 return $newproduct->id;
971 }
972
981 public function copyAll(User $user, $origProductId, Product $destProduct)
982 {
983 require_once DOL_DOCUMENT_ROOT.'/variants/class/ProductCombination2ValuePair.class.php';
984
985 //To prevent a loop
986 if ($origProductId == $destProduct->id) {
987 return -1;
988 }
989
990 $prodcomb2val = new ProductCombination2ValuePair($this->db);
991
992 //Retrieve all product combinations
993 $combinations = $this->fetchAllByFkProductParent($origProductId);
994
995 foreach ($combinations as $combination) {
996 $variations = array();
997
998 foreach ($prodcomb2val->fetchByFkCombination($combination->id) as $tmp_pc2v) {
999 $variations[$tmp_pc2v->fk_prod_attr] = $tmp_pc2v->fk_prod_attr_val;
1000 }
1001
1002 if ($this->createProductCombination(
1003 $user,
1004 $destProduct,
1005 $variations,
1006 array(),
1007 $combination->variation_price_percentage,
1008 $combination->variation_price,
1009 $combination->variation_weight
1010 ) < 0) {
1011 return -1;
1012 }
1013 }
1014
1015 return 1;
1016 }
1017
1023 public function getCombinationLabel($prod_child)
1024 {
1025 $label = '';
1026 $sql = 'SELECT pav.value AS label';
1027 $sql .= ' FROM '.MAIN_DB_PREFIX.'product_attribute_combination pac';
1028 $sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'product_attribute_combination2val pac2v ON pac2v.fk_prod_combination=pac.rowid';
1029 $sql .= ' INNER JOIN '.MAIN_DB_PREFIX.'product_attribute_value pav ON pav.rowid=pac2v.fk_prod_attr_val';
1030 $sql .= ' WHERE pac.fk_product_child='.((int) $prod_child);
1031
1032 $resql = $this->db->query($sql);
1033 if ($resql) {
1034 $num = $this->db->num_rows($resql);
1035
1036 $i = 0;
1037
1038 while ($i < $num) {
1039 $obj = $this->db->fetch_object($resql);
1040
1041 if ($obj->label) {
1042 $label .= ' '.$obj->label;
1043 }
1044 $i++;
1045 }
1046 }
1047 return $label;
1048 }
1049}
1050
1051
1052
1058{
1063 public $db;
1064
1068 public $table_element = 'product_attribute_combination_price_level';
1069
1074 public $id;
1075
1080 public $fk_product_attribute_combination;
1081
1086 public $fk_price_level;
1087
1092 public $variation_price;
1093
1098 public $variation_price_percentage = false;
1099
1103 public $error;
1104
1108 public $errors = array();
1109
1115 public function __construct(DoliDB $db)
1116 {
1117 $this->db = $db;
1118 }
1119
1126 public function fetch($rowid)
1127 {
1128 $sql = "SELECT rowid, fk_product_attribute_combination, fk_price_level, variation_price, variation_price_percentage";
1129 $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element;
1130 $sql .= " WHERE rowid = ".(int) $rowid;
1131
1132 $resql = $this->db->query($sql);
1133 if ($resql) {
1134 $obj = $this->db->fetch_object($resql);
1135 if ($obj) {
1136 return $this->fetchFormObj($obj);
1137 }
1138 }
1139
1140 return -1;
1141 }
1142
1143
1151 public function fetchAll($fk_product_attribute_combination, $fk_price_level = 0)
1152 {
1153 $result = array();
1154
1155 $sql = "SELECT rowid, fk_product_attribute_combination, fk_price_level, variation_price, variation_price_percentage";
1156 $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element;
1157 $sql .= " WHERE fk_product_attribute_combination = ".intval($fk_product_attribute_combination);
1158 if (!empty($fk_price_level)) {
1159 $sql .= ' AND fk_price_level = '.intval($fk_price_level);
1160 }
1161
1162 $res = $this->db->query($sql);
1163 if ($res) {
1164 if ($this->db->num_rows($res) > 0) {
1165 while ($obj = $this->db->fetch_object($res)) {
1166 $productCombinationLevel = new ProductCombinationLevel($this->db);
1167 $productCombinationLevel->fetchFormObj($obj);
1168 $result[$obj->fk_price_level] = $productCombinationLevel;
1169 }
1170 }
1171 } else {
1172 return -1;
1173 }
1174
1175 return $result;
1176 }
1177
1184 public function fetchFormObj($obj)
1185 {
1186 if (!$obj) {
1187 return -1;
1188 }
1189
1190 $this->id = $obj->rowid;
1191 $this->fk_product_attribute_combination = (int) $obj->fk_product_attribute_combination;
1192 $this->fk_price_level = intval($obj->fk_price_level);
1193 $this->variation_price = (float) $obj->variation_price;
1194 $this->variation_price_percentage = (bool) $obj->variation_price_percentage;
1195
1196 return 1;
1197 }
1198
1199
1205 public function save()
1206 {
1207 if (($this->id > 0 && empty($this->fk_product_attribute_combination)) || empty($this->fk_price_level)) {
1208 return -1;
1209 }
1210
1211 // Check if level exist in DB before add
1212 if ($this->fk_product_attribute_combination > 0 && empty($this->id)) {
1213 $sql = "SELECT rowid id";
1214 $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element;
1215 $sql .= " WHERE fk_product_attribute_combination = ".(int) $this->fk_product_attribute_combination;
1216 $sql .= ' AND fk_price_level = '.((int) $this->fk_price_level);
1217
1218 $resql = $this->db->query($sql);
1219 if ($resql) {
1220 $obj = $this->db->fetch_object($resql);
1221 if ($obj) {
1222 $this->id = $obj->id;
1223 }
1224 }
1225 }
1226
1227 // Update
1228 if (!empty($this->id)) {
1229 $sql = 'UPDATE '.MAIN_DB_PREFIX.$this->table_element;
1230 $sql .= ' SET variation_price = '.(float) $this->variation_price.' , variation_price_percentage = '.intval($this->variation_price_percentage);
1231 $sql .= ' WHERE rowid = '.((int) $this->id);
1232
1233 $res = $this->db->query($sql);
1234 if ($res > 0) {
1235 return $this->id;
1236 } else {
1237 $this->error = $this->db->error();
1238 $this->errors[] = $this->error;
1239 return -1;
1240 }
1241 } else {
1242 // Add
1243 $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
1244 $sql .= "fk_product_attribute_combination, fk_price_level, variation_price, variation_price_percentage";
1245 $sql .= ") VALUES (";
1246 $sql .= (int) $this->fk_product_attribute_combination;
1247 $sql .= ", ".intval($this->fk_price_level);
1248 $sql .= ", ".(float) $this->variation_price;
1249 $sql .= ", ".intval($this->variation_price_percentage);
1250 $sql .= ")";
1251
1252 $res = $this->db->query($sql);
1253 if ($res) {
1254 $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
1255 } else {
1256 $this->error = $this->db->error();
1257 $this->errors[] = $this->error;
1258 return -1;
1259 }
1260 }
1261
1262 return $this->id;
1263 }
1264
1265
1271 public function delete()
1272 {
1273 $sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".(int) $this->id;
1274 $res = $this->db->query($sql);
1275
1276 return $res ? 1 : -1;
1277 }
1278
1279
1286 public function deleteAllForCombination($fk_product_attribute_combination)
1287 {
1288 $sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE fk_product_attribute_combination = ".(int) $fk_product_attribute_combination;
1289 $res = $this->db->query($sql);
1290
1291 return $res ? 1 : -1;
1292 }
1293
1294
1301 public function clean($fk_product_attribute_combination)
1302 {
1303 global $conf;
1304
1305 $sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element;
1306 $sql .= " WHERE fk_product_attribute_combination = ".(int) $fk_product_attribute_combination;
1307 $sql .= " AND fk_price_level > ".(int) getDolGlobalString('PRODUIT_MULTIPRICES_LIMIT');
1308 $res = $this->db->query($sql);
1309
1310 return $res ? 1 : -1;
1311 }
1312
1313
1322 public static function createFromParent(DoliDB $db, ProductCombination $productCombination, $fkPriceLevel)
1323 {
1324 $productCombinationLevel = new self($db);
1325 $productCombinationLevel->fk_price_level = $fkPriceLevel;
1326 $productCombinationLevel->fk_product_attribute_combination = $productCombination->id;
1327 $productCombinationLevel->variation_price = $productCombination->variation_price;
1328 $productCombinationLevel->variation_price_percentage = (bool) $productCombination->variation_price_percentage;
1329
1330 return $productCombinationLevel;
1331 }
1332}
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 '.
getDolGlobalString($key, $default='')
Return dolibarr global constant string value.
getEntity($element, $shared=1, $currentobject=null)
Get list of entity id to use.