summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-06-27 14:19:31 +0200
committerChristophe Simonis <chs@odoo.com>2017-06-27 14:19:31 +0200
commitdd43071638773dcb58376b79ec7b1456c8b0454d (patch)
treefd7935e2aa93de03fa3fe12507f8ef74405a255e /addons/web/static/src
parent6501184e6294ffb06e24a0ec8e9666b79a80b59a (diff)
parent278e478d55209bd958836fde7fe1c82c555180dc (diff)
[MERGE] forward port branch saas-16 up to 278e478d552
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/js/chrome/search_filters.js4
-rw-r--r--addons/web/static/src/js/chrome/view_manager.js2
-rw-r--r--addons/web/static/src/js/core/pyeval.js16
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js30
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js149
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js22
-rw-r--r--addons/web/static/src/js/fields/upgrade_fields.js6
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js4
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js18
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js1
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_column.js10
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_model.js13
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js8
-rw-r--r--addons/web/static/src/less/datepicker.less9
-rw-r--r--addons/web/static/src/xml/base.xml18
15 files changed, 196 insertions, 114 deletions
diff --git a/addons/web/static/src/js/chrome/search_filters.js b/addons/web/static/src/js/chrome/search_filters.js
index 69c38056e4a..a0e3c1c077f 100644
--- a/addons/web/static/src/js/chrome/search_filters.js
+++ b/addons/web/static/src/js/chrome/search_filters.js
@@ -155,6 +155,7 @@ ExtendedSearchProposition.Field = Widget.extend({
ExtendedSearchProposition.Char = ExtendedSearchProposition.Field.extend({
tagName: 'input',
+ className: 'o_input',
attributes: {
type: 'text'
},
@@ -186,7 +187,7 @@ ExtendedSearchProposition.DateTime = ExtendedSearchProposition.Field.extend({
{value: "∄", text: _lt("is not set")}
],
get_value: function (index) {
- return this["datewidget_" + (index || 0)].get_value();
+ return this["datewidget_" + (index || 0)].getValue();
},
get_domain: function (field, operator) {
switch (operator.value) {
@@ -251,6 +252,7 @@ ExtendedSearchProposition.Date = ExtendedSearchProposition.DateTime.extend({
ExtendedSearchProposition.Integer = ExtendedSearchProposition.Field.extend({
tagName: 'input',
+ className: 'o_input',
attributes: {
type: 'number',
value: '0',
diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js
index 8d760c568b0..d0beb5bf3dd 100644
--- a/addons/web/static/src/js/chrome/view_manager.js
+++ b/addons/web/static/src/js/chrome/view_manager.js
@@ -307,7 +307,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
self.trigger('switch_mode', view_type, view_options);
}).fail(function(e) {
if (!(e && e.code === 200 && e.data.exception_type)) {
- self.do_warn(_t("Error"), view.controller.display_name + _t(" view couldn't be loaded"));
+ self.do_warn(_t("Error"), view.label + _t(" view couldn't be loaded"));
}
// Restore internal state
self.active_view = old_view;
diff --git a/addons/web/static/src/js/core/pyeval.js b/addons/web/static/src/js/core/pyeval.js
index 75efc9e813f..502c6a4ce90 100644
--- a/addons/web/static/src/js/core/pyeval.js
+++ b/addons/web/static/src/js/core/pyeval.js
@@ -435,6 +435,22 @@ datetime.datetime = py.type('datetime', null, {
this.second,
this.microsecond / 1000);
},
+ __add__: function (other) {
+ if (!py.PY_isInstance(other, datetime.timedelta)) {
+ return py.NotImplemented;
+ }
+ var s = tmxxx(this.year, this.month, this.day + other.days, this.hour, this.minute, this.second + other.seconds);
+ return datetime.datetime.fromJSON(s.year, s.month, s.day, s.hour, s.minute, s.second);
+ },
+ __sub__: function (other) {
+ if (py.PY_isInstance(other, datetime.timedelta)) {
+ return py.PY_add(this, py.PY_negative(other));
+ }
+ return py.NotImplemented;
+ },
+ fromJSON: function (year, month, day, hour, minute, second) {
+ return py.PY_call(datetime.datetime, [year, month, day, hour, minute, second]);
+ },
});
datetime.date = py.type('date', null, {
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index 492d9f26552..52e0aef77e4 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -142,6 +142,21 @@ var AbstractField = Widget.extend({
// this is the last value that was set by the user, unparsed. This is
// used to avoid setting the value twice in a row with the exact value.
this.lastSetValue = undefined;
+
+ // formatType is used to determine which format (and parse) functions
+ // to call to format the field's value to insert into the DOM (typically
+ // put into a span or an input), and to parse the value from the input
+ // to send it to the server. These functions are chosen according to
+ // the 'widget' attrs if is is given, and if it is a valid key, with a
+ // fallback on the field type, ensuring that the value is formatted and
+ // displayed according to the choosen widget, if any.
+ this.formatType = this.attrs.widget in field_utils.format ?
+ this.attrs.widget :
+ this.field.type;
+ // formatOptions (resp. parseOptions) is a dict of options passed to
+ // calls to the format (resp. parse) function.
+ this.formatOptions = {};
+ this.parseOptions = {};
},
/**
* When a field widget is appended to the DOM, its start method is called,
@@ -266,15 +281,15 @@ var AbstractField = Widget.extend({
//--------------------------------------------------------------------------
/**
- * convert the value from the field to a string representation
+ * Converts the value from the field to a string representation.
*
* @private
* @param {any} value (from the field type)
* @returns {string}
*/
_formatValue: function (value) {
- var options = _.extend({}, this.nodeOptions, { data: this.recordData });
- return field_utils.format[this.field.type](value, this.field, options);
+ var options = _.extend({}, this.nodeOptions, { data: this.recordData }, this.formatOptions);
+ return field_utils.format[this.formatType](value, this.field, options);
},
/**
* This method check if a value is the same as the current value of the
@@ -291,15 +306,14 @@ var AbstractField = Widget.extend({
return this.value === value;
},
/**
- * convert a string representation to a valid value, depending on the field
- * type.
+ * Converts a string representation to a valid value.
*
* @private
* @param {string} value
* @returns {any}
*/
_parseValue: function (value) {
- return field_utils.parse[this.field.type](value, this.field);
+ return field_utils.parse[this.formatType](value, this.field, this.parseOptions);
},
/**
* main rendering function. Override this if your widget has the same render
@@ -426,22 +440,18 @@ var AbstractField = Widget.extend({
this.trigger_up('navigation_move', {direction: 'cancel'});
break;
case $.ui.keyCode.UP:
- ev.preventDefault();
ev.stopPropagation();
this.trigger_up('navigation_move', {direction: 'up'});
break;
case $.ui.keyCode.RIGHT:
- ev.preventDefault();
ev.stopPropagation();
this.trigger_up('navigation_move', {direction: 'right'});
break;
case $.ui.keyCode.DOWN:
- ev.preventDefault();
ev.stopPropagation();
this.trigger_up('navigation_move', {direction: 'down'});
break;
case $.ui.keyCode.LEFT:
- ev.preventDefault();
ev.stopPropagation();
this.trigger_up('navigation_move', {direction: 'left'});
break;
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index d4013a8bcdb..82970255b3e 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -285,11 +285,15 @@ var InputField = DebouncedField.extend({
*/
_onNavigationMove: function (ev) {
this._super.apply(this, arguments);
- var input = this.$input[0];
- var selecting = (input.selectionEnd !== input.selectionStart);
- if ((ev.data.direction === "left" && (selecting || input.selectionStart !== 0))
- || (ev.data.direction === "right" && (selecting || input.selectionStart !== input.value.length))) {
- ev.stopPropagation();
+
+ // the following code only makes sense in edit mode, with an input
+ if (this.mode === 'edit') {
+ var input = this.$input[0];
+ var selecting = (input.selectionEnd !== input.selectionStart);
+ if ((ev.data.direction === "left" && (selecting || input.selectionStart !== 0))
+ || (ev.data.direction === "right" && (selecting || input.selectionStart !== input.value.length))) {
+ ev.stopPropagation();
+ }
}
},
});
@@ -322,6 +326,14 @@ var FieldDate = InputField.extend({
supportedFieldTypes: ['date'],
/**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ // use the session timezone when formatting dates
+ this.formatOptions.timezone = true;
+ },
+ /**
* In edit mode, instantiates a DateWidget datepicker and listen to changes.
*
* @override
@@ -396,17 +408,6 @@ var FieldDate = InputField.extend({
this.datewidget.setValue(this.value);
this.$input = this.datewidget.$input;
},
-
- /**
- * FieldDateTime overrides _formatValue to use the session timezone
- *
- * @override
- * @private
- * @param {Moment} value
- */
- _formatValue: function (value) {
- return field_utils.format.date(value, {timezone: true});
- },
});
var FieldDateTime = FieldDate.extend({
@@ -448,17 +449,6 @@ var FieldDateTime = FieldDate.extend({
this.datewidget.setValue(value);
this.$input = this.datewidget.$input;
},
-
- /**
- * FieldDateTime overrides _formatValue to use the session timezone
- *
- * @override
- * @private
- * @param {Moment} value
- */
- _formatValue: function (value) {
- return field_utils.format.datetime(value, {timezone: true});
- },
});
var FieldMonetary = InputField.extend({
@@ -490,6 +480,13 @@ var FieldMonetary = InputField.extend({
this.tagName = 'div';
this.className += ' o_input';
}
+
+ // use the formatFloat function in edit
+ if (this.mode === 'edit') {
+ this.formatType = 'float';
+ }
+ this.formatOptions.currency = this.currency;
+ this.formatOptions.digits = [16, 2];
},
//--------------------------------------------------------------------------
@@ -562,22 +559,6 @@ var FieldMonetary = InputField.extend({
var currencyID = this.record.data[currencyField] && this.record.data[currencyField].res_id;
this.currency = session.get_currency(currencyID);
},
- /**
- * FieldMonetary overrides _formatValue to use the format monetary method
- * in readonly.
- *
- * @override
- * @private
- * @param {float} value
- * @returns {string}
- */
- _formatValue: function (value) {
- var format = field_utils.format[this.mode === 'edit' ? "float" : "monetary"];
- return format(value, this.field, {
- digits: [16, 2],
- currency: this.currency,
- });
- },
});
var FieldBoolean = AbstractField.extend({
@@ -686,6 +667,11 @@ var FieldBoolean = AbstractField.extend({
this.$input.prop('checked', !this.value);
this._setValue(!this.value);
return;
+ case $.ui.keyCode.UP:
+ case $.ui.keyCode.RIGHT:
+ case $.ui.keyCode.DOWN:
+ case $.ui.keyCode.LEFT:
+ ev.preventDefault();
}
this._super.apply(this, arguments);
},
@@ -734,7 +720,7 @@ var FieldInteger = InputField.extend({
}
return value;
}
- return field_utils.format.integer(value, this.field);
+ return this._super.apply(this, arguments);
},
});
@@ -771,31 +757,11 @@ var FieldFloat = InputField.extend({
});
var FieldFloatTime = FieldFloat.extend({
- supportedFieldTypes: ['float'],
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Format the float time value into a human-readable time format.
- *
- * @override
- * @private
- */
- _formatValue: function (value) {
- return field_utils.format.float_time(value);
- },
-
- /**
- * Parse the human-readable formatted time value into a float.
- *
- * @override
- * @private
- */
- _parseValue: function (value) {
- return field_utils.parse.float_time(value);
- },
+ // this is not strictly necessary, as for this widget to be used, the 'widget'
+ // attrs must be set to 'float_time', so the formatType is automatically
+ // 'float_time', but for the sake of clarity, we explicitely define a
+ // FieldFloatTime widget with formatType = 'float_time'.
+ formatType: 'float_time',
});
var FieldText = InputField.extend(TranslatableFieldMixin, {
@@ -853,6 +819,19 @@ var FieldEmail = InputField.extend({
},
//--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns the associated link.
+ *
+ * @override
+ */
+ getFocusableElement: function () {
+ return this.mode === 'readonly' ? this.$el : this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -887,6 +866,17 @@ var FieldPhone = FieldEmail.extend({
this.tagName = 'span';
}
},
+ /**
+ * Returns the associated link only if there is one.
+ *
+ * @override
+ */
+ getFocusableElement: function () {
+ if (this._canCall()) {
+ return this._super.apply(this, arguments);
+ }
+ return $();
+ },
//--------------------------------------------------------------------------
// Private
@@ -941,6 +931,19 @@ var UrlWidget = InputField.extend({
},
//--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns the associated link.
+ *
+ * @override
+ */
+ getFocusableElement: function () {
+ return this.mode === 'readonly' ? this.$el : this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -1041,7 +1044,13 @@ var AbstractFieldBinary = AbstractField.extend({
set_filename: function (value) {
var filename = this.attrs.filename;
if (filename && filename in this.fields) {
- this.trigger_up('update_field', { name: filename, value: value });
+ var changes = {};
+ changes[filename] = value;
+ this.trigger_up('field_changed', {
+ dataPointID: this.dataPointID,
+ changes: changes,
+ viewType: this.viewType,
+ });
}
},
on_clear: function () {
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 3b47b8c64b0..419d1f5f646 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -124,7 +124,7 @@ var FieldMany2One = AbstractField.extend({
* @returns {jQuery}
*/
getFocusableElement: function () {
- return this.$input || $();
+ return this.mode === 'edit' && this.$input || this.$el;
},
/**
* TODO
@@ -206,6 +206,17 @@ var FieldMany2One = AbstractField.extend({
return [];
},
/**
+ * Returns the display_name from a string which contains it but was altered
+ * as a result of the show_address option using a horrible hack.
+ *
+ * @private
+ * @param {string} value
+ * @returns {string} display_name without show_address mess
+ */
+ _getDisplayName: function (value) {
+ return value.split('\n')[0];
+ },
+ /**
* @private
* @param {string} name
*/
@@ -244,7 +255,7 @@ var FieldMany2One = AbstractField.extend({
// and hope for the best that noone tries to uses this mechanism to do
// something else.
if (this.nodeOptions.always_reload) {
- value = value.split('\n')[0];
+ value = this._getDisplayName(value);
}
this.$input.val(value);
if (!this.autocomplete_bound) {
@@ -302,7 +313,7 @@ var FieldMany2One = AbstractField.extend({
.then(function (result) {
// possible selections for the m2o
var values = _.map(result, function (x) {
- x[1] = x[1].split("\n")[0];
+ x[1] = self._getDisplayName(x[1]);
return {
label: _.str.escapeHTML(x[1].trim()) || data.noDisplayContent,
value: x[1],
@@ -478,7 +489,7 @@ var FieldMany2One = AbstractField.extend({
_onInputKeyup: function () {
if (this.$input.val() === "") {
this.reinitialize(false);
- } else if (this.m2o_value !== this.$input.val()) {
+ } else if (this._getDisplayName(this.m2o_value) !== this.$input.val()) {
this.floating = true;
this._updateExternalButton();
}
@@ -1585,7 +1596,7 @@ var FormFieldMany2ManyTags = FieldMany2ManyTags.extend({
var KanbanFieldMany2ManyTags = FieldMany2ManyTags.extend({
_render: function () {
var self = this;
- this.$el.addClass('o_field_many2manytags o_kanban_tags');
+ this.$el.empty().addClass('o_field_many2manytags o_kanban_tags');
_.each(this.value.data, function (m2m) {
// 10th color is invisible
if ('color' in m2m.data && m2m.data.color !== 10) {
@@ -1763,6 +1774,7 @@ var FieldSelection = AbstractField.extend({
this._super.apply(this, arguments);
if (this.field.type === 'many2one') {
this.values = this.record.specialData[this.name];
+ this.formatType = 'many2one';
} else {
this.values = _.reject(this.field.selection, function (v) {
return v[0] === false && v[1] === '';
diff --git a/addons/web/static/src/js/fields/upgrade_fields.js b/addons/web/static/src/js/fields/upgrade_fields.js
index ecf267e03c1..ae8cae56ef3 100644
--- a/addons/web/static/src/js/fields/upgrade_fields.js
+++ b/addons/web/static/src/js/fields/upgrade_fields.js
@@ -6,6 +6,7 @@ odoo.define('web.upgrade_widgets', function (require) {
* When checked, an upgrade popup is showed to the user.
*/
+var AbstractField = require('web.AbstractField');
var basic_fields = require('web.basic_fields');
var core = require('web.core');
var Dialog = require('web.Dialog');
@@ -58,6 +59,7 @@ var AbstractFieldUpgrade = {
* Opens the Upgrade dialog.
*
* @private
+ * @returns {Dialog} the instance of the opened Dialog
*/
_openDialog: function () {
var message = $(QWeb.render('EnterpriseUpgrade'));
@@ -75,7 +77,7 @@ var AbstractFieldUpgrade = {
},
];
- new Dialog(this, {
+ return new Dialog(this, {
size: 'medium',
buttons: buttons,
$content: $('<div>', {
@@ -122,7 +124,7 @@ var AbstractFieldUpgrade = {
var UpgradeBoolean = FieldBoolean.extend(AbstractFieldUpgrade, {
supportedFieldTypes: [],
- events: _.extend({}, FieldBoolean.prototype.events, {
+ events: _.extend({}, AbstractField.prototype.events, {
'click input': '_onInputClicked',
}),
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index af4ca7d7a08..67a4fa504d8 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -423,7 +423,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
});
if (!options.stayInEdit) {
saveDef = saveDef.then(function (fieldNames) {
- var def = fieldNames.length ? self._confirmSave(recordID) : self._setMode('readonly');
+ var def = fieldNames.length ? self._confirmSave(recordID) : self._setMode('readonly', recordID);
return def.then(function () {
return fieldNames;
});
@@ -534,6 +534,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @param {function} ev.data.action the function to execute in the mutex
*/
_onMutexify: function (ev) {
+ ev.stopPropagation(); // prevent other controllers from handling this request
this.mutex.exec(ev.data.action);
},
/**
@@ -578,6 +579,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @param {OdooEvent} event
*/
_onTranslate: function (event) {
+ event.stopPropagation();
var record = this.model.get(event.data.id, {raw: true});
this._rpc({
route: '/web/dataset/call_button',
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 d897a74c696..716624f368b 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -1399,7 +1399,8 @@ var BasicModel = AbstractModel.extend({
* @param {string} [optinos.viewType] the type of view for which the record
* is fetched (usefull to load the adequate fields), by defaults, uses
* record.viewType
- * @returns {Deferred -> Object} resolves to the record
+ * @returns {Deferred -> Object} resolves to the record or is rejected in
+ * case no id given were valid ids
*/
_fetchRecord: function (record, options) {
var self = this;
@@ -1412,6 +1413,9 @@ var BasicModel = AbstractModel.extend({
context: _.extend({}, record.context, {bin_size: true}),
})
.then(function (result) {
+ if (result.length === 0) {
+ return $.Deferred().reject();
+ }
result = result[0];
record.data = _.extend({}, record.data, result);
})
@@ -2194,12 +2198,6 @@ var BasicModel = AbstractModel.extend({
if (!field || field.name === 'id') {
continue;
}
- if (field.type === 'float' ||
- field.type === 'integer' ||
- field.type === 'monetary') {
- context[fieldName] = context[fieldName] || 0;
- continue;
- }
if (field.type === 'date' || field.type === 'datetime') {
if (context[fieldName]) {
context[fieldName] = JSON.parse(JSON.stringify(context[fieldName]));
@@ -2445,7 +2443,11 @@ var BasicModel = AbstractModel.extend({
var fieldName = fieldNames[i];
if (!(fieldName in result)) {
var field = params.fields[fieldName];
- if (field.type === 'one2many' || field.type === 'many2many') {
+ if (field.type === 'float' ||
+ field.type === 'integer' ||
+ field.type === 'monetary') {
+ result[fieldName] = 0;
+ } else if (field.type === 'one2many' || field.type === 'many2many') {
result[fieldName] = [];
} else {
result[fieldName] = null;
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index 08f28438368..25b72e4fb5a 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -828,6 +828,7 @@ var FormRenderer = BasicRenderer.extend({
*/
_onNavigationMove: function (ev) {
ev.stopPropagation();
+
var index;
if (ev.data.direction === "next") {
index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target);
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 99c9500d210..93165066614 100644
--- a/addons/web/static/src/js/views/kanban/kanban_column.js
+++ b/addons/web/static/src/js/views/kanban/kanban_column.js
@@ -40,8 +40,6 @@ var KanbanColumn = Widget.extend({
var value = data.value;
this.id = data.res_id || value;
- // todo: handle group_by_m2o (nameget)
- this.title = value === undefined ? _t('Undefined') : value;
this.folded = !data.isOpen;
this.has_active_field = 'active' in data.fields;
this.size = data.count;
@@ -62,6 +60,14 @@ var KanbanColumn = Widget.extend({
this.record_options = _.clone(recordOptions);
+ if (options.grouped_by_m2o) {
+ // For many2one, a false value means that the field is not set.
+ this.title = value ? value : _t('Undefined');
+ } else {
+ // False and 0 might be valid values for these fields.
+ this.title = value === undefined ? _t('Undefined') : value;
+ }
+
if (options.group_by_tooltip) {
this.tooltipInfo = _.map(options.group_by_tooltip, function (help, field) {
return (data.tooltipData && data.tooltipData[field] && "<div>" + help + "<br>" + data.tooltipData[field] + "</div>") || '';
diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js
index cd69889bda9..3977ac0df87 100644
--- a/addons/web/static/src/js/views/kanban/kanban_model.js
+++ b/addons/web/static/src/js/views/kanban/kanban_model.js
@@ -153,7 +153,7 @@ var KanbanModel = BasicModel.extend({
}).then(function () {
// Remove record from its current group
var old_group;
- for (var i = 0; i < parent.count; i++) {
+ for (var i = 0; i < parent.data.length; i++) {
old_group = self.localData[parent.data[i]];
var index = _.indexOf(old_group.data, recordID);
if (index >= 0) {
@@ -244,9 +244,14 @@ var KanbanModel = BasicModel.extend({
if (groupedByField.type !== 'many2one') {
return $.when();
}
- var groupIds = _.map(list.data, function (id) {
- return self.get(id, {raw: true}).res_id;
- });
+ var groupIds = _.reduce(list.data, function (groupIds, id) {
+ var res_id = self.get(id, {raw: true}).res_id;
+ // The field on which we are grouping might not be set on all records
+ if (res_id) {
+ groupIds.push(res_id);
+ }
+ return groupIds;
+ }, []);
var tooltipFields = [];
var groupedByFieldInfo = list.fieldsInfo.kanban[list.groupedBy[0]];
if (groupedByFieldInfo && groupedByFieldInfo.options) {
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index f713ec6e66e..a917781b580 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -271,10 +271,14 @@ var ListRenderer = BasicRenderer.extend({
_renderButton: function (record, node) {
var $button = $('<button>', {
type: 'button',
- class: 'o_icon_button',
title: node.attrs.string,
});
- $button.append($('<i>', {class: 'fa ' + node.attrs.icon}));
+ if (node.attrs.icon) {
+ $button.addClass('o_icon_button');
+ $button.append($('<i>', {class: 'fa ' + node.attrs.icon}));
+ } else {
+ $button.text(node.attrs.string);
+ }
this._registerModifiers(node, record, $button);
diff --git a/addons/web/static/src/less/datepicker.less b/addons/web/static/src/less/datepicker.less
index bc36f8c5a43..99c909efacc 100644
--- a/addons/web/static/src/less/datepicker.less
+++ b/addons/web/static/src/less/datepicker.less
@@ -28,6 +28,13 @@
}
}
+// Need to put datetimepicker widget above everything else
+// The '.dropdown-menu' part is needed to override default bootstrap rule as
+// this file is unfortunately placed in common assets.
+.bootstrap-datetimepicker-widget.dropdown-menu {
+ z-index: @zindex-modal + 1;
+}
+
.datepicker {
.table-condensed {
> thead {
@@ -66,5 +73,5 @@
}
}
}
- }
+ }
}
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index b673690d379..78cc5fd9ad5 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -961,7 +961,8 @@
<span t-if="attrs.help">?</span>
</label>
<div t-att-style="style">
- <input type="text" size="15" t-att-name="attrs.name"
+ <input type="text" size="15" class="o_input"
+ t-att-name="attrs.name"
t-att-autofocus="attrs.default_focus === '1' ? 'autofocus' : undefined"
t-att-id="element_id"
t-att-value="defaults[attrs.name] || ''"/>
@@ -990,7 +991,9 @@
<span t-if="attrs.help">?</span>
</label>
<div t-att-style="style">
- <select t-att-name="attrs.name" t-att-id="element_id"
+ <select class="o_input"
+ t-att-name="attrs.name"
+ t-att-id="element_id"
t-att-autofocus="attrs.default_focus === '1' || undefined">
<option t-if="prepend_empty"/>
<t t-foreach="attrs.selection" t-as="option">
@@ -1012,7 +1015,7 @@
<li class="o_filter_condition">
<span class="o_or_filter">or</span>
<span>
- <select class="o_searchview_extended_prop_field">
+ <select class="o_input o_searchview_extended_prop_field">
<t t-foreach="widget.attrs.fields" t-as="field">
<option t-att="{'selected': field === widget.attrs.selected ? 'selected' : null}"
t-att-value="field.name">
@@ -1022,18 +1025,19 @@
</select>
<span class="o_searchview_extended_delete_prop fa fa-trash-o"/>
</span>
- <select class="o_searchview_extended_prop_op"/>
+ <select class="o_input o_searchview_extended_prop_op"/>
<span class="o_searchview_extended_prop_value"/>
</li>
</t>
<t t-name="SearchView.extended_search.proposition.float">
- <input t-att-type="widget.decimal_point === '.' ? 'number' : 'text'"
+ <input class="o_input"
+ t-att-type="widget.decimal_point === '.' ? 'number' : 'text'"
t-attf-title="Number using #{widget.decimal_point || '.' } as decimal separator."
t-attf-pattern="[0-9]+([\\#{widget.decimal_point || '.' }][0-9]+)?"
t-attf-value="0#{widget.decimal_point || '.' }0" step="0.01"/>
</t>
<t t-name="SearchView.extended_search.proposition.selection">
- <select>
+ <select class="o_input">
<option t-foreach="widget.field.selection" t-as="element" t-att-value="element[0]">
<t t-esc="element[1]"/>
</option>
@@ -1051,7 +1055,7 @@
</ul>
</div>
<t t-name="GroupByMenuSelector">
- <li><select class="o_add_group o_group_selector">
+ <li><select class="o_input o_add_group o_group_selector">
<t t-foreach="groupableFields" t-as="field">
<option t-att-data-name="field.name"><t t-esc="field.string"/></option>
</t>