diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index cf0dcc4a605..beab40c9ddb 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -978,6 +978,7 @@ class AccountTax(models.Model):
'account_id': tax.account_id.id,
'refund_account_id': tax.refund_account_id.id,
'analytic': tax.analytic,
+ 'price_include': tax.price_include,
})
return {
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js
index b07961b9ef1..8f81b700168 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_model.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js
@@ -773,6 +773,8 @@ var StatementModel = BasicModel.extend({
'__focus': false
});
+ prop.computed_with_tax = tax.price_include
+ prop.tax_amount = tax.amount
prop.amount = tax.base;
prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions);
prop.invalid = !self._isValid(prop);
@@ -1042,7 +1044,8 @@ var StatementModel = BasicModel.extend({
* @returns {object}
*/
_formatToProcessReconciliation: function (line, prop) {
- var amount = -prop.amount;
+ // Do not forward port in master. @CSN will change this
+ var amount = prop.computed_with_tax && -prop.base_amount || -prop.amount;
if (prop.partial_reconcile === true) {
amount = -line.st_line.amount;
}
@@ -1050,6 +1053,10 @@ var StatementModel = BasicModel.extend({
name : prop.label,
debit : amount > 0 ? amount : 0,
credit : amount < 0 ? -amount : 0,
+ // This one isn't usefull for the server,
+ // But since we need to change the amount (and thus its semantics) into base_amount
+ // It might be useful to have a trace in the RPC for debugging purposes
+ computed_with_tax: prop.computed_with_tax,
};
if (!isNaN(prop.id)) {
result.counterpart_aml_id = prop.id;
diff --git a/addons/board/static/src/js/dashboard.js b/addons/board/static/src/js/dashboard.js
index b794d0303a3..558f7e53961 100644
--- a/addons/board/static/src/js/dashboard.js
+++ b/addons/board/static/src/js/dashboard.js
@@ -221,6 +221,10 @@ FormRenderer.include({
params: {action_id: params.actionID}
})
.then(function (action) {
+ if (!action) {
+ // the action does not exist anymore
+ return $.when();
+ }
var view = _.find(action.views, function (descr) {
return descr[1] === params.viewType;
});
diff --git a/addons/board/static/tests/dashboard_tests.js b/addons/board/static/tests/dashboard_tests.js
index d884c33b991..21067b32c44 100644
--- a/addons/board/static/tests/dashboard_tests.js
+++ b/addons/board/static/tests/dashboard_tests.js
@@ -365,4 +365,42 @@ QUnit.test('twice the same action in a dashboard', function (assert) {
form.destroy();
});
+QUnit.test('non-existing action in a dashboard', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'board',
+ data: this.data,
+ arch: '
',
+ intercepts: {
+ load_views: function () {
+ throw new Error('load_views should not be called');
+ }
+ },
+ mockRPC: function (route) {
+ if (route === '/board/static/src/img/layout_1-1-1.png') {
+ return $.when();
+ }
+ if (route === '/web/action/load') {
+ // server answer if the action doesn't exist anymore
+ return $.when(false);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('.oe_action:contains(ABC)').length, 1,
+ "there should be a box for the non-existing action");
+
+ form.destroy();
+});
+
+
});
diff --git a/addons/hr/i18n/hr.pot b/addons/hr/i18n/hr.pot
index b883df62277..06a2bd3b681 100644
--- a/addons/hr/i18n/hr.pot
+++ b/addons/hr/i18n/hr.pot
@@ -599,11 +599,6 @@ msgstr ""
msgid "Private Information"
msgstr ""
-#. module: hr
-#: model:ir.ui.view,arch_db:hr.res_config_settings_view_form
-msgid "Prout prout prout"
-msgstr ""
-
#. module: hr
#: model:ir.ui.view,arch_db:hr.view_hr_job_form
msgid "Recruitment"
diff --git a/addons/hr/views/res_config_settings_views.xml b/addons/hr/views/res_config_settings_views.xml
index 7c7bb5f7fe5..37c2e566b4d 100644
--- a/addons/hr/views/res_config_settings_views.xml
+++ b/addons/hr/views/res_config_settings_views.xml
@@ -9,7 +9,7 @@
Work Organization
-
+
diff --git a/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml b/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml
index 1eaef119e84..537021de787 100644
--- a/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml
+++ b/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml
@@ -401,7 +401,7 @@ elif salary <= 2461.27:
ATN Company Car
ATN.CAR
-
99
+
70
python
result = bool(contract.transport_mode == 'company_car' and contract.car_atn)
@@ -621,7 +621,7 @@ result = -contract.car_atn
Commission on target
commission_on_target
- 1000.0
+ 0.0
Fuel card
diff --git a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py
index f104ea367f9..9174c408f64 100644
--- a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py
+++ b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py
@@ -205,7 +205,7 @@ class HrContract(models.Model):
- 12.0 * contract.transport_employer_cost \
- (1.326 / 1.05 * 12.0) * contract.commission_on_target \
- 220.0 * contract.meal_voucher_paid_by_employer
- gross = remaining_for_gross / (12.0 * 0.05 + 13.0 + 13.0 * 0.3507 + 0.92)
+ gross = remaining_for_gross / (13.0 + 13.0 * 0.3507 + 0.92)
return gross
diff --git a/addons/l10n_ch/models/res_bank.py b/addons/l10n_ch/models/res_bank.py
index 7a60d7f0eae..0ddc98172a3 100644
--- a/addons/l10n_ch/models/res_bank.py
+++ b/addons/l10n_ch/models/res_bank.py
@@ -44,10 +44,9 @@ class ResPartnerBank(models.Model):
@api.depends('acc_number')
def _compute_l10n_ch_postal(self):
for record in self:
- if record.acc_type == 'postal':
- record.l10n_ch_postal = record.sanitized_acc_number
- elif record.acc_type == 'iban':
+ if record.acc_type == 'iban':
record.l10n_ch_postal = record._retrieve_l10n_ch_postal(record.sanitized_acc_number)
+ record.l10n_ch_postal = record.sanitized_acc_number
def _retrieve_l10n_ch_postal(self, iban):
""" Reads a swiss postal account number from a an IBAN and returns it as
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 04bb1b2d44b..8a8af32d2fb 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -1878,11 +1878,18 @@ var PaymentScreenWidget = ScreenWidget.extend({
this.reset_input();
this.render_paymentlines();
this.order_changes();
+ // that one comes from BarcodeEvents
+ $('body').keypress(this.keyboard_handler);
+ // that one comes from the pos, but we prefer to cover all the basis
+ $('body').keydown(this.keyboard_keydown_handler);
+ // legacy vanilla JS listeners
window.document.body.addEventListener('keypress',this.keyboard_handler);
window.document.body.addEventListener('keydown',this.keyboard_keydown_handler);
this._super();
},
hide: function(){
+ $('body').off('keypress', this.keyboard_handler);
+ $('body').off('keydown', this.keyboard_keydown_handler);
window.document.body.removeEventListener('keypress',this.keyboard_handler);
window.document.body.removeEventListener('keydown',this.keyboard_keydown_handler);
this._super();
diff --git a/addons/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py
index f1d20cc6772..fd531467998 100644
--- a/addons/resource/models/resource_mixin.py
+++ b/addons/resource/models/resource_mixin.py
@@ -31,6 +31,16 @@ class ResourceMixin(models.AbstractModel):
values['resource_id'] = resource.id
return super(ResourceMixin, self).create(values)
+ @api.multi
+ def copy_data(self, default=None):
+ if default is None:
+ default = {}
+ resource = self.resource_id.copy()
+ default['resource_id'] = resource.id
+ default['company_id'] = resource.company_id.id
+ default['resource_calendar_id'] = resource.calendar_id.id
+ return super(ResourceMixin, self).copy_data(default)
+
def get_work_days_count(self, from_datetime, to_datetime, calendar=None):
""" Return the number of work days for the resource, taking into account
leaves. An optional calendar can be given in case multiple calendars can
diff --git a/addons/web/static/src/js/chrome/search_filters.js b/addons/web/static/src/js/chrome/search_filters.js
index b9cf1b0fce9..e2eff03546d 100644
--- a/addons/web/static/src/js/chrome/search_filters.js
+++ b/addons/web/static/src/js/chrome/search_filters.js
@@ -187,7 +187,10 @@ var DateTime = Field.extend({
{value: "∄", text: _lt("is not set")}
],
get_value: function (index) {
- return this["datewidget_" + (index || 0)].getValue();
+ // retrieve the datepicker value
+ var value = this["datewidget_" + (index || 0)].getValue();
+ // convert to utc
+ return value.add(-this.getSession().getTZOffset(value), 'minutes');
},
get_domain: function (field, operator) {
switch (operator.value) {
diff --git a/addons/web/static/src/js/core/rpc.js b/addons/web/static/src/js/core/rpc.js
index 6ad91c0e4f9..53d298c9019 100644
--- a/addons/web/static/src/js/core/rpc.js
+++ b/addons/web/static/src/js/core/rpc.js
@@ -51,9 +51,15 @@ return {
}
if (options.method === 'read_group') {
- params.kwargs.domain = options.domain || params.domain || params.kwargs.domain || [];
- params.kwargs.fields = options.fields || params.fields || params.kwargs.fields || [];
- params.kwargs.groupby = options.groupBy || params.groupBy || params.kwargs.groupby || [];
+ if (!(params.args && params.args[0] !== undefined)) {
+ params.kwargs.domain = options.domain || params.domain || params.kwargs.domain || [];
+ }
+ if (!(params.args && params.args[1] !== undefined)) {
+ params.kwargs.fields = options.fields || params.fields || params.kwargs.fields || [];
+ }
+ if (!(params.args && params.args[2] !== undefined)) {
+ params.kwargs.groupby = options.groupBy || params.groupBy || params.kwargs.groupby || [];
+ }
params.kwargs.offset = options.offset || params.offset || params.kwargs.offset;
params.kwargs.limit = options.limit || params.limit || params.kwargs.limit;
// In kwargs, we look for "orderby" rather than "orderBy" (note the absence of capital B),
diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js
index e99f9ebb6f2..501961d6d76 100644
--- a/addons/web/static/src/js/services/data_manager.js
+++ b/addons/web/static/src/js/services/data_manager.js
@@ -423,8 +423,12 @@ return core.Class.extend({
if (fieldsInfo[node.attrs.name].fieldDependencies) {
var deps = fieldsInfo[node.attrs.name].fieldDependencies;
for (var dependency_name in deps) {
+ var dependency_dict = {name: dependency_name, type: deps[dependency_name].type};
if (!(dependency_name in fieldsInfo)) {
- fieldsInfo[dependency_name] = {'name': dependency_name, 'type': deps[dependency_name].type, 'options': deps[dependency_name].options || {}};
+ fieldsInfo[dependency_name] = _.extend({}, dependency_dict, {options: deps[dependency_name].options || {}});
+ }
+ if (!(dependency_name in fields)) {
+ fields[dependency_name] = dependency_dict;
}
}
}
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index a6cfea6e3be..6cc8a3c8143 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -226,9 +226,6 @@ var AbstractView = Class.extend({
return;
}
- attrs.limit = attrs.mode === "tree" ? 80 :
- (attrs.widget === 'many2many_tags' ? 1000 : 40);
-
if (attrs.Widget.prototype.useSubview && !attrs.__no_fetch && !attrs.views[attrs.mode]) {
var context = {};
var regex = /'([a-z]*_view_ref)' *: *'(.*?)'/g;
@@ -244,12 +241,30 @@ var AbstractView = Class.extend({
for (var viewName in views) {
attrs.views[viewName] = views[viewName];
}
+ self._setSubViewLimit(attrs);
}));
+ } else {
+ self._setSubViewLimit(attrs);
}
});
}
return $.when.apply($, defs);
- }
+ },
+ /**
+ * We set here the limit for the number of records fetched (in one page).
+ * This method is only called for subviews, not for main views.
+ *
+ * @private
+ * @param {Object} attrs
+ */
+ _setSubViewLimit: function (attrs) {
+ var view = attrs.views && attrs.views[attrs.mode];
+ var limit = view && view.arch.attrs.limit && parseInt(view.arch.attrs.limit);
+ if (!limit && attrs.widget === 'many2many_tags') {
+ limit = 1000;
+ }
+ attrs.limit = limit || 40;
+ },
});
return AbstractView;
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 4bd12bf2bc3..c16f635a604 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -1588,7 +1588,9 @@ var BasicModel = AbstractModel.extend({
return $.when.apply($, defs).then(function () {
// ensure to fetch up to 'limit' records (may be useful if records of
// the current page have been removed)
- return self._readUngroupedList(list);
+ return self._readUngroupedList(list).then(function () {
+ return self._fetchX2ManysBatched(list);
+ });
});
},
/**
@@ -2516,6 +2518,8 @@ var BasicModel = AbstractModel.extend({
changes[fieldName] = value ?
this.localData[value].model + ',' + this.localData[value].res_id :
false;
+ } else if (type === 'char' && changes[fieldName] === '') {
+ changes[fieldName] = false;
} else if (changes[fieldName] === null) {
changes[fieldName] = false;
}
diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js
index 543ce01191e..21fabbc2274 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -356,6 +356,17 @@ var BasicRenderer = AbstractRenderer.extend({
_hasContent: function () {
return this.state.count !== 0;
},
+ /**
+ * This function is called each time a field widget is created, when it is
+ * ready (after its willStart and Start methods are complete). This is the
+ * place where work having to do with $el should be done.
+ *
+ * @private
+ * @param {Widget} widget the field widget instance
+ * @param {Object} node the attrs coming from the arch
+ */
+ _postProcessField: function (widget, node) {
+ },
/**
* Registers or updates the modifiers data associated to the given node.
* This method is quiet complex as it handles all the needs of the basic
@@ -524,6 +535,7 @@ var BasicRenderer = AbstractRenderer.extend({
));
},
}, modifiersOptions || {}));
+ self._postProcessField(widget, node);
});
return widget;
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index 7b53b60f90c..88e8a99de53 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -485,6 +485,7 @@ var FormController = BasicController.extend({
* @param {OdooEvent} event
*/
_onOpenRecord: function (event) {
+ event.stopPropagation();
var self = this;
var record = this.model.get(event.data.id, {raw: true});
new dialogs.FormViewDialog(self, {
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 55c23b5d3a2..e18615c8caf 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -246,6 +246,17 @@ var FormRenderer = BasicRenderer.extend({
}
return idForLabel;
},
+ /**
+ * @override
+ * @private
+ */
+ _postProcessField: function (widget, node) {
+ this._setIDForLabel(widget, this._getIDForLabel(node.attrs.name));
+ this._handleAttributes(widget.$el, node);
+ if (JSON.parse(node.attrs.default_focus || "0")) {
+ this.defaultFocusField = widget;
+ }
+ },
/**
* @private
* @param {Object} node
@@ -307,23 +318,6 @@ var FormRenderer = BasicRenderer.extend({
this._registerModifiers(node, this.state, $result);
return $result;
},
- /**
- * @override
- * @private
- * @param {string} node
- * @param {Object} record
- * @param {Object} [options]
- * @returns {AbstractField}
- */
- _renderFieldWidget: function (node, record, options, modifiersOptions) {
- var widget = this._super.apply(this, arguments);
- this._setIDForLabel(widget, this._getIDForLabel(node.attrs.name));
- this._handleAttributes(widget.$el, node);
- if (JSON.parse(node.attrs.default_focus || "0")) {
- this.defaultFocusField = widget;
- }
- return widget;
- },
/**
* @private
* @param {Object} node
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 907d312365d..d39a3b280e1 100644
--- a/addons/web/static/src/js/views/kanban/kanban_column.js
+++ b/addons/web/static/src/js/views/kanban/kanban_column.js
@@ -52,7 +52,7 @@ var KanbanColumn = Widget.extend({
this.grouped_by_m2o = options.grouped_by_m2o;
this.editable = options.editable;
this.deletable = options.deletable;
- this.draggable = recordOptions.draggable;
+ this.draggable = options.draggable;
this.records_editable = options.records_editable;
this.records_deletable = options.records_deletable;
this.relation = options.relation;
@@ -97,12 +97,13 @@ var KanbanColumn = Widget.extend({
}
this.$header.find('.o_kanban_header_title').tooltip();
- if (!config.device.isMobile && this.draggable !== false) {
+ if (!config.device.isMobile) {
// deactivate sortable in mobile mode. It does not work anyway,
// and it breaks horizontal scrolling in kanban views. Someday, we
// should find a way to use the touch events to make sortable work.
this.$el.sortable({
connectWith: '.o_kanban_group',
+ containment: this.draggable ? '.o_kanban_view' : 'parent',
revert: 0,
delay: 0,
items: '> .o_kanban_record:not(.o_updating)',
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 495f98b28ee..361573fd3a1 100644
--- a/addons/web/static/src/js/views/kanban/kanban_record.js
+++ b/addons/web/static/src/js/views/kanban/kanban_record.js
@@ -36,7 +36,6 @@ var KanbanRecord = Widget.extend({
this.options = options;
this.editable = options.editable;
this.deletable = options.deletable;
- this.draggable = options.draggable;
this.read_only_mode = options.read_only_mode;
this.qweb = options.qweb;
this.subWidgets = {};
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js
index c9239d97d03..029cef7cf0b 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js
@@ -323,7 +323,7 @@ var KanbanRenderer = BasicRenderer.extend({
var groupByFieldInfo = state.fieldsInfo.kanban[state.groupedBy[0]];
// Deactivate the drag'n'drop if the groupedBy field:
// - is a date or datetime since we group by month or
- // - is readonly
+ // - is readonly (on the field attrs or in the view)
var draggable = true;
if (groupByFieldAttrs) {
if (groupByFieldAttrs.type === "date" || groupByFieldAttrs.type === "datetime") {
@@ -332,6 +332,11 @@ var KanbanRenderer = BasicRenderer.extend({
draggable = !(groupByFieldAttrs.readonly);
}
}
+ if (groupByFieldInfo) {
+ if (draggable && groupByFieldInfo.readonly !== undefined) {
+ draggable = !(groupByFieldInfo.readonly);
+ }
+ }
this.groupedByM2O = groupByFieldAttrs && (groupByFieldAttrs.type === 'many2one');
var grouped_by_field = this.groupedByM2O && groupByFieldAttrs.relation;
var groupByTooltip = groupByFieldInfo && groupByFieldInfo.options.group_by_tooltip;
diff --git a/addons/web/static/src/less/layout_boxed.less b/addons/web/static/src/less/layout_boxed.less
index a81e09f347b..21fcf66efc5 100644
--- a/addons/web/static/src/less/layout_boxed.less
+++ b/addons/web/static/src/less/layout_boxed.less
@@ -15,6 +15,7 @@
}
}
.o_boxed_footer {
+ white-space: nowrap;
border-top: 3px solid @gray;
ul {
margin: 4px 0;
diff --git a/addons/web/static/tests/core/rpc_tests.js b/addons/web/static/tests/core/rpc_tests.js
index 3b7fc006b02..06763e38f0d 100644
--- a/addons/web/static/tests/core/rpc_tests.js
+++ b/addons/web/static/tests/core/rpc_tests.js
@@ -243,6 +243,47 @@ QUnit.module('core', {}, function () {
assert.deepEqual(query.params.kwargs.lazy, undefined, "should not enforce a default value for lazy");
});
+ QUnit.test('read_group with args and kwargs', function (assert) {
+ assert.expect(9);
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'read_group',
+ kwargs: {
+ domain: ['name', '=', 'saucisse'],
+ fields: ['category_id'],
+ groupby: ['country_id'],
+ },
+ });
+
+ assert.deepEqual(query.params.kwargs.domain, ['name', '=', 'saucisse'], "should have ['name', '=', 'saucisse'] category_id as default domain");
+ assert.deepEqual(query.params.kwargs.fields, ['category_id'], "should have category_id as default fields");
+ assert.deepEqual(query.params.kwargs.groupby, ['country_id'], "should have country_id as default groupby");
+
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'read_group',
+ args: [['name', '=', 'saucisse']],
+ kwargs: {
+ fields: ['category_id'],
+ groupby: ['country_id'],
+ },
+ });
+
+ assert.deepEqual(query.params.kwargs.domain, undefined, "should not enforce a default value for domain");
+ assert.deepEqual(query.params.kwargs.fields, ['category_id'], "should have category_id as default fields");
+ assert.deepEqual(query.params.kwargs.groupby, ['country_id'], "should have country_id as default groupby");
+
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'read_group',
+ args: [['name', '=', 'saucisse'], ['category_id'], ['country_id']],
+ });
+
+ assert.deepEqual(query.params.kwargs.domain, undefined, "should not enforce a default value for domain");
+ assert.deepEqual(query.params.kwargs.fields, undefined, "should not enforce a default value for fields");
+ assert.deepEqual(query.params.kwargs.groupby, undefined, "should not enforce a default value for groupby");
+ });
+
QUnit.test('search_read with no domain, nor fields', function (assert) {
assert.expect(5);
var query = rpc.buildQuery({
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index a058a490b69..5064dd81af1 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -738,6 +738,38 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('setting a char field to empty string is saved as a false value', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ viewOptions: {mode: 'edit'},
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].foo, false,
+ 'the foo value should be false');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ form.$('input[type="text"].o_field_widget').val('').trigger('input');
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ form.destroy();
+ });
+
QUnit.test('char field with size attribute', function (assert) {
assert.expect(1);
@@ -1502,6 +1534,55 @@ QUnit.module('basic_fields', {
});
+ QUnit.test('image fields in subviews are loaded correctly', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].__last_update = '2017-02-08 10:00:00';
+ this.data.partner.records[0].document = 'myimage';
+ this.data.partner_type.fields.image = {name: 'image', type: 'binary'};
+ this.data.partner_type.records[0].image = 'product_image';
+ this.data.partner.records[0].timmy = [12];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === 'data:image/png;base64,myimage') {
+ assert.step("The view's image should have been fetched");
+ return $.when('wow');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ assert.verifySteps(["The view's image should have been fetched"]);
+
+ assert.strictEqual(form.$('tr.o_data_row').length, 1,
+ 'There should be one record in the many2many');
+
+ // Actual flow: click on an element of the m2m to get its form view
+ form.$('tbody td:contains(gold)').click();
+ assert.strictEqual($('.modal-dialog').length, 1,
+ 'The modal should have opened');
+ assert.strictEqual($('.modal-dialog').find('.o_field_image > img')[0].src,
+ 'data:image/png;base64,product_image',
+ 'The image of the many2many in its form view should be present');
+
+ form.destroy();
+ });
+
QUnit.module('JournalDashboardGraph', {
beforeEach: function () {
_.extend(this.data.partner.fields, {
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index aa8fa8e4d0c..205473f3191 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -2124,6 +2124,65 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('use the limit attribute in arch (in field o2m inline tree view)', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [1,2,3];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'' +
+ '' +
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.model === 'turtle') {
+ assert.deepEqual(args.args[0], [1,2],
+ 'should only load first 2 records');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('.o_data_row').length, 2,
+ 'should display 2 data rows');
+ form.destroy();
+ });
+
+ QUnit.test('use the limit attribute in arch (in field o2m non inline tree view)', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [1,2,3];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'' +
+ ' ' +
+ ' ',
+ archs: {
+ 'turtle,false,list': ' ',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.model === 'turtle') {
+ assert.deepEqual(args.args[0], [1,2],
+ 'should only load first 2 records');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('.o_data_row').length, 2,
+ 'should display 2 data rows');
+ form.destroy();
+ });
+
QUnit.test('embedded one2many with widget', function (assert) {
assert.expect(1);
@@ -5248,6 +5307,52 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('many2manytag in one2many, onchange, some modifiers, and more than one page', function (assert) {
+ assert.expect(9);
+
+ this.data.partner.records[0].turtles = [1,2,3];
+
+ this.data.partner.onchanges.turtles = function () {};
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '' +
+ '' +
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ',
+ res_id: 1,
+ viewOptions: {mode: 'edit'},
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+ assert.strictEqual(form.$('.o_data_row').length, 2,
+ 'there should be only 2 rows displayed');
+ form.$('.o_list_record_delete').click();
+ form.$('.o_list_record_delete').click();
+
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ 'there should be just one remaining row');
+
+ assert.verifySteps([
+ "read", // initial read on partner
+ "read", // initial read on turtle
+ "read", // batched read on partner (field partner_ids)
+ "read", // after first delete, read on turtle (to fetch 3rd record)
+ "onchange", // after first delete, onchange on field turtles
+ "onchange" // onchange after second delete
+ ]);
+
+ form.destroy();
+ });
+
QUnit.test('onchange many2many in one2many list editable', function (assert) {
assert.expect(14);
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 0094f0ffa40..5e353b991b6 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -167,6 +167,37 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('attributes are transferred on async widgets', function (assert) {
+ assert.expect(1);
+
+ var def = $.Deferred();
+
+ var FieldChar = fieldRegistry.get('char');
+ fieldRegistry.add('asyncwidget', FieldChar.extend({
+ willStart: function () {
+ return def;
+ },
+ }));
+
+ createAsyncView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '' +
+ '' +
+ ' ' +
+ ' ' +
+ ' ',
+ res_id: 2,
+ }).then(function (form) {
+ assert.strictEqual(form.$('.o_field_widget[name=foo]').attr('style'), 'color: blue',
+ "should apply style attribute on fields");
+ form.destroy();
+ delete fieldRegistry.map.asyncwidget;
+ });
+ def.resolve();
+ });
+
QUnit.test('only necessary fields are fetched with correct context', function (assert) {
assert.expect(2);
@@ -1858,6 +1889,52 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('circular many2many\'s', function (assert) {
+ assert.expect(4);
+ this.data.partner_type.fields.partner_ids = {string: "partners", type: "many2many", relation: 'partner'}
+ this.data.partner.records[0].timmy = [12];
+ this.data.partner_type.records[0].partner_ids = [1];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'' +
+ '' +
+ '' +
+ ' ' +
+ ' ' +
+ '' +
+ '' +
+ '' +
+ ' ' +
+ ' ' +
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('td:contains(gold)').length, 1,
+ "should display the name of the many2many on the original form");
+ form.$('td:contains(gold)').click();
+
+ assert.strictEqual($('.modal-dialog').length, 1,
+ 'The partner_type modal should have opened');
+ assert.strictEqual($('.modal-dialog').find('td:contains(first record)').length, 1,
+ "should display the name of the many2many on the modal form");
+
+ $('.modal-dialog').find('td:contains(first record)').click();
+ assert.strictEqual($('.modal-dialog').length, 2,
+ 'There should be 2 modals (partner on top of partner_type) opened');
+
+ form.destroy();
+ });
+
QUnit.test('discard changes on a non dirty form view', function (assert) {
assert.expect(4);
@@ -5636,9 +5713,9 @@ QUnit.module('Views', {
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input[name="foo"]').val(), '***',
"password should be displayed with stars");
- assert.strictEqual(form.$('input[name="display_name"]').prop('autocomplete'), 'coucou',
+ assert.strictEqual(form.$('input[name="display_name"]').attr('autocomplete'), 'coucou',
"attribute autocomplete should be set");
- assert.strictEqual(form.$('input[name="foo"]').prop('autocomplete'), 'new-password',
+ assert.strictEqual(form.$('input[name="foo"]').attr('autocomplete'), 'new-password',
"attribute autocomplete should be set to 'new-password' on password input");
form.destroy();
});
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index f614fa9af64..70e60b37bd5 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -718,6 +718,79 @@ QUnit.module('Views', {
kanban.destroy();
});
+ QUnit.test('prevent drag and drop of record if grouped by readonly', function (assert) {
+ assert.expect(12);
+
+ this.data.partner.fields.foo.readonly = true;
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '' +
+ '' +
+ '' +
+ ' ' +
+ ' ' +
+ '
' +
+ ' ' +
+ ' ',
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/resequence') {
+ return $.when();
+ }
+ if (args.model === 'partner' && args.method === 'write') {
+ throw new Error('should not be draggable');
+ }
+ return this._super(route, args);
+ },
+ });
+ // simulate an update coming from the searchview, with another groupby given
+ kanban.update({groupBy: ['state']});
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 1,
+ "column should contain 1 record(s)");
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 1,
+ "column should contain 1 record(s)");
+ // drag&drop a record in another column
+ var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first');
+ var $group = kanban.$('.o_kanban_group:nth-child(2)');
+ testUtils.dragAndDrop($record, $group);
+ // should not be draggable
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 1,
+ "column should now contain 1 record(s)");
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 1,
+ "column should contain 1 record(s)");
+
+ // simulate an update coming from the searchview, with another groupby given
+ kanban.update({groupBy: ['foo']});
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 1,
+ "column should contain 1 record(s)");
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 2,
+ "column should contain 2 record(s)");
+ // drag&drop a record in another column
+ $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first');
+ $group = kanban.$('.o_kanban_group:nth-child(2)');
+ testUtils.dragAndDrop($record, $group);
+ // should not be draggable
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 1,
+ "column should now contain 1 record(s)");
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 2,
+ "column should contain 2 record(s)");
+
+ // drag&drop a record in the same column
+ var $record1 = kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record:eq(0)');
+ var $record2 = kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record:eq(1)');
+ assert.strictEqual($record1.text(), "blipDEF", "first record should be DEF");
+ assert.strictEqual($record2.text(), "blipGHI", "second record should be GHI");
+ testUtils.dragAndDrop($record2, $record1, {position: 'top'});
+ // should still be able to resequence
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record:eq(0)').text(), "blipGHI",
+ "records should have been resequenced");
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record:eq(1)').text(), "blipDEF",
+ "records should have been resequenced");
+
+ kanban.destroy();
+ });
+
QUnit.test('kanban view with default_group_by', function (assert) {
assert.expect(7);
this.data.partner.records.product_id = 1;
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index 5f003dfac61..9180015685e 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -3190,6 +3190,30 @@ QUnit.module('Views', {
list.destroy();
delete widgetRegistry.map.test;
});
+
+ QUnit.test('use the limit attribute in arch', function (assert) {
+ assert.expect(3);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: ' ',
+ mockRPC: function (route, args) {
+ assert.strictEqual(args.limit, 2,
+ 'should use the correct limit value');
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(list.pager.$el.text().trim(), '1-2 / 4',
+ "pager should be correct");
+
+ assert.strictEqual(list.$('.o_data_row').length, 2,
+ 'should display 2 data rows');
+ list.destroy();
+ });
+
});
});
diff --git a/addons/web_tour/static/src/js/tour_manager.js b/addons/web_tour/static/src/js/tour_manager.js
index 4914dce138f..db7f2113bbd 100644
--- a/addons/web_tour/static/src/js/tour_manager.js
+++ b/addons/web_tour/static/src/js/tour_manager.js
@@ -167,6 +167,7 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
this.running_step_delay = parseInt(local_storage.getItem(get_running_delay_key()), 10) || 10;
this.edition = (_.last(session.server_version_info) === 'e') ? 'enterprise' : 'community';
this._log = [];
+ console.log('Tour Manager is ready. running_tour=' + this.running_tour);
},
/**
* Registers a tour described by the following arguments (in order)
@@ -236,6 +237,7 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
}).bind(this));
},
run: function (tour_name, step_delay) {
+ console.log(_.str.sprintf("Preparing tour %s", tour_name));
if (this.running_tour) {
this._deactivate_tip(this.active_tooltips[this.running_tour]);
this._consume_tour(this.running_tour, _.str.sprintf("Killing tour %s", this.running_tour));
diff --git a/addons/website_event/views/event_templates.xml b/addons/website_event/views/event_templates.xml
index 714c2f6342d..8a9ac57d3f4 100644
--- a/addons/website_event/views/event_templates.xml
+++ b/addons/website_event/views/event_templates.xml
@@ -57,7 +57,7 @@
diff --git a/doc/reference/views.rst b/doc/reference/views.rst
index 0b570bc3ee2..a943113f56e 100644
--- a/doc/reference/views.rst
+++ b/doc/reference/views.rst
@@ -171,6 +171,8 @@ root can have the following attributes:
``create``, ``edit``, ``delete``
allows *dis*\ abling the corresponding action in the view by setting the
corresponding attribute to ``false``
+``limit``
+ the default size of a page. It should be a positive integer
``on_write``
only makes sense on an ``editable`` list. Should be the name of a method
on the list's model. The method will be called with the ``id`` of a record
diff --git a/odoo/tests/phantomtest.js b/odoo/tests/phantomtest.js
index a372821d58e..b77cc3ffe9d 100644
--- a/odoo/tests/phantomtest.js
+++ b/odoo/tests/phantomtest.js
@@ -61,6 +61,7 @@ function PhantomTest() {
console.log('
'+message+' ');
};
this.page.onLoadFinished = function(status) {
+ console.log('PhantomJS: onLoadFinished. Status=', status);
if (status === "success") {
for (var k in self.inject) {
var found = false;