ProductCombination.class.php 36 KB

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