[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:
committed by
Raphael Collet
co-authored by
Raphael Collet
Yannick Tivisse
parent
86f5ca73d9
commit
d04a5b5c8c
@@ -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
|
||||
|
||||
|
@@ -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
@@ -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
@@ -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``. """
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user