diff options
Diffstat (limited to 'addons/web/static/src/js/fields')
| -rw-r--r-- | addons/web/static/src/js/fields/abstract_field.js | 2 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/basic_fields.js | 47 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/field_registry.js | 1 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/field_utils.js | 52 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/relational_fields.js | 254 |
5 files changed, 293 insertions, 63 deletions
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js index 52e0aef77e4..4ae4eec9c05 100644 --- a/addons/web/static/src/js/fields/abstract_field.js +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -273,7 +273,7 @@ var AbstractField = Widget.extend({ */ reset: function (record, event) { this._reset(record, event); - return this._render(); + return this._render() || $.when(); }, //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 82970255b3e..1868a29a237 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -17,7 +17,6 @@ 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'); @@ -476,15 +475,14 @@ var FieldMonetary = InputField.extend({ this._setCurrency(); - if (this.mode === 'edit' && this.currency) { + if (this.mode === 'edit') { this.tagName = 'div'; this.className += ' o_input'; - } - // use the formatFloat function in edit - if (this.mode === 'edit') { + // use the formatFloat function in edit this.formatType = 'float'; } + this.formatOptions.currency = this.currency; this.formatOptions.digits = [16, 2]; }, @@ -514,20 +512,19 @@ var FieldMonetary = InputField.extend({ * @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 = $('<span>', {text: this.currency.symbol}); - if (this.currency.position === "after") { - this.$el.append($currencySymbol); - } else { - this.$el.prepend($currencySymbol); + + if (this.currency) { + // Prepare and add the currency symbol + var $currencySymbol = $('<span>', {text: this.currency.symbol}); + if (this.currency.position === "after") { + this.$el.append($currencySymbol); + } else { + this.$el.prepend($currencySymbol); + } } }, /** @@ -791,6 +788,23 @@ var FieldText = InputField.extend(TranslatableFieldMixin, { } return this._super(); }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Stops the enter navigation in a text area. + * + * @private + * @param {OdooEvent} ev + */ + _onKeydown: function (ev) { + if (ev.which === $.ui.keyCode.ENTER) { + return; + } + this._super.apply(this, arguments); + }, }); /** @@ -2237,6 +2251,7 @@ var AceEditor = DebouncedField.extend({ }); return { + TranslatableFieldMixin: TranslatableFieldMixin, DebouncedField: DebouncedField, FieldEmail: FieldEmail, FieldBinaryFile: FieldBinaryFile, diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js index dcd8f7acd48..efafec6ba6a 100644 --- a/addons/web/static/src/js/fields/field_registry.js +++ b/addons/web/static/src/js/fields/field_registry.js @@ -64,6 +64,7 @@ registry .add('many2many_checkboxes', relational_fields.FieldMany2ManyCheckBoxes) .add('one2many', relational_fields.FieldOne2Many) .add('statusbar', relational_fields.FieldStatus) + .add('reference', relational_fields.FieldReference) .add('one2many_list', relational_fields.FieldOne2Many); // Special fields diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js index 9a6734b7baf..ac4c06454ea 100644 --- a/addons/web/static/src/js/fields/field_utils.js +++ b/addons/web/static/src/js/fields/field_utils.js @@ -427,6 +427,52 @@ function parseFloat(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] === '-') { @@ -494,7 +540,7 @@ return { many2one: formatMany2one, monetary: formatMonetary, one2many: formatX2Many, - reference: _.identity, // todo + reference: formatMany2one, selection: formatSelection, text: formatChar, }, @@ -510,9 +556,9 @@ return { integer: parseInteger, many2many: _.identity, // todo many2one: parseMany2one, - monetary: parseFloat, + monetary: parseMonetary, one2many: _.identity, - reference: _.identity, // todo + reference: parseMany2one, selection: _.identity, // todo text: _.identity, // todo }, diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 419d1f5f646..567c54abcb1 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -657,7 +657,14 @@ var FieldX2Many = AbstractField.extend({ reset: function (record, ev) { if (ev && ev.target === this && ev.data.changes && this.view.arch.tag === 'tree') { var command = ev.data.changes[this.name]; - if (command.operation === 'UPDATE') { + // Here, we only consider 'UPDATE' commands with data, which occur + // with editable list view. In order to keep the current line in + // edition, we call confirmChange which will reset the widgets + // instead of re-rendering the list. 'UPDATE' commands with no data + // can be ignored: they occur in one2manys when the record is + // updated from a dialog and in this case, we can re-render the + // whole subview. + if (command.operation === 'UPDATE' && command.data) { var state = record.data[this.name]; var fieldNames = state.getFieldNames(); this.renderer.confirmChange(state, command.id, fieldNames, ev.initialEvent); @@ -677,7 +684,7 @@ var FieldX2Many = AbstractField.extend({ * * @override * @private - * @returns {Deferred} + * @returns {Deferred|undefined} */ _render: function () { if (!this.view) { @@ -1068,9 +1075,15 @@ var FieldOne2Many = FieldX2Many.extend({ // we don't want interference with the components upstream. ev.stopPropagation(); + var id = ev.data.id; + // trigger an empty 'UPDATE' operation when the user clicks on 'Save' in + // the dialog, to notify the main record that a subrecord of this + // relational field has changed (those changes will be already stored on + // that subrecord, thanks to the 'Save'). + var onSaved = this._setValue.bind(this, { operation: 'UPDATE', id: id }, {}); this._openFormDialog({ - id: ev.data.id, - on_saved: this._setValue.bind(this, { operation: 'NOOP' }, {}), + id: id, + on_saved: onSaved, readonly: this.mode === 'readonly', }); }, @@ -1354,7 +1367,6 @@ var FieldMany2ManyTags = AbstractField.extend({ 'click .o_delete': '_onDeleteTag', }), fieldsToFetch: { - color: {type: 'integer'}, display_name: {type: 'char'}, }, @@ -1367,6 +1379,8 @@ var FieldMany2ManyTags = AbstractField.extend({ if (this.mode === 'edit') { this.className += ' o_input'; } + + this.colorField = this.nodeOptions.color_field; }, //-------------------------------------------------------------------------- @@ -1431,6 +1445,7 @@ var FieldMany2ManyTags = AbstractField.extend({ _getRenderTagsContext: function () { var elements = this.value ? _.pluck(this.value.data, 'data') : []; return { + colorField: this.colorField, elements: elements, readonly: this.mode === "readonly", }; @@ -1558,7 +1573,7 @@ var FormFieldMany2ManyTags = FieldMany2ManyTags.extend({ _onOpenColorPicker: function (event) { var tag_id = $(event.currentTarget).data('id'); var tag = _.findWhere(this.value.data, { res_id: tag_id }); - if (tag && 'color' in tag.data) { // if there is a color field on the related model + if (tag && this.colorField in tag.data) { // if there is a color field on the related model this.$color_picker = $(qweb.render('FieldMany2ManyTag.colorpicker', { 'widget': this, 'tag_id': tag_id, @@ -1583,11 +1598,11 @@ var FormFieldMany2ManyTags = FieldMany2ManyTags.extend({ if (color === current_color) { return; } + var changes = {}; + changes[this.colorField] = color; this.trigger_up('field_changed', { dataPointID: _.findWhere(this.value.data, {res_id: id}).id, - changes: { - color: color, - }, + changes: changes, force_save: true, }); }, @@ -1598,12 +1613,19 @@ var KanbanFieldMany2ManyTags = FieldMany2ManyTags.extend({ var self = this; this.$el.empty().addClass('o_field_many2manytags o_kanban_tags'); _.each(this.value.data, function (m2m) { - // 10th color is invisible - if ('color' in m2m.data && m2m.data.color !== 10) { - $('<span>') - .addClass('o_tag o_tag_color_' + m2m.data.color) + var $tag = $('<span>') .attr('title', _.str.escapeHTML(m2m.data.display_name)) .appendTo(self.$el); + if (self.colorField in m2m.data) { + if (m2m.data[self.colorField] === 10) { + // 10th color is invisible + $tag.hide(); + } else { + $tag.addClass('o_tag o_tag_color_' + m2m.data[self.colorField]); + } + } else { + // display tags in grey by default + $tag.addClass('o_tag o_tag_color_0'); } }); }, @@ -1772,15 +1794,7 @@ var FieldSelection = AbstractField.extend({ */ init: function () { this._super.apply(this, arguments); - if (this.field.type === 'many2one') { - this.values = this.record.specialData[this.name]; - this.formatType = 'many2one'; - } else { - this.values = _.reject(this.field.selection, function (v) { - return v[0] === false && v[1] === ''; - }); - } - this.values = [[false, this.attrs.placeholder || '']].concat(this.values); + this._setValues(); }, //-------------------------------------------------------------------------- @@ -1810,13 +1824,12 @@ var FieldSelection = AbstractField.extend({ * @private */ _renderEdit: function () { - if (!this.$el.children().length) { - for (var i = 0 ; i < this.values.length ; i++) { - this.$el.append($('<option/>', { - value: JSON.stringify(this.values[i][0]), - html: this.values[i][1] - })); - } + this.$el.empty(); + for (var i = 0 ; i < this.values.length ; i++) { + this.$el.append($('<option/>', { + value: JSON.stringify(this.values[i][0]), + html: this.values[i][1] + })); } var value = this.value; if (this.field.type === 'many2one' && value) { @@ -1831,6 +1844,31 @@ var FieldSelection = AbstractField.extend({ _renderReadonly: function () { this.$el.empty().text(this._formatValue(this.value)); }, + /** + * @override + */ + _reset: function () { + this._super.apply(this, arguments); + this._setValues(); + }, + /** + * Sets the possible field values. If the field is a many2one, those values + * may change during the lifecycle of the widget if the domain change (an + * onchange may change the domain). + * + * @private + */ + _setValues: function () { + if (this.field.type === 'many2one') { + this.values = this.record.specialData[this.name]; + this.formatType = 'many2one'; + } else { + this.values = _.reject(this.field.selection, function (v) { + return v[0] === false && v[1] === ''; + }); + } + this.values = [[false, this.attrs.placeholder || '']].concat(this.values); + }, //-------------------------------------------------------------------------- // Handlers @@ -1856,7 +1894,9 @@ var FieldSelection = AbstractField.extend({ }); var FieldRadio = FieldSelection.extend({ - template: 'FieldRadio', + template: null, + className: 'o_field_radio', + tagName: 'span', specialData: "_fetchSpecialMany2ones", supportedFieldTypes: ['selection', 'many2one'], events: _.extend({}, AbstractField.prototype.events, { @@ -1867,14 +1907,12 @@ var FieldRadio = FieldSelection.extend({ */ init: function () { this._super.apply(this, arguments); - if (this.field.type === 'selection') { - this.values = this.field.selection || []; - } else if (this.field.type === 'many2one') { - this.values = _.map(this.record.specialData[this.name], function (val) { - return [val.id, val.display_name]; - }); + if (this.mode === 'edit') { + this.tagName = 'div'; + this.className += this.nodeOptions.horizontal ? 'o_horizontal' : 'o_vertical'; } this.unique_id = _.uniqueId("radio"); + this._setValues(); }, //-------------------------------------------------------------------------- @@ -1898,17 +1936,45 @@ var FieldRadio = FieldSelection.extend({ * @override */ _renderEdit: function () { - var value; + var self = this; + var currentValue; if (this.field.type === 'many2one') { - value = this.value && this.value.data.id; + currentValue = this.value && this.value.data.id; } else { - value = this.value; + currentValue = this.value; } - var index = _.findIndex(this.values, function (option) { - return option[0] === value; + this.$el.empty(); + _.each(this.values, function (value, index) { + self.$el.append(qweb.render('FieldRadio.button', { + checked: value[0] === currentValue, + id: self.unique_id + '_' + value[0], + index: index, + value: value, + })); }); - this.$("input").prop("checked", false); - this.$('input[data-index="' + index + '"]').prop('checked', true); + }, + /** + * @override + */ + _reset: function () { + this._super.apply(this, arguments); + this._setValues(); + }, + /** + * Sets the possible field values. If the field is a many2one, those values + * may change during the lifecycle of the widget if the domain change (an + * onchange may change the domain). + * + * @private + */ + _setValues: function () { + if (this.field.type === 'selection') { + this.values = this.field.selection || []; + } else if (this.field.type === 'many2one') { + this.values = _.map(this.record.specialData[this.name], function (val) { + return [val.id, val.display_name]; + }); + } }, //-------------------------------------------------------------------------- @@ -1930,6 +1996,106 @@ var FieldRadio = FieldSelection.extend({ }, }); +/** + * The FieldReference is a combination of a select (for the model) and + * a FieldMany2one for its value. + * Its intern representation is similar to the many2one (a datapoint with a + * `name_get` as data). + */ +var FieldReference = FieldMany2One.extend({ + supportedFieldTypes: ['reference'], + template: 'FieldReference', + events: _.extend({}, FieldMany2One.prototype.events, { + 'change select': '_onSelectionChange', + }), + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + + // needs to be copied as it is an unmutable object + this.field = _.extend({}, this.field); + if (this.value) { + this._setRelation(this.value.model); + } + }, + /** + * @override + */ + start: function () { + this.$('select').val(this.field.relation); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Add a select in edit mode (for the model). + * + * @override + */ + _renderEdit: function () { + this._super.apply(this, arguments); + + if (this.$('select').val()) { + this.$('.o_input_dropdown').show(); + this.$el.addClass('o_row'); // this class is used to display the two + // components (select & input) on the same line + } else { + // hide the many2one if the selection is empty + this.$('.o_input_dropdown').hide(); + } + + }, + /** + * @override + * @private + */ + _reset: function () { + this._super.apply(this, arguments); + this._setRelation(this.value && this.value.model); + }, + /** + * Set `relation` key in field properties. + * + * @private + * @param {string} model + */ + _setRelation: function (model) { + // used to generate the search in many2one + this.field.relation = model; + }, + /** + * @override + * @private + */ + _setValue: function (value, options) { + value = value || {}; + // we need to specify the model for the change in basic_model + // the value is then now a dict with id, display_name and model + value.model = this.$('select').val(); + return this._super(value, options); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * When the selection (model) changes, the many2one is reset. + * + * @private + */ + _onSelectionChange: function () { + var value = this.$('select').val(); + this.reinitialize(false); + this._setRelation(value); + }, +}); + return { FieldMany2One: FieldMany2One, KanbanFieldMany2One: KanbanFieldMany2One, @@ -1947,6 +2113,8 @@ return { FieldRadio: FieldRadio, FieldSelection: FieldSelection, FieldStatus: FieldStatus, + + FieldReference: FieldReference, }; }); |
