You are here

uc_attribute.module in Ubercart 5

File

uc_attribute/uc_attribute.module
View source
<?php

/**
 * @file
 * Übercart Attribute module.
 *
 * Allows customers to buy slightly different products from the same listing.
 *
 * Many manufacturers provide options to their products. This module provides a way
 * for store administrators to consolidate these options into one product instead of
 * listing each combination separately. This is accomplished through the use of Drupal's
 * hook system.
 * Coded by: Lyle Mantooth
 */
require_once 'uc_attribute_workflow.inc';

/******************************************************************************
 * Drupal Hooks                                                               *
 ******************************************************************************/

/**
 * Implementation of hook_help().
 */
function uc_attribute_help($section) {
  switch ($section) {

    // Help messages for the attributes overview on products and classes.
    case 'node/' . arg(1) . '/edit/attributes':
      return 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'),
      ));
    case 'admin/store/products/classes/' . arg(4) . '/attributes':
      return 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'),
      ));

    // Help message for adding an attribute to a product or class.
    case 'node/' . arg(1) . '/edit/attributes/add':
    case 'admin/store/products/classes/' . arg(4) . '/attributes/add':
      return t('Select the attributes you want to add and submit the form.');

    // Help message for adjusting the options on a product or class.
    case 'node/' . arg(1) . '/edit/options':
    case 'admin/store/products/classes/' . arg(4) . '/options':
      return t('Use the checkboxes to enable options for attributes and the radio buttons to specify defaults for the enabled options. Use the other fields to override the default settings for each option. Attributes with no enabled options will be displayed as text fields.');

    // Help message for the product Adjustments tab.
    case 'node/' . arg(1) . '/edit/adjustments':
      return t('Enter an alternate SKU to be used when the specified set of options are chosen and the product is added to the cart. <b>Warning:</b> Adding or removing attributes from this product will reset all the SKUs on this page to the default product SKU.');
  }
}

/**
 * Implementation of hook_menu().
 */
function uc_attribute_menu($may_cache) {
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/store/products/attributes',
      'title' => t('Manage attributes'),
      'description' => t('Create and edit attributes and options.'),
      'access' => user_access('administer attributes'),
      'callback' => 'uc_attribute_admin',
      'type' => MENU_NORMAL_ITEM,
      'weight' => -1,
    );
    $items[] = array(
      'path' => 'admin/store/products/attributes/overview',
      'title' => t('Overview'),
      'type' => MENU_DEFAULT_LOCAL_TASK,
      'weight' => 0,
    );
    $items[] = array(
      'path' => 'admin/store/products/attributes/add',
      'title' => t('Add an attribute'),
      'access' => user_access('administer attributes'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'uc_attribute_form',
      ),
      'type' => MENU_LOCAL_TASK,
      'weight' => 5,
    );
    $items[] = array(
      'path' => 'admin/store/settings/attributes',
      'title' => t('Attribute settings'),
      'description' => t('Configure the attribute settings'),
      'access' => user_access('administer attributes'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'uc_attribute_admin_settings',
      ),
      'type' => MENU_NORMAL_ITEM,
    );
  }
  else {
    if (intval(arg(4)) > 0) {
      $items[] = array(
        'path' => 'admin/store/products/attributes/' . arg(4) . '/edit',
        'title' => t('Edit attribute'),
        'access' => user_access('administer attributes'),
        'callback' => 'drupal_get_form',
        'callback arguments' => array(
          'uc_attribute_form',
          arg(4),
        ),
        'type' => MENU_CALLBACK,
      );
      $items[] = array(
        'path' => 'admin/store/products/attributes/' . arg(4) . '/delete',
        'access' => user_access('administer attributes'),
        'callback' => 'drupal_get_form',
        'callback arguments' => array(
          'uc_attribute_delete_confirm',
          arg(4),
        ),
        'type' => MENU_CALLBACK,
      );
      $items[] = array(
        'path' => 'admin/store/products/attributes/' . arg(4) . '/options',
        'title' => t('Options'),
        'access' => user_access('administer attributes'),
        'callback' => 'uc_attribute_options',
        'callback arguments' => array(
          arg(4),
        ),
        'type' => MENU_CALLBACK,
      );
      $items[] = array(
        'path' => 'admin/store/products/attributes/' . arg(4) . '/options/overview',
        'title' => t('Overview'),
        'type' => MENU_DEFAULT_LOCAL_TASK,
        'weight' => 0,
      );
      $items[] = array(
        'path' => 'admin/store/products/attributes/' . arg(4) . '/options/add',
        'title' => t('Add an option'),
        'access' => user_access('administer attributes'),
        'callback' => 'drupal_get_form',
        'callback arguments' => array(
          'uc_attribute_option_form',
          arg(4),
          NULL,
        ),
        'type' => MENU_LOCAL_TASK,
        'weight' => 5,
      );
      $items[] = array(
        'path' => 'admin/store/products/attributes/' . arg(4) . '/options/' . arg(6) . '/edit',
        'title' => t('Edit option'),
        'access' => user_access('administer attributes'),
        'callback' => 'drupal_get_form',
        'callback arguments' => array(
          'uc_attribute_option_form',
          arg(4),
          arg(6),
        ),
        'type' => MENU_CALLBACK,
      );
      $items[] = array(
        'path' => 'admin/store/products/attributes/' . arg(4) . '/options/' . arg(6) . '/delete',
        'title' => t('Delete option'),
        'access' => user_access('administer attributes'),
        'callback' => 'drupal_get_form',
        'callback arguments' => array(
          'uc_attribute_option_delete_confirm',
          arg(4),
          arg(6),
        ),
        'type' => MENU_CALLBACK,
      );
    }

    // Menu items for default product class attributes and options.
    $items[] = array(
      'path' => 'admin/store/products/classes/' . arg(4) . '/attributes',
      'title' => t('Attributes'),
      'access' => uc_attribute_product_class_access(),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'uc_object_attributes_form',
        arg(4),
        'class',
      ),
      'weight' => 1,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/store/products/classes/' . arg(4) . '/options',
      'title' => t('Options'),
      'access' => uc_attribute_product_class_access(),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'uc_object_options_form',
        arg(4),
        'class',
      ),
      'weight' => 2,
      'type' => MENU_LOCAL_TASK,
    );

    // Insert subitems into the edit node page for product types.
    if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == 'edit') {
      $node = node_load(arg(1));
      if ($node->nid && in_array($node->type, module_invoke_all('product_types'))) {
        $items[] = array(
          'path' => 'node/' . arg(1) . '/edit/attributes',
          'title' => t('Attributes'),
          'access' => uc_attribute_product_access(arg(1)),
          'callback' => 'drupal_get_form',
          'callback arguments' => array(
            'uc_object_attributes_form',
            arg(1),
            'product',
            'overview',
          ),
          'weight' => 1,
          'type' => MENU_LOCAL_TASK,
        );
        $items[] = array(
          'path' => 'node/' . arg(1) . '/edit/attributes/add',
          'title' => t('Attributes'),
          'access' => uc_attribute_product_access(arg(1)),
          'callback' => 'drupal_get_form',
          'callback arguments' => array(
            'uc_object_attributes_form',
            arg(1),
            'product',
            'add',
          ),
          'weight' => 1,
          'type' => MENU_LOCAL_TASK,
        );
        $items[] = array(
          'path' => 'node/' . arg(1) . '/edit/options',
          'title' => t('Options'),
          'access' => uc_attribute_product_option_access(arg(1)),
          'callback' => 'drupal_get_form',
          'callback arguments' => array(
            'uc_object_options_form',
            arg(1),
            'product',
          ),
          'weight' => 2,
          'type' => MENU_LOCAL_TASK,
        );
        $items[] = array(
          'path' => 'node/' . arg(1) . '/edit/adjustments',
          'title' => t('Adjustments'),
          'access' => uc_attribute_product_option_access(arg(1)),
          'callback' => 'drupal_get_form',
          'callback arguments' => array(
            'uc_product_adjustments_form',
            arg(1),
          ),
          'weight' => 3,
          'type' => MENU_LOCAL_TASK,
        );
      }
    }
    drupal_add_css(drupal_get_path('module', 'uc_attribute') . '/uc_attribute.css');
  }
  return $items;
}

/**
 * Helper function for handling menu access.
 */
function uc_attribute_product_class_access() {
  return user_access('administer product classes') && user_access('administer attributes');
}
function uc_attribute_product_access($nid) {
  $node = node_load($nid);
  if ($node->type == 'product_kit') {
    return FALSE;
  }
  return uc_product_is_product($node) && node_access('update', $node) && (user_access('administer attributes') || user_access('administer product attributes'));
}
function uc_attribute_product_option_access($nid) {
  $node = node_load($nid);
  if ($node->type == 'product_kit') {
    return FALSE;
  }
  return uc_product_is_product($node) && node_access('update', $node) && (user_access('administer attributes') || user_access('administer product attributes') || user_access('administer product options'));
}
function uc_attribute_perm() {
  if ($node->type == 'product_kit') {
    return FALSE;
  }
  return array(
    'administer attributes',
    'administer product attributes',
    'administer product options',
  );
}

/**
 * Implementation of hook_form_alter().
 *
 * Attach option selectors to the form with the "Add to Cart" button.
 */
function uc_attribute_form_alter($form_id, &$form) {
  if (strpos($form_id, 'add_to_cart_form') || strpos($form_id, 'add_product_form')) {

    // If the node has a product list, add attributes to them
    if (count(element_children($form['products']))) {
      foreach (element_children($form['products']) as $key) {
        $form['products'][$key] = _uc_attribute_alter_form($form['products'][$key]);
        if (isset($form['products'][$key]['attributes'])) {
          $form['products'][$key]['attributes']['#tree'] = true;
          $form['products'][$key]['#type'] = 'fieldset';
        }
      }
    }
    else {
      $form['attributes'] = array(
        '#tree' => true,
        '#weight' => -1,
      );
      $form = _uc_attribute_alter_form($form);
    }
  }
}

/**
 * Implementation of hook_nodeapi().
 */
function uc_attribute_nodeapi(&$node, $op, $arg3 = null, $arg4 = null) {
  if (in_array($node->type, module_invoke_all('product_types'))) {
    switch ($op) {
      case 'insert':
        switch ($GLOBALS['db_type']) {
          case 'mysqli':
          case 'mysql':
            db_query("INSERT IGNORE INTO {uc_product_attributes} (nid, aid, ordering, required, display, default_option) SELECT %d, aid, ordering, required, display, default_option FROM {uc_class_attributes} WHERE pcid = '%s'", $node->nid, $node->type);
            db_query("INSERT IGNORE INTO {uc_product_options} (nid, oid, cost, price, weight, ordering) SELECT %d, oid, cost, price, weight, ordering FROM {uc_class_attribute_options} WHERE pcid = '%s'", $node->nid, $node->type);
            break;
          case 'pgsql':
            db_query("INSERT INTO {uc_product_attributes} (nid, aid, ordering, required, display, default_option) SELECT %d, aid, ordering, required, display, default_option FROM {uc_class_attributes} WHERE pcid = '%s'", $node->nid, $node->type);
            db_query("INSERT INTO {uc_product_options} (nid, oid, cost, price, weight, ordering) SELECT %d, oid, cost, price, weight, ordering FROM {uc_class_attribute_options} WHERE pcid = '%s'", $node->nid, $node->type);
            break;
        }
        break;
      case 'delete':
        db_query("DELETE FROM {uc_product_options} WHERE nid = %d", $node->nid);
        db_query("DELETE FROM {uc_product_adjustments} WHERE nid = %d", $node->nid);
        db_query("DELETE FROM {uc_product_attributes} WHERE nid = %d", $node->nid);
        break;
      case 'update index':
        $output = '';
        $attributes = uc_product_get_attributes($node->nid);
        foreach ($attributes as $attribute) {
          $output .= '<h3>' . $attribute->name . '</h3>';
          foreach ($attribute->options as $option) {
            $output .= $option->name . ' ';
          }
          $output .= "\n";
        }
        $result = db_query("SELECT model FROM {uc_product_adjustments} WHERE nid = %d", $node->nid);
        while ($adjustment = db_fetch_object($result)) {
          $output .= '<h2>' . $adjustment->model . "<h2>\n";
        }
        return $output;
    }
  }
}

/******************************************************************************
 * Ubercart Hooks                                                             *
 ******************************************************************************/

/**
 * Stores the customer's choices in the cart.
 */
function uc_attribute_add_to_cart_data($form_values) {
  if (!isset($form_values['attributes'])) {
    return array(
      'attributes' => array(),
      'model' => null,
    );
  }
  $combination = array();
  foreach ($form_values['attributes'] as $aid => $value) {
    if (is_numeric($value)) {
      $attribute = uc_attribute_load($aid, $form_values['nid'], 'product');
      if ($attribute && ($attribute->display == 1 || $attribute->display == 2)) {
        $combination[$aid] = $value;
      }
    }
  }
  ksort($combination);
  $result = db_query("SELECT model FROM {uc_product_adjustments} WHERE nid = %d AND combination LIKE '%s'", $form_values['nid'], serialize($combination));
  $model = db_result($result);

  // Preserve the 'attributes' key to allow other modules to add to the data field.
  return array(
    'attributes' => $form_values['attributes'],
    'model' => $model,
  );
}

/**
 * Implementation of hook_product_class().
 */
function uc_attribute_product_class($type, $op) {
  switch ($op) {
    case 'delete':
      db_query("DELETE FROM {uc_class_attributes} WHERE pcid = '%s'", $type);
      db_query("DELETE FROM {uc_class_attribute_options} WHERE pcid = '%s'", $type);
      break;
  }
}

/**
 * Implementation of hook_cart_item().
 */
function uc_attribute_cart_item($op, &$item) {
  switch ($op) {
    case 'load':
      $item->options = _uc_cart_product_get_options($item);
      $op_costs = 0;
      $op_prices = 0;
      $op_weight = 0;
      foreach ($item->options as $option) {
        $op_costs += $option['cost'];
        $op_prices += $option['price'];
        $op_weight += $option['weight'];
      }
      $item->cost += $op_costs;
      $item->price += $op_prices;
      $item->weight += $op_weight;
      if (!empty($item->data['model'])) {
        $item->model = $item->data['model'];
      }
      break;
  }
}

/******************************************************************************
 * Menu Callbacks                                                             *
 ******************************************************************************/

/**
 * Change the display of attribute option prices.
 *
 * @ingroup forms
 */
function uc_attribute_admin_settings() {
  $form = array();
  $form['uc_attribute_option_price_format'] = array(
    '#type' => 'radios',
    '#title' => t('Option price format'),
    '#default_value' => variable_get('uc_attribute_option_price_format', 'adjustment'),
    '#options' => array(
      'none' => t('Do not display'),
      'adjustment' => t('Display price adjustment'),
      'total' => t('Display total price'),
    ),
    '#description' => t('Formats the price in the attribute selection form when the customer adds a product to their cart. The total price will only be displayed on products with only one price affecting attribute.'),
  );
  return system_settings_form($form);
}

// Displays a paged list and overview of existing product attributes.
function uc_attribute_admin() {
  $header = array(
    array(
      'data' => t('Name'),
      'field' => 'a.name',
      'sort' => 'asc',
    ),
    t('Required'),
    array(
      'data' => t('Order'),
      'field' => 'a.ordering',
    ),
    t('Number of options'),
    t('Display type'),
    t('Operations'),
  );
  $display_types = _uc_attribute_display_types();
  $result = pager_query("SELECT a.aid, a.name, a.required, a.ordering, a.display, COUNT(ao.oid) AS options FROM {uc_attributes} AS a LEFT JOIN {uc_attribute_options} AS ao ON a.aid = ao.aid GROUP BY a.aid, a.name, a.ordering, a.required, a.display" . tablesort_sql($header), 30, 0, "SELECT COUNT(aid) FROM {uc_attributes}");
  while ($attr = db_fetch_object($result)) {
    $ops = array(
      l(t('edit'), 'admin/store/products/attributes/' . $attr->aid . '/edit'),
      l(t('options'), 'admin/store/products/attributes/' . $attr->aid . '/options'),
      l(t('delete'), 'admin/store/products/attributes/' . $attr->aid . '/delete'),
    );
    $rows[] = array(
      $attr->name,
      $attr->required == 1 ? t('Yes') : t('No'),
      array(
        'data' => $attr->ordering,
        'align' => 'center',
      ),
      array(
        'data' => $attr->options,
        'align' => 'center',
      ),
      $display_types[$attr->display],
      implode(' ', $ops),
    );
  }
  if (count($rows) == 0) {
    $rows[] = array(
      array(
        'data' => t('No product attributes have been added yet.'),
        'colspan' => '6',
      ),
    );
  }
  $output = theme('table', $header, $rows) . theme('pager', NULL, 30) . l(t('Add an attribute'), 'admin/store/products/attributes/add');
  return $output;
}

/**
 * Form builder for product attributes.
 *
 * @ingroup forms
 * @see uc_attribute_form_validate
 * @see uc_attribute_form_submit
 */
function uc_attribute_form($aid = NULL) {

  // If an attribute ID is specified, load it and add its ID as a hidden value.
  $attribute = uc_attribute_load($aid);
  if (!empty($attribute)) {
    $form['aid'] = array(
      '#type' => 'hidden',
      '#value' => $aid,
    );
    drupal_set_title(t('Edit attribute: %name', array(
      '%name' => $attribute->name,
    )));
  }
  $form['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Name'),
    '#description' => t('This name will appear to customers on product add to cart forms.'),
    '#default_value' => $attribute->name,
    '#required' => TRUE,
  );
  $form['description'] = array(
    '#type' => 'textfield',
    '#title' => t('Help text'),
    '#description' => t('<b>Optional.</b> Enter the help text that will display beneath the attribute on product add to cart forms.'),
    '#default_value' => $attribute->description,
    '#maxlength' => 255,
  );
  $form['required'] = array(
    '#type' => 'checkbox',
    '#title' => t('Make this attribute required, forcing the customer to choose an option.'),
    '#description' => t('Selecting this for an attribute will disregard any default option you specify.<br />May be overridden at the product level.'),
    '#default_value' => $attribute->required,
  );
  $form['display'] = array(
    '#type' => 'select',
    '#title' => t('Display type'),
    '#description' => t('This specifies how the options for this attribute will be presented.<br />May be overridden at the product level.'),
    '#options' => _uc_attribute_display_types(),
    '#default_value' => isset($attribute->display) ? $attribute->display : 1,
  );
  $form['ordering'] = array(
    '#type' => 'weight',
    '#title' => t('Order'),
    '#description' => t('Multiple attributes on an add to cart form are sorted by this value and then by their name.<br />May be overridden at the product level.'),
    '#default_value' => $attribute->ordering,
  );
  $form['op'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
    '#suffix' => l(t('Cancel'), 'admin/store/products/attributes'),
  );
  return $form;
}

/**
 * Submit function for uc_attribute_add_form().
 */
function uc_attribute_form_submit($form_id, $form_values) {
  if (!empty($form_values['aid'])) {
    db_query("UPDATE {uc_attributes} SET name = '%s', ordering = %d, required = %d, display = %d, description = '%s' WHERE aid = %d", $form_values['name'], $form_values['ordering'], $form_values['required'], $form_values['display'], $form_values['description'], $form_values['aid']);
  }
  else {
    db_query("INSERT INTO {uc_attributes} (name, ordering, required, display, description) VALUES ('%s', %d, %d, %d, '%s')", $form_values['name'], $form_values['ordering'], $form_values['required'], $form_values['display'], $form_values['description']);
  }
  return 'admin/store/products/attributes';
}

// Confirms the deletion of the given attribute.
function uc_attribute_delete_confirm($aid) {
  $attribute = uc_attribute_load($aid);

  // If we got a bunk attribute ID, kick out an error message.
  if (empty($attribute)) {
    drupal_set_message(t('There is no attribute with that ID.'), 'error');
    drupal_goto('admin/store/products/attributes');
  }
  $form['aid'] = array(
    '#type' => 'value',
    '#value' => $aid,
  );
  $form['#redirect'] = 'admin/store/products/attributes';
  $count = db_result(db_query("SELECT COUNT(*) FROM {uc_product_attributes} WHERE aid = %d", $aid));
  $output = confirm_form($form, t('Are you sure you want to delete the attribute %name?', array(
    '%name' => $attribute->name,
  )), 'admin/store/products/attributes', format_plural($count, 'There is @count product with this attribute.', 'There are @count products with this attribute.'), t('Delete'), t('Cancel'));
  return $output;
}
function uc_attribute_delete_confirm_submit($form_id, $form_values) {
  if ($form_values['confirm']) {
    db_query("DELETE FROM {uc_class_attribute_options} WHERE EXISTS (SELECT * FROM {uc_attribute_options} AS ao WHERE {uc_class_attribute_options}.oid = ao.oid AND ao.aid = %d)", $form_values['aid']);
    db_query("DELETE FROM {uc_class_attributes} WHERE aid = %d", $form_values['aid']);
    db_query("DELETE FROM {uc_product_options} WHERE EXISTS (SELECT * FROM {uc_attribute_options} AS ao WHERE {uc_product_options}.oid = ao.oid AND ao.aid = %d)", $form_values['aid']);
    db_query("DELETE FROM {uc_product_adjustments} WHERE EXISTS (SELECT * FROM {uc_product_attributes} AS pa WHERE {uc_product_adjustments}.nid = pa.nid AND pa.aid = %d)", $form_values['aid']);
    db_query("DELETE FROM {uc_product_attributes} WHERE aid = %d", $form_values['aid']);
    db_query("DELETE FROM {uc_attribute_options} WHERE aid = %d", $form_values['aid']);
    db_query("DELETE FROM {uc_attributes} WHERE aid = %d", $form_values['aid']);
    drupal_set_message(t('Product attribute deleted.'));
  }
}

// Display options and the modifications to products they represent.
function uc_attribute_options($aid) {
  $attribute = uc_attribute_load($aid);

  // If we got a bunk attribute ID, kick out an error message.
  if (empty($attribute)) {
    drupal_set_message(t('There is no attribute with that ID.'), 'error');
    drupal_goto('admin/store/products/attributes');
  }
  drupal_set_title(t('Options for %name', array(
    '%name' => $attribute->name,
  )));
  $header = array(
    t('Name'),
    t('Default cost'),
    t('Default price'),
    t('Default weight'),
    t('Order'),
    t('Operations'),
  );
  foreach ($attribute->options as $key => $data) {
    $ops = array(
      l(t('edit'), 'admin/store/products/attributes/' . $aid . '/options/' . $key . '/edit'),
      l(t('delete'), 'admin/store/products/attributes/' . $aid . '/options/' . $key . '/delete'),
    );
    $rows[] = array(
      $data->name,
      $data->cost,
      $data->price,
      $data->weight,
      $data->ordering,
      implode(' ', $ops),
    );
  }
  if (count($rows) == 0) {
    $rows[] = array(
      array(
        'data' => t('No options for this attribute have been added yet.'),
        'colspan' => '6',
      ),
    );
  }
  $output .= theme('table', $header, $rows) . l(t('Add an option'), 'admin/store/products/attributes/' . $aid . '/options/add');
  return $output;
}

/**
 * Form builder for attribute options.
 *
 * @ingroup forms
 * @see uc_attribute_option_form_validate
 * @see uc_attribute_option_form_submit
 */
function uc_attribute_option_form($aid, $oid = NULL) {
  $attribute = uc_attribute_load($aid);

  // If we got a bunk attribute ID, kick out an error message.
  if (empty($attribute)) {
    drupal_set_message(t('There is no attribute with that ID.'), 'error');
    drupal_goto('admin/store/products/attributes');
  }
  $form['aid'] = array(
    '#type' => 'hidden',
    '#value' => $aid,
  );
  if ($oid) {
    $option = $attribute->options[$oid];
    if (!empty($option)) {
      $form['oid'] = array(
        '#type' => 'hidden',
        '#value' => $oid,
      );
      drupal_set_title(t('Edit option: %name', array(
        '%name' => $option->name,
      )));
    }
  }
  else {
    drupal_set_title(t('Options for %name', array(
      '%name' => $attribute->name,
    )));
  }
  $form['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Name'),
    '#description' => t('This name will appear to customers on product add to cart forms.'),
    '#default_value' => $option->name,
    '#required' => TRUE,
    '#weight' => 0,
  );
  $form['ordering'] = array(
    '#type' => 'weight',
    '#title' => t('Order'),
    '#description' => t('Options will be listed sorted by this value and then by their name.<br />May be overridden at the product level.'),
    '#default_value' => $option->ordering,
    '#weight' => 4,
  );
  $form['adjustments'] = array(
    '#type' => 'fieldset',
    '#title' => t('Default adjustments'),
    '#description' => t('Enter a positive or negative value for each adjustment applied when this option is selected.<br />Any of these may be overriden at the product level.'),
    '#collapsible' => FALSE,
    '#weight' => 8,
  );
  $form['adjustments']['cost'] = array(
    '#type' => 'textfield',
    '#title' => t('Cost'),
    '#default_value' => $option->cost,
    '#weight' => 1,
  );
  $form['adjustments']['price'] = array(
    '#type' => 'textfield',
    '#title' => t('Price'),
    '#default_value' => $option->price,
    '#weight' => 2,
  );
  $form['adjustments']['weight'] = array(
    '#type' => 'textfield',
    '#title' => t('Weight'),
    '#default_value' => $option->weight,
    '#weight' => 3,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
    '#suffix' => l(t('Cancel'), 'admin/store/products/attributes/' . $aid . '/options'),
    '#weight' => 10,
  );
  return $form;
}

/**
 * Validate number formats.
 */
function uc_attribute_option_form_validate($form_id, $form_values) {
  $pattern = '/^-?\\d*(\\.\\d*)?$/';
  $price_error = t('This must be in a valid number format. No commas and only one decimal point.');
  if (!is_numeric($form_values['cost']['#value']) && !preg_match($pattern, $form_values['cost']['#value'])) {
    form_set_error('cost', $price_error);
  }
  if (!is_numeric($form_values['price']['#value']) && !preg_match($pattern, $form_values['price']['#value'])) {
    form_set_error('price', $price_error);
  }
  if (!is_numeric($form_values['weight']['#value']) && !preg_match($pattern, $form_values['weight']['#value'])) {
    form_set_error('weight', $price_error);
  }
}

/**
 * Submit function for uc_attribute_option_form().
 */
function uc_attribute_option_form_submit($form_id, $form_values) {
  if (!isset($form_values['oid'])) {
    db_query("INSERT INTO {uc_attribute_options} (aid, name, cost, price, weight, ordering) VALUES (%d, '%s', %f, %f, %f, %d)", $form_values['aid'], $form_values['name'], $form_values['cost'], $form_values['price'], $form_values['weight'], $form_values['ordering']);
  }
  else {
    db_query("UPDATE {uc_attribute_options} SET name = '%s', cost = %f, price = %f, weight = %f, ordering = %d WHERE aid = %d AND oid = %d", $form_values['name'], $form_values['cost'], $form_values['price'], $form_values['weight'], $form_values['ordering'], $form_values['aid'], $form_values['oid']);
  }
  return 'admin/store/products/attributes/' . $form_values['aid'] . '/options';
}

// Confirms deletion of the given attribute option.
function uc_attribute_option_delete_confirm($aid, $oid) {
  $attribute = uc_attribute_load($aid);
  $option = $attribute->options[$oid];
  if (empty($option)) {
    drupal_set_message(t('There is no option with that ID.'), 'error');
    drupal_goto('admin/store/products/attributes/' . $aid . '/options');
  }
  $form['aid'] = array(
    '#type' => 'value',
    '#value' => $aid,
  );
  $form['oid'] = array(
    '#type' => 'value',
    '#value' => $oid,
  );
  $output = confirm_form($form, t('Are you sure you want to delete the option %name?', array(
    '%name' => $option->name,
  )), 'admin/store/products/attributes/' . $aid . '/options', '', t('Delete'), t('Cancel'));
  return $output;
}

/**
 * Submit function for uc_attribute_option_delete_confirm().
 */
function uc_attribute_option_delete_confirm_submit($form_id, $form_values) {
  if ($form_values['confirm']) {
    $match = 'i:' . $form_values['aid'] . ';s:' . strlen($form_values['oid']) . ':"' . $form_values['oid'] . '";';
    db_query("DELETE FROM {uc_product_adjustments} WHERE combination LIKE '%%%s%%'", $match);
    db_query("DELETE ao, co, po FROM {uc_attribute_options} AS ao LEFT JOIN {uc_product_options} AS po ON ao.oid = po.oid LEFT JOIN {uc_class_attribute_options} AS co ON ao.oid = co.oid WHERE ao.oid = %d", $form_values['oid']);
  }
  return 'admin/store/products/attributes/' . $form_values['aid'] . '/options';
}

// Form to associate attributes with products or classes.
function uc_object_attributes_form($id, $type, $view = 'overview') {
  switch ($type) {
    case 'class':
      $class = uc_product_class_load($id);
      if (empty($class->name)) {
        drupal_goto('admin/store/products/classes/' . $id);
      }
      drupal_set_title(check_plain($class->name));
      $attributes = uc_class_get_attributes($id);
      break;
    case 'product':
    default:
      $product = node_load($id);
      if (empty($product->title)) {
        drupal_goto('node/' . $id);
      }
      drupal_set_title(check_plain($product->title));
      $attributes = uc_product_get_attributes($id);
  }
  $used_aids = array();
  foreach ($attributes as $attribute) {
    $used_aids[] = $attribute->aid;
  }
  if ($view == 'overview') {
    $form['#tree'] = TRUE;
    if (count($attributes) > 0) {
      foreach ($attributes as $attribute) {
        $option = $attribute->options[$attribute->default_option];
        $form['attributes'][$attribute->aid] = array(
          'remove' => array(
            '#type' => 'checkbox',
            '#default_value' => 0,
          ),
          'name' => array(
            '#value' => $attribute->name,
          ),
          'option' => array(
            '#value' => $option ? $option->name . ' (' . uc_currency_format($option->price) . ')' : t('n/a'),
          ),
          'required' => array(
            '#type' => 'checkbox',
            '#default_value' => $attribute->required,
          ),
          'ordering' => array(
            '#type' => 'weight',
            '#default_value' => $attribute->ordering,
          ),
          'display' => array(
            '#type' => 'select',
            '#default_value' => $attribute->display,
            '#options' => _uc_attribute_display_types(),
          ),
        );
      }
      $form['save'] = array(
        '#type' => 'submit',
        '#value' => t('Save changes'),
        '#weight' => -2,
      );
    }
  }
  else {
    if ($view == 'add') {

      // Get list of attributes not already assigned to this node or class.
      $unused_attributes = array();
      $result = db_query("SELECT a.aid, a.name FROM {uc_attributes} AS a LEFT JOIN {uc_attribute_options} AS ao ON a.aid = ao.aid GROUP BY a.aid, a.name ORDER BY a.name");
      while ($attribute = db_fetch_object($result)) {
        if (!in_array($attribute->aid, $used_aids)) {
          $unused_attributes[$attribute->aid] = $attribute->name;
        }
      }
      $form['add_attributes'] = array(
        '#type' => 'select',
        '#title' => t('Attributes'),
        '#description' => t('Hold Ctrl + click to select multiple attributes.'),
        '#options' => count($unused_attributes) > 0 ? $unused_attributes : array(
          t('No attributes left to add.'),
        ),
        '#disabled' => count($unused_attributes) == 0 ? TRUE : FALSE,
        '#multiple' => TRUE,
        '#weight' => -1,
      );
      $form['add'] = array(
        '#type' => 'submit',
        '#value' => t('Add attributes'),
        '#suffix' => l(t('Cancel'), $type == 'product' ? 'node/' . $id . '/edit/attributes' : 'admin/store/products/classes/' . $class->pcid . '/attributes'),
        '#weight' => 0,
      );
    }
  }
  $form['id'] = array(
    '#type' => 'value',
    '#value' => $id,
  );
  $form['type'] = array(
    '#type' => 'value',
    '#value' => $type,
  );
  $form['view'] = array(
    '#type' => 'value',
    '#value' => $view,
  );
  return $form;
}

/**
 * Display the formatted attribute form.
 *
 * @ingroup themeable
 */
function theme_uc_object_attributes_form($form) {
  if ($form['view']['#value'] == 'overview') {
    $header = array(
      t('Remove'),
      t('Name'),
      t('Default'),
      t('Required'),
      t('Order'),
      t('Display'),
    );
    if (count(element_children($form['attributes'])) > 0) {
      foreach (element_children($form['attributes']) as $aid) {
        $rows[] = array(
          drupal_render($form['attributes'][$aid]['remove']),
          drupal_render($form['attributes'][$aid]['name']),
          drupal_render($form['attributes'][$aid]['option']),
          drupal_render($form['attributes'][$aid]['required']),
          drupal_render($form['attributes'][$aid]['ordering']),
          drupal_render($form['attributes'][$aid]['display']),
        );
      }
    }
    else {
      $rows[] = array(
        array(
          'data' => t('You must first <a href="!url">add attributes to this !type</a>.', array(
            '!url' => request_uri() . '/add',
            '!type' => $form['type']['#value'],
          )),
          'colspan' => 6,
        ),
      );
    }
    $output = theme('table', $header, $rows);
  }
  $output .= drupal_render($form);
  return $output;
}
function uc_object_attributes_form_submit($form_id, $form_values) {
  if ($form_values['type'] == 'product') {
    $attr_table = '{uc_product_attributes}';
    $opt_table = '{uc_product_options}';
    $id = 'nid';
    $sql_type = '%d';
  }
  else {
    if ($form_values['type'] == 'class') {
      $attr_table = '{uc_class_attributes}';
      $opt_table = '{uc_class_attribute_options}';
      $id = 'pcid';
      $sql_type = "'%s'";
    }
  }
  if ($form_values['view'] == 'overview' && is_array($form_values['attributes'])) {
    foreach ($form_values['attributes'] as $aid => $attribute) {
      if ($attribute['remove']) {
        $remove_aids[] = $aid;
      }
      else {
        db_query("UPDATE {$attr_table} SET ordering = %d, required = %d, display = %d WHERE aid = %d AND {$id} = {$sql_type}", $attribute['ordering'], $attribute['required'], $attribute['display'], $aid, $form_values['id']);
        $changed = TRUE;
      }
    }
    if (count($remove_aids) > 0) {
      $values = array(
        $form_values['id'],
        implode(', ', $remove_aids),
      );
      db_query("DELETE FROM {$opt_table} WHERE EXISTS (SELECT * FROM {uc_attribute_options} AS ao WHERE {$opt_table}.oid = ao.oid AND ao.aid IN (%s)) AND {$opt_table}.{$id} = {$sql_type}", $values);
      db_query("DELETE FROM {$attr_table} WHERE {$id} = {$sql_type} AND aid IN (%s)", $values);
      if ($form_values['type'] == 'product') {
        db_query("DELETE FROM {uc_product_adjustments} WHERE nid = %d", $form_values['id']);
      }
      drupal_set_message(format_plural(count($remove_aids), '@count attribute has been removed.', '@count attributes have been removed.'));
    }
    if ($changed) {
      drupal_set_message(t('The changes have been saved.'));
    }
  }
  else {
    if ($form_values['view'] == 'add') {
      foreach ($form_values['add_attributes'] as $aid) {

        // Enable all options for added attributes.
        $attribute = uc_attribute_load($aid);
        foreach ($attribute->options as $option) {
          db_query("INSERT INTO {$opt_table} ({$id}, oid, cost, price, weight, ordering) VALUES ({$sql_type}, %d, %f, %f, %f, %d)", $form_values['id'], $option->oid, $option->cost, $option->price, $option->weight, $option->ordering);
        }

        // Make the first option (if any) the default.
        $option = reset($attribute->options);
        if ($option) {
          $oid = $option->oid;
        }
        else {
          $oid = 0;
        }
        db_query("INSERT INTO {$attr_table} ({$id}, aid, ordering, default_option, required, display) SELECT {$sql_type}, aid, ordering, %d, required, display FROM {uc_attributes} WHERE aid = %d", $form_values['id'], $oid, $aid);
      }
      if (count($form_values['add_attributes']) > 0) {
        if ($form_values['type'] == 'product') {
          db_query("DELETE FROM {uc_product_adjustments} WHERE nid = %d", $form_values['id']);
        }
        drupal_set_message(format_plural(count($form_values['add_attributes']), '@count attribute has been added.', '@count attributes have been added.'));
      }
    }
  }
  if ($form_values['type'] == 'product') {
    return 'node/' . $form_values['id'] . '/edit/attributes';
  }
  else {
    return 'admin/store/products/classes/' . $form_values['id'] . '/attributes';
  }
}

// Form to assign and modify attribute options on products or classes.
function uc_object_options_form($id, $type) {
  if ($type == 'product') {
    $product = node_load($id);
    drupal_set_title(check_plain($product->title));
    $attributes = uc_product_get_attributes($id);
    $table = '{uc_product_options}';
    $id_type = 'nid';
    $sql_type = '%d';
  }
  else {
    if ($type == 'class') {
      $class = uc_product_class_load($id);
      drupal_set_title(check_plain($class->name));
      $attributes = uc_class_get_attributes($id);
      $table = '{uc_class_attribute_options}';
      $id_type = 'pcid';
      $sql_type = "'%s'";
    }
  }
  foreach ($attributes as $aid => $attribute) {
    $form['attributes'][$aid]['name'] = array(
      '#value' => $attribute->name,
    );
    $form['attributes'][$aid]['aid'] = array(
      '#type' => 'hidden',
      '#value' => $attribute->aid,
    );
    $form['attributes'][$aid]['ordering'] = array(
      '#type' => 'value',
      '#value' => $attribute->ordering,
    );
    $form['attributes'][$aid]['options'] = array(
      '#weight' => 2,
    );
    $base_attr = uc_attribute_load($attribute->aid);
    if ($base_attr->options) {
      $options = array();
      $result = db_query("SELECT ao.aid, ao.oid, ao.name, ao.cost AS default_cost, ao.price AS default_price, ao.weight AS default_weight, ao.ordering AS default_ordering, po.cost, po.price, po.weight, po.ordering, po.ordering IS NULL AS null_order FROM {uc_attribute_options} AS ao LEFT JOIN {$table} AS po ON ao.oid = po.oid AND po.{$id_type} = {$sql_type} WHERE aid = %d ORDER BY null_order, po.ordering, default_ordering, ao.name", $id, $attribute->aid);
      while ($option = db_fetch_object($result)) {
        $oid = $option->oid;
        $options[$oid] = '';
        $form['attributes'][$aid]['options'][$oid]['select'] = array(
          '#type' => 'checkbox',
          '#default_value' => isset($attribute->options[$oid]) ? TRUE : FALSE,
          '#title' => $option->name,
        );
        $form['attributes'][$aid]['options'][$oid]['cost'] = array(
          '#type' => 'textfield',
          '#default_value' => is_null($option->cost) ? $option->default_cost : $option->cost,
          '#size' => 6,
        );
        $form['attributes'][$aid]['options'][$oid]['price'] = array(
          '#type' => 'textfield',
          '#default_value' => is_null($option->price) ? $option->default_price : $option->price,
          '#size' => 6,
        );
        $form['attributes'][$aid]['options'][$oid]['weight'] = array(
          '#type' => 'textfield',
          '#default_value' => is_null($option->weight) ? $option->default_weight : $option->weight,
          '#size' => 5,
        );
        $form['attributes'][$aid]['options'][$oid]['ordering'] = array(
          '#type' => 'weight',
          '#default_value' => is_null($option->ordering) ? $option->default_ordering : $option->ordering,
        );
      }
      $form['attributes'][$aid]['default'] = array(
        '#type' => 'radios',
        '#options' => $options,
        '#default_value' => $attribute->default_option,
      );
    }
    else {
      $form['attributes'][$aid]['default'] = array(
        '#value' => t('This attribute does not have any options.'),
      );
    }
  }
  if (!empty($form['attributes'])) {
    $form['attributes']['#tree'] = TRUE;
    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Submit'),
      '#weight' => 10,
    );
  }
  $form['id'] = array(
    '#type' => 'value',
    '#value' => $id,
  );
  $form['type'] = array(
    '#type' => 'value',
    '#value' => $type,
  );
  return $form;
}

/**
 * Display the option form.
 *
 * @ingroup themeable
 */
function theme_uc_object_options_form($form) {
  $header = array(
    t('Attribute'),
    t('Options'),
    t('Default'),
    t('Cost'),
    t('Price'),
    t('Weight'),
    t('Order'),
  );
  foreach (element_children($form['attributes']) as $key) {
    $row = array();
    $row[] = array(
      'data' => drupal_render($form['attributes'][$key]['aid']) . drupal_render($form['attributes'][$key]['name']),
      'class' => 'attribute',
    );
    if (element_children($form['attributes'][$key]['default'])) {
      $first = TRUE;
      foreach (element_children($form['attributes'][$key]['default']) as $oid) {
        $row[] = drupal_render($form['attributes'][$key]['options'][$oid]['select']);
        $row[] = drupal_render($form['attributes'][$key]['default'][$oid]);
        $row[] = drupal_render($form['attributes'][$key]['options'][$oid]['cost']);
        $row[] = drupal_render($form['attributes'][$key]['options'][$oid]['price']);
        $row[] = drupal_render($form['attributes'][$key]['options'][$oid]['weight']);
        $row[] = drupal_render($form['attributes'][$key]['options'][$oid]['ordering']);
        if (!$first) {
          $row = array_pad($row, -7, '');
        }
        else {
          $first = FALSE;
        }
        $rows[] = $row;
        $row = array();
      }
      unset($form['attributes'][$key]['default']);
    }
    else {
      $row[] = array(
        'data' => drupal_render($form['attributes'][$key]['default']),
        'colspan' => 7,
      );
      $rows[] = $row;
    }
    $rows[] = array(
      array(
        'data' => '<hr />',
        'colspan' => 7,
      ),
    );
  }
  if (count($rows) == 0) {
    $rows[] = array(
      array(
        'data' => t('This !type does not have any attributes.', array(
          '!type' => $form['type']['#value'] == 'product' ? t('product') : t('product class'),
        )),
        'colspan' => 7,
      ),
    );
  }
  $output = theme('table', $header, $rows, array(
    'class' => 'product_attributes',
  )) . drupal_render($form);
  return $output;
}
function uc_object_options_form_validate($form_id, $form_values, $form) {
  if (isset($form_values['attributes'])) {
    foreach ($form_values['attributes'] as $aid => $attribute) {
      $selected_opts = array();
      if (is_array($attribute['options'])) {
        foreach ($attribute['options'] as $oid => $option) {
          if ($option['select'] == 1) {
            $selected_opts[] = $oid;
          }
        }
      }
      if (!empty($selected_opts) && !$form['attributes'][$aid]['default']['#disabled'] && !in_array($attribute['default'], $selected_opts)) {
        form_set_error($attribute['default']);
        $error = TRUE;
      }
    }
  }
  if ($error) {
    drupal_set_message(t('All attributes with enabled options must specify an enabled option as default.'), 'error');
  }
}
function uc_object_options_form_submit($form_id, $form_values) {
  if ($form_values['type'] == 'product') {
    $attr_table = '{uc_product_attributes}';
    $opt_table = '{uc_product_options}';
    $id = 'nid';
    $sql_type = '%d';
  }
  else {
    if ($form_values['type'] == 'class') {
      $attr_table = '{uc_class_attributes}';
      $opt_table = '{uc_class_attribute_options}';
      $id = 'pcid';
      $sql_type = "'%s'";
    }
  }
  foreach ($form_values['attributes'] as $attribute) {
    db_query("UPDATE {$attr_table} SET default_option = %d WHERE {$id} = {$sql_type} AND aid = %d", $attribute['default'], $form_values['id'], $attribute['aid']);
    if (is_array($attribute['options'])) {
      foreach ($attribute['options'] as $oid => $option) {
        db_query("DELETE FROM {$opt_table} WHERE {$id} = {$sql_type} AND oid = %d", $form_values['id'], $oid);
        if ($option['select']) {
          db_query("INSERT INTO {$opt_table} ({$id}, oid, cost, price, weight, ordering) VALUES ({$sql_type}, %d, %f, %f, %f, %d)", $form_values['id'], $oid, $option['cost'], $option['price'], $option['weight'], $option['ordering']);
        }
        else {
          if ($form_values['type'] == 'product') {
            $aid = $attribute['aid'];
            $match = 'i:' . $aid . ';s:' . strlen($oid) . ':"' . $oid . '";';
            db_query("DELETE FROM {uc_product_adjustments} WHERE nid = %d AND combination LIKE '%%%s%%'", $form_values['id'], $match);
          }
        }
      }
    }
  }
  drupal_set_message(t('The !type options have been saved.', array(
    '!type' => $form_values['type'] == 'product' ? t('product') : t('product class'),
  )));
}

/**
 * Form builder to associate option combinations with mutations of a product's model number.
 *
 * @ingroup forms
 * @see uc_product_adjustments_form_submit
 */
function uc_product_adjustments_form($nid) {
  $node = node_load($nid);
  drupal_set_title(check_plain($node->title));

  //Populate table and such.
  $model = $node->model;
  $query_select = "SELECT DISTINCT";
  $query_from = " FROM";
  $query_where = " WHERE";
  $query_order = " ORDER BY";
  $result = db_query("SELECT pa.ordering, a.name, a.ordering, ao.aid, COUNT(po.oid) FROM {uc_product_attributes} AS pa LEFT JOIN {uc_attributes} AS a ON pa.aid = a.aid LEFT JOIN {uc_attribute_options} AS ao ON a.aid = ao.aid LEFT JOIN {uc_product_options} AS po ON ao.oid = po.oid AND po.nid = %d WHERE pa.nid = %d GROUP BY ao.aid, pa.aid, a.name, pa.ordering, a.ordering HAVING count(po.oid) > 0 ORDER BY pa.ordering, a.ordering", $nid, $nid);
  $num_prod_attr = db_num_rows($result);
  $i = 1;
  $attribute_names = '';
  $full_attributes = array();
  $values = array();
  while ($prod_attr = db_fetch_object($result)) {
    $query_select .= " ao{$i}.aid AS aid{$i}, ao{$i}.name AS name{$i}, ao{$i}.oid AS oid{$i}, po{$i}.ordering,";
    $query_from .= " ({uc_product_options} AS po{$i} LEFT JOIN {uc_attribute_options} AS ao{$i} ON po{$i}.oid = ao{$i}.oid AND po{$i}.nid = %d),";
    $values[] = $nid;
    $query_where .= " ao{$i}.aid = " . $prod_attr->aid . " AND";
    $query_order .= " po{$i}.ordering, ao{$i}.name,";
    ++$i;
    $attribute_names .= '<th>' . $prod_attr->name . '</th>';
    $attribute_ids[] = $prod_attr->aid;
  }

  // Remove last connecting parts (commas, "AND")
  $query_select = rtrim($query_select, ',');
  $query_from = rtrim($query_from, ',');
  $query_where = substr($query_where, 0, strlen($query_where) - 4);
  $query_order = rtrim($query_order, ',');
  if ($num_prod_attr) {

    //Get previous values
    $result = db_query("SELECT * FROM {uc_product_adjustments} WHERE nid = %d", $nid);
    $old_vals = array();
    while ($obj = db_fetch_object($result)) {
      $old_vals[] = $obj;
    }
    $result = pager_query($query_select . $query_from . $query_where . $query_order, 20, 0, null, $values);
    $form['original'] = array(
      '#value' => '<div><br /><b>' . t('Default product SKU: @sku', array(
        '@sku' => $model,
      )) . '</b></div>',
    );
    $form['default'] = array(
      '#type' => 'value',
      '#value' => $model,
    );
    $form['table'] = array(
      '#prefix' => '<table class="combinations">',
      '#suffix' => '</table>',
    );
    $form['table']['head'] = array(
      '#prefix' => '<thead><tr>',
      '#suffix' => '</tr></thead>',
      '#value' => $attribute_names . '<th>Alternate SKU</th>',
      '#weight' => 0,
    );
    $form['table']['body'] = array(
      '#prefix' => '<tbody>',
      '#suffix' => '</tbody>',
      '#weight' => 1,
      '#tree' => TRUE,
    );
    $i = 0;
    while ($combo = db_fetch_object($result)) {
      $cells = '';
      $row_title = '';
      $comb_array = array();
      for ($j = 1; $j <= $num_prod_attr; ++$j) {
        $cells .= '<td>' . $combo->{'name' . $j} . '</td>';
        $row_title .= $combo->{'name' . $j} . ', ';
        $comb_array[$combo->{'aid' . $j}] = $combo->{'oid' . $j};
      }
      $row_title = substr($row_title, 0, strlen($row_title) - 2);
      $serial_array = serialize($comb_array);
      $default_model = $model;
      foreach ($old_vals as $ov) {
        if (!count(array_diff_assoc(unserialize($ov->combination), unserialize($serial_array)))) {
          $default_model = $ov->model;
          break;
        }
      }
      $form['table']['body'][$i] = array(
        '#prefix' => '<tr title="' . $row_title . '">',
        '#suffix' => '</tr>',
      );
      $form['table']['body'][$i]['combo'] = array(
        '#value' => $cells,
      );
      $form['table']['body'][$i]['combo_array'] = array(
        '#type' => 'value',
        '#value' => $serial_array,
      );
      $form['table']['body'][$i]['model'] = array(
        '#type' => 'textfield',
        '#default_value' => $default_model,
        '#prefix' => '<td>',
        '#suffix' => '</td>',
      );
      ++$i;
    }
    $form['nid'] = array(
      '#type' => 'hidden',
      '#value' => $nid,
    );
    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Submit'),
    );
  }
  else {
    $form['error'] = array(
      '#value' => '<div><br />' . t('This product does not have any attributes.') . '</div>',
    );
  }
  $form['pager'] = array(
    '#value' => theme('pager'),
  );
  return $form;
}
function uc_product_adjustments_form_submit($form_id, $form_values) {
  foreach ($form_values['body'] as $value) {
    if (!empty($value['model']) && $value['model'] != $form_values['default']) {
      db_query("UPDATE {uc_product_adjustments} SET model = '%s' WHERE nid = %d AND combination = '%s'", $value['model'], $form_values['nid'], $value['combo_array']);
      if (!db_affected_rows()) {
        db_query("INSERT INTO {uc_product_adjustments} (nid, combination, model) VALUES (%d, '%s', '%s')", $form_values['nid'], $value['combo_array'], $value['model']);
      }
    }
    else {
      db_query("DELETE FROM {uc_product_adjustments} WHERE nid = %d AND combination = '%s'", $form_values['nid'], $value['combo_array']);
    }
  }
  drupal_set_message(t('Product adjustments have been saved.'));
  $goto = array(
    $_GET['q'],
  );
  if ($_GET['page']) {
    $goto[] = 'page=' . $_GET['page'];
  }
  return $goto;
}

/******************************************************************************
 * Module Functions                                                           *
 ******************************************************************************/

/**
 * Load an attribute from the database.
 *
 * @param $attr_id
 *   The id of the attribute.
 * @param $nid
 *   Node id. If given, the attribute will have the options that have been
 *   assigned to that node for the attribute.
 */
function uc_attribute_load($attr_id, $nid = NULL, $type = '') {
  if ($nid) {
    switch ($type) {
      case 'product':
        $attribute = db_fetch_object(db_query("SELECT a.aid, a.name, a.ordering AS default_ordering, a.required AS default_required, a.display AS default_display, a.description, pa.default_option, pa.required, pa.ordering, pa.display FROM {uc_attributes} AS a LEFT JOIN {uc_product_attributes} AS pa ON a.aid = pa.aid AND pa.nid = %d WHERE a.aid = %d", $nid, $attr_id));
        $result = db_query("SELECT po.nid, po.oid, po.cost, po.price, po.weight, po.ordering, ao.name, ao.aid FROM {uc_product_options} AS po LEFT JOIN {uc_attribute_options} AS ao ON po.oid = ao.oid AND nid = %d WHERE aid = %d ORDER BY po.ordering, ao.name", $nid, $attr_id);
        break;
      case 'class':
        $attribute = db_fetch_object(db_query("SELECT a.aid, a.name, a.ordering AS default_ordering, a.required AS default_required, a.display AS default_display, a.description, ca.default_option, ca.required, ca.ordering, ca.display FROM {uc_attributes} AS a LEFT JOIN {uc_class_attributes} AS ca ON a.aid = ca.aid AND ca.pcid = '%s' WHERE a.aid = %d", $nid, $attr_id));
        $result = db_query("SELECT co.pcid, co.oid, co.cost, co.price, co.weight, co.ordering, ao.name, ao.aid FROM {uc_class_attribute_options} AS co LEFT JOIN {uc_attribute_options} AS ao ON co.oid = ao.oid AND co.pcid = '%s' WHERE ao.aid = %d ORDER BY co.ordering, ao.name", $nid, $attr_id);
        break;
      default:
        $attribute = db_fetch_object(db_query("SELECT * FROM {uc_attributes} WHERE aid = %d", $attr_id));
        $result = db_query("SELECT * FROM {uc_attribute_options} WHERE aid = %d ORDER BY ordering, name", $attr_id);
        break;
    }
    if (isset($attribute->default_ordering) && is_null($attribute->ordering)) {
      $attribute->ordering = $attribute->default_ordering;
    }
    if (isset($attribute->default_required) && is_null($attribute->required)) {
      $attribute->required = $attribute->default_required;
    }
    if (isset($attribute->default_display) && is_null($attribute->display)) {
      $attribute->display = $attribute->default_display;
    }
  }
  else {
    $attribute = db_fetch_object(db_query("SELECT * FROM {uc_attributes} WHERE aid = %d", $attr_id));
    $result = db_query("SELECT * FROM {uc_attribute_options} WHERE aid = %d ORDER BY ordering, name", $attr_id);
  }
  if ($attribute) {
    $attribute->options = array();
    while ($option = db_fetch_object($result)) {
      $attribute->options[$option->oid] = $option;
    }
  }
  return $attribute;
}

// Loads the option identified by $oid.
function uc_attribute_option_load($oid) {
  return db_fetch_object(db_query("SELECT * FROM {uc_attribute_options} WHERE oid = %d", $oid));
}

// Loads all attributes associated with a product node.
function uc_product_get_attributes($nid) {
  $attributes = array();
  $result = db_query("SELECT upa.aid FROM {uc_product_attributes} AS upa LEFT JOIN {uc_attributes} AS ua ON upa.aid = ua.aid WHERE upa.nid = %d ORDER BY upa.ordering, ua.name", $nid);
  while ($attribute = db_fetch_object($result)) {
    $attributes[$attribute->aid] = uc_attribute_load($attribute->aid, $nid, 'product');
  }
  return $attributes;
}

// Loads all attributes associated with a product class.
function uc_class_get_attributes($pcid) {
  $attributes = array();
  $result = db_query("SELECT uca.aid FROM {uc_class_attributes} AS uca LEFT JOIN {uc_attributes} AS ua ON uca.aid = ua.aid WHERE uca.pcid = '%s' ORDER BY uca.ordering, ua.name", $pcid);
  while ($attribute = db_fetch_object($result)) {
    $attributes[$attribute->aid] = uc_attribute_load($attribute->aid, $pcid, 'class');
  }
  return $attributes;
}

/**
 * Display formatted of data associated with attributes
 *
 * @param $type
 *   options | attributes
 *   Determines how to handle the $data array.
 * @param $data
 *   An array of attribute ids or option ids. If option ids, they are indexed by attribute ids.
 * @return
 *   HTML format of $data.
 *
 * @ingroup themeable
 */
function theme_uc_attribute($type, $data, $nid) {
  if ($type == 'options' && is_array($data)) {
    if (count($data)) {
      $output = '<ul class="options">';
      foreach ($data as $aid => $oid) {
        $attribute = uc_attribute_load($aid);
        $option = $attribute->options[$oid];
        $output .= '<li>' . $option->name . '</li>';
      }
      $output .= '</ul>';
    }
  }
  elseif ($type == 'attributes') {
    if (is_array($data) && count($data)) {
      $output = '<div id="option_key">';
      foreach ($data as $aid) {
        $output .= theme('uc_attribute_options', $aid, $nid);
      }
      $output .= '</div>';
    }
    elseif (is_numeric($data)) {
      $output = '<div id="option_key">';
      $output .= theme('uc_attribute_options', $data);
      $output .= '</div>';
    }
  }
  return $output;
}

/**
 * Format an attribute and its options.
 *
 * @ingroup themeable
 */
function theme_uc_attribute_options($aid, $nid) {
  $attribute = uc_attribute_load($aid, $nid, 'product');
  $output .= '<h2>' . $attribute->name . '</h2>';
  $rows = array();
  foreach ($attribute->options as $key => $input) {
    $rows[] = array(
      $input->oid,
      $input->name,
    );
  }
  if (count($rows) == 0) {
    $rows[] = array(
      array(
        'data' => t('(To be determined by customer.)'),
        'colspan' => '2',
      ),
    );
  }
  $header = array(
    t('Option ID'),
    t('Name'),
  );
  $output .= theme('table', $header, $rows);
  return $output;
}

/**
 * Get the options chosen for a product that is in the cart.
 *
 * @param $item
 *   An element of the array returned by uc_cart_get_contents.
 * @return
 *   Array of options chosen by a customer, indexed by attribute ids. Each
 *   element stores the attribute name and the option object chosen.
 */
function _uc_cart_product_get_options($item) {
  $options = array();
  $data = $item->data;
  if (!empty($data['attributes']) && is_array($data['attributes'])) {
    foreach ($data['attributes'] as $aid => $oid) {
      $attribute = uc_attribute_load($aid, $item->nid, 'product');

      // Only discrete options can affect the price of an item.
      if ($attribute->display && count($attribute->options)) {
        $options[$aid] = (array) $attribute->options[$oid];
        $options[$aid]['attribute'] = $attribute->name;
      }
      else {
        $options[$aid] = array(
          'attribute' => $attribute->name,
          'oid' => 0,
          'name' => $oid,
          'cost' => 0,
          'price' => 0,
          'weight' => 0,
        );
      }
    }
  }
  else {
    $options = array();
  }
  return $options;
}

/**
 * Helper function for uc_attribute_form_alter()
 */
function _uc_attribute_alter_form($product) {
  $nid = intval($product['nid']['#value']);
  $qty = intval($product['qty']['#value']);
  if (!$qty) {
    $qty = 1;
  }

  // If we were passed an actual node object...
  if ($node = node_load($nid)) {

    // Load all product attributes for the given nid.
    $result = db_query("SELECT * FROM {uc_product_attributes} WHERE nid = %d ORDER BY ordering", $nid);
    $priced_attributes = uc_attribute_priced_attributes($nid);

    // Loop through each product attribute and generate its form element.
    while ($relation = db_fetch_object($result)) {
      $attribute = uc_attribute_load($relation->aid, $nid, 'product');

      // Build the attribute's options array.
      $options = array();
      foreach ($attribute->options as $option) {
        switch (variable_get('uc_attribute_option_price_format', 'adjustment')) {
          case 'total':
            $display_price = in_array($relation->aid, $priced_attributes) ? ', ' . uc_currency_format(($node->sell_price + $option->price) * $qty) : '';
            if (count($priced_attributes) == 1) {
              break;
            }
          case 'adjustment':
            $display_price = $option->price != 0 ? ', ' . ($option->price > 0 ? '+' : '') . uc_currency_format($option->price * $qty) : '';
            break;
          case 'none':
          default:
            $display_price = '';
            break;
        }
        $options[$option->oid] = $option->name . $display_price;
      }
      if (count($attribute->options) && $attribute->display > 0) {
        if ($attribute->required) {
          if ($attribute->display == 1) {
            $options = array(
              '' => t('Please select'),
            ) + $options;
          }
          unset($relation->default_option);
        }
        $product['attributes'][$attribute->aid] = array(
          '#type' => $attribute->display == 1 ? 'select' : 'radios',
          '#title' => $attribute->name,
          '#description' => check_markup($attribute->description),
          '#default_value' => $relation->default_option,
          '#options' => $options,
          '#required' => $attribute->required,
        );
      }
      else {
        $product['attributes'][$attribute->aid] = array(
          '#type' => 'textfield',
          '#title' => $attribute->name,
          '#description' => check_markup($attribute->description),
          '#default_value' => $attribute->required == FALSE ? $attribute->options[$relation->default_option]->name : '',
          '#required' => $attribute->required,
        );
      }
      $product['attributes']['#theme'] = 'uc_attribute_add_to_cart';
    }
  }
  return $product;
}

// Returns an array of display types used as options when creating attributes.
function _uc_attribute_display_types() {
  return array(
    0 => t('Text field'),
    1 => t('Select box'),
    2 => t('Radio buttons'),
  );
}

/**
 * Get the price affecting attributes for a product
 *
 * @param $nid
 *   The nid of a product.
 * @return
 *   Array of attribute ids that have price affecting options.
 */
function uc_attribute_priced_attributes($nid) {
  $attributes = db_query("SELECT DISTINCT (pa.aid) FROM {uc_product_attributes} AS pa INNER JOIN {uc_attribute_options} AS ao ON ao.aid = pa.aid INNER JOIN {uc_product_options} AS po ON (po.oid = ao.oid AND po.nid = pa.nid) WHERE pa.nid = %d AND po.price <> 0 AND pa.display <> 0", $nid);
  $aids = array();
  while ($attribute = db_fetch_array($attributes)) {
    $aids[] = $attribute['aid'];
  }
  return $aids;
}

Functions

Namesort descending Description
theme_uc_attribute Display formatted of data associated with attributes
theme_uc_attribute_options Format an attribute and its options.
theme_uc_object_attributes_form Display the formatted attribute form.
theme_uc_object_options_form Display the option form.
uc_attribute_add_to_cart_data Stores the customer's choices in the cart.
uc_attribute_admin
uc_attribute_admin_settings Change the display of attribute option prices.
uc_attribute_cart_item Implementation of hook_cart_item().
uc_attribute_delete_confirm
uc_attribute_delete_confirm_submit
uc_attribute_form Form builder for product attributes.
uc_attribute_form_alter Implementation of hook_form_alter().
uc_attribute_form_submit Submit function for uc_attribute_add_form().
uc_attribute_help Implementation of hook_help().
uc_attribute_load Load an attribute from the database.
uc_attribute_menu Implementation of hook_menu().
uc_attribute_nodeapi Implementation of hook_nodeapi().
uc_attribute_options
uc_attribute_option_delete_confirm
uc_attribute_option_delete_confirm_submit Submit function for uc_attribute_option_delete_confirm().
uc_attribute_option_form Form builder for attribute options.
uc_attribute_option_form_submit Submit function for uc_attribute_option_form().
uc_attribute_option_form_validate Validate number formats.
uc_attribute_option_load
uc_attribute_perm
uc_attribute_priced_attributes Get the price affecting attributes for a product
uc_attribute_product_access
uc_attribute_product_class Implementation of hook_product_class().
uc_attribute_product_class_access Helper function for handling menu access.
uc_attribute_product_option_access
uc_class_get_attributes
uc_object_attributes_form
uc_object_attributes_form_submit
uc_object_options_form
uc_object_options_form_submit
uc_object_options_form_validate
uc_product_adjustments_form Form builder to associate option combinations with mutations of a product's model number.
uc_product_adjustments_form_submit
uc_product_get_attributes
_uc_attribute_alter_form Helper function for uc_attribute_form_alter()
_uc_attribute_display_types
_uc_cart_product_get_options Get the options chosen for a product that is in the cart.