diff options
Diffstat (limited to 'addons/web/static/src/js/views/basic')
| -rw-r--r-- | addons/web/static/src/js/views/basic/basic_model.js | 447 |
1 files changed, 293 insertions, 154 deletions
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..7e4c5927503 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; }); }, @@ -331,11 +336,8 @@ var BasicModel = AbstractModel.extend({ if (field.type === 'one2many' || field.type === 'many2many') { 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; + relDataPoint = this._applyX2ManyOperations(relDataPoint); + data[fieldName] = relDataPoint.res_ids; } else { data[fieldName] = this.get(data[fieldName]) || []; } @@ -376,25 +378,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 +401,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 +409,7 @@ var BasicModel = AbstractModel.extend({ if (element.fieldsInfo) { list.fieldsInfo = element.fieldsInfo; } + this._sortList(list); return list; }, /** @@ -538,6 +531,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 +547,7 @@ var BasicModel = AbstractModel.extend({ var dpParams = { fieldsInfo: relatedFieldsInfo, modelName: field.relation, + parentID: record.id, static: true, type: 'list', viewType: 'default', @@ -570,9 +565,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 +686,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 +815,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 +837,6 @@ var BasicModel = AbstractModel.extend({ }); list.orderedBy = [{name: fieldName, asc: true}].concat(orderedBy); } - this._sortList(list); return this; }, /** @@ -894,6 +893,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. @@ -1014,12 +1043,13 @@ var BasicModel = AbstractModel.extend({ fields: {}, fieldsInfo: {}, modelName: record.fields[fieldName].relation, + 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 +1083,7 @@ var BasicModel = AbstractModel.extend({ context: record.context, data: data, modelName: field.relation, + parentID: record.id, }); id = rec.id; } @@ -1070,12 +1101,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 +1125,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 +1179,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 +1199,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 +1220,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,14 +1230,14 @@ 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); }); @@ -1209,29 +1245,44 @@ var BasicModel = AbstractModel.extend({ } 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 +1294,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 +1488,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]; @@ -1420,7 +1541,7 @@ 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 () { @@ -1783,12 +1904,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 +1924,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 +1956,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; }); }); }); @@ -1961,6 +2078,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 +2089,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 +2106,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 +2138,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)); } @@ -2190,6 +2320,13 @@ var BasicModel = AbstractModel.extend({ 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 +2347,9 @@ 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); - } - }); - }; + relDataPoint = this._applyX2ManyOperations(this.localData[context[fieldName]]); + var ids = relDataPoint.res_ids.slice(0); + ids.toJSON = _generateX2ManyCommands.bind(null, fieldName); context[fieldName] = ids; } @@ -2285,16 +2408,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 +2603,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 +2628,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 +2644,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 +2662,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; @@ -2586,14 +2716,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 +2739,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); } }); }, @@ -2843,10 +2975,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); @@ -2885,12 +3021,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; }); }, @@ -2947,7 +3088,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 +3119,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 +3179,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); }); |
