I have a content type called product.
This product has a multivalue node reference field called: standard accessories.
These accessories are also of the content type products.

How can I add the number of each accessory, for example:
2 x accessory 1
5 x accessory 2
1 x accessory 3

Accessory 1, 2 and 3 are node references and 2, 5 and 1 is the number (a numeric CCK field?) of each accessory.
For each node reference, a number must be entered, 1 or higher.

How can I do this?

Comments

adam_b’s picture

Are you using Ubercart? if so, there are "attribute" filters which might do what you want.

If you're dealing with numeric CCK fields, then http://drupal.org/project/views_calc might help.

tomsm’s picture

No, I am not using Ubercart because it does not support prices per role.
I made a custom content type "products".

A node reference is not a numeric field, so views_calc won't work and I do not need to make a calculation.

I can create a multivalue node reference field and a multivalue numeric field.
But how can I link these field so that value 1 of field 1 is linked to value 1 of field 2, etc.?

tjt756’s picture

I also would like to do something like this. Anyone know how?

Mine is not a product, so I also don't have Ubercart.

Thanks,
TJ

pritchie’s picture

Bump. Having the same issue w/ CCK. Want to specify the quantity of a referenced node.

Seems there should be a generic way to add fields to the reference between the two nodes but I can't see to find it.

mtsanford’s picture

Of the top of my head I'd recommend this:

Limit # of noderefs & integer fields to 10 or some number. (If that's acceptable in your case)

Theme the node input form so that the display of the fields alternates:

int field 1 noderef 1
int field 2 noderef 2
Etc.

With a bit of jquery magic you could even have the list grow visually as required, not showing more than one empty field at a time.

kiliweb’s picture

I'm trying to do exactly the same thing.
Is anyone found a solution ?

hanzhong520’s picture

Who can do this? please help :) or suggest other solutions...

kiliweb’s picture

You can do that with the dev version of CCK 3, by using multigroup.

diond’s picture

Here are the contents of the .module file from a quick module I put together (D6.22 installation using View 6.2x and CCK 6.2x). I used the Node Reference module and an article by Jennifer Hodgdon as a great starting point:

<?php

// $Id:$

/**
 * @file
 * This is a Drupal Module - Node Reference and Quantity Field
 * Implements a Custom Compound CCK Field
 *
 * Copyright 2011 Dion DiFelice, GreenWide.com
 *
 * Licensed under the GNU Public License
 */


/**
 * Implementation of CCK's hook_field_info().
 *
 * Returns basic information about this Custom CCK Field.
 */
function noderef_qty_field_info() {
  return array(
    'noderef_qty' => array(
      'label' => t('Node Reference and Quantity'),
      'description' => t('Stores a Node Reference and Quantity'),
    )
  );
}


/**
 * Implementation of hook_install().
 *
 * Use a CCK Function to install this module.
 */
function noderef_qty_install() {
  content_notify('install', 'noderef_qty');
}

/**
 * Implementation of hook_uninstall().
 *
 * Use a CCK Function to uninstall this module.
 */
function noderef_qty_uninstall() {
  content_notify('uninstall', 'noderef_qty');
}

/**
 * Implementation of hook_enable().
 *
 * Use a CCK Function to enable this module.
 */
function noderef_qty_enable() {
  content_notify('enable', 'noderef_qty');
}

/**
 * Implementation of hook_disable().
 *
 * Use a CCK Function to disable this module.
 */
function noderef_qty_disable() {
  content_notify('disable', 'noderef_qty');
}

/**
 * Implementation of CCK hook_field_settings().
 *
 * Configure the available properties to this Custom CCK Field
 */
function noderef_qty_field_settings($op, $field) {
  switch ($op) {
    case 'form':
      $form = array();
      $form['referenceable_types'] = array(
        '#type' => 'checkboxes',
        '#title' => t('Content types that can be referenced'),
        '#multiple' => TRUE,
        '#default_value' => is_array($field['referenceable_types']) ? $field['referenceable_types'] : array(),
        '#options' => array_map('check_plain', node_get_types('names')),
      );
      if (module_exists('views')) {
        $views = array('--' => '--');
        $all_views = views_get_all_views();
        foreach ($all_views as $view) {
          // Only 'node' views that have fields will work for our purpose.
          if ($view->base_table == 'node' && !empty($view->display['default']->display_options['fields'])) {
            if ($view->type == 'Default') {
              $views[t('Default Views')][$view->name] = $view->name;
            }
            else {
              $views[t('Existing Views')][$view->name] = $view->name;
            }
          }
        }
        $form['advanced'] = array(
           '#type' => 'fieldset',
           '#title' => t('Advanced - Nodes that can be referenced (View)'),
           '#collapsible' => TRUE,
           '#collapsed' => !isset($field['advanced_view']) || $field['advanced_view'] == '--',
         );
        if (count($views) > 1) {
          $form['advanced']['advanced_view'] = array(
            '#type' => 'select',
            '#title' => t('View used to select the nodes'),
            '#options' => $views,
            '#default_value' => isset($field['advanced_view']) ? $field['advanced_view'] : '--',
            '#description' => t('<p>Choose the "Views module" view that selects the nodes that can be referenced.<br />Note:</p>') .
              t('<ul><li>Only views that have fields will work for this purpose.</li><li>This will discard the "Content types" settings above. Use the view\'s "filters" section instead.</li><li>Use the view\'s "fields" section to display additional informations about candidate nodes on node creation/edition form.</li><li>Use the view\'s "sort criteria" section to determine the order in which candidate nodes will be displayed.</li></ul>'),
          );
           $form['advanced']['advanced_view_args'] = array(
            '#type' => 'textfield',
            '#title' => t('View arguments'),
            '#default_value' => isset($field['advanced_view_args']) ? $field['advanced_view_args'] : '',
            '#required' => FALSE,
            '#description' => t('Provide a comma separated list of arguments to pass to the view.'),
          );
        }
        else {
          $form['advanced']['no_view_help'] = array(
            '#value' => t('<p>The list of nodes that can be referenced can be based on a "Views module" view but no appropriate views were found. <br />Note:</p>') .
              t('<ul><li>Only views that have fields will work for this purpose.</li><li>This will discard the "Content types" settings above. Use the view\'s "filters" section instead.</li><li>Use the view\'s "fields" section to display additional informations about candidate nodes on node creation/edition form.</li><li>Use the view\'s "sort criteria" section to determine the order in which candidate nodes will be displayed.</li></ul>'),
          );
        }
      }
      return $form;
      
    case 'save':
      $settings = array();
       $settings[] = 'referenceable_types';
      if (module_exists('views')) {
        $settings[] = 'advanced_view';
        $settings[] = 'advanced_view_args';
      }
      return $settings;

    
    case 'database columns':
      $columns['nid'] = array('type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, 'index' => TRUE);
      $columns['quantity'] = array('type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'sortable' => FALSE, 'default' => '');
      return $columns;
    
  }
}

/**
 * Implementation of CCK hook_field().
 *
 * Validate and sanitize functions available
 */
function noderef_qty_field($op, &$node, $field, &$items, $teaser, $page) {
  switch ($op) {
    case 'validate':
      if (is_array($items)) {
        foreach ($items as $delta => $item) {
          if ($item['quantity'] != '') {
            if (is_numeric(trim($item['quantity']))) {
              if ($item['quantity'] < 1 || $item['quantity'] > 1000000000) {
                form_set_error($field['field_name'],t('"%qty" is not between 1 and 1 000 000 000.',array('%qty' => $item['quantity'])));
              }
            } else {
              form_set_error($field['field_name'],t('"%qty" is not a valid number.',array('%qty' => $item['quantity'])));
            }
          }
        }
     }
     break;

    case 'sanitize':
      foreach ($items as $delta => $item) {
        foreach ($item as $col => $dat) {
          $items[$delta]['safe_' . $col ] = check_plain($item[ $col ]);
        }
      }
      break;
  }
}

/**
 * Implementation of hook_content_is_empty().
 *
 * Ensures a Node Reference was selected 
 */
function noderef_qty_content_is_empty($item, $field) {
  if (empty($item['nid']) || empty($item['quantity'])) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Implementation of hook_widget_info().
 */
function noderef_qty_widget_info() {
  return array(
    'noderef_qty_entry' => array(
      'label' => t('Node Reference and Quantity'),
      'field types' => array('noderef_qty'),
      'multiple values' => CONTENT_HANDLE_CORE,
      'callbacks' => array(
        'default value' => CONTENT_CALLBACK_DEFAULT,
      ),
      'description' => t('An edit widget for Node Items and Quantities.' ),
    ),
  );
}

/**
 * Implementation of Form API's hook_elements().
 *
 * Returns a skeleton Form API array that defines callbacks
 * for the widget form.
 */
function noderef_qty_elements() {
  $elements = array('noderef_qty_entry' =>
    array(
      '#input' => TRUE,
      '#process' => array('noderef_qty_entry_process'),
    ),
  );

  return $elements;
}

/**
 * Process callback for widget
 *
 * Returns a Forms API array that defines the widget's editing form.
 */
function noderef_qty_entry_process($element, $edit, &$form_state, $form) {
  $defaults = $element['#value'];
  $field = content_fields($element['#field_name'], $element['#type_name']);

  $options = noderef_qty_allowed_values($field);
  $options = array('' => t('- None -')) + $options;

  $element['nid'] = array(
    '#title' => t('Item'),
    '#type' => 'select',
    '#default_value' => $defaults['nid'],
    '#description' => t('Please select an item from the dropdown. Choose - None - to delete this item and quantity.'),
    '#options' => $options,
    '#weight' => 2,
  );

  $element['quantity'] = array(
    '#title' => t('Quantity'),
    '#type' => 'textfield',
    '#description' => t('Please enter a quantity between 1 and 1 000 000 000. Leave blank to delete this item and quantity.'),
    '#default_value' => $defaults['quantity'],
    '#weight' => 3,
  );

  return $element;
}

/**
 * Implementation of hook_theme().
 */
function noderef_qty_theme() {
  return array(
    'noderef_qty_entry' => array(
      'arguments' => array('element' => NULL),
    ),
    'noderef_qty_formatter_default' => array(
      'arguments' => array('element' => NULL),
    ),
  );
}

/**
 * FAPI theme for an individual text elements.
 */
function theme_noderef_qty_entry($element) {
  return $element['#children'];
}

/**
 * Implementation of hook_widget().
 */
function noderef_qty_widget(&$form, &$form_state, $field, $items, $delta = 0) {
  $element = array(
    '#type' => $field['widget']['type'],
    '#default_value' => isset($items[$delta]) ? $items[$delta] : '',
  );
  return $element;
}

/**
 * Implementation of CCK's hook_field_formatter_info().
 *
 * Returns information about available field formatters.
 */
function noderef_qty_field_formatter_info() {
  return array(
    'default' => array(
       'label' => t('Node Reference and Quantitye Display'),
       'field types' => array('noderef_qty'),
    ),
  );
}

/**
 * Theme function for default formatter.
 */
function theme_noderef_qty_formatter_default($element = NULL) {
  if(empty($element['#item'])) {
    return '';
  }

  $item = $element['#item'];
  $fields = array('quantity','nid');
  $result = !empty($item['nid']) ? '<span class="noderef_qty_item">' : '';

  foreach($fields as $field) {
    if(!empty($item['safe_' . $field ]) && is_numeric($item['safe_' . $field ]) ) {
      switch($field) {
        
        case 'quantity':
        $result .= '<span class="noderef_qty_' . $field . '">(' . $item['safe_' . $field ] . ')</span> ';
        break;
        
        case 'nid':
        $nid = $item['safe_' . $field ];
        $node_title = db_result(db_query(db_rewrite_sql('SELECT n.title FROM {node} n WHERE n.nid = %d'), $nid));
        $result .= '<span class="noderef_qty_' . $field . '">' . $node_title . '</span> ';
        break;
        
      }
    }
  }

  if(!empty($item['safe_email' ])) {
    $result .= $sep . '<a class="noderef_qty_email" href="mailto:' . $item['safe_email' ] .
      '">' . $item['safe_email' ] . "</a>";
  }

  $result .= !empty($item['nid']) ? '</span>' : '';

  return $result;
}



/**
 * Theme preprocess function for field.tpl.php.
 *
 * The $variables array contains the following arguments:
 * - $node
 * - $field
 * - $items
 * - $teaser
 * - $page
 *
 * @see field.tpl.php
 *
 * TODO : this should live in theme/theme.inc, but then the preprocessor
 * doesn't get called when the theme overrides the template. Bug in theme layer ?
 */
function noderef_qty_preprocess_content_field(&$variables) {
  $element = $variables['element'];
  $field = content_fields($element['#field_name'], $element['#node']->type);

  $variables['node'] = $element['#node'];
  $variables['field'] = $field;
  $variables['items'] = array();

  if ($element['#single']) {
    // Single value formatter.
    foreach (element_children($element['items']) as $delta) {
      $variables['items'][$delta] = $element['items'][$delta]['#item'];
      // Use isset() to avoid undefined index message on #children when field values are empty.
      $variables['items'][$delta]['view'] = isset($element['items'][$delta]['#children']) ? $element['items'][$delta]['#children'] : '';
    }
  }
  else  {
    // Multiple values formatter.
    // We display the 'all items' output as $items[0], as if it was the
    // output of a single valued field.
    // Raw values are still exposed for all items.
    foreach (element_children($element['items']) as $delta) {
      $variables['items'][$delta] = $element['items'][$delta]['#item'];
    }
    $variables['items'][0]['view'] = $element['items']['#children'];
  }

  $variables['teaser'] = $element['#teaser'];
  $variables['page'] = $element['#page'];

  $field_empty = TRUE;

  foreach ($variables['items'] as $delta => $item) {
    if (!isset($item['view']) || (empty($item['view']) && (string)$item['view'] !== '0')) {
      $variables['items'][$delta]['empty'] = TRUE;
    }
    else {
      $field_empty = FALSE;
      $variables['items'][$delta]['empty'] = FALSE;
    }
  }

  $additions = array(
    'field_type' => $field['type'],
    'field_name' => $field['field_name'],
    'field_type_css' => strtr($field['type'], '_', '-'),
    'field_name_css' => strtr($field['field_name'], '_', '-'),
    'label' => check_plain(t($field['widget']['label'])),
    'label_display' => $element['#label_display'],
    'field_empty' => $field_empty,
    'template_files' => array(
      'content-field',
      'content-field-noderef_qty',
      'content-field-'. $element['#field_name'],
      'content-field-'. $element['#node']->type,
      'content-field-'. $element['#field_name'] .'-'. $element['#node']->type,
    ),
  );
  $variables = array_merge($variables, $additions);
}

/**
 * Implementation of hook_theme_registry_alter()
 *
 * Register our custom tpl suggestion with the Drupal Theming Registry.
 */
function noderef_qty_theme_registry_alter(&$theme_registry) {
    $thisPath = drupal_get_path('module', 'noderef_qty');
    array_unshift($theme_registry['content_field']['theme paths'],$thisPath);
}





/**
 * Collects an array of all candidate referenceable nodes.
 */
function noderef_qty_allowed_values($field) {
  $references = _noderef_qty_potential_references($field);

  $options = array();
  foreach ($references as $key => $value) {
    $options[$key] = $value['title'] . ' [nid:' . $key . ']';
  }

  return $options;
}


/**
 * Fetch an array of all candidate referenced nodes.
 *
 * This info is used in various places (allowed values, autocomplete results,
 * input validation...). Some of them only need the nids, others nid + titles,
 * others yet nid + titles + rendered row (for display in widgets).
 * The array we return contains all the potentially needed information, and lets
 * consumers use the parts they actually need.
 *
 * @param $field
 *   The field description.
 * @param $string
 *   Optional string to filter titles on (used by autocomplete).
 * @param $match
 *   Operator to match filtered name against, can be any of:
 *   'contains', 'equals', 'starts_with'
 * @param $ids
 *   Optional node ids to lookup (the $string and $match arguments will be
 *   ignored).
 * @param $limit
 *   If non-zero, limit the size of the result set.
 *
 * @return
 *   An array of valid nodes in the form:
 *   array(
 *     nid => array(
 *       'title' => The node title,
 *       'rendered' => The text to display in widgets (can be HTML)
 *     ),
 *     ...
 *   )
 */
function _noderef_qty_potential_references($field, $string = '', $match = 'contains', $ids = array(), $limit = NULL) {
  static $results = array();

  // Create unique id for static cache.
  $cid = $field['field_name'] .':'. $match .':'. ($string !== '' ? $string : implode('-', $ids)) .':'. $limit;
  if (!isset($results[$cid])) {
    $references = FALSE;
    if (module_exists('views') && !empty($field['advanced_view']) && $field['advanced_view'] != '--') {
      $references = _noderef_qty_potential_references_views($field, $string, $match, $ids, $limit);
    }
    // If the view doesn't exist, we got FALSE, and fallback to the regular 'standard mode'.

    if ($references === FALSE) {
      $references = _noderef_qty_potential_references_standard($field, $string, $match, $ids, $limit);
    }

    // Store the results.
    $results[$cid] = !empty($references) ? $references : array();
  }

  return $results[$cid];
}


/**
 * Helper function for _nodereference_potential_references():
 * case of Views-defined referenceable nodes.
 */
function _noderef_qty_potential_references_views($field, $string = '', $match = 'contains', $ids = array(), $limit = NULL) {
  $view_name = $field['advanced_view'];

  if ($view = views_get_view($view_name)) {
    // We add a display, and let it derive from the 'default' display.
    // TODO: We should let the user pick a display in the fields settings - sort of requires AHAH...
    $display = $view->add_display('content_references');
    $view->set_display($display);

    // TODO from merlinofchaos on IRC : arguments using summary view can defeat the style setting.
    // We might also need to check if there's an argument, and set *its* style_plugin as well.
    $view->display_handler->set_option('style_plugin', 'content_php_array_autocomplete');
    $view->display_handler->set_option('row_plugin', 'fields');
    // Used in content_plugin_style_php_array::render(), to get
    // the 'field' to be used as title.
    $view->display_handler->set_option('content_title_field', 'title');

    // Additional options to let content_plugin_display_references::query()
    // narrow the results.
    $options = array(
      'table' => 'node',
      'field_string' => 'title',
      'string' => $string,
      'match' => $match,
      'field_id' => 'nid',
      'ids' => $ids,
    );
    $view->display_handler->set_option('content_options', $options);

    // TODO : for consistency, a fair amount of what's below
    // should be moved to content_plugin_display_references

    // Limit result set size.
    $limit = isset($limit) ? $limit : 0;
    $view->display_handler->set_option('items_per_page', $limit);

    // Get arguments for the view.
    if (!empty($field['advanced_view_args'])) {
      // TODO: Support Tokens using token.module ?
      $view_args = array_map('trim', explode(',', $field['advanced_view_args']));
    }
    else {
      $view_args = array();
    }

    // We do need title field, so add it if not present (unlikely, but...)
    $fields = $view->get_items('field', $display);
    if (!isset($fields['title'])) {
      $view->add_item($display, 'field', 'node', 'title');
    }

    // If not set, make all fields inline and define a separator.
    $options = $view->display_handler->get_option('row_options');
    if (empty($options['inline'])) {
      $options['inline'] = drupal_map_assoc(array_keys($view->get_items('field', $display)));
    }
    if (empty($options['separator'])) {
      $options['separator'] = '-';
    }
    $view->display_handler->set_option('row_options', $options);

    // Make sure the query is not cached
    $view->is_cacheable = FALSE;

    // Get the results.
    $result = $view->execute_display($display, $view_args);
  }
  else {
    $result = FALSE;
  }
  
  return($result);
}


/**
 * Helper function for _nodereference_potential_references():
 * referenceable nodes defined by content types.
 */
function _noderef_qty_potential_references_standard($field, $string = '', $match = 'contains', $ids = array(), $limit = NULL) {
  $related_types = array();
  $where = array();
  $args = array();

  if (is_array($field['referenceable_types'])) {
    foreach (array_filter($field['referenceable_types']) as $related_type) {
      $related_types[] = "n.type = '%s'";
      $args[] = $related_type;
    }
  }

  $where[] = implode(' OR ', $related_types);

  if (!count($related_types)) {
    return array();
  }

  if ($string !== '') {
    $like = $GLOBALS["db_type"] == 'pgsql' ? "ILIKE" : "LIKE";
    $match_clauses = array(
      'contains' => "$like '%%%s%%'",
      'equals' => "= '%s'",
      'starts_with' => "$like '%s%%'",
    );
    $where[] = 'n.title '. (isset($match_clauses[$match]) ? $match_clauses[$match] : $match_clauses['contains']);
    $args[] = $string;
  }
  elseif ($ids) {
    $where[] = 'n.nid IN (' . db_placeholders($ids) . ')';
    $args = array_merge($args, $ids);
  }

  $where_clause = $where ? 'WHERE ('. implode(') AND (', $where) .')' : '';
  $sql = db_rewrite_sql("SELECT n.nid, n.title AS node_title, n.type AS node_type FROM {node} n $where_clause ORDER BY n.title, n.type");
  $result = $limit ? db_query_range($sql, $args, 0, $limit) : db_query($sql, $args);
  $references = array();
  while ($node = db_fetch_object($result)) {
    $references[$node->nid] = array(
      'title' => $node->node_title,
      'rendered' => check_plain($node->node_title),
    );
  }

  return $references;
}