summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/basic
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js/views/basic')
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js78
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js399
-rw-r--r--addons/web/static/src/js/views/basic/basic_renderer.js24
-rw-r--r--addons/web/static/src/js/views/basic/basic_view.js27
4 files changed, 353 insertions, 175 deletions
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 1e0187b348e..53e5be8ff6e 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -18,6 +18,8 @@ var _t = core._t;
var BasicController = AbstractController.extend(FieldManagerMixin, {
custom_events: _.extend({}, AbstractController.prototype.custom_events, FieldManagerMixin.custom_events, {
+ discard_changes: '_onDiscardChanges',
+ mutexify: '_onMutexify',
reload: '_onReload',
sidebar_data_asked: '_onSidebarDataAsked',
translate: '_onTranslate',
@@ -247,9 +249,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
var self = this;
var def = $.Deferred();
var reload = function () {
- if (!self.isDestroyed()) {
- self.reload();
- }
+ return self.isDestroyed() ? $.when() : self.reload();
};
record = record || this.model.get(this.handle);
var recordID = record.data.id;
@@ -258,13 +258,17 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
context: record.getContext({additionalContext: attrs.context}),
}),
model: record.model,
- record_id: recordID,
+ res_ids: [recordID],
on_closed: function (reason) {
if (!_.isObject(reason)) {
- reload();
+ reload(reason);
}
},
- on_fail: reload,
+ on_fail: function (reason) {
+ reload().always(function() {
+ def.reject(reason);
+ })
+ },
on_success: def.resolve.bind(def),
});
return this.alive(def);
@@ -306,6 +310,26 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
}
},
/**
+ * Disables buttons so that they can't be clicked anymore.
+ *
+ * @private
+ */
+ _disableButtons: function () {
+ if (this.$buttons) {
+ this.$buttons.find('button').attr('disabled', true);
+ }
+ },
+ /**
+ * Enables buttons so they can be clicked again.
+ *
+ * @private
+ */
+ _enableButtons: function () {
+ if (this.$buttons) {
+ this.$buttons.find('button').removeAttr('disabled');
+ }
+ },
+ /**
* Returns the new sidebar env
*
* @private
@@ -385,18 +409,19 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
// Note: it is the model's job to do nothing if there is nothing to save
if (this.canBeSaved(recordID)) {
var self = this;
- var def = this.model.save(recordID, { // Save then leave edit mode
+ var saveDef = this.model.save(recordID, { // Save then leave edit mode
reload: options.reload,
savePoint: options.savePoint,
});
if (!options.stayInEdit) {
- def = def.then(function (fieldNames) {
- return self._confirmSave(recordID).then(function () {
+ saveDef = saveDef.then(function (fieldNames) {
+ var def = fieldNames.length ? self._confirmSave(recordID) : self._setMode('readonly');
+ return def.then(function () {
return fieldNames;
});
});
}
- return def;
+ return saveDef;
} else {
return $.Deferred().reject(); // Cannot be saved
}
@@ -457,6 +482,31 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
//--------------------------------------------------------------------------
/**
+ * Called when a list element asks to discard the changes made to one of
+ * its rows. It can happen with a x2many (if we are in a form view) or with
+ * a list view.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onDiscardChanges: function (ev) {
+ var self = this;
+ ev.stopPropagation();
+ var recordID = ev.data.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
+ self._confirmChange(self.handle, [ev.data.fieldName], ev)
+ .always(ev.data.onSuccess);
+ })
+ .fail(ev.data.onFailure);
+ },
+ /**
* Forces to save directly the changes if the controller is in readonly,
* because in that case the changes come from widgets that are editable even
* in readonly (e.g. Priority).
@@ -471,6 +521,14 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
FieldManagerMixin._onFieldChanged.apply(this, arguments);
},
/**
+ * @private
+ * @param {OdooEvent} ev
+ * @param {function} ev.data.action the function to execute in the mutex
+ */
+ _onMutexify: function (ev) {
+ this.mutex.exec(ev.data.action);
+ },
+ /**
* When a reload event triggers up, we need to reload the full view.
* For example, after a form view dialog saved some data.
*
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 5e5b36472f5..742e59a61c5 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -27,7 +27,6 @@ odoo.define('web.BasicModel', function (require) {
* fieldsInfo: {Object},
* getContext: {function},
* getDomain: {function},
- * getEvalContext: {function},
* getFieldNames: {function},
* groupedBy: {string[]},
* id: {integer},
@@ -228,7 +227,7 @@ var BasicModel = AbstractModel.extend({
options = options || {};
var element = this.localData[id];
var isNew = this.isNew(id);
- var rollback = options.rollback || isNew;
+ var rollback = 'rollback' in options ? options.rollback : isNew;
this._visitChildren(element, function (elem) {
if (rollback && elem._savePoint) {
if (elem._savePoint instanceof Array) {
@@ -284,8 +283,6 @@ var BasicModel = AbstractModel.extend({
* @param {boolean} [options.env=false] if true, will only return res_id
* (if record) or res_ids (if list)
* @param {boolean} [options.raw=false] if true, will not follow relations
- * @param {boolean} [options.noUnsetNumeric=false] if true, will set numeric
- * values to 0 if not set
* @returns {Object}
*/
get: function (id, options) {
@@ -308,7 +305,6 @@ var BasicModel = AbstractModel.extend({
return env;
}
-
if (element.type === 'record') {
var data = _.extend({}, element.data, element._changes);
@@ -321,13 +317,6 @@ var BasicModel = AbstractModel.extend({
if (!field) {
continue;
}
- if (options.noUnsetNumeric) {
- if (field.type === 'float' ||
- field.type === 'integer' ||
- field.type === 'monetary') {
- data[fieldName] = data[fieldName] || 0;
- }
- }
// get relational datapoint
if (field.type === 'many2one') {
@@ -356,11 +345,11 @@ var BasicModel = AbstractModel.extend({
count: element.count,
data: data,
domain: element.domain.slice(0),
+ evalModifiers: element.evalModifiers,
fields: element.fields,
fieldsInfo: element.fieldsInfo,
getContext: element.getContext,
getDomain: element.getDomain,
- getEvalContext: element.getEvalContext,
getFieldNames: element.getFieldNames,
id: element.id,
limit: element.limit,
@@ -410,7 +399,6 @@ var BasicModel = AbstractModel.extend({
fields: element.fields,
getContext: element.getContext,
getDomain: element.getDomain,
- getEvalContext: element.getEvalContext,
getFieldNames: element.getFieldNames,
groupedBy: element.groupedBy,
id: element.id,
@@ -615,10 +603,12 @@ var BasicModel = AbstractModel.extend({
*
* @param {string} record_id
* @param {Object} changes a map field => new value
+ * @param {string} [viewType] current viewType. If not set, we will assume
+ * main viewType from the record
* @returns {string[]} list of changed fields
*/
- notifyChanges: function (record_id, changes) {
- return this.mutex.exec(this._applyChange.bind(this, record_id, changes));
+ notifyChanges: function (record_id, changes, viewType) {
+ return this.mutex.exec(this._applyChange.bind(this, record_id, changes, viewType));
},
/**
* Reload all data for a given resource
@@ -644,7 +634,7 @@ var BasicModel = AbstractModel.extend({
return this._makeDefaultRecord(element.model, params);
}
if (!options.keepChanges) {
- this.discardChanges(id);
+ this.discardChanges(id, {rollback: false});
}
}
@@ -788,7 +778,7 @@ var BasicModel = AbstractModel.extend({
_.extend(record.data, _changes);
def.resolve(changedFields);
}
- });
+ }).fail(def.reject.bind(def));
} else {
def.resolve(changedFields);
}
@@ -902,9 +892,11 @@ var BasicModel = AbstractModel.extend({
*
* @param {string} recordID
* @param {Object} changes
+ * @param {string} [viewType] current viewType. If not set, we will assume
+ * main viewType from the record
* @returns {Deferred}
*/
- _applyChange: function (recordID, changes) {
+ _applyChange: function (recordID, changes, viewType) {
var self = this;
var record = this.localData[recordID];
var field;
@@ -916,7 +908,7 @@ var BasicModel = AbstractModel.extend({
for (var fieldName in changes) {
field = record.fields[fieldName];
if (field.type === 'one2many' || field.type === 'many2many') {
- defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName]));
+ defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], viewType));
} else if (field.type === 'many2one') {
defs.push(this._applyMany2OneChange(record, fieldName, changes[fieldName]));
} else {
@@ -937,7 +929,7 @@ var BasicModel = AbstractModel.extend({
}
var onchangeDef;
if (onChangeFields.length) {
- onchangeDef = self._performOnChange(record, onChangeFields).then(function (result) {
+ onchangeDef = self._performOnChange(record, onChangeFields, viewType).then(function (result) {
delete record._warning;
return _.keys(changes).concat(Object.keys(result && result.value || {}));
});
@@ -976,6 +968,17 @@ var BasicModel = AbstractModel.extend({
record._changes[fieldName] = false;
return $.when();
}
+
+ // here, we check that the many2one really changed. If the res_id is the
+ // same, we do not need to do any extra work. It can happen when the
+ // user edited a manyone (with the small form view button) with an
+ // onchange. In that case, the onchange is triggered, but the actual
+ // value did not change.
+ var relatedID = (record._changes && record._changes[fieldName]) || record.data[fieldName];
+ var relatedRecord = this.localData[relatedID];
+ if (relatedRecord && (data.id === this.localData[relatedID].res_id)) {
+ return $.when();
+ }
var rel_data = _.pick(data, 'id', 'display_name');
var def;
if (rel_data.display_name === undefined) {
@@ -1093,14 +1096,8 @@ var BasicModel = AbstractModel.extend({
});
defs.push(def);
}
- } else if (field.type === 'date') {
- // process data: convert into a moment instance
- record._changes[name] = fieldUtils.parse.date(val);
- } else if (field.type === 'datetime') {
- // process datetime: convert into a moment instance
- record._changes[name] = fieldUtils.parse.datetime(val);
} else {
- record._changes[name] = val;
+ record._changes[name] = self._parseServerValue(field, val);
}
});
return $.when.apply($, defs);
@@ -1115,13 +1112,23 @@ var BasicModel = AbstractModel.extend({
* @param {string} fieldName
* @param {Object} command A command object. It should have a 'operation'
* key. For example, it looks like {operation: ADD, id: 'partner_1'}
+ * @param {string} [viewType] current viewType. If not set, we will assume
+ * main viewType from the record
* @returns {Deferred}
*/
- _applyX2ManyChange: function (record, fieldName, command) {
+ _applyX2ManyChange: function (record, fieldName, command, viewType) {
+ if (command.operation === 'TRIGGER_ONCHANGE') {
+ // the purpose of this operation is to trigger an onchange RPC, so
+ // there is no need to apply any change on the record (the changes
+ // have probably been already applied and saved, usecase: many2many
+ // edition in a dialog)
+ return $.when();
+ }
+
var self = this;
var list = this.localData[record._changes[fieldName] || record.data[fieldName]];
var field = record.fields[fieldName];
- var fieldInfo = record.fieldsInfo[record.viewType][fieldName];
+ var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName];
var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode];
var rec;
var defs = [];
@@ -1144,6 +1151,7 @@ var BasicModel = AbstractModel.extend({
newRecord.fieldsInfo = list.fieldsInfo;
newRecord.viewType = list.viewType;
list._changes.push(newRecord.id);
+ this._sortList(list);
break;
case 'ADD_M2M':
// force to use link command instead of create command
@@ -1163,6 +1171,7 @@ var BasicModel = AbstractModel.extend({
});
list_records[d.id] = rec;
list._changes.push(rec.id);
+ self._sortList(list);
});
// 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
@@ -1243,22 +1252,65 @@ var BasicModel = AbstractModel.extend({
* @see _performOnChange
*
* @param {Object} record resource object of type 'record'
- * @returns {Object} an onchange spec
+ * @param {string} [viewType] current viewType. If not set, we will assume
+ * main viewType from the record
+ * @returns {Object|false} an onchange spec, or false if no onchange should
+ * be applied
*/
- _buildOnchangeSpecs: function (record) {
- // TODO: replace this function by some generic tree function in utils
+ _buildOnchangeSpecs: function (record, viewType) {
+ var hasOnchange = false;
var specs = {};
- _.each(record.getFieldNames(), function (name) {
+ var fieldsInfo = record.fieldsInfo[viewType || record.viewType];
+
+ _.each(Object.keys(fieldsInfo), function (name) {
var field = record.fields[name];
- var fieldInfo = record.fieldsInfo[record.viewType][name];
+ var fieldInfo = fieldsInfo[name];
specs[name] = (field.onChange) || "";
+ if (field.onChange) {
+ hasOnchange = true;
+ }
_.each(fieldInfo.views, function (view) {
_.each(view.fieldsInfo[view.type], function (field, subname) {
- specs[name + '.' + subname] = (view.fields[subname].onChange) || "";
+ var onChange = view.fields[subname].onChange;
+ specs[name + '.' + subname] = onChange || "";
+ if (onChange) {
+ hasOnchange = true;
+ }
});
});
});
- return specs;
+ return hasOnchange ? specs : false;
+ },
+ /**
+ * Evaluate modifiers
+ *
+ * @private
+ * @param {Object} element a valid element object, which will serve as eval
+ * context.
+ * @param {Object} modifiers
+ * @returns {Object}
+ */
+ _evalModifiers: function (element, modifiers) {
+ var result = {};
+ var self = this;
+ var evalContext;
+ function evalModifier (mod) {
+ if (mod === undefined || mod === false || mod === true) {
+ return !!mod;
+ }
+ evalContext = evalContext || self._getEvalContext(element);
+ return new Domain(mod, evalContext).compute(evalContext);
+ }
+ if ('invisible' in modifiers) {
+ result.invisible = evalModifier(modifiers.invisible);
+ }
+ if ('readonly' in modifiers) {
+ result.readonly = evalModifier(modifiers.readonly);
+ }
+ if ('required' in modifiers) {
+ result.required = evalModifier(modifiers.required);
+ }
+ return result;
},
/**
* Fetch all name_gets for the many2ones in a group
@@ -1899,8 +1951,8 @@ var BasicModel = AbstractModel.extend({
// generate update commands for records that have been
// updated (it may happen with editable lists)
_.each(relData, function (relRecord) {
- if (!_.isEmpty(relRecord._changes)) {
- var changes = self._generateChanges(relRecord);
+ var changes = self._generateChanges(relRecord);
+ if (!_.isEmpty(changes)) {
var command = x2ManyCommands.update(relRecord.res_id, changes);
commands[fieldName].push(command);
}
@@ -1918,8 +1970,8 @@ var BasicModel = AbstractModel.extend({
if (_.contains(keptIds, relIds[i])) {
// this is an id that already existed
relRecord = _.findWhere(relData, {res_id: relIds[i]});
- if (!_.isEmpty(relRecord._changes)) {
- changes = this._generateChanges(relRecord);
+ changes = this._generateChanges(relRecord);
+ if (!_.isEmpty(changes)) {
command = x2ManyCommands.update(relRecord.res_id, changes);
didChange = true;
} else {
@@ -2046,19 +2098,24 @@ var BasicModel = AbstractModel.extend({
* @returns {Object}
*/
_getEvalContext: function (element) {
- var evalContext = this.get(element.id, {raw: true, noUnsetNumeric: true}).data;
- evalContext.active_model = element.model;
- evalContext.id = evalContext.id || false;
- evalContext.active_id = evalContext.id;
- evalContext.active_ids = evalContext.id ? [evalContext.id] : [];
+ var evalContext = element.type === 'record' ? this._getRecordEvalContext(element) : {};
+
if (element.parentID) {
- var parent = this.get(element.parentID, {raw: true});
- if (parent.type === 'list' && this.localData[element.parentID].parentID) {
- parent = this.get(this.localData[element.parentID].parentID, {raw: true});
+ var parent = this.localData[element.parentID];
+ if (parent.type === 'list' && parent.parentID) {
+ parent = this.localData[parent.parentID];
+ }
+ if (parent.type === 'record') {
+ evalContext.parent = this._getRecordEvalContext(parent);
}
- _.extend(evalContext, {parent: parent.data});
}
- return _.extend({}, session.user_context, element.context, evalContext);
+ return _.extend({
+ active_id: evalContext.id || false,
+ active_ids: evalContext.id ? [evalContext.id] : [],
+ active_model: element.model,
+ current_date: moment().format('YYYY-MM-DD'),
+ id: evalContext.id || false,
+ }, session.user_context, element.context, evalContext);
},
/**
* Returns the list of field names of the given element according to its
@@ -2072,6 +2129,67 @@ var BasicModel = AbstractModel.extend({
return Object.keys(fieldsInfo && fieldsInfo[element.viewType] || {});
},
/**
+ * Evaluate the record evaluation context. This method is supposed to be
+ * called by _getEvalContext. It basically only generates a dictionary of
+ * current values for the record, with commands for x2manys fields.
+ *
+ * @param {Object} record an element of type 'record'
+ * @returns Object
+ */
+ _getRecordEvalContext: function (record) {
+ var self = this;
+ var relDataPoint;
+ var context = _.extend({}, record.data, record._changes);
+ for (var fieldName in context) {
+ var field = record.fields[fieldName];
+ if (context[fieldName] === null) {
+ context[fieldName] = false;
+ }
+ if (!field) {
+ continue;
+ }
+ if (field.type === 'float' ||
+ field.type === 'integer' ||
+ field.type === 'monetary') {
+ context[fieldName] = context[fieldName] || 0;
+ continue;
+ }
+ if (field.type === 'date' || field.type === 'datetime') {
+ if (context[fieldName]) {
+ context[fieldName] = JSON.parse(JSON.stringify(context[fieldName]));
+ }
+ continue;
+ }
+ if (field.type === 'many2one') {
+ relDataPoint = this.localData[context[fieldName]];
+ context[fieldName] = relDataPoint ? relDataPoint.res_id : false;
+ 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);
+ }
+ });
+ };
+ context[fieldName] = ids;
+ }
+
+ }
+ return context;
+ },
+ /**
* Returns true iff value is considered to be set for the given field's type.
*
* @private
@@ -2104,11 +2222,17 @@ var BasicModel = AbstractModel.extend({
_isX2ManyValid: function (id) {
var self = this;
var isValid = true;
- var element = this.get(id, {raw: true});
- _.each(element.getFieldNames(), function (fieldName) {
- var field = element.fields[fieldName];
- _.each(element.data, function (rec) {
- if (field.required && !self._isFieldSet(rec.data[fieldName], field.type)) {
+ 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.getFieldNames(), function (fieldName) {
+ var field = element.fields[fieldName];
+ var fieldInfo = element.fieldsInfo[element.viewType][fieldName];
+ var rawModifiers = JSON.parse(fieldInfo.modifiers || "{}");
+ var modifiers = self._evalModifiers(record, rawModifiers);
+ var required = 'required' in modifiers ? modifiers.required : field.required;
+ if (required && !self._isFieldSet(recordData[fieldName], field.type)) {
isValid = false;
}
});
@@ -2196,9 +2320,9 @@ var BasicModel = AbstractModel.extend({
viewType: params.viewType,
};
+ dataPoint.evalModifiers = this._evalModifiers.bind(this, dataPoint);
dataPoint.getContext = this._getContext.bind(this, dataPoint);
dataPoint.getDomain = this._getDomain.bind(this, dataPoint);
- dataPoint.getEvalContext = this._getEvalContext.bind(this, dataPoint);
dataPoint.getFieldNames = this._getFieldNames.bind(this, dataPoint);
this.localData[dataPoint.id] = dataPoint;
@@ -2300,60 +2424,48 @@ var BasicModel = AbstractModel.extend({
record._changes[name] = x2manyList.id;
var many2ones = {};
var r;
+ var isCommandList = result[name].length && _.isArray(result[name][0]);
+ if (!isCommandList) {
+ result[name] = [[6, false, result[name]]];
+ }
_.each(result[name], function (value) {
- if (_.isArray(value)) {
- // value is a command
- if (value[0] === 0) {
- // CREATE
- r = self._makeDataPoint({
- modelName: x2manyList.model,
- context: x2manyList.context,
- fieldsInfo: fieldsInfo,
- fields: fields,
- viewType: viewType,
- });
- x2manyList._changes = x2manyList._changes || [];
- x2manyList._changes.push(r.id);
-
- // this is necessary so the fields are initialized
- _.each(r.getFieldNames(), function (fieldName) {
- r.data[fieldName] = null;
- });
-
- r._changes = _.defaults(value[2], r.data);
- for (var name in r._changes) {
- var isFieldInView = name in r.fields;
- if (isFieldInView && r.fields[name].type === 'many2one') {
- var rec = self._makeDataPoint({
- context: r.context,
- modelName: r.fields[name].relation,
- data: {id: r._changes[name]}
- });
- r._changes[name] = rec.id;
- many2ones[name] = true;
- }
- }
- }
- if (value[0] === 6) {
- // REPLACE_WITH
- x2manyList.res_ids = value[2];
- x2manyList.count = x2manyList.res_ids.length;
- defs.push(self._readUngroupedList(x2manyList));
- }
- } else {
- // value is an id
+ // value is a command
+ if (value[0] === 0) {
+ // CREATE
r = self._makeDataPoint({
modelName: x2manyList.model,
context: x2manyList.context,
fieldsInfo: fieldsInfo,
fields: fields,
- res_id: value,
viewType: viewType,
});
- if (!x2manyList._changes) {
- x2manyList._changes = [];
- }
+ x2manyList._changes = x2manyList._changes || [];
x2manyList._changes.push(r.id);
+
+ // this is necessary so the fields are initialized
+ _.each(r.getFieldNames(), function (fieldName) {
+ r.data[fieldName] = null;
+ });
+
+ r._changes = _.defaults(value[2], r.data);
+ for (var name in r._changes) {
+ var isFieldInView = name in r.fields;
+ if (isFieldInView && r.fields[name].type === 'many2one') {
+ var rec = self._makeDataPoint({
+ context: r.context,
+ modelName: r.fields[name].relation,
+ data: {id: r._changes[name]}
+ });
+ r._changes[name] = rec.id;
+ many2ones[name] = true;
+ }
+ }
+ }
+ if (value[0] === 6) {
+ // REPLACE_WITH
+ x2manyList.res_ids = value[2];
+ x2manyList.count = x2manyList.res_ids.length;
+ defs.push(self._readUngroupedList(x2manyList));
}
});
@@ -2361,35 +2473,17 @@ var BasicModel = AbstractModel.extend({
_.each(_.keys(many2ones), function (name) {
defs.push(self._fetchNameGets(x2manyList, name));
});
- } else if (field.type === 'date') {
- // process date: convert into a moment instance
- record._changes[name] = fieldUtils.parse.date(result[name], field, {isUTC: true});
- } else if (field.type === 'datetime') {
- // process datetime: convert into a moment instance
- record._changes[name] = fieldUtils.parse.datetime(result[name], field, {isUTC: true});
} else {
- record._changes[name] = result[name];
+ record._changes[name] = self._parseServerValue(field, result[name]);
}
});
return $.when.apply($, defs)
.then(function () {
- var shouldApplyOnchange = false;
- var field;
- for (var field_name in record.data) {
- field = record.fields[field_name];
- if (field.onChange) {
- shouldApplyOnchange = true;
+ return self._performOnChange(record, fields_key).then(function () {
+ if (record._warning) {
+ return $.Deferred().reject();
}
- }
- if (shouldApplyOnchange) {
- return self._performOnChange(record, fields_key).then(function () {
- if (record._warning) {
- return $.Deferred().reject();
- }
- });
- } else {
- return $.when();
- }
+ });
})
.then(function () {
return self._fetchRelationalData(record);
@@ -2438,27 +2532,52 @@ var BasicModel = AbstractModel.extend({
// no value for the many2one
record[fieldName] = false;
}
- } else if (field.type === 'date') {
- // process data: convert into a moment instance
- record[fieldName] = fieldUtils.parse.date(val, field, {isUTC: true});
- } else if (field.type === 'datetime') {
- // process datetime: convert into a moment instance
- record[fieldName] = fieldUtils.parse.datetime(val, field, {isUTC: true});
+ } else {
+ record[fieldName] = self._parseServerValue(field, val);
}
});
},
/**
+ * Processes date(time) and selection field values sent by the server.
+ * Converts data(time) values to moment instances.
+ * Converts false values of selection fields to 0 if 0 is a valid key,
+ * because the server doesn't make a distinction between false and 0, and
+ * always sends false when value is 0.
+ *
+ * @param {Object} field the field description
+ * @param {*} value
+ * @returns {*} the processed value
+ */
+ _parseServerValue: function (field, value) {
+ if (field.type === 'date' || field.type === 'datetime') {
+ // process date(time): convert into a moment instance
+ value = fieldUtils.parse[field.type](value, field, {isUTC: true});
+ } else if (field.type === 'selection' && value === false) {
+ // process selection: convert false to 0, if 0 is a valid key
+ var hasKey0 = _.find(field.selection, function (option) {
+ return option[0] === 0;
+ });
+ value = hasKey0 ? 0 : value;
+ }
+ return value;
+ },
+ /**
* This method is quite important: it is supposed to perform the /onchange
* rpc and apply the result.
*
* @param {Object} record
* @param {string[]} fields changed fields
+ * @param {string} [viewType] current viewType. If not set, we will assume
+ * main viewType from the record
* @returns {Deferred} The returned deferred can fail, in which case the
* fail value will be the warning message received from the server
*/
- _performOnChange: function (record, fields) {
+ _performOnChange: function (record, fields, viewType) {
var self = this;
- var onchange_spec = this._buildOnchangeSpecs(record);
+ var onchangeSpec = this._buildOnchangeSpecs(record, viewType);
+ if (!onchangeSpec) {
+ return $.when();
+ }
var idList = record.data.id ? [record.data.id] : [];
var options = {
full: true,
@@ -2474,7 +2593,7 @@ var BasicModel = AbstractModel.extend({
return self._rpc({
model: record.model,
method: 'onchange',
- args: [idList, currentData, fields, onchange_spec, context],
+ args: [idList, currentData, fields, onchangeSpec, context],
})
.then(function (result) {
if (!record._changes) {
@@ -2580,7 +2699,7 @@ var BasicModel = AbstractModel.extend({
var choice = _.find(list.fields[rawGroupBy].selection, function (c) {
return c[0] === value;
});
- value = choice[1];
+ value = choice ? choice[1] : false;
}
var newGroup = self._makeDataPoint({
modelName: list.model,
@@ -2595,6 +2714,7 @@ var BasicModel = AbstractModel.extend({
orderedBy: list.orderedBy,
limit: list.limit,
openGroupByDefault: list.openGroupByDefault,
+ parentID: list.id,
type: 'list',
viewType: list.viewType,
});
@@ -2787,9 +2907,12 @@ var BasicModel = AbstractModel.extend({
if (list.orderedBy.length) {
// sort records according to ordered_by[0]
var order = list.orderedBy[0];
- list.data.sort(function (r1, r2) {
- var data1 = self.localData[r1].data;
- var data2 = self.localData[r2].data;
+ 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;
if (data1[order.name] < data2[order.name]) {
return order.asc ? -1 : 1;
}
diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js
index 907ea7ddc07..8bbf9ae45a4 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -91,7 +91,7 @@ var BasicRenderer = AbstractRenderer.extend({
var record = state.id === id ? state : _.findWhere(state.data, {id: id});
if (!record) {
- return this._render();
+ return this._render().then(_.constant([]));
}
var defs = [];
@@ -269,22 +269,6 @@ var BasicRenderer = AbstractRenderer.extend({
return widget.isValid() && (widget.isSet() || !modifiers.required);
},
/**
- * Updates the modifiers evaluation associated to a given modifiers data and
- * a given record. This only updates the modifiers values. To see associated
- * DOM updates: @see _updateAllModifiers @see _applyModifiers.
- *
- * @private
- * @param {Object} modifiersData
- * @param {Object} record
- */
- _computeModifiers: function (modifiersData, record) {
- var evalContext = record.evalContext;
- modifiersData.evaluatedModifiers[record.id]
- = _.mapObject(modifiersData.modifiers, function (modifier) {
- return new Domain(modifier, evalContext).compute(evalContext);
- });
- },
- /**
* Destroys a given widget associated to the given record and removes it
* from internal referencing.
*
@@ -342,7 +326,6 @@ var BasicRenderer = AbstractRenderer.extend({
* record. This allows nodes that will produce an AbstractField instance
* to have their modifiers registered before this field creation as we
* need the readonly modifier to be able to instantiate the AbstractField.
- * (@see _computeModifiers).
*
* - On additional registrations, if the node was already registered but the
* record is different, we evaluate the modifiers for this record and
@@ -399,7 +382,7 @@ var BasicRenderer = AbstractRenderer.extend({
// Evaluate if necessary
if (!modifiersData.evaluatedModifiers[record.id]) {
- this._computeModifiers(modifiersData, record);
+ modifiersData.evaluatedModifiers[record.id] = record.evalModifiers(modifiersData.modifiers);
}
// Element might not be given yet (a second call to the function can
@@ -569,7 +552,6 @@ var BasicRenderer = AbstractRenderer.extend({
* 2) Updates the rendering of the view elements associated to the given
* record to match the new modifiers.
*
- * @see _computeModifiers
* @see _applyModifiers
*
* @private
@@ -582,7 +564,7 @@ var BasicRenderer = AbstractRenderer.extend({
var defs = [];
this.defs = defs; // Potentially filled by widget rerendering
_.each(this.allModifiersData, function (modifiersData) {
- self._computeModifiers(modifiersData, record);
+ modifiersData.evaluatedModifiers[record.id] = record.evalModifiers(modifiersData.modifiers);
self._applyModifiers(modifiersData, record);
});
delete this.defs;
diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js
index e38b7b4584e..736416dea27 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -67,21 +67,36 @@ var BasicView = AbstractView.extend({
var viewType = this.viewType;
var viewFields = Object.keys(record.fieldsInfo[viewType]);
var fieldNames = _.difference(viewFields, Object.keys(record.data));
+ var fieldsInfo = record.fieldsInfo[viewType];
+
// Suppose that in a form view, there is an x2many list view with
- // an x2many field F of the related record, and that F is also
- // displayed in the x2many form view (e.g. as a list or a kanban).
- // In this case, F is represented in record.data
- // (as it is known by the x2many list view), but its related fields
- // (those displayed in the list or kanban) still need to be fetched.
+ // a field F, and that F is also displayed in the x2many form view.
+ // In this case, F is represented in record.data (as it is known by
+ // the x2many list view), but the loaded information may not suffice
+ // in the form view (e.g. if field is a many2many list in the form
+ // view, or if it is displayed by a widget requiring specialData).
// So when this happens, F is added to the list of fieldNames to fetch.
_.each(viewFields, function (name) {
if (!_.contains(fieldNames, name)) {
var fieldType = record.fields[name].type;
+ var fieldInfo = fieldsInfo[name];
+
+ // SpecialData case: field requires specialData that haven't
+ // been fetched yet.
+ if (fieldInfo.Widget) {
+ var requiresSpecialData = fieldInfo.Widget.prototype.specialData;
+ if (requiresSpecialData && !(name in record.specialData)) {
+ fieldNames.push(name);
+ return;
+ }
+ }
+
+ // X2Many case: field is an x2many displayed as a list or
+ // kanban view, but the related fields haven't been loaded yet.
if ((fieldType === 'one2many' || fieldType === 'many2many')) {
if (!('fieldsInfo' in record.data[name])) {
fieldNames.push(name);
} else {
- var fieldInfo = record.fieldsInfo[viewType][name];
var fieldViews = fieldInfo.views || fieldInfo.fieldsInfo || {};
var fieldViewTypes = Object.keys(fieldViews);
var recordViewTypes = Object.keys(record.data[name].fieldsInfo);