diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index c66b1efa82d..0144258ecb8 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -646,8 +646,11 @@ class AccountMoveLine(models.Model):
#compute the default credit/debit of the next line in case of a manual entry
balance = 0
for line in self._context['line_ids']:
- if line[2]:
+ if line[2]: # in case of command 0: add a record with values
balance += line[2].get('debit', 0) - line[2].get('credit', 0)
+ elif line[0] == 2: # line has been deleted
+ line_obj = self.browse(line[1])
+ balance -= line_obj.debit - line_obj.credit
if balance < 0:
rec.update({'debit': -balance})
if balance > 0:
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index 0cbb74dd7a4..0da4a3da3fe 100644
--- a/addons/account/models/account_payment.py
+++ b/addons/account/models/account_payment.py
@@ -259,6 +259,7 @@ class account_register_payments(models.TransientModel):
'currency_id': self.currency_id.id,
'partner_id': invoices[0].commercial_partner_id.id,
'partner_type': MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type],
+ 'multi': False,
}
@api.multi
diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py
index 95a01bfd0c2..8748a03ff34 100644
--- a/addons/account/report/account_aged_partner_balance.py
+++ b/addons/account/report/account_aged_partner_balance.py
@@ -77,7 +77,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
partner_ids = [partner['partner_id'] for partner in partners if partner['partner_id']]
lines = dict((partner['partner_id'] or False, []) for partner in partners)
if not partner_ids:
- return [], [], []
+ return [], [], {}
# This dictionary will store the not due amount of all partners
undue_amounts = {}
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js
index 87bd379879d..f42943f6027 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_model.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js
@@ -130,6 +130,20 @@ var StatementModel = BasicModel.extend({
var line = this.getLine(handle);
var prop = _.clone(_.find(line.mv_lines, {'id': mv_line_id}));
this._addProposition(line, prop);
+
+ // Check whether we have some propositions left
+ // If we don't, it means we are on an empty page
+ // so let's go back to the previous page
+ // Through the offset
+ var propLineIds = _.map(line.reconciliation_proposition, function(prop) {
+ return prop.id;
+ });
+ var leftOversProps = _.filter(line.mv_lines, function(mv_line) {
+ return propLineIds.indexOf(mv_line.id) === -1;
+ });
+ if (line.offset && !leftOversProps.length) {
+ line.offset -= line.limitMoveLines;
+ }
return $.when(this._computeLine(line), this._performMoveLine(handle));
},
/**
@@ -665,7 +679,7 @@ var StatementModel = BasicModel.extend({
return !isNaN(prop.id) && prop.already_paid;
}), 'id'),
"new_aml_dicts": _.map(_.filter(props, function (prop) {
- return isNaN(prop.id);
+ return isNaN(prop.id) && prop.display;
}), self._formatToProcessReconciliation.bind(self, line)),
};
@@ -1262,7 +1276,7 @@ var ManualModel = StatementModel.extend({
});
} else {
var mv_line_ids = _.pluck(_.filter(props, function (prop) {return !isNaN(prop.id);}), 'id');
- var new_mv_line_dicts = _.map(_.filter(props, function (prop) {return isNaN(prop.id);}), self._formatToProcessReconciliation.bind(self, line));
+ var new_mv_line_dicts = _.map(_.filter(props, function (prop) {return isNaN(prop.id) && prop.display;}), self._formatToProcessReconciliation.bind(self, line));
process_reconciliations.push({
id: null,
type: null,
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
index 7578f9c4d1d..8a4ac8b25e7 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
@@ -423,7 +423,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
// mv_lines
var $mv_lines = this.$('.match table tbody').empty();
- _.each(state.mv_lines.slice(0, state.limitMoveLines), function (line) {
+ var stateMvLines = state.mv_lines || [];
+ _.each(stateMvLines.slice(0, state.limitMoveLines), function (line) {
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state}));
if (!isNaN(line.id)) {
$('')
@@ -432,9 +433,9 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
}
$mv_lines.append($line);
});
- this.$('.match .fa-chevron-right').toggleClass('disabled', state.mv_lines.length <= state.limitMoveLines);
+ this.$('.match .fa-chevron-right').toggleClass('disabled', stateMvLines.length <= state.limitMoveLines);
this.$('.match .fa-chevron-left').toggleClass('disabled', !state.offset);
- this.$('.match').css('max-height', !state.mv_lines.length && !state.filter.length ? '0px' : '');
+ this.$('.match').css('max-height', !stateMvLines.length && !state.filter.length ? '0px' : '');
// balance
this.$('.popover').remove();
diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js
index 4fb3f997368..93a8a1818b4 100644
--- a/addons/account/static/tests/reconciliation_tests.js
+++ b/addons/account/static/tests/reconciliation_tests.js
@@ -1720,5 +1720,165 @@ QUnit.module('account', {
clientAction.destroy();
});
+
+ QUnit.test('Manual Reconciliation: remove a prop to attain balance and reconcile', function (assert) {
+ assert.expect(5);
+
+ // tweak the data to fit our needs
+ this.params.data_for_manual_reconciliation_widget['[283, null, "", 0, 6]'] = _.extend({}, this.params.data_for_manual_reconciliation_widget['[null,null]']);
+ this.params.data_for_manual_reconciliation_widget['[283, null, "", 0, 6]'].accounts[0].reconciliation_proposition = [
+ {account_id: 283, account_type: "other", amount_currency_str: "", currency_id: false, date_maturity: "2017-03-18", date: "2017-02-16",
+ total_amount_str: "$ 500.00", partner_id: 8, account_name: "101000 Current Assets", name: "INV/2017/0987", partner_name: "Agrolait",
+ total_amount_currency_str: "", id: 999, credit: 0.0, journal_id: [1, "Customer Invoices"], amount_str: "$ 500.00", debit: 500.0,
+ account_code: "101000", ref: "", already_paid: false}
+ ];
+
+ var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options);
+ testUtils.addMockEnvironment(clientAction, {
+ data: this.params.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'process_move_lines') {
+ assert.deepEqual(args.args,
+ [
+ [{id: null, type: null,
+ mv_line_ids: [399, 402],
+ new_mv_line_dicts: []}
+ ]
+ ], "should call process_move_lines without the new mv line dict");
+ }
+
+ return this._super(route, args);
+ },
+ session: {
+ currencies: {
+ 3: {
+ digits: [69, 2],
+ position: "before",
+ symbol: "$"
+ }
+ }
+ },
+ });
+
+ clientAction.appendTo($('#qunit-fixture'));
+
+ // The first reconciliation "line" is where it happens
+ var widget = clientAction.widgets[0];
+
+ // Add first prop
+ widget.$('.match .cell_account_code:first').trigger('click');
+ assert.notOk( widget.$('.cell_right .line_info_button').length,
+ "should not display the partial reconciliation alert");
+
+ // Add second prop
+ widget.$('.match .cell_account_code:first').trigger('click');
+
+ // Check that a create form is here
+ var writeOffCreate = widget.$('div.create');
+
+ assert.equal(writeOffCreate.length, 1,
+ 'A write-off creation should be present');
+
+ assert.equal(writeOffCreate.find('input[name=amount]').val(), 500,
+ 'The right amount should be proposed for the write-off');
+
+ // remove the first line, the other two will balance one another
+ widget.$('tr[data-line-id="999"] td:first').click()
+
+ var $buttonReconcile = widget.$('button.o_reconcile:not(hidden)');
+ assert.equal($buttonReconcile.length, 1,
+ 'The reconcile button must be visible');
+
+ $buttonReconcile.click();
+
+ clientAction.destroy();
+ });
+
+ QUnit.test('Manual Reconciliation: No lines for account', function (assert) {
+ assert.expect(2);
+
+ var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options);
+ testUtils.addMockEnvironment(clientAction, {
+ data: this.params.data,
+ session: {
+ currencies: {
+ 3: {
+ digits: [69, 2],
+ position: "before",
+ symbol: "$"
+ }
+ }
+ },
+ });
+
+ clientAction.appendTo($('#qunit-fixture'));
+
+ // The second reconciliation "line" is where it happens
+ var widget = clientAction.widgets[1];
+
+ var emptyLine = widget.$('tr.mv_line');
+
+ assert.notOk('data-line-id' in emptyLine.getAttributes(),
+ 'Empty line should be empty');
+
+ emptyLine.find('td:first').click();
+
+ // Check that a create form is here
+ var writeOffCreate = widget.$('div.create .create_account_id');
+
+ assert.equal(writeOffCreate.length, 1,
+ 'A write-off creation should be present');
+
+ clientAction.destroy();
+ });
+
+ QUnit.test('Automatic Reconciliation: Don\'t loose pager when adding last line of a page', function (assert) {
+ assert.expect(2);
+
+ function standard_partner (list, partner_id) {
+ _.each(list, function (item) {
+ item.partner_id = partner_id;
+ });
+ };
+
+ this.params.options.params.limitMoveLines = 1;
+ standard_partner(this.params.mv_lines['[5,"",0,6]'], 8);
+
+ // Overlap of those requests is normal
+ // the pager is implemented as:
+ // if there is at least 1 more line than my limit, the pager is active
+ this.params.mv_lines['[5,"",0,2]'] = this.params.mv_lines['[5,"",0,6]'].slice(0,2);
+ this.params.mv_lines['[5,"",1,2]'] = this.params.mv_lines['[5,"",0,6]'].slice(1,2);
+
+ var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
+ testUtils.addMockEnvironment(clientAction, {
+ data: this.params.data,
+ session: {
+ currencies: {
+ 3: {
+ digits: [69, 2],
+ position: "before",
+ symbol: "$"
+ }
+ }
+ },
+ });
+ clientAction.appendTo($('#qunit-fixture'));
+
+ var widget = clientAction.widgets[0];
+
+ var $nextPage = widget.$('.match .match_controls .fa-chevron-right:not(disabled)');
+ assert.ok($nextPage.length,
+ 'We should have a next page');
+ $nextPage.click();
+
+ // Add the second's page record
+ widget.$('.match tr[data-line-id=112] td:first').click();
+
+ assert.ok(widget.$('.match tr[data-line-id=109]').length,
+ 'the record of the first page must be proposed');
+
+ clientAction.destroy();
+ });
});
});
diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml
index 996eff10cd2..141388fbfd2 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -320,7 +320,7 @@
-
+
@@ -364,6 +364,18 @@
+
+ company.res.partner.bank.form
+ res.partner.bank
+
+ primary
+
+
+ 1
+
+
+
+
account.bank.journal.treeaccount.journal
diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js
index acdf3460ecc..b2b944cc631 100644
--- a/addons/im_livechat/static/src/js/im_livechat.js
+++ b/addons/im_livechat/static/src/js/im_livechat.js
@@ -2,6 +2,7 @@ odoo.define('im_livechat.im_livechat', function (require) {
"use strict";
var bus = require('bus.bus').bus;
+var concurrency = require('web.concurrency');
var config = require('web.config');
var core = require('web.core');
var session = require('web.session');
@@ -313,6 +314,7 @@ var Feedback = Widget.extend({
this.channel_uuid = channel_uuid;
this.server_origin = session.origin;
this.rating = undefined;
+ this.dp = new concurrency.DropPrevious();
},
on_click_smiley: function (ev) {
@@ -348,7 +350,7 @@ var Feedback = Widget.extend({
rate: this.rating,
reason : options.reason
};
- return session.rpc('/im_livechat/feedback', args).then(function () {
+ this.dp.add(session.rpc('/im_livechat/feedback', args)).then(function () {
if (options.close) {
var emoji = RATING_TO_EMOJI[self.rating] || "??" ;
var content = _.str.sprintf(_t("Rating: %s"), emoji);
diff --git a/addons/purchase_stock/models/stock.py b/addons/purchase_stock/models/stock.py
index a108f0bec2a..b3d336ceddc 100644
--- a/addons/purchase_stock/models/stock.py
+++ b/addons/purchase_stock/models/stock.py
@@ -215,6 +215,6 @@ class PushedFlow(models.Model):
_inherit = "stock.location.path"
def _prepare_move_copy_values(self, move_to_copy, new_date):
- res = super()._prepare_move_copy_values(move_to_copy, new_date)
+ res = super(PushedFlow, self)._prepare_move_copy_values(move_to_copy, new_date)
res['purchase_line_id'] = None
return res
diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml
index f59ae7baf5c..5b58670e5ab 100644
--- a/addons/stock/views/product_views.xml
+++ b/addons/stock/views/product_views.xml
@@ -11,7 +11,6 @@
- The following routes will apply to the products in this category taking into account parent categories:
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
index 6c05076ef98..4720d166a4c 100644
--- a/addons/web/static/tests/helpers/test_utils.js
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -547,10 +547,11 @@ function dragAndDrop($el, $to, options) {
var toOffset = $to.offset();
toOffset.top += $to.outerHeight()/2;
toOffset.left += $to.outerWidth()/2;
+ var vertical_offset = (toOffset.top < elementCenter.top) ? -1 : 1;
if (position === 'top') {
- toOffset.top -= $to.outerHeight()/2;
+ toOffset.top -= $to.outerHeight()/2 + vertical_offset;
} else if (position === 'bottom') {
- toOffset.top += $to.outerHeight()/2;
+ toOffset.top += $to.outerHeight()/2 - vertical_offset;
} else if (position === 'left') {
toOffset.left -= $to.outerWidth()/2;
} else if (position === 'right') {
diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py
index 13594353abf..64c6bc05ab9 100644
--- a/addons/website_sale/models/website.py
+++ b/addons/website_sale/models/website.py
@@ -60,8 +60,8 @@ class Website(models.Model):
if not pricelists: # no pricelist for this country, or no GeoIP
pricelists |= all_pl.filtered(lambda pl: not show_visible or pl.selectable or pl.id in (current_pl, order_pl))
- else:
- pricelists |= all_pl.filtered(lambda pl: not show_visible and pl.sudo().code)
+ if not show_visible and not country_code:
+ pricelists |= all_pl.filtered(lambda pl: pl.sudo().code)
# This method is cached, must not return records! See also #8795
return pricelists.ids
diff --git a/addons/website_sale_wishlist/views/website_sale_wishlist_template.xml b/addons/website_sale_wishlist/views/website_sale_wishlist_template.xml
index 6be0378297c..3ee63021073 100644
--- a/addons/website_sale_wishlist/views/website_sale_wishlist_template.xml
+++ b/addons/website_sale_wishlist/views/website_sale_wishlist_template.xml
@@ -69,7 +69,7 @@
-
+
diff --git a/doc/cla/individual/jothimani-r.md b/doc/cla/individual/jothimani-r.md
new file mode 100644
index 00000000000..b8049074c4c
--- /dev/null
+++ b/doc/cla/individual/jothimani-r.md
@@ -0,0 +1,11 @@
+India, 2018-07-31
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Jothimani Rajagopal jothimani1991@gmail.com https://github.com/jothimani-r
diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py
index 6d29690b7e9..7963261f06a 100644
--- a/odoo/addons/base/models/res_users.py
+++ b/odoo/addons/base/models/res_users.py
@@ -883,7 +883,7 @@ class UsersImplied(models.Model):
for user in self.with_context({}):
gs = set(concat(g.trans_implied_ids for g in user.groups_id))
vals = {'groups_id': [(4, g.id) for g in gs]}
- super(UsersImplied, self).write(vals)
+ super(UsersImplied, user).write(vals)
return res
#----------------------------------------------------------
diff --git a/odoo/addons/base/tests/__init__.py b/odoo/addons/base/tests/__init__.py
index 3e6195ed523..36916cc53b7 100644
--- a/odoo/addons/base/tests/__init__.py
+++ b/odoo/addons/base/tests/__init__.py
@@ -33,3 +33,4 @@ from . import test_xmlrpc
from . import test_res_partner_bank
from . import test_reports
from . import test_tests_tags
+from . import test_non_regression
diff --git a/odoo/addons/base/tests/test_non_regression.py b/odoo/addons/base/tests/test_non_regression.py
new file mode 100644
index 00000000000..1f75578079a
--- /dev/null
+++ b/odoo/addons/base/tests/test_non_regression.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+"""
+ Non-Regression Tests
+"""
+
+from openerp.tests.common import TransactionCase
+
+
+class TestNR(TransactionCase):
+ def test_issue26036(self):
+ U = self.env["res.users"]
+ G = self.env["res.groups"]
+
+ group_A = G.create({"name": "A"})
+ group_AA = G.create({"name": "AA", "implied_ids": [(6, 0, [group_A.id])]})
+ group_B = G.create({"name": "B"})
+ group_BB = G.create({"name": "BB", "implied_ids": [(6, 0, [group_B.id])]})
+ group_C = G.create({"name": "C"})
+
+ user_a = U.create({"name": "a", "login": "a", "groups_id": [(6, 0, [group_AA.id])]})
+ user_b = U.create({"name": "b", "login": "b", "groups_id": [(6, 0, [group_BB.id])]})
+
+ (user_a + user_b).write({"groups_id": [(4, group_C.id)]})
+
+ self.assertEqual(user_a.groups_id, (group_AA + group_A + group_C))
+ self.assertEqual(user_b.groups_id, (group_BB + group_B + group_C))
diff --git a/odoo/addons/base/tests/test_qweb.py b/odoo/addons/base/tests/test_qweb.py
index 310c5220897..3b0f6d768cc 100644
--- a/odoo/addons/base/tests/test_qweb.py
+++ b/odoo/addons/base/tests/test_qweb.py
@@ -5,7 +5,6 @@ import collections
import json
import os.path
import re
-import sys
from lxml import etree
@@ -481,12 +480,16 @@ class TestQWebNS(TransactionCase):
"""
})
- error_msg = "Can't convert 'int' object to str implicitly"
- if sys.version_info >= (3, 6):
- error_msg = "must be str, not int"
- with self.assertRaisesRegexp(QWebException, error_msg):
+
+ try:
+ "" + 0
+ except TypeError as e:
+ error_msg = e.args[0]
+
+ with self.assertRaises(QWebException, msg=error_msg):
view1.render()
+
from copy import deepcopy
class FileSystemLoader(object):
def __init__(self, path):
diff --git a/odoo/addons/test_new_api/tests/test_onchange.py b/odoo/addons/test_new_api/tests/test_onchange.py
index a1b2e4edfee..1bfdee908a7 100644
--- a/odoo/addons/test_new_api/tests/test_onchange.py
+++ b/odoo/addons/test_new_api/tests/test_onchange.py
@@ -139,10 +139,14 @@ class TestOnChange(common.TransactionCase):
self.env.cache.invalidate()
result = self.Discussion.onchange(values, 'name', field_onchange)
self.assertIn('messages', result['value'])
- self.assertEqual(result['value']['messages'], [
+ self.assertItemsEqual(result['value']['messages'], [
(5,),
(1, message.id, {
'name': "[%s] %s" % ("Foo", USER.name),
+ 'body': message.body,
+ 'author': message.author.name_get()[0],
+ 'size': message.size,
+ 'important': message.important,
}),
(0, 0, {
'name': "[%s] %s" % ("Foo", USER.name),
@@ -252,7 +256,8 @@ class TestOnChange(common.TransactionCase):
'lines': [
(5,),
(1, line1.id, {'name': partner2.name,
- 'partner': (partner2.id, partner2.name)}),
+ 'partner': (partner2.id, partner2.name),
+ 'tags': [(5,)]}),
(0, 0, {'name': partner2.name,
'partner': (partner2.id, partner2.name),
'tags': [(5,)]}),
@@ -260,17 +265,6 @@ class TestOnChange(common.TransactionCase):
})
# do it again, but this time with a new tag on the second line
- expected_new_tag_result = {
- 'name': partner2.name,
- 'lines': [
- (5,),
- (1, line1.id, {'name': partner2.name,
- 'partner': (partner2.id, partner2.name)}),
- (0, 0, {'name': partner2.name,
- 'partner': (partner2.id, partner2.name),
- 'tags': [(5,), (0, 0, {'name': 'Tag'})]}),
- ],
- }
values = {
'name': partner1.name,
'partner': partner2.id, # this one just changed
@@ -282,13 +276,18 @@ class TestOnChange(common.TransactionCase):
self.env.cache.invalidate()
result = multi.onchange(values, 'partner', field_onchange)
- self.assertEqual(result['value'], expected_new_tag_result)
-
- # ensure ID is not returned when asked and a many2many record is set to be created
- self.env.cache.invalidate()
-
- result = multi.onchange(values, 'partner', dict(field_onchange, **{'lines.tags.id': None}))
- self.assertEqual(result['value'], expected_new_tag_result)
+ self.assertEqual(result['value'], {
+ 'name': partner2.name,
+ 'lines': [
+ (5,),
+ (1, line1.id, {'name': partner2.name,
+ 'partner': (partner2.id, partner2.name),
+ 'tags': [(5,)]}),
+ (0, 0, {'name': partner2.name,
+ 'partner': (partner2.id, partner2.name),
+ 'tags': [(5,), (0, 0, {'name': 'Tag'})]}),
+ ],
+ })
def test_onchange_specific(self):
""" test the effect of field-specific onchange method """
@@ -320,7 +319,8 @@ class TestOnChange(common.TransactionCase):
self.assertIn('participants', result['value'])
self.assertItemsEqual(
result['value']['participants'],
- [(5,)] + [(4, user.id) for user in discussion.participants + demo],
+ [(5,)] + [(1, user.id, {'display_name': user.display_name})
+ for user in discussion.participants + demo],
)
def test_onchange_default(self):
@@ -356,8 +356,6 @@ class TestOnChange(common.TransactionCase):
self.assertEqual(len(discussion.messages), 3)
messages = [(4, msg.id) for msg in discussion.messages]
messages[0] = (1, messages[0][1], {'body': 'test onchange'})
- lines = ["%s:%s" % (m.name, m.body) for m in discussion.messages]
- lines[0] = "%s:%s" % (discussion.messages[0].name, 'test onchange')
values = {
'name': discussion.name,
'moderator': demo.id,
@@ -368,7 +366,8 @@ class TestOnChange(common.TransactionCase):
}
result = discussion.onchange(values, 'messages', field_onchange)
self.assertIn('message_concat', result['value'])
- self.assertEqual(result['value']['message_concat'], "\n".join(lines))
+ self.assertEqual(result['value']['message_concat'],
+ "\n".join(["%s:%s" % (m.name, m.body) for m in discussion.messages]))
def test_onchange_one2many_with_domain_on_related_field(self):
""" test the value of the one2many field when defined with a domain on a related field"""
@@ -414,15 +413,28 @@ class TestOnChange(common.TransactionCase):
'categories': [(4, cat.id) for cat in discussion.categories],
'messages': [(4, msg.id) for msg in discussion.messages],
'participants': [(4, usr.id) for usr in discussion.participants],
+ 'message_changes': 0,
'important_messages': [(4, msg.id) for msg in discussion.important_messages],
'important_emails': [(4, eml.id) for eml in discussion.important_emails],
}
- self.env.cache.invalidate()
result = discussion.onchange(values, 'name', field_onchange)
+ # When one2many domain contains non-computed field, things are ok
+ self.assertEqual(result['value']['important_messages'],
+ [(5,)] + [(4, msg.id) for msg in discussion.important_messages])
+
+ # But here with commit 5676d81, we get value of: [(2, email.id)]
self.assertEqual(
result['value']['important_emails'],
- [(5,), (1, email.id, {'name': u'[Foo Bar] %s' % USER.name})],
+ [(5,),
+ (1, email.id, {
+ 'name': u'[Foo Bar] %s' % USER.name,
+ 'body': email.body,
+ 'author': USER.name_get()[0],
+ 'important': True,
+ 'email_to': demo.email,
+ 'size': email.size,
+ })]
)
def test_onchange_related(self):
diff --git a/odoo/models.py b/odoo/models.py
index 30b0f3db0a0..c720f1cd5d2 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -4982,114 +4982,28 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
if not all(name in self._fields for name in names):
return {}
- class PrefixTree(OrderedDict):
- """ A prefix tree for sequences of field names. The tree is a
- dictionary that associates each given field name to its
- corresponding subtree (in fields order)::
-
- # tree corresponding to dotnames
- # ['name', 'line_ids.product_id', 'line_ids.tags_ids.name']
- {
- 'name': {},
- 'line_ids': {
- 'product_id': {},
- 'tags_ids': {
- 'name': {},
- },
- },
- }
- """
- def __init__(self, model, dotnames):
- super(PrefixTree, self).__init__()
- if not dotnames:
- return
- # group dotnames by prefix
- suffixes = defaultdict(list)
- for dotname in dotnames:
- names = dotname.split('.', 1)
- name_suffixes = suffixes[names[0]]
- if len(names) > 1:
- name_suffixes.append(names[1])
- # fill in self in fields order
- for name in model._fields:
- if name in suffixes:
- self[name] = PrefixTree(model[name], suffixes[name])
-
- def dotnames(self):
- """ Iterate over the sequences of field names. """
- for name, subnames in self.items():
- yield name
- for dotname in subnames.dotnames():
- yield "%s.%s" % (name, dotname)
-
- nametree = PrefixTree(self.browse(), field_onchange)
- dotnames = list(nametree.dotnames())
-
- def snapshot(record, tree=nametree):
- """ Return a dict with the values of record, following nametree. """
- vals = {}
- for name, subnames in tree.items():
- if subnames:
- # x2many fields as {line: snapshot(line), ...}
- vals[name] = OrderedDict(
- (line, snapshot(line, subnames))
- for line in record[name]
- )
- else:
- vals[name] = record[name]
- return vals
-
- def diff(record, old, new, tree=nametree):
- """ Return the values that differ between snapshots.
- The snapshot ``old`` may be empty (for new records).
- """
- result = {}
- for name, subnames in tree.items():
- if name == 'id':
- continue
- if old and old[name] == new[name]:
- continue
- field = record._fields[name]
- if not subnames:
- result[name] = field.convert_to_onchange(new[name], record, {})
- continue
- # x2many fields: serialize value as commands
- result[name] = commands = [(5,)]
- old_val = old.get(name) or {}
- for line, vals in new[name].items():
- vals0 = (old_val.get(line) or snapshot(line, subnames)) if line.id else {}
- line_diff = diff(line, vals0, vals, subnames)
- if not line.id:
- commands.append((0, line.id.ref or 0, line_diff))
- elif line_diff:
- commands.append((1, line.id, line_diff))
- else:
- commands.append((4, line.id))
- return result
-
- # prefetch x2many lines without data (for the initial snapshot)
- for name, subnames in nametree.items():
- if subnames and values.get(name):
- # retrieve all ids in commands, and read the expected fields
- line_ids = []
- for cmd in values[name]:
- if cmd[0] in (1, 4):
- line_ids.append(cmd[1])
- elif cmd[0] == 6:
- line_ids.extend(cmd[2])
- lines = self.browse()[name].browse(line_ids)
- lines.read(list(subnames), load='_classic_write')
+ # filter out keys in field_onchange that do not refer to actual fields
+ dotnames = []
+ for dotname in field_onchange:
+ try:
+ model = self.browse()
+ for name in dotname.split('.'):
+ model = model[name]
+ dotnames.append(dotname)
+ except Exception:
+ pass
# create a new record with values, and attach ``self`` to it
with env.do_in_onchange():
record = self.new(values)
- values = {name: record[name] for name in nametree}
+ values = {name: record[name] for name in record._cache}
# attach ``self`` with a different context (for cache consistency)
record._origin = self.with_context(__onchange=True)
- # make a snapshot based on the initial values of record
+ # load fields on secondary records, to avoid false changes
with env.do_in_onchange():
- before = snapshot(record)
+ for dotname in dotnames:
+ record.mapped(dotname)
# determine which field(s) should be triggered an onchange
todo = list(names) or list(values)
@@ -5108,6 +5022,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
record[name] = value
result = {}
+ dirty = set()
# process names in order (or the keys of values if no name given)
while todo:
@@ -5133,14 +5048,22 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
field.type in ('one2many', 'many2many') and newval._is_dirty()
):
todo.append(name)
+ dirty.add(name)
- # make a snapshot based on the final values of record
+ # determine subfields for field.convert_to_onchange() below
+ Tree = lambda: defaultdict(Tree)
+ subnames = Tree()
+ for dotname in dotnames:
+ subtree = subnames
+ for name in dotname.split('.'):
+ subtree = subtree[name]
+
+ # collect values from dirty fields
with env.do_in_onchange():
- after = snapshot(record)
-
- # determine values that have changed by comparing snapshots
- self.invalidate_cache()
- result['value'] = diff(record, before, after)
+ result['value'] = {
+ name: self._fields[name].convert_to_onchange(record[name], record, subnames[name])
+ for name in dirty
+ }
return result