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