# -*- 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': """
""", }) # 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": """""" }) 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': """