summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-07-07 14:30:30 +0200
committerChristophe Simonis <chs@odoo.com>2017-07-07 14:30:30 +0200
commitbf51fe55a149fb642a410f579dc4b6dcbd794eac (patch)
treef4f208f06094bcef6541fd1fc034e0265de4ad21 /addons/web/static/src/js
parentbf32940142beb879211b7034bb77a7845c9d0555 (diff)
parent0f2b2c4e2b99df8d68456016a4bea86e26c1c80e (diff)
[MERGE] forward port branch saas-16 up to 0f2b2c4e2b9
Diffstat (limited to 'addons/web/static/src/js')
-rw-r--r--addons/web/static/src/js/apps.js20
-rw-r--r--addons/web/static/src/js/chrome/pager.js2
-rw-r--r--addons/web/static/src/js/chrome/view_manager.js3
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js2
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js47
-rw-r--r--addons/web/static/src/js/fields/field_registry.js1
-rw-r--r--addons/web/static/src/js/fields/field_utils.js52
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js254
-rw-r--r--addons/web/static/src/js/services/data_manager.js5
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js77
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js696
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_controller.js11
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js13
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js1
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_view.js1
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js2
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js45
17 files changed, 905 insertions, 327 deletions
diff --git a/addons/web/static/src/js/apps.js b/addons/web/static/src/js/apps.js
index fcf21852d2c..fa513372269 100644
--- a/addons/web/static/src/js/apps.js
+++ b/addons/web/static/src/js/apps.js
@@ -59,7 +59,7 @@ var Apps = Widget.extend({
},
destroy: function() {
- $(window).off("message.apps");
+ $(window).off("message." + this.uniq);
if (this.$ifr) {
this.$ifr.remove();
this.$ifr = null;
@@ -83,13 +83,13 @@ var Apps = Widget.extend({
});
},
'rpc': function(m) {
- self.session.rpc.apply(self.session, m.args).then(function(r) {
+ return self._rpc({route: m.args[0], params: m.args[1]}).then(function(r) {
var w = self.$ifr[0].contentWindow;
w.postMessage({id: m.id, result: r}, client.origin);
});
},
'Model': function(m) {
- return this._rpc({model: m.model, method: m.method, args: m.args})
+ return self._rpc({model: m.model, method: m.args[0], args: m.args[1]})
.then(function(r) {
var w = self.$ifr[0].contentWindow;
w.postMessage({id: m.id, result: r}, client.origin);
@@ -114,10 +114,11 @@ var Apps = Widget.extend({
qs.debug = session.debug;
}
var u = $.param.querystring(client.origin + "/apps/embed/client", qs);
- var css = {width: '100%', height: '400px'};
+ var css = {width: '100%', height: '750px'};
self.$ifr = $('<iframe>').attr('src', u);
- $(window).on("message.apps", self.proxy('_on_message'));
+ self.uniq = _.uniqueId('apps');
+ $(window).on("message." + self.uniq, self.proxy('_on_message'));
self.on('message:ready', self, function(m) {
var w = this.$ifr[0].contentWindow;
@@ -125,8 +126,8 @@ var Apps = Widget.extend({
type: 'ir.actions.client',
tag: this.remote_action_tag,
params: _.extend({}, this.params, {
- db: this.session.db,
- origin: this.session.origin,
+ db: session.db,
+ origin: session.origin,
})
};
w.postMessage({type:'action', action: act}, client.origin);
@@ -145,7 +146,10 @@ var Apps = Widget.extend({
def.resolve();
}, function() {
self.do_warn(_t('Odoo Apps will be available soon'), _t('Showing locally available modules'), true);
- return session.rpc('/web/action/load', {action_id: self.failback_action_id}).then(function(action) {
+ return self._rpc({
+ route: '/web/action/load',
+ params: {action_id: self.failback_action_id},
+ }).then(function(action) {
return self.do_action(action);
}).always(function () {
def.reject();
diff --git a/addons/web/static/src/js/chrome/pager.js b/addons/web/static/src/js/chrome/pager.js
index cd16382be0e..bbe69c6d86a 100644
--- a/addons/web/static/src/js/chrome/pager.js
+++ b/addons/web/static/src/js/chrome/pager.js
@@ -144,7 +144,7 @@ var Pager = Widget.extend({
_edit: function () {
if (this.options.can_edit) {
var self = this;
- var $input = $('<input>', {type: 'text', value: this.$value.html()});
+ var $input = $('<input>', {class: 'o_input', type: 'text', value: this.$value.html()});
this.$value.html($input);
$input.focus();
diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js
index d0beb5bf3dd..11be6fa4e6e 100644
--- a/addons/web/static/src/js/chrome/view_manager.js
+++ b/addons/web/static/src/js/chrome/view_manager.js
@@ -567,11 +567,10 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
// Wrong default_* and search_default_* values will no give the expected result
// Wrong group_by values will simply fail and forbid rendering of the destination view
var ncontext = new Context(
- _.object(_.reject(_.pairs(self.env.context), function(pair) {
+ _.object(_.reject(_.pairs(_.extend({}, action_data.context, self.env.context)), function(pair) {
return pair[0].match('^(?:(?:default_|search_default_|show_).+|.+_view_ref|group_by|group_by_no_leaf|active_id|active_ids)$') !== null;
}))
);
- ncontext.add(action_data.context || {});
ncontext.add({active_model: self.env.modelName});
if (res_ids) {
ncontext.add({
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('&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]);
+}
+
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,
};
});
diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js
index 41fc293e383..c1e69a72ce3 100644
--- a/addons/web/static/src/js/services/data_manager.js
+++ b/addons/web/static/src/js/services/data_manager.js
@@ -355,6 +355,11 @@ return core.Class.extend({
return {};
}),
};
+ if (attrs.options.color_field) {
+ // used by m2m tags
+ attrs.relatedFields[attrs.options.color_field] = { type: 'integer' };
+ attrs.fieldsInfo.default[attrs.options.color_field] = {};
+ }
}
}
return attrs;
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 67a4fa504d8..82dc6f90d26 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -104,39 +104,15 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
return true;
},
/**
- * Discards the changes made to the record whose ID is given, if necessary.
- * Automatically leaves to default mode for the given record.
+ * Waits for the mutex to be unlocked and then calls _.discardChanges.
+ * This ensures that the confirm dialog isn't displayed directly if there is
+ * a pending 'write' rpc.
*
- * @param {string} [recordID] - default to main recordID
- * @param {Object} [options]
- * @param {boolean} [options.readonlyIfRealDiscard=false]
- * After discarding record changes, the usual option is to make the
- * record readonly. However, the view manager calls this function
- * at inappropriate times in the current code and in that case, we
- * don't want to go back to readonly if there is nothing to discard
- * (e.g. when switching record in edit mode in form view, we expect
- * the new record to be in edit mode too, but the view manager calls
- * this function as the URL changes...) @todo get rid of this when
- * the view manager is improved.
- * @returns {Deferred}
+ * @see _.discardChanges
*/
discardChanges: function (recordID, options) {
- var self = this;
- recordID = recordID || this.handle;
- return this.canBeDiscarded(recordID).then(function (needDiscard) {
- if (options && options.readonlyIfRealDiscard && !needDiscard) {
- return;
- }
-
- if (needDiscard) { // Just some optimization
- self.model.discardChanges(recordID);
- }
- if (self.model.isNew(recordID)) {
- self._abandonRecord(recordID);
- return;
- }
- return self._confirmSave(recordID);
- });
+ return this.mutex.exec(function () {})
+ .then(this._discardChanges.bind(this, recordID || this.handle, options));
},
/**
* Method that will be overriden by the views with the ability to have selected ids
@@ -327,6 +303,42 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
}
},
/**
+ * Discards the changes made to the record whose ID is given, if necessary.
+ * Automatically leaves to default mode for the given record.
+ *
+ * @private
+ * @param {string} [recordID] - default to main recordID
+ * @param {Object} [options]
+ * @param {boolean} [options.readonlyIfRealDiscard=false]
+ * After discarding record changes, the usual option is to make the
+ * record readonly. However, the view manager calls this function
+ * at inappropriate times in the current code and in that case, we
+ * don't want to go back to readonly if there is nothing to discard
+ * (e.g. when switching record in edit mode in form view, we expect
+ * the new record to be in edit mode too, but the view manager calls
+ * this function as the URL changes...) @todo get rid of this when
+ * the view manager is improved.
+ * @returns {Deferred}
+ */
+ _discardChanges: function (recordID, options) {
+ var self = this;
+ recordID = recordID || this.handle;
+ return this.canBeDiscarded(recordID)
+ .then(function (needDiscard) {
+ if (options && options.readonlyIfRealDiscard && !needDiscard) {
+ return;
+ }
+ if (needDiscard) { // Just some optimization
+ self.model.discardChanges(recordID);
+ }
+ if (self.model.isNew(recordID)) {
+ self._abandonRecord(recordID);
+ return;
+ }
+ return self._confirmSave(recordID);
+ });
+ },
+ /**
* Enables buttons so they can be clicked again.
*
* @private
@@ -501,11 +513,8 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
var self = this;
ev.stopPropagation();
var recordID = ev.data.recordID;
- this.discardChanges(recordID)
+ this._discardChanges(recordID)
.done(function () {
- if (self.model.isNew(recordID)) {
- self._abandonRecord(recordID);
- }
// TODO this will tell the renderer to rerender the widget that
// asked for the discard but will unfortunately lose the click
// made on another row if any
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 716624f368b..2f06168fd23 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -146,8 +146,11 @@ var BasicModel = AbstractModel.extend({
//--------------------------------------------------------------------------
/**
- * Add a default record to a list object. This method actually make a new
- * record with the _makeDefaultRecord method, then add it to the list object.
+ * Add a default record to a list object. This method actually makes a new
+ * record with the _makeDefaultRecord method, then adds it to the list object.
+ * The default record is added in the data directly. This is meant to be used
+ * by list or kanban controllers (i.e. not for x2manys in form views, as in
+ * this case, we store changes as commands).
*
* @param {string} listID a valid handle for a list object
* @param {Object} [options]
@@ -156,6 +159,7 @@ var BasicModel = AbstractModel.extend({
* @returns {Deferred<string>} resolves to the id of the new created record
*/
addDefaultRecord: function (listID, options) {
+ var self = this;
var list = this.localData[listID];
var context = this._getContext(list);
@@ -169,12 +173,13 @@ var BasicModel = AbstractModel.extend({
};
return this._makeDefaultRecord(list.model, params).then(function (id) {
list.count++;
- list._changes = list._changes || list.data.slice(0);
if (position === 'top') {
- list._changes.unshift(id);
+ list.data.unshift(id);
} else {
- list._changes.push(id);
+ list.data.push(id);
}
+ var record = self.localData[id];
+ list._cache[record.res_id] = id;
return id;
});
},
@@ -327,15 +332,20 @@ var BasicModel = AbstractModel.extend({
} else {
data[fieldName] = this.get(data[fieldName]) || false;
}
- }
- if (field.type === 'one2many' || field.type === 'many2many') {
+ } else if (field.type === 'reference') {
if (options.raw) {
relDataPoint = this.localData[data[fieldName]];
- var relData = relDataPoint._changes || relDataPoint.data;
- var ids = _.map(relData, function (id) {
- return self.localData[id].res_id;
- });
- data[fieldName] = ids;
+ data[fieldName] = relDataPoint ?
+ relDataPoint.model + ',' + relDataPoint.res_id :
+ false;
+ } else {
+ data[fieldName] = this.get(data[fieldName]) || false;
+ }
+ } else if (field.type === 'one2many' || field.type === 'many2many') {
+ if (options.raw) {
+ relDataPoint = this.localData[data[fieldName]];
+ relDataPoint = this._applyX2ManyOperations(relDataPoint);
+ data[fieldName] = relDataPoint.res_ids;
} else {
data[fieldName] = this.get(data[fieldName]) || [];
}
@@ -376,25 +386,15 @@ var BasicModel = AbstractModel.extend({
}
// here, type === 'list'
- var listData, listCount, resIDs;
- if (element._changes) {
- listData = element._changes;
- listCount = listData.length;
- resIDs = _.map(listData, function (elemID) {
- return self.localData[elemID].res_id;
- });
- } else {
- listData = element.data;
- listCount = element.count;
- resIDs = element.res_ids;
- }
- listData = _.map(listData, function (elemID) {
+ // apply potential changes (only for x2many lists)
+ element = this._applyX2ManyOperations(element);
+ var listData = _.map(element.data, function (elemID) {
return self.get(elemID, options);
});
var list = {
aggregateValues: _.extend({}, element.aggregateValues),
context: _.extend({}, element.context),
- count: listCount,
+ count: element.count,
data: listData,
domain: element.domain.slice(0),
fields: element.fields,
@@ -409,7 +409,7 @@ var BasicModel = AbstractModel.extend({
offset: element.offset,
orderedBy: element.orderedBy,
res_id: element.res_id,
- res_ids: resIDs,
+ res_ids: element.res_ids,
type: 'list',
value: element.value,
viewType: element.viewType,
@@ -417,6 +417,7 @@ var BasicModel = AbstractModel.extend({
if (element.fieldsInfo) {
list.fieldsInfo = element.fieldsInfo;
}
+ this._sortList(list);
return list;
},
/**
@@ -538,6 +539,7 @@ var BasicModel = AbstractModel.extend({
id: id,
display_name: display_name,
},
+ parentID: record.id,
});
record.data[field.name] = dataPoint.id;
if (display_name === undefined) {
@@ -553,6 +555,7 @@ var BasicModel = AbstractModel.extend({
var dpParams = {
fieldsInfo: relatedFieldsInfo,
modelName: field.relation,
+ parentID: record.id,
static: true,
type: 'list',
viewType: 'default',
@@ -570,9 +573,11 @@ var BasicModel = AbstractModel.extend({
var recordDP = self._makeDataPoint({
data: data,
modelName: field.relation,
+ parentID: dataPoint.id,
type: 'record',
});
dataPoint.data.push(recordDP.id);
+ dataPoint._cache[recordDP.res_id] = recordDP.id;
});
} else {
dpParams.res_ids = field.value;
@@ -689,7 +694,16 @@ var BasicModel = AbstractModel.extend({
removeLine: function (elementID) {
var record = this.localData[elementID];
var parent = this.localData[record.parentID];
- parent._changes = _.without(parent._changes, elementID);
+ if (parent.parentID) {
+ // x2Many case: the new record has been stored in _changes, as a
+ // command so we remove the command(s) related to that record
+ parent._changes = _.filter(parent._changes, function (change) {
+ return change.id !== elementID;
+ });
+ } else {
+ // main list view case: the new record is in data
+ parent.data = _.without(parent.data, elementID);
+ }
},
/**
* Save a local resource, if needed. This is a complicated operation,
@@ -809,13 +823,7 @@ var BasicModel = AbstractModel.extend({
record.fieldsInfo = _.defaults(record.fieldsInfo, viewInfo.fieldsInfo);
},
/**
- * For list resources, this changes the orderedBy key, then performs the
- * sort directly, in javascript. This is used for sorting static datasets,
- * such as a one2many in a form view. For dynamic datasets, such as a list
- * view, this method will be called, but then the sort will be ignored since
- * we will reload data.
- *
- * @todo don't sort in js when we reload data anyway
+ * For list resources, this changes the orderedBy key.
*
* @param {string} list_id id for the list resource
* @param {string} fieldName valid field name
@@ -837,7 +845,6 @@ var BasicModel = AbstractModel.extend({
});
list.orderedBy = [{name: fieldName, asc: true}].concat(orderedBy);
}
- this._sortList(list);
return this;
},
/**
@@ -894,6 +901,36 @@ var BasicModel = AbstractModel.extend({
//--------------------------------------------------------------------------
/**
+ * Add a default record to a list object. This method actually makes a new
+ * record with the _makeDefaultRecord method, then adds it to the list object
+ * as a 'ADD' command in its _changes. This is meant to be used x2many lists,
+ * not by list or kanban controllers.
+ *
+ * @private
+ * @param {string} listID a valid handle for a list object
+ * @param {Object} [options]
+ * @param {string} [options.position=top] if the new record should be added
+ * on top or on bottom of the list
+ * @returns {Deferred -> string} resolves to the new record id
+ */
+ _addX2ManyDefaultRecord: function (list, options) {
+ var self = this;
+ var params = {
+ context: this._getContext(list),
+ fields: list.fields,
+ fieldsInfo: list.fieldsInfo,
+ parentID: list.id,
+ viewType: list.viewType,
+ };
+ return this._makeDefaultRecord(list.model, params).then(function (id) {
+ var position = options && options.position || 'top';
+ list._changes.push({operation: 'ADD', id: id, position: position});
+ var record = self.localData[id];
+ list._cache[record.res_id] = id;
+ return id;
+ });
+ },
+ /**
* This method is the private version of notifyChanges. Unlike
* notifyChanges, it is not protected by a mutex. Every changes from the
* user to the model go through this method.
@@ -917,8 +954,8 @@ var BasicModel = AbstractModel.extend({
field = record.fields[fieldName];
if (field.type === 'one2many' || field.type === 'many2many') {
defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], viewType));
- } else if (field.type === 'many2one') {
- defs.push(this._applyMany2OneChange(record, fieldName, changes[fieldName]));
+ } else if (field.type === 'many2one' || field.type === 'reference') {
+ defs.push(this._applyX2OneChange(record, fieldName, changes[fieldName]));
} else {
record._changes[fieldName] = changes[fieldName];
}
@@ -960,19 +997,19 @@ var BasicModel = AbstractModel.extend({
});
},
/**
- * Apply a many2one onchange. There is a need for this function because the
- * server only gives an id when a onchange modifies a many2one field. For
- * this reason, we need (sometimes) to do a /name_get to fetch a
- * display_name.
+ * Apply an x2one (either a many2one or a reference field) change. There is
+ * a need for this function because the server only gives an id when a
+ * onchange modifies a many2one field. For this reason, we need (sometimes)
+ * to do a /name_get to fetch a display_name.
*
* @param {Object} record
* @param {string} fieldName
* @param {Object} [data]
* @returns {Deferred}
*/
- _applyMany2OneChange: function (record, fieldName, data) {
+ _applyX2OneChange: function (record, fieldName, data) {
var self = this;
- if (!data) {
+ if (!data || !data.id) {
record._changes[fieldName] = false;
return $.when();
}
@@ -993,12 +1030,16 @@ var BasicModel = AbstractModel.extend({
return $.when();
}
var rel_data = _.pick(data, 'id', 'display_name');
+ var field = record.fields[fieldName];
+
+ // the reference field doesn't store its co-model in its field metadata
+ // but directly in the data (as the co-model isn't fixed)
+ var coModel = field.type === 'reference' ? data.model : field.relation;
var def;
if (rel_data.display_name === undefined) {
- var field = record.fields[fieldName];
// TODO: refactor this to use _fetchNameGet
def = this._rpc({
- model: field.relation,
+ model: coModel,
method: 'name_get',
args: [data.id],
context: record.context,
@@ -1013,13 +1054,14 @@ var BasicModel = AbstractModel.extend({
data: rel_data,
fields: {},
fieldsInfo: {},
- modelName: record.fields[fieldName].relation,
+ modelName: coModel,
+ parentID: record.id,
});
record._changes[fieldName] = rec.id;
});
},
/**
- * Applies the result on an onchange RPC on a record.
+ * Applies the result of an onchange RPC on a record.
*
* @private
* @param {Object} values the result of the onchange RPC (a mapping of
@@ -1053,6 +1095,7 @@ var BasicModel = AbstractModel.extend({
context: record.context,
data: data,
modelName: field.relation,
+ parentID: record.id,
});
id = rec.id;
}
@@ -1070,12 +1113,15 @@ var BasicModel = AbstractModel.extend({
list = self._makeDataPoint({
fieldsInfo: fieldInfo.fieldsInfo,
modelName: field.relation,
+ parentID: record.id,
+ static: true,
type: 'list',
viewtype: fieldInfo.viewType,
});
}
record._changes[name] = list.id;
var shouldLoad = false;
+ list._changes = list._changes || [];
_.each(val, function (command) {
if (command[0] === 0 || command[0] === 1) {
// CREATE or UPDATE
@@ -1091,23 +1137,25 @@ var BasicModel = AbstractModel.extend({
params.res_id = command[1];
}
rec = self._makeDataPoint(params);
- list._changes.push(rec.id);
+ list._cache[rec.res_id] = rec.id;
+ list._changes.push({operation: 'ADD', id: rec.id});
+ if (command[0] === 1) {
+ list._changes.push({operation: 'UPDATE', id: rec.id});
+ }
defs.push(self._applyOnChange(command[2], rec));
} else if (command[0] === 4) {
// LINK TO
- list.res_ids.push(command[1]);
- list.count++;
+ // the dataPoint id will be set when the record will be fetched (for now,
+ // this dataPoint doesn't exist yet)
+ list._changes.push({operation: 'ADD', id: null, resID: command[1]});
shouldLoad = true;
} else if (command[0] === 5) {
// DELETE ALL
- list._changes = [];
+ list._changes = [{operation: 'REMOVE_ALL'}];
}
});
if (shouldLoad) {
- var def = self._readUngroupedList(list).then(function () {
- list._changes = list.data;
- });
- defs.push(def);
+ defs.push(self._readUngroupedList(list));
}
} else {
record._changes[name] = self._parseServerValue(field, val);
@@ -1143,9 +1191,9 @@ var BasicModel = AbstractModel.extend({
var field = record.fields[fieldName];
var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName];
var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode];
- var rec;
+ var def, rec;
var defs = [];
- list._changes = list._changes || list.data.slice(0);
+ list._changes = list._changes || [];
switch (command.operation) {
case 'ADD':
@@ -1163,8 +1211,8 @@ var BasicModel = AbstractModel.extend({
newRecord.fields = list.fields;
newRecord.fieldsInfo = list.fieldsInfo;
newRecord.viewType = list.viewType;
- list._changes.push(newRecord.id);
- this._sortList(list);
+ list._cache[newRecord.res_id] = newRecord.id;
+ list._changes.push(command);
break;
case 'ADD_M2M':
// force to use link command instead of create command
@@ -1184,8 +1232,8 @@ var BasicModel = AbstractModel.extend({
parentID: list.id,
});
list_records[d.id] = rec;
- list._changes.push(rec.id);
- self._sortList(list);
+ list._cache[rec.res_id] = rec.id;
+ list._changes.push({operation: 'ADD', id: rec.id});
});
// read list's records as we only have their ids and optionally their display_name
// (we can't use function readUngroupedList because those records are only in the
@@ -1194,44 +1242,62 @@ var BasicModel = AbstractModel.extend({
// so that if a record is removed and then re-added, it won't be fetched twice
var fieldNames = list.getFieldNames();
if (fieldNames.length) {
- var def = this._rpc({
+ def = this._rpc({
model: list.model,
method: 'read',
args: [_.pluck(data, 'id'), fieldNames]
}).then(function (records) {
_.each(records, function (record) {
list_records[record.id].data = record;
- self._parseServerData(fieldNames, list.fields, record);
+ self._parseServerData(fieldNames, list, record);
});
- return self._fetchX2ManysBatched(list);
+ return $.when(
+ self._fetchX2ManysBatched(list),
+ self._fetchReferencesBatched(list)
+ );
});
defs.push(def);
}
break;
case 'CREATE':
- if (command.data) {
- defs.push(this.addDefaultRecord(list.id).then(function (id) {
+ var options = {position: command.position};
+ def = this._addX2ManyDefaultRecord(list, options).then(function (id) {
+ // FIXME: hack for lunch widget, which does useless default_get and onchange
+ if (command.data) {
return self._applyChange(id, command.data);
- }));
- } else {
- defs.push(this.addDefaultRecord(list.id, {position: command.position}));
- }
+ }
+ });
+ defs.push(def);
break;
case 'UPDATE':
- defs.push(this._applyChange(command.id, command.data));
+ list._changes.push({operation: 'UPDATE', id: command.id});
+ if (command.data) {
+ defs.push(this._applyChange(command.id, command.data));
+ }
break;
case 'REMOVE':
- list._changes = _.difference(list._changes, command.ids);
+ // filter out existing operations involving the current
+ // dataPoint, and add a 'REMOVE' operation only if there is
+ // no 'ADD' operation for that dataPoint, as it would mean
+ // that the record wasn't in the relation yet
+ var idsToRemove = command.ids;
+ list._changes = _.reject(list._changes, function (change) {
+ var idInCommands = _.contains(command.ids, change.id);
+ if (idInCommands && change.operation === 'ADD') {
+ idsToRemove = _.without(idsToRemove, change.id);
+ }
+ return idInCommands;
+ });
+ _.each(idsToRemove, function (id) {
+ list._changes.push({operation: 'REMOVE', id: id});
+ });
break;
case 'REPLACE_WITH':
- // this is certainFly not optimal... and not sure that it is
+ // this is certainly not optimal... and not sure that it is
// correct if some ids are added and some other are removed
- var currentData = _.map(list._changes, function (localId) {
- return self.localData[localId];
- });
- var currentIds = _.pluck(currentData, 'res_id');
- var newIds = _.difference(command.ids, currentIds);
- var removedIds = _.difference(currentIds, command.ids);
+ list._changes = [];
+ var newIds = _.difference(command.ids, list.res_ids);
+ var removedIds = _.difference(list.res_ids, command.ids);
var addDef, removedDef, values;
if (newIds.length) {
values = _.map(newIds, function (id) {
@@ -1243,18 +1309,87 @@ var BasicModel = AbstractModel.extend({
});
}
if (removedIds.length) {
- values = _.filter(currentData, function (dataPoint) {
- return _.contains(removedIds, dataPoint.res_id);
+ var listData = _.map(list.data, function (localId) {
+ return self.localData[localId];
});
removedDef = this._applyX2ManyChange(record, fieldName, {
operation: 'REMOVE',
- ids: _.pluck(values, 'id'),
+ ids: _.map(removedIds, function (resID) {
+ return _.findWhere(listData, {res_id: resID}).id;
+ }),
});
}
return $.when(addDef, removedDef);
}
- return $.when.apply($, defs);
+ return $.when.apply($, defs).then(function () {
+ // ensure to fetch up to 'limit' records (may be useful if records of
+ // the current page have been removed)
+ return self._readUngroupedList(list);
+ });
+ },
+ /**
+ * In dataPoints of type list for x2manys, the changes are stored as a list
+ * of operations (being of type 'ADD', 'REMOVE', 'UPDATE' or 'REMOVE_ALL').
+ * This function applies the operation of such a dataPoint without altering
+ * the original dataPoint. It returns a copy of the dataPoint in which the
+ * 'count', 'data' and 'res_ids' keys have been updated.
+ *
+ * @private
+ * @param {Object} dataPoint of type list
+ * @returns {Object} element of type list in which the commands have been
+ * applied
+ */
+ _applyX2ManyOperations: function (list) {
+ if (!list.static) {
+ // this function only applies on x2many lists
+ return list;
+ }
+ var self = this;
+ list = _.extend({}, list);
+ list.data = list.data.slice(0);
+ list.res_ids = list.res_ids.slice(0);
+ _.each(list._changes, function (change) {
+ var relRecord;
+ if (change.id) {
+ relRecord = self.localData[change.id];
+ }
+ switch(change.operation) {
+ case 'ADD':
+ list.count++;
+ var resID = relRecord ? relRecord.res_id : change.resID;
+ if (change.position === 'top') {
+ list.res_ids.unshift(resID);
+ } else {
+ list.res_ids.push(resID);
+ }
+ break;
+ case 'REMOVE':
+ list.count--;
+ list.res_ids = _.without(list.res_ids, relRecord.res_id);
+ break;
+ case 'REMOVE_ALL':
+ list.count = 0;
+ list.res_ids = [];
+ break;
+ case 'UPDATE':
+ // nothing to do for UPDATE commands
+ break;
+ }
+ });
+ var idsInRange;
+ if (list.limit) {
+ idsInRange = list.res_ids.slice(list.offset, list.offset + list.limit);
+ } else {
+ idsInRange = list.res_ids;
+ }
+ list.data = [];
+ _.each(idsInRange, function (id) {
+ if (list._cache[id]) {
+ list.data.push(list._cache[id]);
+ }
+ });
+ return list;
},
/**
* Helper method to build a 'spec', that is a description of all fields in
@@ -1368,7 +1503,8 @@ var BasicModel = AbstractModel.extend({
var self = this;
var model;
var records = [];
- var ids = _.map(list._changes || list.data, function (localId) {
+ list = this._applyX2ManyOperations(list);
+ var ids = _.map(list.data, function (localId) {
var record = self.localData[localId];
var data = record._changes || record.data;
var many2oneId = data[fieldName];
@@ -1410,7 +1546,7 @@ var BasicModel = AbstractModel.extend({
model: record.model,
method: 'read',
args: [[record.res_id], fieldNames],
- context: _.extend({}, record.context, {bin_size: true}),
+ context: _.extend({}, record.getContext(), {bin_size: true}),
})
.then(function (result) {
if (result.length === 0) {
@@ -1420,15 +1556,143 @@ var BasicModel = AbstractModel.extend({
record.data = _.extend({}, record.data, result);
})
.then(function () {
- self._parseServerData(fieldNames, record.fields, record.data);
+ self._parseServerData(fieldNames, record, record.data);
})
.then(function () {
- return self._fetchX2Manys(record, options).then(function () {
+ return $.when(
+ self._fetchX2Manys(record, options),
+ self._fetchReferences(record)
+ ).then(function () {
return self._postprocess(record, options);
});
});
},
/**
+ * Fetch the extra data (`name_get`) for the reference fields of the record
+ * model.
+ *
+ * @private
+ * @param {Object} record
+ * @returns {Deferred}
+ */
+ _fetchReferences: function (record) {
+ var self = this;
+ var defs = [];
+ var fieldNames = record.getFieldNames();
+ _.each(fieldNames, function (fieldName) {
+ var field = record.fields[fieldName];
+ if (field.type === 'reference') {
+ var value = record.data[fieldName];
+ if (value) {
+ var model = value.split(',')[0];
+ var resID = parseInt(value.split(',')[1]);
+ var def = self._rpc({
+ model: model,
+ method: 'name_get',
+ args: [resID],
+ context: record.getContext({fieldName: fieldName}),
+ }).then(function (result) {
+ var referenceDp = self._makeDataPoint({
+ data: {
+ id: result[0][0],
+ display_name: result[0][1],
+ },
+ modelName: model,
+ parentID: record.id,
+ });
+ record.data[fieldName] = referenceDp.id;
+ });
+ defs.push(def);
+ }
+ }
+ });
+ return $.when.apply($, defs);
+ },
+ /**
+ * Batch requests for one reference field in list (one request by different
+ * model in the field values).
+ *
+ * @see _fetchReferencesBatched
+ * @param {Object} list
+ * @param {string} fieldName
+ * @returns {Deferred}
+ */
+ _fetchReferenceBatched: function (list, fieldName) {
+ var self = this;
+ list = this._applyX2ManyOperations(list);
+
+ // collect ids by model
+ var toFetch = {};
+ _.each(list.data, function (dataPoint) {
+ var record = self.localData[dataPoint];
+ var value = record.data[fieldName];
+ if (value) {
+ var model = value.split(',')[0];
+ var resID = value.split(',')[1];
+ if (!(model in toFetch)) {
+ toFetch[model] = {};
+ }
+ // there could be multiple datapoints with the same model/resID
+ if (toFetch[model][resID]) {
+ toFetch[model][resID].push(dataPoint);
+ } else {
+ toFetch[model][resID] = [dataPoint];
+ }
+ }
+ });
+
+ var defs = [];
+ var def;
+ // one name_get by model
+ _.each(toFetch, function (datapoints, model) {
+ var ids = _.map(Object.keys(datapoints), function (id) { return parseInt(id); });
+ // we need one parent for the context (they all have the same)
+ var parent = datapoints[ids[0]][0];
+ def = self._rpc({
+ model: model,
+ method: 'name_get',
+ args: [ids],
+ context: self.localData[parent].getContext({fieldName: fieldName}),
+ }).then(function (result) {
+ _.each(result, function (el) {
+ var parentIDs = datapoints[el[0]];
+ _.each(parentIDs, function (parentID) {
+ var parent = self.localData[parentID];
+ var referenceDp = self._makeDataPoint({
+ data: {
+ id: el[0],
+ display_name: el[1],
+ },
+ modelName: model,
+ parentID: parent,
+ });
+ parent.data[fieldName] = referenceDp.id;
+ });
+ });
+ });
+ defs.push(def);
+ });
+
+ return $.when.apply($, defs);
+ },
+ /**
+ * Batch requests for references for datapoint of type list.
+ *
+ * @param {Object} list
+ * @returns {Deferred}
+ */
+ _fetchReferencesBatched: function (list) {
+ var defs = [];
+ var fieldNames = list.getFieldNames();
+ for (var i = 0; i < fieldNames.length; i++) {
+ var field = list.fields[fieldNames[i]];
+ if (field.type === 'reference') {
+ defs.push(this._fetchReferenceBatched(list, fieldNames[i]));
+ }
+ }
+ return $.when.apply($, defs);
+ },
+ /**
* This method is incorrectly named. It should be named something like
* _fetchMany2OneData.
*
@@ -1527,7 +1791,9 @@ var BasicModel = AbstractModel.extend({
var context = record.getContext({fieldName: fieldName});
var domain = record.getDomain({fieldName: fieldName});
if (domain.length) {
- var localID = record._changes && record._changes[fieldName] || record.data[fieldName];
+ var localID = (record._changes && fieldName in record._changes) ?
+ record._changes[fieldName] :
+ record.data[fieldName];
if (localID) {
var element = this.localData[localID];
domain = ["|", ["id", "=", element.data.id]].concat(domain);
@@ -1705,7 +1971,9 @@ var BasicModel = AbstractModel.extend({
def = this._searchReadUngroupedList(list);
}
return def.then(function () {
- return self._fetchX2ManysBatched(list);
+ return $.when(
+ self._fetchX2ManysBatched(list),
+ self._fetchReferencesBatched(list));
}).then(function () {
return list;
});
@@ -1759,7 +2027,10 @@ var BasicModel = AbstractModel.extend({
record.data[fieldName] = list.id;
if (!fieldInfo.__no_fetch) {
var def = self._readUngroupedList(list).then(function () {
- return self._fetchX2ManysBatched(list);
+ return $.when(
+ self._fetchX2ManysBatched(list),
+ self._fetchReferencesBatched(list)
+ );
});
defs.push(def);
}
@@ -1783,12 +2054,12 @@ var BasicModel = AbstractModel.extend({
var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo;
var fields = view ? view.fields : fieldInfo.relatedFields;
var viewType = view ? view.type : fieldInfo.viewType;
- var data = list._changes || list.data;
+ list = this._applyX2ManyOperations(list);
var x2mRecords = [];
// step 1: collect ids
var ids = [];
- _.each(data, function (dataPoint) {
+ _.each(list.data, function (dataPoint) {
var record = self.localData[dataPoint];
if (typeof record.data[fieldName] === 'string') {
// in this case, the value is a local ID, which means that the
@@ -1803,6 +2074,7 @@ var BasicModel = AbstractModel.extend({
fieldsInfo: fieldsInfo,
fields: fields,
modelName: field.relation,
+ parentID: record.id,
res_ids: record.data[fieldName],
static: true,
type: 'list',
@@ -1834,25 +2106,20 @@ var BasicModel = AbstractModel.extend({
}
return def.then(function (results) {
// step 3: assign values to correct datapoints
- var dataPoints = _.map(results, function (result) {
- return self._makeDataPoint({
- modelName: field.relation,
- data: result,
- fields: fields,
- fieldsInfo: fieldsInfo,
- viewType: viewType,
- });
- });
-
_.each(x2mRecords, function (record) {
var m2mList = self.localData[record.data[fieldName]];
-
m2mList.data = [];
_.each(m2mList.res_ids, function (res_id) {
- var dataPoint = _.find(dataPoints, function (d) {
- return d.res_id === res_id;
+ var dataPoint = self._makeDataPoint({
+ modelName: field.relation,
+ data: _.findWhere(results, {id: res_id}),
+ fields: fields,
+ fieldsInfo: fieldsInfo,
+ parentID: m2mList.id,
+ viewType: viewType,
});
m2mList.data.push(dataPoint.id);
+ m2mList._cache[res_id] = dataPoint.id;
});
});
});
@@ -1912,6 +2179,7 @@ var BasicModel = AbstractModel.extend({
// process relational fields and handle the null case
var type = record.fields[fieldName].type;
+ var value;
if (type === 'one2many' || type === 'many2many') {
if (commands[fieldName].length) { // replace localId by commands
changes[fieldName] = commands[fieldName];
@@ -1919,8 +2187,13 @@ var BasicModel = AbstractModel.extend({
delete changes[fieldName];
}
} else if (type === 'many2one' && fieldName in changes) {
- var value = changes[fieldName];
+ value = changes[fieldName];
changes[fieldName] = value ? this.localData[value].res_id : false;
+ } else if (type === 'reference' && fieldName in changes) {
+ value = changes[fieldName];
+ changes[fieldName] = value ?
+ this.localData[value].model + ',' + this.localData[value].res_id :
+ false;
} else if (changes[fieldName] === null) {
changes[fieldName] = false;
}
@@ -1961,6 +2234,8 @@ var BasicModel = AbstractModel.extend({
*
* @param {Object} record
* @param {Object} [options]
+ * @param {string} [options.fieldNames] if given, generates the commands for
+ * these fields only
* @param {boolean} [changesOnly=false] if true, only generates commands for
* fields that have changed
* @param {boolean} [options.withReadonly=false] if false, doesn't generate
@@ -1970,11 +2245,15 @@ var BasicModel = AbstractModel.extend({
_generateX2ManyCommands: function (record, options) {
var self = this;
options = options || {};
+ var fields = record.fields;
+ if (options.fieldNames) {
+ fields = _.pick(fields, options.fieldNames);
+ }
var commands = {};
var data = _.extend({}, record.data, record._changes);
var type;
- for (var fieldName in record.fields) {
- type = record.fields[fieldName].type;
+ for (var fieldName in fields) {
+ type = fields[fieldName].type;
if (type === 'many2many' || type === 'one2many') {
commands[fieldName] = [];
@@ -1983,24 +2262,31 @@ var BasicModel = AbstractModel.extend({
continue;
}
var list = this.localData[data[fieldName]];
- if (options.changesOnly && !list._changes) {
+ if (options.changesOnly && (!list._changes || !list._changes.length)) {
// if only changes are requested, skip if there is no change
continue;
}
- var relData = _.map(list._changes || list.data, function (localId) {
- return self.localData[localId];
+ var oldResIDs = list.res_ids.slice(0);
+ var relRecordAdded = [];
+ var relRecordUpdated = [];
+ _.each(list._changes, function (change) {
+ if (change.operation === 'ADD') {
+ relRecordAdded.push(self.localData[change.id]);
+ } else if (change.operation === 'UPDATE') {
+ relRecordUpdated.push(self.localData[change.id]);
+ }
});
- var relIds = _.pluck(relData, 'res_id');
+ list = this._applyX2ManyOperations(list);
if (type === 'many2many' || list._forceM2MLink) {
// deliberately generate a single 'replace' command instead
// of a 'delete' and a 'link' commands with the exact diff
// because 1) performance-wise it doesn't change anything
// and 2) to guard against concurrent updates (policy: force
// a complete override of the actual value of the m2m)
- commands[fieldName].push(x2ManyCommands.replace_with(relIds));
+ commands[fieldName].push(x2ManyCommands.replace_with(list.res_ids));
// generate update commands for records that have been
// updated (it may happen with editable lists)
- _.each(relData, function (relRecord) {
+ _.each(relRecordUpdated, function (relRecord) {
var changes = self._generateChanges(relRecord, options);
if (!_.isEmpty(changes)) {
var command = x2ManyCommands.update(relRecord.res_id, changes);
@@ -2008,29 +2294,29 @@ var BasicModel = AbstractModel.extend({
}
});
} else if (type === 'one2many') {
- var removedIds = _.difference(list.res_ids, relIds);
- var addedIds = _.difference(relIds, list.res_ids);
- var keptIds = _.intersection(list.res_ids, relIds);
+ var removedIds = _.difference(oldResIDs, list.res_ids);
+ var addedIds = _.difference(list.res_ids, oldResIDs);
+ var keptIds = _.intersection(oldResIDs, list.res_ids);
// the didChange variable keeps track of the fact that at
// least one id was updated
var didChange = false;
var changes, command, relRecord;
- for (var i = 0; i < relIds.length; i++) {
- if (_.contains(keptIds, relIds[i])) {
+ for (var i = 0; i < list.res_ids.length; i++) {
+ if (_.contains(keptIds, list.res_ids[i])) {
// this is an id that already existed
- relRecord = _.findWhere(relData, {res_id: relIds[i]});
- changes = this._generateChanges(relRecord, options);
+ relRecord = _.findWhere(relRecordUpdated, {res_id: list.res_ids[i]});
+ changes = relRecord ? this._generateChanges(relRecord, options) : {};
if (!_.isEmpty(changes)) {
command = x2ManyCommands.update(relRecord.res_id, changes);
didChange = true;
} else {
- command = x2ManyCommands.link_to(relIds[i]);
+ command = x2ManyCommands.link_to(list.res_ids[i]);
}
commands[fieldName].push(command);
- } else if (_.contains(addedIds, relIds[i])) {
+ } else if (_.contains(addedIds, list.res_ids[i])) {
// this is a new id
- relRecord = _.findWhere(relData, {res_id: relIds[i]});
+ relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]});
changes = this._generateChanges(relRecord, options);
commands[fieldName].push(x2ManyCommands.create(changes));
}
@@ -2118,14 +2404,14 @@ var BasicModel = AbstractModel.extend({
if (fieldInfo && fieldInfo.domain) {
return Domain.prototype.stringToArray(
fieldInfo.domain,
- this._getEvalContext(element)
+ this._getEvalContext(element, true)
);
}
var fieldParams = element.fields[options.fieldName];
if (fieldParams.domain) {
return Domain.prototype.stringToArray(
fieldParams.domain,
- this._getEvalContext(element)
+ this._getEvalContext(element, true)
);
}
return [];
@@ -2133,7 +2419,7 @@ var BasicModel = AbstractModel.extend({
return Domain.prototype.stringToArray(
element.domain,
- this._getEvalContext(element)
+ this._getEvalContext(element, true)
);
},
/**
@@ -2145,10 +2431,12 @@ var BasicModel = AbstractModel.extend({
* sure. This allows some domains to use the uid key for example
*
* @param {Object} element - an element from the localData
+ * @param {boolean} [forDomain=false] if true, evaluates x2manys as a list of
+ * ids instead of a list of commands
* @returns {Object}
*/
- _getEvalContext: function (element) {
- var evalContext = element.type === 'record' ? this._getRecordEvalContext(element) : {};
+ _getEvalContext: function (element, forDomain) {
+ var evalContext = element.type === 'record' ? this._getRecordEvalContext(element, forDomain) : {};
if (element.parentID) {
var parent = this.localData[element.parentID];
@@ -2156,7 +2444,7 @@ var BasicModel = AbstractModel.extend({
parent = this.localData[parent.parentID];
}
if (parent.type === 'record') {
- evalContext.parent = this._getRecordEvalContext(parent);
+ evalContext.parent = this._getRecordEvalContext(parent, forDomain);
}
}
return _.extend({
@@ -2184,12 +2472,21 @@ var BasicModel = AbstractModel.extend({
* current values for the record, with commands for x2manys fields.
*
* @param {Object} record an element of type 'record'
+ * @param {boolean} [forDomain=false] if true, x2many values are a list of
+ * ids instead of a list of commands
* @returns Object
*/
- _getRecordEvalContext: function (record) {
+ _getRecordEvalContext: function (record, forDomain) {
var self = this;
var relDataPoint;
var context = _.extend({}, record.data, record._changes);
+
+ // calls _generateX2ManyCommands for a given field, and returns the array of commands
+ function _generateX2ManyCommands (fieldName) {
+ var commands = self._generateX2ManyCommands(record, {fieldNames: [fieldName]});
+ return commands[fieldName];
+ }
+
for (var fieldName in context) {
var field = record.fields[fieldName];
if (context[fieldName] === null) {
@@ -2210,23 +2507,18 @@ var BasicModel = AbstractModel.extend({
continue;
}
if (field.type === 'one2many' || field.type === 'many2many') {
- relDataPoint = this.localData[context[fieldName]];
- var relData = relDataPoint._changes || relDataPoint.data;
- var ids = _.map(relData, function (id) {
- return self.localData[id].res_id;
- });
-
- ids.toJSON = function () {
- return _.map(relData, function (id) {
- var resID = self.localData[id].res_id;
- if (typeof resID === 'string') {
- var changes = self._generateChanges(self.localData[id]);
- return x2ManyCommands.create(changes);
- } else {
- return x2ManyCommands.link_to(resID);
- }
- });
- };
+ var ids;
+ if (!context[fieldName] || _.isArray(context[fieldName])) { // no dataPoint created yet
+ ids = context[fieldName] || [];
+ } else {
+ relDataPoint = this._applyX2ManyOperations(this.localData[context[fieldName]]);
+ ids = relDataPoint.res_ids.slice(0);
+ }
+ if (!forDomain) {
+ // when sent to the server, the x2manys values must be a list
+ // of commands in a context, but the list of ids in a domain
+ ids.toJSON = _generateX2ManyCommands.bind(null, fieldName);
+ }
context[fieldName] = ids;
}
@@ -2247,15 +2539,12 @@ var BasicModel = AbstractModel.extend({
*/
_isFieldReadonly: function (record, fieldName, viewType) {
var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName];
- var modifiers;
if (fieldInfo) {
var rawModifiers = JSON.parse(fieldInfo.modifiers || "{}");
- modifiers = this._evalModifiers(record, rawModifiers);
- }
- if (modifiers && 'readonly' in modifiers) {
+ var modifiers = this._evalModifiers(record, rawModifiers);
return modifiers.readonly;
} else {
- return record.fields[fieldName].readonly;
+ return false;
}
},
/**
@@ -2285,16 +2574,19 @@ var BasicModel = AbstractModel.extend({
* an onchange.
*
* @param {string} id id for a local resource of type 'list'. This is
- * assumed to be a list element for a one2many
+ * assumed to be a list element for an x2many
* @returns {boolean}
*/
_isX2ManyValid: function (id) {
var self = this;
var isValid = true;
var element = this.localData[id];
- _.each(element._changes || element.data, function (recordID) {
- var recordData = self.get(recordID, {raw: true}).data;
- var record = self.localData[recordID];
+ _.each(element._changes, function (command) {
+ if (command.operation === 'REMOVE' || command.operation === 'REMOVE_ALL') {
+ return;
+ }
+ var recordData = self.get(command.id, {raw: true}).data;
+ var record = self.localData[command.id];
_.each(element.getFieldNames(), function (fieldName) {
var field = element.fields[fieldName];
var fieldInfo = element.fieldsInfo[element.viewType][fieldName];
@@ -2477,6 +2769,7 @@ var BasicModel = AbstractModel.extend({
context: record.context,
data: {id: result[name]},
modelName: field.relation,
+ parentID: record.id,
});
record._changes[name] = rec.id;
} else if (field.type === 'one2many' || field.type === 'many2many') {
@@ -2501,6 +2794,7 @@ var BasicModel = AbstractModel.extend({
viewType: viewType,
});
record._changes[name] = x2manyList.id;
+ x2manyList._changes = [];
var many2ones = {};
var r;
var isCommandList = result[name].length && _.isArray(result[name][0]);
@@ -2516,10 +2810,11 @@ var BasicModel = AbstractModel.extend({
context: x2manyList.context,
fieldsInfo: fieldsInfo,
fields: fields,
+ parentID: x2manyList.id,
viewType: viewType,
});
- x2manyList._changes = x2manyList._changes || [];
- x2manyList._changes.push(r.id);
+ x2manyList._changes.push({operation: 'ADD', id: r.id});
+ x2manyList._cache[r.res_id] = r.id;
// this is necessary so the fields are initialized
_.each(r.getFieldNames(), function (fieldName) {
@@ -2533,7 +2828,8 @@ var BasicModel = AbstractModel.extend({
var rec = self._makeDataPoint({
context: r.context,
modelName: r.fields[name].relation,
- data: {id: r._changes[name]}
+ data: {id: r._changes[name]},
+ parentID: r.id,
});
r._changes[name] = rec.id;
many2ones[name] = true;
@@ -2545,7 +2841,10 @@ var BasicModel = AbstractModel.extend({
x2manyList.res_ids = value[2];
x2manyList.count = x2manyList.res_ids.length;
var def = self._readUngroupedList(x2manyList).then(function () {
- return self._fetchX2ManysBatched(x2manyList);
+ return $.when(
+ self._fetchX2ManysBatched(x2manyList),
+ self._fetchReferencesBatched(x2manyList)
+ );
});
defs.push(def);
}
@@ -2586,14 +2885,15 @@ var BasicModel = AbstractModel.extend({
* parse the server values to javascript framwork
*
* @param {[string]} fieldNames
- * @param {Object} fields
- * @param {Object} record
+ * @param {Object} element the dataPoint used as parent for the created
+ * dataPoints
+ * @param {Object} data the server data to parse
*/
- _parseServerData: function (fieldNames, fields, record) {
+ _parseServerData: function (fieldNames, element, data) {
var self = this;
_.each(fieldNames, function (fieldName) {
- var field = fields[fieldName];
- var val = record[fieldName];
+ var field = element.fields[fieldName];
+ var val = data[fieldName];
if (field.type === 'many2one') {
// process many2one: split [id, nameget] and create corresponding record
if (val !== false) {
@@ -2608,14 +2908,15 @@ var BasicModel = AbstractModel.extend({
display_name: val[1],
id: val[0],
},
+ parentID: element.id,
});
- record[fieldName] = r.id;
+ data[fieldName] = r.id;
} else {
// no value for the many2one
- record[fieldName] = false;
+ data[fieldName] = false;
}
} else {
- record[fieldName] = self._parseServerValue(field, val);
+ data[fieldName] = self._parseServerValue(field, val);
}
});
},
@@ -2692,6 +2993,14 @@ var BasicModel = AbstractModel.extend({
});
record._warning = true;
}
+ if (result.domain) {
+ var fieldsInfo = record.fieldsInfo[viewType || record.viewType];
+ for (var fieldName in result.domain) {
+ if (fieldsInfo[fieldName]) {
+ fieldsInfo[fieldName].domain = result.domain[fieldName];
+ }
+ }
+ }
return self._applyOnChange(result.value, record).then(function () {
return result;
});
@@ -2843,10 +3152,14 @@ var BasicModel = AbstractModel.extend({
var def;
var ids = [];
var missingIds = [];
- var upper_bound = list.limit ? Math.min(list.offset + list.limit, list.count) : list.count;
+ // generate the current count and res_ids list by applying the changes
+ var listWithChanges = this._applyX2ManyOperations(list);
+ var currentCount = listWithChanges.count;
+ var currentResIDs = listWithChanges.res_ids;
+ var upper_bound = list.limit ? Math.min(list.offset + list.limit, currentCount) : currentCount;
var fieldNames = list.getFieldNames();
for (var i = list.offset; i < upper_bound; i++) {
- var id = list.res_ids[i];
+ var id = currentResIDs[i];
ids.push(id);
if (!list._cache[id]) {
missingIds.push(id);
@@ -2858,7 +3171,7 @@ var BasicModel = AbstractModel.extend({
model: list.model,
method: 'read',
args: [missingIds, fieldNames],
- context: {} // FIXME
+ context: list.getContext(),
});
} else {
def = $.when(_.map(missingIds, function (id) {
@@ -2876,6 +3189,7 @@ var BasicModel = AbstractModel.extend({
dataPoint = self.localData[list._cache[id]];
} else {
dataPoint = self._makeDataPoint({
+ context: list.context,
data: _.findWhere(records, {id: id}),
fieldsInfo: list.fieldsInfo,
fields: list.fields,
@@ -2885,12 +3199,17 @@ var BasicModel = AbstractModel.extend({
});
// add many2one records
- self._parseServerData(fieldNames, dataPoint.fields, dataPoint.data);
+ self._parseServerData(fieldNames, dataPoint, dataPoint.data);
list._cache[id] = dataPoint.id;
}
+ // set the dataPoint id in potential 'ADD' operation adding the current record
+ _.each(list._changes, function (change) {
+ if (change.operation === 'ADD' && !change.id && change.resID === id) {
+ change.id = dataPoint.id;
+ }
+ });
list.data.push(dataPoint.id);
});
- self._sortList(list);
return list;
});
},
@@ -2927,7 +3246,7 @@ var BasicModel = AbstractModel.extend({
route: '/web/dataset/search_read',
model: list.model,
fields: fieldNames,
- context: list.context,
+ context: list.getContext(),
domain: list.domain || [],
limit: list.limit,
offset: list.loadMoreOffset + list.offset,
@@ -2938,6 +3257,7 @@ var BasicModel = AbstractModel.extend({
var ids = _.pluck(result.records, 'id');
var data = _.map(result.records, function (record) {
var dataPoint = self._makeDataPoint({
+ context: list.context,
data: record,
fields: list.fields,
fieldsInfo: list.fieldsInfo,
@@ -2947,7 +3267,7 @@ var BasicModel = AbstractModel.extend({
});
// add many2one records
- self._parseServerData(fieldNames, dataPoint.fields, dataPoint.data);
+ self._parseServerData(fieldNames, dataPoint, dataPoint.data);
return dataPoint.id;
});
if (list.loadMoreOffset) {
@@ -2978,22 +3298,20 @@ var BasicModel = AbstractModel.extend({
},
/**
* Do a in-memory sort of a list resource data points. This method assumes
- * that the list data has already been fetched. Its intended use is for
- * static datasets, such as a one2many in a form view.
+ * that the list data has already been fetched, and that the changes have
+ * been applied. Its intended use is for static datasets, such as a one2many
+ * in a form view.
*
- * @param {Object} list
+ * @param {Object} list list dataPoint on which changes have been applied
*/
_sortList: function (list) {
- var self = this;
if (list.orderedBy.length) {
// sort records according to ordered_by[0]
var order = list.orderedBy[0];
- var data = list._changes || list.data;
- data.sort(function (id1, id2) {
- var r1 = self.localData[id1];
- var r2 = self.localData[id2];
- var data1 = r1._changes || r1.data;
- var data2 = r2._changes || r2.data;
+ var data = list.data;
+ data.sort(function (r1, r2) {
+ var data1 = r1.data;
+ var data2 = r2.data;
if (data1[order.name] < data2[order.name]) {
return order.asc ? -1 : 1;
}
@@ -3040,8 +3358,8 @@ var BasicModel = AbstractModel.extend({
}
}
if (element.type === 'list') {
- var listData = element._changes || element.data;
- _.each(listData, function (elemId) {
+ element = this._applyX2ManyOperations(element);
+ _.each(element.data, function (elemId) {
var elem = self.localData[elemId];
self._visitChildren(elem, fn);
});
diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js
index d4c96a15736..db2394ddd1d 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -112,13 +112,18 @@ var CalendarController = AbstractController.extend({
*/
_onChangeDate: function (event) {
var modelData = this.model.get();
- if (modelData.target_date.toString() === event.data.date) {
+ if (modelData.target_date.isSame(event.data.date)) {
+ // When clicking on same date, toggle between the two views
switch (modelData.scale) {
case 'month': this.model.setScale('week'); break;
case 'week': this.model.setScale('day'); break;
case 'day': this.model.setScale('month'); break;
}
+ } else if (modelData.target_date.week() === event.data.date.week()) {
+ // When clicking on a date in the same week, switch to day view
+ this.model.setScale('day');
} else {
+ // When clicking on a random day of a random other week, switch to week view
this.model.setScale('week');
}
this.model.setDate(event.data.date, true);
@@ -151,8 +156,10 @@ var CalendarController = AbstractController.extend({
self.quick.destroy();
self.quick = null;
self.reload(id);
- }, function () {
+ }, function (error, errorEvent) {
// This will occurs if there are some more fields required
+ // Preventdefaulting the error event will prevent the traceback window
+ errorEvent.preventDefault();
event.data.options.disableQuickCreate = true;
event.data.data.on_save = self.quick.destroy.bind(self.quick);
self._onOpenCreate(event.data);
diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js
index cb371bfe6f9..ded7938605f 100644
--- a/addons/web/static/src/js/views/calendar/calendar_model.js
+++ b/addons/web/static/src/js/views/calendar/calendar_model.js
@@ -393,16 +393,19 @@ return AbstractModel.extend({
this.data.fc_options = this._getFullCalendarOptions();
var defs = _.map(this.data.filters, this._loadFilter.bind(this));
-
return $.when.apply($, defs).then(function () {
- return self._rpc({
+ var eventsDef = $.Deferred().resolve([]);
+ var filterDomain = self._getFilterDomain();
+ if (filterDomain.length || _.isEmpty(self.data.filters)) {
+ eventsDef = self._rpc({
model: self.modelName,
method: 'search_read',
context: self.data.context,
fields: self.fieldNames,
- domain: self.data.domain.concat(self._getRangeDomain()).concat(self._getFilterDomain())
- })
- .then(function (events) {
+ domain: self.data.domain.concat(self._getRangeDomain()).concat(filterDomain)
+ });
+ }
+ return eventsDef.then(function (events) {
self.data.data = _.map(events, self._recordToCalendarEvent.bind(self));
return $.when(
self._loadColors(self.data, self.data.data),
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index 23d63aa9044..a4930c87d37 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -164,6 +164,7 @@ return AbstractRenderer.extend({
init: function (parent, state, params) {
this._super.apply(this, arguments);
this.displayFields = params.displayFields;
+ this.model = params.model;
this.filters = [];
this.color_map = {};
diff --git a/addons/web/static/src/js/views/calendar/calendar_view.js b/addons/web/static/src/js/views/calendar/calendar_view.js
index 135224fb0cc..71a6c30b756 100644
--- a/addons/web/static/src/js/views/calendar/calendar_view.js
+++ b/addons/web/static/src/js/views/calendar/calendar_view.js
@@ -119,6 +119,7 @@ var CalendarView = AbstractView.extend({
this.rendererParams.displayFields = displayFields;
this.rendererParams.eventTemplate = _.findWhere(arch.children, {'tag': 'templates'});
+ this.rendererParams.model = viewInfo.model;
this.loadParams.fieldNames = _.uniq(fieldNames);
this.loadParams.mapping = mapping;
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index e9bc8cf2a2a..f741e98bf4d 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -409,7 +409,7 @@ var FormController = BasicController.extend({
* @private
*/
_onDiscard: function () {
- this.discardChanges();
+ this._discardChanges();
},
/**
* Called when the user clicks on 'Duplicate Record' in the sidebar
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index ee0fd323ce1..2ec9cf3181e 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -50,27 +50,6 @@ var ListController = BasicController.extend({
//--------------------------------------------------------------------------
/**
- * To improve performance, list view must not be rerendered if it is asked
- * to discard all its changes. Indeed, only the in-edition row needs to be
- * discarded in that case.
- *
- * @override
- * @param {string} [recordID] - default to main recordID
- * @returns {Deferred}
- */
- discardChanges: function (recordID) {
- if ((recordID || this.handle) === this.handle) {
- recordID = this.renderer.getEditableRecordID();
- if (recordID === null) {
- return $.when();
- }
- }
- var self = this;
- return this._super(recordID).then(function () {
- self._updateButtons('readonly');
- });
- },
- /**
* Calculate the active domain of the list view. This should be done only
* if the header checkbox has been checked. This is done by evaluating the
* search results, and then adding the dataset domain (i.e. action domain).
@@ -246,6 +225,28 @@ var ListController = BasicController.extend({
.then(this._setMode.bind(this, 'readonly', id));
},
/**
+ * To improve performance, list view must not be rerendered if it is asked
+ * to discard all its changes. Indeed, only the in-edition row needs to be
+ * discarded in that case.
+ *
+ * @override
+ * @private
+ * @param {string} [recordID] - default to main recordID
+ * @returns {Deferred}
+ */
+ _discardChanges: function (recordID) {
+ if ((recordID || this.handle) === this.handle) {
+ recordID = this.renderer.getEditableRecordID();
+ if (recordID === null) {
+ return $.when();
+ }
+ }
+ var self = this;
+ return this._super(recordID).then(function () {
+ self._updateButtons('readonly');
+ });
+ },
+ /**
* @override
* @private
*/
@@ -362,7 +363,7 @@ var ListController = BasicController.extend({
*/
_onDiscard: function (ev) {
ev.stopPropagation(); // So that it is not considered as a row leaving
- this.discardChanges();
+ this._discardChanges();
},
/**
* Called when the user asks to edit a row -> Updates the controller buttons