[MERGE] forward port branch 11.0 up to f9e0f6663f

This commit is contained in:
Christophe Simonis
2018-08-01 14:20:39 +02:00
19 changed files with 312 additions and 156 deletions
+4 -1
View File
@@ -503,8 +503,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:
@@ -68,7 +68,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 = {}
@@ -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)),
};
@@ -1268,7 +1282,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,
@@ -405,7 +405,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)) {
$('<span class="line_info_button fa fa-info-circle"/>')
@@ -414,9 +415,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();
@@ -1701,5 +1701,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_reconciliations') {
assert.deepEqual(args.args,
[
[{id: null, type: null,
mv_line_ids: [399, 402],
new_mv_line_dicts: []}
]
], "should call process_reconciliations 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();
});
});
});
@@ -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');
@@ -306,6 +307,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) {
@@ -315,7 +317,7 @@ var Feedback = Widget.extend({
// only display textearea if bad smiley selected
var close_chat = false;
if (this.rating === 0) {
if (this.rating === 1) {
this.$('.o_livechat_rating_reason').show();
} else {
this.$('.o_livechat_rating_reason').hide();
@@ -341,7 +343,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 content = _.str.sprintf(_t("Rating: :rating_%d"), self.rating);
if (options.reason) {
+1 -1
View File
@@ -192,6 +192,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
-1
View File
@@ -11,7 +11,6 @@
<group string="Logistics">
<field name="route_ids" widget="many2many_tags" groups="stock.group_adv_location"/>
<div colspan="2" attrs="{'invisible': [('parent_id', '=', False)]}">
The following routes will apply to the products in this category taking into account parent categories:
<field name="total_route_ids" nolabel="1" widget="many2many_tags"/>
</div>
<field name="removal_strategy_id" options="{'no_create': True}"/>
+2 -2
View File
@@ -240,8 +240,8 @@
<group>
<group>
<field name="partner_id" attrs="{'invisible': [('picking_type_code', '=', 'internal')]}"/>
<field name="location_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}"/>
<field name="location_dest_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}"/>
<field name="location_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}"/>
<field name="location_dest_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}"/>
<field name="backorder_id" readonly="1" attrs="{'invisible': [('backorder_id','=',False)]}"/>
</group>
<group>
@@ -548,10 +548,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') {
+2 -2
View File
@@ -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
@@ -69,7 +69,7 @@
<del class='text-danger'><t t-esc='wish.price' t-options="{'widget': 'monetary', 'display_currency': wish.currency_id}"/></del>
<br/>
</t>
<t t-esc='wish.price_new' t-options="{'widget': 'monetary', 'display_currency': wish.currency_id}"/>
<t t-esc="wish.product_id.website_price" t-options="{'widget': 'monetary', 'display_currency': website.pricelist_id.currency_id}"/>
</td>
<td class='text-center td-wish-btn'>
<input name="product_id" t-att-value="wish.product_id.id" type="hidden"/>
+11
View File
@@ -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
+1 -1
View File
@@ -695,7 +695,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
#
+1
View File
@@ -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
@@ -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))
+8 -5
View File
@@ -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):
</t>
"""
})
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):
+38 -26
View File
@@ -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):
+29 -106
View File
@@ -5051,114 +5051,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)
@@ -5177,6 +5091,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:
@@ -5202,14 +5117,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