[REM] *: discard old implementation of onchange

Part-of: odoo/odoo#133049
This commit is contained in:
Raphael Collet
2023-08-31 05:11:44 +00:00
parent 2dff83386d
commit 109935dbc1
12 changed files with 12 additions and 1514 deletions
-18
View File
@@ -2519,24 +2519,6 @@ class AccountMove(models.Model):
for move in self:
move.display_name = move._get_move_display_name(show_ref=True)
def onchange(self, values, field_name, field_onchange):
if field_name in ('line_ids', 'invoice_line_ids'):
# Since only one field can be changed at the same time (the record is saved when changing tabs)
# we can avoid building the snapshots for the other field
to_del = 'invoice_line_ids' if field_name == 'line_ids' else 'line_ids'
for key in list(field_onchange):
if key == to_del or key.startswith(f"{to_del}."):
del field_onchange[key]
# test_01_account_tour
# File "/data/build/odoo/addons/account/models/account_move.py", line 2127, in onchange
# del values[to_del]
# KeyError: 'line_ids'
values.pop(to_del, None)
if field_name and not isinstance(field_name, list):
field_name = [field_name]
with self.env.protecting([self._fields[fname] for fname in field_name or []], self):
return super().onchange(values, field_name, field_onchange)
def onchange2(self, values, field_names, fields_spec):
# Since only one field can be changed at the same time (the record is
# saved when changing tabs) we can avoid building the snapshots for the
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import TransactionCase
from odoo.exceptions import UserError
import odoo.tests
@odoo.tests.tagged('post_install', '-at_install')
class TestAutomation(TransactionCase):
@@ -32,7 +32,6 @@ class TestAutomation(TransactionCase):
bilbo.name = "Bilbo"
self.assertFalse(bilbo.active)
def test_02_on_create_restricted(self):
""" on_create action with low portal user """
action = self.env["base.automation"].create({
@@ -68,7 +67,6 @@ class TestAutomation(TransactionCase):
filters.name = "Where is Bilbo Baggins?"
self.assertFalse(filters.active)
def test_03_on_change_restricted(self):
""" on_create action with low portal user """
action = self.env["base.automation"].create({
@@ -86,5 +84,5 @@ class TestAutomation(TransactionCase):
self_portal = self.env["ir.filters"].with_user(self.env.ref("base.user_demo").id)
# simulate a onchange call on name
onchange = self_portal.onchange({}, [], {"name": "1", "active": ""})
self.assertEqual(onchange["value"]["active"], False)
result = self_portal.onchange2({}, [], {"name": {}, "active": {}})
self.assertEqual(result["value"]["active"], False)
-5
View File
@@ -1254,11 +1254,6 @@ class HrExpenseSheet(models.Model):
self = self.with_context(show_payment_journal_id=True)
return super()._read_format(fnames, load)
def onchange(self, values, field_name, field_onchange):
# setting the context in the field on the view is not enough
self = self.with_context(show_payment_journal_id=True)
return super().onchange(values, field_name, field_onchange)
@api.model_create_multi
def create(self, vals_list):
context = clean_context(self.env.context)
+3 -3
View File
@@ -840,14 +840,14 @@ class HolidaysRequest(models.Model):
start=display_date,
)
def onchange(self, values, field_name, field_onchange):
def onchange2(self, values, field_names, fields_spec):
# Try to force the leave_type display_name when creating new records
# This is called right after pressing create and returns the display_name for
# most fields in the view.
if field_onchange.get('employee_id') and 'employee_id' not in self._context and values:
if values and 'employee_id' in fields_spec and 'employee_id' not in self._context:
employee_id = get_employee_from_context(values, self._context, self.env.user.employee_id.id)
self = self.with_context(employee_id=employee_id)
return super().onchange(values, field_name, field_onchange)
return super().onchange2(values, field_names, fields_spec)
def add_follower(self, employee_id):
employee = self.env['hr.employee'].browse(employee_id)
@@ -538,14 +538,14 @@ class HolidaysAllocation(models.Model):
# ORM Overrides methods
####################################################
def onchange(self, values, field_name, field_onchange):
def onchange2(self, values, field_names, fields_spec):
# Try to force the leave_type display_name when creating new records
# This is called right after pressing create and returns the display_name for
# most fields in the view.
if field_onchange.get('employee_id') and 'employee_id' not in self._context and values:
if values and 'employee_id' in fields_spec and 'employee_id' not in self._context:
employee_id = get_employee_from_context(values, self._context, self.env.user.employee_id.id)
self = self.with_context(employee_id=employee_id)
return super().onchange(values, field_name, field_onchange)
return super().onchange2(values, field_names, fields_spec)
@api.depends(
'holiday_type', 'mode_company_id', 'department_id',
-12
View File
@@ -270,18 +270,6 @@ class PurchaseOrder(models.Model):
# To be overridden
return field_name == 'order_line'
def onchange(self, values, field_name, field_onchange):
"""Override onchange to NOT to update all date_planned on PO lines when
date_planned on PO is updated by the change of date_planned on PO lines.
"""
result = super(PurchaseOrder, self).onchange(values, field_name, field_onchange)
if self._must_delete_date_planned(field_name) and 'value' in result:
already_exist = [ol[1] for ol in values.get('order_line', []) if ol[1]]
for line in result['value'].get('order_line', []):
if line[0] < 2 and 'date_planned' in line[2] and line[1] in already_exist:
del line[2]['date_planned']
return result
def _get_report_base_filename(self):
self.ensure_one()
return 'Purchase Order-%s' % (self.name)
@@ -182,17 +182,6 @@ class StockPickingBatch(models.Model):
if any(batch.state == 'done' for batch in self):
raise UserError(_("You cannot delete Done batch transfers."))
def onchange(self, values, field_name, field_onchange):
"""Override onchange to NOT to update all scheduled_date on pickings when
scheduled_date on batch is updated by the change of scheduled_date on pickings.
"""
result = super().onchange(values, field_name, field_onchange)
if field_name == 'picking_ids' and 'value' in result:
for line in result['value'].get('picking_ids', []):
if line[0] < 2 and 'scheduled_date' in line[2]:
del line[2]['scheduled_date']
return result
# -------------------------------------------------------------------------
# Action methods
# -------------------------------------------------------------------------
-26
View File
@@ -1737,32 +1737,6 @@ class UsersView(models.Model):
valid_fields = partition(is_reified_group, fnames)[1]
return super()._read_format(valid_fields, load)
def onchange(self, values, field_name, field_onchange):
# field_name can be either a string, a list or Falsy
if isinstance(field_name, list):
names = field_name
elif field_name:
names = [field_name]
else:
names = []
if any(is_reified_group(field) for field in names):
field_name = (
['groups_id']
+ [field for field in names if not is_reified_group(field)]
)
values.pop('groups_id', None)
values.update(self._remove_reified_groups(values))
field_onchange['groups_id'] = ''
result = super().onchange(values, field_name, field_onchange)
if not field_name: # merged default_get
self._add_reified_groups(
filter(is_reified_group, field_onchange),
result.setdefault('value', {})
)
return result
def onchange2(self, values, field_names, fields_spec):
reified_fnames = [fname for fname in fields_spec if is_reified_group(fname)]
if reified_fnames:
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from . import test_new_fields
from . import test_onchange
from . import test_onchange2
from . import test_attributes
from . import test_one2many
@@ -1,959 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
from odoo.addons.base.tests.common import SavepointCaseWithUserDemo
from odoo.tests import common, Form
from odoo import Command
def strip_prefix(prefix, names):
size = len(prefix)
return [name[size:] for name in names if name.startswith(prefix)]
class TestOnChange(SavepointCaseWithUserDemo):
def setUp(self):
super(TestOnChange, self).setUp()
self.Discussion = self.env['test_new_api.discussion']
self.Message = self.env['test_new_api.message']
self.EmailMessage = self.env['test_new_api.emailmessage']
def test_default_get(self):
""" checking values returned by default_get() """
fields = ['name', 'categories', 'participants', 'messages']
values = self.Discussion.default_get(fields)
self.assertEqual(values, {})
user = self.env.user
field_onchange = self.env['test_new_api.message']._onchange_spec()
values = self.env['test_new_api.message'].onchange({}, [], field_onchange)['value']
self.assertEqual(values['discussion'], False)
self.assertEqual(values['body'], False)
self.assertEqual(values['author'], (user.id, user.display_name))
self.assertEqual(values['name'], f'[] {user.name}')
self.assertEqual(values['size'], 0)
def test_get_field(self):
""" checking that accessing an unknown attribute does nothing special """
with self.assertRaises(AttributeError):
self.Discussion.not_really_a_method()
def test_onchange(self):
""" test the effect of onchange() """
discussion = self.env.ref('test_new_api.discussion_0')
BODY = "What a beautiful day!"
USER = self.env.user
field_onchange = self.Message._onchange_spec()
self.assertEqual(field_onchange.get('author'), '1')
self.assertEqual(field_onchange.get('body'), '1')
self.assertEqual(field_onchange.get('discussion'), '1')
# changing 'discussion' should recompute 'name'
values = {
'discussion': discussion.id,
'name': "[%s] %s" % ('', USER.name),
'body': False,
'author': USER.id,
'size': 0,
}
self.env.invalidate_all()
result = self.Message.onchange(values, 'discussion', field_onchange)
self.assertIn('name', result['value'])
self.assertEqual(result['value']['name'], "[%s] %s" % (discussion.name, USER.name))
# changing 'body' should recompute 'size'
values = {
'discussion': discussion.id,
'name': "[%s] %s" % (discussion.name, USER.name),
'body': BODY,
'author': USER.id,
'size': 0,
}
self.env.invalidate_all()
result = self.Message.onchange(values, 'body', field_onchange)
self.assertIn('size', result['value'])
self.assertEqual(result['value']['size'], len(BODY))
# changing 'body' should not recompute 'name', even if 'discussion' and
# 'name' are not consistent with each other
values = {
'discussion': discussion.id,
'name': False,
'body': BODY,
'author': USER.id,
'size': 0,
}
self.env.invalidate_all()
result = self.Message.onchange(values, 'body', field_onchange)
self.assertNotIn('name', result['value'])
def test_onchange_many2one(self):
Category = self.env['test_new_api.category']
field_onchange = Category._onchange_spec()
self.assertEqual(field_onchange.get('parent'), '1')
root = Category.create(dict(name='root'))
values = {
'name': 'test',
'parent': root.id,
'root_categ': False,
}
self.env.invalidate_all()
result = Category.onchange(values, 'parent', field_onchange).get('value', {})
self.assertIn('root_categ', result)
self.assertEqual(result['root_categ'], (root.id, root.display_name))
values.update(result)
values['parent'] = False
self.env.invalidate_all()
result = Category.onchange(values, 'parent', field_onchange).get('value', {})
self.assertIn('root_categ', result)
self.assertIs(result['root_categ'], False)
def test_onchange_one2many(self):
""" test the effect of onchange() on one2many fields """
USER = self.env.user
# create an independent message
message1 = self.Message.create({'body': "ABC"})
message2 = self.Message.create({'body': "ABC"})
self.assertEqual(message1.name, "[%s] %s" % ('', USER.name))
field_onchange = self.Discussion._onchange_spec()
self.assertEqual(field_onchange.get('name'), '1')
self.assertEqual(field_onchange.get('messages'), '1')
self.assertItemsEqual(
strip_prefix('messages.', field_onchange),
['author', 'body', 'name', 'size', 'important'],
)
# modify discussion name
values = {
'name': "Foo",
'categories': [],
'moderator': False,
'participants': [],
'messages': [
Command.link(message1.id),
Command.link(message2.id),
Command.update(message2.id, {'body': "XYZ"}),
Command.create({
'name': "[%s] %s" % ('', USER.name),
'body': "ABC",
'author': USER.id,
'size': 3,
'important': False,
}),
],
}
self.env.invalidate_all()
result = self.Discussion.onchange(values, 'name', field_onchange)
self.assertIn('messages', result['value'])
self.assertEqual(result['value']['messages'], [
Command.clear(),
Command.update(message1.id, {
'name': "[%s] %s" % ("Foo", USER.name),
'body': "ABC",
'author': (USER.id, USER.display_name),
'size': 3,
'important': False,
}),
Command.update(message2.id, {
'name': "[%s] %s" % ("Foo", USER.name),
'body': "XYZ", # this must be sent back
'author': (USER.id, USER.display_name),
'size': 3,
'important': False,
}),
Command.create({
'name': "[%s] %s" % ("Foo", USER.name),
'body': "ABC",
'author': (USER.id, USER.display_name),
'size': 3,
'important': False,
}),
])
# ensure onchange changing one2many without subfield works
one_level_fields = {k: v for k, v in field_onchange.items() if k.count('.') < 1}
values = dict(values, name='{generate_dummy_message}')
result = self.Discussion.with_context(generate_dummy_message=True).onchange(values, 'name', one_level_fields)
self.assertEqual(result['value']['messages'], [
Command.clear(),
Command.link(message1.id),
Command.link(message2.id),
Command.create({}),
Command.create({}),
])
def test_onchange_one2many_reference(self):
""" test the effect of onchange() on one2many fields with line references """
BODY = "What a beautiful day!"
USER = self.env.user
REFERENCE = "virtualid42"
field_onchange = self.Discussion._onchange_spec()
self.assertEqual(field_onchange.get('name'), '1')
self.assertEqual(field_onchange.get('messages'), '1')
self.assertItemsEqual(
strip_prefix('messages.', field_onchange),
['author', 'body', 'name', 'size', 'important'],
)
# modify discussion name, and check that the reference of the new line
# is returned
values = {
'name': "Foo",
'categories': [],
'moderator': False,
'participants': [],
'messages': [
(0, REFERENCE, {
'name': "[%s] %s" % ('', USER.name),
'body': BODY,
'author': USER.id,
'size': len(BODY),
'important': False,
}),
],
}
self.env.invalidate_all()
result = self.Discussion.onchange(values, 'name', field_onchange)
self.assertIn('messages', result['value'])
self.assertItemsEqual(result['value']['messages'], [
(5, 0, 0),
(0, REFERENCE, {
'name': "[%s] %s" % ("Foo", USER.name),
'body': BODY,
'author': (USER.id, USER.display_name),
'size': len(BODY),
'important': False,
}),
])
def test_onchange_one2many_multi(self):
""" test the effect of multiple onchange methods on one2many fields """
partner1 = self.env['res.partner'].create({'name': 'A partner'})
multi = self.env['test_new_api.multi'].create({'partner': partner1.id})
line1 = multi.lines.create({'multi': multi.id})
field_onchange = multi._onchange_spec()
self.assertEqual(field_onchange, {
'name': '1',
'partner': '1',
'lines': None,
'lines.name': None,
'lines.partner': None,
'lines.tags': None,
'lines.tags.name': None,
})
values = multi._convert_to_write({key: multi[key] for key in ('name', 'partner', 'lines')})
self.assertEqual(values, {
'name': partner1.name,
'partner': partner1.id,
'lines': [Command.set([line1.id])],
})
# modify 'partner'
# -> set 'partner' on all lines
# -> recompute 'name'
# -> set 'name' on all lines
partner2 = self.env['res.partner'].create({'name': 'A second partner'})
values = {
'name': partner1.name,
'partner': partner2.id, # this one just changed
'lines': [Command.set([line1.id]),
Command.create({'name': False, 'partner': False, 'tags': [Command.clear()]})],
}
self.env.invalidate_all()
result = multi.onchange(values, 'partner', field_onchange)
self.assertEqual(result['value'], {
'name': partner2.name,
'lines': [
Command.clear(),
Command.update(line1.id, {
'name': partner2.name,
'partner': (partner2.id, partner2.name),
'tags': [Command.clear()],
}),
Command.create({
'name': partner2.name,
'partner': (partner2.id, partner2.name),
'tags': [Command.clear()],
}),
],
})
# do it again, but this time with a new tag on the second line
values = {
'name': partner1.name,
'partner': partner2.id, # this one just changed
'lines': [Command.set([line1.id]),
Command.create({'name': False,
'partner': False,
'tags': [Command.clear(), Command.create({'name': 'Tag'})]})],
}
self.env.invalidate_all()
result = multi.onchange(values, 'partner', field_onchange)
expected_value = {
'name': partner2.name,
'lines': [
Command.clear(),
Command.update(line1.id, {
'name': partner2.name,
'partner': (partner2.id, partner2.name),
'tags': [Command.clear()],
}),
Command.create({
'name': partner2.name,
'partner': (partner2.id, partner2.name),
'tags': [Command.clear(), Command.create({'name': 'Tag'})],
}),
],
}
self.assertEqual(result['value'], expected_value)
# ensure ID is not returned when asked and a many2many record is set to be created
self.env.invalidate_all()
result = multi.onchange(values, 'partner', dict(field_onchange, **{'lines.tags.id': None}))
self.assertEqual(result['value'], expected_value)
# ensure inverse of one2many field is not returned
self.env.invalidate_all()
result = multi.onchange(values, 'partner', dict(field_onchange, **{'lines.multi': None}))
self.assertEqual(result['value'], expected_value)
def test_onchange_specific(self):
""" test the effect of field-specific onchange method """
discussion = self.env.ref('test_new_api.discussion_0')
demo = self.user_demo
field_onchange = self.Discussion._onchange_spec()
self.assertEqual(field_onchange.get('moderator'), '1')
self.assertItemsEqual(
strip_prefix('participants.', field_onchange),
['display_name'],
)
# first remove demo user from participants
discussion.participants -= demo
self.assertNotIn(demo, discussion.participants)
# check that demo_user is added to participants when set as moderator
values = {
'name': discussion.name,
'moderator': demo.id,
'categories': [Command.link(cat.id) for cat in discussion.categories],
'messages': [Command.link(msg.id) for msg in discussion.messages],
'participants': [Command.link(usr.id) for usr in discussion.participants],
}
self.env.invalidate_all()
result = discussion.onchange(values, 'moderator', field_onchange)
self.assertIn('participants', result['value'])
self.assertItemsEqual(
result['value']['participants'],
[Command.clear()] + [Command.link(user.id) for user in discussion.participants + demo],
)
def test_onchange_default(self):
""" test the effect of a conditional user-default on a field """
Foo = self.env['test_new_api.foo']
field_onchange = Foo._onchange_spec()
self.assertTrue(Foo._fields['value1'].change_default)
self.assertEqual(field_onchange.get('value1'), '1')
# create a user-defined default based on 'value1'
self.env['ir.default'].set('test_new_api.foo', 'value2', 666, condition='value1=42')
# setting 'value1' to 42 should trigger the change of 'value2'
self.env.invalidate_all()
values = {'name': 'X', 'value1': 42, 'value2': False}
result = Foo.onchange(values, 'value1', field_onchange)
self.assertEqual(result['value'], {'value2': 666})
# setting 'value1' to 24 should not trigger the change of 'value2'
self.env.invalidate_all()
values = {'name': 'X', 'value1': 24, 'value2': False}
result = Foo.onchange(values, 'value1', field_onchange)
self.assertEqual(result['value'], {})
def test_onchange_one2many_first(self):
partner = self.env['res.partner'].create({
'name': 'X',
'country_id': self.env.ref('base.be').id,
})
with common.Form(self.env['test_new_api.multi']) as form:
form.partner = partner
self.assertEqual(form.partner, partner)
self.assertEqual(form.name, partner.name)
with form.lines.new() as line:
# the first onchange() must have computed partner
self.assertEqual(line.partner, partner)
def test_onchange_one2many_value(self):
""" test the value of the one2many field inside the onchange """
discussion = self.env.ref('test_new_api.discussion_0')
demo = self.user_demo
field_onchange = self.Discussion._onchange_spec()
self.assertEqual(field_onchange.get('messages'), '1')
self.assertEqual(len(discussion.messages), 3)
messages = [Command.link(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,
'categories': [Command.link(cat.id) for cat in discussion.categories],
'messages': messages,
'participants': [Command.link(usr.id) for usr in discussion.participants],
'message_concat': False,
}
result = discussion.onchange(values, 'messages', field_onchange)
self.assertIn('message_concat', result['value'])
self.assertEqual(result['value']['message_concat'], "\n".join(lines))
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"""
discussion = self.env.ref('test_new_api.discussion_0')
demo = self.user_demo
# mimic UI behaviour, so we get subfields
# (we need at least subfield: 'important_emails.important')
view_info = self.Discussion.get_view(self.env.ref('test_new_api.discussion_form').id, 'form')
field_onchange = self.Discussion._onchange_spec(view_info=view_info)
self.assertEqual(field_onchange.get('messages'), '1')
BODY = "What a beautiful day!"
USER = self.env.user
# create standalone email
email = self.EmailMessage.create({
'discussion': discussion.id,
'name': "[%s] %s" % ('', USER.name),
'body': BODY,
'author': USER.id,
'important': False,
'email_to': demo.email,
})
# check if server-side cache is working correctly
self.env.invalidate_all()
self.assertIn(email, discussion.emails)
self.assertNotIn(email, discussion.important_emails)
email.important = True
self.assertIn(email, discussion.important_emails)
# check that when trigger an onchange, we don't reset important emails
# (force `invalidate` as but appear in onchange only when we get a cache
# miss)
self.env.invalidate_all()
self.assertEqual(len(discussion.messages), 4)
values = {
'name': "Foo Bar",
'moderator': demo.id,
'categories': [Command.link(cat.id) for cat in discussion.categories],
'messages': [Command.link(msg.id) for msg in discussion.messages],
'participants': [Command.link(usr.id) for usr in discussion.participants],
'important_messages': [Command.link(msg.id) for msg in discussion.important_messages],
'important_emails': [Command.link(eml.id) for eml in discussion.important_emails],
}
self.env.invalidate_all()
result = discussion.onchange(values, 'name', field_onchange)
self.assertEqual(
result['value']['important_emails'],
[Command.clear(), Command.update(email.id, {
'name': u'[Foo Bar] %s' % USER.name,
'body': BODY,
'author': (USER.id, USER.display_name),
'size': len(BODY),
'important': True,
'email_to': demo.email,
})],
)
def test_onchange_related(self):
value = {
'message': 1,
'message_name': False,
'message_currency': 2,
}
field_onchange = {
'message': '1',
'message_name': None,
'message_currency': None,
}
onchange_result = {
'message_name': 'Hey dude!',
'message_currency': (self.env.user.id, self.env.user.display_name),
}
self.env.invalidate_all()
Message = self.env['test_new_api.related']
result = Message.onchange(value, 'message', field_onchange)
self.assertEqual(result['value'], onchange_result)
self.env.invalidate_all()
Message = self.env(user=self.user_demo.id)['test_new_api.related']
result = Message.onchange(value, 'message', field_onchange)
self.assertEqual(result['value'], onchange_result)
def test_onchange_many2one_one2many(self):
""" Setting a many2one field should not read the inverse one2many. """
discussion = self.env.ref('test_new_api.discussion_0')
field_onchange = self.Message._onchange_spec()
self.assertEqual(field_onchange.get('discussion'), '1')
values = {
'discussion': discussion.id,
'name': "[%s] %s" % ('', self.env.user.name),
'body': False,
'author': self.env.uid,
'size': 0,
}
called = [False]
orig_read = type(discussion).read
def mock_read(self, fields=None, load='_classic_read'):
if discussion in self and 'messages' in (fields or ()):
called[0] = True
return orig_read(self, fields, load)
# changing 'discussion' on message should not read 'messages' on discussion
with patch.object(type(discussion), 'read', mock_read, create=True):
self.env.invalidate_all()
self.Message.onchange(values, 'discussion', field_onchange)
self.assertFalse(called[0], "discussion.messages has been read")
def test_onchange_one2many_many2one_in_form(self):
order = self.env['test_new_api.monetary_order'].create({
'currency_id': self.env.ref('base.USD').id,
})
# this call to onchange() is made when creating a new line in field
# order.line_ids; check what happens when the line's form view contains
# the inverse many2one field
values = {'order_id': {'id': order.id, 'currency_id': order.currency_id.id}}
field_onchange = dict.fromkeys(['order_id', 'subtotal'], '')
result = self.env['test_new_api.monetary_order_line'].onchange(values, [], field_onchange)
self.assertEqual(result['value']['order_id'], (order.id, order.display_name))
def test_onchange_inherited(self):
""" Setting an inherited field should assign the field on the parent record. """
foo, bar = self.env['test_new_api.multi.tag'].create([{'name': 'Foo'}, {'name': 'Bar'}])
view = self.env['ir.ui.view'].create({
'name': 'Payment form view',
'model': 'test_new_api.payment',
'arch': """
<form>
<field name="move_id" readonly="1" required="0"/>
<field name="tag_id"/>
<field name="tag_name"/>
<field name="tag_repeat"/>
<field name="tag_string"/>
</form>
""",
})
# both fields 'tag_id' and 'tag_name' are inherited through 'move_id';
# assigning 'tag_id' should modify 'move_id.tag_id' accordingly, which
# should in turn recompute `move.tag_name` and `tag_name`
form = Form(self.env['test_new_api.payment'], view)
self.assertEqual(form.tag_name, False)
form.tag_id = foo
self.assertEqual(form.tag_name, 'Foo')
self.assertEqual(form.tag_string, '')
form.tag_repeat = 2
self.assertEqual(form.tag_name, 'Foo')
self.assertEqual(form.tag_string, 'FooFoo')
payment = form.save()
self.assertEqual(payment.tag_id, foo)
self.assertEqual(payment.tag_name, 'Foo')
self.assertEqual(payment.tag_repeat, 2)
self.assertEqual(payment.tag_string, 'FooFoo')
with Form(payment, view) as form:
form.tag_id = bar
self.assertEqual(form.tag_name, 'Bar')
self.assertEqual(form.tag_string, 'BarBar')
form.tag_repeat = 3
self.assertEqual(form.tag_name, 'Bar')
self.assertEqual(form.tag_string, 'BarBarBar')
self.assertEqual(payment.tag_id, bar)
self.assertEqual(payment.tag_name, 'Bar')
self.assertEqual(payment.tag_repeat, 3)
self.assertEqual(payment.tag_string, 'BarBarBar')
def test_display_name(self):
self.env['ir.ui.view'].create({
'name': 'test_new_api.multi.tag form view',
'model': 'test_new_api.multi.tag',
'arch': """
<form>
<field name="name"/>
<field name="display_name"/>
</form>
""",
})
form = common.Form(self.env['test_new_api.multi.tag'])
self.assertEqual(form.name, False)
self.assertEqual(form.display_name, "")
record = form.save()
self.assertEqual(record.name, False)
self.assertEqual(record.display_name, "")
class TestComputeOnchange(common.TransactionCase):
def test_create(self):
model = self.env['test_new_api.compute.onchange']
# compute 'bar' (readonly) and 'baz' (editable)
record = model.create({'active': True})
self.assertEqual(record.bar, "r")
self.assertEqual(record.baz, "z")
# compute 'bar' and 'baz'
record = model.create({'active': True, 'foo': "foo"})
self.assertEqual(record.bar, "foor")
self.assertEqual(record.baz, "fooz")
# compute 'bar' but not 'baz'
record = model.create({'active': True, 'foo': "foo", 'bar': "bar", 'baz': "baz"})
self.assertEqual(record.bar, "foor")
self.assertEqual(record.baz, "baz")
# compute 'bar' and 'baz', but do not change its value
record = model.create({'active': False, 'foo': "foo"})
self.assertEqual(record.bar, "foor")
self.assertEqual(record.baz, False)
# compute 'bar' but not 'baz'
record = model.create({'active': False, 'foo': "foo", 'bar': "bar", 'baz': "baz"})
self.assertEqual(record.bar, "foor")
self.assertEqual(record.baz, "baz")
def test_copy(self):
Model = self.env['test_new_api.compute.onchange']
# create tags
tag_foo, tag_bar = self.env['test_new_api.multi.tag'].create([
{'name': 'foo1'},
{'name': 'bar1'},
])
# compute 'bar' (readonly), 'baz', 'line_ids' and 'tag_ids' (editable)
record = Model.create({'active': True, 'foo': "foo1"})
self.assertEqual(record.bar, "foo1r")
self.assertEqual(record.baz, "foo1z")
self.assertEqual(record.line_ids.mapped('foo'), ['foo1'])
self.assertEqual(record.tag_ids, tag_foo)
# manually update 'baz' and 'lines' to test copy attribute
record.write({
'baz': "baz1",
'line_ids': [Command.create({'foo': 'bar'})],
'tag_ids': [Command.link(tag_bar.id)],
})
self.assertEqual(record.bar, "foo1r")
self.assertEqual(record.baz, "baz1")
self.assertEqual(record.line_ids.mapped('foo'), ['foo1', 'bar'])
self.assertEqual(record.tag_ids, tag_foo + tag_bar)
# copy the record, and check results
copied = record.copy()
self.assertEqual(copied.foo, "foo1 (copy)") # copied and modified
self.assertEqual(copied.bar, "foo1 (copy)r") # computed
self.assertEqual(copied.baz, "baz1") # copied
self.assertEqual(record.line_ids.mapped('foo'), ['foo1', 'bar']) # copied
self.assertEqual(record.tag_ids, tag_foo + tag_bar) # copied
def test_write(self):
model = self.env['test_new_api.compute.onchange']
record = model.create({'active': True, 'foo': "foo"})
self.assertEqual(record.bar, "foor")
self.assertEqual(record.baz, "fooz")
# recompute 'bar' (readonly) and 'baz' (editable)
record.write({'foo': "foo1"})
self.assertEqual(record.bar, "foo1r")
self.assertEqual(record.baz, "foo1z")
# recompute 'bar' but not 'baz'
record.write({'foo': "foo2", 'bar': "bar2", 'baz': "baz2"})
self.assertEqual(record.bar, "foo2r")
self.assertEqual(record.baz, "baz2")
# recompute 'bar' and 'baz', but do not change its value
record.write({'active': False, 'foo': "foo3"})
self.assertEqual(record.bar, "foo3r")
self.assertEqual(record.baz, "baz2")
# recompute 'bar' but not 'baz'
record.write({'active': False, 'foo': "foo4", 'bar': "bar4", 'baz': "baz4"})
self.assertEqual(record.bar, "foo4r")
self.assertEqual(record.baz, "baz4")
def test_set(self):
model = self.env['test_new_api.compute.onchange']
record = model.create({'active': True, 'foo': "foo"})
self.assertEqual(record.bar, "foor")
self.assertEqual(record.baz, "fooz")
# recompute 'bar' (readonly) and 'baz' (editable)
record.foo = "foo1"
self.assertEqual(record.bar, "foo1r")
self.assertEqual(record.baz, "foo1z")
# do not recompute 'baz'
record.baz = "baz2"
self.assertEqual(record.bar, "foo1r")
self.assertEqual(record.baz, "baz2")
# recompute 'baz', but do not change its value
record.active = False
self.assertEqual(record.bar, "foo1r")
self.assertEqual(record.baz, "baz2")
# recompute 'baz', but do not change its value
record.foo = "foo3"
self.assertEqual(record.bar, "foo3r")
self.assertEqual(record.baz, "baz2")
# do not recompute 'baz'
record.baz = "baz4"
self.assertEqual(record.bar, "foo3r")
self.assertEqual(record.baz, "baz4")
def test_set_new(self):
model = self.env['test_new_api.compute.onchange']
record = model.new({'active': True})
self.assertEqual(record.bar, "r")
self.assertEqual(record.baz, "z")
# recompute 'bar' (readonly) and 'baz' (editable)
record.foo = "foo1"
self.assertEqual(record.bar, "foo1r")
self.assertEqual(record.baz, "foo1z")
# do not recompute 'baz'
record.baz = "baz2"
self.assertEqual(record.bar, "foo1r")
self.assertEqual(record.baz, "baz2")
# recompute 'baz', but do not change its value
record.active = False
self.assertEqual(record.bar, "foo1r")
self.assertEqual(record.baz, "baz2")
# recompute 'baz', but do not change its value
record.foo = "foo3"
self.assertEqual(record.bar, "foo3r")
self.assertEqual(record.baz, "baz2")
# do not recompute 'baz'
record.baz = "baz4"
self.assertEqual(record.bar, "foo3r")
self.assertEqual(record.baz, "baz4")
def test_onchange(self):
# check computations of 'bar' (readonly) and 'baz' (editable)
form = common.Form(self.env['test_new_api.compute.onchange'])
self.assertEqual(form.bar, "r")
self.assertEqual(form.baz, False)
form.active = True
self.assertEqual(form.bar, "r")
self.assertEqual(form.baz, "z")
form.foo = "foo1"
self.assertEqual(form.bar, "foo1r")
self.assertEqual(form.baz, "foo1z")
form.baz = "baz2"
self.assertEqual(form.bar, "foo1r")
self.assertEqual(form.baz, "baz2")
form.active = False
self.assertEqual(form.bar, "foo1r")
self.assertEqual(form.baz, "baz2")
form.foo = "foo3"
self.assertEqual(form.bar, "foo3r")
self.assertEqual(form.baz, "baz2")
form.active = True
self.assertEqual(form.bar, "foo3r")
self.assertEqual(form.baz, "foo3z")
with form.line_ids.new() as line:
# check computation of 'bar' (readonly)
self.assertEqual(line.foo, False)
self.assertEqual(line.bar, "r")
line.foo = "foo"
self.assertEqual(line.foo, "foo")
self.assertEqual(line.bar, "foor")
record = form.save()
self.assertEqual(record.bar, "foo3r")
self.assertEqual(record.baz, "foo3z")
form = common.Form(record)
self.assertEqual(form.bar, "foo3r")
self.assertEqual(form.baz, "foo3z")
form.foo = "foo4"
self.assertEqual(form.bar, "foo4r")
self.assertEqual(form.baz, "foo4z")
form.baz = "baz5"
self.assertEqual(form.bar, "foo4r")
self.assertEqual(form.baz, "baz5")
form.active = False
self.assertEqual(form.bar, "foo4r")
self.assertEqual(form.baz, "baz5")
form.foo = "foo6"
self.assertEqual(form.bar, "foo6r")
self.assertEqual(form.baz, "baz5")
def test_onchange_default(self):
form = common.Form(self.env['test_new_api.compute.onchange'].with_context(
default_active=True, default_foo="foo", default_baz="baz",
))
# 'baz' is computed editable, so when given a default value it should
# 'not be recomputed, even if a dependency also has a default value
self.assertEqual(form.foo, "foo")
self.assertEqual(form.bar, "foor")
self.assertEqual(form.baz, "baz")
def test_onchange_once(self):
""" Modifies `foo` field which will trigger an onchange method and
checks it was triggered only one time. """
form = Form(self.env['test_new_api.compute.onchange'].with_context(default_foo="oof"))
record = form.save()
self.assertEqual(record.foo, "oof")
self.assertEqual(record.count, 1, "value onchange must be called only one time")
def test_onchange_one2many(self):
record = self.env['test_new_api.model_parent_m2o'].create({
'name': 'Family',
'child_ids': [
Command.create({'name': 'W', 'cost': 10}),
Command.create({'name': 'X', 'cost': 10}),
Command.create({'name': 'Y'}),
Command.create({'name': 'Z'}),
],
})
self.env.flush_all()
self.assertEqual(record.child_ids.mapped('name'), list('WXYZ'))
self.assertEqual(record.cost, 22)
# modifying a line should not recompute the cost on other lines
with common.Form(record) as form:
with form.child_ids.edit(1) as line:
line.name = 'XXX'
self.assertEqual(form.cost, 15)
with form.child_ids.edit(1) as line:
line.cost = 20
self.assertEqual(form.cost, 32)
with form.child_ids.edit(2) as line:
line.cost = 30
self.assertEqual(form.cost, 61)
def test_onchange_editable_compute_one2many(self):
# create a record with a computed editable field ('edit') on lines
record = self.env['test_new_api.compute_editable'].create({'line_ids': [(0, 0, {'value': 7})]})
self.env.flush_all()
line = record.line_ids
self.assertRecordValues(line, [{'value': 7, 'edit': 7, 'count': 0}])
# retrieve the onchange spec for calling 'onchange'
spec = Form(record)._view['onchange']
# The onchange on 'line_ids' should increment 'count' and keep the value
# of 'edit' (this field should not be recomputed), whatever the order of
# the fields in the dictionary. This ensures that the value set by the
# user on a computed editable field on a line is not lost.
line_ids = [
Command.update(line.id, {'value': 8, 'edit': 9, 'count': 0}),
Command.create({'value': 8, 'edit': 9, 'count': 0}),
]
result = record.onchange({'line_ids': line_ids}, 'line_ids', spec)
expected = {'value': {
'line_ids': [
Command.clear(),
Command.update(line.id, {'value': 8, 'edit': 9, 'count': 8}),
Command.create({'value': 8, 'edit': 9, 'count': 8}),
],
}}
self.assertEqual(result, expected)
# change dict order in lines, and try again
line_ids = [
(op, id_, dict(reversed(list(vals.items()))))
for op, id_, vals in line_ids
]
result = record.onchange({'line_ids': line_ids}, 'line_ids', spec)
self.assertEqual(result, expected)
def test_computed_editable_one2many_domain(self):
""" Test a computed, editable one2many field with a domain. """
record = self.env['test_new_api.one2many'].create({'name': 'foo'})
self.assertRecordValues(record.line_ids, [
{'name': 'foo', 'count': 1},
])
# trigger recomputation by changing name
record.name = 'bar'
self.assertRecordValues(record.line_ids, [
{'name': 'foo', 'count': 1},
{'name': 'bar', 'count': 1},
])
# manually adding a line should not trigger recomputation
record.line_ids.create({'name': 'baz', 'container_id': record.id})
self.assertRecordValues(record.line_ids, [
{'name': 'foo', 'count': 1},
{'name': 'bar', 'count': 1},
{'name': 'baz', 'count': 1},
])
# changing the field in the domain should not trigger recomputation...
record.line_ids[-1].count = 2
self.assertRecordValues(record.line_ids, [
{'name': 'foo', 'count': 1},
{'name': 'bar', 'count': 1},
{'name': 'baz', 'count': 2},
])
# ...and may show cache inconsistencies
record.line_ids[-1].count = 0
self.assertRecordValues(record.line_ids, [
{'name': 'foo', 'count': 1},
{'name': 'bar', 'count': 1},
{'name': 'baz', 'count': 0},
])
self.env.flush_all()
self.env.invalidate_all()
self.assertRecordValues(record.line_ids, [
{'name': 'foo', 'count': 1},
{'name': 'bar', 'count': 1},
])
@@ -1302,171 +1302,6 @@ class PropertiesCase(TestPropertiesMixin):
{'name': 'state', 'type': 'datetime'},
]
@mute_logger('odoo.fields')
def test_properties_field_onchange(self):
"""If we change the definition record, the onchange of the properties field must be triggered."""
message_form = Form(self.env['test_new_api.message'])
with self.assertQueryCount(10):
message_form.discussion = self.discussion_1
message_form.author = self.user
self.assertEqual(
message_form.attributes,
[{
'name': 'discussion_color_code',
'string': 'Color Code',
'type': 'char',
'default': 'blue',
'value': 'blue',
}, {
'name': 'moderator_partner_id',
'string': 'Partner',
'type': 'many2one',
'comodel': 'test_new_api.partner',
'value': False,
}],
msg='Should take the new definition when changing the definition record',
)
# change the discussion field
message_form.discussion = self.discussion_2
properties = message_form.attributes
self.assertEqual(len(properties), 1)
self.assertEqual(
properties[0]['name'],
'state',
msg='Should take the values of the new definition record',
)
with self.assertQueryCount(6):
message = message_form.save()
self.assertEqual(
message.attributes,
{'state': 'draft'},
msg='Should take the default value',
)
# check cached value
cached_value = self.env.cache.get(message, message._fields['attributes'])
self.assertEqual(cached_value, {'state': 'draft'})
# change the definition record, change the definition and add default values
self.assertEqual(message.discussion, self.discussion_2)
with self.assertQueryCount(4):
message.discussion = self.discussion_1
self.assertEqual(
self.discussion_1.attributes_definition,
[{
'name': 'discussion_color_code',
'type': 'char',
'string': 'Color Code',
'default': 'blue',
}, {
'name': 'moderator_partner_id',
'type': 'many2one',
'string': 'Partner',
'comodel': 'test_new_api.partner',
}],
)
self.assertEqual(
message.attributes,
{'discussion_color_code': 'blue', 'moderator_partner_id': False},
)
self.discussion_1.attributes_definition = False
self.discussion_2.attributes_definition = [{
'name': 'test',
'type': 'char',
'default': 'Default',
}]
# change the message discussion to remove the properties
# discussion 1 -> discussion 2
message.discussion = self.discussion_2
message.attributes = [{'name': 'test', 'value': 'Test'}]
onchange_values = message.onchange(
values={
'discussion': self.discussion_1.id,
'attributes': [{
'name': 'test',
'type': 'char',
'default': 'Default',
'value': 'Test',
}],
},
field_name=['discussion'],
field_onchange={'discussion': '1', 'attributes': '1'},
)
self.assertTrue(
'attributes' in onchange_values['value'],
msg='Should have detected the definition record change')
self.assertEqual(
onchange_values['value']['attributes'], [],
msg='Should have reset the properties definition')
# change the message discussion to add new properties
# discussion 2 -> discussion 1
message.discussion = self.discussion_1
onchange_values = message.onchange(
values={
'discussion': self.discussion_2.id,
'attributes': [],
},
field_name=['discussion'],
field_onchange={'discussion': '1', 'attributes': '1'},
)
self.assertTrue(
'attributes' in onchange_values['value'],
msg='Should have detected the definition record change')
self.assertEqual(
onchange_values['value']['attributes'],
[{'name': 'test', 'type': 'char', 'default': 'Default', 'value': 'Default'}],
msg='Should have reset the properties definition to the discussion 1 definition')
# change the definition record and the definition at the same time
message_form = Form(message)
message_form.discussion = self.discussion_2
message_form.attributes = [{
'name': 'new_property',
'type': 'char',
'value': 'test value',
'definition_changed': True,
}]
message = message_form.save()
self.assertEqual(
self.discussion_2.attributes_definition,
[{'name': 'new_property', 'type': 'char'}])
self.assertEqual(
message.attributes,
{'new_property': 'test value'})
# re-write the same parent again and check that value are not reset
message.discussion = message.discussion
self.assertEqual(
message.attributes,
{'new_property': 'test value'})
# trigger a other onchange after setting the properties
# and check that it does not impact the properties
message.discussion.attributes_definition = []
message_form = Form(message)
message.attributes = [{
'name': 'new_property',
'type': 'char',
'value': 'test value',
'definition_changed': True,
}]
message_form.body = "a" * 42
message = message_form.save()
self.assertEqual(
message.attributes,
{'new_property': 'test value'})
@mute_logger('odoo.fields')
def test_properties_field_onchange2(self):
"""If we change the definition record, the onchange of the properties field must be triggered."""
+3 -306
View File
@@ -42,7 +42,7 @@ from collections.abc import MutableMapping
from contextlib import closing
from inspect import getmembers, currentframe
from operator import attrgetter, itemgetter
from typing import List
from typing import Dict, List
import babel
import babel.dates
@@ -6443,311 +6443,8 @@ class BaseModel(metaclass=MetaModel):
res['warning'].get('type') or "",
))
def onchange(self, values, field_name, field_onchange):
""" Perform an onchange on the given field.
:param values: dictionary mapping field names to values, giving the
current state of modification
:param field_name: name of the modified field, or list of field
names (in view order), or False
:param field_onchange: dictionary mapping field names to their
on_change attribute
When ``field_name`` is falsy, the method first adds default values
to ``values``, computes the remaining fields, applies onchange
methods to them, and return all the fields in ``field_onchange``.
"""
# this is for tests using `Form`
self.env.flush_all()
env = self.env
if isinstance(field_name, list):
names = field_name
elif field_name:
names = [field_name]
else:
names = []
first_call = not names
if any(name not in self._fields for name in names):
return {}
def PrefixTree(model, dotnames):
""" Return a prefix tree for sequences of field names. """
if not dotnames:
return {}
# group dotnames by prefix
suffixes = defaultdict(list)
for dotname in dotnames:
# name, *names = dotname.split('.', 1)
names = dotname.split('.', 1)
name = names.pop(0)
suffixes[name].extend(names)
# fill in prefix tree in fields order
tree = OrderedDict()
for name, field in model._fields.items():
if name in suffixes:
tree[name] = subtree = PrefixTree(model[name], suffixes[name])
if subtree and field.type == 'one2many':
subtree.pop(field.inverse_name, None)
return tree
class Snapshot(dict):
""" A dict with the values of a record, following a prefix tree. """
__slots__ = ()
def __init__(self, record, tree, fetch=True):
# put record in dict to include it when comparing snapshots
super(Snapshot, self).__init__({'<record>': record, '<tree>': tree})
if fetch:
for name in tree:
self.fetch(name)
def fetch(self, name):
""" Set the value of field ``name`` from the record's value. """
record = self['<record>']
tree = self['<tree>']
if record._fields[name].type in ('one2many', 'many2many'):
# x2many fields are serialized as a list of line snapshots
self[name] = [Snapshot(line, tree[name]) for line in record[name]]
else:
self[name] = record[name]
def has_changed(self, name):
""" Return whether a field on record has changed. """
if name not in self:
return True
record = self['<record>']
subnames = self['<tree>'][name]
if record._fields[name].type not in ('one2many', 'many2many'):
return self[name] != record[name]
return (
len(self[name]) != len(record[name])
or (
set(line_snapshot["<record>"].id for line_snapshot in self[name])
!= set(record[name]._ids)
)
or any(
line_snapshot.has_changed(subname)
for line_snapshot in self[name]
for subname in subnames
)
)
def diff(self, other, force=False):
""" Return the values in ``self`` that differ from ``other``.
Requires record cache invalidation for correct output!
"""
record = self['<record>']
result = {}
for name, subnames in self['<tree>'].items():
if name == 'id':
continue
if not force and other.get(name) == self[name]:
continue
field = record._fields[name]
if field.type == 'properties':
result[name] = field.convert_to_onchange(self[name], record, {'__snapshot': self})
elif field.type not in ('one2many', 'many2many'):
result[name] = field.convert_to_onchange(self[name], record, {})
else:
# x2many fields: serialize value as commands
result[name] = commands = [Command.clear()]
# The purpose of the following line is to enable the prefetching.
# In the loop below, line._prefetch_ids actually depends on the
# value of record[name] in cache (see prefetch_ids on x2many
# fields). But the cache has been invalidated before calling
# diff(), therefore evaluating line._prefetch_ids with an empty
# cache simply returns nothing, which discards the prefetching
# optimization!
record._cache[name] = tuple(
line_snapshot['<record>'].id for line_snapshot in self[name]
)
for line_snapshot in self[name]:
line = line_snapshot['<record>']
line = line._origin or line
if not line.id:
# new line: send diff from scratch
line_diff = line_snapshot.diff({})
commands.append((Command.CREATE, line.id.ref or 0, line_diff))
else:
# existing line: check diff from database
# (requires a clean record cache!)
line_diff = line_snapshot.diff(Snapshot(line, subnames))
if line_diff:
# send all fields because the web client
# might need them to evaluate modifiers
line_diff = line_snapshot.diff({})
commands.append(Command.update(line.id, line_diff))
else:
commands.append(Command.link(line.id))
return result
nametree = PrefixTree(self.browse(), field_onchange)
if first_call:
names = [name for name in values if name != 'id']
missing_names = [name for name in nametree if name not in values]
defaults = self.default_get(missing_names)
for name in missing_names:
values[name] = defaults.get(name, False)
if name in defaults:
names.append(name)
# prefetch x2many lines: this speeds up the initial snapshot by avoiding
# computing fields on new records as much as possible, as that can be
# costly and is not necessary at all
for name, subnames in nametree.items():
if subnames and values.get(name):
# retrieve all line ids in commands
line_ids = set()
for cmd in values[name]:
if cmd[0] in (Command.UPDATE, Command.LINK):
line_ids.add(cmd[1])
elif cmd[0] == Command.SET:
line_ids.update(cmd[2])
# prefetch stored fields on lines
lines = self[name].browse(line_ids)
fnames = [subname
for subname in subnames
if lines._fields[subname].base_field.store]
lines.fetch(fnames)
# copy the cache of lines to their corresponding new records;
# this avoids computing computed stored fields on new_lines
new_lines = lines.browse(map(NewId, line_ids))
cache = self.env.cache
for fname in fnames:
field = lines._fields[fname]
if not field.translate:
cache.update(new_lines, field, [
field.convert_to_cache(value, new_line, validate=False)
for value, new_line in zip(cache.get_values(lines, field), new_lines)
])
else:
cache.update_raw(
new_lines, field, map(copy.copy, cache.get_values(lines, field)),
)
# Isolate changed values, to handle inconsistent data sent from the
# client side: when a form view contains two one2many fields that
# overlap, the lines that appear in both fields may be sent with
# different data. Consider, for instance:
#
# foo_ids: [line with value=1, ...]
# bar_ids: [line with value=1, ...]
#
# If value=2 is set on 'line' in 'bar_ids', the client sends
#
# foo_ids: [line with value=1, ...]
# bar_ids: [line with value=2, ...]
#
# The idea is to put 'foo_ids' in cache first, so that the snapshot
# contains value=1 for line in 'foo_ids'. The snapshot is then updated
# with the value of `bar_ids`, which will contain value=2 on line.
#
# The issue also occurs with other fields. For instance, an onchange on
# a move line has a value for the field 'move_id' that contains the
# values of the move, among which the one2many that contains the line
# itself, with old values!
#
changed_values = {name: values[name] for name in names}
# set changed values to null in initial_values; not setting them
# triggers default_get() on the new record when creating snapshot0
initial_values = dict(values, **dict.fromkeys(names, False))
# do not force delegate fields to False
for parent_name in self._inherits.values():
if not initial_values.get(parent_name, True):
initial_values.pop(parent_name)
# create a new record with values
record = self.new(initial_values, origin=self)
# make parent records match with the form values; this ensures that
# computed fields on parent records have all their dependencies at
# their expected value
for name in initial_values:
field = self._fields.get(name)
if field and field.inherited:
parent_name, name = field.related.split('.', 1)
record[parent_name]._update_cache({name: record[name]})
# make a snapshot based on the initial values of record
snapshot0 = Snapshot(record, nametree, fetch=(not first_call))
# store changed values in cache; also trigger recomputations based on
# subfields (e.g., line.a has been modified, line.b is computed stored
# and depends on line.a, but line.b is not in the form view)
record._update_cache(changed_values, validate=False)
# update snapshot0 with changed values
for name in names:
if name in nametree:
snapshot0.fetch(name)
# Determine which field(s) should be triggered an onchange. On the first
# call, 'names' only contains fields with a default. If 'self' is a new
# line in a one2many field, 'names' also contains the one2many's inverse
# field, and that field may not be in nametree.
todo = list(unique(itertools.chain(names, nametree))) if first_call else list(names)
done = set()
# mark fields to do as modified to trigger recomputations
protected = [self._fields[name] for name in names]
with self.env.protecting(protected, record):
record.modified(todo)
for name in todo:
field = self._fields[name]
if field.inherited:
# modifying an inherited field should modify the parent
# record accordingly; because we don't actually assign the
# modified field on the record, the modification on the
# parent record has to be done explicitly
parent = record[field.related.split('.')[0]]
parent[name] = record[name]
result = {'warnings': OrderedSet()}
# process names in order
while todo:
# apply field-specific onchange methods
for name in todo:
if field_onchange.get(name):
record._onchange_eval(name, field_onchange[name], result)
done.add(name)
if not env.context.get('recursive_onchanges', True):
break
# determine which fields to process for the next pass
todo = [
name
for name in nametree
if name not in done and snapshot0.has_changed(name)
]
# make the snapshot with the final values of record
snapshot1 = Snapshot(record, nametree)
# determine values that have changed by comparing snapshots
self.env.invalidate_all()
result['value'] = snapshot1.diff(snapshot0, force=first_call)
# format warnings
warnings = result.pop('warnings')
if len(warnings) == 1:
title, message, type = warnings.pop()
if not type:
type = 'dialog'
result['warning'] = dict(title=title, message=message, type=type)
elif len(warnings) > 1:
# concatenate warning titles and messages
title = _("Warnings")
message = '\n\n'.join([warn_title + '\n\n' + warn_message for warn_title, warn_message, warn_type in warnings])
result['warning'] = dict(title=title, message=message, type='dialog')
return result
def onchange(self, values: Dict, field_names: List[str], fields_spec: Dict):
raise NotImplementedError("onchange() is implemented in module 'web'")
def _get_placeholder_filename(self, field):
""" Returns the filename of the placeholder to use,