ProductCombination.class.php 35 KB

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