-
+
% set top_goals = [line['goals'][1], line['goals'][0], line['goals'][2]]
% for goal in top_goals:
@@ -144,9 +144,10 @@
${extra_div | safe}
- 
+ % set path = object.env['res.users'].browse(goal['user_id']).partner_id.image_small
+
-
+
${goal['name']}
@@ -177,8 +178,8 @@
- | Rank |
- Name |
+ Rank |
+ Name |
Performance
% if line['suffix']:
(${line['suffix']})
@@ -199,12 +200,12 @@
% set tdColor = 'gray'
% set mutedColor = '#AAAAAA'
% set tdPercentageColor = '#9A6C8E'
- | ${goal['rank']+1}
+ | ${goal['rank']+1}
|
- ${goal['name']} |
- ${"%.2f" % goal['current']} on ${"%.2f" % line['target']}
+ | ${goal['name']} |
+ ${"%.2f" % goal['current']} on ${"%.2f" % line['target']}
|
- ${goal['completeness'] | int}% |
+ ${goal['completeness'] | int}% |
|
diff --git a/addons/hr_attendance/views/attendance_config_settings_views.xml b/addons/hr_attendance/views/attendance_config_settings_views.xml
index b8c11252fbf..cf72f9f2220 100644
--- a/addons/hr_attendance/views/attendance_config_settings_views.xml
+++ b/addons/hr_attendance/views/attendance_config_settings_views.xml
@@ -7,7 +7,7 @@
@@ -102,7 +98,7 @@
-
+
diff --git a/addons/stock/views/stock_config_settings_views.xml b/addons/stock/views/stock_config_settings_views.xml
index c86a9d08394..3719c4ab3f3 100644
--- a/addons/stock/views/stock_config_settings_views.xml
+++ b/addons/stock/views/stock_config_settings_views.xml
@@ -8,7 +8,7 @@
@@ -68,7 +68,6 @@
-
@@ -91,6 +90,7 @@
stock.quant
form
tree,form,pivot
+ {'search_default_internal_loc': 1, 'search_default_transit_loc': 1}
This analysis gives you a fast overview on the current stock level of your products and their current inventory value.
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index e2f2d7814a4..4e37cb482fd 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -685,7 +685,7 @@ class Database(http.Controller):
request.session.authenticate(name, post['login'], password)
return http.local_redirect('/web/')
except Exception as e:
- error = "Database creation error: %s" % str(e) or repr(e)
+ error = "Database creation error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/duplicate', type='http', auth="none", methods=['POST'], csrf=False)
@@ -696,7 +696,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'duplicate_database', [master_pwd, name, new_name])
return http.local_redirect('/web/database/manager')
except Exception as e:
- error = "Database duplication error: %s" % str(e) or repr(e)
+ error = "Database duplication error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/drop', type='http', auth="none", methods=['POST'], csrf=False)
@@ -706,7 +706,7 @@ class Database(http.Controller):
request._cr = None # dropping a database leads to an unusable cursor
return http.local_redirect('/web/database/manager')
except Exception as e:
- error = "Database deletion error: %s" % str(e) or repr(e)
+ error = "Database deletion error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/backup', type='http', auth="none", methods=['POST'], csrf=False)
@@ -724,7 +724,7 @@ class Database(http.Controller):
return response
except Exception as e:
_logger.exception('Database.backup')
- error = "Database backup error: %s" % str(e) or repr(e)
+ error = "Database backup error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False)
@@ -734,7 +734,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'restore', [master_pwd, name, data, str2bool(copy)])
return http.local_redirect('/web/database/manager')
except Exception as e:
- error = "Database restore error: %s" % str(e) or repr(e)
+ error = "Database restore error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/change_password', type='http', auth="none", methods=['POST'], csrf=False)
@@ -743,7 +743,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'change_admin_password', [master_pwd, master_pwd_new])
return http.local_redirect('/web/database/manager')
except Exception as e:
- error = "Master password update error: %s" % str(e) or repr(e)
+ error = "Master password update error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/list', type='json', auth='none')
diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js
index a4a1d65f7b6..5962bb28acb 100644
--- a/addons/web/static/src/js/chrome/view_manager.js
+++ b/addons/web/static/src/js/chrome/view_manager.js
@@ -20,7 +20,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
custom_events: {
execute_action: function(event) {
var data = event.data;
- this.do_execute_action(data.action_data, data.model, data.res_ids, data.on_closed)
+ this.do_execute_action(data.action_data, data.env, data.on_closed)
.then(data.on_success, data.on_fail);
},
search: function(event) {
@@ -554,16 +554,19 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
* @param {string} [action_data.effect] if given, a visual effect (a
* rainbowman by default) will be displayed when the action is complete,
* with the string (evaluated) given as options.
- * @param {DataSet} dataset a dataset object used to communicate with the server
- * @param {integer[]} [res_ids] the res_ids of the objects on which the action is to be applied (fallback on env.ids if not set)
+ * @param {Object} env
+ * @param {string} env.model the model of the record(s) triggering the action
+ * @param {integer[]} [env.resIDs] the current ids in the environment where the action is triggered
+ * @param {integer} [env.currentID] the id of the record triggering the action
+ * @param {Object} [env.context] a context to pass to the action
* @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
*/
- do_execute_action: function (action_data, model, res_ids, on_closed) {
+ do_execute_action: function (action_data, env, on_closed) {
var self = this;
var result_handler = on_closed || function () {};
- var context = new Context(this.env.context, action_data.context || {});
+ var context = new Context(env.context, action_data.context || {});
// OR NULL hereunder: pyeval waits specifically for a null value, different from undefined
- var record_id = res_ids && res_ids[0] || null;
+ var recordID = env.currentID || null;
// response handler
var handler = function (action) {
@@ -585,11 +588,11 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
}))
);
ncontext.add(action_data.context || {});
- ncontext.add({active_model: self.env.modelName});
- if (res_ids) {
+ ncontext.add({active_model: env.model});
+ if (recordID) {
ncontext.add({
- active_id: record_id,
- active_ids: res_ids,
+ active_id: recordID,
+ active_ids: [recordID],
});
}
ncontext.add(action.context || {});
@@ -611,7 +614,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
if (action_data.special) {
return handler({"type":"ir.actions.act_window_close"});
} else if (action_data.type === "object") {
- var args = res_ids ? [res_ids] : [this.env.ids];
+ var args = recordID ? [[recordID]] : [env.resIDs];
if (action_data.args) {
try {
// Warning: quotes and double quotes problem due to json and xml clash
@@ -623,13 +626,13 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
}
}
args.push(context);
- var dataset = new data.DataSet(this, model, this.env.context);
+ var dataset = new data.DataSet(this, env.model, env.context);
return dataset.call_button(action_data.name, args).then(handler);
} else if (action_data.type === "action") {
return data_manager.load_action(action_data.name, _.extend(pyeval.eval('context', context), {
- active_model: this.env.modelName,
- active_ids: this.env.ids,
- active_id: record_id,
+ active_model: env.model,
+ active_ids: env.resIDs,
+ active_id: recordID,
})).then(handler);
}
},
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 64049d8b1ec..0dce6ec2bb4 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -219,10 +219,16 @@ var FieldMany2One = AbstractField.extend({
/**
* @private
* @param {string} name
+ * @returns {Deferred} resolved after the name_create or when the slowcreate
+ * modal is closed.
*/
_quickCreate: function (name) {
var self = this;
- var slowCreate = this._searchCreatePopup.bind(this, "form", false, this._createContext(name));
+ var def = $.Deferred();
+ var slowCreate = function () {
+ var dialog = self._searchCreatePopup.bind(self, "form", false, self._createContext(name));
+ dialog.on('closed', self, def.resolve.bind(def));
+ };
if (this.nodeOptions.quick_create) {
this.trigger_up('mutexify', {
action: function () {
@@ -235,12 +241,16 @@ var FieldMany2One = AbstractField.extend({
if (self.mode === "edit") {
self.reinitialize({id: result[0], display_name: result[1]});
}
- }, slowCreate);
+ def.resolve();
+ }).fail(function () {
+ slowCreate();
+ });
},
});
} else {
slowCreate();
}
+ return def;
},
/**
* @private
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index 8de118b29b7..644ba8ac985 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -29,6 +29,7 @@ var ajax = require('web.ajax');
var AbstractModel = require('web.AbstractModel');
var AbstractRenderer = require('web.AbstractRenderer');
var AbstractController = require('web.AbstractController');
+var Context = require('web.Context');
var AbstractView = Class.extend({
// name displayed in view switchers
@@ -152,9 +153,9 @@ var AbstractView = Class.extend({
});
},
/**
- * Returns the view model or create an instance of it if there is noney
+ * Returns the view model or create an instance of it if none
*
- * @param {Widget} parent the parent of the model
+ * @param {Widget} parent the parent of the model, if it has to be created
* @return {Object} instance of the view model
*/
getModel: function (parent) {
@@ -167,7 +168,7 @@ var AbstractView = Class.extend({
/**
* Returns the a new view renderer instance
*
- * @param {Widget} parent the parent of the model
+ * @param {Widget} parent the parent of the model, if it has to be created
* @param {Object} state the information related to the rendered view
* @return {Object} instance of the view renderer
*/
@@ -239,7 +240,7 @@ var AbstractView = Class.extend({
* Loads the subviews for x2many fields when they are not inline
*
* @private
- * @param {Widget} parent the parent of the model
+ * @param {Widget} parent the parent of the model, if it has to be created
* @returns {Deferred}
*/
_loadSubviews: function (parent) {
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 3a47f65ab03..6ba9dbea477 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -232,13 +232,17 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
return self.isDestroyed() ? $.when() : self.reload();
};
record = record || this.model.get(this.handle);
- var recordID = record.data.id;
+
this.trigger_up('execute_action', {
action_data: _.extend({}, attrs, {
- context: record.getContext({additionalContext: attrs.context || {}}),
+ context: record.getContext({additionalContext: attrs.context || {}}),
}),
- model: record.model,
- res_ids: [recordID],
+ env: {
+ context: record.getContext(),
+ currentID: record.data.id,
+ model: record.model,
+ resIDs: record.res_ids,
+ },
on_closed: function (reason) {
if (!_.isObject(reason)) {
reload(reason);
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 04fe384afdc..8755805f993 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -649,7 +649,7 @@ var BasicModel = AbstractModel.extend({
var element = this.localData[id];
if (element.type === 'record') {
- if ('currentId' in options && !options.currentId) {
+ if (!options.currentId && (('currentId' in options) || this.isNew(id))) {
var params = {
context: element.context,
fieldsInfo: element.fieldsInfo,
@@ -2310,12 +2310,20 @@ var BasicModel = AbstractModel.extend({
});
list = this._applyX2ManyOperations(list);
if (type === 'many2many' || list._forceM2MLink) {
+ var relRecordCreated = _.filter(relRecordAdded, function (rec) {
+ return typeof rec.res_id === 'string';
+ });
+ var realIDs = _.difference(list.res_ids, _.pluck(relRecordCreated, 'res_id'));
// 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(list.res_ids));
+ commands[fieldName].push(x2ManyCommands.replace_with(realIDs));
+ _.each(relRecordCreated, function (relRecord) {
+ var changes = self._generateChanges(relRecord, options);
+ commands[fieldName].push(x2ManyCommands.create(changes));
+ });
// generate update commands for records that have been
// updated (it may happen with editable lists)
_.each(relRecordUpdated, function (relRecord) {
@@ -2435,6 +2443,12 @@ var BasicModel = AbstractModel.extend({
*/
_getDomain: function (element, options) {
if (options && options.fieldName) {
+ if (element._domains[options.fieldName]) {
+ return Domain.prototype.stringToArray(
+ element._domains[options.fieldName],
+ this._getEvalContext(element, true)
+ );
+ }
var viewType = options.viewType || element.viewType;
var fieldInfo = element.fieldsInfo[viewType][options.fieldName];
if (fieldInfo && fieldInfo.domain) {
@@ -2697,6 +2711,7 @@ var BasicModel = AbstractModel.extend({
var dataPoint = {
_cache: type === 'list' ? {} : undefined,
_changes: null,
+ _domains: {},
aggregateValues: params.aggregateValues || {},
context: params.context || {},
count: params.count || res_ids.length,
@@ -2870,6 +2885,9 @@ var BasicModel = AbstractModel.extend({
r._changes = _.defaults(value[2], r.data);
for (var name in r._changes) {
+ if (r._changes[name] === null) {
+ continue;
+ }
var isFieldInView = name in r.fields;
if (isFieldInView && r.fields[name].type === 'many2one') {
var rec = self._makeDataPoint({
@@ -3021,12 +3039,7 @@ 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];
- }
- }
+ record._domains = _.extend(record._domains, result.domain);
}
return self._applyOnChange(result.value, record).then(function () {
return result;
@@ -3083,7 +3096,9 @@ var BasicModel = AbstractModel.extend({
*/
_readGroup: function (list) {
var self = this;
- var fields = _.uniq(list.getFieldNames().concat(list.groupedBy));
+ var groupByField = list.groupedBy[0];
+ var rawGroupBy = groupByField.split(':')[0];
+ var fields = _.uniq(list.getFieldNames().concat(rawGroupBy));
return this._rpc({
model: list.model,
method: 'read_group',
@@ -3094,8 +3109,6 @@ var BasicModel = AbstractModel.extend({
lazy: true,
})
.then(function (groups) {
- var groupByField = list.groupedBy[0];
- var rawGroupBy = groupByField.split(':')[0];
var previousGroups = _.map(list.data, function (groupID) {
return self.localData[groupID];
});
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 4f5719063ff..5fb79cfa3a0 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -322,10 +322,11 @@ return AbstractRenderer.extend({
element.addClass($render.attr('class'));
var display_hour = '';
if (!event.allDay) {
- display_hour = (event.start.format('HH:mm') === '00:00' ? event.r_start.format('HH:mm') : event.start.format('HH:mm')) + ' - ' +
- (event.end && event.end.format('HH:mm') !== '00:00' ? event.end.format('HH:mm') : event.r_end.format('HH:mm'));
+ var start = event.r_start || event.start;
+ var end = event.r_end || event.end;
+ display_hour = start.format('HH:mm') + ' - ' + end.format('HH:mm');
if (display_hour === '00:00 - 00:00') {
- display_hour = _t('All the day');
+ display_hour = _t('All day');
}
}
element.find('.fc-content .fc-time').text(display_hour);
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 cdfe2cb8d5d..0718b4e202e 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -217,7 +217,7 @@ var FormController = BasicController.extend({
*
* @private
* @override method from field manager mixin
- * @param {string} id
+ * @param {string} id - id of the previously changed record
* @returns {Deferred}
*/
_confirmSave: function (id) {
@@ -228,15 +228,21 @@ var FormController = BasicController.extend({
return this._setMode('readonly');
}
} else {
- // a subrecord changed, so update the corresponding relational field
+ // A subrecord has changed, so update the corresponding relational field
// i.e. the one whose value is a record with the given id or a list
// having a record with the given id in its data
var record = this.model.get(this.handle);
- var fieldsChanged = _.findKey(record.data, function (d) {
- return _.isObject(d) &&
- (d.id === id || _.findWhere(d.data, {id: id}));
- });
- return this.renderer.confirmChange(record, record.id, [fieldsChanged]);
+
+ // Callback function which returns true
+ // if a value recursively contains a record with the given id.
+ // This will be used to determine the list of fields to reload.
+ var containsChangedRecord = function (value) {
+ return _.isObject(value) &&
+ (value.id === id || _.find(value.data, containsChangedRecord));
+ };
+
+ var changedFields = _.findKey(record.data, containsChangedRecord);
+ return this.renderer.confirmChange(record, record.id, [changedFields]);
}
},
/**
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index 2cf6a94285e..6870dbbbe24 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -237,8 +237,12 @@ var KanbanController = BasicController.extend({
}
this.trigger_up('execute_action', {
action_data: attrs,
- model: record.model,
- res_ids: [record.res_id],
+ env: {
+ context: record.getContext(),
+ currentID: record.res_id,
+ model: record.model,
+ resIDs: record.res_ids,
+ },
on_closed: function () {
self.model.reload(record.id).then(function (db_id) {
var data = self.model.get(db_id);
diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js
index 3977ac0df87..71402fc92ec 100644
--- a/addons/web/static/src/js/views/kanban/kanban_model.js
+++ b/addons/web/static/src/js/views/kanban/kanban_model.js
@@ -240,7 +240,7 @@ var KanbanModel = BasicModel.extend({
*/
_readTooltipFields: function (list) {
var self = this;
- var groupedByField = list.fields[list.groupedBy[0]];
+ var groupedByField = list.fields[list.groupedBy[0].split(':')[0]];
if (groupedByField.type !== 'many2one') {
return $.when();
}
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index 4851870c8f8..3ef48792f06 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -282,15 +282,19 @@ var ListRenderer = BasicRenderer.extend({
this._registerModifiers(node, record, $button);
- // TODO this should be moved to a handler
- var self = this;
- $button.on("click", function (e) {
- e.stopPropagation();
- self.trigger_up('button_clicked', {
- attrs: node.attrs,
- record: record,
+ if (record.res_id) {
+ // TODO this should be moved to a handler
+ var self = this;
+ $button.on("click", function (e) {
+ e.stopPropagation();
+ self.trigger_up('button_clicked', {
+ attrs: node.attrs,
+ record: record,
+ });
});
- });
+ } else {
+ $button.prop('disabled', true);
+ }
return $button;
},
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 0ad9aa2a416..29f8666455a 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -922,6 +922,140 @@ QUnit.module('relational_fields', {
});
});
+ QUnit.test('list in form: create with one2many with many2one', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ return $.when({p: [[0, 0, {display_name: 'new record'}]]});
+ } else if (args.method === 'name_get') {
+ // This should not be called at all and thus is not accounted for
+ // in the assert.expect. If this is called, you broke this test.
+ assert.notOk(_.str.startsWith(args.args[0][0], 'virtual_'),
+ "should not call name_get for the m2o inside o2m which has no value");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual($('td.o_data_cell:first').text(), 'new record',
+ "should have created the new record in the o2m with the correct name");
+
+ form.destroy();
+ });
+
+ QUnit.test('list in form: default_get with x2many create', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.onchanges.timmy = function (obj) {
+ assert.deepEqual(
+ obj.timmy,
+ [
+ [6, false, []],
+ [0, false, {display_name: 'brandon is the new timmy', name: 'brandon'}]
+ ],
+ "should have properly created the x2many command list");
+ obj.int_field = obj.timmy.length;
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ return $.when({timmy: [[0, 0, {display_name: 'brandon is the new timmy', name: 'brandon'}]]});
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual($('td.o_data_cell:first').text(), 'brandon is the new timmy',
+ "should have created the new record in the m2m with the correct name");
+ assert.strictEqual($('input.o_field_integer').val(), '2',
+ "should have called and executed the onchange properly");
+
+ form.destroy();
+ });
+
+ QUnit.test('list in form: call button in sub view', function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records[0].p = [2];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/product/get_formview_id') {
+ return $.when(false);
+ }
+ return this._super.apply(this, arguments);
+ },
+ intercepts: {
+ execute_action: function (event) {
+ assert.strictEqual(event.data.env.model, 'product',
+ 'should call with correct model in env');
+ assert.strictEqual(event.data.env.currentID, 37,
+ 'should call with correct currentID in env');
+ assert.deepEqual(event.data.env.resIDs, [37],
+ 'should call with correct resIDs in env');
+ },
+ },
+ archs: {
+ 'product,false,form': '',
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('td.o_data_cell:first').click(); // edit first one2many line
+ form.$('.o_external_button').click(); // open product sub view in modal
+ $('button:contains("Just do it !")').click(); // click on action button
+ $('button:contains("Just don\'t do it !")').click(); // click on object button
+
+ form.destroy();
+ });
+
QUnit.test('autocompletion in a many2one, in form view with a domain', function (assert) {
assert.expect(1);
@@ -1418,6 +1552,115 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('many2one in one2many: domain updated by an onchange', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.onchanges = {
+ trululu: function () {},
+ };
+
+ var domain = [];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return $.when({
+ domain: {
+ trululu: domain,
+ },
+ });
+ }
+ if (args.method === 'name_search') {
+ assert.deepEqual(args.kwargs.args, domain,
+ "sent domain should be correct");
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // add a first row with a specific domain for the m2o
+ domain = [['id', 'in', [10]]]; // domain for subrecord 1
+ form.$('.o_field_x2many_list_row_add a').click(); // triggers the onchange
+ form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search
+
+ // add a second row with another domain for the m2o
+ domain = [['id', 'in', [5]]]; // domain for subrecord 2
+ form.$('.o_field_x2many_list_row_add a').click(); // triggers the onchange
+ form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search
+
+ // check again the first row to ensure that the domain hasn't change
+ domain = [['id', 'in', [10]]]; // domain for subrecord 1 should have been kept
+ form.$('.o_data_row:first .o_data_cell').click();
+ form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search
+
+ form.destroy();
+ });
+
+ QUnit.test('updating a many2one from a many2many', function (assert) {
+ assert.expect(4);
+
+ this.data.turtle.records[1].turtle_trululu = 1;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ archs: {
+ 'partner,false,form': '',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'get_formview_id') {
+ assert.deepEqual(args.args[0], [1], "should call get_formview_id with correct id");
+ return $.when(false);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ // Opening the modal
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_data_row td:contains(first record)').click();
+ form.$('.o_external_button').click();
+ assert.strictEqual($('.modal').length, 1,
+ "should have one modal in body");
+
+ // Changing the 'trululu' value
+ $('.modal input[name="display_name"]').val('test').trigger('input');
+ $('.modal button.btn-primary').click();
+
+ // Test whether the value has changed
+ assert.strictEqual($('.modal').length, 0,
+ "the modal should be closed");
+ assert.equal(form.$('.o_data_cell:contains(test)').text(), 'test',
+ "the partner name should have been updated to 'test'");
+
+ form.destroy();
+ });
+
QUnit.module('FieldOne2Many');
QUnit.test('one2many basic properties', function (assert) {
@@ -3847,9 +4090,9 @@ QUnit.module('relational_fields', {
res_id: 1,
intercepts: {
execute_action: function (event) {
- assert.deepEqual(event.data.res_ids, [2],
+ assert.deepEqual(event.data.env.currentID, 2,
'should call with correct id');
- assert.strictEqual(event.data.model, 'partner',
+ assert.strictEqual(event.data.env.model, 'partner',
'should call with correct model');
assert.strictEqual(event.data.action_data.name, 'method_name',
"should call correct method");
@@ -3891,9 +4134,9 @@ QUnit.module('relational_fields', {
res_id: 1,
intercepts: {
execute_action: function (event) {
- assert.deepEqual(event.data.res_ids, [2],
+ assert.deepEqual(event.data.env.currentID, 2,
'should call with correct id');
- assert.strictEqual(event.data.model, 'partner',
+ assert.strictEqual(event.data.env.model, 'partner',
'should call with correct model');
assert.strictEqual(event.data.action_data.name, 'method_name',
"should call correct method");
diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js
index 53577d90e40..8c23fc4f97c 100644
--- a/addons/web/static/tests/views/basic_model_tests.js
+++ b/addons/web/static/tests/views/basic_model_tests.js
@@ -956,6 +956,30 @@ QUnit.module('Views', {
model.destroy();
});
+ QUnit.test('group on date field with magic grouping method', function (assert) {
+ assert.expect(1);
+
+ this.params.fieldNames = ['foo'];
+ this.params.groupedBy = ['date:month'];
+ this.params.res_id = undefined;
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'read_group') {
+ assert.deepEqual(args.kwargs.fields, ['foo', 'date'],
+ "should have correctly trimmed the magic grouping info from the field name");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ model.load(this.params);
+ model.destroy();
+ });
+
+
QUnit.test('read group when grouped by a selection field', function (assert) {
assert.expect(5);
@@ -1949,4 +1973,39 @@ QUnit.module('Views', {
model.destroy();
});
+ QUnit.test('reload a new record', function (assert) {
+ assert.expect(6);
+
+ this.params.context = {};
+ this.params.fieldNames = ['product_id', 'category', 'product_ids'];
+ this.params.res_id = undefined;
+ this.params.type = 'record';
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super(route, args);
+ },
+ });
+
+ model.load(this.params).then(function (recordID) {
+ model.reload(recordID).then(function (recordID) {
+ assert.verifySteps(['default_get', 'default_get'],
+ "two default_get RPCs should have been done");
+ var record = model.get(recordID);
+ assert.strictEqual(record.data.product_id, false,
+ "m2o default value should be false");
+ assert.deepEqual(record.data.product_ids.data, [],
+ "o2m default should be []");
+ assert.deepEqual(record.data.category.data, [],
+ "m2m default should be []");
+ });
+ });
+
+ model.destroy();
+ });
+
+
});});
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index 31b1babc9b3..6aa01daac5a 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -789,7 +789,7 @@ QUnit.module('Views', {
assert.strictEqual($event1Avatars.find('span').length, 0,
"should not have a span for more attendees since there is only one");
- var $event2Avatars = calendar.$('.fc-event:contains(All the day) .o_calendar_avatars');
+ var $event2Avatars = calendar.$('.fc-event:contains(All day) .o_calendar_avatars');
assert.strictEqual($event2Avatars.find('img').length, 3, "should have 3 avatars");
assert.strictEqual($event2Avatars.find('span').text(), '+2',
"should indicate there are 2 more attendees that we don't show");
@@ -1034,6 +1034,46 @@ QUnit.module('Views', {
"should display 5 events");
calendar.destroy();
});
+
+ QUnit.test('events starting at midnight', function (assert) {
+ assert.expect(2);
+
+ var calendar = createView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch: '',
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ });
+
+ // Click on Tuesday 12am
+ var $view = $('#qunit-fixture').contents();
+ $view.prependTo('body');
+ var top = calendar.$('.fc-axis:contains(12am)').offset().top + 5;
+ var left = calendar.$('.fc-day:eq(2)').offset().left + 5;
+ try {
+ testUtils.triggerPositionalMouseEvent(left, top, "mousedown");
+ testUtils.triggerPositionalMouseEvent(left, top, "mouseup");
+ } catch (e) {
+ calendar.destroy();
+ $view.remove();
+ throw new Error('The test failed to simulate a click on the screen.' +
+ 'Your screen is probably too small or your dev tools are open.');
+ }
+ assert.ok($('.modal-dialog.modal-sm').length,
+ "should open the quick create dialog");
+
+ // Creating the event
+ $('.modal-body input:first').val('new event in quick create').trigger('input');
+ $('.modal button.btn:contains(Create)').trigger('click').trigger('click');
+ assert.strictEqual(calendar.$('.fc-event:contains(new event in quick create)').length, 1,
+ "should display the new record");
+
+ calendar.destroy();
+ $view.remove();
+ });
});
});
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 3055b6e08d2..b1ded45d9a2 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -903,7 +903,7 @@ QUnit.module('Views', {
testUtils.intercept(form, 'execute_action', function (event) {
assert.strictEqual(event.data.action_data.name, "post",
"should trigger execute_action with correct method name");
- assert.deepEqual(event.data.res_ids, [2], "should have correct id in event data");
+ assert.deepEqual(event.data.env.currentID, 2, "should have correct id in event data");
event.data.on_success();
event.data.on_closed();
});
@@ -956,7 +956,7 @@ QUnit.module('Views', {
testUtils.intercept(form, 'execute_action', function (event) {
assert.step('execute_action');
- assert.deepEqual(event.data.res_ids, [resID],
+ assert.deepEqual(event.data.env.currentID, resID,
"execute action should be done on correct record id");
event.data.on_success();
event.data.on_closed();
@@ -1006,7 +1006,7 @@ QUnit.module('Views', {
testUtils.intercept(form, 'execute_action', function (event) {
assert.step('execute_action');
- assert.deepEqual(event.data.res_ids, [resID],
+ assert.deepEqual(event.data.env.currentID, resID,
"execute action should be done on correct record id");
event.data.on_success();
event.data.on_closed();
@@ -3597,8 +3597,9 @@ QUnit.module('Views', {
},
intercepts: {
execute_action: function (e) {
- assert.deepEqual(e.data.action_data.context, {test: 2},
- "button context should have been evaluated and given to the action, without previous context");
+ assert.deepEqual(e.data.action_data.context, {
+ 'test': 2
+ }, "button context should have been evaluated and given to the action, with magicc without previous context");
},
},
});
@@ -3631,8 +3632,8 @@ QUnit.module('Views', {
},
intercepts: {
execute_action: function (e) {
- assert.deepEqual(e.data.action_data.context, {},
- "button context should have been evaluated and given to the action, without previous context");
+ assert.deepEqual(e.data.action_data.context, {
+ }, "button context should have been evaluated and given to the action, with magic keys but without previous context");
},
},
});
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index f0370ac7cf8..2247a64fa3c 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -984,7 +984,7 @@ QUnit.module('Views', {
});
QUnit.test('list view, editable, without data', function (assert) {
- assert.expect(9);
+ assert.expect(11);
this.data.foo.records = [];
@@ -998,6 +998,7 @@ QUnit.module('Views', {
'' +
'' +
'' +
+ '' +
'',
viewOptions: {
action: {
@@ -1033,7 +1034,14 @@ QUnit.module('Views', {
assert.strictEqual(list.$('tbody tr:eq(0) td:eq(1)').text().trim(), "",
"the date field td should not have any content");
+ assert.strictEqual(list.$('.o_list_button button').prop('disabled'), true,
+ "buttons should be disabled while the record is not yet created");
+
list.$buttons.find('.o_list_button_save').click();
+
+ assert.strictEqual(list.$('.o_list_button button').prop('disabled'), false,
+ "buttons should not be disabled once the record is created");
+
list.destroy();
});
@@ -1163,9 +1171,9 @@ QUnit.module('Views', {
},
intercepts: {
execute_action: function (event) {
- assert.deepEqual(event.data.res_ids, [1],
+ assert.deepEqual(event.data.env.currentID, 1,
'should call with correct id');
- assert.strictEqual(event.data.model, 'foo',
+ assert.strictEqual(event.data.env.model, 'foo',
'should call with correct model');
assert.strictEqual(event.data.action_data.name, 'button_action',
"should call correct method");
diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js
index 06053dd1cea..6b167c59edd 100644
--- a/addons/web_editor/static/src/js/backend.js
+++ b/addons/web_editor/static/src/js/backend.js
@@ -383,7 +383,7 @@ var FieldTextHtml = AbstractField.extend({
* @override
*/
commitChanges: function () {
- if (!this.loaded) {
+ if (!this.loaded || this.mode === 'readonly') {
return;
}
// switch to WYSIWYG mode if currently in code mode to get all changes
diff --git a/addons/web_editor/static/tests/web_editor_tests.js b/addons/web_editor/static/tests/web_editor_tests.js
index 283931e9b0d..41ada113cba 100644
--- a/addons/web_editor/static/tests/web_editor_tests.js
+++ b/addons/web_editor/static/tests/web_editor_tests.js
@@ -188,4 +188,34 @@ QUnit.test('field htmlsimple does not crash when commitChanges is called in mode
form.destroy();
});
+QUnit.test('html_frame does not crash when saving in readonly', function (assert) {
+ // The 'Save' action may be triggered even in readonly (e.g. when clicking
+ // on a button in the form view)
+ assert.expect(0);
+
+ var form = testUtils.createView({
+ View: FormView,
+ model: 'mass.mailing',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ mockRPC: function (route) {
+ if (_.str.startsWith(route, '/test')) {
+ // manually call the callback to simulate that the iframe has
+ // been correctly loaded
+ window.odoo[$.deparam(route).callback + '_content'].call();
+ return $.when();
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.saveRecord(); // before the fix done in this commit, it crashed here
+ form.destroy();
+});
+
});
diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js
index 0a1cd2e2923..234a43463dd 100644
--- a/addons/website/static/src/js/website.snippets.animation.js
+++ b/addons/website/static/src/js/website.snippets.animation.js
@@ -416,8 +416,9 @@ animation.registry.media_video = animation.Class.extend({
if (!this.$target.has('> iframe').length) {
var editor = ' ';
var size = ' ';
- this.$target.html(editor+size+'');
+ this.$target.html(editor+size);
}
+ this.$target.html(this.$target.html()+'');
return this._super.apply(this, arguments);
},
});
diff --git a/addons/website/views/website_config_settings_views.xml b/addons/website/views/website_config_settings_views.xml
index 7b11f25fe13..829b1f3e999 100644
--- a/addons/website/views/website_config_settings_views.xml
+++ b/addons/website/views/website_config_settings_views.xml
@@ -9,7 +9,7 @@
|