[REF] core: compute fields before database insertion.

Until now, stored compute fields were computed after database insertion.
This meant that required fields should not be computed, for instance,
unless some hackish code was added to make it work.  Another trick was
to provide some default value, but this actually prevents the field for
being computed after insertion.

This commit provides a field parameter to specify that the field should
be precomputed: adding precompute=True on the field definition force the
method create() to compute its value before inserting the new record in
the database.  For the reason explained below, precomputing fields is
not always correct, and therefore the default remains to not precompute
a field.

Some stored fields must be computed after insertion, for instance:
* statistics fields computed with search/read_group/...
* fields referencing the current record (res.partner.commercial_partner_id)
* fields referencing another record that does not exist yet (think about
  records created by one2many fields)
* fields depending on the create_date/write_date/create_uid/write_uid

Those fields shouldn't be defined with precompute=True, which triggers
their computation post record creation.  This is why, by safety, we
consider the default behavior to be precompute=False.

closes odoo/odoo#80449

Signed-off-by: Raphael Collet (rco) <rco@openerp.com>
Co-authored-by: Raphael Collet <rco@odoo.com>
Co-authored-by: Yannick Tivisse <yti@odoo.com>
This commit is contained in:
Victor Feyens
2021-11-30 14:30:48 +00:00
committed by Raphael Collet
co-authored by Raphael Collet Yannick Tivisse
parent 86f5ca73d9
commit d04a5b5c8c
7 changed files with 525 additions and 32 deletions
+124 -3
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
import logging
from odoo import models, fields, api, _, Command
from odoo.exceptions import AccessError, ValidationError
@@ -1088,8 +1088,8 @@ class ModelChildM2o(models.Model):
@api.depends('parent_id.name')
def _compute_sizes(self):
for record in self:
record.size1 = len(self.parent_id.name)
record.size2 = len(self.parent_id.name)
record.size1 = len(record.parent_id.name)
record.size2 = len(record.parent_id.name)
@api.depends('name')
def _compute_cost(self):
@@ -1390,3 +1390,124 @@ class TriggerRight(models.Model):
def _compute_left_size(self):
for record in self:
record.left_size = len(record.left_ids)
class Precompute(models.Model):
_name = 'test_new_api.precompute'
_description = 'model with precomputed fields'
name = fields.Char(required=True)
# both fields are precomputed
lower = fields.Char(compute='_compute_names', store=True, precompute=True)
upper = fields.Char(compute='_compute_names', store=True, precompute=True)
# precomputed that depends on precomputed fields
lowup = fields.Char(compute='_compute_lowup', store=True, precompute=True)
# kind of precomputed related field traversing a many2one
partner_id = fields.Many2one('res.partner')
commercial_id = fields.Many2one('res.partner', compute='_compute_commercial_id',
store=True, precompute=True)
# precomputed depending on one2many fields
line_ids = fields.One2many('test_new_api.precompute.line', 'parent_id')
size = fields.Integer(compute='_compute_size', store=True, precompute=True)
@api.depends('name')
def _compute_names(self):
for record in self:
record.lower = (record.name or "").lower()
record.upper = (record.name or "").upper()
@api.depends('lower', 'upper')
def _compute_lowup(self):
for record in self:
record.lowup = record.lower + record.upper
@api.depends('partner_id.commercial_partner_id')
def _compute_commercial_id(self):
for record in self:
record.commercial_id = record.partner_id.commercial_partner_id
@api.depends('line_ids.size')
def _compute_size(self):
for record in self:
record.size = sum(record.line_ids.mapped('size'))
class PrecomputeLine(models.Model):
_name = 'test_new_api.precompute.line'
_description = 'secondary model with precomputed fields'
parent_id = fields.Many2one('test_new_api.precompute')
name = fields.Char(required=True)
size = fields.Integer(compute='_compute_size', store=True, precompute=True)
@api.depends('name')
def _compute_size(self):
for line in self:
line.size = len(line.name or "")
class PrecomputeCombo(models.Model):
_name = 'test_new_api.precompute.combo'
_description = 'yet another model with precomputed fields'
_logger = logging.getLogger('precompute_setter')
name = fields.Char()
reader = fields.Char(compute='_compute_reader', precompute=True, store=True)
editer = fields.Char(compute='_compute_editer', precompute=True, store=True, readonly=False)
setter = fields.Char(compute='_compute_setter', precompute=True, inverse='_inverse_setter', store=True)
@api.depends('name')
def _compute_reader(self):
for record in self:
record.reader = record.name
@api.depends('name')
def _compute_editer(self):
for record in self:
record.editer = record.name
@api.depends('name')
def _compute_setter(self):
for record in self:
record.setter = record.name
def _inverse_setter(self):
self._logger.warning("Unexpected inverse of %s.setter", self._name, stack_info=True)
class PrecomputeEditable(models.Model):
_name = 'test_new_api.precompute.editable'
_description = 'yet another model with precomputed editable fields'
foo = fields.Char()
bar = fields.Char(compute='_compute_bar', precompute=True, store=True, readonly=False)
baz = fields.Char(compute='_compute_baz', precompute=True, store=True, readonly=False)
baz2 = fields.Char(compute='_compute_baz2', precompute=True, store=True)
@api.depends('foo')
def _compute_bar(self):
self.bar = "COMPUTED"
@api.depends('bar')
def _compute_baz(self):
self.baz = "COMPUTED"
@api.depends('baz')
def _compute_baz2(self):
# this field is a trick to get the value of baz if it ever is recomputed
# during the precomputation of bar
for record in self:
record.baz2 = record.baz
class PrecomputeRequired(models.Model):
_name = 'test_new_api.precompute.required'
_description = 'a model with precomputed required fields'
partner_id = fields.Many2one('res.partner', required=True)
name = fields.Char(related='partner_id.name', precompute=True, store=True, required=True)
@@ -79,3 +79,8 @@ access_test_new_api_model_constrained_unlinks,access_test_new_api_model_constrai
access_test_new_api_trigger_left,access_test_new_api_trigger_left,model_test_new_api_trigger_left,,1,1,1,1
access_test_new_api_trigger_middle,access_test_new_api_trigger_middle,model_test_new_api_trigger_middle,,1,1,1,1
access_test_new_api_trigger_right,access_test_new_api_trigger_right,model_test_new_api_trigger_right,,1,1,1,1
access_test_new_api_precompute,access_test_new_api_precompute,model_test_new_api_precompute,,1,0,0,0
access_test_new_api_precompute_line,access_test_new_api_precompute_line,model_test_new_api_precompute_line,,1,0,0,0
access_test_new_api_precompute_combo,access_test_new_api_precompute_combo,model_test_new_api_precompute_combo,,1,0,0,0
access_test_new_api_precompute_editable,access_test_new_api_precompute_editable,model_test_new_api_precompute_editable,,1,0,0,0
access_test_new_api_precompute_required,access_test_new_api_precompute_required,model_test_new_api_precompute_required,,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
79 access_test_new_api_trigger_left access_test_new_api_trigger_left model_test_new_api_trigger_left 1 1 1 1
80 access_test_new_api_trigger_middle access_test_new_api_trigger_middle model_test_new_api_trigger_middle 1 1 1 1
81 access_test_new_api_trigger_right access_test_new_api_trigger_right model_test_new_api_trigger_right 1 1 1 1
82 access_test_new_api_precompute access_test_new_api_precompute model_test_new_api_precompute 1 0 0 0
83 access_test_new_api_precompute_line access_test_new_api_precompute_line model_test_new_api_precompute_line 1 0 0 0
84 access_test_new_api_precompute_combo access_test_new_api_precompute_combo model_test_new_api_precompute_combo 1 0 0 0
85 access_test_new_api_precompute_editable access_test_new_api_precompute_editable model_test_new_api_precompute_editable 1 0 0 0
86 access_test_new_api_precompute_required access_test_new_api_precompute_required model_test_new_api_precompute_required 1 0 0 0
@@ -1630,6 +1630,26 @@ class TestFields(TransactionCaseWithUserDemo):
# check that this does not generate an infinite recursion
new_disc._convert_to_write(new_disc._cache)
def test_40_new_convert_to_write(self):
new_disc = self.env['test_new_api.discussion'].new({
'name': "Stuff",
'moderator': self.env.uid,
'participants': [(6, 0, self.env.user.ids)],
})
# Put the user groups in the cache of the new record
new_disc.participants.groups_id
# Check that the groups in the cache are not returned by convert_to_write
# because no real change happened, the values are identical except that
# self.env.user.groups_id._ids = (Id1, Id2, ...) whereas
# new_disc.participants.groups_id._ids = (NewId(origin=Id1), NewId(origin=Id2), ...)
field = new_disc._fields.get("participants")
# make sure that there is no inverse field for discussions on res_users,
# as the test depends on it
self.assertFalse(new_disc.pool.field_inverses[field])
convert = field.convert_to_write(new_disc["participants"], new_disc)
self.assertEqual(convert, [(6, 0, self.env.user.ids)])
def test_40_new_inherited_fields(self):
""" Test the behavior of new records with inherited fields. """
email = self.env['test_new_api.emailmessage'].new({'body': 'XXX'})
@@ -3411,3 +3431,179 @@ class TestWrongRelatedError(common.TransactionCase):
)
with self.assertRaisesRegex(KeyError, errMsg):
self.registry.setup_models(self.env.cr)
class TestPrecomputeModel(common.TransactionCase):
def test_precompute_consistency(self):
Model = self.registry['test_new_api.precompute']
self.assertEqual(Model.lower.compute, Model.upper.compute)
self.assertTrue(Model.lower.precompute)
self.assertTrue(Model.upper.precompute)
# see what happens if not both are precompute
self.addCleanup(self.registry.reset_changes)
self.patch(Model.upper, 'precompute', False)
with self.assertLogs('odoo.modules.registry', level='WARNING'):
self.registry.setup_models(self.cr)
self.registry.field_computed
def test_precompute_dependencies_base(self):
Model = self.registry['test_new_api.precompute']
self.assertTrue(Model.lower.precompute)
self.assertTrue(Model.upper.precompute)
self.assertTrue(Model.lowup.precompute)
# see what happens if precompute depends on non-precompute
self.addCleanup(self.registry.reset_changes)
self.patch(Model.lower, 'precompute', False)
self.patch(Model.upper, 'precompute', False)
with self.assertWarns(UserWarning):
self.registry.setup_models(self.cr)
self.registry.field_triggers
def test_precompute_dependencies_many2one(self):
Model = self.registry['test_new_api.precompute']
Partner = self.registry['res.partner']
# Model.commercial_id depends on partner_id.commercial_partner_id, and
# precomputation is valid when traversing many2one fields
self.assertTrue(Model.commercial_id.precompute)
self.assertFalse(Partner.commercial_partner_id.precompute)
def test_precompute_dependencies_one2many(self):
Model = self.registry['test_new_api.precompute']
Line = self.registry['test_new_api.precompute.line']
self.assertTrue(Model.size.precompute)
self.assertTrue(Line.size.precompute)
# see what happens if precompute depends on non-precompute
self.addCleanup(self.registry.reset_changes)
# ensure that Model.size.precompute is restored after setup_models()
self.patch(Model.size, 'precompute', True)
self.patch(Line.size, 'precompute', False)
with self.assertWarns(UserWarning):
self.registry.setup_models(self.cr)
self.registry.field_triggers
class TestPrecompute(common.TransactionCase):
def test_precompute(self):
model = self.env['test_new_api.precompute']
# warmup
model.create({'name': 'Foo', 'line_ids': [Command.create({'name': 'bar'})]})
# the creation makes one insert query for the main record, and one for the line
with self.assertQueries([
insert(model, 'name', 'lower', 'upper', 'lowup', 'commercial_id', 'size'),
insert(model.line_ids, 'parent_id', 'name', 'size'),
]):
record = model.create({'name': 'Foo', 'line_ids': [Command.create({'name': 'bar'})]})
# check the values in the database
self.cr.execute(f'SELECT * FROM "{model._table}" WHERE id=%s', [record.id])
[row] = self.cr.dictfetchall()
self.assertEqual(row['name'], 'Foo')
self.assertEqual(row['lower'], 'foo')
self.assertEqual(row['upper'], 'FOO')
self.assertEqual(row['lowup'], 'fooFOO')
self.assertEqual(row['size'], 3)
def test_precompute_combo(self):
model = self.env['test_new_api.precompute.combo']
# warmup
model.create({})
QUERIES = [insert(model, 'name', 'reader', 'editer', 'setter')]
# no value at all
with self.assertQueries(QUERIES):
record = model.create({'name': 'A'})
self.assertEqual(record.reader, 'A')
self.assertEqual(record.editer, 'A')
self.assertEqual(record.setter, 'A')
# default value
with self.assertQueries(QUERIES), self.assertLogs('precompute_setter', level='WARNING'):
defaults = dict(default_reader='X', default_editer='Y', default_setter='Z')
record = model.with_context(**defaults).create({'name': 'A'})
self.assertEqual(record.reader, 'A')
self.assertEqual(record.editer, 'Y')
self.assertEqual(record.setter, 'Z')
# explicit value
with self.assertQueries(QUERIES), self.assertLogs('precompute_setter', level='WARNING'):
record = model.create({'name': 'A', 'reader': 'X', 'editer': 'Y', 'setter': 'Z'})
self.assertEqual(record.reader, 'A')
self.assertEqual(record.editer, 'Y')
self.assertEqual(record.setter, 'Z')
def test_precompute_editable(self):
model = self.env['test_new_api.precompute.editable']
# no value for bar, no value for baz
record = model.create({'foo': 'foo'})
self.assertEqual(record.bar, 'COMPUTED')
self.assertEqual(record.baz, 'COMPUTED')
self.assertEqual(record.baz2, 'COMPUTED')
# value for bar, no value for baz
record = model.create({'foo': 'foo', 'bar': 'bar'})
self.assertEqual(record.bar, 'bar')
self.assertEqual(record.baz, 'COMPUTED')
self.assertEqual(record.baz2, 'COMPUTED')
# no value for bar, value for baz: the computation of bar should not
# recompute baz in memory, in case a third field depends on it
record = model.create({'foo': 'foo', 'baz': 'baz'})
self.assertEqual(record.bar, 'COMPUTED')
self.assertEqual(record.baz, 'baz')
self.assertEqual(record.baz2, 'baz')
# value for bar, value for baz
record = model.create({'foo': 'foo', 'bar': 'bar', 'baz': 'baz'})
self.assertEqual(record.bar, 'bar')
self.assertEqual(record.baz, 'baz')
self.assertEqual(record.baz2, 'baz')
def test_precompute_required(self):
model = self.env['test_new_api.precompute.required']
field = type(model).name
self.assertTrue(field.related)
self.assertTrue(field.store)
self.assertTrue(field.required)
partner = self.env['res.partner'].create({'name': 'Foo'})
# this will crash if field is not precomputed
record = model.create({'partner_id': partner.id})
self.assertEqual(record.name, 'Foo')
# check the queries being made
QUERIES = [insert(model, 'partner_id', 'name')]
with self.assertQueries(QUERIES):
record = model.create({'partner_id': partner.id})
def test_precompute_batch(self):
model = self.env['test_new_api.precompute.required']
partners = self.env['res.partner'].create([
{'name': name}
for name in "Foo Bar Baz".split()
])
# warmup
model.create({'partner_id': partners[0].id})
model.flush()
model.invalidate_cache()
# check the number of queries: 1 SELECT + 3 INSERT
with self.assertQueryCount(4):
model.create([{'partner_id': pid} for pid in partners.ids])
@@ -283,3 +283,72 @@ class One2manyCase(TransactionCase):
order.write({
'line_ids': [Command.link(line0.id), Command.link(line1.id)],
})
def test_new_real_interactions(self):
""" Test and specify the interactions between new and real records.
Through m2o and o2m, with real/unreal records on both sides, the behavior
varies greatly. At least, the behavior will be clearly consistent and any
change will have to adapt the current test.
"""
##############
# REAL - NEW #
##############
parent = self.env['test_new_api.model_parent_m2o'].create({'name': 'parentB'})
new_child = self.env['test_new_api.model_child_m2o'].new({'name': 'B', 'parent_id': parent.id})
# wanted behavior: when creating a new with a real parent id, the child
# isn't present in the parent childs until true creation
self.assertFalse(parent.child_ids)
self.assertEqual(new_child.parent_id, parent)
# current (wanted?) behavior: when adding a new record to a real record
# o2m, the record is created, but not linked to the new in cache
# REAL.O2M += NEW RECORD
parent.child_ids += new_child
self.assertTrue(parent.child_ids)
self.assertNotEqual(parent.child_ids, new_child)
#############
# NEW - NEW #
#############
# wanted behavior: linking new records to new records is totally fine
new_parent = self.env['test_new_api.model_parent_m2o'].new({
"name": 'parentC3PO',
"child_ids": [(0, 0, {"name": "C3"})],
})
self.assertEqual(new_parent, new_parent.child_ids.parent_id)
self.assertFalse(new_parent.id)
self.assertTrue(new_parent.child_ids)
self.assertFalse(new_parent.child_ids.ids)
new_child = self.env['test_new_api.model_child_m2o'].new({
'name': 'PO',
})
new_parent.child_ids += new_child
self.assertIn(new_child, new_parent.child_ids)
self.assertEqual(len(new_parent.child_ids), 2)
self.assertListEqual(new_parent.child_ids.mapped('name'), ['C3', 'PO'])
new_child2 = self.env['test_new_api.model_child_m2o'].new({
'name': 'R2D2',
'parent_id': new_parent.id,
})
self.assertIn(new_child2, new_parent.child_ids)
self.assertEqual(len(new_parent.child_ids), 3)
self.assertListEqual(new_parent.child_ids.mapped('name'), ['C3', 'PO', 'R2D2'])
###############################
# NEW TO REAL CONVERSION TEST #
###############################
# A bit out of scope, but was interesting to check everything was
# working fine on the way.
name = type(new_parent).name
child_ids = type(new_parent).child_ids
parent = self.env['test_new_api.model_parent_m2o'].create({
'name': name.convert_to_write(new_parent.name, new_parent),
'child_ids': child_ids.convert_to_write(new_parent.child_ids, new_parent),
})
self.assertEqual(len(parent.child_ids), 3)
self.assertEqual(parent, parent.child_ids.parent_id)
self.assertEqual(parent.child_ids.mapped('name'), ['C3', 'PO', 'R2D2'])
+31 -1
View File
@@ -187,6 +187,18 @@ class Field(MetaField('DummyField', (object,), {})):
.. seealso:: :ref:`Advanced Fields/Compute fields <reference/fields/compute>`
:param bool precompute: whether the field should be computed before record insertion
in database. Should be used to specify manually some fields as precompute=True
when the field can be computed before record insertion.
(e.g. avoid statistics fields based on search/read_group), many2one
linking to the previous record, ... (default: `False`)
.. warning::
Precompute only happens if no explicit value and no default value is
provided to create(). This means that a default value disables the
precomputation, even if the field is specified as precompute=True.
:param bool compute_sudo: whether the field should be recomputed as superuser
to bypass access rights (by default ``True`` for stored fields, ``False``
for non stored fields)
@@ -236,6 +248,7 @@ class Field(MetaField('DummyField', (object,), {})):
recursive = False # whether self depends on itself
compute = None # compute(recs) computes field on recs
compute_sudo = False # whether field should be recomputed as superuser
precompute = False # whether field has to be computed before creation
inverse = None # inverse(recs) inverses field on recs
search = None # search(recs, operator, value) searches on self
related = None # sequence of field names, for related fields
@@ -244,7 +257,7 @@ class Field(MetaField('DummyField', (object,), {})):
string = None # field label
help = None # field tooltip
invisible = False # whether the field is invisible
invisible = False # whether the field is invisible
readonly = False # whether the field is readonly
required = False # whether the field is required
states = None # set readonly and required depending on state
@@ -384,6 +397,13 @@ class Field(MetaField('DummyField', (object,), {})):
attrs['compute_sudo'] = attrs.get('compute_sudo', attrs.get('related_sudo', True))
attrs['copy'] = attrs.get('copy', False)
attrs['readonly'] = attrs.get('readonly', True)
if attrs.get('precompute'):
if not attrs.get('compute') and not attrs.get('related'):
warnings.warn(f"precompute attribute doesn't make any sense on non computed field {self}")
attrs['precompute'] = False
elif not attrs.get('store'):
warnings.warn(f"precompute attribute has no impact on non stored field {self}")
attrs['precompute'] = False
if attrs.get('company_dependent'):
# by default, company-dependent fields are not stored, not computed
# in superuser mode and not copied
@@ -701,6 +721,7 @@ class Field(MetaField('DummyField', (object,), {})):
for dotnames in registry.field_depends[self]:
field_seq = []
model_name = self.model_name
check_precompute = self.precompute
for index, fname in enumerate(dotnames.split('.')):
Model = registry[model_name]
@@ -720,6 +741,12 @@ class Field(MetaField('DummyField', (object,), {})):
self.recursive = True
warnings.warn(f"Field {self} should be declared with recursive=True")
# precomputed fields can depend on non-precomputed ones, as long
# as they are reachable through at least one many2one field
if check_precompute and field.store and field.compute and not field.precompute:
warnings.warn(f"Field {self} cannot be precomputed as it depends on non-precomputed field {field}")
self.precompute = False
field_seq.append(field)
# do not make self trigger itself: for instance, a one2many
@@ -732,6 +759,9 @@ class Field(MetaField('DummyField', (object,), {})):
for inv_field in Model.pool.field_inverses[field]:
yield tuple(field_seq) + (inv_field,)
if check_precompute and field.type == 'many2one':
check_precompute = False
model_name = field.comodel_name
############################################################################
+97 -28
View File
@@ -2840,17 +2840,17 @@ class BaseModel(metaclass=MetaModel):
continue # don't update custom fields
new = field.update_db(self, columns)
if new and field.compute:
fields_to_compute.append(field.name)
fields_to_compute.append(field)
if fields_to_compute:
@self.pool.post_init
def mark_fields_to_compute():
recs = self.with_context(active_test=False).search([], order='id')
if not recs:
return
for field in fields_to_compute:
_logger.info("Storing computed values of %s.%s", recs._name, field)
self.env.add_to_compute(recs._fields[field], recs)
# mark existing records for computation now, so that computed
# required fields are flushed before the NOT NULL constraint is
# added to the database
cr.execute('SELECT id FROM "{}"'.format(self._table))
records = self.browse(row[0] for row in cr.fetchall())
for field in fields_to_compute:
_logger.info("Prepare computation of %s", field)
self.env.add_to_compute(field, records)
if self._auto:
self._add_sql_constraints()
@@ -4000,28 +4000,14 @@ Fields:
self = self.browse()
self.check_access_rights('create')
bad_names = {'id', 'parent_path'}
if self._log_access:
# the superuser can set log_access fields while loading registry
if not(self.env.uid == SUPERUSER_ID and not self.pool.ready):
bad_names.update(LOG_ACCESS_COLUMNS)
vals_list = self._prepare_create_values(vals_list)
# classify fields for each record
data_list = []
inversed_fields = set()
for vals in vals_list:
# add missing defaults
vals = self._add_missing_default_values(vals)
# set magic fields
for name in bad_names:
vals.pop(name, None)
if self._log_access:
vals.setdefault('create_uid', self.env.uid)
vals.setdefault('create_date', self.env.cr.now())
vals.setdefault('write_uid', self.env.uid)
vals.setdefault('write_date', self.env.cr.now())
precomputed = vals.pop('__precomputed__', ())
# distribute fields into sets for various purposes
data = {}
@@ -4047,11 +4033,12 @@ Fields:
stored[key] = val
if field.inherited:
inherited[field.related_field.model_name][key] = val
elif field.inverse:
elif field.inverse and field not in precomputed:
inversed[key] = val
inversed_fields.add(field)
# protect non-readonly computed fields against (re)computation
if field.compute and not field.readonly:
# protect editable computed fields and precomputed fields
# against (re)computation
if field.compute and (not field.readonly or field.precompute):
protected.update(self.pool.field_computed.get(field, [field]))
data_list.append(data)
@@ -4118,6 +4105,88 @@ Fields:
records._check_company()
return records
def _prepare_create_values(self, vals_list):
""" Clean up and complete the given create values, and return a list of
new vals containing:
* default values,
* discarded forbidden values (magic fields),
* precomputed fields.
:param list vals_list: List of create values
:returns: new list of completed create values
:rtype: dict
"""
bad_names = ['id', 'parent_path']
if self._log_access:
# the superuser can set log_access fields while loading registry
if not(self.env.uid == SUPERUSER_ID and not self.pool.ready):
bad_names.extend(LOG_ACCESS_COLUMNS)
# also discard precomputed readonly fields (to force their computation)
bad_names.extend(
fname
for fname, field in self._fields.items()
if field.precompute and field.readonly
)
result_vals_list = []
for vals in vals_list:
# add default values
vals = self._add_missing_default_values(vals)
# add magic fields
for fname in bad_names:
vals.pop(fname, None)
if self._log_access:
vals.setdefault('create_uid', self.env.uid)
vals.setdefault('create_date', self.env.cr.now())
vals.setdefault('write_uid', self.env.uid)
vals.setdefault('write_date', self.env.cr.now())
result_vals_list.append(vals)
# add precomputed fields
self._add_precomputed_values(result_vals_list)
return result_vals_list
def _add_precomputed_values(self, vals_list):
""" Add missing precomputed fields to ``vals_list`` values.
Only applies for precompute=True fields.
:param dict vals_list: list(dict) of create values
"""
precomputable = {
fname: field
for fname, field in self._fields.items()
if field.precompute
}
if not precomputable:
return
# determine which vals must be completed
vals_list_todo = [
vals
for vals in vals_list
if any(fname not in vals for fname in precomputable)
]
if not vals_list_todo:
return
# create new records for the vals that must be completed
records = self.browse().concat(*(self.new(vals) for vals in vals_list_todo))
for record, vals in zip(records, vals_list_todo):
vals['__precomputed__'] = precomputed = set()
for fname, field in precomputable.items():
if fname not in vals:
# computed stored fields with a column
# have to be computed before create
# s.t. required and constraints can be applied on those fields.
vals[fname] = field.convert_to_write(record[fname], self)
precomputed.add(field)
@api.model
def _create(self, data_list):
""" Create records from the stored field values in ``data_list``. """
+3
View File
@@ -321,6 +321,9 @@ class Registry(Mapping):
if len({field.compute_sudo for field in fields}) > 1:
_logger.warning("%s: inconsistent 'compute_sudo' for computed fields: %s",
model_name, ", ".join(field.name for field in fields))
if len({field.precompute for field in fields}) > 1:
_logger.warning("%s: inconsistent 'precompute' for computed fields: %s",
model_name, ", ".join(field.name for field in fields))
return computed
@lazy_property