money.module in Money field 5
Same filename and directory in other branches
This module defines the "money" CCK field. It uses the Currency API, which is included in the Currency module, to get a list of valid currencies.
Only amounts with 2 decimals can be used. Any decimal separator and any digit group separator can be used, but it defaults to the comma and the dot respectively, which is according to ISO 31-0. The separators can be changed at any point, only integers are stored in the database.
File
money.moduleView source
<?php
/**
* @file
* This module defines the "money" CCK field. It uses the Currency API, which
* is included in the Currency module, to get a list of valid currencies.
*
* Only amounts with 2 decimals can be used. Any decimal separator and any
* digit group separator can be used, but it defaults to the comma and the dot
* respectively, which is according to ISO 31-0. The separators can be changed
* at any point, only integers are stored in the database.
*/
//----------------------------------------------------------------------------
// CCK hooks.
/**
* Implementation of hook_field_info().
*/
function money_field_info() {
return array(
'money' => array(
'label' => t('Money'),
),
);
}
/**
* Implementation of hook_field_settings().
*/
function money_field_settings($op, $field) {
switch ($op) {
case 'form':
$form = array();
$form['decimal_separator'] = array(
'#type' => 'textfield',
'#title' => t('Decimal separator'),
'#default_value' => _money_get_decimal_separator($field['decimal_separator']),
'#size' => 5,
'#maxlength' => 255,
'#description' => t('Three decimal separators are used across the planet: the dot
(English-speaking countries), the comma (Europe) and the momayyez
(Arab world and Iran). ISO 31-0 specifies both the dot and the comma
as valid, but prefers the comma, this is also the default.'),
);
$form['digit_group_separator'] = array(
'#type' => 'textfield',
'#title' => t('Digit group separator'),
'#default_value' => _money_get_digit_group_separator($field['digit_group_separator']),
'#size' => 5,
'#maxlength' => 255,
'#description' => t('Three digit group separators are used across the planet: the comma
(English-speaking countries), the dot (Europe) and the space. ISO
31-0 specifies only the space as valid, this is also the default.'),
);
$form['displayed_decimals'] = array(
'#type' => 'select',
'#title' => t('Displayed decimals'),
'#options' => array(
0,
1,
2,
),
'#default_value' => _money_get_displayed_decimals($field['displayed_decimals']),
'#description' => t('The number of decimals that will be displayed in the formatters.
This does <em>not</em> affect the stored number of decimals, or the
number of decimals that can be entered, which will always be 2.'),
);
$form['currency_list'] = array(
'#value' => theme('money_field_settings_currency_list', currency_api_get_list()),
);
$form['allowed_currencies'] = array(
'#type' => 'textarea',
'#rows' => 5,
'#title' => t('Currencies'),
'#description' => t('Enter the 3-letter ISO codes for the currencies that you want to allow, separated by commas. Leave empty to allow all currencies.'),
'#default_value' => isset($field['allowed_currencies']) ? $field['allowed_currencies'] : '',
);
return $form;
case 'validate':
if (!in_array($field['displayed_decimals'], array(
0,
1,
2,
))) {
form_set_error('displayed_decimals', t('Invalid number of displayed decimals. Valid numbers are 0, 1 and 2.'));
}
// Empty list of allowed currencies means that *all* currencies will be
// used!
if (!empty($field['allowed_currencies'])) {
$valid_currencies = array_keys(currency_api_get_list());
$allowed_currencies = _money_parse_currencies($field['allowed_currencies']);
foreach ($allowed_currencies as $currency) {
if (!in_array($currency, $valid_currencies)) {
form_set_error('allowed_currencies', t('The currency %currency is not a valid currency.', array(
'%currency' => $currency,
)));
}
}
}
break;
case 'save':
return array(
'decimal_separator',
'digit_group_separator',
'displayed_decimals',
'allowed_currencies',
);
case 'database columns':
$columns['amount'] = array(
'type' => 'bigint',
'length' => 13,
'not null' => TRUE,
'default' => 0,
'unsigned' => FALSE,
'sortable' => TRUE,
);
$columns['currency'] = array(
'type' => 'varchar',
'length' => 3,
);
return $columns;
case 'filters':
$currencies = _money_parse_currencies($field['allowed_currencies']);
$options = array_combine($currencies, $currencies);
$filters['currency'] = array(
'name' => t('Filter by currency'),
// It seems Views requires your operator handler to contain at least
// an "OR" operator if you want the multiple select to mark the
// selected currencies as selected when a user revisits the form. It's
// stored correctly in the DB though. So, as a work-around, I opted to
// use "OR" and "NOR" and then use the desired operators in the actual
// filter handler.
'operator' => array(
'OR' => t('Is One Of'),
'NOR' => t('Is None Of'),
),
'value' => array(
'#type' => 'select',
'#multiple' => TRUE,
'#options' => $options,
),
'handler' => 'money_views_handler_filter_currency',
'help' => t('This filter allows you to filter by currency (or multiple currencies).'),
);
$filters['amount'] = array(
'name' => t('Filter by amount'),
'operator' => 'views_handler_operator_gtlt',
'value' => array(
'#type' => 'textfield',
'#size' => 5,
),
'handler' => 'money_views_handler_filter_amount',
'help' => t('This filter allows you to filter by amount.'),
);
return $filters;
}
}
/**
* Implementation of hook_field().
*/
function money_field($op, &$node, $field, &$items, $teaser, $page) {
switch ($op) {
case 'validate':
$allowed_currencies = _money_parse_currencies($field['allowed_currencies']);
if (is_array($items)) {
foreach ($items as $delta => $item) {
// Generate the regular expression to validate the entered amounts.
$decimal_separator = preg_quote(_money_get_decimal_separator($field['decimal_separator']));
$digit_group_separator = preg_quote(_money_get_digit_group_separator($field['digit_group_separator']));
$regexp = "/^-?(((\\d{1,3}" . $digit_group_separator . ")?(\\d{3}" . $digit_group_separator . ")*(\\d{3}){1})|\\d+)(" . $decimal_separator . "\\d{1,2})?\$/";
// Validate the currency.
if (!in_array($item['currency'], $allowed_currencies)) {
form_set_error($field['field_name'] . '][' . $delta . '][currency', t('The currency %currency is not allowed.', array(
'%currency' => t($item['currency']),
)));
}
// Validate the amount:
// - allow empty value when the field is not required
// - ensure the amount was entered according to the format
if ($field['required'] && $item['amount'] == '') {
form_set_error($field['field_name'] . '][' . $delta . '][amount', t('You did not enter an amount.'));
}
else {
if (!empty($item['amount']) && !preg_match($regexp, $item['amount'])) {
form_set_error($field['field_name'] . '][' . $delta . '][amount', t('The amount is formatted invalidly.'));
}
}
}
}
break;
}
}
/**
* Implementation of hook_field_formatter_info().
*/
function money_field_formatter_info() {
return array(
'code_after_amount' => array(
'label' => t('3-letter code after amount'),
'field types' => array(
'money',
),
),
'code_before_amount' => array(
'label' => t('3-letter code before amount'),
'field types' => array(
'money',
),
),
'full_after_amount' => array(
'label' => t('Full name after amount'),
'field types' => array(
'money',
),
),
'full_before_amount' => array(
'label' => t('Full name before amount'),
'field types' => array(
'money',
),
),
'symbol_after_amount' => array(
'label' => t('Symbol after amount'),
'field types' => array(
'money',
),
),
'symbol_before_amount' => array(
'label' => t('Symbol before amount'),
'field types' => array(
'money',
),
),
);
}
/**
* Implementation of hook_field_formatter().
*/
function money_field_formatter($field, $item, $formatter, $node) {
if (empty($item['amount'])) {
return '';
}
else {
$decimal_separator = _money_get_decimal_separator($field['decimal_separator']);
$digit_group_separator = _money_get_digit_group_separator($field['digit_group_separator']);
$displayed_decimals = _money_get_displayed_decimals($field['displayed_decimals']);
$formatted_amount = check_plain(number_format($item['amount'] / 100, $displayed_decimals, $decimal_separator, $digit_group_separator));
$currencies = currency_api_get_list();
$symbols = currency_api_get_symbols();
switch ($formatter) {
case 'code_after_amount':
return $formatted_amount . ' ' . $item['currency'];
case 'code_before_amount':
return $item['currency'] . ' ' . $formatted_amount;
case 'full_after_amount':
return $formatted_amount . ' ' . $currencies[$item['currency']];
case 'full_before_amount':
return $currencies[$item['currency']] . ' ' . $formatted_amount;
case 'symbol_after_amount':
return $formatted_amount . ' ' . $symbols[$item['currency']];
case 'symbol_before_amount':
return $symbols[$item['currency']] . ' ' . $formatted_amount;
}
}
}
/**
* Implementation of hook_widget_info().
*/
function money_widget_info() {
return array(
'money_default' => array(
'label' => 'Select list for the currency, text field for the amount',
'field types' => array(
'money',
),
),
);
}
/**
* Implementation of hook_widget().
*/
function money_widget($op, &$node, $field, &$items) {
if ($field['widget']['type'] == 'money_default') {
switch ($op) {
case 'prepare form values':
$decimal_separator = _money_get_decimal_separator($field['decimal_separator']);
$digit_group_separator = _money_get_digit_group_separator($field['digit_group_separator']);
$displayed_decimals = _money_get_displayed_decimals($field['displayed_decimals']);
if (!count($items)) {
$items[0] = array();
}
else {
foreach ($items as $delta => $item) {
$items[$delta]['amount'] = check_plain(number_format($item['amount'] / 100, $displayed_decimals, $decimal_separator, $digit_group_separator));
}
}
break;
case 'form':
drupal_add_css(drupal_get_path('module', 'money') . '/money.css');
$decimal_separator = _money_get_decimal_separator($field['widget']['decimal_separator']);
$allowed_currencies = _money_parse_currencies($field['allowed_currencies']);
// Variables to be used in the "currency" form item.
$currency_options = array_combine($allowed_currencies, $allowed_currencies);
// Variables to be used in the "amount" form item.
if (isset($field['widget']['default_value'][0]['amount'])) {
$amount_default = check_plain(number_format($field['widget']['default_value'][0]['amount'] / 100, 2, $decimal_separator, $digit_group_separator));
}
else {
$amount_default = check_plain(number_format("0{$decimal_separator}00" / 100, 2, $decimal_separator, $digit_group_separator));
}
$amount_description = t('Use "@decimal_separator" as the decimal separator and (optionally)
"@digit_group_separator" as the digit group separator. You can
only enter two decimals.', array(
'@decimal_separator' => $field['decimal_separator'],
'@digit_group_separator' => $field['digit_group_separator'],
));
// If this field is configured as a multiple value field, make sure
// that there are at least 3 form items.
while ($field['multiple'] && count($items) < 3) {
$items[] = array();
}
// Create the prefix in which we'll store first the label, then a
// container div in which we'll put the actual form elements.
$prefix = '<div class="form-item">';
$prefix .= '<label>' . t($field['widget']['label']);
if (!empty($field['required'])) {
$prefix .= '<span class="form-required" title="' . t('This field is required.') . '">*</span>';
}
$prefix .= '</label>';
// Actual form creation begins here.
$form = array();
$form[$field['field_name']]['#tree'] = TRUE;
$form[$field['field_name']]['#prefix'] = $prefix;
$form[$field['field_name']]['#type'] = $field['multiple'] ? 'fieldset' : 'markup';
$form[$field['field_name']]['#suffix'] = '</div>';
$description = $amount_description;
$description .= !empty($field['widget']['description']) ? '<br />' . $field['widget']['description'] : '';
foreach ($items as $delta => $item) {
// These are the actual form items for each money field.
$form[$field['field_name']][$delta]['#tree'] = TRUE;
$form[$field['field_name']][$delta]['currency'] = array(
'#type' => 'select',
'#options' => $currency_options,
'#default_value' => isset($item['currency']) ? $item['currency'] : $field['widget']['default_value'][0]['currency'],
'#attributes' => array(
'class' => 'money-field money-field-currency',
),
'#prefix' => '<div class="container-inline money-field-form-items">',
);
$form[$field['field_name']][$delta]['amount'] = array(
'#type' => 'textfield',
'#size' => 20,
'#maxlength' => 25,
'#default_value' => isset($item['amount']) ? $item['amount'] : $amount_default,
'#attributes' => array(
'class' => 'money-field money-field-amount',
),
'#description' => $delta == end(array_keys($items)) ? $description : NULL,
'#suffix' => '</div>',
);
}
return $form;
case 'process form values':
foreach ($items as $delta => $item) {
if (empty($item['amount'])) {
unset($items[$delta]['amount']);
}
else {
$items[$delta]['amount'] = _money_amount_to_integer($item['amount'], $field['decimal_separator'], $field['digit_group_separator']);
}
}
break;
}
}
}
//----------------------------------------------------------------------------
// Token hooks.
/**
* Implementation of hook_token_values().
*/
function money_token_values($type, $object = NULL) {
if ($type == 'field') {
$tokens = array();
$first = $object[0];
$tokens['money'] = $first['view'];
$tokens['currency'] = $first['currency'];
$tokens['amount'] = $first['amount'];
return $tokens;
}
}
/**
* Implementation of hook_token_list().
*/
function money_token_list($type = 'all') {
if ($type == 'field' || $type == 'all') {
$tokens = array();
$tokens['money']['money'] = t('The combination of both the currency and the amount.');
$tokens['money']['currency'] = t('A 3-letter ISO currency code.');
$tokens['money']['amount'] = t('An amount, formatted as specified in the field settings.');
return $tokens;
}
}
//----------------------------------------------------------------------------
// Views handlers.
/**
* Views filter handler: amount filter.
*/
function money_views_handler_filter_amount($op, $filter_data, $filter_info, &$query) {
$converted_amount = _money_amount_to_integer($filter_data['value'], $filter_info['content_field']['decimal_separator'], $filter_info['content_field']['digit_group_separator']);
$table = $filter_info['table'];
$table_field = $filter_info['content_db_info']['columns']['amount']['column'];
$operator = $filter_data['operator'];
$query
->add_table($table);
$query
->add_where("{$table}.{$table_field} {$operator} ('" . $converted_amount . "')");
}
/**
* Views filter handler: currency filter.
*/
function money_views_handler_filter_currency($op, $filter_data, $filter_info, &$query) {
$currencies = $filter_data['value'];
$table = $filter_info['table'];
$table_field = $filter_info['content_db_info']['columns']['currency']['column'];
// For the rationale behind this, see the comment for the "currency" filter
// in money_field_settings().
$operator = $filter_data['operator'] == 'OR' ? 'IN' : 'NOT IN';
$query
->add_table($table);
$query
->add_where("{$table}.{$table_field} {$operator} ('" . implode("','", $filter_data['value']) . "')");
}
//----------------------------------------------------------------------------
// Private functions.
/**
* Parse currency codes from a comma-separated list.
*
* @param $currencies_string
* A string containing a list of currency codes, separated by commas. If the
* string is empty, all currencies will be returned.
* @return
* An array of currency code.
*/
function _money_parse_currencies($currencies_string) {
return empty($currencies_string) ? array_keys(currency_api_get_list()) : explode(',', str_replace(' ', '', trim($currencies_string)));
}
/**
* Get the decimal separator from a variable, use the default if the variable
* is empty.
*
* @param $decimal_separator
* A variable that possibly contains a decimal separator.
* @return
* A decimal separator, either the variable or the default (a comma).
*/
function _money_get_decimal_separator($decimal_separator = NULL) {
return !empty($decimal_separator) ? $decimal_separator : ',';
}
/**
* Get the digit group separator from a variable, use the default if the
* variable is empty.
*
* @param $digit_group_separator
* A variable that possibly contains a digit group separator.
* @return
* A digit group separator, either the variable or the default (a space).
*/
function _money_get_digit_group_separator($digit_group_separator = NULL) {
return !empty($digit_group_separator) ? $digit_group_separator : ' ';
}
/**
* Get the number of displayed decimals, use the default if the variable is
* empty.
*
* @param $displayed_decimals
* A variable that possibly contains the number of displayed decimals
* @return
* A number of displayed decimals, either the variable or the default (2).
*/
function _money_get_displayed_decimals($displayed_decimals = NULL) {
return isset($displayed_decimals) ? $displayed_decimals : 2;
}
/**
* Convert the amount (a string entered by the user) to an integer.
*
* @param $amount
* The amount to convert.
* @return
* The corresponding integer.
*/
function _money_amount_to_integer($amount, $decimal_separator, $digit_group_separator) {
$decimal_separator = _money_get_decimal_separator($decimal_separator);
$digit_group_separator = _money_get_digit_group_separator($digit_group_separator);
// Convert the entered amount to be compatible with PHP's number
// notation: a dot as a decimal separator, nothing as a digit
// group separator.
$converted_amount = str_replace(array(
$decimal_separator,
$digit_group_separator,
), array(
'.',
'',
), $amount);
// Now convert the amount to make it storable as an integer.
// We are always working with a maximum of 2 decimals, this means
// that one unit in the database corresponds to 1/100th of a unit
// in reality (i.e. in forms and on display).
$converted_amount *= 100;
// PHP's conversion from floats to ints is choppy at best, so make sure it
// gets converted properly.
$converted_amount = $converted_amount < 0 ? (int) ($converted_amount - 0.01) : (int) ($converted_amount + 0.01);
return $converted_amount;
}
//----------------------------------------------------------------------------
// Themeing functions.
/**
* @ingroup themeable
* @{
*/
/**
* Format the list of currencies that is displayed in the money field settings
* form.
*
* @param $currencies
* An array of currencies, where the keys are the currency codes and the
* values are the full names, with bracketed currency codes appended. (An
* array returned by currency_api_get_list()).
* @return
* A rendered list of currencies.
*/
function theme_money_field_settings_currency_list($currencies) {
$output = '';
$cols = 2;
$width = 100 / $cols;
$chunks = array_chunk(array_values($currencies), 2);
$output .= '<div id="money-field-settings-currency-list" class="clear-block">';
$output .= '<h3>' . t('Available currencies') . '</h3>';
foreach ($chunks as $chunk) {
$output .= '<div style="float: left; width:' . $width . '%">';
$output .= theme('item_list', $chunk);
$output .= '</div>';
}
$output .= '</div>';
return $output;
}
/**
* @} End of "ingroup themeable".
*/
//----------------------------------------------------------------------------
// PHP 4 compatibility.
/**
* Replace array_combine().
*
* Borrowed from PHP Compat 1.5 (http://pear.php.net/package/PHP_Compat).
*/
if (!function_exists('array_combine')) {
function array_combine($keys, $values) {
if (!is_array($keys)) {
user_error('array_combine() expects parameter 1 to be array, ' . gettype($keys) . ' given', E_USER_WARNING);
return;
}
if (!is_array($values)) {
user_error('array_combine() expects parameter 2 to be array, ' . gettype($values) . ' given', E_USER_WARNING);
return;
}
$key_count = count($keys);
$value_count = count($values);
if ($key_count !== $value_count) {
user_error('array_combine() Both parameters should have equal number of elements', E_USER_WARNING);
return false;
}
if ($key_count === 0 || $value_count === 0) {
user_error('array_combine() Both parameters should have number of elements at least 0', E_USER_WARNING);
return false;
}
$keys = array_values($keys);
$values = array_values($values);
$combined = array();
for ($i = 0; $i < $key_count; $i++) {
$combined[$keys[$i]] = $values[$i];
}
return $combined;
}
}
Functions
Name![]() |
Description |
---|---|
money_field | Implementation of hook_field(). |
money_field_formatter | Implementation of hook_field_formatter(). |
money_field_formatter_info | Implementation of hook_field_formatter_info(). |
money_field_info | Implementation of hook_field_info(). |
money_field_settings | Implementation of hook_field_settings(). |
money_token_list | Implementation of hook_token_list(). |
money_token_values | Implementation of hook_token_values(). |
money_views_handler_filter_amount | Views filter handler: amount filter. |
money_views_handler_filter_currency | Views filter handler: currency filter. |
money_widget | Implementation of hook_widget(). |
money_widget_info | Implementation of hook_widget_info(). |
theme_money_field_settings_currency_list | Format the list of currencies that is displayed in the money field settings form. |
_money_amount_to_integer | Convert the amount (a string entered by the user) to an integer. |
_money_get_decimal_separator | Get the decimal separator from a variable, use the default if the variable is empty. |
_money_get_digit_group_separator | Get the digit group separator from a variable, use the default if the variable is empty. |
_money_get_displayed_decimals | Get the number of displayed decimals, use the default if the variable is empty. |
_money_parse_currencies | Parse currency codes from a comma-separated list. |