summaryrefslogtreecommitdiff
path: root/addons/web/static
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static')
-rw-r--r--addons/web/static/lib/py.js/lib/py.js12
-rw-r--r--addons/web/static/src/js/chrome/action_manager.js6
-rw-r--r--addons/web/static/src/js/chrome/action_manager_act_window.js6
-rw-r--r--addons/web/static/src/js/core/dialog.js6
-rw-r--r--addons/web/static/src/js/core/dom.js2
-rw-r--r--addons/web/static/src/js/tools/debug_manager.js4
-rw-r--r--addons/web/static/src/js/views/abstract_view.js2
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js38
-rw-r--r--addons/web/static/src/js/views/basic/basic_view.js15
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js15
-rw-r--r--addons/web/static/src/js/views/control_panel/control_panel_controller.js9
-rw-r--r--addons/web/static/src/js/views/control_panel/control_panel_model.js5
-rw-r--r--addons/web/static/src/js/views/control_panel/control_panel_renderer.js2
-rw-r--r--addons/web/static/src/js/views/control_panel/control_panel_view.js2
-rw-r--r--addons/web/static/src/js/views/form/form_view.js18
-rw-r--r--addons/web/static/src/js/views/graph/graph_renderer.js9
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_column.js10
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_record.js6
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js3
-rw-r--r--addons/web/static/src/js/views/list/list_view.js15
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_controller.js3
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_model.js33
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_renderer.js29
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_view.js8
-rw-r--r--addons/web/static/src/js/widgets/attach_document.js5
-rw-r--r--addons/web/static/src/js/widgets/switch_company_menu.js2
-rw-r--r--addons/web/static/src/scss/bootstrap_overridden.scss4
-rw-r--r--addons/web/static/src/scss/kanban_view_mobile.scss3
-rw-r--r--addons/web/static/src/scss/layout_background.scss54
-rw-r--r--addons/web/static/src/scss/layout_boxed.scss67
-rw-r--r--addons/web/static/src/scss/layout_clean.scss32
-rw-r--r--addons/web/static/src/scss/primary_variables.scss3
-rw-r--r--addons/web/static/src/scss/report.scss15
-rw-r--r--addons/web/static/src/xml/base.xml26
-rw-r--r--addons/web/static/src/xml/kanban.xml4
-rw-r--r--addons/web/static/src/xml/menu.xml2
-rw-r--r--addons/web/static/tests/chrome/action_manager_tests.js193
-rw-r--r--addons/web/static/tests/core/py_utils_tests.js7
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js166
-rw-r--r--addons/web/static/tests/helpers/test_utils_create.js1
-rw-r--r--addons/web/static/tests/helpers/test_utils_dom.js17
-rw-r--r--addons/web/static/tests/tools/debug_manager_tests.js4
-rw-r--r--addons/web/static/tests/views/calendar_tests.js92
-rw-r--r--addons/web/static/tests/views/control_panel_tests.js62
-rw-r--r--addons/web/static/tests/views/form_tests.js72
-rw-r--r--addons/web/static/tests/views/graph_tests.js33
-rw-r--r--addons/web/static/tests/views/kanban_mobile_tests.js6
-rw-r--r--addons/web/static/tests/views/kanban_tests.js63
-rw-r--r--addons/web/static/tests/views/pivot_tests.js161
-rw-r--r--addons/web/static/tests/views/search_filters_menu_tests.js140
-rw-r--r--addons/web/static/tests/views/search_view_tests.js34
51 files changed, 1233 insertions, 293 deletions
diff --git a/addons/web/static/lib/py.js/lib/py.js b/addons/web/static/lib/py.js/lib/py.js
index 06d834321de..bd794323995 100644
--- a/addons/web/static/lib/py.js/lib/py.js
+++ b/addons/web/static/lib/py.js/lib/py.js
@@ -282,7 +282,10 @@ var py = {};
var name_pattern = new RegExp('^' + Name + '$');
var strip = new RegExp('^' + Whitespace);
return function tokenize(s) {
- var max=s.length, tokens = [], start, end = undefined;
+ s = s
+ .replace(/\\\"/g, '\\u0022') // for double quote
+ .replace(/\\\'/g, '\\u0027'); // for single quote
+ var max=s.length, tokens = [], start, end;
// /g flag makes repeated exec() have memory
var pseudoprog = new RegExp(PseudoToken, 'g');
@@ -302,7 +305,6 @@ var py = {};
end = pseudoprog.lastIndex;
// strip leading space caught by Whitespace
var token = s.slice(start, end).replace(strip, '');
- var initial = token[0];
if (number_pattern.test(token)) {
tokens.push(create(symbols['(number)'], {
@@ -310,9 +312,13 @@ var py = {};
}));
} else if (string_pattern.test(token)) {
var m = string_pattern.exec(token);
+ var value = (m[3] !== undefined ? m[3] : m[5]);
+ value
+ .replace(/\\u0022/g, '"')
+ .replace(/\\u0027/g, "'");
tokens.push(create(symbols['(string)'], {
unicode: !!(m[2] || m[4]),
- value: (m[3] !== undefined ? m[3] : m[5])
+ value: value
}));
} else if (token in symbols) {
var symbol;
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index c798189c0a0..107c6e314eb 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -332,9 +332,6 @@ var ActionManager = Widget.extend({
// update the internal state and the DOM
self._pushController(controller);
- // toggle the fullscreen mode for actions in target='fullscreen'
- self._toggleFullscreen();
-
// store the action into the sessionStorage so that it can be
// fully restored on F5
self.call('session_storage', 'setItem', 'current_action', action._originalAction);
@@ -720,6 +717,9 @@ var ActionManager = Widget.extend({
action: this.getCurrentAction(),
controller: controller,
});
+
+ // toggle the fullscreen mode for actions in target='fullscreen'
+ this._toggleFullscreen();
},
/**
* Pushes the given state, with additional information about the given
diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js
index 1a688ffa6b2..106f9f2aa47 100644
--- a/addons/web/static/src/js/chrome/action_manager_act_window.js
+++ b/addons/web/static/src/js/chrome/action_manager_act_window.js
@@ -93,7 +93,7 @@ ActionManager.include({
action = state.action;
options = _.extend(options, {
additional_context: context,
- resID: state.id,
+ resID: state.id || undefined, // empty string with bbq
viewType: state.view_type,
});
}
@@ -396,7 +396,7 @@ ActionManager.include({
var inline = action.target === 'inline';
var options = {
action_id: action.id,
- toolbar: inDialog && inline,
+ toolbar: !inDialog && !inline,
};
var views = action.views.slice();
if (!inline && !(inDialog && action.views[0][1] === 'form')) {
@@ -635,7 +635,7 @@ ActionManager.include({
_.object(_.reject(_.pairs(env.context), function (pair) {
return pair[0].match('^(?:(?:default_|search_default_|show_).+|' +
'.+_view_ref|group_by|group_by_no_leaf|active_id|' +
- 'active_ids)$') !== null;
+ 'active_ids|orderedBy)$') !== null;
}))
);
ctx.add(actionData.context || {});
diff --git a/addons/web/static/src/js/core/dialog.js b/addons/web/static/src/js/core/dialog.js
index 492ea06c7bd..83eded92c8a 100644
--- a/addons/web/static/src/js/core/dialog.js
+++ b/addons/web/static/src/js/core/dialog.js
@@ -143,7 +143,11 @@ var Dialog = Widget.extend({
$.when(def).always(self.close.bind(self));
}
});
- self.$footer.append($button);
+ if (self.technical) {
+ self.$footer.append($button);
+ } else {
+ self.$footer.prepend($button);
+ }
});
},
diff --git a/addons/web/static/src/js/core/dom.js b/addons/web/static/src/js/core/dom.js
index 848e406c4b2..b8a5b8be3a4 100644
--- a/addons/web/static/src/js/core/dom.js
+++ b/addons/web/static/src/js/core/dom.js
@@ -506,7 +506,7 @@ var dom = {
var $dropdownMenu = $('<ul/>', {class: 'dropdown-menu'});
$extraItemsToggle = $('<li/>', {class: 'nav-item dropdown o_extra_menu_items'})
- .append($('<a/>', {href: '#', class: 'nav-link dropdown-toggle o-no-caret', 'data-toggle': 'dropdown'})
+ .append($('<a/>', {role: 'button', href: '#', class: 'nav-link dropdown-toggle o-no-caret', 'data-toggle': 'dropdown', 'aria-expanded': false})
.append($('<i/>', {class: 'fa fa-plus'})))
.append($dropdownMenu);
$extraItemsToggle.insertAfter($items.last());
diff --git a/addons/web/static/src/js/tools/debug_manager.js b/addons/web/static/src/js/tools/debug_manager.js
index a348f54acd9..f09d8745f04 100644
--- a/addons/web/static/src/js/tools/debug_manager.js
+++ b/addons/web/static/src/js/tools/debug_manager.js
@@ -365,6 +365,10 @@ DebugManager.include({
views: [[false, 'list'], [false, 'form']],
type: 'ir.actions.act_window',
domain: [['res_model', '=', this._action.res_model], ['res_id', '=', selectedIDs[0]]],
+ context: {
+ default_res_model: this._action.res_model,
+ default_res_id: selectedIDs[0],
+ },
});
},
get_metadata: function() {
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index 75de2be9446..5921c1b4bc3 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -105,7 +105,7 @@ var AbstractView = Factory.extend({
this.arch = this.fieldsView.arch;
this.fields = this.fieldsView.viewFields;
this.userContext = params.userContext || {};
- this.withControlPanel = params.withControlPanel;
+ this.withControlPanel = this.withControlPanel && params.withControlPanel;
// the boolean parameter 'isEmbedded' determines if the view should be
// considered as a subview. For now this is only used by the graph
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 8266b1dee42..9706251ed07 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -1758,7 +1758,7 @@ var BasicModel = AbstractModel.extend({
model: list.model,
method: 'read',
args: [_.pluck(data, 'id'), fieldNames],
- context: record.context,
+ context: _.extend({}, record.context, field.context),
}).then(function (records) {
_.each(records, function (record) {
list_records[record.id].data = record;
@@ -1833,7 +1833,7 @@ var BasicModel = AbstractModel.extend({
addDef = this._applyX2ManyChange(record, fieldName, {
operation: 'ADD_M2M',
ids: values
- });
+ }, viewType);
}
if (removedIds.length) {
var listData = _.map(list.data, function (localId) {
@@ -1847,7 +1847,7 @@ var BasicModel = AbstractModel.extend({
}
return _.findWhere(listData, {res_id: resID}).id;
}),
- });
+ }, viewType);
}
return $.when(addDef, removedDef);
}
@@ -2827,7 +2827,7 @@ var BasicModel = AbstractModel.extend({
var ids = record.data[fieldName] || [];
var list = self._makeDataPoint({
count: ids.length,
- context: record.context,
+ context: _.extend({}, record.context, field.context),
fieldsInfo: fieldsInfo,
fields: view ? view.fields : fieldInfo.relatedFields,
limit: fieldInfo.limit,
@@ -4289,9 +4289,12 @@ var BasicModel = AbstractModel.extend({
}
return list;
}).then(function () {
- self._fetchX2ManysSingleBatch(list);
- self._fetchReferencesSingleBatch(list);
- return list;
+ var fetchingDefs = [];
+ fetchingDefs.push(self._fetchX2ManysSingleBatch(list));
+ fetchingDefs.push(self._fetchReferencesSingleBatch(list));
+ return $.when.apply($, fetchingDefs).then(function () {
+ return list;
+ });
});
});
},
@@ -4322,11 +4325,24 @@ var BasicModel = AbstractModel.extend({
}
return def.then(function () {
var resIDs = [];
- // generate the current count and res_ids list by applying the changes
- var currentCount = list.count;
var currentResIDs = list.res_ids;
- var effectiveLimit = (list.limit || 0) - (list.tempLimitIncrement || 0);
- var upperBound = effectiveLimit ? Math.min(list.offset + effectiveLimit, currentCount) : currentCount;
+ // if new records have been added to the list, their virtual ids have
+ // been pushed at the end of res_ids (or at the beginning, depending
+ // on the editable property), ignoring completely the current page
+ // where the records have actually been created ; for that reason,
+ // we use orderedResIDs which is a freezed order with the virtual ids
+ // at the correct position where they were actually inserted ; however,
+ // when we use orderedResIDs, we must filter out ids that are not in
+ // res_ids, which correspond to records that have been removed from
+ // the relation (this information being taken into account in res_ids
+ // but not in orderedResIDs)
+ if (list.orderedResIDs) {
+ currentResIDs = list.orderedResIDs.filter(function (resID) {
+ return list.res_ids.indexOf(resID) >= 0;
+ });
+ }
+ var currentCount = currentResIDs.length;
+ var upperBound = list.limit ? Math.min(list.offset + list.limit, currentCount) : currentCount;
var fieldNames = list.getFieldNames();
for (var i = list.offset; i < upperBound; i++) {
var resId = currentResIDs[i];
diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js
index d5d6cfa18d5..39444fdfd75 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -145,6 +145,21 @@ var BasicView = AbstractView.extend({
if (_.difference(fieldViewTypes, recordViewTypes).length) {
fieldNames.push(name);
}
+
+ if (record.data[name].viewType === 'default') {
+ // Use case: x2many (tags) in x2many list views
+ // When opening the x2many record form view, the
+ // x2many will be reloaded but it may not have
+ // the same fields (ex: tags in list and list in
+ // form) so we need to merge the fieldsInfo to
+ // avoid losing the initial fields (display_name)
+ var defaultFieldInfo = record.data[name].fieldsInfo.default;
+ _.each(fieldViews, function (fieldView) {
+ _.each(fieldView.fieldsInfo, function (x2mFieldInfo) {
+ _.defaults(x2mFieldInfo, defaultFieldInfo);
+ });
+ });
+ }
}
}
// Many2one: context is not the same between the different views
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 1f8c9423cac..7c06a7d818d 100644
--- a/addons/web/static/src/js/views/calendar/calendar_model.js
+++ b/addons/web/static/src/js/views/calendar/calendar_model.js
@@ -244,18 +244,18 @@ return AbstractModel.extend({
this.setDate(this.data.target_date.clone().add(-1, this.data.scale));
},
/**
- * @todo: this should not work. it ignores the domain/context
- *
* @override
- * @param {any} _handle ignored
- * @param {Object} params
+ * @param {Object} [params.context]
* @param {Array} [params.domain]
* @returns {Deferred}
*/
- reload: function (_handle, params) {
+ reload: function (handle, params) {
if (params.domain) {
this.data.domain = params.domain;
}
+ if (params.context) {
+ this.data.context = params.context;
+ }
return this._loadCalendar();
},
/**
@@ -385,6 +385,9 @@ return AbstractModel.extend({
* @returns {Object}
*/
_getFullCalendarOptions: function () {
+ var week_start = _t.database.parameters.week_start || 0;
+ // calendar uses index 0 for Sunday but Odoo stores it as 7
+ week_start = week_start % 7;
return {
defaultView: (this.mode === "month")? "month" : ((this.mode === "week")? "agendaWeek" : ((this.mode === "day")? "agendaDay" : "agendaWeek")),
header: false,
@@ -404,7 +407,7 @@ return AbstractModel.extend({
monthNamesShort: moment.monthsShort(),
dayNames: moment.weekdays(),
dayNamesShort: moment.weekdaysShort(),
- firstDay: _t.database.parameters.week_start,
+ firstDay: week_start,
slotLabelFormat: _t.database.parameters.time_format.search("%H") != -1 ? 'H:mm': 'h(:mm)a',
};
},
diff --git a/addons/web/static/src/js/views/control_panel/control_panel_controller.js b/addons/web/static/src/js/views/control_panel/control_panel_controller.js
index 991cee949e8..3d6e62f4bf9 100644
--- a/addons/web/static/src/js/views/control_panel/control_panel_controller.js
+++ b/addons/web/static/src/js/views/control_panel/control_panel_controller.js
@@ -160,12 +160,11 @@ var ControlPanelController = mvc.Controller.extend({
*/
_onFacetRemoved: function (ev) {
ev.stopPropagation();
- var group = ev.data.group;
- if (!group) {
- group = this.renderer.getLastFacet();
+ var group = ev.data.group || this.renderer.getLastFacet();
+ if (group) {
+ this.model.deactivateGroup(group.id);
+ this._reportNewQueryAndRender();
}
- this.model.deactivateGroup(group.id);
- this._reportNewQueryAndRender();
},
/**
* @private
diff --git a/addons/web/static/src/js/views/control_panel/control_panel_model.js b/addons/web/static/src/js/views/control_panel/control_panel_model.js
index 9353022eacf..67b1a4c64f8 100644
--- a/addons/web/static/src/js/views/control_panel/control_panel_model.js
+++ b/addons/web/static/src/js/views/control_panel/control_panel_model.js
@@ -645,10 +645,7 @@ var ControlPanelModel = mvc.Model.extend({
* @returns {Object} context
*/
_getFilterContext: function (filter) {
- var context = {};
- if (filter.type === 'favorite') {
- _.extend(context, filter.context);
- }
+ var context = filter.context || {};
// the following code aims to restore this:
// https://github.com/odoo/odoo/blob/master/addons/web/static/src/js/views/search/search_inputs.js#L498
// this is required for the helpdesk tour to pass
diff --git a/addons/web/static/src/js/views/control_panel/control_panel_renderer.js b/addons/web/static/src/js/views/control_panel/control_panel_renderer.js
index eea7fdae430..665fe73bf7d 100644
--- a/addons/web/static/src/js/views/control_panel/control_panel_renderer.js
+++ b/addons/web/static/src/js/views/control_panel/control_panel_renderer.js
@@ -102,7 +102,7 @@ var ControlPanelRenderer = Renderer.extend({
//--------------------------------------------------------------------------
/**
- * @returns {Object}
+ * @returns {Object|undefined}
*/
getLastFacet: function () {
return this.state.facets.slice(-1)[0];
diff --git a/addons/web/static/src/js/views/control_panel/control_panel_view.js b/addons/web/static/src/js/views/control_panel/control_panel_view.js
index 381e5dcf7e5..2bde26340e7 100644
--- a/addons/web/static/src/js/views/control_panel/control_panel_view.js
+++ b/addons/web/static/src/js/views/control_panel/control_panel_view.js
@@ -141,6 +141,7 @@ var ControlPanelView = Factory.extend({
'Ω';
if (filter.type === 'filter') {
filter.domain = attrs.domain;
+ filter.context = pyUtils.eval('context', attrs.context);
if (attrs.date) {
filter.fieldName = attrs.date;
filter.fieldType = this.fields[attrs.date].type;
@@ -196,6 +197,7 @@ var ControlPanelView = Factory.extend({
var groupOfGroupBys = [];
var groupNumber = 1;
+
_.each(preFilters, function (preFilter) {
if (preFilter.attrs && preFilter.attrs.invisible) {
return;
diff --git a/addons/web/static/src/js/views/form/form_view.js b/addons/web/static/src/js/views/form/form_view.js
index d49f2b005fa..5a00350c68f 100644
--- a/addons/web/static/src/js/views/form/form_view.js
+++ b/addons/web/static/src/js/views/form/form_view.js
@@ -26,13 +26,20 @@ var FormView = BasicView.extend({
* @override
*/
init: function (viewInfo, params) {
+ var hasSidebar = params.hasSidebar;
this._super.apply(this, arguments);
var mode = params.mode || (params.currentId ? 'readonly' : 'edit');
this.loadParams.type = 'record';
+ // this is kind of strange, but the param object is modified by
+ // AbstractView, so we only need to use its hasSidebar value if it was
+ // not already present in the beginning of this method
+ if (hasSidebar === undefined) {
+ hasSidebar = params.hasSidebar;
+ }
+ this.controllerParams.hasSidebar = hasSidebar;
this.controllerParams.disableAutofocus = params.disable_autofocus;
- this.controllerParams.hasSidebar = params.hasSidebar;
this.controllerParams.toolbarActions = viewInfo.toolbar;
this.controllerParams.footerToButtons = params.footerToButtons;
@@ -63,7 +70,6 @@ var FormView = BasicView.extend({
/**
* @override
- * @param {string} [action.target]
*/
_extractParamsFromAction: function (action) {
var params = this._super.apply(this, arguments);
@@ -73,6 +79,7 @@ var FormView = BasicView.extend({
params.withControlPanel = !(inDialog || inline);
params.footerToButtons = inDialog;
params.hasSearchView = inDialog ? false : params.hasSearchView;
+ params.hasSidebar = !inDialog && !inline;
params.searchMenuTypes = inDialog ? [] : params.searchMenuTypes;
if (inDialog || inline || fullscreen) {
params.mode = 'edit';
@@ -115,9 +122,14 @@ var FormView = BasicView.extend({
while (matches = regex.exec(attrs.context)) {
context[matches[1]] = matches[2];
}
+
+ // Remove *_view_ref coming from parent view
+ var refinedContext = _.pick(self.loadParams.context, function (value, key) {
+ return key.indexOf('_view_ref') === -1;
+ });
defs.push(parent.loadViews(
field.relation,
- new Context(context, self.userContext, self.loadParams.context).eval(),
+ new Context(context, self.userContext, refinedContext).eval(),
[[null, attrs.mode === 'tree' ? 'list' : attrs.mode]])
.then(function (views) {
for (var viewName in views) {
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 fe4efab1b20..38c2f02351e 100644
--- a/addons/web/static/src/js/views/graph/graph_renderer.js
+++ b/addons/web/static/src/js/views/graph/graph_renderer.js
@@ -337,8 +337,8 @@ return AbstractRenderer.extend({
var self = this;
// Remove Undefined of first GroupBy
- var graphData = _.filter(this.state.data, function(elem){
- return elem.labels[0] !== undefined;
+ var graphData = _.filter(this.state.data, function (elem) {
+ return elem.labels[0] !== undefined && elem.labels[0] !== _t("Undefined");
});
// undefined label value becomes a string 'Undefined' translated
@@ -391,10 +391,9 @@ return AbstractRenderer.extend({
var tick = 0;
var serie, tickLabel;
var identity = function (p) {return p;};
-
- for (var i = 0; i < this.state.data.length; i++) {
+ for (var i = 0; i < graphData.length; i++) {
if (graphData[i].labels[0] !== tickLabel) {
- tickLabel = this.state.data[i].labels[0];
+ tickLabel = graphData[i].labels[0];
ticksLabels.push(tickLabel);
tick++;
}
diff --git a/addons/web/static/src/js/views/kanban/kanban_column.js b/addons/web/static/src/js/views/kanban/kanban_column.js
index 57ecc6f4b4e..b526b80a971 100644
--- a/addons/web/static/src/js/views/kanban/kanban_column.js
+++ b/addons/web/static/src/js/views/kanban/kanban_column.js
@@ -114,7 +114,6 @@ var KanbanColumn = Widget.extend({
revert: 0,
delay: 0,
items: '> .o_kanban_record:not(.o_updating)',
- helper: 'clone',
cursor: 'move',
over: function () {
self.$el.addClass('o_kanban_hover');
@@ -122,6 +121,15 @@ var KanbanColumn = Widget.extend({
out: function () {
self.$el.removeClass('o_kanban_hover');
},
+ start: function (event, ui) {
+ ui.item.addClass('o_currently_dragged');
+ },
+ stop: function (event, ui) {
+ var item = ui.item;
+ setTimeout(function () {
+ item.removeClass('o_currently_dragged');
+ });
+ },
update: function (event, ui) {
var record = ui.item.data('record');
var index = self.records.indexOf(record);
diff --git a/addons/web/static/src/js/views/kanban/kanban_record.js b/addons/web/static/src/js/views/kanban/kanban_record.js
index d9d7ac77fe2..46f6964dd1c 100644
--- a/addons/web/static/src/js/views/kanban/kanban_record.js
+++ b/addons/web/static/src/js/views/kanban/kanban_record.js
@@ -5,7 +5,6 @@ odoo.define('web.KanbanRecord', function (require) {
* This file defines the KanbanRecord widget, which corresponds to a card in
* a Kanban view.
*/
-
var config = require('web.config');
var core = require('web.core');
var Domain = require('web.Domain');
@@ -210,6 +209,11 @@ var KanbanRecord = Widget.extend({
* @private
*/
_openRecord: function () {
+ if (this.$el.hasClass('o_currently_dragged')) {
+ // this record is currently being dragged and dropped, so we do not
+ // want to open it.
+ return;
+ }
var editMode = this.$el.hasClass('oe_kanban_global_click_edit');
this.trigger_up('open_record', {
id: this.db_id,
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js
index 8619448e793..b7f2dd62b28 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js
@@ -143,7 +143,7 @@ KanbanRenderer.include({
onSuccess: function () {
// update the columns and tabs positions (optionally with an animation)
var updateFunc = animate ? 'animate' : 'css';
- self.$('.o_kanban_mobile_tab').removeClass('o_current');
+ self.$('.o_kanban_mobile_tab, .o_kanban_group').removeClass('o_current');
_.each(self.widgets, function (column, index) {
var columnID = column.id || column.db_id;
var $column = self.$('.o_kanban_group[data-id="' + columnID + '"]');
@@ -157,6 +157,7 @@ KanbanRenderer.include({
} else if (index === moveToIndex) {
$column[updateFunc]({left: '0%'});
$tab[updateFunc]({left: '50%'});
+ $column.addClass('o_current');
$tab.addClass('o_current');
} else if (index < moveToIndex) {
$column.css({left: '-100%'});
diff --git a/addons/web/static/src/js/views/list/list_view.js b/addons/web/static/src/js/views/list/list_view.js
index 291cb8de8a0..a28eb4da03a 100644
--- a/addons/web/static/src/js/views/list/list_view.js
+++ b/addons/web/static/src/js/views/list/list_view.js
@@ -57,8 +57,21 @@ var ListView = BasicView.extend({
//--------------------------------------------------------------------------
// Private
- //---
+ //--------------------------------------------------------------------------
+ /**
+ * @override
+ */
+ _extractParamsFromAction: function (action) {
+ var params = this._super.apply(this, arguments);
+ var inDialog = action.target === 'new';
+ var inline = action.target === 'inline';
+ params.hasSidebar = !inDialog && !inline;
+ return params;
+ },
+ /**
+ * @override
+ */
_updateMVCParams: function () {
this._super.apply(this, arguments);
this.controllerParams.noLeaf = !!this.loadParams.context.group_by_no_leaf;
diff --git a/addons/web/static/src/js/views/pivot/pivot_controller.js b/addons/web/static/src/js/views/pivot/pivot_controller.js
index a4833b40d81..ac48ab769ff 100644
--- a/addons/web/static/src/js/views/pivot/pivot_controller.js
+++ b/addons/web/static/src/js/views/pivot/pivot_controller.js
@@ -345,7 +345,8 @@ var PivotController = AbstractController.extend({
var col_id = $target.data('id');
var measure = $target.data('measure');
var isAscending = $target.hasClass('o_pivot_measure_row_sorted_asc');
- this.model.sortRows(col_id, measure, isAscending);
+ var dataType = $target.data('data_type');
+ this.model.sortRows(col_id, measure, isAscending, dataType);
this.update({}, {reload: false});
},
/**
diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js
index 1a0d3625440..5a6423fbe34 100644
--- a/addons/web/static/src/js/views/pivot/pivot_model.js
+++ b/addons/web/static/src/js/views/pivot/pivot_model.js
@@ -457,16 +457,23 @@ var PivotModel = AbstractModel.extend({
* @param {any} col_id
* @param {any} measure
* @param {any} descending
+ * @param {'data'|'comparisonData'|'variation'} [dataType]
*/
- sortRows: function (col_id, measure, descending) {
+ sortRows: function (col_id, measure, descending, dataType) {
var cells = this.data.cells;
+ var comparisonFunction = compare;
+ if (this.data.compare) {
+ dataType = dataType || 'data';
+ comparisonFunction = specialCompare;
+ }
this._traverseTree(this.data.main_row.root, function (header) {
- header.children.sort(compare);
+ header.children.sort(comparisonFunction);
});
this.data.sorted_column = {
id: col_id,
measure: measure,
order: descending ? 'desc' : 'asc',
+ dataType: dataType,
};
function _getValue (id1, id2) {
if ((id1 in cells) && (id2 in cells[id1])) {
@@ -476,12 +483,26 @@ var PivotModel = AbstractModel.extend({
}
function compare (row1, row2) {
- var values1 = _getValue(row1.id, col_id),
- values2 = _getValue(row2.id, col_id),
- value1 = values1 ? values1[measure] : 0,
- value2 = values2 ? values2[measure] : 0;
+ var values1 = _getValue(row1.id, col_id);
+ var values2 = _getValue(row2.id, col_id);
+ var value1 = values1 ? values1[measure] : 0;
+ var value2 = values2 ? values2[measure] : 0;
return descending ? value1 - value2 : value2 - value1;
}
+ function specialCompare (row1, row2) {
+ var values1 = _getValue(row1.id, col_id);
+ var values2 = _getValue(row2.id, col_id);
+ var value1 = values1 ? values1[measure] : {data: 0, comparisonData: 0, variation: {magnitude: 0}};
+ var value2 = values2 ? values2[measure] : {data: 0, comparisonData: 0, variation: {magnitude: 0}};
+ if (dataType === 'variation') {
+ return descending ?
+ value1[dataType].magnitude - value2[dataType].magnitude:
+ value2[dataType].magnitude - value1[dataType].magnitude;
+ }
+ return descending ?
+ value1[dataType] - value2[dataType]:
+ value2[dataType] - value1[dataType];
+ }
},
/**
* Toggle the active state for a given measure, then reload the data.
diff --git a/addons/web/static/src/js/views/pivot/pivot_renderer.js b/addons/web/static/src/js/views/pivot/pivot_renderer.js
index a68d91330a9..787e28e38a9 100644
--- a/addons/web/static/src/js/views/pivot/pivot_renderer.js
+++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js
@@ -116,6 +116,8 @@ var PivotRenderer = AbstractRenderer.extend({
_renderHeaders: function ($thead, headers, nbrCols) {
var self = this;
var i, j, cell, $row, $cell;
+ var measure, dataType;
+ var id, measureCellIds = [];
var groupbyLabels = _.map(this.state.colGroupBys, function (gb) {
return self.state.fields[gb.split(':')[0]].string;
@@ -144,10 +146,13 @@ var PivotRenderer = AbstractRenderer.extend({
$cell.data('id', cell.id);
}
if (cell.measure) {
+ measure = cell.measure;
+ measureCellIds.push(cell.id);
$cell.addClass('o_pivot_measure_row text-muted')
- .text(this.state.fields[cell.measure].string);
- $cell.data('id', cell.id).data('measure', cell.measure);
- if (!this.compare && cell.id === this.state.sortedColumn.id && cell.measure === this.state.sortedColumn.measure) {
+ .text(this.state.fields[measure].string);
+ $cell.data('id', cell.id).data('measure', measure);
+ if (cell.id === this.state.sortedColumn.id &&
+ measure === this.state.sortedColumn.measure) {
$cell.addClass('o_pivot_measure_row_sorted_' + this.state.sortedColumn.order);
if (this.state.sortedColumn.order == 'asc') {
$cell.attr('aria-sorted', 'ascending');
@@ -167,12 +172,30 @@ var PivotRenderer = AbstractRenderer.extend({
}
if (this.compare) {
var colLabels = [this.timeRangeDescription, this.comparisonTimeRangeDescription, 'Variation'];
+ var dataTypes = ['data', 'comparisonData', 'variation'];
$row = $('<tr>');
for (i = 0; i < 3 * nbrCols; i++) {
+ id = measureCellIds[~~(i / 3)];
+ measure = this.state.measures[(~~(i / 3)) % this.state.measures.length];
+ dataType = dataTypes[i % 3];
$cell = $('<th>')
+ .addClass('o_pivot_measure_row text-muted')
+ .data('data_type', dataType)
+ .data('id', id)
+ .data('measure', measure)
.text(colLabels[i % 3])
.attr('rowspan', 1)
.attr('colspan', 1);
+ if (dataType === this.state.sortedColumn.dataType &&
+ id === this.state.sortedColumn.id &&
+ measure === this.state.sortedColumn.measure) {
+ $cell.addClass('o_pivot_measure_row_sorted_' + this.state.sortedColumn.order);
+ if (this.state.sortedColumn.order === 'asc') {
+ $cell.attr('aria-sorted', 'ascending');
+ } else {
+ $cell.attr('aria-sorted', 'descending');
+ }
+ }
$row.append($cell);
}
$thead.append($row);
diff --git a/addons/web/static/src/js/views/pivot/pivot_view.js b/addons/web/static/src/js/views/pivot/pivot_view.js
index 38d8d80a9d7..5e05a2fe7e7 100644
--- a/addons/web/static/src/js/views/pivot/pivot_view.js
+++ b/addons/web/static/src/js/views/pivot/pivot_view.js
@@ -70,9 +70,6 @@ var PivotView = AbstractView.extend({
widgets[name] = field.attrs.widget;
}
- if (field.attrs.string) {
- measures[name].string = field.attrs.string;
- }
if (field.attrs.invisible && py.eval(field.attrs.invisible)) {
delete measures[name];
@@ -89,6 +86,11 @@ var PivotView = AbstractView.extend({
if (field.attrs.type === 'measure' && !(field.attrs.name in measures)) {
measures[field.attrs.name] = self.fields[field.attrs.name];
}
+
+ if (field.attrs.string) {
+ measures[name].string = field.attrs.string;
+ }
+
if (field.attrs.type === 'measure' || 'operator' in field.attrs) {
activeMeasures.push(name);
measures[name] = self.fields[name];
diff --git a/addons/web/static/src/js/widgets/attach_document.js b/addons/web/static/src/js/widgets/attach_document.js
index 041b021d60b..13496f3aea7 100644
--- a/addons/web/static/src/js/widgets/attach_document.js
+++ b/addons/web/static/src/js/widgets/attach_document.js
@@ -11,7 +11,7 @@ var _t = core._t;
var AttachDocument = Widget.extend({
template: 'AttachDocument',
events: {
- 'click span.o_attach_document': '_onClickAttachDocument',
+ 'click': '_onClickAttachDocument',
'change input.o_input_file': '_onFileChanged',
},
/**
@@ -76,6 +76,9 @@ var AttachDocument = Widget.extend({
* @param {Event} ev
*/
_onClickAttachDocument: function (ev) {
+ if ($(ev.target).is('input.o_input_file')) {
+ return;
+ }
var fieldNames = this.getParent().canBeSaved(this.state.id);
if (fieldNames.length) {
return this._notifyInvalidFields(fieldNames);
diff --git a/addons/web/static/src/js/widgets/switch_company_menu.js b/addons/web/static/src/js/widgets/switch_company_menu.js
index c9498950a04..cc0f8825643 100644
--- a/addons/web/static/src/js/widgets/switch_company_menu.js
+++ b/addons/web/static/src/js/widgets/switch_company_menu.js
@@ -53,7 +53,7 @@ var SwitchCompanyMenu = Widget.extend({
} else {
a = '<span style="margin-right: 24px;"/>';
}
- companiesList += '<a href="#" class="dropdown-item" data-menu="company" data-company-id="' +
+ companiesList += '<a role="menuitem" href="#" class="dropdown-item" data-menu="company" data-company-id="' +
company[0] + '">' + a + company[1] + '</a>';
});
this.$('.dropdown-menu').html(companiesList);
diff --git a/addons/web/static/src/scss/bootstrap_overridden.scss b/addons/web/static/src/scss/bootstrap_overridden.scss
index edff888acbf..b81146a811f 100644
--- a/addons/web/static/src/scss/bootstrap_overridden.scss
+++ b/addons/web/static/src/scss/bootstrap_overridden.scss
@@ -126,8 +126,8 @@ $badge-font-weight: normal !default;
// Padding applied to the modal body
$modal-inner-padding: $o-horizontal-padding !default;
-$modal-lg: 980px !default;
-$modal-md: 650px !default;
+$modal-lg: $o-modal-lg !default;
+$modal-md: $o-modal-md !default;
// Breadcrumbs
diff --git a/addons/web/static/src/scss/kanban_view_mobile.scss b/addons/web/static/src/scss/kanban_view_mobile.scss
index b74ae8aaea5..1c5a9eab531 100644
--- a/addons/web/static/src/scss/kanban_view_mobile.scss
+++ b/addons/web/static/src/scss/kanban_view_mobile.scss
@@ -36,6 +36,9 @@
margin-left: 0; // override the margin-left: -1px of the desktop mode
border: none;
+ &.o_current {
+ position: initial;
+ }
.o_kanban_header {
display: none;
}
diff --git a/addons/web/static/src/scss/layout_background.scss b/addons/web/static/src/scss/layout_background.scss
index ff2d0cef3b0..9c47ac72a27 100644
--- a/addons/web/static/src/scss/layout_background.scss
+++ b/addons/web/static/src/scss/layout_background.scss
@@ -34,34 +34,48 @@
background-position: bottom center;
background-repeat: no-repeat;
min-height: 620px;
+
+ .table {
+ border-top: 1px solid gray('300');
+ }
+
+ .table td, .table th {
+ border-top: none;
+ }
thead tr th, h2 {
color: $o-brand-odoo;
}
tbody {
color: gray('700');
- tr:nth-child(odd) {
- background-color: rgba(220, 205, 216, 0.2);
- }
- tr td {
- border-top: none;
+ tr {
+ &:nth-child(odd) {
+ background-color: rgba(220, 205, 216, 0.2);
+ }
+ &.o_line_section {
+ color: $o-brand-odoo;
+ background-color: rgba(73, 80, 87, 0.2) !important;
+ }
}
}
/*Total table*/
- .row > .col-4 > table {
- tr:nth-child(odd) {
- background-color: transparent !important;
- }
- tr:first-child td {
- /*we could get rid of this rule if .border-black td would be terminated in report.scss*/
- border-top: none !important;
- }
- tr:last-child td {
- color: $o-brand-odoo;
- }
- }
- #total {
- table tr:last-child td {
- border-bottom: 1px solid black;
+ /* row div rule compat 12.0 */
+ .row > div > table,
+ div#total table {
+ tr {
+ &:nth-child(odd) {
+ background-color: transparent !important;
+ }
+ /* first-child & last-child rule compat 12.0 */
+ &:first-child,
+ &:last-child,
+ &.o_subtotal,
+ &.o_total {
+ border-top: none !important;
+ td {
+ color: $o-brand-odoo;
+ border-top: 1px solid gray('400') !important;
+ }
+ }
}
}
}
diff --git a/addons/web/static/src/scss/layout_boxed.scss b/addons/web/static/src/scss/layout_boxed.scss
index b0b2fb1f743..bf19b6fb7ae 100644
--- a/addons/web/static/src/scss/layout_boxed.scss
+++ b/addons/web/static/src/scss/layout_boxed.scss
@@ -23,52 +23,73 @@
}
}
.o_report_layout_boxed {
- // This is not a general rule since the id is not used in all the reports,
- // we should make a general structure for report body to use it everywhere
- #informations {
- border: 1px solid gray('700');
- padding-top: 10px;
- margin: 0;
- }
> h2 {
text-transform: uppercase;
}
table {
border: 1px solid gray('700');
thead {
+ border-bottom: 2px solid gray('700');
tr th {
- border-bottom: 3px solid gray('700') !important;
text-transform: uppercase;
+ border: 1px solid gray('700');
}
}
tbody {
color: gray('700');
tr {
td {
+ // remove border-top from standard layout
border-top: none;
border-right: 1px solid gray('700');
}
- td:last-child {
+ &.o_line_section td,
+ &.o_line_note td,
+ &.is-subtotal td {
+ border-top: 1px solid gray('700');
+ border-bottom: 1px solid gray('700');
+ }
+ &.o_line_section td {
+ background-color: gray('700');
+ color: #fff;
+ }
+ &.is-subtotal,
+ td.o_price_total {
background-color: gray('200');
}
}
}
}
+ /* compat 12.0 */
+ .page > table:not(.o_main_table) tr td:last-child {
+ background-color: gray('200');
+ }
+ /* compat 12.0 */
+ .row:not(#total) > div > table tr:not(:first-child):not(:last-child) td:last-child {
+ background-color: gray('200');
+ }
/*Total table*/
- .row > .col-4 > table {
- td {
- border-right: none;
- }
- tr:last-child td, tr:first-child td {
- /*we could get rid of this rule if .border-black td would be terminated in report.scss*/
- border-top: none !important;
- }
- tr:first-child td {
- background-color: transparent;
- }
- tr:last-child td {
- background-color: gray('700');
- color: #fff;
+ /* row div rule compat 12.0 */
+ .row > div > table,
+ div#total table {
+ tr {
+ &:first-child,
+ &.o_subtotal {
+ border-bottom: 1px solid gray('700');
+
+ td:first-child {
+ border-right: none;
+ }
+ }
+ &:last-child td,
+ &.o_total td {
+ background-color: gray('700');
+ color: #fff;
+
+ &:first-child {
+ border-right: none;
+ }
+ }
}
}
}
diff --git a/addons/web/static/src/scss/layout_clean.scss b/addons/web/static/src/scss/layout_clean.scss
index 06b2e808dd4..910abc763a4 100644
--- a/addons/web/static/src/scss/layout_clean.scss
+++ b/addons/web/static/src/scss/layout_clean.scss
@@ -18,12 +18,10 @@
font-weight: bolder;
}
.pagenumber {
- width: 35px;
- height: 35px;
border: 3px solid $o-brand-odoo;
background-color: $o-brand-primary;
color: white;
- padding-top: 6px;
+ padding: 4px 8px;
text-align: center;
}
}
@@ -36,6 +34,10 @@
color: $o-brand-odoo;
}
table {
+ &.o_main_table {
+ margin-bottom: 0;
+ }
+
thead {
color: $o-brand-odoo;
tr th {
@@ -66,18 +68,22 @@
}
}
/*Total table*/
- .row > .col-4 {
- width: 43%;
- position: relative;
- top: -20px;
- > table {
- tr:last-child td {
- border-top: 1px solid gray('200') !important;
- }
- tr:first-child td {
- /*we could get rid of this rule if .border-black td would be terminated in report.scss*/
+ /* compat 12.0 */
+ .row:not(#total) > div:has(table) {
+ top: -16px;
+ }
+ /* row col rule compat 12.0 */
+ .row > div > table,
+ div#total table {
+ tr {
+ &:first-child td,
+ &.o_subtotal {
border-top: none !important;
}
+ &:last-child td,
+ &.o_total {
+ border-top: 1px solid gray('200') !important;
+ }
}
}
}
diff --git a/addons/web/static/src/scss/primary_variables.scss b/addons/web/static/src/scss/primary_variables.scss
index 45d1b7ac807..2bdc03a132b 100644
--- a/addons/web/static/src/scss/primary_variables.scss
+++ b/addons/web/static/src/scss/primary_variables.scss
@@ -65,6 +65,9 @@ $o-sheet-vpadding: 24px;
$o-notification-error-bg-color: #F16567;
$o-notification-info-bg-color: #FCFBEA;
+$o-modal-lg: 980px;
+$o-modal-md: 650px;
+
// Needed for having no spacing between sheet and mail body in mass_mailing:
// Different required cancel paddings between web and web_enterprise
$o-sheet-cancel-tpadding: 0px;
diff --git a/addons/web/static/src/scss/report.scss b/addons/web/static/src/scss/report.scss
index 37a7630225b..e901b5b9364 100644
--- a/addons/web/static/src/scss/report.scss
+++ b/addons/web/static/src/scss/report.scss
@@ -44,3 +44,18 @@ body {
.o_underline {
text-decoration: underline;
}
+
+/*Total table*/
+div#total {
+ page-break-inside: avoid;
+ table {
+ tr {
+ &.o_subtotal,
+ &.o_total {
+ td {
+ border-top: 1px solid black !important;
+ }
+ }
+ }
+ }
+}
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 102e70efa1e..55d744d3a60 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -235,7 +235,7 @@
<t t-name="WebClient.DebugManager">
<li class="o_debug_manager" role="menuitem">
- <a href="#" title="Open Developer Tools" aria-label="Open Developer Tools" data-toggle="dropdown" aria-expanded="false" tabindex="-1" data-display="static">
+ <a role="button" href="#" title="Open Developer Tools" aria-label="Open Developer Tools" data-toggle="dropdown" aria-expanded="false" tabindex="-1" data-display="static">
<span class="fa fa-bug"/>
</a>
<div class="dropdown-menu dropdown-menu-right o_debug_dropdown" role="menu"/>
@@ -853,7 +853,7 @@
<span class="o_badge_text" t-att-title="el.display_name"><span role="img" t-attf-aria-label="Tag color: #{colornames[color]}"/><t t-esc="el.display_name"/></span>
</t>
<t t-if="colorField">
- <a href="#" class="dropdown-toggle o-no-caret" data-toggle="dropdown">
+ <a role="button" href="#" class="dropdown-toggle o-no-caret" data-toggle="dropdown" aria-expanded="false">
<t t-raw="_badge_text"/>
</a>
</t>
@@ -1080,10 +1080,10 @@
<t t-if="widget.items.length !== 0">
<t t-set="group" t-value="0"/>
<t t-foreach="widget.items" t-as="item">
- <div t-if="group !== item.groupNumber" class="dropdown-divider" data-removable="1"/>
+ <div t-if="group !== item.groupNumber" role="separator" class="dropdown-divider" data-removable="1"/>
<div class="o_menu_item"
t-att-data-id="item.id">
- <a href="#" t-attf-class="dropdown-item#{item.isActive ? ' selected ' : ''}">
+ <a role="menuitem" href="#" t-attf-class="dropdown-item#{item.isActive ? ' selected ' : ''}">
<t t-esc="item.description"/>
<span t-if="item.isRemovable" t-att-data-id="item.id" class="o_trash_button" > <span class="fa fa-trash-o"></span> </span>
<span t-if="item.hasOptions" t-att-data-id="item.id" class="o_submenu_switcher">
@@ -1093,10 +1093,10 @@
<ul t-if="item.hasOptions &amp;&amp; widget.openItems[item.id]" class="list-unstyled">
<t t-set="subgroup" t-value="0"/>
<t t-foreach="item.options" t-as="option">
- <li t-if="subgroup !== option.groupNumber" class="dropdown-divider" data-removable="1"/>
+ <li t-if="subgroup !== option.groupNumber" role="separator" class="dropdown-divider" data-removable="1"/>
<li class="o_item_option"
t-att-data-item_id="item.id" t-att-data-option_id="option.optionId">
- <a href="#" t-attf-class="dropdown-item#{item.currentOptionId === option.optionId ? ' selected': ''} pl-5"><t t-esc="option.description"/></a>
+ <a role="menuitem" href="#" t-attf-class="dropdown-item#{item.currentOptionId === option.optionId ? ' selected': ''} pl-5"><t t-esc="option.description"/></a>
</li>
<t t-set="subgroup" t-value="option.groupNumber"/>
</t>
@@ -1117,8 +1117,8 @@
</div>
<t t-name="GroupByMenuGenerator">
- <div t-if="widget.presentedFields.length !== 0 &amp;&amp; widget.items.length !== 0" class="dropdown-divider o_generator_menu"/>
- <button t-if="widget.presentedFields.length !== 0" type="button" class="dropdown-item o_generator_menu o_add_custom_group">Add Custom Group</button>
+ <div t-if="widget.presentedFields.length !== 0 &amp;&amp; widget.items.length !== 0" role="separator" class="dropdown-divider o_generator_menu"/>
+ <button t-if="widget.presentedFields.length !== 0" type="button" class="dropdown-item o_generator_menu o_add_custom_group" aria-expanded="false">Add Custom Group</button>
<div t-if="widget.generatorMenuIsOpen" class="dropdown-item-text o_generator_menu">
<select class="w-auto o_input o_group_selector o_add_group">
<t t-foreach="widget.presentedFields" t-as="field">
@@ -1132,8 +1132,8 @@
</t>
<t t-name="FilterMenuGenerator">
- <div t-if="Object.keys(widget.fields).length !== 0 &amp;&amp; widget.items.length !== 0" class="dropdown-divider o_generator_menu"/>
- <button t-if="Object.keys(widget.fields).length !== 0" type="button" class="dropdown-item o_generator_menu o_add_custom_filter">Add Custom Filter</button>
+ <div t-if="Object.keys(widget.fields).length !== 0 &amp;&amp; widget.items.length !== 0" role="separator" class="dropdown-divider o_generator_menu"/>
+ <button t-if="Object.keys(widget.fields).length !== 0" type="button" class="dropdown-item o_generator_menu o_add_custom_filter" aria-expanded="false">Add Custom Filter</button>
<div t-if="widget.generatorMenuIsOpen" class="dropdown-item-text o_generator_menu o_add_filter_menu">
<button class="btn btn-primary o_apply_filter" type="button">Apply</button>
<button class="btn btn-secondary o_add_condition" type="button">
@@ -1335,7 +1335,7 @@
<label for="o_favorite_share_all_users" class="custom-control-label">Share with all users <span class="fa fa-users" role="img" aria-label="Users" title="Users"/></label>
</div>
<div class="dropdown-item-text o_save_favorite">
- <button role="menuitem" type="button" class="btn btn-primary">Save</button>
+ <button type="button" class="btn btn-primary">Save</button>
</div>
</div>
</div>
@@ -1495,7 +1495,7 @@
<t t-name="UserMenu">
<li class="o_user_menu">
- <a class="dropdown-toggle" data-toggle="dropdown" data-display="static" aria-expanded="false" href="#">
+ <a role="button" class="dropdown-toggle" data-toggle="dropdown" data-display="static" aria-expanded="false" href="#">
<img class="rounded-circle oe_topbar_avatar" t-att-src="_s + '/web/static/src/img/user_menu_avatar.png'" alt="Avatar"/>
<span class="oe_topbar_name"/>
</a>
@@ -1516,7 +1516,7 @@
<t t-name="SwitchCompanyMenu">
<li class="o_switch_company_menu">
- <a class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false" href="#" aria-label="Dropdown menu" title="Dropdown menu">
+ <a role="button" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span t-attf-class="#{widget.isMobile ? 'fa fa-building-o' : 'oe_topbar_name'}"/>
</a>
<div class="dropdown-menu dropdown-menu-right" role="menu"/>
diff --git a/addons/web/static/src/xml/kanban.xml b/addons/web/static/src/xml/kanban.xml
index b91b8844752..220395fef02 100644
--- a/addons/web/static/src/xml/kanban.xml
+++ b/addons/web/static/src/xml/kanban.xml
@@ -24,8 +24,8 @@
<a t-if="widget.deletable and widget.id" role="menuitem" class="dropdown-item o_column_delete" href="#">Delete</a>
</t>
<t t-if="widget.has_active_field and widget.archivable">
- <a href="#" class="dropdown-item o_column_archive_records">Archive All</a>
- <a href="#" class="dropdown-item o_column_unarchive_records">Unarchive All</a>
+ <a role="menuitem" href="#" class="dropdown-item o_column_archive_records">Archive All</a>
+ <a role="menuitem" href="#" class="dropdown-item o_column_unarchive_records">Unarchive All</a>
</t>
</div>
</span>
diff --git a/addons/web/static/src/xml/menu.xml b/addons/web/static/src/xml/menu.xml
index 4cf905674d7..3838cc6c226 100644
--- a/addons/web/static/src/xml/menu.xml
+++ b/addons/web/static/src/xml/menu.xml
@@ -97,7 +97,7 @@
<t t-extend="UserMenu.Actions">
<t t-jquery=".dropdown-divider" t-operation="before">
- <a href="#" data-menu="shortcuts" class="dropdown-item d-none d-md-inline-block">Shortcuts</a>
+ <a role="menuitem" href="#" data-menu="shortcuts" class="dropdown-item d-none d-md-inline-block">Shortcuts</a>
</t>
</t>
diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js
index e1b14b4d363..990c166a419 100644
--- a/addons/web/static/tests/chrome/action_manager_tests.js
+++ b/addons/web/static/tests/chrome/action_manager_tests.js
@@ -735,6 +735,37 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
+ QUnit.test('properly load default record', function (assert) {
+ assert.expect(5);
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ return this._super.apply(this, arguments);
+ },
+ });
+ actionManager.loadState({
+ action: 3,
+ id: "", // might happen with bbq and id=& in URL
+ model: 'partner',
+ view_type: 'form',
+ });
+
+ assert.containsOnce(actionManager, '.o_form_view',
+ "should have rendered a form view");
+
+ assert.verifySteps([
+ '/web/action/load',
+ 'load_views',
+ 'default_get',
+ ]);
+
+ actionManager.destroy();
+ });
+
QUnit.test('load requested view for act window actions', function (assert) {
assert.expect(6);
@@ -2095,6 +2126,39 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
+ QUnit.test('sidebar is present in list view', function (assert) {
+ assert.expect(5);
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC: function (route, args) {
+ var res = this._super.apply(this, arguments);
+ if (args.method === 'load_views') {
+ assert.strictEqual(args.kwargs.options.toolbar, true,
+ "should ask for toolbar information");
+ return res.then(function (fieldsViews) {
+ fieldsViews.list.toolbar = {
+ print: [{name: "Print that record"}],
+ };
+ return fieldsViews;
+ });
+ }
+ return res;
+ },
+ });
+ actionManager.doAction(3);
+
+ assert.isNotVisible(actionManager.$('.o_cp_sidebar button.o_dropdown_toggler_btn:contains("Print")'));
+ assert.isNotVisible(actionManager.$('.o_cp_sidebar button.o_dropdown_toggler_btn:contains("Action")'));
+ testUtils.dom.clickFirst(actionManager.$('input.custom-control-input'));
+ assert.isVisible(actionManager.$('.o_cp_sidebar button.o_dropdown_toggler_btn:contains("Print")'));
+ assert.isVisible(actionManager.$('.o_cp_sidebar button.o_dropdown_toggler_btn:contains("Action")'));
+
+ actionManager.destroy();
+ });
+
QUnit.test('can switch between views', function (assert) {
assert.expect(18);
@@ -2155,6 +2219,62 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
+ QUnit.test('orderedBy in context is not propagated when executing another action', function (assert) {
+ assert.expect(6);
+
+ this.data.partner.fields.foo.sortable = true,
+
+ this.archs['partner,false,form'] = '<header>' +
+ '<button name="8" string="Execute action" type="action"/>' +
+ '</header>';
+
+ var searchReadCount = 1;
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/search_read') {
+ if (searchReadCount === 1) {
+ assert.strictEqual(args.model, 'partner');
+ assert.notOk(args.sort);
+ }
+ if (searchReadCount === 2) {
+ assert.strictEqual(args.model, 'partner');
+ assert.strictEqual(args.sort, "foo ASC");
+ }
+ if (searchReadCount === 3) {
+ assert.strictEqual(args.model, 'pony');
+ assert.notOk(args.sort);
+ }
+ searchReadCount += 1;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ actionManager.doAction(3);
+
+ // Simulate the activation of a filter
+ var searchData = {
+ domains: [[["foo", "=", "yop"]]],
+ contexts: [{
+ orderedBy: [],
+ }],
+ };
+ actionManager.trigger_up('search', searchData);
+
+ // Sort records
+ actionManager.$('.o_list_view th.o_column_sortable').click();
+
+ // get to the form view of the model, on the first record
+ actionManager.$('.o_data_cell:first').click();
+
+ // Change model by clicking on the button within the form
+ actionManager.$('.o_form_view button').click();
+
+ actionManager.destroy();
+ });
+
QUnit.test('breadcrumbs are updated when switching between views', function (assert) {
assert.expect(10);
@@ -3314,6 +3434,79 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
+ QUnit.test('fullscreen on action change: back to a "current" action', function (assert) {
+ assert.expect(3);
+
+ this.actions[0].target = 'fullscreen';
+ this.archs['partner,false,form'] = '<form>' +
+ '<button name="1" type="action" class="oe_stat_button" />' +
+ '</form>';
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ intercepts: {
+ toggle_fullscreen: function (ev) {
+ var fullscreen = ev.data.fullscreen;
+
+ switch (toggleFullscreenCalls) {
+ case 0:
+ assert.strictEqual(fullscreen, false);
+ break;
+ case 1:
+ assert.strictEqual(fullscreen, true);
+ break;
+ case 2:
+ assert.strictEqual(fullscreen, false);
+ break;
+ }
+ },
+ },
+
+ });
+
+ var toggleFullscreenCalls = 0;
+ actionManager.doAction(6);
+
+ toggleFullscreenCalls = 1;
+ actionManager.$('button[name=1]').click();
+
+ toggleFullscreenCalls = 2;
+ $('.breadcrumb li a:first').click();
+
+ actionManager.destroy();
+ });
+
+ QUnit.test('fullscreen on action change: all "fullscreen" actions', function (assert) {
+ assert.expect(3);
+
+ this.actions[5].target = 'fullscreen';
+ this.archs['partner,false,form'] = '<form>' +
+ '<button name="1" type="action" class="oe_stat_button" />' +
+ '</form>';
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ intercepts: {
+ toggle_fullscreen: function (ev) {
+ var fullscreen = ev.data.fullscreen;
+ assert.strictEqual(fullscreen, true);
+ },
+ },
+ });
+
+ actionManager.doAction(6);
+
+ actionManager.$('button[name=1]').click();
+
+ $('.breadcrumb li a:first').click();
+
+ actionManager.destroy();
+ });
+
QUnit.module('"ir.actions.act_window_close" actions');
QUnit.test('close the currently opened dialog', function (assert) {
diff --git a/addons/web/static/tests/core/py_utils_tests.js b/addons/web/static/tests/core/py_utils_tests.js
index d59fd30f957..be5760fc6c7 100644
--- a/addons/web/static/tests/core/py_utils_tests.js
+++ b/addons/web/static/tests/core/py_utils_tests.js
@@ -1254,6 +1254,13 @@ QUnit.module('core', function () {
assert.checkAST(expr);
});
+ QUnit.test("parse escaped quoted strings", function (assert) {
+ assert.expect(2);
+
+ assert.checkAST(`'\"'`, "a string containing \"");
+ assert.checkAST(`'\'`, "a string containing \'");
+ });
+
QUnit.module('pyutils (_normalizeDomain)');
QUnit.assert.checkNormalization = function (domain, normalizedDomain) {
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 6c3493828ad..15999ca952e 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -228,6 +228,90 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('widget many2many_checkboxes in a subview', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="Turtles">' +
+ '<field name="turtles" mode="tree">' +
+ '<tree>' +
+ '<field name="id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'turtle,false,form': '<form>' +
+ '<field name="partner_ids" widget="many2many_checkboxes"/>' +
+ '</form>',
+ },
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_data_cell').click();
+ // edit the partner_ids field by (un)checking boxes on the widget
+ var $firstCheckbox = $('.modal .custom-control-input').first();
+ $firstCheckbox.click();
+ assert.ok($firstCheckbox.prop('checked'), "the checkbox should be ticked");
+ var $secondCheckbox = $('.modal .custom-control-input').eq(1);
+ $secondCheckbox.click();
+ assert.notOk($secondCheckbox.prop('checked'), "the checkbox should be unticked");
+ form.destroy();
+ });
+
+ QUnit.test('delete a record while adding another one in a multipage', function (assert) {
+ // in a many2one with at least 2 pages, add a new line. Delete the line above it.
+ // (the onchange makes it so that the virtualID is inserted in the middle of the currentResIDs.)
+ // it should load the next line to display it on the page.
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [2, 3];
+ this.data.partner.onchanges.turtles = function (obj) {
+ obj.turtles = [[5]].concat(obj.turtles);
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="1" decoration-muted="turtle_bar == False">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_bar"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ // add a line (virtual record)
+ form.$('.o_field_x2many_list_row_add a').click();
+ form.$('.o_input').val('pi').trigger('input');
+ // delete the line above it
+ form.$('.o_list_record_remove').first().click();
+ // the next line should be displayed below the newly added one
+ assert.strictEqual(form.$('.o_data_row').length, 2, "should have 2 records");
+ assert.strictEqual(form.$('.o_data_row .o_data_cell:first-child').text(), 'pikawa',
+ "should display the correct records on page 1");
+
+ form.destroy();
+ });
+
QUnit.test('one2many, onchange, edition and multipage...', function (assert) {
assert.expect(7);
@@ -252,7 +336,7 @@ QUnit.module('relational_fields', {
'</form>',
res_id: 1,
mockRPC: function (route, args) {
- assert.step(args.method + ' ' + args.model)
+ assert.step(args.method + ' ' + args.model);
return this._super(route, args);
},
viewOptions: {
@@ -362,6 +446,39 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('many2many read, field context is properly sent', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.timmy.context = {hello: 'world'};
+ this.data.partner.records[0].timmy = [12];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'read' && args.model === 'partner_type') {
+ assert.step(args.kwargs.context.hello);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.verifySteps(['world']);
+
+ form.$buttons.find('.o_form_button_edit').click();
+ var $m2mInput = form.$('.o_field_many2manytags input');
+ $m2mInput.click();
+ $m2mInput.autocomplete('widget').find('li:first()').click();
+ assert.verifySteps(['world', 'world']);
+
+ form.destroy();
+ });
+
QUnit.module('FieldStatus');
QUnit.test('static statusbar widget on many2one field', function (assert) {
@@ -1323,6 +1440,53 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('widget many2many_tags in one2many with display_name', function (assert) {
+ assert.expect(4);
+ this.data.turtle.records[0].partner_ids = [2];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="partner_ids" widget="many2many_tags"/>' + // will use display_name
+ '</tree>' +
+ '<form>' +
+ '<sheet>' +
+ '<field name="partner_ids"/>' +
+ '</sheet>' +
+ '</form>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'partner,false,list': '<tree><field name="foo"/></tree>',
+ },
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_field_one2many[name="turtles"] .o_list_view .o_field_many2manytags[name="partner_ids"]').text().replace(/\s/g, ''),
+ "secondrecordaaa", "the tags should be correctly rendered");
+
+ // open the x2m form view
+ form.$('.o_field_one2many[name="turtles"] .o_list_view td.o_data_cell:first').click();
+ assert.strictEqual($('.modal .o_form_view .o_field_many2many[name="partner_ids"] .o_list_view .o_data_cell').text(),
+ "blipMy little Foo Value", "the list view should be correctly rendered with foo");
+
+ $('.modal button.o_form_button_cancel').click();
+ assert.strictEqual(form.$('.o_field_one2many[name="turtles"] .o_list_view .o_field_many2manytags[name="partner_ids"]').text().replace(/\s/g, ''),
+ "secondrecordaaa", "the tags should still be correctly rendered");
+
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_field_one2many[name="turtles"] .o_list_view .o_field_many2manytags[name="partner_ids"]').text().replace(/\s/g, ''),
+ "secondrecordaaa", "the tags should still be correctly rendered");
+
+ form.destroy();
+ });
+
QUnit.test('widget many2many_tags: tags title attribute', function (assert) {
assert.expect(1);
this.data.turtle.records[0].partner_ids = [2];
diff --git a/addons/web/static/tests/helpers/test_utils_create.js b/addons/web/static/tests/helpers/test_utils_create.js
index 0adb13d466f..5a64512260e 100644
--- a/addons/web/static/tests/helpers/test_utils_create.js
+++ b/addons/web/static/tests/helpers/test_utils_create.js
@@ -135,6 +135,7 @@ function createAsyncView(params) {
currentId: 'res_id' in params ? params.res_id : undefined,
domain: params.domain || [],
context: params.context || {},
+ hasSidebar: false,
});
// patch the View to handle the groupBy given in params, as we can't give it
// in init (unlike the domain and context which can be set in the action)
diff --git a/addons/web/static/tests/helpers/test_utils_dom.js b/addons/web/static/tests/helpers/test_utils_dom.js
index b89f5a8eb5f..dcf9eab6e15 100644
--- a/addons/web/static/tests/helpers/test_utils_dom.js
+++ b/addons/web/static/tests/helpers/test_utils_dom.js
@@ -17,6 +17,10 @@ odoo.define('web.test_utils_dom', function () {
* events, but it is enough to help test drag and drop operations with jqueryUI
* sortable.
*
+ * @todo: remove the withTrailingClick option when the jquery update branch is
+ * merged. This is not the default as of now, because handlers are triggered
+ * synchronously, which is not the same as the 'reality'.
+ *
* @param {jqueryElement} $el
* @param {jqueryElement} $to
* @param {Object} [options]
@@ -27,9 +31,13 @@ odoo.define('web.test_utils_dom', function () {
* @param {boolean} [options.continueMove=false] whether to trigger the
* mousedown action (will only work after another call of this function with
* without this option)
+ * @param {boolean} [options.withTrailingClick=false] if true, this utility
+ * function will also trigger a click on the target after the mouseup event
+ * (this is actually what happens when a drag and drop operation is done)
*/
function dragAndDrop($el, $to, options) {
- var position = (options && options.position) || 'center';
+ options = options || {};
+ var position = options.position || 'center';
var elementCenter = $el.offset();
var toOffset = $to.offset();
@@ -58,7 +66,7 @@ function dragAndDrop($el, $to, options) {
toOffset.top += bound.top;
}
$el.trigger($.Event("mouseenter"));
- if (!(options && options.continueMove)) {
+ if (!(options.continueMove)) {
elementCenter.left += $el.outerWidth() / 2;
elementCenter.top += $el.outerHeight() / 2;
@@ -75,12 +83,15 @@ function dragAndDrop($el, $to, options) {
pageY: toOffset.top
}));
- if (!(options && options.disableDrop)) {
+ if (!options.disableDrop) {
$el.trigger($.Event("mouseup", {
which: 1,
pageX: toOffset.left,
pageY: toOffset.top
}));
+ if (options.withTrailingClick) {
+ $el.click();
+ }
} else {
// It's impossible to drag another element when one is already
// being dragged. So it's necessary to finish the drop when the test is
diff --git a/addons/web/static/tests/tools/debug_manager_tests.js b/addons/web/static/tests/tools/debug_manager_tests.js
index fde90105bfb..fb454aae0d1 100644
--- a/addons/web/static/tests/tools/debug_manager_tests.js
+++ b/addons/web/static/tests/tools/debug_manager_tests.js
@@ -46,6 +46,10 @@ QUnit.module('DebugManager', {}, function () {
intercepts: {
do_action: function (event) {
assert.deepEqual(event.data.action, {
+ context: {
+ default_res_model: "test.model",
+ default_res_id: 5,
+ },
domain: [["res_model", "=", "test.model"],["res_id", "=", 5]],
name: "Manage Attachments",
res_model: "ir.attachment",
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index 3f4c6376d5b..264a00cc52a 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -557,6 +557,98 @@ QUnit.module('Views', {
$view.remove();
});
+ QUnit.test('default week start (US)', function (assert) {
+ // if not given any option, default week start is on Sunday
+ assert.expect(1);
+ var done = assert.async();
+
+ createAsyncView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch:
+ '<calendar class="o_calendar_test" '+
+ 'date_start="start" '+
+ 'date_stop="stop" '+
+ 'mode="week">'+
+ '</calendar>',
+ archs: archs,
+
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ }).then(function (calendar) {
+ assert.strictEqual(calendar.$('.fc-day-header').first().text(), "Sun 12/11",
+ "The first day of the week should be Sunday");
+ calendar.destroy();
+ done();
+ });
+ });
+
+ QUnit.test('European week start', function (assert) {
+ // the week start depends on the locale
+ assert.expect(1);
+ var done = assert.async();
+
+ createAsyncView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch:
+ '<calendar class="o_calendar_test" '+
+ 'date_start="start" '+
+ 'date_stop="stop" '+
+ 'mode="week">'+
+ '</calendar>',
+ archs: archs,
+
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ translateParameters: {
+ week_start: 1,
+ },
+ }).then(function (calendar) {
+ assert.strictEqual(calendar.$('.fc-day-header').first().text(), "Mon 12/12",
+ "The first day of the week should be Monday");
+ calendar.destroy();
+ done();
+ });
+ });
+
+ QUnit.test('week numbering', function (assert) {
+ // week number depends on the week start, which depends on the locale
+ // the calendar library uses numbers [0 .. 6], while Odoo uses [1 .. 7]
+ // so if the modulo is not done, the week number is incorrect
+ assert.expect(1);
+ var done = assert.async();
+
+ createAsyncView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch:
+ '<calendar class="o_calendar_test" '+
+ 'date_start="start" '+
+ 'date_stop="stop" '+
+ 'mode="week">'+
+ '</calendar>',
+ archs: archs,
+
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ translateParameters: {
+ week_start: 7,
+ },
+ }).then(function (calendar) {
+ assert.strictEqual(calendar.$('.fc-week-number').text(), "W51",
+ "We should be on the 51st week");
+ calendar.destroy();
+ done();
+ });
+ });
+
QUnit.test('create event with timezone in week mode with formViewDialog European locale', function (assert) {
assert.expect(8);
diff --git a/addons/web/static/tests/views/control_panel_tests.js b/addons/web/static/tests/views/control_panel_tests.js
index 08cc1391719..ee191e4d3a9 100644
--- a/addons/web/static/tests/views/control_panel_tests.js
+++ b/addons/web/static/tests/views/control_panel_tests.js
@@ -120,6 +120,7 @@ QUnit.module('Views', {
assert.deepEqual(
controlPanelFactory.loadParams.groups,
[[{
+ context: {},
description: "Hello",
groupNumber: 2,
domain: "[]",
@@ -198,6 +199,7 @@ QUnit.module('Views', {
controlPanelFactory.loadParams.groups,
[[
{
+ "context": {},
"description": "Hello One",
"domain": "[]",
"groupNumber": 2,
@@ -205,6 +207,7 @@ QUnit.module('Views', {
"type": "filter"
},
{
+ "context": {},
"description": "Hello Two",
"domain": "[('bar', '=', 3)]",
"groupNumber": 2,
@@ -233,6 +236,7 @@ QUnit.module('Views', {
[
[
{
+ context: {},
description: "Hello One",
domain: "[]",
groupNumber: 2,
@@ -242,6 +246,7 @@ QUnit.module('Views', {
],
[
{
+ context: {},
description: "Hello Two",
domain: "[('bar', '=', 3)]",
groupNumber: 4,
@@ -267,6 +272,7 @@ QUnit.module('Views', {
[
[
{
+ context: {},
description: "Hello",
domain: "[]",
groupNumber: 2,
@@ -353,5 +359,61 @@ QUnit.module('Views', {
controlPanel.destroy();
});
+
+ QUnit.module('Control Panel behaviour');
+
+ QUnit.test('remove a facet with backspace', async function (assert) {
+ assert.expect(2);
+
+ var controlPanel = await createControlPanel({
+ model: 'partner',
+ arch: "<search><filter name=\"filterA\" string=\"A\" domain=\"[]\"/></search>",
+ data: this.data,
+ searchMenuTypes: ['filter'],
+ });
+ testUtils.dom.click(controlPanel.$('.o_filters_menu_button'));
+ testUtils.dom.click($('.o_menu_item a'));
+ assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').text().trim(), 'A',
+ 'should have a facet with A');
+
+ // delete a facet
+ controlPanel.$('input.o_searchview_input').trigger($.Event('keydown', {
+ which: $.ui.keyCode.BACKSPACE,
+ keyCode: $.ui.keyCode.BACKSPACE,
+ }));
+ assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').length, 0,
+ 'there should be no facet');
+
+ // delete nothing (should not crash)
+ controlPanel.$('input.o_searchview_input').trigger($.Event('keydown', {
+ which: $.ui.keyCode.BACKSPACE,
+ keyCode: $.ui.keyCode.BACKSPACE,
+ }));
+
+ controlPanel.destroy();
+ });
+
+ QUnit.module('Control Panel Rendering');
+
+ QUnit.test('invisible filters are not rendered', async function (assert) {
+ assert.expect(2);
+ var controlPanel = await createControlPanel({
+ model: 'partner',
+ arch: "<search>" +
+ "<filter name=\"filterA\" string=\"A\" domain=\"[]\"/>" +
+ "<filter name=\"filterB\" string=\"B\" invisible=\"1\" domain=\"[]\"/>" +
+ "</search>",
+ data: this.data,
+ searchMenuTypes: ['filter'],
+ context: {
+ search_disable_custom_filters: true,
+ },
+ });
+ testUtils.dom.click(controlPanel.$('.o_filters_menu_button'));
+ assert.containsOnce(controlPanel, '.o_menu_item a:contains("A")');
+ assert.containsNone(controlPanel, '.o_menu_item a:contains("B")');
+
+ controlPanel.destroy();
+ });
});
});
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index a120a6a7547..e1cb3277dba 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -17,6 +17,7 @@ var Widget = require('web.Widget');
var _t = core._t;
var createView = testUtils.createView;
var createAsyncView = testUtils.createAsyncView;
+var createActionManager = testUtils.createActionManager;
QUnit.module('Views', {
beforeEach: function () {
@@ -299,7 +300,78 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('Form and subview with _view_ref contexts', function (assert) {
+ assert.expect(2);
+
+ this.data.product.fields.partner_type_ids = {string: "one2many field", type: "one2many", relation: "partner_type"},
+ this.data.product.records = [{id: 1, name: 'Tromblon', partner_type_ids: [12,14]}];
+ this.data.partner.records[0].product_id = 1;
+
+ var actionManager = createActionManager({
+ data: this.data,
+ archs: {
+ 'product,false,form': '<form>'+
+ '<field name="name"/>'+
+ '<field name="partner_type_ids" context="{\'tree_view_ref\': \'some_other_tree_view\'}"/>' +
+ '</form>',
+ 'partner_type,false,list': '<tree>'+
+ '<field name="color"/>'+
+ '</tree>',
+ 'product,false,search': '<search></search>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'load_views') {
+ var context = args.kwargs.context;
+ if (args.model === 'product') {
+ assert.deepEqual(context, {tree_view_ref: 'some_tree_view'},
+ 'The correct _view_ref should have been sent to the server, first time');
+ }
+ if (args.model === 'partner_type') {
+ assert.deepEqual(context, {tree_view_ref: 'some_other_tree_view'},
+ 'The correct _view_ref should have been sent to the server for the subview');
+ }
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="name"/>' +
+ '<field name="product_id" context="{\'tree_view_ref\': \'some_tree_view\'}"/>' +
+ '</form>',
+ res_id: 1,
+
+ mockRPC: function(route, args) {
+ if (args.method === 'get_formview_action') {
+ return $.when({
+ res_id: 1,
+ type: 'ir.actions.act_window',
+ target: 'current',
+ res_model: args.model,
+ context: args.kwargs.context,
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'views': [[false, 'form']],
+ });
+ }
+ return this._super(route, args);
+ },
+
+ interceptsPropagate: {
+ do_action: function (ev) {
+ actionManager.doAction(ev.data.action);
+ },
+ },
+ });
+ form.$('.o_field_widget[name="product_id"]').click();
+ form.destroy();
+ actionManager.destroy();
+ });
QUnit.test('invisible fields are properly hidden', function (assert) {
assert.expect(4);
diff --git a/addons/web/static/tests/views/graph_tests.js b/addons/web/static/tests/views/graph_tests.js
index 73f2179dc8f..d651afc030c 100644
--- a/addons/web/static/tests/views/graph_tests.js
+++ b/addons/web/static/tests/views/graph_tests.js
@@ -785,13 +785,40 @@ QUnit.module('Views', {
'</graph>',
});
- assert.strictEqual(graph.$("svg.nvd3-svg:contains('Undefined')").length, 0);
- assert.strictEqual(graph.$("svg.nvd3-svg:contains('March')").length, 1);
+ assert.strictEqual(graph.$("svg.nvd3-svg .nv-x:contains('Undefined')").length, 0);
+ assert.strictEqual(graph.$("svg.nvd3-svg:contains('January')").length, 1);
testUtils.dom.click(graph.$buttons.find('.o_graph_button[data-mode=bar]'));
- assert.strictEqual(graph.$("svg.nvd3-svg:contains('Undefined')").length, 1);
+ assert.strictEqual(graph.$("svg.nvd3-svg .nv-x:contains('Undefined')").length, 1);
+ assert.strictEqual(graph.$("svg.nvd3-svg:contains('January')").length, 1);
+
+ graph.destroy();
+ });
+
+ QUnit.test('Undefined should appear in bar, pie graph but not in line graph with multiple groupbys', function (assert) {
+ assert.expect(6);
+
+ var graph = createView({
+ View: GraphView,
+ model: "foo",
+ groupBy:['date', 'color_id'],
+ data: this.data,
+ arch: '<graph string="Partners" type="line">' +
+ '<field name="bar"/>' +
+ '</graph>',
+ });
+
+ assert.strictEqual(graph.$("svg.nvd3-svg .nv-x:contains('Undefined')").length, 0);
assert.strictEqual(graph.$("svg.nvd3-svg:contains('January')").length, 1);
+ graph.$buttons.find('.o_graph_button[data-mode=bar]').click();
+ assert.strictEqual(graph.$("svg.nvd3-svg .nv-x:contains('Undefined')").length, 1);
+ assert.strictEqual(graph.$("svg.nvd3-svg:contains('January')").length, 1);
+
+ // Undefined should not appear after switching back to line chart
+ graph.$buttons.find('.o_graph_button[data-mode=line]').click();
+ assert.strictEqual(graph.$("svg.nvd3-svg .nv-x:contains('Undefined')").length, 0);
+ assert.strictEqual(graph.$("svg.nvd3-svg:contains('January')").length, 1);
graph.destroy();
});
});
diff --git a/addons/web/static/tests/views/kanban_mobile_tests.js b/addons/web/static/tests/views/kanban_mobile_tests.js
index 98e65d1ea9c..a9eec223143 100644
--- a/addons/web/static/tests/views/kanban_mobile_tests.js
+++ b/addons/web/static/tests/views/kanban_mobile_tests.js
@@ -56,7 +56,7 @@ QUnit.module('Views', {
QUnit.module('KanbanView Mobile');
QUnit.test('mobile grouped rendering', function (assert) {
- assert.expect(9);
+ assert.expect(11);
var kanban = createView({
View: KanbanView,
@@ -75,6 +75,8 @@ QUnit.module('Views', {
assert.containsN(kanban, '.o_kanban_group', 2, "should have 2 columns" );
assert.hasClass(kanban.$('.o_kanban_mobile_tab:first'),'o_current',
"first tab is the active tab with class 'o_current'");
+ assert.hasClass(kanban.$('.o_kanban_group:first'),'o_current',
+ "first column is the active column with class 'o_current'");
assert.containsN(kanban, '.o_kanban_group:first > div.o_kanban_record', 2,
"there are 2 records in active tab");
assert.strictEqual(kanban.$('.o_kanban_group:nth(1) > div.o_kanban_record').length, 0,
@@ -89,6 +91,8 @@ QUnit.module('Views', {
kanban.$('.o_kanban_mobile_tab:nth(1)').trigger('click');
assert.hasClass(kanban.$('.o_kanban_mobile_tab:nth(1)'),'o_current',
"second tab is now active with class 'o_current'");
+ assert.hasClass(kanban.$('.o_kanban_group:nth(1)'),'o_current',
+ "second column is now active with class 'o_current'");
assert.strictEqual(kanban.$('.o_kanban_group:nth(1) > div.o_kanban_record').length, 2,
"the 2 records of the second group have now been loaded");
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index c398e9df09f..6702a9bee37 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -465,7 +465,7 @@ QUnit.module('Views', {
});
QUnit.test('quick create record with quick_create_view', function (assert) {
- assert.expect(17);
+ assert.expect(19);
var kanban = createView({
View: KanbanView,
@@ -497,6 +497,7 @@ QUnit.module('Views', {
},
});
+ assert.containsOnce(kanban, '.o_cp_controller', 'should have one control panel');
assert.containsOnce(kanban, '.o_kanban_group:first .o_kanban_record',
"first column should contain one record");
@@ -508,6 +509,7 @@ QUnit.module('Views', {
"should have a quick create element in the first column");
assert.strictEqual($quickCreate.find('.o_form_view.o_xxs_form_view').length, 1,
"should have rendered an XXS form view");
+ assert.containsOnce(kanban, '.o_cp_controller', 'should not have instantiated an extra control panel');
assert.strictEqual($quickCreate.find('input').length, 2,
"should have two inputs");
assert.strictEqual($quickCreate.find('.o_field_widget').length, 3,
@@ -2376,9 +2378,60 @@ QUnit.module('Views', {
kanban.destroy();
});
+ QUnit.test('wait x2manys batch fetches to re-render', function (assert) {
+ assert.expect(4);
+ var done = assert.async();
+
+ var def;
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<field name="product_id"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="category_ids" widget="many2many_tags"/>' +
+ '</div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['product_id'],
+ mockRPC: function (route, args) {
+ var result = this._super(route, args);
+ if (args.method === 'read') {
+ return $.when(def).then(function() {
+ return result;
+ });
+ }
+ return result;
+ },
+ });
+
+ def = $.Deferred();
+ assert.containsN(kanban, '.o_tag', 2);
+ kanban.update({groupBy: ['state']});
+
+ def.resolve().then(function () {
+ assert.containsN(kanban, '.o_tag', 2,
+ 'Should display 2 tags after update');
+ assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_tag').text(),
+ 'gold', 'First category should be \'gold\'');
+ assert.strictEqual(kanban.$('.o_kanban_group:eq(2) .o_tag').text(),
+ 'silver', 'Second category should be \'silver\'');
+ kanban.destroy();
+ done();
+ });
+ });
+
QUnit.test('can drag and drop a record from one column to the next', function (assert) {
assert.expect(9);
+ // @todo: remove this resequenceDef whenever the jquery upgrade branch
+ // is merged. This is currently necessary to simulate the reality: we
+ // need the click handlers to be executed after the end of the drag and
+ // drop operation, not before.
+ var resequenceDef = $.Deferred();
+
var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test
this.data.partner.fields.sequence = {type: 'number', string: "Sequence"};
var kanban = createView({
@@ -2388,7 +2441,7 @@ QUnit.module('Views', {
arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
'<field name="product_id"/>' +
'<templates><t t-name="kanban-box">' +
- '<div><field name="foo"/>' +
+ '<div class="oe_kanban_global_click"><field name="foo"/>' +
'<t t-if="widget.editable"><span class="thisiseditable">edit</span></t>' +
'</div>' +
'</t></templates>' +
@@ -2397,7 +2450,7 @@ QUnit.module('Views', {
mockRPC: function (route, args) {
if (route === '/web/dataset/resequence') {
assert.ok(true, "should call resequence");
- return $.when(true);
+ return resequenceDef.then(_.constant(true));
}
return this._super(route, args);
},
@@ -2410,13 +2463,15 @@ QUnit.module('Views', {
var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first');
var $group = kanban.$('.o_kanban_group:nth-child(2)');
envIDs = [3, 2, 4, 1]; // first record of first column moved to the bottom of second column
- testUtils.dom.dragAndDrop($record, $group);
+ testUtils.dom.dragAndDrop($record, $group, {withTrailingClick: true});
+ resequenceDef.resolve();
assert.containsOnce(kanban, '.o_kanban_group:nth-child(1) .o_kanban_record');
assert.containsN(kanban, '.o_kanban_group:nth-child(2) .o_kanban_record', 3);
assert.containsN(kanban, '.thisiseditable', 4);
assert.deepEqual(kanban.exportState().resIds, envIDs);
+ resequenceDef.resolve();
kanban.destroy();
});
diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js
index 46c28529bed..930fadbbb7b 100644
--- a/addons/web/static/tests/views/pivot_tests.js
+++ b/addons/web/static/tests/views/pivot_tests.js
@@ -149,6 +149,23 @@ QUnit.module('Views', {
pivot.destroy();
});
+ QUnit.test('pivot rendering with string attribute on non stored field', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.fubar = {string: "Fubar", type: "integer", store:false};
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot string="Partners">' +
+ '<field name="fubar" string="fubar" type="measure"/>' +
+ '</pivot>',
+ });
+ assert.containsOnce(pivot, '.o_pivot', 'Non stored fields can have a string attribute');
+ pivot.destroy();
+ });
+
QUnit.test('pivot rendering with invisible attribute on field', function (assert) {
assert.expect(2);
// when invisible, a field should neither be an active measure,
@@ -1415,7 +1432,7 @@ QUnit.module('Views', {
actionManager.destroy();
});
- QUnit.test('export data in excel with comparison', function (assert) {
+ QUnit.test('export data in excel with comparison', function (assert) {
assert.expect(10);
this.data.partner.records[0].date = '2016-12-15';
@@ -1570,5 +1587,147 @@ QUnit.module('Views', {
unpatchDate();
actionManager.destroy();
});
+
+ QUnit.module('Sort in comparison mode', {
+ beforeEach: function () {
+ this.data.partner.records[0].date = '2016-12-15';
+ this.data.partner.records[1].date = '2016-12-17';
+ this.data.partner.records[2].date = '2016-11-22';
+ this.data.partner.records[3].date = '2016-11-03';
+
+ this.data.partner.fields.company_type = {string: "Company Type", type: "selection", selection: [["company", "Company"], ["individual", "individual"]], searchable: true, store: true, sortable: true};
+
+ this.data.partner.records[0].company_type = 'company';
+ this.data.partner.records[1].company_type = 'individual';
+ this.data.partner.records[2].company_type = 'company';
+ this.data.partner.records[3].company_type = 'individual';
+
+
+ this.unpatchDate = patchDate(2016, 11, 20, 1, 0, 0);
+
+ this.actualResult = function () {
+ var actualResult = $('.o_pivot .o_pivot_cell_value div').map(function() {
+ return $( this ).text();
+ }).get().join();
+ return actualResult;
+ };
+
+ // create an action manager to test the interactions with the search view
+ this.actionManager = createActionManager({
+ data: this.data,
+ archs: {
+ 'partner,false,pivot': '<pivot>' +
+ '<field name="date" interval="day" type="row"/>' +
+ '<field name="company_type" type="col"/>' +
+ '<field name="foo" type="measure"/>' +
+ '</pivot>',
+ 'partner,false,search': '<search></search>',
+ },
+ debug: 1,
+ });
+
+ this.actionManager.doAction({
+ res_model: 'partner',
+ type: 'ir.actions.act_window',
+ views: [[false, 'pivot']],
+ flags: {
+ pivot: {
+ additionalMeasures: ['product_id'],
+ }
+ }
+ });
+
+ // open time range menu
+ testUtils.dom.click($('.o_control_panel .o_time_range_menu_button'));
+ // select 'Today' as range
+ testUtils.fields.editInput($('.o_control_panel .o_time_range_selector'), 'this_month');
+ // check checkbox 'Compare To'
+ testUtils.dom.click($('.o_control_panel .o_time_range_menu .o_comparison_checkbox'));
+ // Click on 'Apply' button
+ testUtils.dom.click($('.o_control_panel .o_time_range_menu .o_apply_range'));
+ // We are in comparison mode
+ },
+ afterEach: function () {
+ this.unpatchDate();
+ this.actionManager.destroy();
+ },
+ }, function () {
+ QUnit.test('when clicking on measure, sort by "This Month" (period of interest)', function (assert) {
+ assert.expect(1);
+
+ // click on 'Foo' in column Total/Company
+ testUtils.dom.click($('.o_pivot_measure_row').eq(0));
+ var results = [
+ "12", "17", "-29.41%", "1", "2", "-50%", "13", "19", "-31.58%",
+ "12", "0", "100%", "12", "0" , "100%",
+ "1", "0", "100%", "1" , "0", "100%",
+ "0", "17", "-100%", "0", "17", "-100%",
+ "0", "2", "-100%", "0" , "2" , "-100%"
+ ];
+ assert.strictEqual(this.actualResult(), results.join());
+ });
+
+ QUnit.test(
+ 'click on a period of interest header sort according to the appropriate column ' +
+ 'first in ascending order then in descending order',
+ function (assert) {
+ assert.expect(2);
+
+ // click on 'This Month' in column Total/Individual/Foo
+ testUtils.dom.click($('.o_pivot_measure_row').eq(6));
+ var results = [
+ "12", "17", "-29.41%", "1", "2", "-50%", "13", "19", "-31.58%",
+ "1", "0", "100%", "1" , "0", "100%",
+ "12", "0", "100%", "12", "0" , "100%",
+ "0", "17", "-100%", "0", "17", "-100%",
+ "0", "2", "-100%", "0" , "2" , "-100%"
+ ];
+ var actualResult = $('.o_pivot .o_pivot_cell_value div').text();
+ assert.strictEqual(actualResult, results.join(''));
+
+ // click once again on 'This Month' in column Total/Individual/Foo
+ testUtils.dom.click($('.o_pivot_measure_row').eq(6));
+ results = [
+ "12", "17", "-29.41%", "1", "2", "-50%", "13", "19", "-31.58%",
+ "12", "0", "100%", "12", "0" , "100%",
+ "0", "17", "-100%", "0", "17", "-100%",
+ "0", "2", "-100%", "0" , "2" , "-100%",
+ "1", "0", "100%", "1" , "0", "100%"
+ ];
+ assert.strictEqual(this.actualResult(), results.join());
+ });
+
+ QUnit.test('click on a period of comparison header sort according to appropriate column',
+ function (assert) {
+ assert.expect(1);
+
+ // click on 'Previous Period' in column Total/Individual/Foo
+ testUtils.dom.click($('.o_pivot_measure_row').eq(7));
+ var results = [
+ "12", "17", "-29.41%", "1", "2", "-50%", "13", "19", "-31.58%",
+ "0", "2", "-100%", "0" , "2" , "-100%",
+ "12", "0", "100%", "12", "0" , "100%",
+ "1", "0", "100%", "1" , "0", "100%",
+ "0", "17", "-100%", "0", "17", "-100%"
+ ];
+ assert.strictEqual(this.actualResult(), results.join());
+ });
+
+ QUnit.test('click on a variation header sort according to appropriate column',
+ function (assert) {
+ assert.expect(1);
+
+ // click on 'Variation' in column Total/Individual/Foo
+ testUtils.dom.click($('.o_pivot_measure_row').eq(11));
+ var results = [
+ "12", "17", "-29.41%", "1", "2", "-50%", "13", "19", "-31.58%",
+ "12", "0", "100%", "12", "0" , "100%",
+ "1", "0", "100%", "1" , "0", "100%",
+ "0", "17", "-100%", "0", "17", "-100%",
+ "0", "2", "-100%", "0" , "2" , "-100%"
+ ];
+ assert.strictEqual(this.actualResult(), results.join());
+ });
+ });
});
});
diff --git a/addons/web/static/tests/views/search_filters_menu_tests.js b/addons/web/static/tests/views/search_filters_menu_tests.js
deleted file mode 100644
index e9246520f1d..00000000000
--- a/addons/web/static/tests/views/search_filters_menu_tests.js
+++ /dev/null
@@ -1,140 +0,0 @@
-odoo.define('web.search_filters_menu_tests', function (require) {
-"use strict";
-
-var FilterMenu = require('web.FilterMenu');
-var testUtils = require('web.test_utils');
-
-function createFilterMenu(filters, fields, params) {
- params = params || {};
- var target = params.debug ? document.body : $('#qunit-fixture');
- var menu = new FilterMenu(null, filters, fields);
- testUtils.mock.addMockEnvironment(menu, params);
- menu.appendTo(target);
- return menu;
-}
-
-QUnit.module('FilterMenu', {
- beforeEach: function () {
- this.filters = [
- {
- isActive: false,
- description: 'some filter',
- domain: '',
- groupNumber: 1,
- },
- ];
- this.fields = {
- boolean_field: {string: "Boolean Field", type: "boolean", default: true, searchable: true},
- date_field: {string: "A date", type: "date", searchable: true},
- char_field: {string: "Char Field", type: "char", default: "foo", searchable: true, trim: true},
- };
- },
-}, function () {
-
- QUnit.test('simple rendering with no filter', function (assert) {
- assert.expect(4);
-
- var filterMenu = createFilterMenu([], this.fields);
- testUtils.dom.click(filterMenu.$('span.fa-filter'));
- assert.containsOnce(filterMenu, '.dropdown-divider');
- assert.ok(!filterMenu.$('.dropdown-divider').is(':visible'));
- assert.containsN(filterMenu, '.dropdown-divider, .dropdown-item, .dropdown-item-text', 3,
- 'should have 3 elements: a hidden divider, a add custom filter item, a apply button + add condition button');
- assert.containsOnce(filterMenu, '.o_add_custom_filter.o_closed_menu');
- filterMenu.destroy();
- });
-
- QUnit.test('simple rendering with a filter', function (assert) {
- assert.expect(2);
-
- var filterMenu = createFilterMenu(this.filters, this.fields);
- assert.containsN(filterMenu, '.dropdown-divider, .dropdown-item, .dropdown-item-text', 5,
- 'should have 4 elements: a hidden, separator, a filter, a separator, a add custom filter item, a apply button + add condition button');
- assert.containsOnce(filterMenu, '.o_add_custom_filter.o_closed_menu');
- filterMenu.destroy();
- });
-
- QUnit.test('click on add custom filter opens the submenu', function (assert) {
- assert.expect(3);
-
- var filterMenu = createFilterMenu([], this.fields);
- // open menu dropdown
- testUtils.dom.click(filterMenu.$('span.fa-filter'));
- // open add custom filter submenu
- testUtils.dom.click(filterMenu.$('.o_add_custom_filter'));
- assert.hasClass(filterMenu.$('.o_add_custom_filter'), 'o_open_menu');
- assert.ok(filterMenu.$('.o_add_filter_menu').is(':visible'));
- assert.containsN(filterMenu, '.dropdown-divider, .dropdown-item, .dropdown-item-text', 4,
- 'should have 3 elements: a hidden divider, a add custom filter item, a proposition, a apply button + add condition button');
-
- filterMenu.destroy();
- });
-
- QUnit.test('removing last prop disable the apply button', function (assert) {
- assert.expect(2);
-
- var filterMenu = createFilterMenu([], this.fields);
- // open menu dropdown and custom filter submenu
- testUtils.dom.click(filterMenu.$('span.fa-filter'));
- testUtils.dom.click(filterMenu.$('.o_add_custom_filter'));
-
- // remove the current unique proposition
- testUtils.dom.click(filterMenu.$('.o_searchview_extended_delete_prop'));
-
- assert.ok(filterMenu.$('.o_apply_filter').attr('disabled'));
-
- assert.containsN(filterMenu, '.dropdown-divider, .dropdown-item, .dropdown-item-text', 3,
- 'should have 3 elements: a hidden separator, a add custom filter item, a apply button + add condition button');
-
- filterMenu.destroy();
- });
-
- QUnit.test('readding a proposition reenable apply button', function (assert) {
- assert.expect(1);
-
- var filterMenu = createFilterMenu([], this.fields);
- // open menu dropdown and custom filter submenu, remove existing prop
- testUtils.dom.click(filterMenu.$('span.fa-filter'));
- testUtils.dom.click(filterMenu.$('.o_add_custom_filter'));
- testUtils.dom.click(filterMenu.$('.o_searchview_extended_delete_prop'));
- // read a proposition
- testUtils.dom.click(filterMenu.$('.o_add_condition'));
- assert.ok(!filterMenu.$('.o_apply_filter').attr('disabled'));
-
- filterMenu.destroy();
- });
-
- QUnit.test('adding a simple filter works', function (assert) {
- assert.expect(6);
-
- delete this.fields.date_field;
- var filterMenu = createFilterMenu([], this.fields, {
- debug: true,
- intercepts: {
- new_filters: function (ev) {
- var filter = ev.data.filters[0];
- assert.strictEqual(filter.type, 'filter');
- assert.strictEqual(filter.description, 'Boolean Field is true');
- assert.strictEqual(filter.domain, '[[\"boolean_field\",\"=\",True]]');
- filterMenu.update([{
- isActive: true,
- description: '?',
- domain: '?',
- groupNumber: 1,
- }]);
- },
- },
- });
- // open menu dropdown and custom filter submenu, remove existing prop
- testUtils.dom.click(filterMenu.$('span.fa-filter'));
- testUtils.dom.click(filterMenu.$('.o_add_custom_filter'));
- // click on apply to activate filter
- testUtils.dom.click(filterMenu.$('.o_apply_filter'));
- assert.hasClass(filterMenu.$('.o_add_custom_filter'), 'o_closed_menu');
- assert.containsNone(filterMenu, '.o_filter_condition');
- assert.containsOnce(filterMenu, '.dropdown-divider:visible',
- 'there should be a separator between filters and add custom filter menu');
- filterMenu.destroy();
- });
-});
-});
diff --git a/addons/web/static/tests/views/search_view_tests.js b/addons/web/static/tests/views/search_view_tests.js
index fa3fe817c2a..f4b0163ce07 100644
--- a/addons/web/static/tests/views/search_view_tests.js
+++ b/addons/web/static/tests/views/search_view_tests.js
@@ -2,6 +2,7 @@ odoo.define('web.search_view_tests', function (require) {
"use strict";
var FormView = require('web.FormView');
+var ListView = require('web.ListView');
var testUtils = require('web.test_utils');
var createActionManager = testUtils.createActionManager;
@@ -694,6 +695,39 @@ QUnit.module('Search View', {
window.Date = RealDate;
});
+ QUnit.test('`context` key in <filter> is used', function (assert) {
+ assert.expect(3);
+
+ this.archs['partner,4,search'] = '<search>'+
+ '<filter string="AAA" name="some_filter" context="{\'coucou_1\': 1}"></filter>' +
+ '</search>';
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/search_read') {
+ assert.step(JSON.stringify(args.context));
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ actionManager.doAction(5);
+
+ // select filter
+ testUtils.dom.click($('.o_search_options .fa-filter'));
+ testUtils.dom.click($('.o_menu_item:contains(AAA)'));
+
+ assert.verifySteps([
+ "{}",
+ "{\"coucou_1\":1}",
+ ]);
+
+ actionManager.destroy();
+ });
+
QUnit.module('Favorites Menu');
QUnit.test('dynamic filters are saved dynamic', function (assert) {