ProductCombination.class.php 36 KB

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