You are here

money.module in Money field 7

Same filename and directory in other branches
  1. 5 money.module
  2. 6 money.module

This module defines the Money field.

File

money.module
View source
<?php

/**
 * @file
 * This module defines the Money field.
 */

/**
 * Implements hook_init().
 */
function money_init() {
  if (module_exists('diff')) {
    module_load_include('inc', 'money', 'includes/money.diff');
  }
  if (module_exists('feeds')) {
    module_load_include('inc', 'money', 'includes/money.feeds');
  }
}

/**
 * Implements hook_field_info().
 */
function money_field_info() {
  return array(
    'money' => array(
      'label' => t('Money'),
      'description' => t('This field stores and renders an amount with its currency.'),
      'settings' => array(
        'min' => '',
        'max' => '',
        'precision' => 10,
        'scale' => 2,
      ),
      'instance_settings' => array(
        'min' => '',
        'max' => '',
      ),
      'default_widget' => 'money_widget',
      'default_formatter' => 'money_default',
      'property_type' => 'money',
      'property_callbacks' => array(
        'money_field_property_info_callback',
      ),
    ),
  );
}

/**
 * Additional callback to adapt the property info of money field.
 *
 * @see entity_metadata_field_entity_property_info()
 */
function money_field_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
  $property =& $info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
  $property['type'] = $field['cardinality'] != 1 ? 'list<money>' : 'money';
  $property['getter callback'] = 'entity_metadata_field_verbatim_get';
  $property['setter callback'] = 'entity_metadata_field_verbatim_set';
  $property['auto creation'] = 'entity_property_create_array';
  $property['property info'] = array(
    'amount' => array(
      'label' => 'Amount',
      'type' => 'decimal',
      'setter callback' => 'entity_property_verbatim_set',
      'getter callback' => 'entity_property_verbatim_get',
    ),
    'currency' => array(
      'label' => 'Currency',
      'type' => 'text',
      'setter callback' => 'entity_property_verbatim_set',
      'getter callback' => 'entity_property_verbatim_get',
    ),
  );
  unset($property['query callback']);
}

/**
 * Implements hook_field_settings_form().
 */
function money_field_settings_form($field, $instance, $has_data) {
  $settings = $field['settings'];
  $form = array();
  $form['precision'] = array(
    '#type' => 'select',
    '#title' => t('Precision'),
    '#options' => drupal_map_assoc(range(10, 32)),
    '#default_value' => $settings['precision'],
    '#description' => t('The total number of digits to store in the database, including those to the right of the decimal.'),
    '#disabled' => $has_data,
  );
  $form['scale'] = array(
    '#type' => 'select',
    '#title' => t('Scale'),
    '#options' => drupal_map_assoc(range(0, 10)),
    '#default_value' => $settings['scale'],
    '#description' => t('The number of digits to the right of the decimal.'),
    '#disabled' => $has_data,
  );
  return $form;
}

/**
 * Implements hook_field_info_alter().
 */
function money_field_info_alter(&$info) {

  // Add min/max settings to decimal field types.
  if (isset($info['money'])) {
    $precision = $info['money']['settings']['precision'];
    $scale = $info['money']['settings']['scale'];
    $min = (double) ('-' . str_repeat('9', $precision - $scale) . '.' . str_repeat('9', $scale));
    $max = (double) (str_repeat('9', $precision - $scale) . '.' . str_repeat('9', $scale));
    $info['money']['settings']['min'] = $min;
    $info['money']['settings']['max'] = $max;
  }
}

/**
 * Implements hook_field_instance_settings_form().
 */
function money_field_instance_settings_form($field, $instance) {
  $settings = $instance['settings'];
  $form['min'] = array(
    '#type' => 'textfield',
    '#title' => t('Minimum'),
    '#default_value' => $settings['min'],
    '#description' => t('The minimum value that should be allowed in this field. Leave blank for no minimum.'),
    '#element_validate' => array(
      '_element_validate_limit',
    ),
    '#weight' => -1,
  );
  $form['max'] = array(
    '#type' => 'textfield',
    '#title' => t('Maximum'),
    '#default_value' => $settings['max'],
    '#description' => t('The maximum value that should be allowed in this field. Leave blank for no maximum.'),
    '#element_validate' => array(
      '_element_validate_limit',
    ),
    '#weight' => -1,
  );
  return $form;
}

/**
 * Validate the element for min/max limits.
 * Copied from http://drupalcode.org/sandbox/nouriassafi/1603812.git fork of formatted_number module.
 */
function _element_validate_limit($element, &$form_state) {
  $value = $element['#value'];
  $field_name = $form_state['values']['instance']['field_name'];
  $field = $form_state['field'][$field_name][LANGUAGE_NONE]['field'];
  if ($value != '' && !is_numeric($value)) {
    form_error($element, t('%name must be a number.', array(
      '%name' => $element['#title'],
    )));
  }
  if ($value != '' && isset($field['settings']['min'])) {
    if ($element['#name'] == 'instance[settings][min]' && $value < $field['settings']['min']) {
      form_error($element, t('%name: the value may be no less than %min.', array(
        '%name' => $element['#title'],
        '%min' => $field['settings']['min'],
      )));
    }
    if ($element['#name'] == 'instance[settings][max]' && $value > $field['settings']['max']) {
      form_error($element, t('%name: the value may be no greater than %max.', array(
        '%name' => $element['#title'],
        '%max' => $field['settings']['max'],
      )));
    }
  }
}

/**
 * Implements hook_field_validate().
 *
 * Possible error codes:
 * - 'money_min': The value is less than the allowed minimum value.
 * - 'money_max': The value is greater than the allowed maximum value.
 * - 'money_currency': Currency is missing.
 * - 'money_amount': Amount is missing.
 */
function money_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  foreach ($items as $delta => $item) {
    if ($item['amount'] != '') {
      $min = is_numeric($instance['settings']['min']) ? $instance['settings']['min'] : $field['settings']['min'];
      if (is_numeric($min) && $item['amount'] < $min) {
        $errors[$field['field_name']][$langcode][$delta][] = array(
          'error' => 'money_min',
          'message' => t('%name: the value may be no less than %min.', array(
            '%name' => $instance['label'],
            '%min' => $min,
          )),
        );
      }
      $max = is_numeric($instance['settings']['max']) ? $instance['settings']['max'] : $field['settings']['max'];
      if (is_numeric($max) && $item['amount'] > $max) {
        $errors[$field['field_name']][$langcode][$delta][] = array(
          'error' => 'money_max',
          'message' => t('%name: the value may be no greater than %max.', array(
            '%name' => $instance['label'],
            '%max' => $max,
          )),
        );
      }
      if (empty($item['currency'])) {
        $errors[$field['field_name']][$langcode][$delta][] = array(
          'error' => 'money_currency',
          'message' => t('%name: currency is required when an amount is specified.', array(
            '%name' => $instance['label'],
          )),
        );
      }
    }
    if (!is_numeric($item['amount']) && $item['currency']) {
      $errors[$field['field_name']][$langcode][$delta][] = array(
        'error' => 'money_amount',
        'message' => t('%name: a valid amount is required when a currency is specified.', array(
          '%name' => $instance['label'],
        )),
      );
    }
  }
}

/**
 * Implements hook_field_is_empty().
 */
function money_field_is_empty($item, $field) {
  if (!is_numeric($item['amount']) && empty($item['currency'])) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Implements hook_field_formatter_info().
 */
function money_field_formatter_info() {
  return array(
    'money_default' => array(
      'label' => t('Default'),
      'field types' => array(
        'money',
      ),
    ),
    'money_unformatted' => array(
      'label' => t('Unformatted'),
      'field types' => array(
        'money',
      ),
    ),
  );
}

/**
 * Implements hook_field_formatter_view().
 */
function money_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $element = array();

  // Dependency required by CurrencyLocalePattern class.
  ctools_include('export');
  foreach ($items as $delta => $item) {
    $currency = $item['currency'];
    $currency_object = currency_load($currency);
    $currency_pattern = CurrencyLocalePattern::loadFromEnv();
    $symbol = $currency_object->sign;
    $decimal_separator = $currency_pattern->symbol_decimal_separator;
    $grouping_separator = $currency_pattern->symbol_grouping_separator;

    // Amount with currency-based decimal and grouping separators.
    $amount = number_format($item['amount'], $field['settings']['scale'], $decimal_separator, $grouping_separator);

    // Amount with currency-based decimal and grouping separators and a currency symbol.
    $amount_with_symbol = $display['type'] == 'money_default' ? $currency_object
      ->format($item['amount']) : $item['amount'];
    $output = '';
    foreach (explode('|', $instance['widget']['settings']['currency_display_mode']) as $option) {
      switch ($option) {
        case 'a':

          // The amount.
          $output .= $amount;
          break;
        case 'as':

          // Amount with currency symbol.
          $output .= $amount_with_symbol;
          break;
        case 'c':

          // Currency code.
          $output .= $currency;
          break;
        case 's':

          // Currency symbol.
          $output .= $symbol;
          break;
        case '+':

          // Separator.
          $output .= $display['type'] == 'money_default' ? " " : ' ';
          break;
      }
    }
    $element[$delta] = array(
      '#markup' => $output,
    );
  }
  return $element;
}

/**
 * Implements hook_field_widget_info().
 */
function money_field_widget_info() {
  return array(
    'money_widget' => array(
      'label' => t('Amount and currency'),
      'field types' => array(
        'money',
      ),
      'settings' => array(
        'currency_select_mode' => 'name',
        'currency_display_mode' => 'ac',
        'decimals_display_mode' => 'field',
        'currencies' => array(
          'allowed_currencies' => array(),
        ),
      ),
    ),
  );
}

/**
 * Implements hook_field_widget_form().
 */
function money_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $value = isset($items[$delta]['amount']) ? $items[$delta]['amount'] : '';

  // Dependency required by CurrencyLocalePattern class.
  ctools_include('export');
  $decimal_separator = CurrencyLocalePattern::loadFromEnv()->symbol_decimal_separator;

  // Substitute the decimal separator.
  $value = strtr($value, '.', $decimal_separator);
  $element += array(
    '#type' => 'fieldset',
    '#tree' => TRUE,
    '#attributes' => array(
      'class' => array(
        'container-inline',
      ),
    ),
  );
  $element['amount'] = array(
    '#type' => 'textfield',
    '#default_value' => $value,
    // Allow a slightly larger size than the field length to allow for some
    // configurations where all characters won't fit in input field.
    '#size' => $field['settings']['precision'] + 4,
    // Allow two extra characters for signed values and decimal separator.
    '#maxlength' => $field['settings']['precision'] + 2,
    '#attributes' => array(
      'class' => array(
        'formatted-number',
      ),
      'decimals' => $field['settings']['scale'],
    ),
    '#attached' => array(
      'css' => array(),
      'js' => array(
        // Removed dependency to format_number module.
        // Removed dependency to formatted_number module.
        array(
          'data' => array(),
          // Removed dependency to format_number module.
          'type' => 'setting',
        ),
      ),
    ),
  );
  $element['currency'] = array(
    '#type' => 'select',
    '#default_value' => isset($items[$delta]['currency']) ? $items[$delta]['currency'] : array(),
    '#options' => money_get_widget_currencies($instance, $element),
  );
  $element['#element_validate'][] = 'money_field_widget_validate';
  return $element;
}

/**
 * Build currency options for the given field/widget.
 */
function money_get_widget_currencies($instance, $element) {
  $widget = $instance['widget'];
  $settings = $widget['settings'];

  // Currently implemented modes: code, name. See money_field_widget_settings_form().
  $mode = $settings['currency_select_mode'];

  // Prepare the array of allowed currencies.
  if (isset($settings['currencies']['allowed_currencies']) && is_array($settings['currencies']['allowed_currencies'])) {

    // Obtain the list of allowed currencies. Note that this array is in the form of 'code' => boolean.
    $allowed_currencies = array_filter($settings['currencies']['allowed_currencies']);
  }
  else {

    // Initialize array when the list has not been already set in field settings.
    $allowed_currencies = array();
  }

  // When no currency has been specified in widget settings we allow them all.
  if (empty($allowed_currencies)) {

    // Note that this array is built in the form of 'code' => 'name'.
    $allowed_currencies = currency_options();
  }
  else {

    // One or more currencies have been specified in widget settings.
    if ($mode == 'name') {

      // Build the array in the form of 'code' => 'name' extracting the
      // allowed currencies from the array returned from currency_api.
      $allowed_currencies = array_intersect_key(currency_options(), $allowed_currencies);
    }
  }

  // If the requested mode is 'code', then we need to transform the array
  // so that item keys are also used for values.
  if ($mode == 'code') {
    $allowed_currencies = array_keys($allowed_currencies);
    $allowed_currencies = array_combine($allowed_currencies, $allowed_currencies);
  }

  // When field is not required, an additional empty currency is pushed on top of the resulting list.
  if (!$element['#required']) {
    $allowed_currencies = array(
      '' => $mode == 'code' ? '---' : t('-- Select currency --'),
    ) + $allowed_currencies;
  }
  return $allowed_currencies;
}

/**
 * Implements hook_field_widget_settings_form().
 */
function money_field_widget_settings_form($field, $instance) {
  $widget = $instance['widget'];
  $settings = $widget['settings'];
  $options = array(
    'code' => t('Currency code'),
    'name' => t('Currency name'),
  );
  $form['currency_select_mode'] = array(
    '#type' => 'radios',
    '#title' => t('Currency selection mode'),
    '#options' => $options,
    '#default_value' => $settings['currency_select_mode'],
    '#required' => TRUE,
    '#description' => t('Choose the format of the label that will be displayed for options of the currency select list.'),
  );
  $options = money_get_display_modes();
  $form['currency_display_mode'] = array(
    '#type' => 'select',
    '#title' => t('Currency display mode'),
    '#options' => $options,
    '#default_value' => $settings['currency_display_mode'],
    '#required' => TRUE,
    '#description' => t('Choose the format that will be used to display this money field when a node is rendered.'),
  );
  if (function_exists('currency_api_get_currencies')) {
    $options = array(
      'field' => t('Field precision'),
      'currency' => t('Currency precision'),
    );
    $form['decimals_display_mode'] = array(
      '#type' => 'radios',
      '#title' => t('Decimals display mode'),
      '#options' => $options,
      '#default_value' => $settings['decimals_display_mode'],
      '#required' => TRUE,
      '#description' => t('Choose the method to select the number of decimals used to display the field. The standard precision for each currency is displayed in the <em>Available currencies</em> list.'),
    );
    $currency_options = array();
    foreach (currency_api_get_currencies() as $code => $currency) {
      $currency_options[$code] = $currency['name'] . ' [' . $currency['decimals'] . ']';
    }
  }
  else {
    $currency_options = currency_options();
  }
  $form['currencies'] = array(
    '#type' => 'fieldset',
    '#title' => t('Available currencies'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#description' => t('Choose the currencies that you want to enable for this field. Do not select any currency to enable them all.'),
  );
  if (function_exists('currency_api_get_currencies')) {
    $form['currencies']['#description'] .= ' ' . t('The number between square brakets indicates the standard precision for each currency.');
  }
  if (isset($settings['currencies']['allowed_currencies']) && is_array($settings['currencies']['allowed_currencies'])) {

    // Get filtered array.
    $allowed_currencies = array_filter($settings['currencies']['allowed_currencies']);

    // If not empty, create array for the form element values.
    if (!empty($allowed_currencies)) {
      $allowed_currencies = array_keys($allowed_currencies);
      $allowed_currencies = array_combine($allowed_currencies, $allowed_currencies);
    }
  }
  else {
    $allowed_currencies = array();
  }
  $form['currencies']['allowed_currencies'] = array(
    '#type' => 'checkboxes',
    '#options' => $currency_options,
    '#default_value' => $allowed_currencies,
    '#checkall' => TRUE,
    '#prefix' => '<div class="money-field-currency-checkboxes">',
    '#suffix' => '</div>',
    '#attached' => array(
      'css' => array(
        drupal_get_path('module', 'money') . '/money.css',
      ),
    ),
  );
  return $form;
}

/**
 * Obtain display modes for money fields.
 */
function money_get_display_modes() {
  return array(
    'as' => t('Localized format of amount with currency symbol'),
    's|a' => t('Symbol + Amount'),
    's|+|a' => t('Symbol + Space + Amount'),
    'a|s' => t('Amount + Symbol'),
    'a|+|s' => t('Amount + Space + Symbol'),
    's|a|+|c' => t('Symbol + Amount + Space + Currency Code'),
    's|+|a|+|c' => t('Symbol + Space + Amount + Space + Currency Code'),
    'a|+|c' => t('Amount + Space + Currency Code'),
    'c|+|a' => t('Currency Code + Space + Amount'),
    'c|+|as' => t('Currency Code + Space + Localized format of amount with currency symbol'),
    'c|+|a|s' => t('Currency Code + Space + Amount + Symbol'),
    'c|+|a|+|s' => t('Currency Code + Space + Amount + Space + Symbol'),
  );
}

/**
 * FAPI validation of an individual number element.
 */
function money_field_widget_validate($element, &$form_state) {
  $instance = field_widget_instance($element, $form_state);
  $value = $element['amount']['#value'];

  // Dependency required by CurrencyLocalePattern class.
  ctools_include('export');
  $decimal_separator = CurrencyLocalePattern::loadFromEnv()->symbol_decimal_separator;

  // Reject invalid characters.
  if (!empty($value)) {
    $regexp = '@([^-0-9\\' . $decimal_separator . '])|(.-)@';
    $message = t('Only numbers and the decimal separator (@separator) allowed in %field.', array(
      '%field' => $instance['label'],
      '@separator' => $decimal_separator,
    ));
    if ($value != preg_replace($regexp, '', $value)) {
      form_error($element, $message);
    }
    else {

      // Verify that only one decimal separator exists in the field.
      if (substr_count($value, $decimal_separator) > 1) {
        $message = t('%field: There should only be one decimal separator (@separator).', array(
          '%field' => t($instance['label']),
          '@separator' => $decimal_separator,
        ));
        form_error($element, $message);
      }
      else {

        // Substitute the decimal separator; things should be fine.
        $value = strtr($value, $decimal_separator, '.');
      }
      form_set_value($element['amount'], $value, $form_state);
    }
  }
}

/**
 * Implements hook_field_widget_error().
 */
function money_field_widget_error($element, $error, $form, &$form_state) {
  form_error($element, $error['message']);
}

Functions