summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/fields/field_utils.js
blob: beba1b07f5034ed9144ef264f9c697779515e00e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
odoo.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 || isNaN(value)) {
        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.
 * @param {function} [options.humanReadable] if returns true,
 *   formatFloat acts like utils.human_number
 * @returns {string}
 */
function formatFloat(value, field, options) {
    options = options || {};
    if (value === false) {
        return "";
    }
    if (options.humanReadable && options.humanReadable(value)) {
        return utils.human_number(value, options.decimals, options.minDigits, options.formatterCallback);
    }
    var l10n = core._t.database.parameters;
    var precision;
    if (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 float value, from a float converted with a
 * factor.
 *
 * @param {number} value
 * @param {number} [options.factor]
 *          Conversion factor, default value is 1.0
 * @returns {string}
 */
function formatFloatFactor(value, field, options) {
    var factor = options.factor || 1;
    return formatFloat(value * factor, field, options);
}

/**
 * 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
 * @param {Object} [field]
 *        a description of the field (note: this parameter is ignored)
 * @param {Object} [options]
 * @param {boolean} [options.noLeadingZeroHour] if true, format like 1:30
 *        otherwise, format like 01:30
 * @returns {string}
 */
function formatFloatTime(value, field, options) {
    options = options || {};
    var pattern = options.noLeadingZeroHour ? '%1d:%02d' : '%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 '********'
 * @param {function} [options.humanReadable] if returns true,
 *   formatFloat acts like utils.human_number
 * @returns {string}
 */
function formatInteger(value, field, options) {
    options = options || {};
    if (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 "";
    }
    if (options.humanReadable && options.humanReadable(value)) {
        return utils.human_number(value, options.decimals, options.minDigits, options.formatterCallback);
    }
    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 datapoint 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) {
    if (!value) {
        value = '';
    } else if (_.isArray(value)) {
        // value is a pair [id, nameget]
        value = value[1];
    } else {
        // value is a datapoint, so we read its display_name field, which
        // may in turn be a datapoint (if the name field is a many2one)
        while (value.data) {
            value = 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.
 * @param {boolean} [options.forceString=false]
 *        if false, returns a string encoding the html formatted value (with
 *        whitespace encoded as '&nbsp;')
 * @returns {string}
 */
function formatMonetary(value, field, options) {
    if (value === false) {
        return "";
    }
    options = Object.assign({ forceString: false }, 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,
        _.extend({}, options , {digits: digits})
    );

    if (!currency || options.noSymbol) {
        return formatted_value;
    }
    const ws = options.forceString ? ' ' : '&nbsp;';
    if (currency.position === "after") {
        return formatted_value + ws + currency.symbol;
    } else {
        return currency.symbol + ws + formatted_value;
    }
}
/**
 * Returns a string representing the given value (multiplied by 100)
 * concatenated with '%'.
 *
 * @param {number | false} value
 * @param {Object} [field]
 * @param {Object} [options]
 * @param {function} [options.humanReadable] if returns true, parsing is avoided
 * @returns {string}
 */
function formatPercentage(value, field, options) {
    options = options || {};
    let result = formatFloat(value * 100, field, options) || '0';
    if (!options.humanReadable || !options.humanReadable(value * 100)) {
        result = parseFloat(result).toString().replace('.', _t.database.parameters.decimal_point);
    }
    return result + (options.noSymbol ? '' : '%');
}
/**
 * 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
////////////////////////////////////////////////////////////////////////////////

/**
 * Smart date inputs are shortcuts to write dates quicker.
 * These shortcuts should respect the format ^[+-]\d+[dmwy]?$
 * 
 * e.g.
 *   "+1d" or "+1" will return now + 1 day
 *   "-2w" will return now - 2 weeks
 *   "+3m" will return now + 3 months
 *   "-4y" will return now + 4 years
 *
 * @param {string} value
 * @returns {Moment|false} Moment date object
 */
function parseSmartDateInput(value) {
    const units = {
        d: 'days',
        m: 'months',
        w: 'weeks',
        y: 'years',
    };
    const re = new RegExp(`^([+-])(\\d+)([${Object.keys(units).join('')}]?)$`);
    const match = re.exec(value);
    if (match) {
        let date = moment();
        const offset = parseInt(match[2], 10);
        const unit = units[match[3] || 'd'];
        if (match[1] === '+') {
            date.add(offset, unit);
        } else {
            date.subtract(offset, unit);
        }
        return date;
    }
    return false;
}

/**
 * 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 = time.getLangDateFormatWoZero();
    var date;
    const smartDate = parseSmartDateInput(value);
    if (smartDate) {
        date = smartDate;
    } else {
        if (options && options.isUTC) {
            value = value.padStart(10, "0"); // server may send "932-10-10" for "0932-10-10" on some OS
            date = moment.utc(value);
        } else {
            date = moment.utc(value, [datePattern, datePatternWoZero, moment.ISO_8601]);
        }
    }
    if (date.isValid()) {
        if (date.year() === 0) {
            date.year(moment.utc().year());
        }
        if (date.year() >= 1000){
            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;
    }
    const datePattern = time.getLangDateFormat();
    const timePattern = time.getLangTimeFormat();
    const datePatternWoZero = time.getLangDateFormatWoZero();
    const timePatternWoZero = time.getLangTimeFormatWoZero();
    var pattern1 = datePattern + ' ' + timePattern;
    var pattern2 = datePatternWoZero + ' ' + timePatternWoZero;
    var datetime;
    const smartDate = parseSmartDateInput(value);
    if (smartDate) {
        datetime = smartDate;
    } else {
        if (options && options.isUTC) {
            value = value.padStart(19, "0"); // server may send "932-10-10" for "0932-10-10" on some OS
            // 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]);
            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() >= 1000) {
            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('&nbsp;');
    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]);
}

/**
 * Parse a String containing float and unconvert it with a conversion factor
 *
 * @param {number} [options.factor]
 *          Conversion factor, default value is 1.0
 */
function parseFloatFactor(value, field, options) {
    var parsed = parseFloat(value);
    var factor = options.factor || 1.0;
    return parsed / factor;
}

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 float and unconvert it with a conversion factor
 * of 100. The percentage can be a regular xx.xx float or a xx%.
 *
 * @param {string} value
 *                The string to be parsed
 * @returns {float}
 * @throws {Error} if the value couldn't be converted to float
 */
function parsePercentage(value) {
    return parseFloat(value) / 100;
}

/**
 * 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_factor: formatFloatFactor,
        float_time: formatFloatTime,
        html: _.identity, // todo
        integer: formatInteger,
        many2many: formatX2Many,
        many2one: formatMany2one,
        many2one_reference: formatInteger,
        monetary: formatMonetary,
        one2many: formatX2Many,
        percentage: formatPercentage,
        reference: formatMany2one,
        selection: formatSelection,
        text: formatChar,
    },
    parse: {
        binary: _.identity,
        boolean: _.identity, // todo
        char: _.identity, // todo
        date: parseDate, // todo
        datetime: parseDateTime, // todo
        float: parseFloat,
        float_factor: parseFloatFactor,
        float_time: parseFloatTime,
        html: _.identity, // todo
        integer: parseInteger,
        many2many: _.identity, // todo
        many2one: parseMany2one,
        many2one_reference: parseInteger,
        monetary: parseMonetary,
        one2many: _.identity,
        percentage: parsePercentage,
        reference: parseMany2one,
        selection: _.identity, // todo
        text: _.identity, // todo
    },
};

});