flectra.define('web.field_utils', function (require) { "use strict"; /** * Field Utils * * This file contains two types of functions: formatting functions and parsing * functions. * * Each field type has to display in string form at some point, but it should be * stored in memory with the actual value. For example, a float value of 0.5 is * represented as the string "0.5" but is kept in memory as a float. A date * (or datetime) value is always stored as a Moment.js object, but displayed as * a string. This file contains all sort of functions necessary to perform the * conversions. */ var core = require('web.core'); var dom = require('web.dom'); var session = require('web.session'); var time = require('web.time'); var utils = require('web.utils'); var _t = core._t; //------------------------------------------------------------------------------ // Formatting //------------------------------------------------------------------------------ /** * Convert binary to bin_size * * @param {string} [value] base64 representation of the binary (might be already a bin_size!) * @param {Object} [field] * a description of the field (note: this parameter is ignored) * @param {Object} [options] additional options (note: this parameter is ignored) * * @returns {string} bin_size (which is human-readable) */ function formatBinary(value, field, options) { if (!value) { return ''; } return utils.binaryToBinsize(value); } /** * @todo Really? it returns a jQuery element... We should try to avoid this and * let DOM utility functions handle this directly. And replace this with a * function that returns a string so we can get rid of the forceString. * * @param {boolean} value * @param {Object} [field] * a description of the field (note: this parameter is ignored) * @param {Object} [options] additional options * @param {boolean} [options.forceString=false] if true, returns a string * representation of the boolean rather than a jQueryElement * @returns {jQuery|string} */ function formatBoolean(value, field, options) { if (options && options.forceString) { return value ? _t('True') : _t('False'); } return dom.renderCheckbox({ prop: { checked: value, disabled: true, }, }); } /** * Returns a string representing a char. If the value is false, then we return * an empty string. * * @param {string|false} value * @param {Object} [field] * a description of the field (note: this parameter is ignored) * @param {Object} [options] additional options * @param {boolean} [options.escape=false] if true, escapes the formatted value * @param {boolean} [options.isPassword=false] if true, returns '********' * instead of the formatted value * @returns {string} */ function formatChar(value, field, options) { value = typeof value === 'string' ? value : ''; if (options && options.isPassword) { return _.str.repeat('*', value ? value.length : 0); } if (options && options.escape) { value = _.escape(value); } return value; } /** * Returns a string representing a date. If the value is false, then we return * an empty string. Note that this is dependant on the localization settings * * @param {Moment|false} value * @param {Object} [field] * a description of the field (note: this parameter is ignored) * @param {Object} [options] additional options * @param {boolean} [options.timezone=true] use the user timezone when formating the * date * @returns {string} */ function formatDate(value, field, options) { if (value === false) { return ""; } if (field && field.type === 'datetime') { if (!options || !('timezone' in options) || options.timezone) { value = value.clone().add(session.getTZOffset(value), 'minutes'); } } var date_format = time.getLangDateFormat(); return value.format(date_format); } /** * Returns a string representing a datetime. If the value is false, then we * return an empty string. Note that this is dependant on the localization * settings * * @params {Moment|false} * @param {Object} [field] * a description of the field (note: this parameter is ignored) * @param {Object} [options] additional options * @param {boolean} [options.timezone=true] use the user timezone when formating the * date * @returns {string} */ function formatDateTime(value, field, options) { if (value === false) { return ""; } if (!options || !('timezone' in options) || options.timezone) { value = value.clone().add(session.getTZOffset(value), 'minutes'); } return value.format(time.getLangDatetimeFormat()); } /** * Returns a string representing a float. The result takes into account the * user settings (to display the correct decimal separator). * * @param {float|false} value the value that should be formatted * @param {Object} [field] a description of the field (returned by fields_get * for example). It may contain a description of the number of digits that * should be used. * @param {Object} [options] additional options to override the values in the * python description of the field. * @param {integer[]} [options.digits] the number of digits that should be used, * instead of the default digits precision in the field. * @returns {string} */ function formatFloat(value, field, options) { if (value === false) { return ""; } var l10n = core._t.database.parameters; var precision; if (options && options.digits) { precision = options.digits[1]; } else if (field && field.digits) { precision = field.digits[1]; } else { precision = 2; } var formatted = _.str.sprintf('%.' + precision + 'f', value || 0).split('.'); formatted[0] = utils.insert_thousand_seps(formatted[0]); return formatted.join(l10n.decimal_point); } /** * Returns a string representing a time value, from a float. The idea is that * we sometimes want to display something like 1:45 instead of 1.75, or 0:15 * instead of 0.25. * * @param {float} value * @returns {string} */ function formatFloatTime(value) { var pattern = '%02d:%02d'; if (value < 0) { value = Math.abs(value); pattern = '-' + pattern; } var hour = Math.floor(value); var min = Math.round((value % 1) * 60); if (min === 60){ min = 0; hour = hour + 1; } return _.str.sprintf(pattern, hour, min); } /** * Returns a string representing an integer. If the value is false, then we * return an empty string. * * @param {integer|false} value * @param {Object} [field] * a description of the field (note: this parameter is ignored) * @param {Object} [options] additional options * @param {boolean} [options.isPassword=false] if true, returns '********' * @returns {string} */ function formatInteger(value, field, options) { if (options && options.isPassword) { return _.str.repeat('*', String(value).length); } if (!value && value !== 0) { // previously, it returned 'false'. I don't know why. But for the Pivot // view, I want to display the concept of 'no value' with an empty // string. return ""; } return utils.insert_thousand_seps(_.str.sprintf('%d', value)); } /** * Returns a string representing an many2one. If the value is false, then we * return an empty string. Note that it accepts two types of input parameters: * an array, in that case we assume that the many2one value is of the form * [id, nameget], and we return the nameget, or it can be an object, and in that * case, we assume that it is a record from a BasicModel. * * @param {Array|Object|false} value * @param {Object} [field] * a description of the field (note: this parameter is ignored) * @param {Object} [options] additional options * @param {boolean} [options.escape=false] if true, escapes the formatted value * @returns {string} */ function formatMany2one(value, field, options) { value = value && (_.isArray(value) ? value[1] : value.data.display_name) || ''; if (options && options.escape) { value = _.escape(value); } return value; } /** * Returns a string indicating the number of records in the relation. * * @param {Object} value a valid element from a BasicModel, that represents a * list of values * @returns {string} */ function formatX2Many(value) { if (value.data.length === 0) { return _t('No records'); } else if (value.data.length === 1) { return _t('1 record'); } else { return value.data.length + _t(' records'); } } /** * Returns a string representing a monetary value. The result takes into account * the user settings (to display the correct decimal separator, currency, ...). * * @param {float|false} value the value that should be formatted * @param {Object} [field] * a description of the field (returned by fields_get for example). It * may contain a description of the number of digits that should be used. * @param {Object} [options] * additional options to override the values in the python description of * the field. * @param {Object} [options.currency] the description of the currency to use * @param {integer} [options.currency_id] * the id of the 'res.currency' to use (ignored if options.currency) * @param {string} [options.currency_field] * the name of the field whose value is the currency id * (ignore if options.currency or options.currency_id) * Note: if not given it will default to the field currency_field value * or to 'currency_id'. * @param {Object} [options.data] * a mapping of field name to field value, required with * options.currency_field * @param {integer[]} [options.digits] * the number of digits that should be used, instead of the default * digits precision in the field. Note: if the currency defines a * precision, the currency's one is used. * @returns {string} */ function formatMonetary(value, field, options) { if (value === false) { return ""; } options = options || {}; var currency = options.currency; if (!currency) { var currency_id = options.currency_id; if (!currency_id && options.data) { var currency_field = options.currency_field || field.currency_field || 'currency_id'; currency_id = options.data[currency_field] && options.data[currency_field].res_id; } currency = session.get_currency(currency_id); } var digits = (currency && currency.digits) || options.digits; if (options.field_digits === true) { digits = field.digits || digits; } var formatted_value = formatFloat(value, field, { digits: digits, }); if (!currency || options.noSymbol) { return formatted_value; } if (currency.position === "after") { return formatted_value += ' ' + currency.symbol; } else { return currency.symbol + ' ' + formatted_value; } } /** * Returns a string representing the value of the selection. * * @param {string|false} value * @param {Object} [field] * a description of the field (note: this parameter is ignored) * @param {Object} [options] additional options * @param {boolean} [options.escape=false] if true, escapes the formatted value */ function formatSelection(value, field, options) { var val = _.find(field.selection, function (option) { return option[0] === value; }); if (!val) { return ''; } value = val[1]; if (options && options.escape) { value = _.escape(value); } return value; } //////////////////////////////////////////////////////////////////////////////// // Parse //////////////////////////////////////////////////////////////////////////////// /** * Create an Date object * The method toJSON return the formated value to send value server side * * @param {string} value * @param {Object} [field] * a description of the field (note: this parameter is ignored) * @param {Object} [options] additional options * @param {boolean} [options.isUTC] the formatted date is utc * @param {boolean} [options.timezone=false] format the date after apply the timezone * offset * @returns {Moment|false} Moment date object */ function parseDate(value, field, options) { if (!value) { return false; } var datePattern = time.getLangDateFormat(); var datePatternWoZero = datePattern.replace('MM','M').replace('DD','D'); var date; if (options && options.isUTC) { date = moment.utc(value); } else { date = moment.utc(value, [datePattern, datePatternWoZero, moment.ISO_8601], true); } if (date.isValid()) { if (date.year() === 0) { date.year(moment.utc().year()); } if (date.year() >= 1900) { date.toJSON = function () { return this.clone().locale('en').format('YYYY-MM-DD'); }; return date; } } throw new Error(_.str.sprintf(core._t("'%s' is not a correct date"), value)); } /** * Create an Date object * The method toJSON return the formated value to send value server side * * @param {string} value * @param {Object} [field] * a description of the field (note: this parameter is ignored) * @param {Object} [options] additional options * @param {boolean} [options.isUTC] the formatted date is utc * @param {boolean} [options.timezone=false] format the date after apply the timezone * offset * @returns {Moment|false} Moment date object */ function parseDateTime(value, field, options) { if (!value) { return false; } var datePattern = time.getLangDateFormat(), timePattern = time.getLangTimeFormat(); var datePatternWoZero = datePattern.replace('MM','M').replace('DD','D'), timePatternWoZero = timePattern.replace('HH','H').replace('mm','m').replace('ss','s'); var pattern1 = datePattern + ' ' + timePattern; var pattern2 = datePatternWoZero + ' ' + timePatternWoZero; var datetime; if (options && options.isUTC) { // phatomjs crash if we don't use this format datetime = moment.utc(value.replace(' ', 'T') + 'Z'); } else { datetime = moment.utc(value, [pattern1, pattern2, moment.ISO_8601], true); if (options && options.timezone) { datetime.add(-session.getTZOffset(datetime), 'minutes'); } } if (datetime.isValid()) { if (datetime.year() === 0) { datetime.year(moment.utc().year()); } if (datetime.year() >= 1900) { datetime.toJSON = function () { return this.clone().locale('en').format('YYYY-MM-DD HH:mm:ss'); }; return datetime; } } throw new Error(_.str.sprintf(core._t("'%s' is not a correct datetime"), value)); } /** * Parse a String containing number in language formating * * @param {string} value * The string to be parsed with the setting of thousands and * decimal separator * @returns {float|NaN} the number value contained in the string representation */ function parseNumber(value) { if (core._t.database.parameters.thousands_sep) { var escapedSep = _.str.escapeRegExp(core._t.database.parameters.thousands_sep); value = value.replace(new RegExp(escapedSep, 'g'), ''); } if (core._t.database.parameters.decimal_point) { value = value.replace(core._t.database.parameters.decimal_point, '.'); } return Number(value); } /** * Parse a String containing float in language formating * * @param {string} value * The string to be parsed with the setting of thousands and * decimal separator * @returns {float} * @throws {Error} if no float is found respecting the language configuration */ function parseFloat(value) { var parsed = parseNumber(value); if (isNaN(parsed)) { throw new Error(_.str.sprintf(core._t("'%s' is not a correct float"), value)); } return parsed; } /** * Parse a String containing currency symbol and returns amount * * @param {string} value * The string to be parsed * We assume that a monetary is always a pair (symbol, amount) separated * by a non breaking space. A simple float can also be accepted as value * @param {Object} [field] * a description of the field (returned by fields_get for example). * @param {Object} [options] additional options. * @param {Object} [options.currency] - the description of the currency to use * @param {integer} [options.currency_id] * the id of the 'res.currency' to use (ignored if options.currency) * @param {string} [options.currency_field] * the name of the field whose value is the currency id * (ignore if options.currency or options.currency_id) * Note: if not given it will default to the field currency_field value * or to 'currency_id'. * @param {Object} [options.data] * a mapping of field name to field value, required with * options.currency_field * * @returns {float} the float value contained in the string representation * @throws {Error} if no float is found or if parameter does not respect monetary condition */ function parseMonetary(value, field, options) { var values = value.split(' '); if (values.length === 1) { return parseFloat(value); } else if (values.length !== 2) { throw new Error(_.str.sprintf(core._t("'%s' is not a correct monetary field"), value)); } options = options || {}; var currency = options.currency; if (!currency) { var currency_id = options.currency_id; if (!currency_id && options.data) { var currency_field = options.currency_field || field.currency_field || 'currency_id'; currency_id = options.data[currency_field] && options.data[currency_field].res_id; } currency = session.get_currency(currency_id); } return parseFloat(values[0] === currency.symbol ? values[1] : values[0]); } function parseFloatTime(value) { var factor = 1; if (value[0] === '-') { value = value.slice(1); factor = -1; } var float_time_pair = value.split(":"); if (float_time_pair.length !== 2) return factor * parseFloat(value); var hours = parseInteger(float_time_pair[0]); var minutes = parseInteger(float_time_pair[1]); return factor * (hours + (minutes / 60)); } /** * Parse a String containing integer with language formating * * @param {string} value * The string to be parsed with the setting of thousands and * decimal separator * @returns {integer} * @throws {Error} if no integer is found respecting the language configuration */ function parseInteger(value) { var parsed = parseNumber(value); // do not accept not numbers or float values if (isNaN(parsed) || parsed % 1 || parsed < -2147483648 || parsed > 2147483647) { throw new Error(_.str.sprintf(core._t("'%s' is not a correct integer"), value)); } return parsed; } /** * Creates an object with id and display_name. * * @param {Array|number|string|Object} value * The given value can be : * - an array with id as first element and display_name as second element * - a number or a string representing the id (the display_name will be * returned as undefined) * - an object, simply returned untouched * @returns {Object} (contains the id and display_name) * Note: if the given value is not an array, a string or a * number, the value is returned untouched. */ function parseMany2one(value) { if (_.isArray(value)) { return { id: value[0], display_name: value[1], }; } if (_.isNumber(value) || _.isString(value)) { return { id: parseInt(value, 10), }; } return value; } return { format: { binary: formatBinary, boolean: formatBoolean, char: formatChar, date: formatDate, datetime: formatDateTime, float: formatFloat, float_time: formatFloatTime, html: _.identity, // todo integer: formatInteger, many2many: formatX2Many, many2one: formatMany2one, monetary: formatMonetary, one2many: formatX2Many, reference: formatMany2one, selection: formatSelection, text: formatChar, }, parse: { binary: _.identity, boolean: _.identity, // todo char: _.identity, // todo date: parseDate, // todo datetime: parseDateTime, // todo float: parseFloat, float_time: parseFloatTime, html: _.identity, // todo integer: parseInteger, many2many: _.identity, // todo many2one: parseMany2one, monetary: parseMonetary, one2many: _.identity, reference: parseMany2one, selection: _.identity, // todo text: _.identity, // todo }, }; });