odoo.define('web.basic_fields', function (require) {
"use strict";
/**
* This module contains most of the basic (meaning: non relational) field
* widgets. Field widgets are supposed to be used in views inheriting from
* BasicView, so, they can work with the records obtained from a BasicModel.
*/
var AbstractField = require('web.AbstractField');
var ajax = require('web.ajax');
var config = require('web.config');
var core = require('web.core');
var crash_manager = require('web.crash_manager');
var datepicker = require('web.datepicker');
var dom = require('web.dom');
var Domain = require('web.Domain');
var DomainSelector = require('web.DomainSelector');
var DomainSelectorDialog = require('web.DomainSelectorDialog');
var field_utils = require('web.field_utils');
var framework = require('web.framework');
var session = require('web.session');
var utils = require('web.utils');
var view_dialogs = require('web.view_dialogs');
var qweb = core.qweb;
var _t = core._t;
var DebouncedField = AbstractField.extend({
events: _.extend({}, AbstractField.prototype.events, {
'input': '_onInput',
'change': '_onChange',
}),
/**
* for field widgets that may have a large number of field changes quickly,
* it could be a good idea to debounce the changes. In that case, this is
* the suggested value.
*/
DEBOUNCE: 1000,
/**
* Override init to debounce the input changes to make sure they are not
* done too quickly. Note that this is done here and not on the prototype,
* so each inputfield has its own debounced function to work with.
* Also, if the debounce value is set to 0, no debouncing is done, which is
* really useful for the unit tests.
*
* @override
*/
init: function () {
this._super.apply(this, arguments);
if (this.DEBOUNCE && this.mode === 'edit') {
this._doDebouncedAction = _.debounce(this._doDebouncedAction.bind(this), this.DEBOUNCE);
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Should return the current value of the field, in the DOM (for example,
* the content of the input)
*
* @abstract
* @private
* @returns {*}
*/
_getValue: function () {
},
/**
* Notifies the outside world of the new value (checked from the DOM).
* This method is debounced so that onchanges are not triggered too quickly.
*
* @private
*/
_doDebouncedAction: function () {
this._setValue(this._getValue());
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* We immediately notify the outside world when this field confirms its
* changes.
*
* @private
*/
_onChange: function () {
this._setValue(this._getValue());
},
/**
* Called when the user is typing text -> By default this only calls a
* debounced method to notify the outside world of the changes.
* @see _doDebouncedAction
*
* @private
*/
_onInput: function () {
this._doDebouncedAction();
},
});
var InputField = DebouncedField.extend({
/**
* The very purpose of this field is to be an input tag in edit mode.
*
* @override
*/
init: function () {
this._super.apply(this, arguments);
if (this.mode === 'edit') {
this.tagName = 'input';
}
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Returns the associated element.
*
* @override
*/
getFocusableElement: function () {
return this.$input || $();
},
/**
* Do not re-render this field if it was the origin of the onchange call.
* FIXME: make the onchange work on itself without disturbing the user typing
*
* @override
*/
reset: function (record, event) {
this._reset(record, event);
if (event && event.target === this) {
return $.when();
} else {
return this._render();
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @returns {string} the content of the input
*/
_getValue: function () {
return this.$input.val();
},
/**
* Formats an input element for edit mode. This is in a separate function so
* extending widgets can use it on their input without having input as tagName.
*
* @private
* @param {jQuery|undefined} $input
* The element to prepare and save as the $input attribute.
* If no element is given, the is created.
* @returns {jQuery} the prepared this.$input element
*/
_prepareInput: function ($input) {
this.$input = $input || $("");
this.$input.addClass('o_form_input');
this.$input.attr({
type: 'text',
placeholder: this.attrs.placeholder || "",
});
// Save cursor position to restore it after updating value
var selectionStart = this.$input[0].selectionStart;
var selectionEnd = this.$input[0].selectionEnd;
this.$input.val(this._formatValue(this.value));
this.$input[0].selectionStart = selectionStart;
this.$input[0].selectionEnd = selectionEnd;
return this.$input;
},
/**
* Formats the HTML input tag for edit mode and stores selection status.
*
* @override
* @private
*/
_renderEdit: function () {
// Keep a reference to the input so $el can become something else
// without losing track of the actual input.
this._prepareInput(this.$el);
},
/**
* Resets the content to the formated value in readonly mode.
*
* @override
* @private
*/
_renderReadonly: function () {
this.$el.html(this._formatValue(this.value));
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Implement keyboard movements. Mostly useful for its environment, such
* as a list view.
*
* @override
* @private
* @param {any} event
*/
_onKeydown: function (event) {
var input = this.$input[0];
var is_not_selecting;
switch (event.which) {
case $.ui.keyCode.DOWN:
this.trigger_up('move_down');
break;
case $.ui.keyCode.UP:
this.trigger_up('move_up');
break;
case $.ui.keyCode.LEFT:
is_not_selecting = input.selectionEnd === input.selectionStart;
if (is_not_selecting && input.selectionStart === 0) {
this.trigger_up('move_left');
}
break;
case $.ui.keyCode.RIGHT:
is_not_selecting = input.selectionEnd === input.selectionStart;
if (is_not_selecting && input.selectionEnd === input.value.length) {
this.trigger_up('move_right');
}
break;
case $.ui.keyCode.ENTER:
this.trigger_up('move_next_line');
break;
}
this._super.apply(this, arguments);
},
});
var FieldChar = InputField.extend({
supportedFieldTypes: ['char'],
tagName: 'span',
});
var FieldDate = InputField.extend({
className: "o_form_field_date",
tagName: "span",
supportedFieldTypes: ['date'],
/**
* In edit mode, instantiates a DateWidget datepicker and listen to changes.
*
* @override
*/
start: function () {
var self = this;
var def;
if (this.mode === 'edit') {
this.datewidget = this._makeDatePicker();
this.datewidget.on('datetime_changed', this, function () {
var value = this._getDateWidgetValue();
if ((!value && this.value) || (value && !value.isSame(this.value))) {
this._setValue(value);
}
});
def = this.datewidget.appendTo('
').done(function () {
self.datewidget.$el.addClass(self.$el.attr('class'));
self._prepareInput(self.datewidget.$input);
self.replaceElement(self.datewidget.$el);
});
}
return $.when(def, this._super.apply(this, arguments));
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @private
*/
_doDebouncedAction: function () {
this.datewidget.change_datetime();
},
/**
* return the datepicker value
*
* @private
*/
_getDateWidgetValue: function () {
return this.datewidget.get_value();
},
/**
* Instantiates a new DateWidget datepicker.
*
* @private
*/
_makeDatePicker: function () {
return new datepicker.DateWidget(this, {defaultDate: this.value});
},
/**
* Set the datepicker to the right value rather than the default one.
*
* @override
* @private
*/
_renderEdit: function () {
this.datewidget.set_value(this.value);
this.$input = this.datewidget.$input;
},
/**
* FieldDateTime overrides _formatValue to use the session timezone
*
* @override
* @private
* @param {Moment} value
*/
_formatValue: function (value) {
return field_utils.format.date(value, {timezone: true});
},
});
var FieldDateTime = FieldDate.extend({
supportedFieldTypes: ['datetime'],
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* return the datepicker value
*
* @private
*/
_getDateWidgetValue: function () {
var value = this.datewidget.get_value();
return value && value.add(-this.getSession().tzOffset, 'minutes');
},
/**
* Instantiates a new DateTimeWidget datepicker rather than DateWidget.
*
* @override
* @private
*/
_makeDatePicker: function () {
var value = this.value && this.value.clone().add(this.getSession().tzOffset, 'minutes');
return new datepicker.DateTimeWidget(this, {defaultDate: value});
},
/**
* Set the datepicker to the right value rather than the default one.
*
* @override
* @private
*/
_renderEdit: function () {
var value = this.value && this.value.clone().add(this.getSession().tzOffset, 'minutes');
this.datewidget.set_value(value);
this.$input = this.datewidget.$input;
},
/**
* FieldDateTime overrides _formatValue to use the session timezone
*
* @override
* @private
* @param {Moment} value
*/
_formatValue: function (value) {
return field_utils.format.datetime(value, {timezone: true});
},
});
var FieldMonetary = InputField.extend({
className: 'o_form_field_monetary o_list_number',
supportedFieldTypes: ['float', 'monetary'],
resetOnAnyFieldChange: true, // Have to listen to currency changes
/**
* Float fields using a monetary widget have an additional currency_field
* parameter which defines the name of the field from which the currency
* should be read.
*
* They are also displayed differently than other inputs in
* edit mode. They are a div containing a span with the currency symbol and
* the actual input.
*
* If no currency field is given or the field does not exist, we fallback
* to the default input behavior instead.
*
* @override
*/
init: function () {
this._super.apply(this, arguments);
this._setCurrency();
if (this.mode === 'edit' && this.currency) {
this.tagName = 'div';
}
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* For monetary fields, 0 is a valid value.
*
* @override
*/
isSet: function () {
return this.value === 0 || this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* For monetary fields, the input is inside a div, alongside a span
* containing the currency symbol.
*
* @override
* @private
*/
_renderEdit: function () {
if (!this.currency) {
this._super.apply(this, arguments);
return;
}
this.$el.empty();
// Prepare and add the input
this._prepareInput().appendTo(this.$el);
// prepare and add the currency symbol
var $currencySymbol = $('', {text: this.currency.symbol});
if (this.currency.position === "after") {
this.$el.append($currencySymbol);
} else {
this.$el.prepend($currencySymbol);
}
},
/**
* Re-gets the currency as its value may have changed.
* @see FieldMonetary.resetOnAnyFieldChange
*
* @override
* @private
*/
_reset: function () {
this._super.apply(this, arguments);
this._setCurrency();
},
/**
* Deduces the currency description from the field options and view state.
* The description is then available at this.currency.
*
* @private
*/
_setCurrency: function () {
var currencyField = this.attrs.currency_field || this.field.currency_field || 'currency_id';
var currencyID = this.record.data[currencyField] && this.record.data[currencyField].res_id;
this.currency = session.get_currency(currencyID);
},
/**
* FieldMonetary overrides _formatValue to use the format monetary method
* in readonly.
*
* @override
* @private
* @param {float} value
* @returns {string}
*/
_formatValue: function (value) {
var format = field_utils.format[this.mode === 'edit' ? "float" : "monetary"];
return format(value, this.field, {
digits: [16, 2],
currency: this.currency,
});
},
});
var FieldBoolean = AbstractField.extend({
className: 'o_field_boolean',
events: _.extend({}, AbstractField.prototype.events, {
change: '_onChange',
}),
supportedFieldTypes: ['boolean'],
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
* @returns {jQuery} the focusable checkbox input
*/
getFocusableElement: function () {
return this.$input || $();
},
/**
* A boolean field is always set since false is a valid value.
*
* @override
*/
isSet: function () {
return true;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* The actual checkbox is designed in css to have full control over its
* appearance, as opposed to letting the browser and the os decide how
* a checkbox should look. The actual input is disabled and hidden.
*
* @override
* @private
*/
_renderReadonly: function () {
var $checkbox = this._formatValue(this.value);
this.$input = $checkbox.find('input');
this.$el.empty().append($checkbox);
},
/**
* The actual checkbox is designed in css to have full control over its
* appearance, as opposed to letting the browser and the os decide how
* a checkbox should look. The actual input is hidden.
*
* @override
* @private
*/
_renderEdit: function () {
this._renderReadonly();
this.$input.prop('disabled', false);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Properly update the value when the checkbox is (un)ticked to trigger
* possible onchanges.
*
* @private
*/
_onChange: function () {
this._setValue(this.$input[0].checked);
},
/**
* Implement keyboard movements. Mostly useful for its environment, such
* as a list view.
*
* @override
* @private
* @param {KeyEvent} event
*/
_onKeydown: function (event) {
this._super.apply(this, arguments);
switch (event.which) {
case $.ui.keyCode.DOWN:
this.trigger_up('move_down');
break;
case $.ui.keyCode.UP:
this.trigger_up('move_up');
break;
case $.ui.keyCode.LEFT:
this.trigger_up('move_left');
break;
case $.ui.keyCode.RIGHT:
this.trigger_up('move_right');
break;
case $.ui.keyCode.ENTER:
this.$input.prop('checked', !this.value);
this._setValue(!this.value);
break;
}
},
});
var FieldInteger = InputField.extend({
supportedFieldTypes: ['integer'],
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* For integer fields, 0 is a valid value.
*
* @override
*/
isSet: function () {
return this.value === 0 || this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Format integer value
*
* Note: We have to overwrite this method to allow virtual ids. A virtual id
* is a character string composed of an integer and has a dash and other
* information.
* E.g: in calendar, the recursive event have virtual id linked to a real id
* virtual event id "23-20170418020000" is linked to the event id 23
*
* @override
* @private
* @param {integer|string} value
* @returns {string}
*/
_formatValue: function (value) {
if (typeof value === 'string') {
if (!/^[0-9]+-/.test(value)) {
throw new Error('"' + value + '" is not an integer or a virtual id');
}
return value;
}
return field_utils.format.integer(value, this.field);
},
});
var FieldFloat = InputField.extend({
supportedFieldTypes: ['float'],
/**
* Float fields have an additional precision parameter that is read from
* either the field node in the view or the field python definition itself.
*
* @override
*/
init: function () {
this._super.apply(this, arguments);
if (this.attrs.digits) {
this.nodeOptions.digits = JSON.parse(this.attrs.digits);
}
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* For float fields, 0 is a valid value.
*
* @override
*/
isSet: function () {
return this.value === 0 || this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Format value according to precision parameter.
*
* @override
* @private
*/
_renderReadonly: function () {
var value = this._formatValue(this.value);
var $span = $('').addClass('o_form_field o_form_field_number').text(value);
this.$el.html($span);
},
});
var FieldFloatTime = FieldFloat.extend({
supportedFieldTypes: ['float'],
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Format the float time value into a human-readable time format.
*
* @override
* @private
*/
_formatValue: function (value) {
return field_utils.format.float_time(value);
},
/**
* Parse the human-readable formatted time value into a float.
*
* @override
* @private
*/
_parseValue: function (value) {
return field_utils.parse.float_time(value);
},
});
var FieldText = DebouncedField.extend({
supportedFieldTypes: ['text'],
/**
* In edit mode, the text widget contains a textarea. We append it in
* start() instead of _renderEdit() to keep the same textarea even
* if several _renderEdit are done. This allows to keep the cursor
* position and to autoresize only once.
*
* @override
*/
start: function () {
this.$el.addClass('o_list_text o_form_textarea');
if (this.mode === 'edit') {
this.$textarea = $('