diff --git a/odoo/addons/base/tests/test_res_users.py b/odoo/addons/base/tests/test_res_users.py
index ac6dd21a07d..3640e9a2575 100644
--- a/odoo/addons/base/tests/test_res_users.py
+++ b/odoo/addons/base/tests/test_res_users.py
@@ -441,7 +441,7 @@ class TestUsersGroupWarning(TransactionCase):
with self.assertQueryCount(__system__=436), \
Form(self.test_group_user.with_context(show_user_group_warning=True), view='base.view_users_form') as UserForm:
UserForm._values[self.sales_categ_field] = False
- UserForm._perform_onchange([self.sales_categ_field])
+ UserForm._perform_onchange(self.sales_categ_field)
self.assertEqual(
UserForm.user_group_warning,
@@ -449,7 +449,7 @@ class TestUsersGroupWarning(TransactionCase):
)
UserForm._values[self.sales_categ_field] = self.group_sales_administrator.id
- UserForm._perform_onchange([self.sales_categ_field])
+ UserForm._perform_onchange(self.sales_categ_field)
self.assertFalse(UserForm.user_group_warning)
def test_user_group_inheritance_warning(self):
@@ -463,7 +463,7 @@ class TestUsersGroupWarning(TransactionCase):
with self.assertQueryCount(__system__=437), \
Form(self.test_group_user.with_context(show_user_group_warning=True), view='base.view_users_form') as UserForm:
UserForm._values[self.sales_categ_field] = self.group_sales_user.id
- UserForm._perform_onchange([self.sales_categ_field])
+ UserForm._perform_onchange(self.sales_categ_field)
self.assertEqual(
UserForm.user_group_warning,
@@ -471,7 +471,7 @@ class TestUsersGroupWarning(TransactionCase):
)
UserForm._values[self.sales_categ_field] = self.group_sales_administrator.id
- UserForm._perform_onchange([self.sales_categ_field])
+ UserForm._perform_onchange(self.sales_categ_field)
self.assertFalse(UserForm.user_group_warning)
def test_user_group_inheritance_warning_multi(self):
@@ -488,7 +488,7 @@ class TestUsersGroupWarning(TransactionCase):
Form(self.test_group_user.with_context(show_user_group_warning=True), view='base.view_users_form') as UserForm:
UserForm._values[self.sales_categ_field] = self.group_sales_user.id
UserForm._values[self.project_categ_field] = self.group_project_user.id
- UserForm._perform_onchange([self.sales_categ_field])
+ UserForm._perform_onchange(self.sales_categ_field)
self.assertTrue(
UserForm.user_group_warning,
@@ -496,7 +496,7 @@ class TestUsersGroupWarning(TransactionCase):
)
UserForm._values[self.sales_categ_field] = self.group_sales_administrator.id
- UserForm._perform_onchange([self.sales_categ_field])
+ UserForm._perform_onchange(self.sales_categ_field)
self.assertEqual(
UserForm.user_group_warning,
@@ -515,7 +515,7 @@ class TestUsersGroupWarning(TransactionCase):
with self.assertQueryCount(__system__=437), \
Form(self.test_group_user.with_context(show_user_group_warning=True), view='base.view_users_form') as UserForm:
UserForm._values[self.timesheets_categ_field] = self.group_timesheets_user_own_timesheet.id
- UserForm._perform_onchange([self.timesheets_categ_field])
+ UserForm._perform_onchange(self.timesheets_categ_field)
self.assertEqual(
UserForm.user_group_warning,
@@ -523,7 +523,7 @@ class TestUsersGroupWarning(TransactionCase):
)
UserForm._values[self.timesheets_categ_field] = self.group_timesheets_user_all_timesheet.id
- UserForm._perform_onchange([self.timesheets_categ_field])
+ UserForm._perform_onchange(self.timesheets_categ_field)
self.assertFalse(UserForm.user_group_warning)
def test_user_group_parent_inheritance_no_warning(self):
@@ -536,6 +536,6 @@ class TestUsersGroupWarning(TransactionCase):
with self.assertQueryCount(__system__=420), \
Form(self.test_group_user.with_context(show_user_group_warning=True), view='base.view_users_form') as UserForm:
UserForm._values[self.field_service_categ_field] = self.group_field_service_user.id
- UserForm._perform_onchange([self.field_service_categ_field])
+ UserForm._perform_onchange(self.field_service_categ_field)
self.assertFalse(UserForm.user_group_warning)
diff --git a/odoo/addons/test_testing_utilities/menu.xml b/odoo/addons/test_testing_utilities/menu.xml
index 1f936afdefa..7e8c91ab505 100644
--- a/odoo/addons/test_testing_utilities/menu.xml
+++ b/odoo/addons/test_testing_utilities/menu.xml
@@ -113,6 +113,23 @@
+
+ Parent Inline Tree with context on field
+ test_testing_utilities.parent
+
+
+
+
+
Non Normalised Domains
test_testing_utilities.a
diff --git a/odoo/addons/test_testing_utilities/tests/test_form_impl.py b/odoo/addons/test_testing_utilities/tests/test_form_impl.py
index 133b7051140..bdb422fc040 100644
--- a/odoo/addons/test_testing_utilities/tests/test_form_impl.py
+++ b/odoo/addons/test_testing_utilities/tests/test_form_impl.py
@@ -365,6 +365,13 @@ class TestO2M(TransactionCase):
"should not have set v (and thus not name)"
)
+ def test_o2m_parent_context(self):
+ """ Test the o2m form with a context on the field that uses 'parent'. """
+ view = 'test_testing_utilities.o2m_parent_context'
+ with Form(self.env['test_testing_utilities.parent'], view=view) as f:
+ with f.subs.new() as s:
+ s.value = 42
+
def test_o2m_default(self):
""" Tests that default_get can return defaults for the o2m
"""
@@ -621,7 +628,7 @@ class TestNestedO2M(TransactionCase):
})
form = Form(obj)
form.qty_producing = 1
- form._perform_onchange(['move_raw_ids'])
+ form._perform_onchange('move_raw_ids')
form.save()
def test_empty_update(self):
diff --git a/odoo/models.py b/odoo/models.py
index 5b1a4ab6037..81261cc2e63 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -6331,7 +6331,7 @@ class BaseModel(metaclass=MetaModel):
if name == 'id':
continue
field = record._fields[name]
- if (field.type == 'properties' and field.definition_record in field_name
+ if (field.type == 'properties' and field.definition_record in names
and other.get(name) == self[name] == []):
# TODO: The parent field on "record" can be False, if it was changed,
# (even if if was changed to a not Falsy value) because of
diff --git a/odoo/tests/form.py b/odoo/tests/form.py
index 34d7e0e0af7..1a9873711c4 100644
--- a/odoo/tests/form.py
+++ b/odoo/tests/form.py
@@ -283,9 +283,9 @@ class Form:
vals.clear()
vals['id'] = False
- # call onchange with an empty list of fields; this retrieves default
- # values, applies onchanges and return the result
- self._perform_onchange([])
+ # call onchange with no field; this retrieves default values, applies
+ # onchanges and return the result
+ self._perform_onchange()
# fill in whatever fields are still missing with falsy values
vals.update({
field_name: _cleanup_from_default(field_info['type'], False)
@@ -323,7 +323,7 @@ class Form:
value = value.id
self._values[field_name] = value
- self._perform_onchange([field_name])
+ self._perform_onchange(field_name)
def _get_modifier(self, field_name, modifier, *, view=None, vals=None):
if view is None:
@@ -398,26 +398,27 @@ class Form:
def _get_context(self, field_name):
""" Return the context of a given field. """
- context = self._view['contexts'].get(field_name)
- if not context:
+ context_str = self._view['contexts'].get(field_name)
+ if not context_str:
return {}
+ eval_context = self._get_eval_context()
+ return safe_eval(context_str, eval_context)
- # see _getEvalContext
- # the context for a field's evals (of domain/context) is the composition of:
- # * the parent's values
- # * ??? element.context ???
- # * the environment's context (?)
- # * a few magic values
- eval_context = dict(self._values_to_save(all_fields=True))
- eval_context.update(self._env.context)
- eval_context.update(
- id=self._record.id,
- active_id=self._record.id,
- active_ids=self._record.ids,
- active_model=self._record._name,
- current_date=date.today().strftime("%Y-%m-%d"),
- )
- return safe_eval(context, eval_context, {'context': eval_context})
+ def _get_eval_context(self):
+ """ Return the context dict to eval something. """
+ context = {
+ 'id': self._record.id,
+ 'active_id': self._record.id,
+ 'active_ids': self._record.ids,
+ 'active_model': self._record._name,
+ 'current_date': date.today().strftime("%Y-%m-%d"),
+ **self._env.context,
+ }
+ return {
+ **context,
+ 'context': context,
+ **self._values_to_save(all_fields=True),
+ }
def __enter__(self):
""" This makes the Form usable as a context manager. """
@@ -562,22 +563,27 @@ class Form:
result[field_name] = value
return result
- def _perform_onchange(self, field_names, context=None):
- assert isinstance(field_names, list)
+ def _perform_onchange(self, field_name=None):
+ assert field_name is None or isinstance(field_name, str)
- # marks any onchange source as changed
- self._changed.update(field_names)
+ # marks onchange source as changed
+ if field_name:
+ self._changed.add(field_name)
- # skip calling onchange() if there's no trigger on any of the changed
- # fields
+ # skip calling onchange() if there's no on_change on the field
spec = self._view['onchange']
- if field_names and not any(spec[field_name] for field_name in field_names):
+ if field_name and not spec[field_name]:
return
record = self._record
- if context is not None:
- record = record.with_context(**context)
- result = record.onchange(self._onchange_values(), field_names, spec)
+
+ # if the onchange is triggered by a field, add the context of that field
+ if field_name:
+ context = self._get_context(field_name)
+ if context:
+ record = record.with_context(**context)
+
+ result = record.onchange(self._onchange_values(), field_name, spec)
self._env.flush_all()
self._env.clear() # discard cache and pending recomputations
@@ -740,6 +746,11 @@ class O2MForm(Form):
vals = {**self._values, '•parent•': self._proxy._form._values}
return super()._get_modifier(field_name, modifier, view=view, vals=vals)
+ def _get_eval_context(self):
+ eval_context = super()._get_eval_context()
+ eval_context['parent'] = Dotter(self._proxy._form._values)
+ return eval_context
+
def _onchange_values(self):
values = super()._onchange_values()
# computed o2m may not have a relation_field(?)
@@ -768,8 +779,7 @@ class O2MForm(Form):
else:
raise AssertionError(f"Expected command 0 or 1, found {cmd!r}")
- # FIXME: should be called when performing on change => value needs to be serialised into parent every time?
- proxy._form._perform_onchange([proxy._field], self._env.context)
+ proxy._form._perform_onchange(proxy._field)
def _values_to_save(self, all_fields=False):
""" Validates values and returns only fields modified since
@@ -905,7 +915,7 @@ class O2MProxy(X2MProxy):
raise AssertionError("Expected command 0 or 1, got %s" % commands[cidx])
# remove reified record
del self._records[index]
- self._form._perform_onchange([self._field])
+ self._form._perform_onchange(self._field)
def _command_index(self, for_record):
""" Takes a record index and finds the corresponding record index
@@ -962,7 +972,7 @@ class M2MProxy(X2MProxy, collections.abc.Sequence):
f"trying to assign a {record._name!r} object to a {comodel_name!r} field"
self._get_ids().append(record.id)
- parent._perform_onchange([self._field])
+ parent._perform_onchange(self._field)
# pylint: disable=redefined-builtin
def remove(self, id=None, index=None):
@@ -976,14 +986,14 @@ class M2MProxy(X2MProxy, collections.abc.Sequence):
del self._get_ids()[index]
else:
self._get_ids().remove(id)
- self._form._perform_onchange([self._field])
+ self._form._perform_onchange(self._field)
def clear(self):
""" Removes all existing records in the m2m
"""
self._assert_editable()
self._get_ids()[:] = []
- self._form._perform_onchange([self._field])
+ self._form._perform_onchange(self._field)
def read_record(record, fields):
@@ -1031,3 +1041,15 @@ def _cleanup_from_default(type_, value):
elif type_ == 'date' and isinstance(value, date):
return odoo.fields.Date.to_string(value)
return value
+
+
+class Dotter:
+ """ Simple wrapper for a dict where keys are accessed as readonly attributes. """
+ __slots__ = ['__values']
+
+ def __init__(self, values):
+ self.__values = values
+
+ def __getattr__(self, key):
+ val = self.__values[key]
+ return Dotter(val) if isinstance(val, dict) else val