[FIX] core: invoke constraint methods once per write

Consider a constraint method with two fields: a normal field, and a
field with an inverse method.  When calling write() with both fields,
the constraint method should be invoked once.

closes odoo/odoo#64433

X-original-commit: 25760348859f594abb796b5bc7b28f63e298cd53
Signed-off-by: Raphael Collet (rco) <rco@openerp.com>
Co-authored-by: Thibault Delavallée <tde@odoo.com>
This commit is contained in:
Raphael Collet
2021-01-12 15:59:10 +00:00
co-authored by Thibault Delavallée
parent e8fea60bb8
commit 65fd4373c5
3 changed files with 47 additions and 5 deletions
@@ -400,6 +400,7 @@ class ComputeInverse(models.Model):
foo = fields.Char()
bar = fields.Char(compute='_compute_bar', inverse='_inverse_bar', store=True)
baz = fields.Char()
@api.depends('foo')
def _compute_bar(self):
@@ -412,6 +413,11 @@ class ComputeInverse(models.Model):
for record in self:
record.foo = record.bar
@api.constrains('bar', 'baz')
def _check_constraint(self):
if self._context.get('log_constraint'):
self._context.get('log', []).append('constraint')
class MultiComputeInverse(models.Model):
""" Model with the same inverse method for several fields. """
@@ -556,7 +556,6 @@ class TestFields(TransactionCaseWithUserDemo):
self.assertEqual(foo1.name, 'Bar')
self.assertEqual(foo2.name, 'Bar')
# create/write on 'foo' should only invoke the compute method
log = []
model = self.env['test_new_api.compute.inverse'].with_context(log=log)
@@ -676,6 +675,38 @@ class TestFields(TransactionCaseWithUserDemo):
discussion.name = "X"
discussion.flush()
def test_15_constraint_inverse(self):
""" test constraint method on normal field and field with inverse """
log = []
model = self.env['test_new_api.compute.inverse'].with_context(log=log, log_constraint=True)
# create/write with normal field only
log.clear()
record = model.create({'baz': 'Hi'})
self.assertCountEqual(log, ['constraint'])
log.clear()
record.write({'baz': 'Ho'})
self.assertCountEqual(log, ['constraint'])
# create/write with inverse field only
log.clear()
record = model.create({'bar': 'Hi'})
self.assertCountEqual(log, ['inverse', 'constraint'])
log.clear()
record.write({'bar': 'Ho'})
self.assertCountEqual(log, ['inverse', 'constraint'])
# create/write with both fields only
log.clear()
record = model.create({'bar': 'Hi', 'baz': 'Hi'})
self.assertCountEqual(log, ['inverse', 'constraint'])
log.clear()
record.write({'bar': 'Ho', 'baz': 'Ho'})
self.assertCountEqual(log, ['inverse', 'constraint'])
def test_16_compute_unassigned(self):
model = self.env['test_new_api.compute.unassigned']
+9 -4
View File
@@ -1257,10 +1257,15 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
yield dbid, xid, converted, dict(extras, record=stream.index)
def _validate_fields(self, field_names):
def _validate_fields(self, field_names, excluded_names=()):
""" Invoke the constraint methods for which at least one field name is
in ``field_names`` and none is in ``excluded_names``.
"""
field_names = set(field_names)
excluded_names = set(excluded_names)
for check in self._constraint_methods:
if not field_names.isdisjoint(check._constrains):
if (not field_names.isdisjoint(check._constrains)
and excluded_names.isdisjoint(check._constrains)):
check(self)
@api.model
@@ -3681,7 +3686,7 @@ Fields:
# validate non-inversed fields first
inverse_fields = [f.name for fs in determine_inverses.values() for f in fs]
real_recs._validate_fields(set(vals) - set(inverse_fields))
real_recs._validate_fields(vals, inverse_fields)
for fields in determine_inverses.values():
# inverse records that are not being computed
@@ -3896,7 +3901,7 @@ Fields:
# check Python constraints for non-stored inversed fields
for data in data_list:
data['record']._validate_fields(set(data['inversed']) - set(data['stored']))
data['record']._validate_fields(data['inversed'], data['stored'])
if self._check_company_auto:
records._check_company()