uc_attribute.module 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568
  1. <?php
  2. /**
  3. * @file
  4. * Ubercart Attribute module.
  5. *
  6. * Allows customers to buy slightly different products from the same listing.
  7. *
  8. * Many manufacturers provide options to their products. This module provides
  9. * a way for store administrators to consolidate these options into one product
  10. * instead of listing each combination separately.
  11. */
  12. /**
  13. * Implements hook_help().
  14. */
  15. function uc_attribute_help($path, $arg) {
  16. switch ($path) {
  17. // Help messages for the attributes overview on products and classes.
  18. case 'node/%/edit/attributes':
  19. return '<p>' . t('Add attributes to this product using the <a href="!url">add attributes form</a>. You may then adjust the settings for these attributes on this page and go on to configure their options in the <em>Options</em> tab.', array('!url' => url('node/' . $arg[1] . '/edit/attributes/add'))) . '</p>';
  20. case 'admin/store/products/classes/%/attributes':
  21. return '<p>' . t('Add attributes to the product class using the <a href="!url">add attributes form</a>. You may then adjust the settings for these attributes on this page and go on to configure their options in the <em>Options</em> tab.', array('!url' => url('admin/store/products/classes/' . $arg[4] . '/attributes/add'))) . '</p>';
  22. // Help message for adding an attribute to a product or class.
  23. case 'node/%/edit/attributes/add':
  24. case 'admin/store/products/classes/%/attributes/add':
  25. return '<p>' . t('Select the attributes you want to add and submit the form.') . '</p>';
  26. // Help message for adjusting the options on a product or class.
  27. case 'node/%/edit/options':
  28. case 'admin/store/products/classes/%/options':
  29. return '<p>' . t('Use the checkboxes to enable options for attributes and the radio buttons to specify the default option. Attributes with no enabled options will be displayed as text fields. Drag and drop the options to reorder them.') . '</p><p>' . t('The cost, price and weight fields will make adjustments against the original product, so you may enter positive or negative amounts here, or enter 0 if the option should make no adjustment.') . '</p>';
  30. // Help message for the product Adjustments tab.
  31. case 'node/%/edit/adjustments':
  32. return '<p>' . t('Enter an alternate SKU to be used when the specified set of options are chosen and the product is added to the cart.') . '</p><p>' . t('<b>Warning:</b> Adding or removing attributes from this product will reset all the SKUs on this page to the default product SKU.') . '</p>';
  33. }
  34. }
  35. /**
  36. * Implements hook_menu().
  37. */
  38. function uc_attribute_menu() {
  39. $items['admin/store/products/attributes'] = array(
  40. 'title' => 'Attributes',
  41. 'description' => 'Create and edit product attributes and options.',
  42. 'page callback' => 'uc_attribute_admin',
  43. 'access arguments' => array('administer attributes'),
  44. 'weight' => -8,
  45. 'file' => 'uc_attribute.admin.inc',
  46. );
  47. $items['admin/store/products/attributes/add'] = array(
  48. 'title' => 'Add an attribute',
  49. 'description' => 'Add a product attribute.',
  50. 'page callback' => 'drupal_get_form',
  51. 'page arguments' => array('uc_attribute_form'),
  52. 'access arguments' => array('administer attributes'),
  53. 'type' => MENU_LOCAL_ACTION,
  54. 'file' => 'uc_attribute.admin.inc',
  55. );
  56. $items['admin/store/products/attributes/%uc_attribute/edit'] = array(
  57. 'title' => 'Edit attribute',
  58. 'description' => 'Edit a product attribute.',
  59. 'page callback' => 'drupal_get_form',
  60. 'page arguments' => array('uc_attribute_form', 4),
  61. 'access arguments' => array('administer attributes'),
  62. 'file' => 'uc_attribute.admin.inc',
  63. );
  64. $items['admin/store/products/attributes/%uc_attribute/delete'] = array(
  65. 'title' => 'Delete attribute',
  66. 'description' => 'Delete a product attribute.',
  67. 'page callback' => 'drupal_get_form',
  68. 'page arguments' => array('uc_attribute_delete_confirm', 4),
  69. 'access arguments' => array('administer attributes'),
  70. 'file' => 'uc_attribute.admin.inc',
  71. );
  72. $items['admin/store/products/attributes/%uc_attribute/options'] = array(
  73. 'title' => 'Options',
  74. 'description' => "Edit a product attribute's options.",
  75. 'page callback' => 'drupal_get_form',
  76. 'page arguments' => array('uc_attribute_options_form', 4),
  77. 'access arguments' => array('administer attributes'),
  78. 'file' => 'uc_attribute.admin.inc',
  79. );
  80. $items['admin/store/products/attributes/%uc_attribute/options/add'] = array(
  81. 'title' => 'Add an option',
  82. 'description' => 'Add a product attribute option.',
  83. 'page callback' => 'drupal_get_form',
  84. 'page arguments' => array('uc_attribute_option_form', 4, NULL),
  85. 'access arguments' => array('administer attributes'),
  86. 'type' => MENU_LOCAL_ACTION,
  87. 'file' => 'uc_attribute.admin.inc',
  88. );
  89. $items['admin/store/products/attributes/%uc_attribute/options/%uc_attribute_option/edit'] = array(
  90. 'title' => 'Edit option',
  91. 'description' => 'Edit a product attribute option.',
  92. 'page callback' => 'drupal_get_form',
  93. 'page arguments' => array('uc_attribute_option_form', 4, 6),
  94. 'access arguments' => array('administer attributes'),
  95. 'file' => 'uc_attribute.admin.inc',
  96. );
  97. $items['admin/store/products/attributes/%uc_attribute/options/%uc_attribute_option/delete'] = array(
  98. 'title' => 'Delete option',
  99. 'description' => 'Delete a product attribute option.',
  100. 'page callback' => 'drupal_get_form',
  101. 'page arguments' => array('uc_attribute_option_delete_confirm', 4, 6),
  102. 'access arguments' => array('administer attributes'),
  103. 'file' => 'uc_attribute.admin.inc',
  104. );
  105. // Menu items for default product class attributes and options.
  106. $items['admin/store/products/classes/%uc_product_class/attributes'] = array(
  107. 'title' => 'Attributes',
  108. 'description' => 'Administer product class attributes.',
  109. 'page callback' => 'drupal_get_form',
  110. 'page arguments' => array('uc_object_attributes_form', 4, 'class'),
  111. 'access callback' => 'uc_attribute_product_class_access',
  112. 'type' => MENU_LOCAL_TASK,
  113. 'weight' => 1,
  114. 'file' => 'uc_attribute.admin.inc',
  115. );
  116. $items['admin/store/products/classes/%uc_product_class/options'] = array(
  117. 'title' => 'Options',
  118. 'description' => 'Administer product class options.',
  119. 'page callback' => 'drupal_get_form',
  120. 'page arguments' => array('uc_object_options_form', 4, 'class'),
  121. 'access callback' => 'uc_attribute_product_class_access',
  122. 'type' => MENU_LOCAL_TASK,
  123. 'weight' => 2,
  124. 'file' => 'uc_attribute.admin.inc',
  125. );
  126. // Insert subitems into the edit node page for product types.
  127. $items['node/%node/edit/attributes'] = array(
  128. 'title' => 'Attributes',
  129. 'description' => 'Edit product attributes.',
  130. 'page callback' => 'drupal_get_form',
  131. 'page arguments' => array('uc_object_attributes_form', 1, 'product', 'overview'),
  132. 'access callback' => 'uc_attribute_product_access',
  133. 'access arguments' => array(1),
  134. 'type' => MENU_LOCAL_TASK,
  135. 'weight' => 1,
  136. 'file' => 'uc_attribute.admin.inc',
  137. );
  138. $items['node/%node/edit/attributes/add'] = array(
  139. 'title' => 'Add an attribute',
  140. 'description' => 'Add an attribute to this product.',
  141. 'page callback' => 'drupal_get_form',
  142. 'page arguments' => array('uc_object_attributes_form', 1, 'product', 'add'),
  143. 'access callback' => 'uc_attribute_product_access',
  144. 'access arguments' => array(1),
  145. 'type' => MENU_LOCAL_ACTION,
  146. 'weight' => 1,
  147. 'file' => 'uc_attribute.admin.inc',
  148. );
  149. $items['node/%node/edit/options'] = array(
  150. 'title' => 'Options',
  151. 'description' => 'Administer product attribute options.',
  152. 'page callback' => 'drupal_get_form',
  153. 'page arguments' => array('uc_object_options_form', 1, 'product'),
  154. 'access callback' => 'uc_attribute_product_option_access',
  155. 'access arguments' => array(1),
  156. 'type' => MENU_LOCAL_TASK,
  157. 'weight' => 2,
  158. 'file' => 'uc_attribute.admin.inc',
  159. );
  160. $items['node/%node/edit/adjustments'] = array(
  161. 'title' => 'Adjustments',
  162. 'description' => 'Administer SKU adjustments for different variants of this product.',
  163. 'page callback' => 'drupal_get_form',
  164. 'page arguments' => array('uc_product_adjustments_form', 1),
  165. 'access callback' => 'uc_attribute_product_option_access',
  166. 'access arguments' => array(1),
  167. 'type' => MENU_LOCAL_TASK,
  168. 'weight' => 3,
  169. 'file' => 'uc_attribute.admin.inc',
  170. );
  171. return $items;
  172. }
  173. /**
  174. * Implements hook_admin_paths().
  175. */
  176. function uc_attribute_admin_paths() {
  177. $paths = array(
  178. 'node/*/edit/attributes' => TRUE,
  179. 'node/*/edit/attributes/add' => TRUE,
  180. 'node/*/edit/options' => TRUE,
  181. 'node/*/edit/adjustments' => TRUE,
  182. );
  183. return $paths;
  184. }
  185. /**
  186. * Access callback for editing a product class's attributes and options.
  187. */
  188. function uc_attribute_product_class_access() {
  189. return user_access('administer product classes') && user_access('administer attributes');
  190. }
  191. /**
  192. * Access callback for editing a product's attributes.
  193. */
  194. function uc_attribute_product_access($node) {
  195. if ($node->type == 'product_kit') {
  196. return FALSE;
  197. }
  198. return uc_product_is_product($node) && node_access('update', $node) && (user_access('administer attributes') || user_access('administer product attributes'));
  199. }
  200. /**
  201. * Access callback for editing a product's options.
  202. */
  203. function uc_attribute_product_option_access($node) {
  204. if ($node->type == 'product_kit') {
  205. return FALSE;
  206. }
  207. return uc_product_is_product($node) && isset($node->attributes) && node_access('update', $node) && (user_access('administer attributes') || user_access('administer product attributes') || user_access('administer product options'));
  208. }
  209. /**
  210. * Implements hook_permission().
  211. */
  212. function uc_attribute_permission() {
  213. return array(
  214. 'administer attributes' => array(
  215. 'title' => t('Administer attributes'),
  216. ),
  217. 'administer product attributes' => array(
  218. 'title' => t('Administer product attributes'),
  219. ),
  220. 'administer product options' => array(
  221. 'title' => t('Administer product options'),
  222. ),
  223. );
  224. }
  225. /**
  226. * Implements hook_theme().
  227. */
  228. function uc_attribute_theme() {
  229. return array(
  230. 'uc_attribute_option' => array(
  231. 'variables' => array('option' => '', 'price' => ''),
  232. 'file' => 'uc_attribute.theme.inc',
  233. ),
  234. 'uc_attribute_add_to_cart' => array(
  235. 'render element' => 'form',
  236. 'file' => 'uc_attribute.theme.inc',
  237. ),
  238. 'uc_object_attributes_form' => array(
  239. 'render element' => 'form',
  240. 'file' => 'uc_attribute.admin.inc',
  241. ),
  242. 'uc_object_options_form' => array(
  243. 'render element' => 'form',
  244. 'file' => 'uc_attribute.admin.inc',
  245. ),
  246. 'uc_attribute_options_form' => array(
  247. 'render element' => 'form',
  248. 'file' => 'uc_attribute.admin.inc',
  249. ),
  250. 'uc_product_attributes' => array(
  251. 'render element' => 'attributes',
  252. 'file' => 'uc_attribute.admin.inc',
  253. ),
  254. );
  255. }
  256. /**
  257. * Implements hook_form_FORM_ID_alter() for uc_product_settings_form().
  258. */
  259. function uc_attribute_form_uc_product_settings_form_alter(&$form, &$form_state) {
  260. $form['attributes'] = array(
  261. '#type' => 'fieldset',
  262. '#title' => 'Attribute settings',
  263. '#group' => 'product-settings',
  264. '#weight' => -3,
  265. );
  266. $form['attributes']['uc_attribute_option_price_format'] = array(
  267. '#type' => 'radios',
  268. '#title' => t('Option price format'),
  269. '#default_value' => variable_get('uc_attribute_option_price_format', 'adjustment'),
  270. '#options' => array(
  271. 'none' => t('Do not display'),
  272. 'adjustment' => t('Display price adjustment'),
  273. 'total' => t('Display total price'),
  274. ),
  275. '#description' => t('Determines how price variations are displayed to the customer. Prices may be displayed directly next to each attribute option in the attribute selection form either as a total price for the product with that option or as an adjustment (+ or -) showing how that option affects the product base price. Note that the price will always be displayed as an adjustment for attributes that can have multiple options (using checkboxes).'),
  276. );
  277. }
  278. /**
  279. * Implements hook_module_implements_alter().
  280. *
  281. * Ensures that attribute form changes are made after (e.g.) product kits.
  282. */
  283. function uc_attribute_module_implements_alter(&$implementations, $hook) {
  284. if ($hook == 'uc_form_alter') {
  285. $group = $implementations['uc_attribute'];
  286. unset($implementations['uc_attribute']);
  287. $implementations['uc_attribute'] = $group;
  288. }
  289. }
  290. /**
  291. * Implements hook_uc_form_alter().
  292. *
  293. * Attaches option selectors to the form with the "Add to Cart" button.
  294. *
  295. * This function also handles selecting attributes for products added to orders
  296. * manually.
  297. */
  298. function uc_attribute_uc_form_alter(&$form, &$form_state, $form_id) {
  299. if (strpos($form_id, 'add_to_cart_form') || $form_id == 'uc_order_add_product_form') {
  300. $use_ajax = strpos($form_id, 'add_to_cart_form') && variable_get('uc_product_update_node_view', FALSE);
  301. $node =& $form['node']['#value'];
  302. $id = $form_id . '-' . $node->nid . '-attributes';
  303. // If the node has a product list, add attributes to them.
  304. if (isset($form['products']) || isset($form['sub_products'])) {
  305. if (isset($form['products'])) {
  306. $element = &$form['products'];
  307. }
  308. else {
  309. $element = &$form['sub_products'];
  310. }
  311. foreach (element_children($element) as $key) {
  312. $element[$key]['attributes'] = _uc_attribute_alter_form($id . '-' . $key, $node->products[$key], $use_ajax);
  313. if (is_array($element[$key]['attributes'])) {
  314. $element[$key]['attributes']['#tree'] = TRUE;
  315. $element[$key]['#type'] = 'fieldset';
  316. }
  317. }
  318. }
  319. // If not, add attributes to the node.
  320. else {
  321. $form['attributes'] = _uc_attribute_alter_form($id, $node, $use_ajax);
  322. if (is_array($form['attributes'])) {
  323. $form['attributes']['#tree'] = TRUE;
  324. $form['attributes']['#weight'] = -1;
  325. }
  326. }
  327. }
  328. }
  329. /**
  330. * Implements hook_node_load().
  331. */
  332. function uc_attribute_node_load($nodes, $types) {
  333. $product_types = array_intersect(uc_product_types(), $types);
  334. if (empty($product_types)) {
  335. return;
  336. }
  337. foreach ($nodes as &$node) {
  338. if (uc_product_is_product($node->type)) {
  339. $attributes = uc_product_get_attributes($node->nid);
  340. if (is_array($attributes) && !empty($attributes)) {
  341. $node->attributes = $attributes;
  342. }
  343. }
  344. }
  345. }
  346. /**
  347. * Implements hook_node_insert().
  348. */
  349. function uc_attribute_node_insert($node) {
  350. // Set attributes from class attributes.
  351. $select = db_select('uc_class_attributes', 'ca')
  352. ->fields('ca', array(
  353. 'aid',
  354. 'label',
  355. 'ordering',
  356. 'required',
  357. 'display',
  358. 'default_option',
  359. ))
  360. ->condition('pcid', $node->type);
  361. // SELECT $node->nid AS nid.
  362. $select->addExpression(':nid', 'nid', array(':nid' => $node->nid));
  363. db_insert('uc_product_attributes')
  364. ->from($select)
  365. ->execute();
  366. // Set options from class options.
  367. $select = db_select('uc_class_attribute_options', 'co')
  368. ->fields('co', array(
  369. 'oid',
  370. 'cost',
  371. 'price',
  372. 'weight',
  373. 'ordering',
  374. ))
  375. ->condition('pcid', $node->type);
  376. $select->addExpression(':nid', 'nid', array(':nid' => $node->nid));
  377. db_insert('uc_product_options')
  378. ->from($select)
  379. ->execute();
  380. }
  381. /**
  382. * Implements hook_node_delete().
  383. */
  384. function uc_attribute_node_delete($node) {
  385. db_delete('uc_product_options')
  386. ->condition('nid', $node->nid)
  387. ->execute();
  388. db_delete('uc_product_adjustments')
  389. ->condition('nid', $node->nid)
  390. ->execute();
  391. db_delete('uc_product_attributes')
  392. ->condition('nid', $node->nid)
  393. ->execute();
  394. }
  395. /**
  396. * Implements hook_node_update_index().
  397. */
  398. function uc_attribute_node_update_index($node) {
  399. $output = '';
  400. $attributes = uc_product_get_attributes($node->nid);
  401. foreach ($attributes as $attribute) {
  402. $output .= '<h3>' . _uc_attribute_get_name($attribute) . '</h3>';
  403. foreach ($attribute->options as $option) {
  404. $output .= $option->name . ' ';
  405. }
  406. $output .= "\n";
  407. }
  408. $result = db_query("SELECT model FROM {uc_product_adjustments} WHERE nid = :nid", array(':nid' => $node->nid));
  409. while ($model = $result->fetchField()) {
  410. $output .= '<h2>' . $model . "</h2>\n";
  411. }
  412. return $output;
  413. }
  414. /**
  415. * Implements hook_uc_product_models().
  416. *
  417. * @param int $nid
  418. * Node number for product type node.
  419. *
  420. * @return array
  421. * Array of SKUs (model numbers) for this $nid.
  422. */
  423. function uc_attribute_uc_product_models($nid) {
  424. // Get all the SKUs for all the attributes on this node.
  425. $models = db_query("SELECT DISTINCT model FROM {uc_product_adjustments} WHERE nid = :nid", array(':nid' => $nid))->fetchCol();
  426. return $models;
  427. }
  428. /**
  429. * Stores the customer's choices in the cart.
  430. */
  431. function uc_attribute_uc_add_to_cart_data($form_values) {
  432. if (isset($form_values['attributes'])) {
  433. return array('attributes' => $form_values['attributes']);
  434. }
  435. else {
  436. return array('attributes' => array());
  437. }
  438. }
  439. /**
  440. * Implements hook_uc_order_product_alter().
  441. */
  442. function uc_attribute_uc_order_product_alter(&$product, $order) {
  443. // Convert the attribute and option ids to their current names. This
  444. // preserves the important data in case the attributes or options are
  445. // changed later.
  446. if (!empty($product->data['attributes'])) {
  447. $attributes_keys = array_keys($product->data['attributes']);
  448. if (is_numeric(array_shift($attributes_keys))) {
  449. $attributes = array();
  450. $options = _uc_cart_product_get_options($product);
  451. foreach ($options as $aid => $option) {
  452. $attributes[$option['attribute']][$option['oid']] = $option['name'];
  453. }
  454. $product->data['attributes'] = $attributes;
  455. }
  456. }
  457. }
  458. /**
  459. * Implements hook_uc_product_class().
  460. */
  461. function uc_attribute_uc_product_class($type, $op) {
  462. switch ($op) {
  463. case 'delete':
  464. db_delete('uc_class_attributes')
  465. ->condition('pcid', $type)
  466. ->execute();
  467. db_delete('uc_class_attribute_options')
  468. ->condition('pcid', $type)
  469. ->execute();
  470. break;
  471. }
  472. }
  473. /**
  474. * Implements hook_uc_product_alter().
  475. */
  476. function uc_attribute_uc_product_alter(&$node) {
  477. if (isset($node->data['attributes']) && is_array($node->data['attributes'])) {
  478. $options = _uc_cart_product_get_options($node);
  479. foreach ($options as $option) {
  480. $node->cost += $option['cost'];
  481. $node->price += $option['price'];
  482. $node->weight += $option['weight'];
  483. }
  484. $combination = array();
  485. foreach ($node->data['attributes'] as $aid => $value) {
  486. if (is_numeric($value)) {
  487. $attribute = uc_attribute_load($aid, $node->nid, 'product');
  488. if ($attribute && ($attribute->display == 1 || $attribute->display == 2)) {
  489. $combination[$aid] = $value;
  490. }
  491. }
  492. }
  493. ksort($combination);
  494. $model = db_query("SELECT model FROM {uc_product_adjustments} WHERE nid = :nid AND combination LIKE :combo", array(':nid' => $node->nid, ':combo' => serialize($combination)))->fetchField();
  495. if (!empty($model)) {
  496. $node->model = $model;
  497. }
  498. }
  499. }
  500. /**
  501. * Implements hook_uc_product_description().
  502. */
  503. function uc_attribute_uc_product_description($product) {
  504. $description = array(
  505. 'attributes' => array(
  506. '#product' => array(
  507. '#type' => 'value',
  508. '#value' => $product,
  509. ),
  510. '#theme' => 'uc_product_attributes',
  511. '#weight' => 1,
  512. ),
  513. );
  514. $desc =& $description['attributes'];
  515. // Cart version of the product has numeric attribute => option values so we
  516. // need to retrieve the right ones.
  517. $weight = 0;
  518. if (empty($product->order_id)) {
  519. foreach (_uc_cart_product_get_options($product) as $option) {
  520. if (!isset($desc[$option['aid']])) {
  521. $desc[$option['aid']]['#attribute_name'] = $option['attribute'];
  522. $desc[$option['aid']]['#options'] = array($option['name']);
  523. }
  524. else {
  525. $desc[$option['aid']]['#options'][] = $option['name'];
  526. }
  527. $desc[$option['aid']]['#weight'] = $weight++;
  528. }
  529. }
  530. elseif (isset($product->data['attributes'])) {
  531. foreach ($product->data['attributes'] as $attribute => $option) {
  532. $desc[] = array(
  533. '#attribute_name' => $attribute,
  534. '#options' => $option,
  535. '#weight' => $weight++,
  536. );
  537. }
  538. }
  539. return $description;
  540. }
  541. /**
  542. * Loads attribute objects from the database.
  543. *
  544. * @todo If we feel it necessary, we could optimize this, by inverting the
  545. * logic; that is, we could make uc_attribute load call this function and allow
  546. * this function to minimize the number of queries necessary. -cha0s
  547. *
  548. * @param array $aids
  549. * Attribute IDs to load.
  550. * @param string $type
  551. * The type of attribute. 'product', or 'class'. Any other type will fetch
  552. * a base attribute.
  553. * @param int $id
  554. * The ID of the product/class this attribute belongs to.
  555. *
  556. * @return array
  557. * An array of loaded attributes.
  558. */
  559. function uc_attribute_load_multiple(array $aids = array(), $type = '', $id = NULL) {
  560. $sql = uc_attribute_type_info($type);
  561. // Product/class attributes.
  562. if (!empty($type)) {
  563. // Seems like a big query to get attribute IDs, but it's all about the sort.
  564. // (I'm not sure if the default ordering is propagating down correctly here.
  565. // It appears that product/class attributes with no ordering won't let the
  566. // attribute's propagate down, as it does when loading. -cha0s)
  567. $query = db_select($sql['attr_table'], 'uca')
  568. ->fields('uca', array('aid'))
  569. ->condition("uca.{$sql['id']}", $id);
  570. $query->leftJoin('uc_attributes', 'ua', 'uca.aid = ua.aid');
  571. $query->orderBy('uca.ordering')
  572. ->orderBy('ua.name');
  573. }
  574. // Base attributes.
  575. else {
  576. $query = db_select('uc_attributes', 'ua')
  577. ->fields('ua', array('aid'))
  578. ->orderBy('ordering')
  579. ->orderBy('name');
  580. }
  581. // Filter by the attribute IDs requested.
  582. if (!empty($aids)) {
  583. // Sanity check - filter out non-numeric attribute IDs.
  584. $aids = array_filter($aids, 'is_numeric');
  585. $query->condition('ua.aid', $aids, 'IN');
  586. }
  587. $aids = $query->execute()->fetchCol();
  588. // Load the attributes.
  589. $attributes = array();
  590. foreach ($aids as $aid) {
  591. $attributes[$aid] = uc_attribute_load($aid, $id, $type);
  592. }
  593. return $attributes;
  594. }
  595. /**
  596. * Loads an attribute from the database.
  597. *
  598. * @param int $aid
  599. * The ID of the attribute.
  600. * @param int $id
  601. * The ID of the product/class this attribute belongs to.
  602. * @param string $type
  603. * The type of attribute. 'product', or 'class'. Any other type will fetch
  604. * a base attribute.
  605. *
  606. * @return object|false
  607. * The attribute object, or FALSE if it doesn't exist.
  608. */
  609. function uc_attribute_load($aid, $id = NULL, $type = '') {
  610. $sql = uc_attribute_type_info($type);
  611. switch ($type) {
  612. case 'product':
  613. case 'class':
  614. // Read attribute data.
  615. $query = db_select('uc_attributes', 'a')
  616. ->fields('a', array('aid', 'name', 'description'))
  617. ->condition('a.aid', $aid);
  618. $query->leftJoin($sql['attr_table'], 'pa', "a.aid = pa.aid AND pa.{$sql['id']} = :id", array(':id' => $id));
  619. $query->fields('pa', array('label', 'default_option', 'required', 'ordering', 'display', $sql['id']));
  620. $query->addField('a', 'label', 'default_label');
  621. $query->addField('a', 'ordering', 'default_ordering');
  622. $query->addField('a', 'required', 'default_required');
  623. $query->addField('a', 'display', 'default_display');
  624. $attribute = $query->execute()->fetchObject();
  625. // Don't try to build it further if it failed already.
  626. if (!$attribute) {
  627. return FALSE;
  628. }
  629. // Set any missing defaults.
  630. foreach (array('ordering', 'required', 'display', 'label') as $field) {
  631. if (isset($attribute->{"default_$field"}) && is_null($attribute->$field)) {
  632. $attribute->$field = $attribute->{"default_$field"};
  633. }
  634. }
  635. if (empty($attribute->label)) {
  636. $attribute->label = $attribute->name;
  637. }
  638. // Read option data.
  639. $query = db_select($sql['opt_table'], 'po')
  640. ->fields('po', array($sql['id'], 'oid', 'cost', 'price', 'weight', 'ordering'));
  641. $query->leftJoin('uc_attribute_options', 'ao', "po.oid = ao.oid AND po.{$sql['id']} = :id", array(':id' => $id));
  642. $query->fields('ao', array('name', 'aid'))
  643. ->condition('aid', $aid)
  644. ->orderBy('po.ordering')
  645. ->orderBy('ao.name');
  646. $result = $query->execute();
  647. break;
  648. default:
  649. // Read attribute and option data.
  650. $attribute = db_query("SELECT * FROM {uc_attributes} WHERE aid = :aid", array(':aid' => $aid))->fetchObject();
  651. $result = db_query("SELECT * FROM {uc_attribute_options} WHERE aid = :aid ORDER BY ordering, name", array(':aid' => $aid));
  652. // Don't try to build it further if it failed already.
  653. if (!$attribute) {
  654. return FALSE;
  655. }
  656. break;
  657. }
  658. // Got an attribute?
  659. if ($attribute) {
  660. // Get its options, too.
  661. $attribute->options = array();
  662. foreach ($result as $option) {
  663. $attribute->options[$option->oid] = $option;
  664. }
  665. uc_attribute_translate($attribute);
  666. }
  667. return $attribute;
  668. }
  669. /**
  670. * Fetches an array of attribute objects that belong to a product.
  671. *
  672. * @param int $nid
  673. * Product whose attributes to load.
  674. *
  675. * @return object[]
  676. * The array of attribute objects.
  677. */
  678. function uc_attribute_load_product_attributes($nid) {
  679. return uc_attribute_load_multiple(array(), 'product', $nid);
  680. }
  681. /**
  682. * Saves an attribute object to the database.
  683. *
  684. * @param object $attribute
  685. * The attribute object to save.
  686. *
  687. * @return int
  688. * The integer result from drupal_write_record().
  689. */
  690. function uc_attribute_save(&$attribute) {
  691. // Insert or update?
  692. $key = empty($attribute->aid) ? array() : 'aid';
  693. return drupal_write_record('uc_attributes', $attribute, $key);
  694. }
  695. /**
  696. * Deletes an attribute from the database.
  697. *
  698. * @param int $aid
  699. * Attribute ID to delete.
  700. *
  701. * @return int
  702. * The Drupal SAVED_DELETED flag.
  703. */
  704. function uc_attribute_delete($aid) {
  705. // Delete the class attributes and their options.
  706. uc_attribute_subject_delete($aid, 'class');
  707. // Delete the product attributes and their options.
  708. uc_attribute_subject_delete($aid, 'product');
  709. // Delete base attributes and their options.
  710. db_delete('uc_attribute_options')
  711. ->condition('aid', $aid)
  712. ->execute();
  713. db_delete('uc_attributes')
  714. ->condition('aid', $aid)
  715. ->execute();
  716. return SAVED_DELETED;
  717. }
  718. /**
  719. * Loads an attribute option from the database.
  720. *
  721. * @param int $oid
  722. * Option ID to load.
  723. *
  724. * @return object
  725. * The attribute option object.
  726. */
  727. function uc_attribute_option_load($oid) {
  728. return db_query("SELECT * FROM {uc_attribute_options} WHERE oid = :oid", array(':oid' => $oid))->fetchObject();
  729. }
  730. /**
  731. * Saves an attribute object to the database.
  732. *
  733. * @param object $option
  734. * The attribute option object to save.
  735. *
  736. * @return int
  737. * The integer result from drupal_write_record().
  738. */
  739. function uc_attribute_option_save(&$option) {
  740. // Insert or update?
  741. $key = empty($option->oid) ? array() : 'oid';
  742. return drupal_write_record('uc_attribute_options', $option, $key);
  743. }
  744. /**
  745. * Deletes an attribute option from the database.
  746. *
  747. * @param int $oid
  748. * Option ID to delete.
  749. *
  750. * @return int
  751. * The Drupal SAVED_DELETED flag.
  752. */
  753. function uc_attribute_option_delete($oid) {
  754. // Delete the class attribute options.
  755. uc_attribute_subject_option_delete($oid, 'class');
  756. // Delete the product attribute options (and the adjustments!).
  757. uc_attribute_subject_option_delete($oid, 'product');
  758. // Delete base attributes and their options.
  759. db_delete('uc_attribute_options')
  760. ->condition('oid', $oid)
  761. ->execute();
  762. return SAVED_DELETED;
  763. }
  764. /**
  765. * Saves a product/class attribute.
  766. *
  767. * @param &$attribute
  768. * The product/class attribute.
  769. * @param string $type
  770. * Is this a product or a class?
  771. * @param string $id
  772. * The product/class ID.
  773. * @param bool $save_options
  774. * Save the product/class attribute's options, too?
  775. *
  776. * @return int
  777. * The integer result from drupal_write_record().
  778. */
  779. function uc_attribute_subject_save(&$attribute, $type, $id, $save_options = FALSE) {
  780. $sql = uc_attribute_type_info($type);
  781. // Insert or update?
  782. $key = uc_attribute_subject_exists($attribute->aid, $type, $id) ? array('aid', $sql['id']) : array();
  783. // First, save the options. First because if this is an insert, we'll set
  784. // a default option for the product/class attribute.
  785. if ($save_options && is_array($attribute->options)) {
  786. foreach ($attribute->options as $option) {
  787. // Sanity check!
  788. $option = (object) $option;
  789. uc_attribute_subject_option_save($option, $type, $id);
  790. }
  791. // Is this an insert? If so, we'll set the default option.
  792. if (empty($key)) {
  793. $default_option = 0;
  794. // Make the first option (if any) the default.
  795. if (!empty($attribute->options) && is_array($attribute->options)) {
  796. $option = (object) reset($attribute->options);
  797. $default_option = $option->oid;
  798. }
  799. $attribute->default_option = $default_option;
  800. }
  801. }
  802. // Merge in the product/class attribute's ID and save.
  803. $attribute->{$type == 'product' ? 'nid' : 'pcid'} = $id;
  804. $result = drupal_write_record($sql['attr_table'], $attribute, $key);
  805. return $result;
  806. }
  807. /**
  808. * Deletes an attribute and all options associated with it.
  809. *
  810. * @param int $aid
  811. * The base attribute ID.
  812. * @param string $type
  813. * Is this a product or a class?
  814. * @param $id
  815. * The product/class ID.
  816. *
  817. * @return int
  818. * The Drupal SAVED_DELETED flag.
  819. */
  820. function uc_attribute_subject_delete($aid, $type, $id = NULL) {
  821. $sql = uc_attribute_type_info($type);
  822. $query = db_select('uc_attribute_options', 'a')
  823. ->fields('a', array('oid'));
  824. $query->join($sql['opt_table'], 'subject', 'a.oid = subject.oid');
  825. // Base conditions, and an ID check if necessary.
  826. $conditions = db_and()
  827. ->condition('aid', $aid);
  828. if ($id) {
  829. $conditions->condition($sql['id'], $id);
  830. }
  831. $query->condition($conditions);
  832. $result = $query->execute();
  833. while ($oid = $result->fetchField()) {
  834. // Don't delete the adjustments one at a time. We'll do it in bulk soon for
  835. // efficiency.
  836. uc_attribute_subject_option_delete($oid, $type, $id, FALSE);
  837. }
  838. db_delete($sql['attr_table'])
  839. ->condition($conditions)
  840. ->execute();
  841. // If this is a product attribute, wipe any associated adjustments.
  842. if ($type == 'product') {
  843. uc_attribute_adjustments_delete(array(
  844. 'aid' => $aid,
  845. 'nid' => $id,
  846. ));
  847. }
  848. return SAVED_DELETED;
  849. }
  850. /**
  851. * Loads a product/class attribute option.
  852. *
  853. * @param int $oid
  854. * The product/class attribute option ID.
  855. * @param string $type
  856. * Is this a product or a class?
  857. * @param int $id
  858. * The product/class ID.
  859. *
  860. * @return object
  861. * An object containing the product/class attribute option.
  862. */
  863. function uc_attribute_subject_option_load($oid, $type, $id) {
  864. $sql = uc_attribute_type_info($type);
  865. $query = db_select($sql['opt_table'], 'po');
  866. $query->leftJoin('uc_attribute_options', 'ao', 'po.oid = ao.oid');
  867. $query->fields('po', array($sql['id'], 'oid', 'cost', 'price', 'weight', 'ordering'))
  868. ->fields('ao', array('name', 'aid'))
  869. ->condition('po.oid', $oid)
  870. ->condition("po.{$sql['id']}", $id)
  871. ->orderBy('po.ordering')
  872. ->orderBy('ao.name');
  873. return $query->execute()->fetchObject();
  874. }
  875. /**
  876. * Saves a product/class attribute option.
  877. *
  878. * @param &$option
  879. * The product/class attribute option.
  880. * @param string $type
  881. * Is this a product or a class?
  882. * @param int $id
  883. * The product/class ID.
  884. *
  885. * @return int
  886. * The integer result from drupal_write_record().
  887. */
  888. function uc_attribute_subject_option_save(&$option, $type, $id) {
  889. $sql = uc_attribute_type_info($type);
  890. // Insert or update?
  891. $key = uc_attribute_subject_option_exists($option->oid, $type, $id) ? array('oid', $sql['id']) : array();
  892. // Merge in the product/class attribute option's ID, and save.
  893. $option->{$type == 'product' ? 'nid' : 'pcid'} = $id;
  894. $result = drupal_write_record($sql['opt_table'], $option, $key);
  895. return $result;
  896. }
  897. /**
  898. * Deletes a product/class attribute option.
  899. *
  900. * @param $oid
  901. * The base attribute's option ID.
  902. * @param string $type
  903. * Is this a product or a class?
  904. * @param int $id
  905. * The product/class ID.
  906. *
  907. * @return int
  908. * The Drupal SAVED_DELETED flag.
  909. */
  910. function uc_attribute_subject_option_delete($oid, $type, $id = NULL, $adjustments = TRUE) {
  911. $sql = uc_attribute_type_info($type);
  912. // Delete the option.
  913. $query = db_delete($sql['opt_table'])
  914. ->condition('oid', $oid);
  915. // Base conditions, and an ID check if necessary.
  916. if ($id) {
  917. $query->condition($sql['id'], $id);
  918. }
  919. $query->execute();
  920. // If this is a product, clean up the associated adjustments.
  921. if ($adjustments && $type == 'product') {
  922. uc_attribute_adjustments_delete(array(
  923. 'aid' => uc_attribute_option_load($oid)->aid,
  924. 'oid' => $oid,
  925. 'nid' => $id,
  926. ));
  927. }
  928. return SAVED_DELETED;
  929. }
  930. /**
  931. * Deletes an attribute adjustment.
  932. *
  933. * @param array $fields
  934. * Fields used to build a condition to delete adjustments against. Fields
  935. * currently handled are 'aid', 'oid', and 'nid'.
  936. *
  937. * @return int
  938. * The Drupal SAVED_DELETED flag.
  939. */
  940. function uc_attribute_adjustments_delete(array $fields) {
  941. // Build the serialized string to match against adjustments.
  942. $match = '';
  943. if (!empty($fields['aid'])) {
  944. $match .= serialize((integer) $fields['aid']);
  945. }
  946. if (!empty($fields['oid'])) {
  947. $match .= serialize((string) $fields['oid']);
  948. }
  949. // Assemble the conditions and args for the SQL.
  950. $query = db_delete('uc_product_adjustments');
  951. // If we have to match aid or oid...
  952. if ($match) {
  953. $query->condition('combination', '%' . db_like($match) . '%', 'LIKE');
  954. }
  955. // If we've got a node ID to match.
  956. if (!empty($fields['nid'])) {
  957. $query->condition('nid', $fields['nid']);
  958. }
  959. // Delete what's necessary.
  960. if ($query->conditions()) {
  961. $query->execute();
  962. }
  963. return SAVED_DELETED;
  964. }
  965. /**
  966. * Checks if a product/class attribute exists.
  967. *
  968. * @param int $aid
  969. * The base attribute ID.
  970. * @param string $type
  971. * Is this a product or a class?
  972. * @param int $id
  973. * The product/class attribute's ID.
  974. *
  975. * @return bool
  976. * TRUE if the attribute exists.
  977. */
  978. function uc_attribute_subject_exists($aid, $type, $id) {
  979. $sql = uc_attribute_type_info($type);
  980. $query = db_select($sql['attr_table'], 'a')
  981. ->fields('a', array('aid'))
  982. ->condition('aid', $aid)
  983. ->condition($sql['id'], $id);
  984. return FALSE !== $query->execute()->fetchField();
  985. }
  986. /**
  987. * Checks if a product/class attribute option exists.
  988. *
  989. * @param int $oid
  990. * The base attribute option ID.
  991. * @param int $id
  992. * The product/class attribute option's ID.
  993. * @param string $type
  994. * Is this a product or a class?
  995. *
  996. * @return bool
  997. * TRUE if the attribute option exists.
  998. */
  999. function uc_attribute_subject_option_exists($oid, $type, $id) {
  1000. $sql = uc_attribute_type_info($type);
  1001. $query = db_select($sql['opt_table'], 'o')
  1002. ->fields('o', array('oid'))
  1003. ->condition('oid', $oid)
  1004. ->condition($sql['id'], $id);
  1005. return FALSE !== $query->execute()->fetchField();
  1006. }
  1007. /**
  1008. * Returns a list of names to abstract queries between products and classes.
  1009. *
  1010. * @param string $type
  1011. * Is this a product or a class?
  1012. *
  1013. * @return array
  1014. * Array of information helpful for creating SQL queries dealing
  1015. * with attributes.
  1016. */
  1017. function uc_attribute_type_info($type) {
  1018. switch ($type) {
  1019. case 'product':
  1020. return array(
  1021. 'attr_table' => 'uc_product_attributes',
  1022. 'opt_table' => 'uc_product_options',
  1023. 'id' => 'nid',
  1024. );
  1025. case 'class':
  1026. return array(
  1027. 'attr_table' => 'uc_class_attributes',
  1028. 'opt_table' => 'uc_class_attribute_options',
  1029. 'id' => 'pcid',
  1030. );
  1031. }
  1032. }
  1033. /**
  1034. * Loads all attributes associated with a product node.
  1035. *
  1036. * @param int $nid
  1037. * The product node id.
  1038. *
  1039. * @return array
  1040. * The attributes.
  1041. */
  1042. function uc_product_get_attributes($nid) {
  1043. $attributes = array();
  1044. $result = db_query("SELECT upa.aid FROM {uc_product_attributes} upa LEFT JOIN {uc_attributes} ua ON upa.aid = ua.aid WHERE upa.nid = :nid ORDER BY upa.ordering, ua.name", array(':nid' => $nid));
  1045. foreach ($result as $attribute) {
  1046. $attributes[$attribute->aid] = uc_attribute_load($attribute->aid, $nid, 'product');
  1047. }
  1048. return $attributes;
  1049. }
  1050. /**
  1051. * Loads all attributes associated with a product class.
  1052. *
  1053. * @param int $pcid
  1054. * The product class id.
  1055. *
  1056. * @return array
  1057. * The attributes.
  1058. */
  1059. function uc_class_get_attributes($pcid) {
  1060. $attributes = array();
  1061. $result = db_query("SELECT uca.aid FROM {uc_class_attributes} uca LEFT JOIN {uc_attributes} ua ON uca.aid = ua.aid WHERE uca.pcid = :type ORDER BY uca.ordering, ua.name", array(':type' => $pcid));
  1062. foreach ($result as $attribute) {
  1063. $attributes[$attribute->aid] = uc_attribute_load($attribute->aid, $pcid, 'class');
  1064. }
  1065. return $attributes;
  1066. }
  1067. /**
  1068. * Gets the options chosen for a product that is in the cart.
  1069. *
  1070. * @param mixed $item
  1071. * An element of the array returned by uc_cart_get_contents.
  1072. *
  1073. * @return array
  1074. * Array of options chosen by a customer, indexed by attribute ids. Each
  1075. * element stores the attribute name and the option object chosen.
  1076. */
  1077. function _uc_cart_product_get_options($item) {
  1078. $options = array();
  1079. if (empty($item->data)) {
  1080. return $options;
  1081. }
  1082. $data = $item->data;
  1083. $node = node_load($item->nid);
  1084. $index = 0;
  1085. if (!empty($data['attributes']) && is_array($data['attributes'])) {
  1086. foreach ($data['attributes'] as $aid => $selected) {
  1087. if (isset($node->attributes[$aid])) {
  1088. $attribute = $node->attributes[$aid];
  1089. $name = _uc_attribute_get_name($attribute);
  1090. // Only discrete options can affect the price of an item.
  1091. if ($attribute->display && count($attribute->options)) {
  1092. // There may be many selected options, or just one.
  1093. foreach ((array) $selected as $oid) {
  1094. if ($oid > 0) {
  1095. $options[$index] = (array) $attribute->options[$oid];
  1096. $options[$index]['attribute'] = $name;
  1097. $index++;
  1098. }
  1099. }
  1100. }
  1101. else {
  1102. // Handle textfield attributes.
  1103. $options[$index] = array(
  1104. 'attribute' => $name,
  1105. 'aid' => $aid,
  1106. 'oid' => 0,
  1107. 'name' => $selected,
  1108. 'cost' => 0,
  1109. 'price' => 0,
  1110. 'weight' => 0,
  1111. );
  1112. }
  1113. $index++;
  1114. }
  1115. }
  1116. }
  1117. else {
  1118. $options = array();
  1119. }
  1120. return $options;
  1121. }
  1122. /**
  1123. * Ajax callback for attribute selection form elements.
  1124. */
  1125. function uc_attribute_option_ajax($form, $form_state) {
  1126. $parents = $form_state['triggering_element']['#array_parents'];
  1127. $wrapper = '#' . $form_state['triggering_element']['#ajax']['wrapper'];
  1128. while ($key = array_pop($parents)) {
  1129. if ($key == 'attributes') {
  1130. array_push($parents, $key);
  1131. $element = drupal_array_get_nested_value($form, $parents);
  1132. $commands[] = ajax_command_replace($wrapper, drupal_render($element));
  1133. break;
  1134. }
  1135. }
  1136. if (strpos($form['#form_id'], 'add_to_cart_form') !== FALSE) {
  1137. $commands = array_merge($commands, uc_product_view_ajax_commands($form_state, array('display_price', 'weight', 'cost')));
  1138. }
  1139. $commands[] = ajax_command_prepend($wrapper, theme('status_messages'));
  1140. return array('#type' => 'ajax', '#commands' => $commands);
  1141. }
  1142. /**
  1143. * Helper function for uc_attribute_form_alter().
  1144. *
  1145. * @param $id
  1146. * The unique id to use to wrap these form elements.
  1147. * @param &$product
  1148. * The product node for which the attribute form elements are to be attached.
  1149. * @param bool $use_ajax
  1150. * TRUE to add ajax to the form. Note that ajax may be added even if this is
  1151. * FALSE, if there are multiple attributes and one or more of them is set to
  1152. * display total price.
  1153. *
  1154. * @return array
  1155. * Form API array with attribute elements to add to the product form.
  1156. *
  1157. * @see theme_uc_attribute_add_to_cart()
  1158. * @see uc_attribute_option_ajax()
  1159. */
  1160. function _uc_attribute_alter_form($id, &$product, $use_ajax) {
  1161. // If the product doesn't have attributes, return the form as it is.
  1162. if (empty($product->attributes) || !is_array($product->attributes)) {
  1163. return array();
  1164. }
  1165. $nid = $product->nid;
  1166. $attributes = $product->attributes;
  1167. $priced_attributes = uc_attribute_priced_attributes($nid);
  1168. // At this point, $product->data is the node author's userdata
  1169. // as a string, as populated by user_node_load(). We don't need that.
  1170. if (empty($product->data) || !is_array($product->data)) {
  1171. $product->data = array();
  1172. }
  1173. // If the form is being built for the first time, populate attributes
  1174. // with their default values.
  1175. if (!isset($product->data['attributes'])) {
  1176. $values = array();
  1177. foreach ($priced_attributes as $aid) {
  1178. if (!$attributes[$aid]->required && ($attributes[$aid]->display == 1 || $attributes[$aid]->display == 2)) {
  1179. $values[$aid] = $attributes[$aid]->default_option;
  1180. }
  1181. }
  1182. if (!empty($values)) {
  1183. $data = $product->data;
  1184. $data['attributes'] = $values;
  1185. if (isset($product->qty)) {
  1186. // Preserve the quantity (for product-kit sub-products).
  1187. $qty = $product->qty;
  1188. }
  1189. $product = uc_product_load_variant($product->nid, $data);
  1190. if (isset($qty)) {
  1191. $product->qty = $qty;
  1192. }
  1193. }
  1194. }
  1195. // Initialize the form element.
  1196. $form_attributes = array(
  1197. '#theme' => 'uc_attribute_add_to_cart',
  1198. '#id' => $id,
  1199. );
  1200. $price_format = variable_get('uc_attribute_option_price_format', 'adjustment');
  1201. // Loop through each product attribute and generate its form element.
  1202. foreach ($attributes as $attribute) {
  1203. // Build the attribute's options array.
  1204. $options = array();
  1205. foreach ($attribute->options as $option) {
  1206. $display_price = '';
  1207. if (in_array($attribute->aid, $priced_attributes)) {
  1208. $data = array('display_only' => TRUE) + $product->data;
  1209. if (empty($data['attributes'])) {
  1210. $data['attributes'] = array();
  1211. }
  1212. switch ($price_format) {
  1213. case 'total':
  1214. // Only display total price for non-checkbox options.
  1215. // !TODO Fix attribute option total price display for product kits.
  1216. if ($attribute->display != 3 && !isset($product->data['kit_id'])) {
  1217. $use_ajax = $use_ajax || (count($priced_attributes) > 1);
  1218. $data['attributes'] = array($attribute->aid => $option->oid) + $data['attributes'];
  1219. $variant = node_view(uc_product_load_variant($product->nid, $data), 'teaser');
  1220. $display_price = uc_currency_format($variant['display_price']['#value']);
  1221. break;
  1222. }
  1223. case 'adjustment':
  1224. if ($attribute->display == 3 || !$use_ajax) {
  1225. // For checkboxes, or if the node totals are not being updated,
  1226. // we compare this attribute against base price.
  1227. if (empty($base)) { // only build the base once.
  1228. unset($data['attributes']);
  1229. $base = node_view(uc_product_load_variant($product->nid, $data), 'teaser');
  1230. }
  1231. $data['attributes'] = array($attribute->aid => $option->oid);
  1232. $variant = node_view(uc_product_load_variant($product->nid, $data), 'teaser');
  1233. $adjustment = $variant['display_price']['#value'] - $base['display_price']['#value'];
  1234. }
  1235. else {
  1236. // Otherwise we compare against current total price.
  1237. if (empty($selected)) {
  1238. $selected = node_view(uc_product_load_variant($product->nid, $data), 'teaser');
  1239. }
  1240. $data['attributes'] = array($attribute->aid => $option->oid) + $data['attributes'];
  1241. $variant = node_view(uc_product_load_variant($product->nid, $data), 'teaser');
  1242. $adjustment = $variant['display_price']['#value'] - $selected['display_price']['#value'];
  1243. }
  1244. if ($adjustment != 0) {
  1245. $display_price = $adjustment > 0 ? '+' : '-';
  1246. $display_price .= uc_currency_format(abs($adjustment));
  1247. }
  1248. break;
  1249. }
  1250. }
  1251. // Select options are check_plain()ed, but radio button labels are not.
  1252. $options[$option->oid] = theme('uc_attribute_option', array(
  1253. 'option' => $attribute->display == 2 ? check_plain($option->name) : $option->name,
  1254. 'price' => $display_price,
  1255. ));
  1256. }
  1257. if (count($attribute->options) && $attribute->display > 0) {
  1258. if ($attribute->required) {
  1259. if ($attribute->display == 1) {
  1260. $options = array('' => t('Please select')) + $options;
  1261. }
  1262. $attribute->default_option = NULL;
  1263. }
  1264. $attr_type = '';
  1265. switch ($attribute->display) {
  1266. case 1:
  1267. $attr_type = 'select';
  1268. break;
  1269. case 2:
  1270. $attr_type = 'radios';
  1271. break;
  1272. case 3:
  1273. $attr_type = 'checkboxes';
  1274. $attribute->default_option = array();
  1275. break;
  1276. }
  1277. $form_attributes[$attribute->aid] = array(
  1278. '#type' => $attr_type,
  1279. '#default_value' => $attribute->default_option,
  1280. '#options' => $options,
  1281. );
  1282. if ($use_ajax) {
  1283. $form_attributes[$attribute->aid]['#ajax'] = array(
  1284. 'callback' => 'uc_attribute_option_ajax',
  1285. 'wrapper' => $id,
  1286. );
  1287. }
  1288. }
  1289. elseif ($attribute->display > 0) {
  1290. $form_attributes[$attribute->aid] = array(
  1291. '#type' => 'textfield',
  1292. '#default_value' => '',
  1293. );
  1294. if (!$attribute->required && isset($attribute->options[$attribute->default_option])) {
  1295. $form_attributes[$attribute->aid]['#default_value'] = $attribute->options[$attribute->default_option]->name;
  1296. }
  1297. }
  1298. else {
  1299. $form_attributes[$attribute->aid] = array(
  1300. '#type' => 'textfield',
  1301. '#default_value' => '',
  1302. );
  1303. }
  1304. $name = _uc_attribute_get_name($attribute, FALSE);
  1305. if (!is_null($name)) {
  1306. $form_attributes[$attribute->aid]['#title'] = check_plain($name);
  1307. }
  1308. $form_attributes[$attribute->aid]['#description'] = filter_xss($attribute->description);
  1309. $form_attributes[$attribute->aid]['#required'] = $attribute->required;
  1310. }
  1311. return $form_attributes;
  1312. }
  1313. /**
  1314. * Returns an array of display types used as options when creating attributes.
  1315. *
  1316. * @return array
  1317. * Array of display types used as options when creating attributes.
  1318. */
  1319. function _uc_attribute_display_types() {
  1320. return array(
  1321. 0 => t('Text field'),
  1322. 1 => t('Select box'),
  1323. 2 => t('Radio buttons'),
  1324. 3 => t('Checkboxes'),
  1325. );
  1326. }
  1327. /**
  1328. * Gets the price affecting attributes for a product.
  1329. *
  1330. * @param int $nid
  1331. * The nid of a product.
  1332. *
  1333. * @return array
  1334. * Array of attribute ids that have price affecting options.
  1335. */
  1336. function uc_attribute_priced_attributes($nid) {
  1337. $aids = db_query("SELECT DISTINCT (pa.aid) FROM {uc_product_attributes} pa INNER JOIN {uc_attribute_options} ao ON ao.aid = pa.aid INNER JOIN {uc_product_options} po ON (po.oid = ao.oid AND po.nid = pa.nid) WHERE pa.nid = :nid AND po.price <> :price AND pa.display <> :display", array(':nid' => $nid, ':price' => 0, ':display' => 0))->fetchCol();
  1338. return $aids;
  1339. }
  1340. /**
  1341. * Returns the attribute name to display.
  1342. *
  1343. * An attribute with a label set returns that label except when set to
  1344. * '<none>' . In this case, a NULL is returned. The $title argument forces the
  1345. * function to return the name property instead of label when label is set to
  1346. * '<none>'.
  1347. *
  1348. * The NULL return value is typically used by forms so they know to hide the
  1349. * #title property of the element.
  1350. *
  1351. * @param object $attribute
  1352. * Attribute object.
  1353. * @param bool $title
  1354. * TRUE indicates the function is to return the attribute name when its label
  1355. * is set to '<none>'.
  1356. *
  1357. * @return string|null
  1358. * When the attribute label is set and not '<none>', it is returned.
  1359. * Otherwise, the attribute name is returned when $title is TRUE and NULL is
  1360. * returned when $title is FALSE.
  1361. */
  1362. function _uc_attribute_get_name($attribute, $title = TRUE) {
  1363. if (!$title && $attribute->label == '<none>') {
  1364. return NULL;
  1365. }
  1366. else {
  1367. return (empty($attribute->label) || ($attribute->label == '<none>' && $title) ? $attribute->name : $attribute->label);
  1368. }
  1369. }
  1370. /**
  1371. * Implements hook_i18n_string_info().
  1372. */
  1373. function uc_attribute_i18n_string_info() {
  1374. $groups['uc_attribute'] = array(
  1375. 'title' => t('Ubercart attributes'),
  1376. 'description' => t('Translatable Ubercart attributes'),
  1377. 'format' => FALSE,
  1378. 'list' => TRUE,
  1379. );
  1380. return $groups;
  1381. }
  1382. /**
  1383. * Refreshes translated attribute and option strings.
  1384. */
  1385. function uc_attribute_i18n_string_refresh() {
  1386. $attributes = db_query("SELECT aid, name, label, description FROM {uc_attributes}");
  1387. foreach ($attributes as $attribute) {
  1388. i18n_string_update('uc_attribute:attribute:' . $attribute->aid . ':name', $attribute->name);
  1389. i18n_string_update('uc_attribute:attribute:' . $attribute->aid . ':label', $attribute->label);
  1390. i18n_string_update('uc_attribute:attribute:' . $attribute->aid . ':description', $attribute->description);
  1391. $options = db_query("SELECT oid, name FROM {uc_attribute_options} WHERE aid = :aid", array(':aid' => $attribute->aid));
  1392. foreach ($options as $option) {
  1393. i18n_string_update('uc_attribute:option:' . $option->oid . ':name', $option->name);
  1394. }
  1395. }
  1396. return TRUE;
  1397. }
  1398. /**
  1399. * Translates an attribute.
  1400. *
  1401. * @param &$attribute
  1402. * The attribute object to translate.
  1403. */
  1404. function uc_attribute_translate(&$attribute) {
  1405. if (function_exists('i18n_string')) {
  1406. $attribute->name = i18n_string('uc_attribute:attribute:' . $attribute->aid . ':name', $attribute->name);
  1407. $attribute->label = i18n_string('uc_attribute:attribute:' . $attribute->aid . ':label', $attribute->label);
  1408. $attribute->description = i18n_string('uc_attribute:attribute:' . $attribute->aid . ':description', $attribute->description);
  1409. foreach ($attribute->options as &$option) {
  1410. $option->name = i18n_string('uc_attribute:option:' . $option->oid . ':name', $option->name);
  1411. }
  1412. }
  1413. }