Odoo18-Base/odoo/addons/test_new_api/tests/test_onchange.py

1348 lines
52 KiB
Python
Raw Permalink Normal View History

2025-01-06 10:57:38 +07:00
# -*- 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 TransactionCase, Form
from odoo import Command
from odoo.tools.misc import submap
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().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
fields_spec = self.env['test_new_api.message']._get_fields_spec()
values = self.env['test_new_api.message'].onchange({}, [], fields_spec)['value']
self.assertEqual(values['discussion'], False)
self.assertEqual(values['body'], False)
self.assertEqual(values['author'], {'id': user.id, 'display_name': user.display_name})
self.assertEqual(values['name'], f'[] {user.name}')
self.assertEqual(values['size'], 0)
def test_default_x2many(self):
""" checking default values for x2many fields """
tag = self.env['test_new_api.multi.tag'].create({'name': 'alpha'})
model = self.env['test_new_api.multi'].with_context(default_tags=[Command.set(tag.ids)])
values = model.default_get(['tags'])
self.assertEqual(values, {'tags': [Command.set(tag.ids)]})
fields_spec = {'tags': {}}
result = model.onchange({}, [], fields_spec)
self.assertEqual(result['value'], {'tags': [(Command.LINK, tag.id, {'id': tag.id})]})
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
fields_spec = self.Message._get_fields_spec()
self.assertEqual(
submap(fields_spec, ('author', 'body', 'discussion')),
{
'author': {'fields': {'display_name': {}}},
'body': {},
'discussion': {'fields': {'display_name': {}}},
}
)
# changing 'discussion' should recompute 'name'
values = {
'discussion': discussion.id,
'name': f"[] {USER.name}",
'body': False,
'author': USER.id,
'size': 0,
}
self.env.invalidate_all()
result = self.Message.onchange(values, ['discussion'], fields_spec)
self.assertEqual(result['value'], {
'name': f"[{discussion.name}] {USER.name}",
})
# changing 'body' should recompute 'size'
values = {
'discussion': discussion.id,
'name': f"[{discussion.name}] {USER.name}",
'body': BODY,
'author': USER.id,
'size': 0,
}
self.env.invalidate_all()
result = self.Message.onchange(values, ['body'], fields_spec)
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'], fields_spec)
self.assertNotIn('name', result['value'])
def test_onchange_many2one(self):
Category = self.env['test_new_api.category']
fields_spec = Category._get_fields_spec()
self.assertEqual(fields_spec, {
'name': {},
'parent': {'fields': {'display_name': {}}},
'root_categ': {'fields': {'display_name': {}}},
'dummy': {},
'color': {},
})
root = Category.create(dict(name='root'))
# set 'parent' to root, and check that 'root_categ' is computed as expected
values = {
'name': 'test',
'parent': root.id,
'root_categ': False,
}
self.env.invalidate_all()
result = Category.onchange(values, ['parent'], fields_spec)
self.assertEqual(result['value'], {
'root_categ': {'id': root.id, 'display_name': root.name},
})
# set 'parent' to False, and check that 'root_categ' is computed as expected
values = {
'name': 'test',
'parent': False,
'root_categ': root.id,
}
self.env.invalidate_all()
result = Category.onchange(values, ['parent'], fields_spec)
self.assertEqual(result['value'], {
'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))
fields_spec = self.Discussion._get_fields_spec()
self.assertEqual(
submap(fields_spec, ('name', 'messages')),
{
'name': {},
'messages': {'fields': {
'author': {'fields': {'display_name': {}}},
'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, "virtual3", {
'name': f"[] {USER.name}",
'body': "ABC",
'author': USER.id,
'size': 3,
'important': False,
}),
],
}
self.env.invalidate_all()
result = self.Discussion.onchange(values, ['name'], fields_spec)
self.assertIn('messages', result['value'])
self.assertEqual(result['value']['messages'], [
Command.update(message1.id, {'name': f"[Foo] {USER.name}"}),
Command.update(message2.id, {'name': f"[Foo] {USER.name}"}),
Command.update("virtual3", {'name': f"[Foo] {USER.name}"}),
])
# ensure onchange changing one2many without subfield works
one_level_fields_spec = {field_name: {} for field_name in fields_spec}
values = dict(values, name='{generate_dummy_message}')
result = self.Discussion.with_context(generate_dummy_message=True).onchange(values, ['name'], one_level_fields_spec)
self.assertEqual(result['value']['messages'], [
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
fields_spec = self.Discussion._get_fields_spec()
self.assertEqual(
submap(fields_spec, ('name', 'messages')),
{
'name': {},
'messages': {'fields': {
'author': {'fields': {'display_name': {}}},
'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': [
(Command.CREATE, 'virtual1', {
'name': f"[] {USER.name}",
'body': BODY,
'author': USER.id,
'size': len(BODY),
'important': False,
}),
],
}
self.env.invalidate_all()
result = self.Discussion.onchange(values, ['name'], fields_spec)
self.assertIn('messages', result['value'])
self.assertItemsEqual(result['value']['messages'], [
(Command.UPDATE, 'virtual1', {'name': f"[Foo] {USER.name}"}),
])
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})
self.assertEqual(multi.partner, partner1)
self.assertEqual(multi.name, partner1.name)
self.assertEqual(multi.lines, line1)
self.assertEqual(line1.partner, partner1)
self.assertEqual(line1.name, False)
fields_spec = multi._get_fields_spec()
self.assertEqual(fields_spec, {
'name': {},
'partner': {'fields': {'display_name': {}}},
'lines': {
'fields': {
'name': {},
'partner': {'fields': {'display_name': {}}},
'tags': {'fields': {'name': {}}},
},
},
})
# 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.CREATE, 'virtual2', {'name': False, 'partner': False, 'tags': [Command.clear()]}),
],
}
self.env.invalidate_all()
result = multi.onchange(values, ['partner'], fields_spec)
self.assertEqual(result['value'], {
'name': partner2.name,
'lines': [
Command.update(line1.id, {
'name': partner2.name,
'partner': {'id': partner2.id, 'display_name': partner2.name},
}),
Command.update('virtual2', {
'name': partner2.name,
'partner': {'id': partner2.id, 'display_name': partner2.name},
}),
],
})
# 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.CREATE, 'virtual2', {
'name': False,
'partner': False,
'tags': [Command.create({'name': 'Tag'})],
}),
],
}
self.env.invalidate_all()
result = multi.onchange(values, ['partner'], fields_spec)
expected_value = {
'name': partner2.name,
'lines': [
Command.update(line1.id, {
'name': partner2.name,
'partner': {'id': partner2.id, 'display_name': partner2.name},
}),
Command.update('virtual2', {
'name': partner2.name,
'partner': {'id': partner2.id, 'display_name': partner2.name},
}),
],
}
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()
fields_spec = multi._get_fields_spec()
fields_spec['lines']['fields']['tags']['fields']['id'] = {}
result = multi.onchange(values, ['partner'], fields_spec)
self.assertEqual(result['value'], expected_value)
# ensure inverse of one2many field is not returned
self.env.invalidate_all()
fields_spec = multi._get_fields_spec()
fields_spec['lines']['fields']['multi'] = {}
result = multi.onchange(values, ['partner'], fields_spec)
self.assertEqual(result['value'], expected_value)
def test_onchange_one2many_default(self):
""" test onchange on x2many field with default value in context """
default_messages = [Command.create({'body': 'A'})]
model = self.Discussion.with_context(default_messages=default_messages)
# the command CREATE below must be applied on the empty value, instead
# of the default value above
values = {
'name': 'Stuff',
'messages': [(Command.CREATE, 'virtual1', {'name': '[] ', 'body': 'B'})],
}
fields_spec = {
'name': {},
'messages': {'fields': {
'name': {},
'body': {},
}},
}
result = model.onchange(values, ['name'], fields_spec)
self.assertEqual(result['value'], {
'messages': [Command.update('virtual1', {'name': f'[Stuff] {self.env.user.name}'})],
})
def test_fields_specific(self):
""" test the effect of field-specific onchange method """
discussion = self.env.ref('test_new_api.discussion_0')
demo = self.user_demo
fields_spec = self.Discussion._get_fields_spec()
self.assertEqual(fields_spec.get('moderator'), {'fields': {'display_name': {}}})
self.assertEqual(fields_spec.get('participants'), {'fields': {'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 = {
'moderator': demo.id,
}
self.env.invalidate_all()
result = discussion.onchange(values, ['moderator'], fields_spec)
self.assertIn('participants', result['value'])
self.assertItemsEqual(
result['value']['participants'],
[(Command.LINK, demo.id, {'id': demo.id, 'display_name': demo.display_name})],
)
def test_onchange_default(self):
""" test the effect of a conditional user-default on a field """
Foo = self.env['test_new_api.foo']
fields_spec = Foo._get_fields_spec()
self.assertTrue(type(Foo).value1.change_default)
self.assertIn('value1', Foo._onchange_methods)
# 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'], fields_spec)
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'], fields_spec)
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 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
fields_spec = self.Discussion._get_fields_spec()
self.assertEqual(fields_spec, {
'important_emails': {'fields': {
'author': {'fields': {'display_name': {}}},
'body': {},
'email_to': {},
'important': {},
'name': {},
'size': {},
}},
'message_concat': {},
'messages': {'fields': {
'author': {'fields': {'display_name': {}}},
'body': {},
'important': {},
'name': {},
'size': {},
}},
'moderator': {'fields': {'display_name': {}}},
'name': {},
'participants': {'fields': {'display_name': {}}},
})
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'], fields_spec)
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')
fields_spec = self.Discussion._get_fields_spec(view_info=view_info)
self.assertEqual(fields_spec, {
'name': {},
'moderator': {'fields': {'display_name': {}}},
'messages': {'fields': {
'name': {},
'body': {},
'important': {},
'author': {'fields': {'display_name': {}}},
'size': {}
}},
'important_emails': {'fields': {
'name': {},
'body': {},
'important': {},
'email_to': {},
'author': {'fields': {'display_name': {}}},
'size': {}},
},
'participants': {'fields': {'display_name': {}}},
'message_concat': {},
})
BODY = "What a beautiful day!"
USER = self.env.user
# create standalone email
email = self.EmailMessage.create({
'discussion': discussion.id,
'name': f"[] {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,
'important_messages': [Command.set(discussion.important_messages.ids)],
'important_emails': [Command.set(discussion.important_emails.ids)],
}
self.env.invalidate_all()
result = discussion.onchange(values, ['name'], fields_spec)
self.assertEqual(
result['value']['important_emails'],
[Command.update(email.id, {
'name': f'[Foo Bar] {USER.name}',
})],
)
def test_onchange_related(self):
user = self.env.user
values = {
'message': 1,
'message_name': False,
'message_currency': 2,
}
fields_spec = {
'message': {'fields': {'display_name': {}}},
'message_name': {},
'message_currency': {'fields': {'display_name': {}}},
}
expected = {
'message_name': 'Hey dude!',
'message_currency': {'id': user.id, 'display_name': user.display_name},
}
self.env.invalidate_all()
Message = self.env['test_new_api.related']
result = Message.onchange(values, ['message'], fields_spec)
self.assertEqual(result['value'], expected)
self.env.invalidate_all()
Message = self.env(user=self.user_demo.id)['test_new_api.related']
result = Message.onchange(values, ['message'], fields_spec)
self.assertEqual(result['value'], expected)
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')
fields_spec = self.Message._get_fields_spec()
self.assertEqual(fields_spec, {
'discussion': {'fields': {'display_name': {}}},
'name': {},
'author': {'fields': {'display_name': {}}},
'size': {},
'attributes': {},
'body': {},
})
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'], fields_spec)
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}}
fields_spec = {
'order_id': {},
'subtotal': {},
}
result = self.env['test_new_api.monetary_order_line'].onchange(values, [], fields_spec)
self.assertEqual(result['value']['order_id'], order.id)
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_onchange_inherited_in_one2many(self):
move = self.env['test_new_api.move'].create({})
view = self.env["ir.ui.view"].create({
"model": "test_new_api.move",
"type": "form",
"arch": """<form>
<field name="payment_ids">
<form>
<!-- tag_repeat is inherited from move model -->
<field name="tag_repeat" />
</form>
</field>
</form>"""
})
with Form(move, view) as form:
with form.payment_ids.new() as line:
line.tag_repeat = 1
self.assertEqual(len(form.payment_ids), 1)
self.assertEqual(len(move.payment_ids), 1)
self.assertEqual(move.payment_ids.move_id, move)
self.assertEqual(move.payment_ids.tag_repeat, 1)
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 = 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, "")
def test_reading_one2many_and_inverse_is_not_supported(self):
# when reading a relation one2many, sometime the relation itself defines
# the many2one toward the parent model. Adding this to the fields_spec
# is not supported
USER = self.env.user
fields_spec = {
'name': {},
'messages': {'fields': {
'name': {},
'author': {'fields': {'display_name': {}}},
# add the inverse of field 'messages' into its subfields
'discussion': {'fields': {'display_name': {}}},
}},
}
# modify discussion name with a special value that adds a new message
values = {
'name': "{generate_dummy_message}",
}
result = self.Discussion.with_context(generate_dummy_message=True).onchange(values, ['name'], fields_spec)
self.assertEqual(result['value']['messages'], [
Command.create({
'name': f'[{{generate_dummy_message}}] {USER.name}',
'author': {'id': 1, 'display_name': f'{USER.name}'},
'discussion': False, # this value is False because the main record does not exist yet
}),
])
def test_reading_many2one_extra_fields(self):
Category = self.env['test_new_api.category']
root = Category.create(dict(name='root'))
fields_spec = {
'name': {},
'parent': {'fields': {'display_name': {}, 'color': {}}},
'root_categ': {'fields': {'display_name': {}, 'color': {}}},
}
# set 'parent' to root, and check that 'root_categ' is computed as expected
values = {
'name': 'test',
'parent': root.id,
'root_categ': False,
}
self.env.invalidate_all()
result = Category.onchange(values, ['parent'], fields_spec)
self.assertEqual(result['value'], {
'root_categ': {'id': root.id, 'display_name': root.name, 'color': root.color},
})
def test_one2many_field_with_context_many2one(self):
""" test many2one fields with a context on their one2many container field """
multi = self.env['test_new_api.multi'].create({})
line = multi.lines.create({'multi': multi.id})
self.assertFalse(multi.partner)
self.assertFalse(multi.name)
self.assertEqual(multi.lines, line)
self.assertFalse(line.partner)
self.assertFalse(line.name)
fields_spec = multi._get_fields_spec()
self.assertEqual(fields_spec, {
'name': {},
'partner': {'fields': {'display_name': {}}},
'lines': {
'fields': {
'name': {},
'partner': {'fields': {'display_name': {}}},
'tags': {'fields': {'name': {}}},
},
},
})
# add a context on field 'lines' to change the display_name of subfield 'partner'
fields_spec['lines']['context'] = {'show_email': True}
# create a partner (for a change)
partner = self.env['res.partner'].create({
'name': 'A partner',
'email': 'foo@example.com',
})
display_name = partner.display_name
display_name_email = partner.with_context(show_email=True).display_name
self.assertNotEqual(display_name_email, display_name)
# modify 'partner'
# -> set 'partner' on all lines
# -> recompute 'name'
# -> set 'name' on all lines
values = {
'partner': partner.id, # this one just changed
}
self.env.invalidate_all()
result = multi.onchange(values, ['partner'], fields_spec)
self.assertEqual(result['value'], {
'name': partner.name,
'lines': [
Command.update(line.id, {
'name': partner.name,
'partner': {'id': partner.id, 'display_name': display_name_email},
}),
],
})
def test_one2many_field_with_context_many2many(self):
""" test relational fields with a context on their one2many container field """
partner = self.env['res.partner'].create({'name': 'A partner'})
multi = self.env['test_new_api.multi'].create({'partner': partner.id})
line = multi.lines.create({'multi': multi.id})
self.assertEqual(multi.partner, partner)
self.assertEqual(multi.name, partner.name)
self.assertEqual(multi.lines, line)
self.assertEqual(line.partner, partner)
self.assertEqual(line.name, False)
fields_spec = {
'name': {},
'partner': {'fields': {'display_name': {}}},
'lines': {
# this context should change the 'display_name' of 'tags'
'context': {'special_tag': True},
'fields': {
'name': {},
'partner': {'fields': {'display_name': {}}},
'tags': {'fields': {'display_name': {}}},
},
},
'tags': {'fields': {'display_name': {}}},
}
# set field 'tags': this should modify 'tags' on all lines
tag = self.env['test_new_api.multi.tag'].create({'name': 'tag'})
self.assertEqual(tag.display_name, 'tag')
self.assertEqual(tag.with_context(special_tag=True).display_name, 'tag!')
values = {
'tags': [Command.link(tag.id)],
}
self.env.invalidate_all()
result = multi.onchange(values, ['tags'], fields_spec)
self.assertEqual(result['value'], {
'lines': [
Command.update(line.id, {
'tags': [(Command.LINK, tag.id, {'id': tag.id, 'display_name': 'tag!'})],
}),
],
})
def test_one2many_field_with_many2many_subfield(self):
""" test relational fields with a context on their one2many container field """
tag1 = self.env['test_new_api.multi.tag'].create({'name': 'a1'})
tag2 = self.env['test_new_api.multi.tag'].create({'name': 'a2'})
multi = self.env['test_new_api.multi'].create({})
line = multi.lines.create({
'multi': multi.id,
'tags': [Command.set(tag1.ids)],
})
# having 'lines' below force the prefetching of the subfields 'name'
# and 'tags'; this test ensures that the ids of 'tags' are "newified"
# in the new record
values = {
'lines': [(Command.CREATE, 'virtual1', {'name': 'X', 'tags': []})],
'tags': [Command.link(tag2.id)],
}
fields_spec = {
'name': {},
'lines': {'fields': {
'name': {},
'tags': {},
}},
'tags': {},
}
self.env.invalidate_all()
result = multi.onchange(values, ['tags'], fields_spec)
self.assertEqual(result['value'], {
'lines': [
Command.update(line.id, {
'tags': [(Command.LINK, tag2.id, {'id': tag2.id})],
}),
Command.update('virtual1', {
'tags': [(Command.LINK, tag2.id, {'id': tag2.id})],
}),
],
})
class TestComputeOnchange2(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_copy_batch(self):
partners = self.env['test_new_api.partner'].create([
{'name': f'Partner {index}'} for index in range(5)
])
# warmup
partners.copy()
with self.assertQueryCount(1):
new_partners = partners.copy()
for old_partner, new_partner in zip(partners, new_partners):
self.assertEqual(new_partner.name, old_partner.name)
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 = 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 = 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 = 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 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': [Command.create({'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'
fields_spec = record._get_fields_spec()
# 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, 'virtual2', {'value': 8, 'edit': 9, 'count': 0}),
]
result = record.onchange({'line_ids': line_ids}, ['line_ids'], fields_spec)
expected = {'value': {
'line_ids': [
Command.update(line.id, {'count': 8}),
Command.update('virtual2', {'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'], fields_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},
])
def test_one2many_compute(self):
""" Test a computed, editable one2many field with a domain. """
record = self.env['test_new_api.compute_editable'].create(
{'line_ids': [Command.create({})]},
)
with Form(record) as form:
form.precision_rounding = 0.0001
def test_new_one2many_on_existing_record(self):
discussion = self.env['test_new_api.discussion'].create({
'messages': [Command.create({'body': 'Required Body', 'important': True})],
'name': 'Required field',
'participants': [Command.set(self.env.user.ids)],
})
with Form(discussion, 'test_new_api.discussion_form_2') as discussion_form:
self.assertEqual(len(discussion_form.messages), 1)
with discussion_form.messages.new() as message_form:
# this actually checks that during the onchange, the new message
# has access to its sibling messages through the discussion
self.assertEqual(message_form.has_important_sibling, True)
message_form.body = 'Required Body'
self.assertEqual(len(discussion_form.messages), 2)
# Test the same flow but when the important message is archived.
# The _compute_has_important_sibling should take in account archived siblings.
discussion = self.env['test_new_api.discussion'].create({
'messages': [
Command.create({'body': 'Archived Important sibling', 'important': True, 'active': False}),
],
'name': 'Required field',
'participants': [Command.set(self.env.user.ids)],
})
with Form(discussion, 'test_new_api.discussion_form_2') as discussion_form:
self.assertEqual(len(discussion_form.messages), 0)
with discussion_form.messages.new() as message_form:
# this actually checks that during the onchange, the new message
# has access to its archived sibling messages through the discussion
self.assertEqual(message_form.has_important_sibling, True)
message_form.body = 'Required Body'
self.assertEqual(len(discussion_form.messages), 1)
def test_protection_shared_compute(self):
model = self.env['test_new_api.shared.compute']
START = 5
with Form(model) as form:
self.assertEqual(form.start, 0)
self.assertEqual(form.end, 10)
form.start = START
self.assertEqual(form.start, START, "updating 'start' should not recompute it")
self.assertEqual(form.end, 10, "updating 'start' should not recompute 'end'")
form.name = f"{START}->{START + 20}"
self.assertEqual(form.start, START, "updating 'name' should recompute 'start'")
self.assertEqual(form.end, START + 20, "updating 'name' should recompute 'end'")
def test_new_one2many_traversing_many2one_second_onchange(self):
discussion = self.env['test_new_api.discussion'].create({
'name': 'Required field',
'messages': [Command.create({'body': 'Msg1: Existing', 'important': True})],
'participants': [Command.set(self.env.user.ids)],
})
with Form(discussion, 'test_new_api.discussion_form_2') as discussion_form:
self.assertEqual(len(discussion_form.messages), 1)
with discussion_form.messages.new() as message_form:
# check that Msg1 is visible in the one2many during onchange()
self.assertEqual(message_form.has_important_sibling, True)
message_form.body = 'Msg2: New'
self.assertEqual(len(discussion_form.messages), 2)
with discussion_form.messages.new() as message_form:
# check that Msg1 is visible in the one2many during onchange()
self.assertEqual(message_form.has_important_sibling, True)
message_form.body = 'Msg3: New'