uc_product_kit.module 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171
  1. <?php
  2. /**
  3. * @file
  4. * The product kit module for Ubercart.
  5. *
  6. * Product kits are groups of products that are sold as a unit.
  7. */
  8. define('UC_PRODUCT_KIT_UNMUTABLE_NO_LIST', -1);
  9. define('UC_PRODUCT_KIT_UNMUTABLE_WITH_LIST', 0);
  10. define('UC_PRODUCT_KIT_MUTABLE', 1);
  11. /**
  12. * Implements hook_form_FORM_ID_alter() for uc_product_settings_form().
  13. */
  14. function uc_product_kit_form_uc_product_settings_form_alter(&$form, &$form_state) {
  15. $form['product_kit'] = array(
  16. '#type' => 'fieldset',
  17. '#title' => 'Product kit settings',
  18. '#group' => 'product-settings',
  19. '#weight' => -5,
  20. );
  21. $form['product_kit']['uc_product_kit_mutable'] = array(
  22. '#type' => 'radios',
  23. '#title' => t('Product kit cart display'),
  24. '#options' => array(
  25. UC_PRODUCT_KIT_UNMUTABLE_NO_LIST => t('As a unit. Customers may only change how many kits they are buying. Do not list component products.'),
  26. UC_PRODUCT_KIT_UNMUTABLE_WITH_LIST => t('As a unit. Customers may only change how many kits they are buying. List component products.'),
  27. UC_PRODUCT_KIT_MUTABLE => t('As individual products. Customers may add or remove kit components at will. Discounts entered below are not applied to the kit price'),
  28. ),
  29. '#default_value' => variable_get('uc_product_kit_mutable', 0),
  30. );
  31. }
  32. /**
  33. * Implements hook_form_FORM_ID_alter() for node_delete_confirm().
  34. */
  35. function uc_product_kit_form_node_delete_confirm_alter(&$form, &$form_state) {
  36. if (uc_product_is_product((integer) $form['nid']['#value'])) {
  37. $kits = db_query("SELECT COUNT(k.nid) FROM {node} n JOIN {uc_product_kits} k ON n.vid = k.vid WHERE k.vid IN (SELECT DISTINCT vid FROM {uc_product_kits} WHERE product_id = :nid) GROUP BY k.nid HAVING COUNT(product_id) = 1", array(':nid' => $form['nid']['#value']))->fetchField();
  38. if ($kits) {
  39. $description = $form['description']['#markup'];
  40. $form['description']['#markup'] = format_plural($kits, 'There is 1 product kit that consists of only this product. It will be deleted as well.', 'There are @count product kits that consist of only this products. They will be deleted as well.') . ' ' . $description;
  41. }
  42. }
  43. }
  44. /**
  45. * Implements hook_uc_form_alter().
  46. *
  47. * Puts a product list on the form, so product kit attributes will work on the
  48. * order admin edit form. See uc_attribute_form_alter().
  49. */
  50. function uc_product_kit_uc_form_alter(&$form, &$form_state, $form_id) {
  51. if ($form_id == 'uc_order_add_product_form') {
  52. if (!isset($form['sub_products'])) {
  53. // We only want product kits.
  54. $kit = $form['node']['#value'];
  55. if ($kit->type !== 'product_kit') {
  56. return;
  57. }
  58. $products = array('#tree' => TRUE);
  59. foreach ($kit->products as $kit_product) {
  60. $products[$kit_product->nid] = array();
  61. }
  62. // Add the products to the beginning of the form for visual aesthetics.
  63. $form = array_merge(array('sub_products' => $products), $form);
  64. }
  65. }
  66. }
  67. /**
  68. * Implements hook_node_info().
  69. *
  70. * @return
  71. * Node type information for product kits.
  72. */
  73. function uc_product_kit_node_info() {
  74. return array(
  75. 'product_kit' => array(
  76. 'name' => t('Product kit'),
  77. 'base' => 'uc_product_kit',
  78. 'description' => t('Use <em>product kits</em> to list two or more products together, presenting a logical and convenient grouping of items to the customer.'),
  79. 'title_label' => t('Name'),
  80. 'body_label' => t('Description'),
  81. ),
  82. );
  83. }
  84. /**
  85. * Implements hook_prepare().
  86. */
  87. function uc_product_kit_prepare($node) {
  88. $defaults = array(
  89. 'mutable' => variable_get('uc_product_kit_mutable', UC_PRODUCT_KIT_UNMUTABLE_WITH_LIST),
  90. 'products' => array(),
  91. 'default_qty' => 1,
  92. 'ordering' => 0,
  93. );
  94. foreach ($defaults as $key => $value) {
  95. if (!isset($node->$key)) {
  96. $node->$key = $value;
  97. }
  98. }
  99. }
  100. /**
  101. * Implements hook_insert().
  102. *
  103. * Adds a row to {uc_products} to make a product. Extra information about the
  104. * component products are stored in {uc_product_kits}.
  105. *
  106. * @param &$node
  107. * The node object being saved.
  108. *
  109. * @see uc_product_insert()
  110. */
  111. function uc_product_kit_insert(&$node) {
  112. $obj = new stdClass();
  113. $obj->vid = $node->vid;
  114. $obj->nid = $node->nid;
  115. $obj->model = '';
  116. $obj->list_price = 0;
  117. $obj->cost = 0;
  118. $obj->sell_price = 0;
  119. $obj->weight = 0;
  120. $obj->weight_units = variable_get('uc_weight_unit', 'lb');
  121. $obj->default_qty = $node->default_qty;
  122. $obj->ordering = $node->ordering;
  123. $obj->shippable = FALSE;
  124. $values = array();
  125. $placeholders = array();
  126. foreach ($node->products as $product) {
  127. if (is_numeric($product)) {
  128. $product = node_load($product);
  129. }
  130. $kit = array(
  131. 'vid' => $node->vid,
  132. 'nid' => $node->nid,
  133. 'product_id' => $product->nid,
  134. 'mutable' => $node->mutable,
  135. 'qty' => 1,
  136. 'synchronized' => 1,
  137. );
  138. drupal_write_record('uc_product_kits', $kit);
  139. $obj->model .= $product->model . ' / ';
  140. $obj->list_price += $product->list_price;
  141. $obj->cost += $product->cost;
  142. $obj->sell_price += $product->sell_price;
  143. $obj->weight += $product->weight * uc_weight_conversion($product->weight_units, $obj->weight_units);
  144. if ($product->shippable) {
  145. $obj->shippable = TRUE;
  146. }
  147. }
  148. $obj->model = rtrim($obj->model, ' / ');
  149. drupal_write_record('uc_products', $obj);
  150. }
  151. /**
  152. * Implements hook_update().
  153. *
  154. * Updates information in {uc_products} as well as {uc_product_kits}. Because
  155. * component products are known when the form is loaded, discount information
  156. * can be input and saved.
  157. *
  158. * @param &$node
  159. * The node to be updated.
  160. *
  161. * @see uc_product_update()
  162. */
  163. function uc_product_kit_update(&$node) {
  164. $obj = new stdClass();
  165. $obj->vid = $node->vid;
  166. $obj->nid = $node->nid;
  167. $obj->model = '';
  168. $obj->list_price = 0;
  169. $obj->cost = 0;
  170. $obj->sell_price = 0;
  171. $obj->weight = 0;
  172. $obj->weight_units = variable_get('uc_weight_unit', 'lb');
  173. $obj->default_qty = $node->default_qty;
  174. $obj->ordering = $node->ordering;
  175. $obj->shippable = FALSE;
  176. if (!isset($node->kit_total) && isset($node->synchronized) && isset($node->sell_price)) {
  177. $override_discounts = !$node->synchronized;
  178. $node->kit_total = $node->sell_price;
  179. }
  180. else {
  181. $override_discounts = isset($node->kit_total) && is_numeric($node->kit_total);
  182. }
  183. $product_count = count($node->products);
  184. // Get the price of all the products without any discounts. This number is
  185. // used if a total kit price was specified to calculate the individual
  186. // product discounts.
  187. if ($override_discounts) {
  188. $base_price = 0;
  189. foreach ($node->products as $nid) {
  190. // Usually, $node is $form_state['values'] cast as an object.
  191. // However, there could be times where node_save() is called with an
  192. // actual product kit node. $node->products is an array of objects and
  193. // $node->items doesn't exist then.
  194. if (is_numeric($nid)) {
  195. $product = node_load($nid, NULL, TRUE);
  196. if (!isset($node->items[$nid]['qty']) || $node->items[$nid]['qty'] === '') {
  197. $node->items[$nid]['qty'] = 1;
  198. }
  199. }
  200. else {
  201. $product = $nid;
  202. $nid = $product->nid;
  203. $node->items[$nid] = (array) $product;
  204. }
  205. $base_price += $product->sell_price * $node->items[$nid]['qty'];
  206. }
  207. }
  208. if (empty($node->revision)) {
  209. db_delete('uc_product_kits')
  210. ->condition('vid', $node->vid)
  211. ->execute();
  212. }
  213. foreach ($node->products as $nid) {
  214. if (is_numeric($nid)) {
  215. $product = node_load($nid);
  216. }
  217. else {
  218. $product = $nid;
  219. $nid = $product->nid;
  220. }
  221. // When a total kit price is specified, calculate the individual product
  222. // discounts needed to reach it, taking into account the product quantities
  223. // and their relative prices. More expensive products should be given a
  224. // proportionally higher discount.
  225. if ($override_discounts) {
  226. // After all the algebra that went into finding this formula, it's
  227. // surprising how simple it is.
  228. $discount = ($node->kit_total - $base_price) * $product->sell_price / $base_price;
  229. }
  230. elseif (isset($node->items[$nid]['discount'])) {
  231. $discount = (float) $node->items[$nid]['discount'];
  232. }
  233. elseif (isset($node->products[$nid]->discount)) {
  234. $discount = $node->products[$nid]->discount;
  235. }
  236. else {
  237. $discount = 0;
  238. }
  239. if (isset($node->items)) {
  240. if (!isset($node->items[$nid]['qty']) || $node->items[$nid]['qty'] === '') {
  241. $node->items[$nid]['qty'] = 1;
  242. }
  243. $product->qty = $node->items[$nid]['qty'];
  244. $product->ordering = isset($node->items[$nid]['ordering']) ? $node->items[$nid]['ordering'] : 0;
  245. }
  246. else {
  247. $product->qty = $node->products[$nid]->qty;
  248. $product->ordering = $node->products[$nid]->ordering;
  249. }
  250. // Discounts are always saved, but they are only applied if the kit can't
  251. // be changed by the customer.
  252. if ($node->mutable != UC_PRODUCT_KIT_MUTABLE) {
  253. $product->sell_price += $discount;
  254. }
  255. $obj->model .= $product->model . ' / ';
  256. $obj->list_price += $product->list_price * $product->qty;
  257. $obj->cost += $product->cost * $product->qty;
  258. $obj->sell_price += $product->sell_price * $product->qty;
  259. $obj->weight += $product->weight * $product->qty * uc_weight_conversion($product->weight_units, $obj->weight_units);
  260. if ($product->shippable) {
  261. $obj->shippable = TRUE;
  262. }
  263. db_insert('uc_product_kits')
  264. ->fields(array(
  265. 'vid' => $node->vid,
  266. 'nid' => $node->nid,
  267. 'product_id' => $nid,
  268. 'mutable' => $node->mutable,
  269. 'qty' => $product->qty,
  270. 'discount' => $discount,
  271. 'ordering' => $product->ordering,
  272. 'synchronized' => $override_discounts ? 0 : 1,
  273. ))
  274. ->execute();
  275. }
  276. $obj->model = rtrim($obj->model, ' / ');
  277. if ($node->mutable == UC_PRODUCT_KIT_MUTABLE && !empty($discount)) {
  278. drupal_set_message(t('Product kit discounts are not applied because the customer can remove components from their cart.'));
  279. }
  280. if (!empty($node->revision)) {
  281. drupal_write_record('uc_products', $obj);
  282. }
  283. else {
  284. db_merge('uc_products')
  285. ->key(array('vid' => $obj->vid))
  286. ->fields(array(
  287. 'model' => $obj->model,
  288. 'list_price' => $obj->list_price,
  289. 'cost' => $obj->cost,
  290. 'sell_price' => $obj->sell_price,
  291. 'weight' => $obj->weight,
  292. 'weight_units' => $obj->weight_units,
  293. 'default_qty' => $obj->default_qty,
  294. 'ordering' => $obj->ordering,
  295. 'shippable' => $obj->shippable ? 1 : 0,
  296. ))
  297. ->execute();
  298. }
  299. // When a kit is updated, remove matching kits from the cart, as there is no
  300. // simple way to handle product addition or removal at this point.
  301. if (module_exists('uc_cart')) {
  302. db_delete('uc_cart_products')
  303. ->condition('data', '%' . db_like('s:6:"kit_id";s:' . strlen($node->nid) . ':"' . $node->nid . '";') . '%', 'LIKE')
  304. ->execute();
  305. }
  306. }
  307. /**
  308. * Implements hook_delete().
  309. */
  310. function uc_product_kit_delete(&$node) {
  311. if (module_exists('uc_cart')) {
  312. db_delete('uc_cart_products')
  313. ->condition('data', '%' . db_like('s:6:"kit_id";s:' . strlen($node->nid) . ':"' . $node->nid . '";') . '%', 'LIKE')
  314. ->execute();
  315. }
  316. db_delete('uc_product_kits')
  317. ->condition('nid', $node->nid)
  318. ->execute();
  319. db_delete('uc_products')
  320. ->condition('nid', $node->nid)
  321. ->execute();
  322. }
  323. /**
  324. * Implements hook_load().
  325. */
  326. function uc_product_kit_load($nodes) {
  327. $vids = array();
  328. foreach ($nodes as $nid => $node) {
  329. $vids[$nid] = $node->vid;
  330. }
  331. $all_products = array();
  332. $result = db_query("SELECT nid, product_id, mutable, qty, discount, ordering, synchronized FROM {uc_product_kits} WHERE vid IN (:vids) ORDER BY nid, ordering", array(':vids' => $vids));
  333. while ($prod = $result->fetchObject()) {
  334. $nodes[$prod->nid]->mutable = $prod->mutable;
  335. $nodes[$prod->nid]->synchronized = $prod->synchronized;
  336. // Add the component information.
  337. $data = array();
  338. if ($prod->mutable != UC_PRODUCT_KIT_MUTABLE) {
  339. $data = array('kit_id' => $prod->nid, 'kit_discount' => $prod->discount);
  340. }
  341. $product = uc_product_load_variant($prod->product_id, $data);
  342. $product->qty = $prod->qty;
  343. $product->discount = $prod->discount;
  344. $product->ordering = $prod->ordering;
  345. // Add product to the kit.
  346. $nodes[$prod->nid]->products[$product->nid] = $product;
  347. }
  348. // Add product data to kits.
  349. uc_product_load($nodes);
  350. }
  351. /**
  352. * Implements hook_module_implements_alter().
  353. *
  354. * Ensure that our component products have their discounts applied before any
  355. * other product alterations are made.
  356. */
  357. function uc_product_kit_module_implements_alter(&$implementations, $hook) {
  358. if ($hook == 'uc_product_alter') {
  359. $group = $implementations['uc_product_kit'];
  360. unset($implementations['uc_product_kit']);
  361. $implementations = array('uc_product_kit' => $group) + $implementations;
  362. }
  363. }
  364. /**
  365. * Implements hook_theme().
  366. */
  367. function uc_product_kit_theme() {
  368. return array(
  369. 'uc_product_kit_items_form' => array(
  370. 'render element' => 'form',
  371. 'file' => 'uc_product_kit.theme.inc',
  372. ),
  373. 'uc_product_kit_add_to_cart' => array(
  374. 'variables' => array('form' => NULL, 'view_mode' => 'full'),
  375. 'file' => 'uc_product_kit.theme.inc',
  376. ),
  377. 'uc_product_kit_list_item' => array(
  378. 'arguments' => array('product' => NULL),
  379. 'file' => 'uc_product_kit.theme.inc',
  380. ),
  381. );
  382. }
  383. /**
  384. * Implements hook_node_update().
  385. *
  386. * Ensures product kit discounts are updated if their component nodes are
  387. * updated or deleted.
  388. */
  389. function uc_product_kit_node_update($node) {
  390. $result = db_query("SELECT DISTINCT nid FROM {uc_product_kits} WHERE product_id = :nid", array(':nid' => $node->nid));
  391. while ($nid = $result->fetchField()) {
  392. $kit = node_load($nid, NULL, TRUE);
  393. node_save($kit);
  394. }
  395. }
  396. /**
  397. * Implements hook_node_delete().
  398. *
  399. * Ensures product kit discounts are updated if their component nodes are
  400. * deleted.
  401. */
  402. function uc_product_kit_node_delete($node) {
  403. $empty = array();
  404. $result = db_query("SELECT DISTINCT nid FROM {uc_product_kits} WHERE product_id = :nid", array(':nid' => $node->nid));
  405. while ($nid = $result->fetchField()) {
  406. $kit = node_load($nid, NULL, TRUE);
  407. unset($kit->products[$node->nid]);
  408. if (empty($kit->products)) {
  409. $empty[] = $kit->nid;
  410. }
  411. else {
  412. node_save($kit);
  413. }
  414. }
  415. if ($empty) {
  416. node_delete_multiple($empty);
  417. }
  418. }
  419. /**
  420. * Implements hook_forms().
  421. *
  422. * Registers an "Add to Cart" form for each product kit.
  423. *
  424. * @see uc_product_kit_add_to_cart_form()
  425. * @see uc_catalog_buy_it_now_form()
  426. */
  427. function uc_product_kit_forms($form_id, $args) {
  428. $forms = array();
  429. if (isset($args[0]) && isset($args[0]->nid) && isset($args[0]->type)) {
  430. $product = $args[0];
  431. if ($product->type == 'product_kit') {
  432. $forms['uc_product_kit_add_to_cart_form_' . $product->nid] = array('callback' => 'uc_product_kit_add_to_cart_form');
  433. $forms['uc_product_add_to_cart_form_' . $product->nid] = array('callback' => 'uc_product_kit_add_to_cart_form');
  434. $forms['uc_catalog_buy_it_now_form_' . $product->nid] = array('callback' => 'uc_product_kit_buy_it_now_form');
  435. }
  436. }
  437. return $forms;
  438. }
  439. /**
  440. * Implements hook_form().
  441. *
  442. * @ingroup forms
  443. */
  444. function uc_product_kit_form(&$node, $form_state) {
  445. $form['title'] = array(
  446. '#type' => 'textfield',
  447. '#title' => t('Name'),
  448. '#required' => TRUE,
  449. '#weight' => -5,
  450. '#default_value' => $node->title,
  451. '#description' => t('Name of the product kit'),
  452. );
  453. // Create an array of products on the site for use in the product selector.
  454. $product_types = uc_product_types();
  455. $products = array();
  456. // Disregard other product kits.
  457. unset($product_types[array_search('product_kit', $product_types)]);
  458. // Query the database and loop through the results.
  459. $products = db_query("SELECT nid, title FROM {node} WHERE type IN (:types) ORDER BY title, nid", array(':types' => $product_types))->fetchAllKeyed();
  460. $form['base'] = array(
  461. '#type' => 'fieldset',
  462. '#title' => t('Product kit information'),
  463. '#collapsible' => TRUE,
  464. '#collapsed' => FALSE,
  465. '#weight' => -10,
  466. '#group' => 'additional_settings',
  467. );
  468. $form['base']['mutable'] = array(
  469. '#type' => 'radios',
  470. '#title' => t('How is this product kit handled by the cart?'),
  471. '#options' => array(
  472. UC_PRODUCT_KIT_UNMUTABLE_NO_LIST => t('As a unit. Customers may only change how many kits they are buying. Do not list component products.'),
  473. UC_PRODUCT_KIT_UNMUTABLE_WITH_LIST => t('As a unit. Customers may only change how many kits they are buying. List component products.'),
  474. UC_PRODUCT_KIT_MUTABLE => t('As individual products. Customers may add or remove kit components at will. Discounts entered below are not applied to the kit price'),
  475. ),
  476. '#default_value' => $node->mutable,
  477. );
  478. $form['base']['products'] = array(
  479. '#type' => 'select',
  480. '#multiple' => TRUE,
  481. '#required' => TRUE,
  482. '#title' => t('Products'),
  483. '#options' => $products,
  484. '#default_value' => array_keys($node->products),
  485. );
  486. $total = 0;
  487. $base_total = 0;
  488. $form['base']['items'] = array(
  489. '#tree' => TRUE,
  490. '#theme' => 'uc_product_kit_items_form',
  491. '#weight' => 1,
  492. '#description' => t('Enter a positive or negative discount to raise or lower the item price by that amount. The change is applied to each item in the kit.'),
  493. );
  494. if (!empty($node->products)) {
  495. foreach ($node->products as $i => $product) {
  496. $form['base']['items'][$i] = array(
  497. '#type' => 'fieldset',
  498. );
  499. $form['base']['items'][$i]['link'] = array(
  500. '#type' => 'item',
  501. '#markup' => l($product->title, 'node/' . $i),
  502. );
  503. $form['base']['items'][$i]['qty'] = array(
  504. '#type' => 'uc_quantity',
  505. '#title' => t('Quantity'),
  506. '#title_display' => 'invisible',
  507. '#default_value' => $product->qty,
  508. );
  509. $form['base']['items'][$i]['ordering'] = array(
  510. '#type' => 'weight',
  511. '#title' => t('List position'),
  512. '#title_display' => 'invisible',
  513. '#default_value' => isset($product->ordering) ? $product->ordering : 0,
  514. '#attributes' => array('class' => array('uc-product-kit-item-ordering')),
  515. );
  516. $form['base']['items'][$i]['discount'] = array(
  517. '#type' => 'textfield',
  518. '#title' => t('Discount'),
  519. '#title_display' => 'invisible',
  520. '#field_prefix' => uc_currency_format($product->sell_price) . ' + ',
  521. '#default_value' => isset($product->discount) ? number_format($product->discount, 3, '.', '') : 0,
  522. '#size' => 5,
  523. );
  524. $total += $product->sell_price * $product->qty;
  525. $base_total += $product->sell_price * $product->qty;
  526. if (isset($product->discount)) {
  527. $total += $product->discount * $product->qty;
  528. }
  529. }
  530. if (!$node->synchronized && $node->sell_price != $total) {
  531. // Component products have changed their prices. Recalculate discounts
  532. // to keep the same total.
  533. $total = $base_total;
  534. foreach ($node->products as $i => $product) {
  535. $discount = ($node->sell_price - $base_total) * $product->sell_price / $base_total;
  536. $total += $discount * $product->qty;
  537. $form['base']['items'][$i]['discount']['#default_value'] = number_format($discount, 3, '.', '');
  538. }
  539. }
  540. $form['base']['kit_total'] = array(
  541. '#type' => 'uc_price',
  542. '#title' => t('Total price'),
  543. '#default_value' => $node->synchronized ? '' : $total,
  544. '#description' => t('If this field is set, the discounts of the individual products will be recalculated to equal this value. Currently, the total sell price is %price.', array('%price' => uc_currency_format($total))),
  545. '#empty_zero' => FALSE,
  546. );
  547. }
  548. if (variable_get('uc_product_add_to_cart_qty', FALSE)) {
  549. $form['base']['default_qty'] = array(
  550. '#type' => 'uc_quantity',
  551. '#title' => t('Default quantity to add to cart'),
  552. '#default_value' => $node->default_qty,
  553. '#description' => t('Use 0 to disable the quantity field next to the add to cart button.'),
  554. '#weight' => 27,
  555. '#allow_zero' => TRUE,
  556. );
  557. }
  558. else {
  559. $form['base']['default_qty'] = array(
  560. '#type' => 'value',
  561. '#value' => $node->default_qty,
  562. );
  563. }
  564. $form['base']['ordering'] = array(
  565. '#type' => 'weight',
  566. '#title' => t('List position'),
  567. '#description' => t("Specify a value to set this product's position in product lists.<br />Products in the same position will be sorted alphabetically."),
  568. '#delta' => 25,
  569. '#default_value' => $node->ordering,
  570. '#weight' => 30,
  571. );
  572. // Disable all shipping related functionality.
  573. $form['shipping']['#access'] = FALSE;
  574. return $form;
  575. }
  576. /**
  577. * Implements hook_view().
  578. */
  579. function uc_product_kit_view($node, $view_mode) {
  580. // Give modules a chance to alter this product. If it is a variant, this
  581. // will have been done already by uc_product_load_variant(), so we check a
  582. // flag to be sure not to alter twice.
  583. $variant = empty($node->variant) ? uc_product_load_variant($node->nid) : $node;
  584. if (module_exists('uc_cart') && empty($variant->data['display_only'])) {
  585. $add_to_cart_form = drupal_get_form('uc_product_kit_add_to_cart_form_' . $variant->nid, clone $variant);
  586. if (variable_get('uc_product_update_node_view', FALSE)) {
  587. $variant = $add_to_cart_form['node']['#value'];
  588. }
  589. }
  590. // Calculate the display price.
  591. $display_price = 0;
  592. $suffixes = array();
  593. if ($node->mutable != UC_PRODUCT_KIT_MUTABLE) {
  594. // If this is a non-mutable kit, then sum the display price of each of the
  595. // component products.
  596. foreach ($variant->products as $product) {
  597. $build = node_view($product, $view_mode);
  598. $display_price += $build['display_price']['#value'] * $product->qty;
  599. $suffixes += $build['display_price']['#suffixes'];
  600. }
  601. }
  602. else {
  603. // For mutable, just use the price.
  604. $display_price = $variant->price;
  605. $suffixes = array();
  606. }
  607. $node->content['display_price'] = array(
  608. '#theme' => 'uc_product_price',
  609. '#value' => $display_price,
  610. '#suffixes' => $suffixes,
  611. '#attributes' => array(
  612. 'class' => array(
  613. 'product-kit',
  614. 'display-price',
  615. ),
  616. ),
  617. );
  618. $node->content['model'] = array(
  619. '#theme' => 'uc_product_model',
  620. '#model' => $variant->model,
  621. '#view_mode' => $view_mode,
  622. );
  623. $node->content['list_price'] = array(
  624. '#theme' => 'uc_product_price',
  625. '#title' => t('List price:'),
  626. '#value' => $variant->list_price,
  627. '#attributes' => array(
  628. 'class' => array(
  629. 'product-kit',
  630. 'list-price',
  631. ),
  632. ),
  633. );
  634. $node->content['cost'] = array(
  635. '#theme' => 'uc_product_price',
  636. '#title' => t('Cost:'),
  637. '#value' => $variant->cost,
  638. '#attributes' => array(
  639. 'class' => array(
  640. 'product-kit',
  641. 'cost',
  642. ),
  643. ),
  644. '#access' => user_access('administer products'),
  645. );
  646. $node->content['sell_price'] = array(
  647. '#theme' => 'uc_product_price',
  648. '#title' => t('Price:'),
  649. '#value' => $variant->sell_price,
  650. '#attributes' => array(
  651. 'class' => array(
  652. 'product-kit',
  653. 'sell-price',
  654. ),
  655. ),
  656. );
  657. $node->content['weight'] = array(
  658. '#theme' => 'uc_product_weight',
  659. '#amount' => $variant->weight,
  660. '#units' => $variant->weight_units,
  661. '#view_mode' => $view_mode,
  662. );
  663. if ($node->mutable != UC_PRODUCT_KIT_UNMUTABLE_NO_LIST) {
  664. $node->content['products'] = array('#weight' => 6);
  665. $i = 0;
  666. foreach ($node->products as $product) {
  667. $node->content['products'][$product->nid]['qty'] = array(
  668. '#markup' => '<div class="product-qty">' . theme('uc_product_kit_list_item', array('product' => $product)) . '</div>',
  669. );
  670. $node->content['products'][$product->nid]['#weight'] = $i++;
  671. }
  672. }
  673. if (isset($add_to_cart_form)) {
  674. $node->content['add_to_cart'] = array(
  675. '#theme' => 'uc_product_kit_add_to_cart',
  676. '#view_mode' => $view_mode,
  677. '#form' => $add_to_cart_form,
  678. );
  679. }
  680. $node->content['#node'] = $variant;
  681. return $node;
  682. }
  683. /**
  684. * Lets the cart know how many of which products are included in a kit.
  685. *
  686. * uc_attribute_form_alter() hooks into this form to add attributes to each
  687. * element in $form['products'].
  688. *
  689. * @see uc_product_kit_add_to_cart_form_validate()
  690. * @see uc_product_kit_add_to_cart_form_submit()
  691. *
  692. * @ingroup forms
  693. */
  694. function uc_product_kit_add_to_cart_form($form, &$form_state, $node) {
  695. $form['nid'] = array('#type' => 'value', '#value' => $node->nid);
  696. $form['products'] = array('#tree' => TRUE);
  697. foreach ($node->products as $i => $product) {
  698. $form['products'][$i] = array('#title' => check_plain($product->title));
  699. $form['products'][$i]['nid'] = array('#type' => 'hidden', '#value' => $product->nid);
  700. $form['products'][$i]['qty'] = array('#type' => 'hidden', '#value' => $product->qty);
  701. }
  702. if ($node->default_qty > 0 && variable_get('uc_product_add_to_cart_qty', FALSE)) {
  703. $form['qty'] = array(
  704. '#type' => 'uc_quantity',
  705. '#title' => t('Quantity'),
  706. '#default_value' => $node->default_qty,
  707. );
  708. }
  709. else {
  710. $form['qty'] = array('#type' => 'hidden', '#value' => $node->default_qty ? $node->default_qty : 1);
  711. }
  712. $form['actions'] = array('#type' => 'actions');
  713. $form['actions']['submit'] = array(
  714. '#type' => 'submit',
  715. '#value' => t('Add to cart'),
  716. '#id' => 'edit-submit-' . $node->nid,
  717. '#attributes' => array(
  718. 'class' => array('node-add-to-cart'),
  719. ),
  720. );
  721. $form['node'] = array(
  722. '#type' => 'value',
  723. '#value' => isset($form_state['storage']['variant']) ? $form_state['storage']['variant'] : $node,
  724. );
  725. uc_form_alter($form, $form_state, __FUNCTION__);
  726. return $form;
  727. }
  728. /**
  729. * Form validation handler for uc_product_add_to_cart_form().
  730. *
  731. * @see uc_product_kit_add_to_cart_form()
  732. * @see uc_product_add_to_cart_form_validate()
  733. */
  734. function uc_product_kit_add_to_cart_form_validate($form, &$form_state) {
  735. uc_product_add_to_cart_form_validate($form, $form_state);
  736. foreach ($form_state['storage']['variant']->products as &$product) {
  737. $data = module_invoke_all('uc_add_to_cart_data', $form_state['values']['products'][$product->nid]);
  738. $data += $product->data;
  739. $qty = $product->qty;
  740. $product = uc_product_load_variant($product->nid, $data);
  741. $product->qty = $qty;
  742. }
  743. }
  744. /**
  745. * Adds each product kit's component to the cart in the correct quantities.
  746. *
  747. * @see uc_product_kit_add_to_cart_form()
  748. */
  749. function uc_product_kit_add_to_cart_form_submit($form, &$form_state) {
  750. if (variable_get('uc_cart_add_item_msg', TRUE)) {
  751. $node = node_load($form_state['values']['nid']);
  752. drupal_set_message(t('<strong>@product-title</strong> added to <a href="!url">your shopping cart</a>.', array('@product-title' => $node->title, '!url' => url('cart'))));
  753. }
  754. $form_state['redirect'] = uc_cart_add_item($form_state['values']['nid'], $form_state['values']['qty'], $form_state['values']);
  755. }
  756. /**
  757. * Add-to-cart button with any extra fields.
  758. *
  759. * @see uc_product_kit_buy_it_now_form_validate()
  760. * @see uc_product_kit_buy_it_now_form_submit()
  761. *
  762. * @ingroup forms
  763. */
  764. function uc_product_kit_buy_it_now_form($form, &$form_state, $node) {
  765. $form['nid'] = array(
  766. '#type' => 'hidden',
  767. '#value' => $node->nid,
  768. );
  769. if ($node->type == 'product_kit') {
  770. $form['products'] = array('#tree' => TRUE);
  771. foreach ($node->products as $i => $product) {
  772. $form['products'][$i] = array('#title' => check_plain($product->title));
  773. $form['products'][$i]['nid'] = array('#type' => 'hidden', '#value' => $product->nid);
  774. $form['products'][$i]['qty'] = array('#type' => 'hidden', '#value' => $product->qty);
  775. }
  776. }
  777. $form['actions'] = array('#type' => 'actions');
  778. $form['actions']['submit'] = array(
  779. '#type' => 'submit',
  780. '#value' => t('Add to cart'),
  781. '#id' => 'edit-submit-' . $node->nid,
  782. '#attributes' => array(
  783. 'class' => array('list-add-to-cart'),
  784. ),
  785. );
  786. uc_form_alter($form, $form_state, __FUNCTION__);
  787. return $form;
  788. }
  789. /**
  790. * Redirects to the product kit page so attributes may be selected.
  791. *
  792. * @see uc_product_kit_buy_it_now_form()
  793. */
  794. function uc_product_kit_buy_it_now_form_validate($form, &$form_state) {
  795. if (module_exists('uc_attribute')) {
  796. $node = node_load($form_state['values']['nid']);
  797. if (is_array($node->products)) {
  798. foreach ($node->products as $nid => $product) {
  799. $attributes = uc_product_get_attributes($nid);
  800. if (!empty($attributes)) {
  801. drupal_set_message(t('This product has options that need to be selected before purchase. Please select them in the form below.'), 'error');
  802. drupal_goto('node/' . $form_state['values']['nid']);
  803. }
  804. }
  805. }
  806. }
  807. }
  808. /**
  809. * Form submission handler for uc_product_kit_buy_it_now_form().
  810. *
  811. * @see uc_product_kit_buy_it_now_form()
  812. */
  813. function uc_product_kit_buy_it_now_form_submit($form, &$form_state) {
  814. $node = node_load($form_state['values']['nid']);
  815. if (module_exists('uc_attribute')) {
  816. $attributes = uc_product_get_attributes($node->nid);
  817. if (!empty($attributes)) {
  818. drupal_set_message(t('This product has options that need to be selected before purchase. Please select them in the form below.'), 'error');
  819. $form_state['redirect'] = drupal_get_path_alias('node/' . $form_state['values']['nid']);
  820. return;
  821. }
  822. if (is_array($node->products)) {
  823. foreach ($node->products as $nid => $product) {
  824. $attributes = uc_product_get_attributes($nid);
  825. if (!empty($attributes)) {
  826. drupal_set_message(t('This product has options that need to be selected before purchase. Please select them in the form below.'), 'error');
  827. $form_state['redirect'] = drupal_get_path_alias('node/' . $form_state['values']['nid']);
  828. return;
  829. }
  830. }
  831. }
  832. }
  833. $form_state['redirect'] = uc_cart_add_item($form_state['values']['nid'], 1, $form_state['values'], NULL, variable_get('uc_cart_add_item_msg', TRUE));
  834. }
  835. /**
  836. * Implements hook_uc_product_types().
  837. */
  838. function uc_product_kit_uc_product_types() {
  839. return array('product_kit');
  840. }
  841. /**
  842. * Implements hook_uc_store_status().
  843. */
  844. function uc_product_kit_uc_store_status() {
  845. if (module_exists('filefield')) {
  846. // Check for filefields on products.
  847. if ($field = variable_get('uc_image_product_kit', '')) {
  848. $instances = content_field_instance_read(array('field_name' => $field, 'type_name' => 'product_kit'));
  849. }
  850. else {
  851. $instances = array();
  852. }
  853. if (!count($instances)) {
  854. return array(array('status' => 'warning', 'title' => t('Images'), 'desc' => t('Product kits do not have an image field. You may add a %field_name at the <a href="!add_url">Add field page</a> and make sure it is set as the Ubercart image in the <a href="!edit_url">content type settings</a> under the Ubercart product settings fieldset.', array('%field_name' => $field, '!add_url' => url('admin/structure/types/manage/product-kit/fields'), '!edit_url' => url('admin/structure/types/manage/product-kit')))));
  855. }
  856. }
  857. }
  858. /**
  859. * Implements hook_uc_add_to_cart().
  860. */
  861. function uc_product_kit_uc_add_to_cart($nid, $qty, $kit_data) {
  862. $node = node_load($nid);
  863. if ($node->type == 'product_kit') {
  864. $cart = uc_cart_get_contents();
  865. $unique = uniqid('', TRUE);
  866. $update = array();
  867. $product_data = array();
  868. foreach ($node->products as $product) {
  869. $data = array('kit_id' => $node->nid, 'module' => 'uc_product_kit') + module_invoke_all('uc_add_to_cart_data', $kit_data['products'][$product->nid]);
  870. $product_data[$product->nid] = $data;
  871. foreach ($cart as $item) {
  872. if ($item->nid == $product->nid && isset($item->data['kit_id']) && $item->data['kit_id'] == $node->nid) {
  873. // There is something in the cart like the product kit. Update
  874. // by default, but check that it's possible.
  875. $data['unique_id'] = $item->data['unique_id'];
  876. if ($item->data == $data) {
  877. // This product is a candidate for updating the cart quantity.
  878. // Make sure the data arrays will compare as equal when serialized.
  879. $product_data[$product->nid] = $item->data;
  880. $update[$product->nid] = TRUE;
  881. }
  882. }
  883. }
  884. }
  885. // The product kit can update its items only if they all can be updated.
  886. if (count($update) != count($node->products)) {
  887. foreach ($node->products as $product) {
  888. $data = $product_data[$product->nid];
  889. $data['unique_id'] = $unique;
  890. uc_cart_add_item($product->nid, $product->qty * $qty, $data, NULL, FALSE, FALSE, FALSE);
  891. }
  892. }
  893. else {
  894. foreach ($node->products as $product) {
  895. $data = $product_data[$product->nid];
  896. uc_cart_add_item($product->nid, $product->qty * $qty, $data, NULL, FALSE, FALSE, FALSE);
  897. }
  898. }
  899. // Rebuild the cart items cache.
  900. uc_cart_get_contents(NULL, 'rebuild');
  901. return array(array('success' => FALSE, 'silent' => TRUE, 'message' => ''));
  902. }
  903. }
  904. /**
  905. * Implements hook_uc_product_alter().
  906. */
  907. function uc_product_kit_uc_product_alter(&$variant) {
  908. if (isset($variant->data['kit_id'])) {
  909. // If this is a kit component load, we would cause infinite recursion trying
  910. // to node_load() the parent, but we already have the discount available.
  911. if (isset($variant->data['kit_discount'])) {
  912. $discount = $variant->data['kit_discount'];
  913. }
  914. elseif (($kit = node_load($variant->data['kit_id'])) && $kit->mutable != UC_PRODUCT_KIT_MUTABLE) {
  915. $discount = $kit->products[$variant->nid]->discount;
  916. }
  917. else {
  918. $discount = 0;
  919. }
  920. $variant->price += $discount;
  921. $variant->data['module'] = 'uc_product_kit';
  922. }
  923. }
  924. /**
  925. * Implements hook_uc_order_product_alter().
  926. *
  927. * The hookups for making product kits work on the order edit admin screen.
  928. *
  929. * @param $product
  930. * The order product being saved.
  931. * @param $order
  932. * The order being edited.
  933. */
  934. function uc_product_kit_uc_order_product_alter(&$product, $order) {
  935. if (empty($product->type) || $product->type !== 'product_kit') {
  936. return;
  937. }
  938. // Have to save each individual product if this is a kit.
  939. foreach ($product->products as $kit_product) {
  940. $qty = $kit_product->qty * $product->qty;
  941. $data = isset($kit_product->data) ? $kit_product->data : array();
  942. $data += module_invoke_all('uc_add_to_cart_data', $_POST['product_controls']['sub_products'][$kit_product->nid]);
  943. $data['shippable'] = $product->shippable;
  944. $kit_product = uc_product_load_variant($kit_product->nid, $data);
  945. $kit_product->qty = $qty;
  946. drupal_alter('uc_order_product', $kit_product, $order);
  947. // Save the individual item to the order.
  948. uc_order_product_save($order->order_id, $kit_product);
  949. }
  950. // Don't save the base kit node, though.
  951. $product->skip_save = TRUE;
  952. }
  953. /**
  954. * Implements hook_uc_cart_display().
  955. *
  956. * Displays either the kit as a whole, or each individual product based on the
  957. * store configuration. Each product in the cart that was added by
  958. * uc_product_kit was also given a unique kit id in order to help prevent
  959. * collisions. The side effect is that identical product kits are listed
  960. * separately if added separately. The customer may still change the quantity
  961. * of kits like other products.
  962. *
  963. * @param $item
  964. * An item in the shopping cart.
  965. *
  966. * @return
  967. * A form element array to be processed by uc_cart_view_form().
  968. */
  969. function uc_product_kit_uc_cart_display($item) {
  970. static $elements = array();
  971. static $products;
  972. $unique_id = $item->data['unique_id'];
  973. $kit = node_load($item->data['kit_id']);
  974. if ($kit->mutable == UC_PRODUCT_KIT_MUTABLE) {
  975. return uc_product_uc_cart_display($item);
  976. }
  977. else {
  978. if (!isset($products[$unique_id])) {
  979. // Initialize table row.
  980. $kit_qty = $item->qty / $kit->products[$item->nid]->qty;
  981. $element = array();
  982. $element['nid'] = array('#type' => 'value', '#value' => $kit->nid);
  983. $element['module'] = array('#type' => 'value', '#value' => 'uc_product_kit');
  984. $element['remove'] = array('#type' => 'submit', '#value' => t('Remove'));
  985. $element['title'] = array('#markup' => l($kit->title, 'node/' . $kit->nid));
  986. $element['qty'] = array(
  987. '#type' => 'uc_quantity',
  988. '#title' => t('Quantity'),
  989. '#title_display' => 'invisible',
  990. '#default_value' => $kit_qty,
  991. );
  992. $element['description'] = array('#markup' => '');
  993. $element['#total'] = 0;
  994. $element['#suffixes'] = array();
  995. $element['#extra'] = array();
  996. $element['#entity'] = $kit; // Override the entity associated with this
  997. // render-array to be the kit itself.
  998. $elements[$unique_id] = $element;
  999. }
  1000. // Add product specific information.
  1001. $extra = uc_product_get_description($item);
  1002. if ($kit->mutable == UC_PRODUCT_KIT_UNMUTABLE_WITH_LIST) {
  1003. $elements[$unique_id]['#extra'][] = array(
  1004. 'data' => theme('uc_product_kit_list_item', array('product' => $item)) . $extra,
  1005. 'class' => array('kit-component-cart-desc'),
  1006. );
  1007. }
  1008. // Build the kit item product variant.
  1009. if (!isset($item->type)) {
  1010. $node = node_load($item->nid);
  1011. $item->type = $node->type;
  1012. }
  1013. $build = node_view($item);
  1014. $elements[$unique_id]['#total'] += $build['display_price']['#value'] * $item->qty;
  1015. $elements[$unique_id]['#suffixes'] += $build['display_price']['#suffixes'];
  1016. $elements[$unique_id]['data'][$item->nid] = $item;
  1017. $products[$unique_id][] = $item->nid;
  1018. // Check if all products in this kit have been accounted for.
  1019. $done = TRUE;
  1020. foreach ($kit->products as $product) {
  1021. if (!in_array($product->nid, $products[$unique_id])) {
  1022. $done = FALSE;
  1023. break;
  1024. }
  1025. }
  1026. if ($done) {
  1027. drupal_add_css(drupal_get_path('module', 'uc_product_kit') . '/uc_product_kit.css');
  1028. $elements[$unique_id]['data'] = array('#type' => 'value', '#value' => serialize($elements[$unique_id]['data']));
  1029. if ($kit->mutable == UC_PRODUCT_KIT_UNMUTABLE_WITH_LIST) {
  1030. $elements[$unique_id]['description']['#markup'] .= theme('item_list', array('items' => $elements[$unique_id]['#extra'], 'attributes' => array('class' => array('product-description'))));
  1031. }
  1032. $element = $elements[$unique_id];
  1033. unset($products[$unique_id]);
  1034. unset($elements[$unique_id]);
  1035. return $element;
  1036. }
  1037. }
  1038. return array();
  1039. }
  1040. /**
  1041. * Implements hook_uc_update_cart_item().
  1042. *
  1043. * Handles individual products or entire kits.
  1044. */
  1045. function uc_product_kit_uc_update_cart_item($nid, $data = array(), $qty, $cid = NULL) {
  1046. if (!$nid) {
  1047. return NULL;
  1048. }
  1049. $cid = !(is_null($cid) || empty($cid)) ? $cid : uc_cart_get_id();
  1050. if (isset($data['kit_id'])) {
  1051. // Product was listed individually.
  1052. uc_product_uc_update_cart_item($nid, $data, $qty, $cid);
  1053. }
  1054. else {
  1055. $kit = node_load($nid);
  1056. foreach ($data as $p_nid => $product) {
  1057. uc_product_uc_update_cart_item($p_nid, $product->data, $qty * $kit->products[$p_nid]->qty, $cid);
  1058. }
  1059. }
  1060. }
  1061. /**
  1062. * Implements hook_views_api().
  1063. */
  1064. function uc_product_kit_views_api() {
  1065. return array(
  1066. 'api' => 3,
  1067. 'path' => drupal_get_path('module', 'uc_product_kit') . '/views',
  1068. );
  1069. }