summaryrefslogtreecommitdiff
path: root/addons/web
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web')
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js15
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js25
-rw-r--r--addons/web/static/src/js/views/abstract_controller.js12
-rw-r--r--addons/web/static/src/js/views/abstract_renderer.js8
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js37
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js5
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js1
-rw-r--r--addons/web/static/src/js/views/graph/graph_renderer.js64
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_controller.js48
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_model.js5
-rw-r--r--addons/web/static/src/less/datepicker.less3
-rw-r--r--addons/web/static/src/less/fields.less2
-rw-r--r--addons/web/static/src/less/form_view.less4
-rw-r--r--addons/web/static/src/less/kanban_column_progressbar.less1
-rw-r--r--addons/web/static/src/less/kanban_dashboard.less1
-rw-r--r--addons/web/static/src/less/list_view.less6
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js11
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js146
-rw-r--r--addons/web/static/tests/views/calendar_tests.js35
-rw-r--r--addons/web/static/tests/views/kanban_tests.js112
20 files changed, 511 insertions, 30 deletions
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 2fb0521ffb5..fbb6d0e7c6b 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -1028,6 +1028,21 @@ var FieldPhone = FieldEmail.extend({
},
/**
+ * Remove possibly present ­ characters when saving number
+ *
+ * @override
+ * @private
+ */
+ _setValue: function (value, options) {
+ // NOT NEEDED AS OF SAAS-11.3
+ if (value) {
+ // remove possibly pasted ­ characters
+ value = value.replace(/\u00AD/g, '');
+ }
+ return this._super(value, options);
+ },
+
+ /**
* Phone fields are clickable in readonly on small screens ~= on phones.
* This can be overriden by call-capable modules to display a clickable
* link in different situations, like always regardless of screen size,
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 353da62706f..a977147460a 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -1292,12 +1292,27 @@ var FieldOne2Many = FieldX2Many.extend({
// we don't want interference with the components upstream.
ev.stopPropagation();
+ var self = this;
var id = ev.data.id;
- // trigger an empty 'UPDATE' operation when the user clicks on 'Save' in
- // the dialog, to notify the main record that a subrecord of this
- // relational field has changed (those changes will be already stored on
- // that subrecord, thanks to the 'Save').
- var onSaved = this._setValue.bind(this, { operation: 'UPDATE', id: id }, {});
+ var onSaved = function (record, hasChanged) {
+ if (!hasChanged) {
+ return;
+ }
+ if (_.some(self.value.data, {id: record.id})) {
+ // the record already exists in the relation, so trigger an
+ // empty 'UPDATE' operation when the user clicks on 'Save' in
+ // the dialog, to notify the main record that a subrecord of
+ // this relational field has changed (those changes will be
+ // already stored on that subrecord, thanks to the 'Save').
+ self._setValue({ operation: 'UPDATE', id: record.id });
+ } else {
+ // the record isn't in the relation yet, so add it ; this can
+ // happen if the user clicks on 'Save & New' in the dialog (the
+ // opened record will be updated, and other records will be
+ // created)
+ self._setValue({ operation: 'ADD', id: record.id });
+ }
+ };
this._openFormDialog({
id: id,
on_saved: onSaved,
diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js
index 1007aa6c720..ff7ab5a43bb 100644
--- a/addons/web/static/src/js/views/abstract_controller.js
+++ b/addons/web/static/src/js/views/abstract_controller.js
@@ -102,6 +102,18 @@ var AbstractController = Widget.extend(ControlPanelMixin, {
}
return this._super.apply(this, arguments);
},
+ /**
+ * Called each time the controller is attached into the DOM.
+ */
+ on_attach_callback: function () {
+ this.renderer.on_attach_callback();
+ },
+ /**
+ * Called each time the controller is detached from the DOM.
+ */
+ on_detach_callback: function () {
+ this.renderer.on_detach_callback();
+ },
//--------------------------------------------------------------------------
// Public
diff --git a/addons/web/static/src/js/views/abstract_renderer.js b/addons/web/static/src/js/views/abstract_renderer.js
index 935fcc6643e..2394fb31a64 100644
--- a/addons/web/static/src/js/views/abstract_renderer.js
+++ b/addons/web/static/src/js/views/abstract_renderer.js
@@ -36,6 +36,14 @@ return Widget.extend({
this.$el.addClass(this.arch.attrs.class);
return $.when(this._render(), this._super());
},
+ /**
+ * Called each time the renderer is attached into the DOM.
+ */
+ on_attach_callback: function () {},
+ /**
+ * Called each time the renderer is detached from the DOM.
+ */
+ on_detach_callback: function () {},
//--------------------------------------------------------------------------
// Public
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 b17562348f7..de9001c328a 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -3167,6 +3167,9 @@ var BasicModel = AbstractModel.extend({
* @param {Object} [options]
* @param {string[]} [options.fieldNames] the fields to fetch for a record
* @param {boolean} [options.onlyGroups=false]
+ * @param {boolean} [options.keepEmptyGroups=false] if set, the groups not
+ * present in the read_group anymore (empty groups) will stay in the
+ * datapoint (used to mimic the kanban renderer behaviour for example)
* @returns {Deferred}
*/
_load: function (dataPoint, options) {
@@ -3319,9 +3322,29 @@ var BasicModel = AbstractModel.extend({
*/
_makeDefaultRecord: function (modelName, params) {
var self = this;
+
+ var determineExtraFields = function() {
+ // Fields that are present in the originating view, that need to be initialized
+ // Hence preventing their value to crash when getting back to the originating view
+ var parentRecord = self.localData[params.parentID];
+
+ var originView = parentRecord && parentRecord.fieldsInfo;
+ if (!originView || !originView[parentRecord.viewType])
+ return [];
+
+ var fieldsFromOrigin = _.filter(Object.keys(originView[parentRecord.viewType]),
+ function(fieldname) {
+ return params.fields[fieldname] !== undefined;
+ });
+
+ return fieldsFromOrigin;
+ }
+
var fieldNames = Object.keys(params.fieldsInfo[params.viewType]);
var fields_key = _.without(fieldNames, '__last_update');
+ var extraFields = determineExtraFields();
+
return this._rpc({
model: modelName,
method: 'default_get',
@@ -3339,7 +3362,7 @@ var BasicModel = AbstractModel.extend({
viewType: params.viewType,
});
- return self.applyDefaultValues(record.id, result)
+ return self.applyDefaultValues(record.id, result, {fieldNames: _.union(fieldNames, extraFields)})
.then(function () {
var def = $.Deferred();
self._performOnChange(record, fields_key).always(function () {
@@ -3603,6 +3626,8 @@ var BasicModel = AbstractModel.extend({
} else if (_.contains(['one2many', 'many2many'], fieldType)) {
var x2mCommands = commands[0][2][fieldName];
defs.push(self._processX2ManyCommands(r, fieldName, x2mCommands));
+ } else {
+ r._changes[fieldName] = self._parseServerValue(field, r._changes[fieldName]);
}
}
}
@@ -3801,6 +3826,16 @@ var BasicModel = AbstractModel.extend({
defs.push(self._load(newGroup, options));
}
});
+ if (options && options.keepEmptyGroups) {
+ // Find the groups that were available in a previous
+ // readGroup but are not there anymore.
+ // Note that these groups are put after existing groups so
+ // the order is not conserved. A sort *might* be useful.
+ var emptyGroupsIDs = _.difference(_.pluck(previousGroups, 'id'), list.data);
+ _.each(emptyGroupsIDs, function (groupID) {
+ list.data.push(groupID);
+ });
+ }
return $.when.apply($, defs).then(function () {
// generate the res_ids of the main list, being the concatenation
// of the fetched res_ids in each group
diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js
index 6467e4695c6..92890766b34 100644
--- a/addons/web/static/src/js/views/calendar/calendar_model.js
+++ b/addons/web/static/src/js/views/calendar/calendar_model.js
@@ -253,7 +253,10 @@ return AbstractModel.extend({
* @param {Moment} start
*/
setDate: function (start) {
- this.data.start_date = this.data.end_date = this.data.target_date = this.data.highlight_date = start;
+ // keep highlight/target_date in localtime
+ this.data.highlight_date = this.data.target_date = start.clone();
+ // set dates in UTC with timezone applied manually
+ this.data.start_date = this.data.end_date = start;
this.data.start_date.utc().add(this.getSession().getTZOffset(this.data.start_date), 'minutes');
switch (this.data.scale) {
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 ea56c841c86..4c73fde121b 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -99,6 +99,7 @@ var FormController = BasicController.extend({
* @todo convert to new style
*/
on_attach_callback: function () {
+ this._super.apply(this, arguments);
this.autofocus();
},
/**
diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js
index 8ce6ec14aba..1fbd6572a9a 100644
--- a/addons/web/static/src/js/views/graph/graph_renderer.js
+++ b/addons/web/static/src/js/views/graph/graph_renderer.js
@@ -42,6 +42,30 @@ return AbstractRenderer.extend({
nv.utils.offWindowResize(this.to_remove);
this._super();
},
+ /**
+ * The graph view uses the nv(d3) lib to render the graph. This lib requires
+ * that the rendering is done directly into the DOM (so that it can correctly
+ * compute positions). However, the views are always rendered in fragments,
+ * and appended to the DOM once ready (to prevent them from flickering). We
+ * here use the on_attach_callback hook, called when the widget is attached
+ * to the DOM, to perform the rendering. This ensures that the rendering is
+ * always done in the DOM.
+ *
+ * @override
+ */
+ on_attach_callback: function () {
+ this._super.apply(this, arguments);
+ this.isInDOM = true;
+ this._renderGraph();
+ },
+ /**
+ * @override
+ */
+ on_detach_callback: function () {
+ this._super.apply(this, arguments);
+ this.isInDOM = false;
+ },
+
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -50,10 +74,9 @@ return AbstractRenderer.extend({
* Render the chart.
*
* Note that This method is synchronous, but the actual rendering is done
- * asynchronously (in a setTimeout). The reason for that is that nvd3/d3
- * needs to be in the DOM to correctly render itself. So, we trick Odoo by
- * returning immediately, then wait a tiny interval before actually
- * displaying the data.
+ * asynchronously. The reason for that is that nvd3/d3 needs to be in the
+ * DOM to correctly render itself. So, we trick Odoo by returning
+ * immediately, then we render the chart when the widget is in the DOM.
*
* @override
* @private
@@ -76,21 +99,15 @@ return AbstractRenderer.extend({
description: _t("Try to add some records, or make sure that " +
"there is no active filter in the search bar."),
}));
- } else {
- var self = this;
- setTimeout(function () {
- self.$el.empty();
- var chart = self['_render' + _.str.capitalize(self.state.mode) + 'Chart']();
- if (chart && chart.tooltip.chartContainer) {
- self.to_remove = chart.update;
- nv.utils.onWindowResize(chart.update);
- chart.tooltip.chartContainer(self.el);
- }
- }, 0);
+ } else if (this.isInDOM) {
+ // only render the graph if the widget is already in the DOM (this
+ // happens typically after an update), otherwise, it will be
+ // rendered when the widget will be attached to the DOM (see
+ // 'on_attach_callback')
+ this._renderGraph();
}
return this._super.apply(this, arguments);
},
-
/**
* Helper function to set up data properly for the multiBarChart model in
* nvd3.
@@ -327,6 +344,21 @@ return AbstractRenderer.extend({
chart(svg);
return chart;
},
+ /**
+ * Renders the graph according to its type. This function must be called
+ * when the renderer is in the DOM (for nvd3 to render the graph correctly).
+ *
+ * @private
+ */
+ _renderGraph: function () {
+ this.$el.empty();
+ var chart = this['_render' + _.str.capitalize(this.state.mode) + 'Chart']();
+ if (chart && chart.tooltip.chartContainer) {
+ this.to_remove = chart.update;
+ nv.utils.onWindowResize(chart.update);
+ chart.tooltip.chartContainer(this.el);
+ }
+ },
});
});
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 8a779becdea..9c227fba2ac 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -10,6 +10,7 @@ odoo.define('web.KanbanController', function (require) {
var BasicController = require('web.BasicController');
var Context = require('web.Context');
var core = require('web.core');
+var Domain = require('web.Domain');
var view_dialogs = require('web.view_dialogs');
var _t = core._t;
@@ -189,10 +190,57 @@ var KanbanController = BasicController.extend({
resIDs: record.res_ids,
},
on_closed: function () {
+ var recordModel = self.model.localData[record.id];
+ var group = self.model.localData[recordModel.parentID];
+ var parent = self.model.localData[group.parentID];
+
self.model.reload(record.id).then(function (db_id) {
var data = self.model.get(db_id);
var kanban_record = event.target;
kanban_record.update(data);
+
+ // Check if we still need to display the record. Some fields of the domain are
+ // not guaranteed to be in data. This is for example the case if the action
+ // contains a domain on a field which is not in the Kanban view. Therefore,
+ // we need to handle multiple cases based on 3 variables:
+ // domInData: all domain fields are in the data
+ // activeInDomain: 'active' is already in the domain
+ // activeInData: 'active' is available in the data
+
+ var domain = (parent ? parent.domain : group.domain) || [];
+ var domInData = _.every(domain, function (d) {
+ return d[0] in data.data;
+ });
+ var activeInDomain = _.pluck(domain, 0).indexOf('active') !== -1;
+ var activeInData = 'active' in data.data;
+
+ // Case # | domInData | activeInDomain | activeInData
+ // 1 | true | true | true => no domain change
+ // 2 | true | true | false => not possible
+ // 3 | true | false | true => add active in domain
+ // 4 | true | false | false => no domain change
+ // 5 | false | true | true => no evaluation
+ // 6 | false | true | false => no evaluation
+ // 7 | false | false | true => replace domain
+ // 8 | false | false | false => no evaluation
+
+ // There are 3 cases which cannot be evaluated since we don't have all the
+ // necessary information. The complete solution would be to perform a RPC in
+ // these cases, but this is out of scope. A simpler one is to do a try / catch.
+
+ if (domInData && !activeInDomain && activeInData) {
+ domain = domain.concat([['active', '=', true]]);
+ } else if (!domInData && !activeInDomain && activeInData) {
+ domain = [['active', '=', true]];
+ }
+ try {
+ var visible = new Domain(domain).compute(data.evalContext);
+ } catch (e) {
+ return;
+ }
+ if (!visible) {
+ kanban_record.destroy();
+ }
});
},
});
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 15d6284cfad..411d4cc7b14 100644
--- a/addons/web/static/src/js/views/kanban/kanban_model.js
+++ b/addons/web/static/src/js/views/kanban/kanban_model.js
@@ -419,7 +419,10 @@ var KanbanModel = BasicModel.extend({
while (element) {
if (element.progressBar) {
return def.then(function (data) {
- return self._load(element, {onlyGroups: true}).then(function () {
+ return self._load(element, {
+ keepEmptyGroups: true,
+ onlyGroups: true,
+ }).then(function () {
return data;
});
});
diff --git a/addons/web/static/src/less/datepicker.less b/addons/web/static/src/less/datepicker.less
index 99c909efacc..fdf3a585b57 100644
--- a/addons/web/static/src/less/datepicker.less
+++ b/addons/web/static/src/less/datepicker.less
@@ -17,11 +17,12 @@
.o_datepicker_input {
width: 100%;
+ cursor: pointer;
}
.o_datepicker_button {
.o-position-absolute(2px, 4px);
- cursor: pointer;
+ pointer-events: none; // let click events go through the underlying input
&:after {
.o-caret-down;
}
diff --git a/addons/web/static/src/less/fields.less b/addons/web/static/src/less/fields.less
index ee9f1d9556a..ff6fe9eb20b 100644
--- a/addons/web/static/src/less/fields.less
+++ b/addons/web/static/src/less/fields.less
@@ -185,7 +185,7 @@
display: inline-block;
padding: 0;
margin: 0;
- vertical-align: middle;
+ vertical-align: baseline;
> .o_priority_star {
display: inline-block;
font-size: 1.35em;
diff --git a/addons/web/static/src/less/form_view.less b/addons/web/static/src/less/form_view.less
index 60a3671caa2..c3cbe2e3494 100644
--- a/addons/web/static/src/less/form_view.less
+++ b/addons/web/static/src/less/form_view.less
@@ -248,6 +248,10 @@
border-left-color: @gray-lighter;
}
}
+
+ .caret {
+ margin-top: -2px;
+ }
}
.o-status-more {
diff --git a/addons/web/static/src/less/kanban_column_progressbar.less b/addons/web/static/src/less/kanban_column_progressbar.less
index 86fe0853de2..f4cffd6c097 100644
--- a/addons/web/static/src/less/kanban_column_progressbar.less
+++ b/addons/web/static/src/less/kanban_column_progressbar.less
@@ -91,6 +91,7 @@
margin-left: 3%;
color: @headings-color;
text-align: right;
+ white-space: nowrap;
.o-transform-origin(right, center);
&.o_kanban_grow {
diff --git a/addons/web/static/src/less/kanban_dashboard.less b/addons/web/static/src/less/kanban_dashboard.less
index adf67f31bf0..947b78739f7 100644
--- a/addons/web/static/src/less/kanban_dashboard.less
+++ b/addons/web/static/src/less/kanban_dashboard.less
@@ -223,6 +223,7 @@
}
.o_favorite {
+ top: 3px;
left: 0;
right: auto;
}
diff --git a/addons/web/static/src/less/list_view.less b/addons/web/static/src/less/list_view.less
index c6d81293fbf..73bd50e1966 100644
--- a/addons/web/static/src/less/list_view.less
+++ b/addons/web/static/src/less/list_view.less
@@ -37,12 +37,14 @@
.user-select(none); // Prevent unwanted selection while sorting
&::after {
- margin-left: 6px;
- position: absolute;
font-family: FontAwesome;
content: "\f0d7";
opacity: 0;
}
+ &:not(:empty)::after {
+ margin-left: 6px;
+ position: absolute;
+ }
&.o-sort-up {
cursor: n-resize;
&::after {
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index e395d1a0d7e..f321db1d172 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -3080,7 +3080,7 @@ QUnit.module('basic_fields', {
QUnit.module('PhoneWidget');
QUnit.test('phone field in form view on extra small screens', function (assert) {
- assert.expect(7);
+ assert.expect(8);
var form = createView({
View: FormView,
@@ -3127,6 +3127,15 @@ QUnit.module('basic_fields', {
assert.strictEqual($phoneLink.attr('href'), 'tel:new',
"should still have proper tel prefix");
+ // NOT NEEDED AS OF SAAS-11.3
+ // save phone with ­ and verify it is removed
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('input[type="text"].o_field_widget').val('h\u00ADi').trigger('input');
+ form.$buttons.find('.o_form_button_save').click();
+ $phoneLink = form.$('a.o_form_uri.o_field_widget');
+ assert.strictEqual($phoneLink.attr('href'), 'tel:hi',
+ "U+00AD should have been removed");
+
form.destroy();
});
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index b755dc3440c..64ffc31096c 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -4246,7 +4246,6 @@ QUnit.module('relational_fields', {
form.destroy();
});
-
QUnit.test('one2many list (non editable): edition', function (assert) {
assert.expect(12);
@@ -5115,6 +5114,32 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('one2many and default_get (with date)', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.p.default = [
+ [0, false, {date: '2017-10-08'}],
+ ];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="date"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ assert.strictEqual(form.$('.o_data_cell').text(), '10/08/2017',
+ "should correctly display the date");
+
+ form.destroy();
+ });
+
QUnit.test('one2many and onchange (with integer)', function (assert) {
assert.expect(4);
@@ -8780,6 +8805,53 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('add a line, edit it and "Save & New"', function (assert) {
+ assert.expect(5);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree><field name="display_name"/></tree>' +
+ '<form><field name="display_name"/></form>' +
+ '</field>' +
+ '</form>',
+ });
+
+ assert.strictEqual(form.$('.o_data_row').length, 0,
+ "there should be no record in the relation");
+
+ // add a new record
+ form.$('.o_field_x2many_list_row_add a').click();
+ $('.modal .o_field_widget').val('new record').trigger('input');
+ $('.modal .modal-footer .btn-primary:first').click(); // Save & Close
+
+ assert.strictEqual(form.$('.o_data_row .o_data_cell').text(), 'new record',
+ "should display the new record");
+
+ // reopen freshly added record and edit it
+ form.$('.o_data_row .o_data_cell').click();
+ $('.modal .o_field_widget').val('new record edited').trigger('input');
+
+ // save it, and choose to directly create another record
+ $('.modal .modal-footer .btn-primary:nth(1)').click(); // Save & New
+
+ assert.strictEqual($('.modal').length, 1,
+ "the model should still be open");
+ assert.strictEqual($('.modal .o_field_widget').text(), '',
+ "should have cleared the input");
+
+ $('.modal .o_field_widget').val('another new record').trigger('input');
+ $('.modal .modal-footer .btn-primary:first').click(); // Save & Close
+
+ assert.strictEqual(form.$('.o_data_row .o_data_cell').text(),
+ 'new record editedanother new record', "should display the two records");
+
+ form.destroy();
+ });
+
QUnit.module('FieldMany2Many');
QUnit.test('many2many kanban: edition', function (assert) {
@@ -11304,6 +11376,78 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('one2many with extra field from server not in form', function (assert) {
+ assert.expect(6);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p" >' +
+ '<tree>' +
+ '<field name="datetime"/>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,form': '<form>' +
+ '<field name="display_name"/>' +
+ '</form>'},
+ mockRPC: function(route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ args.args[1].p[0][2].datetime = '2018-04-05 12:00:00';
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ var x2mList = form.$('.o_field_x2many_list[name=p]');
+
+ // Add a record in the list
+ x2mList.find('.o_field_x2many_list_row_add a').click();
+
+ var modal = $('.modal-dialog.modal-lg');
+
+ var nameInput = modal.find('input.o_input[name=display_name]');
+ nameInput.val('michelangelo').trigger('input');
+
+ // Save the record in the modal (though it is still virtual)
+ modal.find('.btn-primary').first().click();
+
+ assert.equal(x2mList.find('.o_data_row').length, 1,
+ 'There should be 1 records in the x2m list');
+
+ var newlyAdded = x2mList.find('.o_data_row').eq(0);
+
+ assert.equal(newlyAdded.find('.o_data_cell').first().text(), '',
+ 'The create_date field should be empty');
+ assert.equal(newlyAdded.find('.o_data_cell').eq(1).text(), 'michelangelo',
+ 'The display name field should have the right value');
+
+ // Save the whole thing
+ form.$buttons.find('.o_form_button_save').click();
+
+ x2mList = form.$('.o_field_x2many_list[name=p]');
+
+ // Redo asserts in RO mode after saving
+ assert.equal(x2mList.find('.o_data_row').length, 1,
+ 'There should be 1 records in the x2m list');
+
+ newlyAdded = x2mList.find('.o_data_row').eq(0);
+
+ assert.equal(newlyAdded.find('.o_data_cell').first().text(), '04/05/2018 12:00:00',
+ 'The create_date field should have the right value');
+ assert.equal(newlyAdded.find('.o_data_cell').eq(1).text(), 'michelangelo',
+ 'The display name field should have the right value');
+
+ form.destroy();
+ });
+
QUnit.test('one2many invisible depends on parent field', function (assert) {
assert.expect(4);
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index b073bc988dd..7430c6b994b 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -1824,6 +1824,41 @@ QUnit.module('Views', {
assert.ok(!$groupBy.is(':visible'), 'groupby menu should not be visible');
actionManager.destroy();
});
+
+ QUnit.test('timezone does not affect current day', function (assert) {
+ assert.expect(2);
+
+ var calendar = createView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch:
+ '<calendar date_start="start_date">'+
+ '<field name="name"/>'+
+ '</calendar>',
+ archs: archs,
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ session: {
+ getTZOffset: function () {
+ return -2400; // 40 hours timezone
+ },
+ },
+
+ });
+
+ var $sidebar = calendar.$('.o_calendar_sidebar');
+
+ assert.strictEqual($sidebar.find('.ui-datepicker-current-day').text(), "12", "should highlight the target day");
+
+ // go to previous day
+ $sidebar.find('.ui-datepicker-current-day').prev().click();
+
+ assert.strictEqual($sidebar.find('.ui-datepicker-current-day').text(), "11", "should highlight the selected day");
+
+ calendar.destroy();
+ });
});
});
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index b26cc8a3bd5..7aed9cad8f9 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -2511,6 +2511,73 @@ QUnit.module('Views', {
kanban.destroy();
});
+ QUnit.test('button executes action and check domain', function (assert) {
+ assert.expect(2);
+
+ var data = this.data;
+ data.partner.fields.active = {string: "Active", type: "boolean", default: true};
+ for (var k in this.data.partner.records) {
+ data.partner.records[k].active = true;
+ }
+
+ var kanban = createView({
+ View: KanbanView,
+ model: "partner",
+ data: data,
+ arch:
+ '<kanban>' +
+ '<templates><div t-name="kanban-box">' +
+ '<field name="foo"/>' +
+ '<field name="active"/>' +
+ '<button type="object" name="a1" />' +
+ '<button type="object" name="toggle_active" />' +
+ '</div></templates>' +
+ '</kanban>',
+ });
+
+ testUtils.intercept(kanban, 'execute_action', function (event) {
+ data.partner.records[0].active = false;
+ event.data.on_closed();
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_record:contains(yop)').length, 1, "should display 'yop' record");
+ kanban.$('.o_kanban_record:contains(yop) button[data-name="toggle_active"]').click();
+ assert.strictEqual(kanban.$('.o_kanban_record:contains(yop)').length, 0, "should remove 'yop' record from the view");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('button executes action with domain field not in view', function (assert) {
+ assert.expect(1);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: "partner",
+ data: this.data,
+ domain: [['bar', '=', true]],
+ arch:
+ '<kanban>' +
+ '<templates><div t-name="kanban-box">' +
+ '<field name="foo"/>' +
+ '<button type="object" name="a1" />' +
+ '<button type="object" name="toggle_action" />' +
+ '</div></templates>' +
+ '</kanban>',
+ });
+
+ testUtils.intercept(kanban, 'execute_action', function (event) {
+ event.data.on_closed();
+ });
+
+ try {
+ kanban.$('.o_kanban_record:contains(yop) button[data-name="toggle_action"]').click();
+ assert.strictEqual(true, true, 'Everything went fine');
+ } catch (e) {
+ assert.strictEqual(true, false, 'Error triggered at action execution');
+ }
+ kanban.destroy();
+ });
+
QUnit.test('rendering date and datetime', function (assert) {
assert.expect(2);
@@ -3150,6 +3217,51 @@ QUnit.module('Views', {
kanban.destroy();
});
+ QUnit.test('drag & drop records grouped by m2o with progressbar', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].product_id = false;
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<kanban>' +
+ '<progressbar field="foo" colors=\'{"yop": "success", "gnap": "warning", "blip": "danger"}\'/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="int_field"/>' +
+ '</div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['product_id'],
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/resequence') {
+ return $.when(true);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "1",
+ "counter should contain the correct value");
+
+ testUtils.dragAndDrop(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)'), kanban.$('.o_kanban_group:eq(1)'));
+ assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "0",
+ "counter should contain the correct value");
+
+ testUtils.dragAndDrop(kanban.$('.o_kanban_group:eq(1) .o_kanban_record:eq(2)'), kanban.$('.o_kanban_group:eq(0)'));
+ assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "1",
+ "counter should contain the correct value");
+
+ testUtils.dragAndDrop(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)'), kanban.$('.o_kanban_group:eq(1)'));
+ assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "0",
+ "counter should contain the correct value");
+
+ kanban.destroy();
+ });
+
QUnit.test('progress bar subgroup count recompute', function(assert) {
assert.expect(2);