diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index d727c4c0d5a61..126b8a9dcb007 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -2519,24 +2519,6 @@ def _compute_display_name(self): 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 diff --git a/addons/base_automation/tests/test_automation.py b/addons/base_automation/tests/test_automation.py index 1acdf6178c79e..831b5a92fa8f7 100644 --- a/addons/base_automation/tests/test_automation.py +++ b/addons/base_automation/tests/test_automation.py @@ -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 @@ def test_01_on_create(self): 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 @@ def test_02_on_create_restricted(self): 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 @@ def test_03_on_change_restricted(self): 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) diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index d2792c0088962..bde446849f452 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -1254,11 +1254,6 @@ def _read_format(self, fnames, load='_classic_read'): 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) diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 31fbd7327d689..d78402680a8f8 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -840,14 +840,14 @@ def _compute_display_name(self): 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) diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index 5b010fd88b68b..f0b83196ac4db 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -538,14 +538,14 @@ def _update_accrual(self): # 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', diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index b7f9639d3885a..8181c4fd3f8b5 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -270,18 +270,6 @@ def _must_delete_date_planned(self, field_name): # 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) diff --git a/addons/stock_picking_batch/models/stock_picking_batch.py b/addons/stock_picking_batch/models/stock_picking_batch.py index 54c97e801a216..fb37a9c0321a8 100644 --- a/addons/stock_picking_batch/models/stock_picking_batch.py +++ b/addons/stock_picking_batch/models/stock_picking_batch.py @@ -182,17 +182,6 @@ def _unlink_if_not_done(self): 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 # ------------------------------------------------------------------------- diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py index 05b5b544e7fee..af97c306b3a2b 100644 --- a/odoo/addons/base/models/res_users.py +++ b/odoo/addons/base/models/res_users.py @@ -1737,32 +1737,6 @@ def _read_format(self, fnames, load='_classic_read'): 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: diff --git a/odoo/addons/test_new_api/tests/__init__.py b/odoo/addons/test_new_api/tests/__init__.py index cc971ccfd1867..fb9e5fb6eb186 100644 --- a/odoo/addons/test_new_api/tests/__init__.py +++ b/odoo/addons/test_new_api/tests/__init__.py @@ -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 diff --git a/odoo/addons/test_new_api/tests/test_onchange.py b/odoo/addons/test_new_api/tests/test_onchange.py deleted file mode 100644 index 62321733efd22..0000000000000 --- a/odoo/addons/test_new_api/tests/test_onchange.py +++ /dev/null @@ -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': """ -
- - - - - - - """, - }) - - # 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 = 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}, - ]) diff --git a/odoo/addons/test_new_api/tests/test_properties.py b/odoo/addons/test_new_api/tests/test_properties.py index 9de447a4861d6..f08eb8871a1e6 100644 --- a/odoo/addons/test_new_api/tests/test_properties.py +++ b/odoo/addons/test_new_api/tests/test_properties.py @@ -1302,171 +1302,6 @@ def test_properties_field_change_definition(self): {'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.""" diff --git a/odoo/models.py b/odoo/models.py index 53b36c56b41a6..8ee2e6f0cc3d3 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -42,7 +42,7 @@ 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 @@ def _onchange_eval(self, field_name, onchange, result): 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, '': 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[''] - tree = self[''] - 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[''] - subnames = self[''][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[""].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[''] - result = {} - for name, subnames in self[''].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[''].id for line_snapshot in self[name] - ) - for line_snapshot in self[name]: - line = line_snapshot[''] - 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,